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" "sourcecraft.dev/bigbes/sr-ht-core/auth" "sourcecraft.dev/bigbes/sr-ht-ecore/csrf" "sourcecraft.dev/bigbes/sr-ht-ecore/ecoretest" "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" ) // selfOrigin is what the synthetic instance config gives this service, and so // what the chrome and the same-origin guard read as ours. var selfOrigin = ecoretest.Origin(serviceName) // --- 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: ecoretest.Config(serviceName), 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) } } // The same-origin guard is sr-ht-ecore's csrf middleware on the browser group, // and no longer three per-handler calls. What is ours to test is that it is // mounted over every mutating route — including the one whose handler used to // carry the check and now does not — and that a request refusing to say where // it came from is refused rather than waved through. func TestMutationsAreRefusedWithoutSameOriginEvidence(t *testing.T) { h := newHarness(t) h.store.add(&core.Repo{Name: "db", OwnerID: 10, OwnerName: "owner", Path: "/d", Visibility: core.VisibilityPublic}) caller := testCaller(10, "owner") post := func(target, origin string, form url.Values) *httptest.ResponseRecorder { req := httptest.NewRequest("POST", target, strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") if origin != "" { req.Header.Set("Origin", origin) } req = req.WithContext(authn.WithCaller(req.Context(), caller)) rec := httptest.NewRecorder() h.router.ServeHTTP(rec, req) return rec } for _, tc := range []struct{ name, target string }{ {"create", "/create"}, {"keys", "/settings/keys"}, {"settings", "/~owner/db/settings"}, } { t.Run(tc.name+" cross-origin", func(t *testing.T) { rec := post(tc.target, "https://evil.example", url.Values{"name": {"x"}, "visibility": {"PUBLIC"}}) require.Equal(t, http.StatusForbidden, rec.Code) assert.Contains(t, rec.Body.String(), csrf.Message) }) t.Run(tc.name+" no headers", func(t *testing.T) { // Neither Origin nor Referer: a request that will not say where it // came from cannot be shown to have come from us. rec := post(tc.target, "", url.Values{"name": {"x"}, "visibility": {"PUBLIC"}}) assert.Equal(t, http.StatusForbidden, rec.Code) }) } } // Nothing behind the login cookie may be reused for the next viewer: these URLs // say nothing about who the page was rendered for. func TestPagesAreNotCacheable(t *testing.T) { h := newHarness(t) rec := h.do("GET", "/", testCaller(1, "alice"), nil) require.Equal(t, http.StatusOK, rec.Code) assert.Equal(t, "private, no-store", rec.Header().Get("Cache-Control")) assert.Equal(t, "Cookie, Authorization", rec.Header().Get("Vary")) } // panicView is a View whose Build panics, which is the only route into a // handler panic this package can reach from a test. type panicView struct{} func (panicView) Name() string { return "boom" } func (panicView) Label() string { return "Boom" } func (panicView) Template() string { return pages.ErrorPage + ".html" } func (panicView) Applies([]browse.TableInfo) bool { return true } func (panicView) Build(context.Context, BrowseSession, *core.Repo, string, url.Values) (any, error) { panic("the store went away") } // A panic before the response has started is answered with the error page every // other bug gets, and the panic value stays in the log. func TestAPanicBecomesTheErrorPage(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, panicView{}) rec := h.do("GET", "/~alice/db/view/boom", nil, nil) require.Equal(t, http.StatusInternalServerError, rec.Code) assert.Contains(t, rec.Body.String(), pages.InternalMessage) assert.NotContains(t, rec.Body.String(), "the store went away") } 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, `