package web import ( "context" "crypto/rand" "errors" "fmt" "net/http" "net/http/httptest" "net/url" "os" "path/filepath" "strings" "testing" "time" "github.com/dolthub/dolt/go/libraries/doltcore/creds" "github.com/go-chi/chi/v5" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/vaughan0/go-ini" "sourcecraft.dev/bigbes/sr-ht-core/auth" "sourcecraft.dev/bigbes/sr-ht-ecore/pages" "sourcecraft.dev/bigbes/sr-ht-dolt/authn" "sourcecraft.dev/bigbes/sr-ht-dolt/browse" "sourcecraft.dev/bigbes/sr-ht-dolt/core" "sourcecraft.dev/bigbes/sr-ht-dolt/db" ) const selfOrigin = "https://dolt.example" // testConfig synthesizes a config with the origins the chrome/CSRF checks read. func testConfig() ini.File { return ini.File{ "sr.ht": ini.Section{ "environment": "development", "site-name": "sr.ht", "owner-name": "admin", "owner-email": "admin@example.com", }, "dolt.sr.ht": ini.Section{"origin": selfOrigin}, "meta.sr.ht": ini.Section{"origin": "https://meta.example"}, "git.sr.ht": ini.Section{"origin": "https://git.example"}, "todo.sr.ht": ini.Section{"origin": "https://todo.example"}, "paste.sr.ht": ini.Section{"origin": "https://paste.example"}, } } // --- fakes ------------------------------------------------------------------- type fakeStore struct { repos map[string]*core.Repo // key "owner/name" byID map[int]*core.Repo acls map[int]map[int]core.AccessMode // repoID -> userID -> mode keys map[int][]*db.DoltKey // userID -> keys nextID int nextKeyID int createErr error createdCalls []*core.Repo deletedRepos []int } func newFakeStore() *fakeStore { return &fakeStore{ repos: map[string]*core.Repo{}, byID: map[int]*core.Repo{}, acls: map[int]map[int]core.AccessMode{}, keys: map[int][]*db.DoltKey{}, nextID: 1, nextKeyID: 1, } } func (f *fakeStore) add(r *core.Repo) *core.Repo { r.ID = f.nextID f.nextID++ f.repos[r.OwnerName+"/"+r.Name] = r f.byID[r.ID] = r return r } func (f *fakeStore) CreateRepo(_ context.Context, r *core.Repo) (*core.Repo, error) { if f.createErr != nil { return nil, f.createErr } if _, ok := f.repos[r.OwnerName+"/"+r.Name]; ok { return nil, db.ErrNameTaken } cp := *r out := f.add(&cp) f.createdCalls = append(f.createdCalls, out) return out, nil } func (f *fakeStore) GetRepoByOwnerAndName(_ context.Context, owner, name string) (*core.Repo, error) { r, ok := f.repos[owner+"/"+name] if !ok { return nil, db.ErrNotFound } return r, nil } func (f *fakeStore) ListReposByOwner(_ context.Context, owner string, viewer *core.Caller) ([]*core.Repo, error) { var out []*core.Repo for _, r := range f.repos { if r.OwnerName != owner { continue } visible := r.Visibility == core.VisibilityPublic if viewer != nil && (viewer.UserID == r.OwnerID || f.hasACL(r.ID, viewer.UserID)) { visible = true } if visible { out = append(out, r) } } return out, nil } func (f *fakeStore) ListReposForDashboard(_ context.Context, userID int) ([]*core.Repo, error) { var out []*core.Repo for _, r := range f.byID { if r.OwnerID == userID || f.hasACL(r.ID, userID) { out = append(out, r) } } return out, nil } func (f *fakeStore) UpdateRepo(_ context.Context, id int, description string, visibility core.Visibility) error { r, ok := f.byID[id] if !ok { return db.ErrNotFound } r.Description = description r.Visibility = visibility return nil } func (f *fakeStore) DeleteRepo(_ context.Context, id int) error { r, ok := f.byID[id] if !ok { return db.ErrNotFound } delete(f.byID, id) delete(f.repos, r.OwnerName+"/"+r.Name) f.deletedRepos = append(f.deletedRepos, id) return nil } func (f *fakeStore) hasACL(repoID, userID int) bool { m, ok := f.acls[repoID] if !ok { return false } _, ok = m[userID] return ok } func (f *fakeStore) EffectiveAccess(_ context.Context, userID, repoID int) (*core.AccessMode, error) { m, ok := f.acls[repoID] if !ok { return nil, nil } mode, ok := m[userID] if !ok { return nil, nil } return &mode, nil } func (f *fakeStore) ListACL(_ context.Context, repoID int) ([]*db.ACLEntry, error) { var out []*db.ACLEntry for uid, mode := range f.acls[repoID] { out = append(out, &db.ACLEntry{RepoID: repoID, UserID: uid, Username: fmt.Sprintf("user%d", uid), Mode: mode}) } return out, nil } func (f *fakeStore) UpsertACL(_ context.Context, repoID, userID int, mode core.AccessMode) error { if f.acls[repoID] == nil { f.acls[repoID] = map[int]core.AccessMode{} } f.acls[repoID][userID] = mode return nil } func (f *fakeStore) DeleteACL(_ context.Context, repoID, userID int) error { if !f.hasACL(repoID, userID) { return db.ErrNotFound } delete(f.acls[repoID], userID) return nil } func (f *fakeStore) InsertKey(_ context.Context, userID int, kid string, pubkey []byte, comment string) (*db.DoltKey, error) { for _, ks := range f.keys { for _, k := range ks { if k.KID == kid { return nil, db.ErrKeyExists } } } k := &db.DoltKey{ID: f.nextKeyID, UserID: userID, KID: kid, PubKey: pubkey, Comment: comment, Created: time.Now()} f.nextKeyID++ f.keys[userID] = append(f.keys[userID], k) return k, nil } func (f *fakeStore) ListKeysByUser(_ context.Context, userID int) ([]*db.DoltKey, error) { return f.keys[userID], nil } func (f *fakeStore) DeleteKey(_ context.Context, id, userID int) error { ks := f.keys[userID] for i, k := range ks { if k.ID == id { f.keys[userID] = append(ks[:i], ks[i+1:]...) return nil } } return db.ErrNotFound } type fakeStoreManager struct { initErr error initCalls []string deleteCalls []string evictCalls []string } func (m *fakeStoreManager) InitStore(_ context.Context, absPath, _, _ string) error { m.initCalls = append(m.initCalls, absPath) return m.initErr } func (m *fakeStoreManager) DeleteStore(_ context.Context, _, absPath string) error { m.deleteCalls = append(m.deleteCalls, absPath) return nil } func (m *fakeStoreManager) Evict(diskPath string) error { m.evictCalls = append(m.evictCalls, diskPath) return nil } type fakeSession struct { branches []browse.Branch commits []browse.CommitInfo tables []browse.TableInfo rows *browse.RowPage // rowsByTable, when set, lets a test return a distinct page per table name // (as the beads view needs). A named miss falls back to rows. A table absent // from a non-nil map is reported as ErrTableNotFound, mirroring the store. rowsByTable map[string]*browse.RowPage summary *browse.CommitDiff closed bool } func (s *fakeSession) Branches(context.Context) ([]browse.Branch, error) { return s.branches, nil } func (s *fakeSession) Log(_ context.Context, _, _ string, _ int) ([]browse.CommitInfo, string, error) { return s.commits, "", nil } func (s *fakeSession) Tables(_ context.Context, _ string) ([]browse.TableInfo, error) { return s.tables, nil } func (s *fakeSession) Rows(_ context.Context, _, table string, _, _ int) (*browse.RowPage, error) { if s.rowsByTable != nil { if p, ok := s.rowsByTable[table]; ok { return p, nil } return nil, fmt.Errorf("%w: %s", browse.ErrTableNotFound, table) } return s.rows, nil } func (s *fakeSession) CommitSummary(_ context.Context, _ string) (*browse.CommitDiff, error) { return s.summary, nil } func (s *fakeSession) Close() error { s.closed = true; return nil } type fakeBrowse struct{ sess *fakeSession } func (b *fakeBrowse) Open(context.Context, string) (BrowseSession, error) { if b.sess == nil { return &fakeSession{}, nil } return b.sess, nil } type fakeUsers struct { byName map[string]*core.Caller } func (u *fakeUsers) LookupUser(_ context.Context, username string) (*core.Caller, error) { c, ok := u.byName[username] if !ok { return nil, errors.New("no such user") } return c, nil } // --- harness ----------------------------------------------------------------- type harness struct { router chi.Router app *app store *fakeStore stores *fakeStoreManager browse *fakeBrowse users *fakeUsers } func newHarness(t *testing.T) *harness { t.Helper() return newHarnessWithStatic(t, "") } // newHarnessWithStatic is newHarness for the tests that need a real static // tree on disk — the asset routes, and the stylesheet the layout links. func newHarnessWithStatic(t *testing.T, staticDir string) *harness { t.Helper() store := newFakeStore() stores := &fakeStoreManager{} fb := &fakeBrowse{} users := &fakeUsers{byName: map[string]*core.Caller{}} cfg := Config{ Conf: testConfig(), ReposRoot: "/var/lib/dolt", StaticDir: staticDir, Stores: stores, Repos: store, Browse: fb, Users: users, RepoDiskPath: func(owner, name string) string { return "/var/lib/dolt/~" + owner + "/" + name }, } r := chi.NewRouter() a, err := newApp(cfg) if err != nil { t.Fatalf("newApp: %v", err) } a.mount(r) return &harness{router: r, app: a, store: store, stores: stores, browse: fb, users: users} } // do issues a request through the router, optionally with an authenticated // caller injected into the context (as OptionalCookieMiddleware would). func (h *harness) do(method, target string, caller *auth.AuthContext, form url.Values) *httptest.ResponseRecorder { var req *http.Request if form != nil { req = httptest.NewRequest(method, target, strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Origin", selfOrigin) // same-origin by default } else { req = httptest.NewRequest(method, target, nil) } if caller != nil { req = req.WithContext(authn.WithCaller(req.Context(), caller)) } rec := httptest.NewRecorder() h.router.ServeHTTP(rec, req) return rec } func testCaller(id int, name string) *auth.AuthContext { return &auth.AuthContext{UserID: id, Username: name, UserType: auth.USER_TYPE_USER, Email: name + "@example.com"} } // validDoltPubKeyStr returns a 52-char base32 dolt public key (over 32 random // bytes) in dolt's custom alphabet, exactly the shape `dolt login` emits. func validDoltPubKeyStr(t *testing.T) string { t.Helper() pub := make([]byte, ed25519PubKeyLen) if _, err := rand.Read(pub); err != nil { t.Fatalf("rand: %v", err) } return creds.B32CredsEncoding.EncodeToString(pub) } // --- tests ------------------------------------------------------------------- func TestOverviewAnonymousPublicPrivate(t *testing.T) { h := newHarness(t) h.store.add(&core.Repo{Name: "pub", OwnerID: 1, OwnerName: "alice", Path: "/p", Visibility: core.VisibilityPublic}) h.store.add(&core.Repo{Name: "sec", OwnerID: 1, OwnerName: "alice", Path: "/s", Visibility: core.VisibilityPrivate}) if rec := h.do("GET", "/~alice/pub", nil, nil); rec.Code != http.StatusOK { t.Fatalf("public overview: got %d, want 200", rec.Code) } rec := h.do("GET", "/~alice/sec", nil, nil) if rec.Code != http.StatusNotFound { t.Fatalf("private overview anon: got %d, want 404", rec.Code) } if strings.Contains(rec.Body.String(), "sec") && strings.Contains(rec.Body.String(), "clone") { t.Fatalf("private repo leaked details to anonymous") } } func TestPrivateVisibleToOwner(t *testing.T) { h := newHarness(t) h.store.add(&core.Repo{Name: "sec", OwnerID: 7, OwnerName: "alice", Path: "/s", Visibility: core.VisibilityPrivate}) rec := h.do("GET", "/~alice/sec", testCaller(7, "alice"), nil) if rec.Code != http.StatusOK { t.Fatalf("owner private overview: got %d, want 200", rec.Code) } } func TestDashboardLists(t *testing.T) { h := newHarness(t) h.store.add(&core.Repo{Name: "mine", OwnerID: 3, OwnerName: "bob", Path: "/m", Visibility: core.VisibilityPrivate}) rec := h.do("GET", "/", testCaller(3, "bob"), nil) if rec.Code != http.StatusOK { t.Fatalf("dashboard: got %d", rec.Code) } if !strings.Contains(rec.Body.String(), "~bob/mine") { t.Fatalf("dashboard missing owned repo; body=%s", rec.Body.String()) } if !strings.Contains(rec.Body.String(), "/create") { t.Fatalf("dashboard missing create link") } // Anonymous dashboard shows the blurb, not the list. anon := h.do("GET", "/", nil, nil) if !strings.Contains(anon.Body.String(), "Log in") { t.Fatalf("anon dashboard missing login blurb") } } func TestCreateValidationAndSuccess(t *testing.T) { h := newHarness(t) caller := testCaller(5, "carol") // Anonymous create is redirected to login. if rec := h.do("GET", "/create", nil, nil); rec.Code != http.StatusSeeOther { t.Fatalf("anon create form: got %d, want 303", rec.Code) } // Invalid name. bad := h.do("POST", "/create", caller, url.Values{"name": {"bad name!"}, "visibility": {"PUBLIC"}}) if bad.Code != http.StatusBadRequest { t.Fatalf("invalid name: got %d, want 400", bad.Code) } // Success. ok := h.do("POST", "/create", caller, url.Values{"name": {"gooddb"}, "visibility": {"PUBLIC"}, "description": {"hi"}}) if ok.Code != http.StatusSeeOther { t.Fatalf("create success: got %d, want 303; body=%s", ok.Code, ok.Body.String()) } if got := ok.Header().Get("Location"); got != "/~carol/gooddb" { t.Fatalf("create redirect: got %q", got) } if len(h.stores.initCalls) != 1 || h.stores.initCalls[0] != "/var/lib/dolt/~carol/gooddb" { t.Fatalf("InitStore not called correctly: %v", h.stores.initCalls) } if _, err := h.store.GetRepoByOwnerAndName(context.Background(), "carol", "gooddb"); err != nil { t.Fatalf("repo row not created: %v", err) } } func TestCreateStoreFailureRollsBackRow(t *testing.T) { h := newHarness(t) h.stores.initErr = errors.New("disk full") caller := testCaller(5, "carol") rec := h.do("POST", "/create", caller, url.Values{"name": {"gooddb"}, "visibility": {"PUBLIC"}}) if rec.Code != http.StatusInternalServerError { t.Fatalf("create with store failure: got %d, want 500", rec.Code) } if _, err := h.store.GetRepoByOwnerAndName(context.Background(), "carol", "gooddb"); !errors.Is(err, db.ErrNotFound) { t.Fatalf("orphan repo row survived store failure: %v", err) } if len(h.store.deletedRepos) != 1 { t.Fatalf("row not rolled back: %v", h.store.deletedRepos) } } func TestCreateCSRFRejected(t *testing.T) { h := newHarness(t) caller := testCaller(5, "carol") req := httptest.NewRequest("POST", "/create", strings.NewReader(url.Values{"name": {"x"}, "visibility": {"PUBLIC"}}.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Origin", "https://evil.example") req = req.WithContext(authn.WithCaller(req.Context(), caller)) rec := httptest.NewRecorder() h.router.ServeHTTP(rec, req) if rec.Code != http.StatusForbidden { t.Fatalf("cross-origin create: got %d, want 403", rec.Code) } } func TestSettingsOwnerGate(t *testing.T) { h := newHarness(t) h.store.add(&core.Repo{Name: "db", OwnerID: 10, OwnerName: "owner", Path: "/d", Visibility: core.VisibilityPublic}) // Anonymous → login redirect. if rec := h.do("GET", "/~owner/db/settings", nil, nil); rec.Code != http.StatusSeeOther { t.Fatalf("anon settings: got %d, want 303", rec.Code) } // Non-owner on a PUBLIC repo → 403. if rec := h.do("GET", "/~owner/db/settings", testCaller(99, "intruder"), nil); rec.Code != http.StatusForbidden { t.Fatalf("non-owner settings: got %d, want 403", rec.Code) } // Owner → 200. if rec := h.do("GET", "/~owner/db/settings", testCaller(10, "owner"), nil); rec.Code != http.StatusOK { t.Fatalf("owner settings: got %d, want 200", rec.Code) } } func TestSettingsNonOwnerPrivateIs404(t *testing.T) { h := newHarness(t) h.store.add(&core.Repo{Name: "db", OwnerID: 10, OwnerName: "owner", Path: "/d", Visibility: core.VisibilityPrivate}) rec := h.do("GET", "/~owner/db/settings", testCaller(99, "intruder"), nil) if rec.Code != http.StatusNotFound { t.Fatalf("non-owner private settings: got %d, want 404", rec.Code) } } func TestSettingsUpdateAndDelete(t *testing.T) { h := newHarness(t) repo := h.store.add(&core.Repo{Name: "db", OwnerID: 10, OwnerName: "owner", Path: "/var/lib/dolt/~owner/db", Visibility: core.VisibilityPublic}) owner := testCaller(10, "owner") upd := h.do("POST", "/~owner/db/settings", owner, url.Values{"action": {"update"}, "description": {"new desc"}, "visibility": {"PRIVATE"}}) if upd.Code != http.StatusOK { t.Fatalf("update: got %d", upd.Code) } if repo.Description != "new desc" || repo.Visibility != core.VisibilityPrivate { t.Fatalf("update not applied: %+v", repo) } // Delete requires a matching name confirmation. badDel := h.do("POST", "/~owner/db/settings", owner, url.Values{"action": {"delete"}, "confirm_name": {"wrong"}}) if badDel.Code != http.StatusBadRequest { t.Fatalf("delete wrong confirm: got %d, want 400", badDel.Code) } del := h.do("POST", "/~owner/db/settings", owner, url.Values{"action": {"delete"}, "confirm_name": {"db"}}) if del.Code != http.StatusSeeOther { t.Fatalf("delete: got %d, want 303", del.Code) } if len(h.stores.deleteCalls) != 1 || len(h.stores.evictCalls) != 1 { t.Fatalf("store delete/evict not called: del=%v evict=%v", h.stores.deleteCalls, h.stores.evictCalls) } } func TestSettingsACLAddRemove(t *testing.T) { h := newHarness(t) repo := h.store.add(&core.Repo{Name: "db", OwnerID: 10, OwnerName: "owner", Path: "/d", Visibility: core.VisibilityPublic}) h.users.byName["dave"] = &core.Caller{UserID: 42, Username: "dave"} owner := testCaller(10, "owner") add := h.do("POST", "/~owner/db/settings", owner, url.Values{"action": {"acl_add"}, "username": {"dave"}, "mode": {"RW"}}) if add.Code != http.StatusOK { t.Fatalf("acl add: got %d; body=%s", add.Code, add.Body.String()) } if !h.store.hasACL(repo.ID, 42) { t.Fatalf("acl not added") } // Unknown user rejected. if bad := h.do("POST", "/~owner/db/settings", owner, url.Values{"action": {"acl_add"}, "username": {"ghost"}, "mode": {"RO"}}); bad.Code != http.StatusBadRequest { t.Fatalf("acl add unknown user: got %d, want 400", bad.Code) } rm := h.do("POST", "/~owner/db/settings", owner, url.Values{"action": {"acl_remove"}, "user_id": {"42"}}) if rm.Code != http.StatusOK { t.Fatalf("acl remove: got %d", rm.Code) } if h.store.hasACL(repo.ID, 42) { t.Fatalf("acl not removed") } } func TestKeysAddDeleteAndFragmentPage(t *testing.T) { h := newHarness(t) caller := testCaller(11, "keyuser") // The page renders and contains the hash-fragment JS. page := h.do("GET", "/settings/keys", caller, nil) if page.Code != http.StatusOK { t.Fatalf("keys page: got %d", page.Code) } if !strings.Contains(page.Body.String(), "window.location.hash") { t.Fatalf("keys page missing hash-fragment JS") } // Add a key using a valid dolt base32 public key. pub := validDoltPubKeyStr(t) add := h.do("POST", "/settings/keys", caller, url.Values{"pubkey": {pub}, "comment": {"laptop"}}) if add.Code != http.StatusOK { t.Fatalf("key add: got %d; body=%s", add.Code, add.Body.String()) } keys, _ := h.store.ListKeysByUser(context.Background(), 11) if len(keys) != 1 { t.Fatalf("key not stored: %d", len(keys)) } // Invalid key rejected. if bad := h.do("POST", "/settings/keys", caller, url.Values{"pubkey": {"not-base32-!!"}}); bad.Code != http.StatusBadRequest { t.Fatalf("invalid key: got %d, want 400", bad.Code) } // Delete. del := h.do("POST", "/settings/keys", caller, url.Values{"delete_id": {fmt.Sprint(keys[0].ID)}}) if del.Code != http.StatusOK { t.Fatalf("key delete: got %d", del.Code) } if ks, _ := h.store.ListKeysByUser(context.Background(), 11); len(ks) != 0 { t.Fatalf("key not deleted") } } // What the nav contains — which services appear, in what order, which one is // marked active, where the login link points — is sr-ht-ecore's chrome and is // tested there. What is ours is that every page is drawn through it at all, and // that the one page we ask to be full-bleed gets its own wrapper. func TestPagesAreDrawnThroughTheSharedChrome(t *testing.T) { h := newHarness(t) h.store.add(&core.Repo{Name: "db", OwnerID: 1, OwnerName: "alice", Path: "/d", Visibility: core.VisibilityPublic}) h.browse.sess = &fakeSession{ branches: []browse.Branch{{Name: "main", Head: "abcdef1234567890"}}, rows: &browse.RowPage{Columns: []string{"id"}, Rows: [][]string{{"1"}}, Total: 1}, } dash := h.do("GET", "/", testCaller(1, "someone"), nil) require.Equal(t, http.StatusOK, dash.Code) body := dash.Body.String() // The switcher and the brand come from the shared partials; the dolt label // is the red service suffix chrome derives from our config section. assert.Contains(t, body, "https://git.example", "shared switcher not rendered") assert.Contains(t, body, `dolt`, "brand label not rendered") // The default page width, which every page but the row browser keeps. assert.Contains(t, body, `
`) rows := h.do("GET", "/~alice/db/table/main/things", nil, nil) require.Equal(t, http.StatusOK, rows.Code) assert.Contains(t, rows.Body.String(), `
`, "the row browser must be full-bleed") } // The 404 and 403 templates this service carried are sr-ht-ecore's error page // now: one body, drawn through our own chrome, with the shared sentence. The // wording matters here — a 404 that described the missing thing would tell an // anonymous viewer which private databases exist. func TestRefusalsRenderTheSharedErrorPage(t *testing.T) { h := newHarness(t) h.store.add(&core.Repo{Name: "db", OwnerID: 10, OwnerName: "owner", Path: "/d", Visibility: core.VisibilityPublic}) missing := h.do("GET", "/~owner/nosuch", nil, nil) require.Equal(t, http.StatusNotFound, missing.Code) assert.Contains(t, missing.Body.String(), pages.NotFoundMessage) assert.Contains(t, missing.Body.String(), "404 — Not Found") assert.Contains(t, missing.Body.String(), `dolt`, "the error page is drawn through our chrome") denied := h.do("GET", "/~owner/db/settings", testCaller(99, "intruder"), nil) require.Equal(t, http.StatusForbidden, denied.Code) assert.Contains(t, denied.Body.String(), "Only the owner may change database settings.") } // leakyView renders a page whose content block reads a field its envelope does // not carry, so executing it fails halfway. It is the shape of the bug the old // renderer turned into a disclosure. type leakyView struct{} func (leakyView) Name() string { return "leaky" } func (leakyView) Label() string { return "Leaky" } func (leakyView) Template() string { return "keys.html" } func (leakyView) Applies([]browse.TableInfo) bool { return true } func (leakyView) Build(context.Context, BrowseSession, *core.Repo, string, url.Values) (any, error) { return nil, nil } // A template that fails halfway answers a fixed sentence. The previous renderer // wrote "template render error: "+err.Error() into the body, which hands the // viewer the template's name and the field path that was not there; the error // belongs in the log and nowhere else. func TestATemplateFailureTellsTheViewerNothing(t *testing.T) { h := newHarness(t) h.store.add(&core.Repo{Name: "db", OwnerID: 1, OwnerName: "alice", Path: "/d", Visibility: core.VisibilityPublic}) h.browse.sess = &fakeSession{ branches: []browse.Branch{{Name: "main", Head: "abcdef1234567890"}}, tables: issueTables(), } setViews(t, h, leakyView{}) rec := h.do("GET", "/~alice/db/view/leaky", nil, nil) require.Equal(t, http.StatusInternalServerError, rec.Code) body := rec.Body.String() assert.Equal(t, "internal server error\n", body) assert.NotContains(t, body, "keys.html", "the template name must not reach the viewer") assert.NotContains(t, body, "Keys", "the field path must not reach the viewer") assert.NotContains(t, body, "can't evaluate") } // The static tree is served by sr-ht-ecore's assets handler, which is tested // there. What is ours is that we mounted it: that the hashed stylesheet this // build produced is the one the layout links, that a name whose bytes cannot // change under it is cacheable and one whose bytes can is not, and that // /static/ answers a page rather than an inventory of the build. func TestStaticTreeIsServedWithACachePolicyAndNoListing(t *testing.T) { dir := t.TempDir() require.NoError(t, os.WriteFile(filepath.Join(dir, "main.min.0badc0de.css"), []byte("body{}"), 0o644)) require.NoError(t, os.WriteFile(filepath.Join(dir, "logo.svg"), []byte(""), 0o644)) h := newHarnessWithStatic(t, dir) hashed := h.do("GET", "/static/main.min.0badc0de.css", nil, nil) require.Equal(t, http.StatusOK, hashed.Code) assert.Equal(t, "public, max-age=31536000, immutable", hashed.Header().Get("Cache-Control")) assert.Empty(t, hashed.Header().Get("Vary"), "an immutable asset must not vary on the cookie") unhashed := h.do("GET", "/static/logo.svg", nil, nil) require.Equal(t, http.StatusOK, unhashed.Code) assert.Equal(t, "public, max-age=3600", unhashed.Header().Get("Cache-Control")) listing := h.do("GET", "/static/", nil, nil) assert.Equal(t, http.StatusNotFound, listing.Code, "the static tree must not publish a listing") assert.NotContains(t, listing.Body.String(), `x"}}, Total: 1}, } logRec := h.do("GET", "/~alice/db/log", nil, nil) if logRec.Code != http.StatusOK { t.Fatalf("log page: got %d", logRec.Code) } if !strings.Contains(logRec.Body.String(), "abcdef12") || !strings.Contains(logRec.Body.String(), "hours ago") { t.Fatalf("log page missing short hash / reltime; body=%s", logRec.Body.String()) } tblRec := h.do("GET", "/~alice/db/table/main/things", nil, nil) if tblRec.Code != http.StatusOK { t.Fatalf("table page: got %d", tblRec.Code) } // html/template must escape the cell content. if strings.Contains(tblRec.Body.String(), "x") { t.Fatalf("table cell not HTML-escaped") } if !strings.Contains(tblRec.Body.String(), "<b>x</b>") { t.Fatalf("table cell escaping wrong; body=%s", tblRec.Body.String()) } }