package web
import (
"context"
"crypto/rand"
"crypto/sha1"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
"sort"
"strings"
"testing"
"github.com/fernet/fernet-go"
"github.com/vaughan0/go-ini"
"sourcecraft.dev/bigbes/sr-ht-core/crypto"
"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"
)
// testConf carries the crypto keys established in TestMain so tests can seal
// unified-login cookies the way meta.sr.ht does.
var testConf ini.File
func TestMain(m *testing.M) {
var fk fernet.Key
if err := fk.Generate(); err != nil {
panic("generate fernet key: " + err.Error())
}
seed := make([]byte, 32)
if _, err := rand.Read(seed); err != nil {
panic("generate webhook seed: " + err.Error())
}
testConf = ini.File{
"sr.ht": ini.Section{"network-key": fk.Encode()},
"webhooks": ini.Section{"private-key": base64.StdEncoding.EncodeToString(seed)},
}
crypto.InitCrypto(testConf)
os.Exit(m.Run())
}
// ---- fixtures -------------------------------------------------------------
const (
headRev = "1111111111111111111111111111111111111111"
oldRev = "2222222222222222222222222222222222222222"
agentTk = "test-agent-token"
)
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
}
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{},
}
}
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 <len>\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
}
// fakeSearcher returns one fixed hit whose snippet carries the <mark> 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 <mark>authoritative</mark> & boring`,
}},
}, nil
}
// stubTokenStore knows exactly one live agent token.
type stubTokenStore struct{}
func (stubTokenStore) LookupAgentToken(_ context.Context, hash []byte) (authn.AgentToken, error) {
want := authn.HashToken(agentTk)
if string(hash) != string(want) {
return authn.AgentToken{}, authn.ErrUnknownToken
}
return authn.AgentToken{ID: 1, Name: "test", Hash: want}, nil
}
// 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()
conf := ini.File{
"sr.ht": ini.Section{
"network-key": testConf.Section("sr.ht")["network-key"],
"site-name": "sourcehut",
"environment": "development",
"owner-name": "bigbes",
},
"webhooks": ini.Section{"private-key": testConf.Section("webhooks")["private-key"]},
"spec.sr.ht": ini.Section{"origin": "https://spec.example"},
"meta.sr.ht": ini.Section{"origin": "https://meta.example"},
"git.sr.ht": ini.Section{"origin": "https://git.example"},
// Extra service sections to exercise nav ordering/exclusions.
"todo.sr.ht": ini.Section{"origin": "https://todo.example"},
"paste.sr.ht": ini.Section{"origin": "https://paste.example"},
"pages.sr.ht": ini.Section{"origin": "https://pages.example"},
"hub.sr.ht": ini.Section{"origin": "https://hub.example"},
}
resolver, err := authn.NewResolver("bigbes", stubTokenStore{})
if err != nil {
t.Fatalf("NewResolver: %v", err)
}
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: authn.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, "<h1") {
t.Fatalf("body was not rendered as markdown:\n%s", body)
}
// The rendered wikilink must point at the target document's own
// extensionless address.
if !strings.Contains(body, `href="/~bigbes/rfcs/specs/0003-old"`) {
t.Fatalf("wikilink not resolved to an extensionless address:\n%s", body)
}
}
func TestDocumentRawFormat(t *testing.T) {
h, _ := testServer(t)
rec := get(t, h, "/~bigbes/rfcs/specs/0007-storage.md", "bigbes")
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want 200", rec.Code)
}
if ct := rec.Header().Get("Content-Type"); !strings.HasPrefix(ct, "text/markdown") {
t.Fatalf("content-type = %q, want text/markdown", ct)
}
body := rec.Body.String()
if body != headDocs["specs/0007-storage.md"] {
t.Fatalf(".md is not the verbatim source:\n%s", body)
}
if !strings.HasPrefix(body, "---\n") {
t.Fatal(".md must include the frontmatter")
}
if rec.Header().Get("X-Spec-Rev") != headRev {
t.Fatalf("X-Spec-Rev = %q, want %q", rec.Header().Get("X-Spec-Rev"), headRev)
}
}
func TestDocumentJSONFormat(t *testing.T) {
h, _ := testServer(t)
rec := get(t, h, "/~bigbes/rfcs/specs/0007-storage.json", "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, "application/json") {
t.Fatalf("content-type = %q, want application/json", ct)
}
var payload docJSON
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
t.Fatalf("decode: %v\n%s", err, rec.Body.String())
}
if payload.ID != "SPEC-0007" || payload.DocID != "SPEC-0007" {
t.Fatalf("id = %q / doc_id = %q, want SPEC-0007", payload.ID, payload.DocID)
}
if payload.Address != "specs/0007-storage" {
t.Fatalf("address = %q, want the extensionless address", payload.Address)
}
if payload.Path != "specs/0007-storage.md" {
t.Fatalf("path = %q, want the tree path", payload.Path)
}
if payload.Rev != headRev {
t.Fatalf("rev = %q, want %q", payload.Rev, headRev)
}
if !strings.Contains(payload.Body, "# Proposal storage model") {
t.Fatalf("body missing:\n%s", payload.Body)
}
if strings.Contains(payload.Body, "id: SPEC-0007") {
t.Fatal("body must be the body, with the frontmatter lifted into metadata")
}
if payload.Status != "draft" || len(payload.Tags) != 2 {
t.Fatalf("metadata not carried: %+v", payload)
}
}
func TestPinnedRevReachesAnotherTree(t *testing.T) {
h, _ := testServer(t)
head := get(t, h, "/~bigbes/rfcs/specs/0007-storage", "bigbes")
if !strings.Contains(head.Body.String(), "Proposal storage model") {
t.Fatal("approved head did not render the current title")
}
for _, target := range []string{
"/~bigbes/rfcs/specs/0007-storage?rev=" + oldRev,
"/~bigbes/rfcs/specs/0007-storage.md?rev=" + oldRev,
"/~bigbes/rfcs/specs/0007-storage.json?rev=" + oldRev,
"/~bigbes/rfcs?rev=" + oldRev,
} {
rec := get(t, h, target, "bigbes")
if rec.Code != http.StatusOK {
t.Fatalf("%s: status = %d\n%s", target, rec.Code, rec.Body.String())
}
if !strings.Contains(rec.Body.String(), "Storage, first draft") {
t.Fatalf("%s: pinned read did not reach the older revision:\n%s", target, rec.Body.String())
}
}
}
func TestPinnedPageKeepsThePinOnItsLinks(t *testing.T) {
h, _ := testServer(t)
rec := get(t, h, "/~bigbes/rfcs?rev="+headRev, "bigbes")
if rec.Code != http.StatusOK {
t.Fatalf("status = %d", rec.Code)
}
if !strings.Contains(rec.Body.String(), "/~bigbes/rfcs/specs/0007-storage?rev="+headRev) {
t.Fatalf("space listing dropped the pin:\n%s", rec.Body.String())
}
rec = get(t, h, "/~bigbes/rfcs/specs/0007-storage?rev="+headRev, "bigbes")
if !strings.Contains(rec.Body.String(), `href="/~bigbes/rfcs/specs/0003-old?rev=`+headRev+`"`) {
t.Fatalf("a wikilink out of a pinned page dropped the pin:\n%s", rec.Body.String())
}
}
func TestUnknownRevIs404(t *testing.T) {
h, _ := testServer(t)
rec := get(t, h, "/~bigbes/rfcs/specs/0007-storage?rev=deadbeef", "bigbes")
if rec.Code != http.StatusNotFound {
t.Fatalf("status = %d, want 404", rec.Code)
}
}
func TestDocumentIDRedirectsToItsPath(t *testing.T) {
h, _ := testServer(t)
rec := get(t, h, "/~bigbes/rfcs/SPEC-0007", "bigbes")
if rec.Code != http.StatusFound {
t.Fatalf("status = %d, want 302\n%s", rec.Code, rec.Body.String())
}
if loc := rec.Header().Get("Location"); loc != "/~bigbes/rfcs/specs/0007-storage" {
t.Fatalf("location = %q", loc)
}
}
// ---- addressing rule ------------------------------------------------------
func TestDocumentWithoutFrontmatterIsAddressedByPath(t *testing.T) {
h, _ := testServer(t)
rec := get(t, h, "/~bigbes/rfcs/notes/plain.json", "bigbes")
if rec.Code != http.StatusOK {
t.Fatalf("status = %d\n%s", rec.Code, rec.Body.String())
}
var payload docJSON
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
t.Fatal(err)
}
if payload.DocID != "" {
t.Fatalf("doc_id = %q, want empty for a document with no id:", payload.DocID)
}
if payload.ID != "notes/plain" {
t.Fatalf("id = %q, want the path minus its extension", payload.ID)
}
if payload.Title != "Just a note" {
t.Fatalf("title = %q, want the first H1", payload.Title)
}
}
func TestBacklinksAreRendered(t *testing.T) {
h, _ := testServer(t)
rec := get(t, h, "/~bigbes/rfcs/specs/0003-old", "bigbes")
if rec.Code != http.StatusOK {
t.Fatalf("status = %d\n%s", rec.Code, rec.Body.String())
}
body := rec.Body.String()
i := strings.Index(body, "<h4>Backlinks</h4>")
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)
}
}
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 --------------------------------------------------
func TestForgedCookieYieldsLoggedInNav(t *testing.T) {
h, _ := testServer(t)
rec := get(t, h, "/", "bigbes")
if rec.Code != http.StatusOK {
t.Fatalf("status = %d", rec.Code)
}
body := rec.Body.String()
if !strings.Contains(body, "Logged in as") || !strings.Contains(body, ">bigbes<") {
t.Fatalf("cookie did not produce a logged-in nav:\n%s", body)
}
if !strings.Contains(body, "~bigbes/rfcs") {
t.Fatal("logged-in landing page missing the space list")
}
if !strings.Contains(body, "https://todo.example") {
t.Fatal("nav missing an expected service")
}
if strings.Contains(body, "https://paste.example") || strings.Contains(body, "https://pages.example") {
t.Fatal("nav must exclude paste/pages")
}
nav := body[strings.Index(body, `<ul class="navbar-nav">`):strings.Index(body, "</ul>")]
if strings.Contains(nav, "hub.example") {
t.Fatal("hub is the brand, never a switcher item")
}
if !strings.Contains(nav, "nav-item active") {
t.Fatal("spec should be the active nav item")
}
if !strings.Contains(body, "DEVELOPMENT ENVIRONMENT") {
t.Fatal("non-production environment banner missing")
}
}
// A cookie sealed for somebody who is not the instance owner carries no
// authority: authn resolves it to anonymous, and the nav must agree.
func TestNonOwnerCookieIsAnonymous(t *testing.T) {
h, _ := testServer(t)
rec := get(t, h, "/", "someoneelse")
body := rec.Body.String()
if strings.Contains(body, "Logged in as") {
t.Fatalf("a non-owner cookie produced a logged-in nav:\n%s", body)
}
if strings.Contains(body, "~bigbes/rfcs") {
t.Fatal("a non-owner must not see the space list")
}
}
func TestAnonymousLandingRendersWithoutContent(t *testing.T) {
h, _ := testServer(t)
rec := get(t, h, "/", "")
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want 200", rec.Code)
}
body := rec.Body.String()
if !strings.Contains(body, "return_to=") {
t.Fatal("login URL missing return_to")
}
if strings.Contains(body, "~bigbes/rfcs") {
t.Fatal("anonymous landing page leaked a space name")
}
}
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, "")
if rec.Code != http.StatusFound {
t.Fatalf("%s: status = %d, want 302\n%s", target, rec.Code, rec.Body.String())
}
loc := rec.Header().Get("Location")
if !strings.HasPrefix(loc, "https://meta.example/login?return_to=") {
t.Fatalf("%s: location = %q", target, loc)
}
if !strings.Contains(loc, "spec.example") {
t.Fatalf("%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, "<mark>authoritative</mark>") {
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())
}
}
func TestStaticLogoIsServed(t *testing.T) {
h, _ := testServer(t)
rec := get(t, h, "/static/logo.svg", "")
if rec.Code != http.StatusOK {
t.Fatalf("status = %d", rec.Code)
}
if cc := rec.Header().Get("Cache-Control"); !strings.Contains(cc, "max-age") {
t.Fatalf("cache-control = %q", cc)
}
}
// TestHashedCSSIsImmutable checks the cache policy without depending on a built
// stylesheet: `make css` needs sassc and the shared sourcehut partials, neither
// of which a test may assume.
func TestHashedCSSIsImmutable(t *testing.T) {
for _, name := range []string{"main.min.79713f25.css", "main.min.abc123.css"} {
if !hashedCSSRe.MatchString(name) {
t.Fatalf("%s should be recognised as a hashed stylesheet", name)
}
}
for _, name := range []string{"main.css", "main.min.css", "logo.svg"} {
if hashedCSSRe.MatchString(name) {
t.Fatalf("%s should not be recognised as a hashed stylesheet", name)
}
}
}
// ---- 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)
}
}
}