package httpapi import ( "bytes" "context" "encoding/json" "errors" "log/slog" "net/http" "net/http/httptest" "strings" "testing" "time" "textmachine/platform/internal/auth" "textmachine/platform/internal/pgstore" "textmachine/platform/internal/pricing" "textmachine/platform/internal/runner" "textmachine/platform/internal/runs" ) // These tests assert the WIRE, field for field, because the wire is the contract (openapi 0.2.0) and // a projection is exactly the kind of code that drifts from it silently: nothing in Go fails when a // json tag is misspelled, and the client that breaks is in another repository. type fakeLibrary struct { lib pgstore.Library book pgstore.Book run *pgstore.Run usage pgstore.Usage err error } func (f *fakeLibrary) ListBooks(context.Context, string, int, string) (pgstore.Library, error) { return f.lib, f.err } func (f *fakeLibrary) GetBook(context.Context, string, string) (pgstore.Book, *pgstore.Run, error) { return f.book, f.run, f.err } func (f *fakeLibrary) ReadUsage(context.Context, string) (pgstore.Usage, error) { return f.usage, f.err } type fakeRuns struct { bounds pricing.Bounds run pgstore.Run err error got runs.StartRequest // stopped and resumed record which run each control handle was called for, so a test can assert // that the path parameter reaches the service rather than only that the status code is right. stopped string resumed string } func (f *fakeRuns) Bounds(context.Context, string, string) (pricing.Bounds, error) { return f.bounds, f.err } func (f *fakeRuns) Start(_ context.Context, in runs.StartRequest) (pgstore.Run, error) { f.got = in return f.run, f.err } func (f *fakeRuns) Stop(_ context.Context, _, runID string) (pgstore.Run, error) { f.stopped = runID return f.run, f.err } func (f *fakeRuns) Resume(_ context.Context, _, runID string) (pgstore.Run, error) { f.resumed = runID return f.run, f.err } func v0Server(t *testing.T, lib Library, rn Runs) http.Handler { t.Helper() return v0ServerWith(t, Deps{Library: lib, Runs: rn}) } // v0ServerWith builds the real handler chain with whatever this test wants mounted on it. Every // wire test goes through New — not through a handler in isolation — because the guard, the body cap // and the security headers are part of the surface being asserted. func v0ServerWith(t *testing.T, d Deps) http.Handler { t.Helper() d.Log = slog.New(slog.NewJSONHandler(&bytes.Buffer{}, nil)) d.Auth = &auth.Authenticator{ Sessions: liveSessions{}, IdleTTL: time.Hour, Deny: ProblemHandler(http.StatusUnauthorized, "Session missing or invalid"), } h, err := New(d) if err != nil { t.Fatal(err) } return h } func call(t *testing.T, h http.Handler, method, path, body string) *httptest.ResponseRecorder { t.Helper() var r *http.Request if body == "" { r = httptest.NewRequest(method, path, nil) } else { r = httptest.NewRequest(method, path, strings.NewReader(body)) r.Header.Set("Content-Type", "application/json") } r.Header.Set("Authorization", "Bearer token") w := httptest.NewRecorder() h.ServeHTTP(w, r) return w } func decode(t *testing.T, w *httptest.ResponseRecorder) map[string]any { t.Helper() var m map[string]any if err := json.Unmarshal(w.Body.Bytes(), &m); err != nil { t.Fatalf("body %q: %v", w.Body.String(), err) } return m } func TestTheLibraryResponseCarriesEveryRequiredField(t *testing.T) { eta := 42 lib := &fakeLibrary{lib: pgstore.Library{ Revision: 1841, Books: []pgstore.Book{{ ID: "bk_7c1", Title: "蛊真人", SourceLang: "zh", TargetLang: "ru", Genre: "xianxia", Status: "translating", ChapterCount: 500, CharacterCount: 23_000_000, NoteCount: 3, AddedAt: time.Unix(0, 0).UTC(), Progress: pgstore.Progress{DraftDone: 7, DraftTotal: 20, EditDone: 1, EditTotal: 20, ETASeconds: &eta}, }}, }} w := call(t, v0Server(t, lib, &fakeRuns{}), "GET", "/v0/books", "") if w.Code != http.StatusOK { t.Fatalf("status %d: %s", w.Code, w.Body) } got := decode(t, w) for _, k := range []string{"revision", "next_cursor", "books"} { if _, ok := got[k]; !ok { t.Errorf("Library is missing the required field %q", k) } } // next_cursor is present on EVERY list response, and null on the last page: introducing it later // would silently cut the tail off a client that does not read the field. if got["next_cursor"] != nil { t.Errorf("next_cursor on the last page is %v, want null", got["next_cursor"]) } books, _ := got["books"].([]any) if len(books) != 1 { t.Fatalf("books: %v", got["books"]) } book, _ := books[0].(map[string]any) for _, k := range []string{"id", "title", "source_lang", "target_lang", "status", "chapter_count", "added_at", "progress", "note_count"} { if _, ok := book[k]; !ok { t.Errorf("Book is missing the required field %q", k) } } progress, _ := book["progress"].(map[string]any) draft, _ := progress["draft"].(map[string]any) if draft["done"] != float64(7) || draft["total"] != float64(20) { t.Errorf("progress.draft = %v", progress["draft"]) } if progress["eta_seconds"] != float64(42) { t.Errorf("progress.eta_seconds = %v", progress["eta_seconds"]) } // Money never crosses this boundary in any form (D39.84): not a sum, not a rate, not a ceiling // in dollars. if bytes.Contains(w.Body.Bytes(), []byte("usd")) || bytes.Contains(w.Body.Bytes(), []byte("micro")) { t.Errorf("a money field reached the wire: %s", w.Body) } } // Absent, not zero: the screen must render without an estimate rather than show "0 s left". func TestAnAbsentEtaTravelsAsNullAndNotAsZero(t *testing.T) { lib := &fakeLibrary{lib: pgstore.Library{Books: []pgstore.Book{{ID: "bk_1"}}}} w := call(t, v0Server(t, lib, &fakeRuns{}), "GET", "/v0/books", "") got := decode(t, w) books, _ := got["books"].([]any) book, _ := books[0].(map[string]any) progress, _ := book["progress"].(map[string]any) if v, ok := progress["eta_seconds"]; !ok || v != nil { t.Errorf("eta_seconds = %v (present: %v), want an explicit null", v, ok) } } func TestTheBookCardCarriesItsRunOrAnExplicitNull(t *testing.T) { lib := &fakeLibrary{book: pgstore.Book{ID: "bk_1", Revision: 12}} w := call(t, v0Server(t, lib, &fakeRuns{}), "GET", "/v0/books/bk_1", "") got := decode(t, w) if v, ok := got["run"]; !ok || v != nil { t.Errorf("run = %v (present: %v), want an explicit null for a book that never ran", v, ok) } if got["revision"] != float64(12) { t.Errorf("a card without a run carries revision %v, want the book's own", got["revision"]) } finished := time.Unix(0, 0).UTC() lib.book.Revision = 1900 lib.run = &pgstore.Run{ID: "run_1", Revision: 12, Status: "paused", VerifyBank: true, CeilingChapters: 100, PausedReason: "credit_exhausted", StartedAt: finished, FinishedAt: &finished} got = decode(t, call(t, v0Server(t, lib, &fakeRuns{}), "GET", "/v0/books/bk_1", "")) run, _ := got["run"].(map[string]any) for _, k := range []string{"id", "revision", "status", "verify_bank", "ceiling_chapters", "paused_reason", "started_at"} { if _, ok := run[k]; !ok { t.Errorf("Run is missing the required field %q", k) } } if run["paused_reason"] != "credit_exhausted" || run["status"] != "paused" { t.Errorf("a paused run: %v", run) } // The BOOK's counter, not the run's: one counter per book (contract §Revision). if got["revision"] != float64(1900) { t.Errorf("a card with a run carries revision %v", got["revision"]) } } // paused_reason is required and null in every state but `paused`: a client that had to tell "absent" // from "null" would carry a branch the contract does not describe. func TestPausedReasonIsNullRatherThanAbsentOnALiveRun(t *testing.T) { lib := &fakeLibrary{book: pgstore.Book{ID: "bk_1"}, run: &pgstore.Run{ID: "run_1", Status: "translating"}} got := decode(t, call(t, v0Server(t, lib, &fakeRuns{}), "GET", "/v0/books/bk_1", "")) run, _ := got["run"].(map[string]any) v, ok := run["paused_reason"] if !ok || v != nil { t.Errorf("paused_reason = %v (present: %v)", v, ok) } if v, ok := run["finished_at"]; !ok || v != nil { t.Errorf("finished_at = %v (present: %v)", v, ok) } } func TestRunOptionsCarriesTheThreeBoundsAndNoArithmeticForTheClient(t *testing.T) { rn := &fakeRuns{bounds: pricing.Bounds{Min: 1, Max: 166, Default: 166}} got := decode(t, call(t, v0Server(t, &fakeLibrary{}, rn), "GET", "/v0/books/bk_1/run-options", "")) ceiling, _ := got["ceiling"].(map[string]any) if ceiling["min_chapters"] != float64(1) || ceiling["max_chapters"] != float64(166) || ceiling["default_chapters"] != float64(166) { t.Fatalf("ceiling bounds: %v", ceiling) } // The chapters-to-money conversion lives on the platform and is not exposed in ANY form. if len(ceiling) != 3 { t.Errorf("CeilingBounds carries %d fields: %v", len(ceiling), ceiling) } } // max_chapters of 0 is legal and means "no run can start at all": the client shows the exhausted // state instead of a scale. func TestAnExhaustedAccountGetsAZeroMaximum(t *testing.T) { rn := &fakeRuns{bounds: pricing.Bounds{Min: 1, Max: 0, Default: 0}} got := decode(t, call(t, v0Server(t, &fakeLibrary{}, rn), "GET", "/v0/books/bk_1/run-options", "")) ceiling, _ := got["ceiling"].(map[string]any) if ceiling["max_chapters"] != float64(0) || ceiling["default_chapters"] != float64(0) { t.Errorf("an exhausted account: %v", ceiling) } } // Both fields are REQUIRED, and the reason ceiling_chapters is required is money: a run started // without a declared ceiling spends past the limit the user is entitled to set beforehand. func TestStartingARunRequiresBothFieldsAndAnsweredAccepted(t *testing.T) { rn := &fakeRuns{run: pgstore.Run{ID: "run_1", Status: "translating", CeilingChapters: 100, VerifyBank: true, StartedAt: time.Unix(0, 0).UTC()}} h := v0Server(t, &fakeLibrary{}, rn) for _, body := range []string{`{}`, `{"verify_bank":true}`, `{"ceiling_chapters":10}`, `not json`} { if w := call(t, h, "POST", "/v0/books/bk_1/runs", body); w.Code != http.StatusBadRequest { t.Errorf("body %s answered %d, want 400", body, w.Code) } } w := call(t, h, "POST", "/v0/books/bk_1/runs", `{"verify_bank":true,"ceiling_chapters":100}`) if w.Code != http.StatusAccepted { t.Fatalf("status %d: %s", w.Code, w.Body) } if rn.got.CeilingChapters != 100 || !rn.got.VerifyBank || rn.got.BookID != "bk_1" || rn.got.UserID != "u1" { t.Errorf("the request reached the service as %+v", rn.got) } got := decode(t, w) if got["id"] != "run_1" || got["ceiling_chapters"] != float64(100) { t.Errorf("the accepted run: %v", got) } } // The contract's status codes, and the one deliberate departure from them, named out loud. func TestDomainFailuresBecomeTheContractsStatusCodes(t *testing.T) { cases := []struct { name string err error want int }{ {"a book that is not this account's", pgstore.ErrNoBook, http.StatusNotFound}, {"a book already being translated", pgstore.ErrRunInFlight, http.StatusConflict}, // 409 and not 400: the request was legal when run-options was read, and a hold taken for // another book between that read and this call is what moved the bounds. {"a ceiling that no longer fits", runs.ErrCeilingOutOfBounds, http.StatusConflict}, {"an account that cannot pay", pgstore.ErrInsufficientCredit, http.StatusConflict}, // ⚠ 503 is NOT among the statuses the contract enumerates for this operation. It is used // because every alternative lies: the request is valid, the object exists, and the state is // not in conflict — the DEPLOYMENT cannot tell the engine its ceiling (row 145). {"a deployment that cannot pass a ceiling", runner.ErrCeilingNotWired, http.StatusServiceUnavailable}, } for _, tc := range cases { h := v0Server(t, &fakeLibrary{}, &fakeRuns{err: tc.err}) w := call(t, h, "POST", "/v0/books/bk_1/runs", `{"verify_bank":false,"ceiling_chapters":10}`) if w.Code != tc.want { t.Errorf("%s: %d, want %d", tc.name, w.Code, tc.want) } if ct := w.Header().Get("Content-Type"); ct != "application/problem+json" { t.Errorf("%s: content type %q", tc.name, ct) } // Neither title nor detail may carry engine or database text: what a client puts on a screen // is whatever this says. if bytes.Contains(w.Body.Bytes(), []byte("pgstore")) || bytes.Contains(w.Body.Bytes(), []byte("runner:")) { t.Errorf("%s: internals reached the wire: %s", tc.name, w.Body) } } } func TestUsageCarriesAShareAndItsState(t *testing.T) { for _, tc := range []struct { percent int state string }{{100, "ok"}, {11, "ok"}, {10, "low"}, {1, "low"}, {0, "low"}} { lib := &fakeLibrary{usage: pgstore.Usage{RemainingPercent: tc.percent, Spendable: true}} got := decode(t, call(t, v0Server(t, lib, &fakeRuns{}), "GET", "/v0/usage", "")) if got["state"] != tc.state || got["remaining_percent"] != float64(tc.percent) { t.Errorf("%d%%: %v, want state %q", tc.percent, got, tc.state) } if v, ok := got["paused_reason"]; !ok || v != nil { t.Errorf("%d%%: paused_reason = %v (present %v)", tc.percent, v, ok) } // No window, no resets_at, no sums (contract §Usage). for _, forbidden := range []string{"resets_at", "windows", "period", "usd", "amount"} { if _, ok := got[forbidden]; ok { t.Errorf("usage carries %q", forbidden) } } } } // Every contract route sits behind the session guard, and an anonymous caller meets 401 before 404: // the shape of the surface is not public information. func TestEveryContractRouteRequiresASession(t *testing.T) { h, err := New(Deps{ Log: slog.New(slog.NewJSONHandler(&bytes.Buffer{}, nil)), Auth: &auth.Authenticator{ Sessions: deadSessions{}, IdleTTL: time.Hour, Deny: ProblemHandler(http.StatusUnauthorized, "Session missing or invalid"), }, Library: &fakeLibrary{}, Runs: &fakeRuns{}, }) if err != nil { t.Fatal(err) } for _, path := range []string{"/v0/books", "/v0/books/bk_1", "/v0/books/bk_1/run-options", "/v0/usage"} { w := call(t, h, "GET", path, "") if w.Code != http.StatusUnauthorized { t.Errorf("%s answered %d to a caller with no session", path, w.Code) } } w := call(t, h, "POST", "/v0/books/bk_1/runs", `{"verify_bank":false,"ceiling_chapters":1}`) if w.Code != http.StatusUnauthorized { t.Errorf("starting a run answered %d to a caller with no session", w.Code) } } // The access log names the ROUTE and never the path: a raw path carries book and run ids, which // identify a user's library in an operator's index. func TestTheAccessLogNamesTheRouteOfAContractCall(t *testing.T) { var logs bytes.Buffer h, err := New(Deps{ Log: slog.New(slog.NewJSONHandler(&logs, nil)), Auth: &auth.Authenticator{ Sessions: liveSessions{}, IdleTTL: time.Hour, Deny: ProblemHandler(http.StatusUnauthorized, "Session missing or invalid"), }, Library: &fakeLibrary{book: pgstore.Book{ID: "bk_secret"}}, Runs: &fakeRuns{}, }) if err != nil { t.Fatal(err) } call(t, h, "GET", "/v0/books/bk_secret", "") if !strings.Contains(logs.String(), `"route":"GET /v0/books/{bookId}"`) { t.Errorf("the access log does not name the route: %s", logs.String()) } if strings.Contains(logs.String(), "bk_secret") { t.Errorf("a book id reached the log: %s", logs.String()) } } // A limit that is not a page size is a request the server cannot honour; a cursor it cannot use is // its own duty to reject (contract §NextCursor). func TestABadPageRequestIsRefused(t *testing.T) { h := v0Server(t, &fakeLibrary{err: pgstore.ErrBadCursor}, &fakeRuns{}) if w := call(t, h, "GET", "/v0/books?cursor=nonsense", ""); w.Code != http.StatusBadRequest { t.Errorf("a stale cursor answered %d", w.Code) } ok := v0Server(t, &fakeLibrary{}, &fakeRuns{}) for _, q := range []string{"limit=0", "limit=-1", "limit=abc"} { if w := call(t, ok, "GET", "/v0/books?"+q, ""); w.Code != http.StatusBadRequest { t.Errorf("%s answered %d, want 400", q, w.Code) } } } // Without a database there is no library to serve, and the prefix stays a guarded 404 rather than a // half-mounted surface. func TestWithoutAReadModelTheVersionedPrefixIsAGuarded404(t *testing.T) { h, err := New(Deps{ Log: slog.New(slog.NewJSONHandler(&bytes.Buffer{}, nil)), Auth: &auth.Authenticator{ Sessions: liveSessions{}, IdleTTL: time.Hour, Deny: ProblemHandler(http.StatusUnauthorized, "Session missing or invalid"), }, }) if err != nil { t.Fatal(err) } if w := call(t, h, "GET", "/v0/books", ""); w.Code != http.StatusNotFound { t.Errorf("status %d, want 404", w.Code) } } // A ceiling below the schema's own minimum is a MALFORMED request, and 409 is defined to mean the // bounds moved between the run-options read and this call — so a client answered 409 re-reads // run-options and retries a request that can never succeed. func TestACeilingBelowTheSchemaMinimumIsARejectedRequestAndNotAMovedBound(t *testing.T) { rn := &fakeRuns{run: pgstore.Run{ID: "run_1"}} h := v0Server(t, &fakeLibrary{}, rn) for _, body := range []string{`{"verify_bank":false,"ceiling_chapters":0}`, `{"verify_bank":false,"ceiling_chapters":-3}`} { w := call(t, h, "POST", "/v0/books/bk_1/runs", body) if w.Code != http.StatusBadRequest { t.Errorf("%s answered %d, want 400", body, w.Code) } } if rn.got.CeilingChapters != 0 { t.Errorf("an illegal ceiling reached the service: %+v", rn.got) } } // RunRequest does not close the object, and inside 0.x a minor bump is where optional fields appear: // a server that refused the whole request would break a client generated against a later 0.x for a // field it was free to ignore. func TestAnUnknownRequestPropertyIsIgnoredRatherThanRefused(t *testing.T) { rn := &fakeRuns{run: pgstore.Run{ID: "run_1", Status: "translating", CeilingChapters: 10}} w := call(t, v0Server(t, &fakeLibrary{}, rn), "POST", "/v0/books/bk_1/runs", `{"verify_bank":true,"ceiling_chapters":10,"a_field_from_a_later_minor":"x"}`) if w.Code != http.StatusAccepted { t.Fatalf("status %d: %s", w.Code, w.Body) } if rn.got.CeilingChapters != 10 || !rn.got.VerifyBank { t.Errorf("the known fields did not survive: %+v", rn.got) } } // ONE counter per book. Reading the card's revision off the RUN row made it lag: a unit_done bumps // the book and its chapter and not the run, so a client that had applied stream frame id=2 got 0 // back and — obeying the contract — dropped the read. // ONE counter per book on the wire. ⚠ The RULE moved in P5 and this test moved with it: the store now // answers a run's revision from its book on every path that hands one out — the card here and the // stop/resume handles, which have no book to override from — so the pin on the rule itself lives in // `pgstore.TestEveryRunTheStoreHandsOutCarriesItsBooksRevision` and `runs.TestEveryRunCarryingAnswerUsesTheBooksRevision`. // What THIS layer owes is that it projects the number it was given and invents nothing, which is // what a fake store can prove and the rule itself no longer is. func TestTheCardProjectsTheRevisionTheStoreGaveAndInventsNone(t *testing.T) { // The two numbers are DIFFERENT on purpose: equal ones cannot tell "projected what it was given" // from "overrode it with the book's", which is exactly the discriminating power an earlier // revision of this test lost when the fixture was levelled (found by acceptance). lib := &fakeLibrary{ book: pgstore.Book{ID: "bk_1", Revision: 41}, run: &pgstore.Run{ID: "run_1", Status: "translating", Revision: 12}, } got := decode(t, call(t, v0Server(t, lib, &fakeRuns{}), "GET", "/v0/books/bk_1", "")) if got["revision"] != float64(41) { t.Errorf("BookDetail.revision = %v, want the book's 41", got["revision"]) } run, _ := got["run"].(map[string]any) if run["revision"] != float64(12) { t.Errorf("Run.revision = %v, want the 12 the store handed over: this layer projects, it does not decide", run["revision"]) } } // "Exhausted" is a fact about the balance, not about the rounded share: $9 left of a $1000 grant // floors to 0% while a run can still be started, and the account screen must not contradict the run // dialog. func TestASmallRemainderIsLowAndNotExhausted(t *testing.T) { lib := &fakeLibrary{usage: pgstore.Usage{RemainingPercent: 0, Spendable: true}} got := decode(t, call(t, v0Server(t, lib, &fakeRuns{}), "GET", "/v0/usage", "")) if got["state"] != "low" { t.Errorf("state %v with money still spendable, want low", got["state"]) } lib.usage = pgstore.Usage{RemainingPercent: 0, Spendable: false} got = decode(t, call(t, v0Server(t, lib, &fakeRuns{}), "GET", "/v0/usage", "")) if got["state"] != "exhausted" { t.Errorf("state %v with nothing left, want exhausted", got["state"]) } } // An instance with a read model and no runner is a read REPLICA, not a broken one: the library is // not run machinery. It used to answer 404 to the library it was holding, while its own boot line // said it was serving one. func TestAnInstanceWithoutARunnerStillServesTheLibrary(t *testing.T) { h, err := New(Deps{ Log: slog.New(slog.NewJSONHandler(&bytes.Buffer{}, nil)), Auth: &auth.Authenticator{ Sessions: liveSessions{}, IdleTTL: time.Hour, Deny: ProblemHandler(http.StatusUnauthorized, "Session missing or invalid"), }, Library: &fakeLibrary{lib: pgstore.Library{Books: []pgstore.Book{{ID: "bk_1"}}}}, }) if err != nil { t.Fatal(err) } for _, path := range []string{"/v0/books", "/v0/books/bk_1", "/v0/usage"} { if w := call(t, h, "GET", path, ""); w.Code != http.StatusOK { t.Errorf("%s answered %d on a read-only instance, want 200", path, w.Code) } } // The run surface stays a guarded 404 rather than a handler that would fail on every call. if w := call(t, h, "GET", "/v0/books/bk_1/run-options", ""); w.Code != http.StatusNotFound { t.Errorf("run-options answered %d with no run lifecycle, want 404", w.Code) } if w := call(t, h, "POST", "/v0/books/bk_1/runs", `{"verify_bank":false,"ceiling_chapters":1}`); w.Code != http.StatusNotFound { t.Errorf("starting a run answered %d with no run lifecycle, want 404", w.Code) } } // The CSRF layer covers the CONTRACT surface, not just /auth/. Without it a cross-site page could // start a paid run with the browser's ambient cookie — the guard that pins the session check does // not see this, because a session is present in both cases. func TestACrossSiteRequestCannotStartARun(t *testing.T) { rn := &fakeRuns{run: pgstore.Run{ID: "run_1"}} h := v0Server(t, &fakeLibrary{}, rn) r := httptest.NewRequest("POST", "/v0/books/bk_1/runs", strings.NewReader(`{"verify_bank":false,"ceiling_chapters":10}`)) r.Header.Set("Content-Type", "application/json") r.AddCookie(&http.Cookie{Name: "__Host-tm_session", Value: "token"}) r.Header.Set("Sec-Fetch-Site", "cross-site") r.Header.Set("Origin", "https://evil.example") w := httptest.NewRecorder() h.ServeHTTP(w, r) if w.Code != http.StatusForbidden { t.Fatalf("a cross-site run start answered %d, want 403", w.Code) } if rn.got.CeilingChapters != 0 { t.Errorf("the cross-site request reached the service: %+v", rn.got) } // A same-origin browser request without the client header is refused for the same reason. r = httptest.NewRequest("POST", "/v0/books/bk_1/runs", strings.NewReader(`{"verify_bank":false,"ceiling_chapters":10}`)) r.Header.Set("Content-Type", "application/json") r.AddCookie(&http.Cookie{Name: "__Host-tm_session", Value: "token"}) w = httptest.NewRecorder() h.ServeHTTP(w, r) if w.Code == http.StatusAccepted { t.Error("a cookie-borne POST without X-TM-Client started a run") } } // A route mounted without its guard must fail CLOSED, and loudly: answering 401 would hide the // wiring defect, and serving an anonymous caller as user "" would give them somebody's library. func TestAHandlerWithNoPrincipalFailsClosed(t *testing.T) { var logs bytes.Buffer h := &v0{lib: &fakeLibrary{}, runs: &fakeRuns{}, log: slog.New(slog.NewJSONHandler(&logs, nil))} w := httptest.NewRecorder() h.listBooks(w, httptest.NewRequest("GET", "/v0/books", nil)) if w.Code != http.StatusInternalServerError { t.Errorf("a handler with no principal answered %d, want 500", w.Code) } } // Neither title nor detail may carry engine or database text (contract §Problem): what a client puts // on a screen is whatever this says. func TestAnInternalErrorNeverPutsItsTextOnTheWire(t *testing.T) { secret := "pgstore: relation \"books\" does not exist at character 42" h := v0Server(t, &fakeLibrary{err: errors.New(secret)}, &fakeRuns{}) w := call(t, h, "GET", "/v0/books", "") if w.Code != http.StatusInternalServerError { t.Fatalf("status %d", w.Code) } if strings.Contains(w.Body.String(), "pgstore") || strings.Contains(w.Body.String(), "relation") { t.Errorf("the internal message reached the wire: %s", w.Body) } }