go-doc/backend/tests/integration_test.go

843 lines
26 KiB
Go
Raw Permalink Normal View History

2026-10-07 15:03:50 +02:00
// Package tests contains the end-to-end API suite.
//
// These are true integration tests: every request travels over HTTP to a live
// server, every assertion inspects the real filesystem. There are no mocks, no
// fakes and no in-memory doubles anywhere in this file -- if the flat-file engine
// breaks, these tests break.
package tests
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"time"
"go-doc/router"
"go-doc/storage"
)
// harness wires a real storage engine to a real HTTP server over a temporary
// data directory.
type harness struct {
t *testing.T
dataDir string
engine *storage.Engine
server *httptest.Server
client *http.Client
docsDir string
notePath string
}
func newHarness(t *testing.T) *harness {
t.Helper()
dataDir := t.TempDir()
engine, err := storage.New(dataDir)
if err != nil {
t.Fatalf("integration: provision engine: %v", err)
}
handler, err := router.NewWithConfig(router.Config{
Engine: engine,
Logger: discardLogger(),
})
if err != nil {
t.Fatalf("integration: build router: %v", err)
}
server := httptest.NewServer(handler)
t.Cleanup(server.Close)
return &harness{
t: t,
dataDir: dataDir,
engine: engine,
server: server,
client: server.Client(),
docsDir: filepath.Join(dataDir, "docs"),
notePath: filepath.Join(dataDir, "comments"),
}
}
// request performs an HTTP call and returns the response with its body.
func (h *harness) request(method, path, contentType, body string) (*http.Response, []byte) {
h.t.Helper()
var reader io.Reader
if body != "" {
reader = strings.NewReader(body)
}
req, err := http.NewRequest(method, h.server.URL+path, reader)
if err != nil {
h.t.Fatalf("integration: build %s %s: %v", method, path, err)
}
if contentType != "" {
req.Header.Set("Content-Type", contentType)
}
resp, err := h.client.Do(req)
if err != nil {
h.t.Fatalf("integration: %s %s: %v", method, path, err)
}
defer resp.Body.Close()
payload, err := io.ReadAll(resp.Body)
if err != nil {
h.t.Fatalf("integration: read body of %s %s: %v", method, path, err)
}
return resp, payload
}
func (h *harness) expectStatus(method, path, contentType, body string, want int) []byte {
h.t.Helper()
resp, payload := h.request(method, path, contentType, body)
if resp.StatusCode != want {
h.t.Fatalf("%s %s: expected HTTP %d, got %d (body: %s)", method, path, want, resp.StatusCode, payload)
}
return payload
}
func decodeJSON[T any](t *testing.T, payload []byte) T {
t.Helper()
var out T
if err := json.Unmarshal(payload, &out); err != nil {
t.Fatalf("integration: decode %s: %v", payload, err)
}
return out
}
// requireFile asserts a file exists on the real filesystem and returns its bytes.
func requireFile(t *testing.T, path string) []byte {
t.Helper()
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("integration: expected %s to exist: %v", path, err)
}
return data
}
func requireNoFile(t *testing.T, path string) {
t.Helper()
if _, err := os.Stat(path); err == nil {
t.Fatalf("integration: expected %s to be absent", path)
} else if !os.IsNotExist(err) {
t.Fatalf("integration: stat %s: %v", path, err)
}
}
// ---------------------------------------------------------------------------
// TestIntegrationDocumentLifecycle
// ---------------------------------------------------------------------------
func TestIntegrationDocumentLifecycle(t *testing.T) {
t.Parallel()
h := newHarness(t)
t.Run("health reports the live data root", func(t *testing.T) {
payload := h.expectStatus("GET", "/api/health", "", "", http.StatusOK)
health := decodeJSON[struct {
Status string `json:"status"`
DataRoot string `json:"data_root"`
}](t, payload)
if health.Status != "ok" {
t.Fatalf("expected status ok, got %q", health.Status)
}
if health.DataRoot != h.dataDir {
t.Fatalf("expected data root %q, got %q", h.dataDir, health.DataRoot)
}
})
t.Run("POST creates a slugified Markdown file on disk", func(t *testing.T) {
payload := h.expectStatus(
"POST", "/api/docs/Core%20Service%20API", "text/markdown",
"# Core Service API\n\nBilling and invoicing endpoints.\n",
http.StatusCreated,
)
saved := decodeJSON[struct {
Slug string `json:"slug"`
Title string `json:"title"`
Created bool `json:"created"`
}](t, payload)
if saved.Slug != "core-service-api" {
t.Fatalf("expected slug core-service-api, got %q", saved.Slug)
}
if !saved.Created {
t.Fatal("expected created=true on first write")
}
if saved.Title != "Core Service API" {
t.Fatalf("expected the H1 to become the title, got %q", saved.Title)
}
onDisk := requireFile(t, filepath.Join(h.docsDir, "core-service-api.md"))
if !bytes.Contains(onDisk, []byte("Billing and invoicing endpoints.")) {
t.Fatalf("file content mismatch: %s", onDisk)
}
})
t.Run("GET returns the raw Markdown with the right content type", func(t *testing.T) {
resp, payload := h.request("GET", "/api/docs/core-service-api", "", "")
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
if ct := resp.Header.Get("Content-Type"); !strings.HasPrefix(ct, "text/markdown") {
t.Fatalf("expected text/markdown, got %q", ct)
}
if !strings.HasPrefix(string(payload), "# Core Service API") {
t.Fatalf("unexpected markdown body: %s", payload)
}
})
t.Run("GET ?format=json returns the document envelope", func(t *testing.T) {
payload := h.expectStatus("GET", "/api/docs/core-service-api?format=json", "", "", http.StatusOK)
doc := decodeJSON[struct {
Slug string `json:"slug"`
Title string `json:"title"`
Content string `json:"content"`
Size int64 `json:"size"`
}](t, payload)
if doc.Slug != "core-service-api" || doc.Title != "Core Service API" {
t.Fatalf("unexpected envelope: %+v", doc)
}
if doc.Size <= 0 {
t.Fatalf("expected a positive size, got %d", doc.Size)
}
})
t.Run("POST with a JSON body updates the file in place", func(t *testing.T) {
h.expectStatus(
"POST", "/api/docs/core-service-api", "application/json",
`{"title":"Core Service API v2","content":"# Core Service API v2\n\nRewritten.\n"}`,
http.StatusOK,
)
onDisk := string(requireFile(t, filepath.Join(h.docsDir, "core-service-api.md")))
if strings.Contains(onDisk, "Billing and invoicing") {
t.Fatalf("update did not replace the content wholesale: %s", onDisk)
}
if !strings.Contains(onDisk, "# Core Service API v2") {
t.Fatalf("update missing from disk: %s", onDisk)
}
})
t.Run("GET /api/docs indexes the document", func(t *testing.T) {
payload := h.expectStatus("GET", "/api/docs", "", "", http.StatusOK)
index := decodeJSON[struct {
Count int `json:"count"`
Docs []storage.DocMeta `json:"docs"`
}](t, payload)
if index.Count != 1 || index.Docs[0].Slug != "core-service-api" {
t.Fatalf("unexpected index: %+v", index)
}
})
t.Run("search filters the index", func(t *testing.T) {
h.expectStatus("POST", "/api/docs/Payments%20Gateway", "text/markdown", "# Payments Gateway\n", http.StatusCreated)
payload := h.expectStatus("GET", "/api/docs?q=payment", "", "", http.StatusOK)
index := decodeJSON[struct {
Count int `json:"count"`
}](t, payload)
if index.Count != 1 {
t.Fatalf("expected exactly one search hit, got %d", index.Count)
}
payload = h.expectStatus("GET", "/api/docs?q=nothing-matches", "", "", http.StatusOK)
index = decodeJSON[struct {
Count int `json:"count"`
}](t, payload)
if index.Count != 0 {
t.Fatalf("expected no hits, got %d", index.Count)
}
})
t.Run("DELETE removes the file and cascades the feedback", func(t *testing.T) {
h.expectStatus("POST", "/api/docs/core-service-api/comments", "application/json",
`{"author":"ana","role":"developer","content":"delete me"}`, http.StatusCreated)
requireFile(t, filepath.Join(h.notePath, "core-service-api-comments.json"))
h.expectStatus("DELETE", "/api/docs/core-service-api", "", "", http.StatusNoContent)
requireNoFile(t, filepath.Join(h.docsDir, "core-service-api.md"))
requireNoFile(t, filepath.Join(h.notePath, "core-service-api-comments.json"))
h.expectStatus("GET", "/api/docs/core-service-api", "", "", http.StatusNotFound)
h.expectStatus("DELETE", "/api/docs/core-service-api", "", "", http.StatusNotFound)
})
}
// ---------------------------------------------------------------------------
// TestIntegrationCommentTracking
// ---------------------------------------------------------------------------
func TestIntegrationCommentTracking(t *testing.T) {
t.Parallel()
h := newHarness(t)
h.expectStatus("POST", "/api/docs/Deploy%20Pipeline", "text/markdown", "# Deploy Pipeline\n", http.StatusCreated)
type created struct {
Comment storage.Comment `json:"comment"`
Badge string `json:"badge"`
}
cases := []struct {
role string
author string
content string
badge string
}{
{"user", "sam", "The wizard confused me", "🟢"},
{"Operator", "robin", "Rollback takes 20 minutes", "🔵"},
{"DEVELOPER", "kit", "Add a --dry-run flag", "🔴"},
}
for _, tc := range cases {
t.Run("role "+tc.role+" receives badge "+tc.badge, func(t *testing.T) {
body := fmt.Sprintf(`{"author":%q,"role":%q,"content":%q}`, tc.author, tc.role, tc.content)
payload := h.expectStatus("POST", "/api/docs/deploy-pipeline/comments", "application/json", body, http.StatusCreated)
result := decodeJSON[created](t, payload)
if result.Badge != tc.badge {
t.Fatalf("expected badge %q, got %q", tc.badge, result.Badge)
}
if result.Comment.Badge != tc.badge {
t.Fatalf("expected the stored badge %q, got %q", tc.badge, result.Comment.Badge)
}
if result.Comment.ID == "" {
t.Fatal("expected a generated comment ID")
}
if result.Comment.Timestamp.IsZero() {
t.Fatal("expected a timestamp")
}
if string(result.Comment.Role) != strings.ToLower(tc.role) {
t.Fatalf("expected the canonical role, got %q", result.Comment.Role)
}
})
}
t.Run("the feedback file is a JSON array in insertion order", func(t *testing.T) {
raw := requireFile(t, filepath.Join(h.notePath, "deploy-pipeline-comments.json"))
if !strings.HasPrefix(strings.TrimSpace(string(raw)), "[") {
t.Fatalf("expected a JSON array, got: %s", raw)
}
var stored []storage.Comment
if err := json.Unmarshal(raw, &stored); err != nil {
t.Fatalf("feedback file is not valid JSON: %v", err)
}
if len(stored) != 3 {
t.Fatalf("expected 3 comments, got %d", len(stored))
}
for i, want := range []struct{ role, badge string }{
{"user", "🟢"},
{"operator", "🔵"},
{"developer", "🔴"},
} {
if string(stored[i].Role) != want.role || stored[i].Badge != want.badge {
t.Fatalf("comment %d: got %s/%s, want %s/%s", i, stored[i].Role, stored[i].Badge, want.role, want.badge)
}
}
})
t.Run("GET lists the comments with their badges", func(t *testing.T) {
payload := h.expectStatus("GET", "/api/docs/deploy-pipeline/comments", "", "", http.StatusOK)
listed := decodeJSON[struct {
Comments []storage.Comment `json:"comments"`
Count int `json:"count"`
}](t, payload)
if listed.Count != 3 || len(listed.Comments) != 3 {
t.Fatalf("expected 3 comments, got %d", listed.Count)
}
if listed.Comments[2].Badge != "🔴" {
t.Fatalf("expected the developer badge, got %q", listed.Comments[2].Badge)
}
})
t.Run("DELETE removes a single comment by ID", func(t *testing.T) {
payload := h.expectStatus("GET", "/api/docs/deploy-pipeline/comments", "", "", http.StatusOK)
listed := decodeJSON[struct {
Comments []storage.Comment `json:"comments"`
}](t, payload)
target := listed.Comments[0].ID
h.expectStatus("DELETE", "/api/docs/deploy-pipeline/comments/"+target, "", "", http.StatusNoContent)
h.expectStatus("DELETE", "/api/docs/deploy-pipeline/comments/"+target, "", "", http.StatusNotFound)
remaining := decodeJSON[struct {
Count int `json:"count"`
}](t, h.expectStatus("GET", "/api/docs/deploy-pipeline/comments", "", "", http.StatusOK))
if remaining.Count != 2 {
t.Fatalf("expected 2 remaining comments, got %d", remaining.Count)
}
})
t.Run("stats aggregate by role", func(t *testing.T) {
payload := h.expectStatus("GET", "/api/docs/deploy-pipeline/stats", "", "", http.StatusOK)
stats := decodeJSON[storage.CommentStats](t, payload)
if stats.Total != 2 {
t.Fatalf("expected total 2, got %d", stats.Total)
}
if stats.ByRole["operator"] != 1 || stats.ByRole["developer"] != 1 {
t.Fatalf("unexpected role tally: %+v", stats.ByRole)
}
})
t.Run("the role catalogue is exposed for the UI", func(t *testing.T) {
payload := h.expectStatus("GET", "/api/roles", "", "", http.StatusOK)
catalogue := decodeJSON[struct {
Roles []struct {
Role string `json:"role"`
Badge string `json:"badge"`
Label string `json:"label"`
Hex string `json:"hex"`
} `json:"roles"`
}](t, payload)
want := map[string][3]string{
"user": {"🟢", "User", "#22c55e"},
"operator": {"🔵", "Operator", "#3b82f6"},
"developer": {"🔴", "Developer", "#ef4444"},
}
if len(catalogue.Roles) != 3 {
t.Fatalf("expected 3 roles, got %d", len(catalogue.Roles))
}
for _, role := range catalogue.Roles {
expected, ok := want[role.Role]
if !ok {
t.Fatalf("unexpected role %q", role.Role)
}
if role.Badge != expected[0] || role.Label != expected[1] || role.Hex != expected[2] {
t.Fatalf("role %s mismatch: %+v", role.Role, role)
}
}
})
}
// ---------------------------------------------------------------------------
// TestIntegrationValidationAndErrors
// ---------------------------------------------------------------------------
func TestIntegrationValidationAndErrors(t *testing.T) {
t.Parallel()
h := newHarness(t)
t.Run("unknown document returns a structured 404", func(t *testing.T) {
payload := h.expectStatus("GET", "/api/docs/does-not-exist", "", "", http.StatusNotFound)
body := decodeJSON[struct {
Error struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
Status int `json:"status"`
}](t, payload)
if body.Error.Code != "not_found" || body.Status != http.StatusNotFound {
t.Fatalf("unexpected error body: %s", payload)
}
})
t.Run("invalid role returns 400 invalid_role", func(t *testing.T) {
h.expectStatus("POST", "/api/docs/guarded", "text/markdown", "# Guarded\n", http.StatusCreated)
payload := h.expectStatus("POST", "/api/docs/guarded/comments", "application/json",
`{"author":"mallory","role":"root","content":"escalate"}`, http.StatusBadRequest)
if !strings.Contains(string(payload), "invalid_role") {
t.Fatalf("expected invalid_role, got %s", payload)
}
requireNoFile(t, filepath.Join(h.notePath, "guarded-comments.json"))
})
t.Run("empty comment content returns 400 invalid_comment", func(t *testing.T) {
payload := h.expectStatus("POST", "/api/docs/guarded/comments", "application/json",
`{"author":"ana","role":"user","content":" "}`, http.StatusBadRequest)
if !strings.Contains(string(payload), "invalid_comment") {
t.Fatalf("expected invalid_comment, got %s", payload)
}
})
t.Run("malformed JSON returns 400 invalid_body", func(t *testing.T) {
payload := h.expectStatus("POST", "/api/docs/guarded/comments", "application/json", `{"author":`, http.StatusBadRequest)
if !strings.Contains(string(payload), "invalid_body") {
t.Fatalf("expected invalid_body, got %s", payload)
}
})
t.Run("traversal slugs are refused", func(t *testing.T) {
for _, path := range []string{
"/api/docs/..%2F..%2Fetc%2Fpasswd",
"/api/docs/%2Fetc%2Fpasswd",
} {
h.expectStatus("POST", path, "text/markdown", "nope", http.StatusBadRequest)
}
})
t.Run("oversized documents are refused", func(t *testing.T) {
huge := strings.Repeat("a", int(router.DefaultMaxDocBytes)+1024)
h.expectStatus("POST", "/api/docs/huge", "text/markdown", huge, http.StatusBadRequest)
requireNoFile(t, filepath.Join(h.docsDir, "huge.md"))
})
t.Run("unknown API routes return JSON 404, not an HTML page", func(t *testing.T) {
payload := h.expectStatus("GET", "/api/nope", "", "", http.StatusNotFound)
if !strings.Contains(string(payload), "not_found") {
t.Fatalf("expected a JSON error, got %s", payload)
}
})
}
// ---------------------------------------------------------------------------
// TestIntegrationCORS
// ---------------------------------------------------------------------------
func TestIntegrationCORS(t *testing.T) {
t.Parallel()
h := newHarness(t)
t.Run("preflight is answered for the Vite dev server", func(t *testing.T) {
resp, _ := h.request("OPTIONS", "/api/docs/core-service-api", "", "")
if resp.StatusCode != http.StatusNoContent {
t.Fatalf("expected 204, got %d", resp.StatusCode)
}
if origin := resp.Header.Get("Access-Control-Allow-Origin"); origin != "*" {
t.Fatalf("expected a wildcard origin, got %q", origin)
}
methods := resp.Header.Get("Access-Control-Allow-Methods")
for _, verb := range []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"} {
if !strings.Contains(methods, verb) {
t.Fatalf("expected %s in %q", verb, methods)
}
}
if headers := resp.Header.Get("Access-Control-Allow-Headers"); !strings.Contains(headers, "Content-Type") {
t.Fatalf("expected Content-Type to be allowed, got %q", headers)
}
})
t.Run("cross-origin reads carry the CORS header", func(t *testing.T) {
req, err := http.NewRequest("GET", h.server.URL+"/api/docs", nil)
if err != nil {
t.Fatal(err)
}
req.Header.Set("Origin", "http://127.0.0.1:5173")
resp, err := h.client.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if origin := resp.Header.Get("Access-Control-Allow-Origin"); origin != "*" {
t.Fatalf("expected a CORS header, got %q", origin)
}
})
t.Run("an allow-list echoes only permitted origins", func(t *testing.T) {
dataDir := t.TempDir()
engine, err := storage.New(dataDir)
if err != nil {
t.Fatal(err)
}
handler, err := router.NewWithConfig(router.Config{
Engine: engine,
AllowedOrigins: []string{"http://127.0.0.1:5173"},
Logger: discardLogger(),
})
if err != nil {
t.Fatal(err)
}
server := httptest.NewServer(handler)
defer server.Close()
for origin, wantEchoed := range map[string]bool{
"http://127.0.0.1:5173": true,
"http://evil.example.com": false,
} {
req, err := http.NewRequest("GET", server.URL+"/api/health", nil)
if err != nil {
t.Fatal(err)
}
req.Header.Set("Origin", origin)
resp, err := server.Client().Do(req)
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
got := resp.Header.Get("Access-Control-Allow-Origin")
if wantEchoed && got != origin {
t.Fatalf("expected %q to be echoed, got %q", origin, got)
}
if !wantEchoed && got == origin {
t.Fatalf("origin %q should not have been echoed", origin)
}
}
})
}
// ---------------------------------------------------------------------------
// TestIntegrationConcurrentLoad
// ---------------------------------------------------------------------------
func TestIntegrationConcurrentLoad(t *testing.T) {
t.Parallel()
h := newHarness(t)
const (
docs = 40
perDoc = 30
commentsTotal = docs * perDoc
)
t.Run("concurrent writers all land on disk", func(t *testing.T) {
var wg sync.WaitGroup
errs := make(chan error, docs)
for d := 0; d < docs; d++ {
wg.Add(1)
go func(index int) {
defer wg.Done()
title := fmt.Sprintf("Load Service %02d", index)
body := fmt.Sprintf("# %s\n\n## For Users\n\nUsage.\n\n## For Operators\n\nRunbook.\n\n## For Developers\n\nAPI.\n", title)
resp, payload := h.request("POST", "/api/docs/"+storage.Slugify(title), "text/markdown", body)
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
errs <- fmt.Errorf("save %s returned %d: %s", title, resp.StatusCode, payload)
return
}
for n := 0; n < perDoc; n++ {
role := []string{"user", "operator", "developer"}[n%3]
comment := fmt.Sprintf(
`{"author":"load-%02d","role":%q,"content":"comment %d"}`, index, role, n)
resp, payload := h.request("POST", "/api/docs/"+storage.Slugify(title)+"/comments", "application/json", comment)
if resp.StatusCode != http.StatusCreated {
errs <- fmt.Errorf("comment on %s returned %d: %s", title, resp.StatusCode, payload)
return
}
}
}(d)
}
wg.Wait()
close(errs)
for err := range errs {
t.Fatal(err)
}
})
t.Run("the filesystem holds every document", func(t *testing.T) {
entries, err := os.ReadDir(h.docsDir)
if err != nil {
t.Fatal(err)
}
if len(entries) != docs {
t.Fatalf("expected %d Markdown files, got %d", docs, len(entries))
}
})
t.Run("the filesystem holds every comment", func(t *testing.T) {
entries, err := os.ReadDir(h.notePath)
if err != nil {
t.Fatal(err)
}
if len(entries) != docs {
t.Fatalf("expected %d feedback files, got %d", docs, len(entries))
}
total := 0
for _, entry := range entries {
raw, err := os.ReadFile(filepath.Join(h.notePath, entry.Name()))
if err != nil {
t.Fatal(err)
}
var stored []storage.Comment
if err := json.Unmarshal(raw, &stored); err != nil {
t.Fatalf("%s is not a valid JSON array: %v", entry.Name(), err)
}
total += len(stored)
}
if total != commentsTotal {
t.Fatalf("expected %d comments across all files, got %d", commentsTotal, total)
}
})
t.Run("the API agrees with the filesystem", func(t *testing.T) {
payload := h.expectStatus("GET", "/api/stats", "", "", http.StatusOK)
stats := decodeJSON[struct {
Docs int `json:"docs"`
Comments int `json:"comments"`
}](t, payload)
if stats.Docs != docs {
t.Fatalf("expected %d docs, got %d", docs, stats.Docs)
}
if stats.Comments != commentsTotal {
t.Fatalf("expected %d comments, got %d", commentsTotal, stats.Comments)
}
})
t.Run("no temp files leaked and every comment is badged", func(t *testing.T) {
entries, err := os.ReadDir(h.docsDir)
if err != nil {
t.Fatal(err)
}
for _, entry := range entries {
if strings.HasPrefix(entry.Name(), ".godoc-tmp-") {
t.Fatalf("leaked atomic-write temp file: %s", entry.Name())
}
}
for d := 0; d < docs; d++ {
slug := storage.Slugify(fmt.Sprintf("Load Service %02d", d))
raw := requireFile(t, filepath.Join(h.notePath, slug+"-comments.json"))
var stored []storage.Comment
if err := json.Unmarshal(raw, &stored); err != nil {
t.Fatal(err)
}
if len(stored) != perDoc {
t.Fatalf("%s: expected %d comments, got %d", slug, perDoc, len(stored))
}
for _, comment := range stored {
if comment.Badge != comment.Role.Badge() {
t.Fatalf("%s: comment %s has badge %q for role %s", slug, comment.ID, comment.Badge, comment.Role)
}
}
}
})
}
// ---------------------------------------------------------------------------
// TestIntegrationPersistenceAcrossRestart
// ---------------------------------------------------------------------------
func TestIntegrationPersistenceAcrossRestart(t *testing.T) {
t.Parallel()
dataDir := t.TempDir()
// First "process": write through the API.
engine, err := storage.New(dataDir)
if err != nil {
t.Fatal(err)
}
handler, err := router.NewWithConfig(router.Config{Engine: engine, Logger: discardLogger()})
if err != nil {
t.Fatal(err)
}
first := httptest.NewServer(handler)
post := func(method, path, contentType, body string) []byte {
t.Helper()
req, err := http.NewRequest(method, first.URL+path, strings.NewReader(body))
if err != nil {
t.Fatal(err)
}
req.Header.Set("Content-Type", contentType)
resp, err := first.Client().Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
payload, _ := io.ReadAll(resp.Body)
if resp.StatusCode >= 300 {
t.Fatalf("%s %s returned %d: %s", method, path, resp.StatusCode, payload)
}
return payload
}
post("POST", "/api/docs/Persistent%20Store", "text/markdown", "# Persistent Store\n\nSurvives restarts.\n")
post("POST", "/api/docs/persistent-store/comments", "application/json",
`{"author":"ana","role":"developer","content":"still here after a restart"}`)
first.Close()
// Second "process": a brand new engine over the same directory.
restarted, err := storage.New(dataDir)
if err != nil {
t.Fatal(err)
}
handler, err = router.NewWithConfig(router.Config{Engine: restarted, Logger: discardLogger()})
if err != nil {
t.Fatal(err)
}
second := httptest.NewServer(handler)
defer second.Close()
t.Run("the document survives", func(t *testing.T) {
resp, err := second.Client().Get(second.URL + "/api/docs/persistent-store?format=json")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
doc := decodeJSON[struct {
Title string `json:"title"`
Content string `json:"content"`
}](t, mustReadAll(resp))
if doc.Title != "Persistent Store" {
t.Fatalf("unexpected title %q", doc.Title)
}
if !strings.Contains(doc.Content, "Survives restarts.") {
t.Fatalf("unexpected content %q", doc.Content)
}
})
t.Run("the feedback survives with its badge", func(t *testing.T) {
resp, err := second.Client().Get(second.URL + "/api/docs/persistent-store/comments")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
listed := decodeJSON[struct {
Count int `json:"count"`
Comments []storage.Comment `json:"comments"`
}](t, mustReadAll(resp))
if listed.Count != 1 {
t.Fatalf("expected 1 comment, got %d", listed.Count)
}
if listed.Comments[0].Badge != "🔴" {
t.Fatalf("expected the developer badge, got %q", listed.Comments[0].Badge)
}
})
}
func mustReadAll(resp *http.Response) []byte {
data, err := io.ReadAll(resp.Body)
if err != nil {
panic(err)
}
return data
}
func TestIntegrationTimingBudget(t *testing.T) {
t.Parallel()
h := newHarness(t)
start := time.Now()
h.expectStatus("POST", "/api/docs/timing", "text/markdown", strings.Repeat("Line of content.\n\n", 500), http.StatusCreated)
for i := 0; i < 100; i++ {
h.expectStatus("POST", "/api/docs/timing/comments", "application/json",
fmt.Sprintf(`{"author":"bench","role":"user","content":"note %d"}`, i), http.StatusCreated)
}
h.expectStatus("GET", "/api/docs/timing", "", "", http.StatusOK)
if elapsed := time.Since(start); elapsed > 30*time.Second {
t.Fatalf("write path took %s, which is far too slow for a flat-file engine", elapsed)
}
}