package web import ( "context" "crypto/sha1" "encoding/hex" "encoding/json" "fmt" "net/http" "net/http/httptest" "os" "sort" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "sourcecraft.dev/bigbes/sr-ht-core/auth" "sourcecraft.dev/bigbes/sr-ht-core/crypto" "sourcecraft.dev/bigbes/sr-ht-ecore/bearer" "sourcecraft.dev/bigbes/sr-ht-ecore/csrf" "sourcecraft.dev/bigbes/sr-ht-ecore/ecoretest" // Aliased: this file's own helper for signing a request in is called login. ecorelogin "sourcecraft.dev/bigbes/sr-ht-ecore/login" "sourcecraft.dev/bigbes/sr-ht-spec/authn" "sourcecraft.dev/bigbes/sr-ht-spec/core" "sourcecraft.dev/bigbes/sr-ht-spec/search" "sourcecraft.dev/bigbes/sr-ht-spec/service" ) // TestMain installs sr-ht-ecore's fixed test keyset, which is what lets these // tests seal a unified-login cookie the way meta.sr.ht does with no meta and no // network. The keys are constants there, so it is idempotent: two packages of // this service can both call it without the second rotating what the first // sealed with. func TestMain(m *testing.M) { ecoretest.InitCrypto() // The agent credential is a signed tokens.sr.ht working token now, so it // cannot be a constant: it is minted here, once the signing key exists. agentTk = agentToken("spec:read") os.Exit(m.Run()) } // ---- fixtures ------------------------------------------------------------- const ( headRev = "1111111111111111111111111111111111111111" oldRev = "2222222222222222222222222222222222222222" ) // agentTk is a live working token for the instance owner, carrying spec:read — // what an agent reading through the web UI presents. Set by TestMain. var agentTk string // agentToken mints a signed working token the way tokens.sr.ht does. func agentToken(grantString string) string { bt := &auth.BearerToken{ Version: auth.TokenVersion, Expires: auth.ToTimestamp(time.Now().Add(time.Hour)), Grants: grantString, ClientID: bearer.TokensClientID, Username: "bigbes", } return bt.Encode() } var demoSpace = core.SpaceRef{Owner: "bigbes", Name: "rfcs"} // headDocs is the space at its approved head. SPEC-0007 links to SPEC-0003, so // SPEC-0003 has a backlink; notes/plain.md has no frontmatter at all and is // therefore addressed by its path, per the design's addressing rule. var headDocs = map[string]string{ "specs/0007-storage.md": `--- id: SPEC-0007 title: Proposal storage model status: draft tags: [storage, review] summary: How proposals are stored. --- # Proposal storage model Git is authoritative, and this supersedes [[SPEC-0003]]. `, "specs/0003-old.md": `--- id: SPEC-0003 title: Older storage sketch status: superseded --- # Older storage sketch Superseded by the storage model. `, "notes/plain.md": `# Just a note No frontmatter here at all. `, } // oldDocs is the same space at a pinned, older revision: the title differs, so // a test can prove ?rev= actually reached a different tree. var oldDocs = map[string]string{ "specs/0007-storage.md": `--- id: SPEC-0007 title: Storage, first draft status: draft --- # Storage, first draft An earlier sketch. `, } // fakeReader is an in-memory Reader: a space is a revision-keyed set of // documents. It exists so the handlers can be tested against a document set // rather than against Postgres plus a tree of bare repositories. type fakeReader struct { revs map[string]map[string]string // rev -> path -> content head string // proposals, diffs and actErr back the Phase 4 review-page tests. proposals // is keyed by id; diffs by proposal id; actErr, when set, is what Approve // and Reject return instead of acting. proposals map[int]service.Proposal diffs map[int][]service.ProposalDoc actErr error // mark/marked back the digest_mark tests: the owner's "last looked at" // timestamp and whether one has been set. MarkDigestSeen moves them. mark time.Time marked bool // threads back the Phase 5b comment tests: review threads by proposal id, // with ids handed out by nextThread. The authority rules below are service/'s // and are restated here only so the fake refuses what the real service // refuses — a page that draws a control the service would reject would pass // against a fake that accepts everything. threads map[int][]*service.Thread nextThread int } func newFakeReader() *fakeReader { return &fakeReader{ revs: map[string]map[string]string{headRev: headDocs, oldRev: oldDocs}, head: headRev, proposals: map[int]service.Proposal{}, diffs: map[int][]service.ProposalDoc{}, threads: map[int][]*service.Thread{}, } } func (f *fakeReader) ListSpaces(context.Context) ([]core.SpaceRef, error) { return []core.SpaceRef{demoSpace}, nil } // at resolves a revision the way service.ResolveRev does: ApprovedRev means the // approved head, anything else must name a revision that exists. func (f *fakeReader) at(ref core.SpaceRef, rev string) (string, map[string]string, error) { if ref != demoSpace { return "", nil, fmt.Errorf("%w: space %s", service.ErrNotFound, ref) } if rev == service.ApprovedRev { rev = f.head } docs, ok := f.revs[rev] if !ok { return "", nil, fmt.Errorf("%w: revision %q in %s", service.ErrNotFound, rev, ref) } return rev, docs, nil } func (f *fakeReader) Snapshot(_ context.Context, ref core.SpaceRef, rev string) (*Snapshot, error) { resolved, docs, err := f.at(ref, rev) if err != nil { return nil, err } paths := make([]string, 0, len(docs)) for p := range docs { paths = append(paths, p) } sort.Strings(paths) // Built through service.ArchiveFrom, exactly as serviceReader gets it: the // addressing rule and the link graph under test are the production ones, // not a fixture's idea of them. sd := make([]service.Document, 0, len(paths)) for _, p := range paths { data := []byte(docs[p]) sd = append(sd, service.Document{ Path: p, Blob: blobSha(data), Rev: resolved, Data: data, }) } arc, bodies, err := service.ArchiveFrom(ref, resolved, sd) if err != nil { return nil, err } return &Snapshot{Ref: ref, Rev: resolved, Archive: arc, Bodies: bodies}, nil } // blobSha is git's object name for a blob: sha1 over "blob \0" and the // content. Spelled out rather than taken from go-git so that this package — // including its tests — never imports the git layer, which is the layering rule // the Reader interface exists to keep. func blobSha(data []byte) string { h := sha1.New() fmt.Fprintf(h, "blob %d", len(data)) h.Write([]byte{0}) h.Write(data) return hex.EncodeToString(h.Sum(nil)) } func (f *fakeReader) ReadDocument(_ context.Context, ref core.SpaceRef, rev, p string) (service.Document, error) { resolved, docs, err := f.at(ref, rev) if err != nil { return service.Document{}, err } content, ok := docs[p] if !ok { return service.Document{}, fmt.Errorf("%w: %s in %s at %s", service.ErrNotFound, p, ref, resolved) } return service.Document{ Path: p, Blob: blobSha([]byte(content)), Rev: resolved, Data: []byte(content), }, nil } func (f *fakeReader) GetProposal(_ context.Context, id int) (service.Proposal, error) { p, ok := f.proposals[id] if !ok { return service.Proposal{}, fmt.Errorf("%w: proposal %d", service.ErrNotFound, id) } return p, nil } func (f *fakeReader) ListProposals(_ context.Context, ref core.SpaceRef, state core.ProposalState) ([]service.Proposal, error) { var out []service.Proposal for _, p := range f.proposals { if p.Space == ref && p.State == state { out = append(out, p) } } sort.Slice(out, func(i, j int) bool { return out[i].ID > out[j].ID }) return out, nil } func (f *fakeReader) ProposalDiff(_ context.Context, p service.Proposal) ([]service.ProposalDoc, error) { return f.diffs[p.ID], nil } func (f *fakeReader) Approve(_ context.Context, _ core.SpaceRef, id int) (service.Proposal, error) { if f.actErr != nil { return service.Proposal{}, f.actErr } p := f.proposals[id] p.State = core.StateMerged p.Approval = core.ApprovalHuman f.proposals[id] = p return p, nil } func (f *fakeReader) Reject(_ context.Context, _ core.SpaceRef, id int) (service.Proposal, error) { if f.actErr != nil { return service.Proposal{}, f.actErr } p := f.proposals[id] p.State = core.StateRejected f.proposals[id] = p return p, nil } func (f *fakeReader) Threads(_ context.Context, p authn.Principal, proposalID int) ([]service.Thread, error) { if !p.CanRead() { return nil, fmt.Errorf("%w: %s may not read review threads", service.ErrForbidden, p) } out := make([]service.Thread, 0, len(f.threads[proposalID])) for _, t := range f.threads[proposalID] { out = append(out, *t) } return out, nil } func (f *fakeReader) CommentOn(_ context.Context, req service.CommentRequest) (service.Thread, error) { if !req.Principal.IsOwner() { return service.Thread{}, fmt.Errorf("%w: %s may not open a review thread", service.ErrForbidden, req.Principal) } f.nextThread++ t := &service.Thread{ Root: service.Comment{ ID: f.nextThread, Body: req.Body, Author: req.Principal.Owner, Created: time.Now(), }, DocPath: req.DocPath, Anchor: req.Anchor, Block: -1, } f.threads[req.ProposalID] = append(f.threads[req.ProposalID], t) return *t, nil } func (f *fakeReader) ReplyTo(_ context.Context, p authn.Principal, threadID int, body string) (service.Comment, error) { if !p.CanRead() { return service.Comment{}, fmt.Errorf("%w: %s may not comment", service.ErrForbidden, p) } t := f.thread(threadID) if t == nil { return service.Comment{}, fmt.Errorf("%w: no review thread %d", service.ErrNotFound, threadID) } f.nextThread++ author, agent := p.Owner, false if p.IsAgent() { author, agent = p.Agent, true } reply := service.Comment{ ID: f.nextThread, ParentID: threadID, Body: body, Author: author, Agent: agent, Created: time.Now(), } t.Replies = append(t.Replies, reply) return reply, nil } func (f *fakeReader) ResolveThread(_ context.Context, p authn.Principal, threadID int, resolved bool) error { if !p.IsOwner() { return fmt.Errorf("%w: %s may not resolve a review thread", service.ErrForbidden, p) } t := f.thread(threadID) if t == nil { return fmt.Errorf("%w: no review thread %d", service.ErrNotFound, threadID) } if !resolved { t.Resolved = nil return nil } now := time.Now() t.Resolved = &now return nil } // thread finds a root thread by id across every proposal, the way a global // thread id resolves in the real store. func (f *fakeReader) thread(id int) *service.Thread { for _, ts := range f.threads { for _, t := range ts { if t.Root.ID == id { return t } } } return nil } func (f *fakeReader) Inbox(_ context.Context) ([]service.Proposal, error) { var out []service.Proposal for _, p := range f.proposals { if p.State == core.StateOpen { out = append(out, p) } } sort.Slice(out, func(i, j int) bool { return out[i].ID > out[j].ID }) return out, nil } func (f *fakeReader) Digest(_ context.Context) ([]service.Proposal, error) { var out []service.Proposal for _, p := range f.proposals { if p.State == core.StateMerged && p.Approval == core.ApprovalPolicy { out = append(out, p) } } sort.Slice(out, func(i, j int) bool { return out[i].ID > out[j].ID }) return out, nil } func (f *fakeReader) DigestMark(context.Context) (time.Time, bool, error) { return f.mark, f.marked, nil } func (f *fakeReader) MarkDigestSeen(_ context.Context, seenAt time.Time) error { f.mark, f.marked = seenAt, true return nil } // fakeSearcher returns one fixed hit whose snippet carries the tags // bleve's highlighter emits. type fakeSearcher struct { last search.Query err error } func (s *fakeSearcher) Search(_ context.Context, q search.Query) (search.Results, error) { s.last = q if s.err != nil { return search.Results{}, s.err } return search.Results{ Total: 1, Hits: []search.Hit{{ Space: demoSpace, ID: "SPEC-0007", Rev: headRev, Path: "specs/0007-storage.md", Title: "Proposal storage model", Section: "specs", Score: 1.5, Snippet: `Git is authoritative & boring`, }}, }, nil } // stubUsers resolves the owner an instance token names to a local row. type stubUsers struct{} func (stubUsers) LookupUser(_ context.Context, username string) (authn.InstanceUser, error) { return authn.InstanceUser{ID: 1, Username: username}, nil } // testResolver is the resolver the daemon builds, with the one agent credential // plane wired. Its origin is never reached: these tests present stateless // tokens, which carry no row id and so skip the revocation round trip. func testResolver(t *testing.T) *authn.Resolver { t.Helper() v, err := bearer.New(bearer.Options{ Origin: "https://tokens.srht.invalid", ClientID: "spec.sr.ht", NodeID: "web-test", }) if err != nil { t.Fatalf("bearer.New: %v", err) } resolver, err := authn.NewResolver("bigbes", authn.WithInstancePlane(v, stubUsers{})) if err != nil { t.Fatalf("NewResolver: %v", err) } return resolver } // testServer wires a Server with a fresh fake reader/searcher behind the same // middleware the daemon installs, and returns the handler. func testServer(t *testing.T) (http.Handler, *fakeSearcher) { t.Helper() h, _, sr := testServerWith(t, newFakeReader()) return h, sr } // testServerWith is testServer with a caller-supplied reader, so the review-page // tests can seed proposals into it and still get the same middleware stack. func testServerWith(t *testing.T, reader *fakeReader) (http.Handler, *fakeReader, *fakeSearcher) { t.Helper() // The synthetic instance config of sr-ht-ecore: this service's own section // with an origin, meta's, tokens' (which /tokens redirects to), and the rest // of the instance so that these pages render against a realistic switcher. // Which entries the switcher shows and in what order is ecore's decision and // is tested there. conf := ecoretest.Config(authn.ConfigSection) resolver := testResolver(t) searcher := &fakeSearcher{} srv, err := New(Options{ Conf: conf, Reader: reader, Searcher: searcher, Resolver: resolver, }) if err != nil { t.Fatalf("New: %v", err) } return srv.Handler(), reader, searcher } // login seals a unified-login cookie for the given user onto a request — the // same shape meta.sr.ht writes, sealed with the shared network key. func login(req *http.Request, user string) { payload, _ := json.Marshal(map[string]string{"name": user}) req.AddCookie(&http.Cookie{Name: ecorelogin.CookieName, Value: string(crypto.Encrypt(payload))}) } func get(t *testing.T, h http.Handler, target, user string) *httptest.ResponseRecorder { t.Helper() req := httptest.NewRequest(http.MethodGet, target, nil) if user != "" { login(req, user) } rec := httptest.NewRecorder() h.ServeHTTP(rec, req) return rec } func getAgent(t *testing.T, h http.Handler, target, token string) *httptest.ResponseRecorder { t.Helper() req := httptest.NewRequest(http.MethodGet, target, nil) req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) return rec } // ---- URL grammar ---------------------------------------------------------- func TestDocumentAddressHasNoExtension(t *testing.T) { h, _ := testServer(t) rec := get(t, h, "/~bigbes/rfcs/specs/0007-storage", "bigbes") if rec.Code != http.StatusOK { t.Fatalf("status = %d, want 200\n%s", rec.Code, rec.Body.String()) } if ct := rec.Header().Get("Content-Type"); !strings.HasPrefix(ct, "text/html") { t.Fatalf("content-type = %q, want text/html", ct) } body := rec.Body.String() if !strings.Contains(body, "Proposal storage model") { t.Fatal("rendered page missing the document title") } if !strings.Contains(body, "Backlinks") if i < 0 { t.Fatal("no backlinks section") } if !strings.Contains(body[i:], "/~bigbes/rfcs/specs/0007-storage") { t.Fatalf("SPEC-0007 links to SPEC-0003 but is not listed as a backlink:\n%s", body[i:]) } } // ---- not found ------------------------------------------------------------ func TestMissingDocumentIs404(t *testing.T) { h, _ := testServer(t) for _, target := range []string{ "/~bigbes/rfcs/specs/nope", "/~bigbes/rfcs/specs/nope.md", "/~bigbes/rfcs/specs/nope.json", } { rec := get(t, h, target, "bigbes") if rec.Code != http.StatusNotFound { t.Fatalf("%s: status = %d, want 404\n%s", target, rec.Code, rec.Body.String()) } } } func TestMissingSpaceIs404(t *testing.T) { h, _ := testServer(t) rec := get(t, h, "/~bigbes/nope", "bigbes") if rec.Code != http.StatusNotFound { t.Fatalf("status = %d, want 404", rec.Code) } } // The router's own two refusals are this service's error page and not net/http's // plain text. They are the ones a viewer is most likely to meet — a mistyped URL // and a stale bookmark — and until chimw.RenderRefusals was installed they were // the only refusals on this surface with no nav to get out of. func TestRoutingFailuresRenderTheChromePage(t *testing.T) { h, _ := testServer(t) rec := get(t, h, "/no/such/route", "bigbes") assert.Equal(t, http.StatusNotFound, rec.Code) assert.Contains(t, rec.Body.String(), "navbar-brand", "the 404 carries the chrome") // A method the router does not allow on a path it does serve: /inbox/seen is // a POST, so a GET of it is chi's 405 rather than a miss. rec = get(t, h, "/inbox/seen", "bigbes") assert.Equal(t, http.StatusMethodNotAllowed, rec.Code) assert.Contains(t, rec.Body.String(), "navbar-brand", "the 405 carries the chrome") } // Every read route answers HEAD, because RFC 9110 §9.3.2 makes it mandatory for // anything serving GET and because a monitor and a revalidating cache both reach // for it. The pair shares a handler, so the status is the GET's — including the // 404 of a document that is not there, which must not become a 200 on a method // that returns no body to contradict it. func TestReadRoutesAnswerHead(t *testing.T) { h, _ := testServer(t) for target, want := range map[string]int{ "/": http.StatusOK, "/healthz": http.StatusOK, "/inbox": http.StatusOK, "/~bigbes/rfcs": http.StatusOK, "/~bigbes/rfcs/specs/0007-storage": http.StatusOK, "/~bigbes/rfcs/specs/nope": http.StatusNotFound, } { t.Run(target, func(t *testing.T) { req := httptest.NewRequest(http.MethodHead, target, nil) login(req, "bigbes") rec := httptest.NewRecorder() h.ServeHTTP(rec, req) assert.Equal(t, want, rec.Code, "HEAD %s", target) }) } } func TestTrailingSlashRedirectsToTheSpace(t *testing.T) { h, _ := testServer(t) rec := get(t, h, "/~bigbes/rfcs/", "bigbes") if rec.Code != http.StatusFound { t.Fatalf("status = %d, want 302", rec.Code) } if loc := rec.Header().Get("Location"); loc != "/~bigbes/rfcs" { t.Fatalf("location = %q", loc) } } // ---- identity and chrome -------------------------------------------------- // // What the shared chrome itself decides — the switcher's order, the services it // excludes, the shape of a login URL, the environment banner — is sr-ht-ecore's // and is tested there. What is left here is the seam: that this service hands // ecore the identity its own authn resolved, and that the page around the // chrome shows the right thing to that identity. // The owner's cookie must reach the chrome as an identity: ecore renders the // login block from the username it is given, so a greeting by name is the proof // that view() passed one. func TestOwnerCookieReachesTheChromeAsAnIdentity(t *testing.T) { h, _ := testServer(t) rec := get(t, h, "/", "bigbes") require.Equal(t, http.StatusOK, rec.Code) body := rec.Body.String() assert.Contains(t, body, "Logged in as") assert.Contains(t, body, ">bigbes<") assert.Contains(t, body, "~bigbes/rfcs", "the logged-in landing page lists the spaces") } // A cookie sealed for somebody who is not the instance owner carries no // authority: authn resolves it to anonymous, and the page must agree. func TestNonOwnerCookieIsAnonymous(t *testing.T) { h, _ := testServer(t) rec := get(t, h, "/", "someoneelse") body := rec.Body.String() assert.NotContains(t, body, "Logged in as", "a non-owner cookie produced a logged-in nav") assert.NotContains(t, body, "~bigbes/rfcs", "a non-owner must not see the space list") } func TestAnonymousLandingRendersWithoutContent(t *testing.T) { h, _ := testServer(t) rec := get(t, h, "/", "") require.Equal(t, http.StatusOK, rec.Code) body := rec.Body.String() assert.Contains(t, body, "https://meta.example/login", "the landing page offers a login") assert.NotContains(t, body, "~bigbes/rfcs", "anonymous landing page leaked a space name") } // The read gate is this package's; the URL it redirects to is the chrome's. The // assertions below are about the gate firing at all and about it sending a // browser somewhere it can come back from — not about how ecore spells a login // URL. func TestAnonymousContentRedirectsToLogin(t *testing.T) { h, _ := testServer(t) for _, target := range []string{ "/~bigbes/rfcs", "/~bigbes/rfcs/specs/0007-storage", "/search?q=storage", } { rec := get(t, h, target, "") require.Equal(t, http.StatusFound, rec.Code, "%s: body %s", target, rec.Body) loc := rec.Header().Get("Location") assert.True(t, strings.HasPrefix(loc, "https://meta.example/login?return_to="), "%s: location = %q", target, loc) assert.Contains(t, loc, "spec.example", "%s: return_to does not point back at us: %q", target, loc) } } func TestAnonymousMachineFormatsAre401(t *testing.T) { h, _ := testServer(t) for _, target := range []string{ "/~bigbes/rfcs/specs/0007-storage.md", "/~bigbes/rfcs/specs/0007-storage.json", } { rec := get(t, h, target, "") if rec.Code != http.StatusUnauthorized { t.Fatalf("%s: status = %d, want 401", target, rec.Code) } if strings.Contains(rec.Body.String(), "Proposal storage model") { t.Fatalf("%s: content leaked to an anonymous client", target) } } } func TestAgentTokenReads(t *testing.T) { h, _ := testServer(t) rec := getAgent(t, h, "/~bigbes/rfcs/specs/0007-storage.md", agentTk) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want 200\n%s", rec.Code, rec.Body.String()) } if !strings.Contains(rec.Body.String(), "id: SPEC-0007") { t.Fatal("agent read did not return the document") } } func TestUnknownAgentTokenIs401(t *testing.T) { h, _ := testServer(t) rec := getAgent(t, h, "/~bigbes/rfcs/specs/0007-storage.md", "not-a-token") if rec.Code != http.StatusUnauthorized { t.Fatalf("status = %d, want 401", rec.Code) } } // ---- search --------------------------------------------------------------- func TestSearchRendersSnippetAsHTML(t *testing.T) { h, searcher := testServer(t) rec := get(t, h, "/search?q=authoritative&space=~bigbes/rfcs", "bigbes") if rec.Code != http.StatusOK { t.Fatalf("status = %d\n%s", rec.Code, rec.Body.String()) } if searcher.last.Text != "authoritative" { t.Fatalf("query text = %q", searcher.last.Text) } if refs := searcher.last.Spaces.Refs(); len(refs) != 1 || refs[0] != demoSpace { t.Fatalf("space filter = %s", searcher.last.Spaces) } if searcher.last.Spaces.Everything() { t.Fatal("naming a space must restrict the search, not widen it") } body := rec.Body.String() if !strings.Contains(body, "authoritative") { t.Fatalf("snippet was escaped instead of rendered as HTML:\n%s", body) } // The hit's URL is the pinned, extensionless address. if !strings.Contains(body, "/~bigbes/rfcs/specs/0007-storage?rev="+headRev) { t.Fatalf("hit href is not a pinned extensionless address:\n%s", body) } } func TestSearchWithoutQueryDoesNotSearch(t *testing.T) { h, searcher := testServer(t) rec := get(t, h, "/search", "bigbes") if rec.Code != http.StatusOK { t.Fatalf("status = %d", rec.Code) } if searcher.last.Text != "" { t.Fatal("an empty query must not reach the index") } } // ---- static and health ---------------------------------------------------- func TestHealthz(t *testing.T) { h, _ := testServer(t) rec := get(t, h, "/healthz", "") if rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), "ok") { t.Fatalf("healthz = %d %q", rec.Code, rec.Body.String()) } } // The same-origin guard is router-wide (Handler) rather than a line three // handlers remember, and this is what that buys: it runs before routing, so a // mutation aimed at an address this surface does not serve is refused too, and // a form added tomorrow is protected by having been registered. func TestTheGuardCoversRoutesThisSurfaceDoesNotServe(t *testing.T) { h, _ := testServer(t) rec := post(t, h, "/no/such/route", "bigbes", "") assert.Equal(t, http.StatusForbidden, rec.Code) assert.Contains(t, rec.Body.String(), csrf.Message) // With this site's own Origin the guard passes and the router answers for // itself — the refusal above is the guard's and not the router's. rec = post(t, h, "/no/such/route", "bigbes", ecoretest.Origin(authn.ConfigSection)) assert.Equal(t, http.StatusNotFound, rec.Code) } // The asset tree is mounted and served publicly. Which lifetime a name earns is // sr-ht-ecore/assets' rule and is tested there; what this asserts is that this // service's static mount reaches the embedded tree at all, and that the page // policy (private, no-store) does not follow an asset out. func TestStaticLogoIsServed(t *testing.T) { h, _ := testServer(t) rec := get(t, h, "/static/logo.svg", "") assert.Equal(t, http.StatusOK, rec.Code) assert.Contains(t, rec.Header().Get("Cache-Control"), "max-age") assert.Empty(t, rec.Header().Get("Vary"), "an asset is the same bytes for everybody") } // An asset name that is not in the tree gets this service's own 404 page rather // than net/http's plaintext one — a URL typed by hand is a dead end without a // nav to get out of. func TestMissingAssetGetsTheChromePage(t *testing.T) { h, _ := testServer(t) rec := get(t, h, "/static/nope.css", "") assert.Equal(t, http.StatusNotFound, rec.Code) assert.Contains(t, rec.Body.String(), "navbar-brand", "the 404 carries the chrome") } // ---- unit-level grammar --------------------------------------------------- func TestSplitFormat(t *testing.T) { cases := []struct { in string addr string f format }{ {"specs/0007-storage", "specs/0007-storage", formatHTML}, {"specs/0007-storage.md", "specs/0007-storage", formatRaw}, {"specs/0007-storage.json", "specs/0007-storage", formatJSON}, // A document whose own name ends in ".json" is still addressable: the // selector is peeled off once, from the tail. {"notes/2026.json.md", "notes/2026.json", formatRaw}, {"notes/report", "notes/report", formatHTML}, } for _, c := range cases { addr, f := splitFormat(c.in) if addr != c.addr || f != c.f { t.Fatalf("splitFormat(%q) = %q/%v, want %q/%v", c.in, addr, f, c.addr, c.f) } } }