package mcpsrv_test import ( "context" "encoding/json" "errors" "fmt" "testing" "time" "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "sourcecraft.dev/bigbes/sr-ht-core/auth" "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" "sourcecraft.dev/bigbes/sr-ht-dolt/mcpsrv" ) // The tool half of the suite: what the surface answers, driven through a real // in-process MCP client over fakes — no Postgres, no store on disk. The // transport half (who the answer is computed for, and what a request refused // before a handler looks like) is http_test.go. // testOrigin is what [dolt.sr.ht]origin says on the instance these tests // pretend to be, and therefore the Host the allowlist admits. const testOrigin = "https://dolt.example.org" // --- the fixture ------------------------------------------------------------ // // One owner with one database of every shape that matters, because the two // things this surface has to get right are both properties of a *set*: which // databases a caller may list, and what a listing says about one it cannot fully // read. const ( aliceID = 1 // the owner of every fixture database bobID = 2 // a grantee: an ACL entry on the private one and nothing else carolID = 3 // a stranger: an account with no relationship to any of them ) // storePath is the on-disk store dir of a database, as db/ stores it on the // row. It never leaves the server (nothing in a tool result carries it), which // is why the fakes key on it rather than on a name. func storePath(owner, name string) string { return "/stores/~" + owner + "/" + name } type fixture struct { name string visibility core.Visibility acl map[int]core.AccessMode // session is what opening this database's store yields. A nil one is a store // that will not open at all, which is the entry a listing has to survive. session *fakeSession } func fixtures() []fixture { return []fixture{ { name: "notes", visibility: core.VisibilityPublic, session: &fakeSession{ branches: []browse.Branch{{Name: "main", Head: "aaaa"}}, commits: []browse.CommitInfo{{Hash: "aaaa", Date: headTime}}, tables: []browse.TableInfo{{Name: "notes", Columns: []browse.ColumnInfo{{Name: "id"}}}}, }, }, { // The one a beads-aware tool will answer about: its tables carry the // fingerprint beads.Applies looks for. name: "tracker", visibility: core.VisibilityPublic, session: &fakeSession{ branches: []browse.Branch{{Name: "main", Head: "bbbb"}}, commits: []browse.CommitInfo{{Hash: "bbbb", Date: headTime}}, tables: beadsTables(), }, }, { // A store created by a repository that has never been pushed to. name: "empty", visibility: core.VisibilityPublic, session: &fakeSession{}, }, { // A store this daemon cannot open. It is in the fixture rather than in // one test of its own because the property worth pinning is that it // does not take the rest of the listing with it. name: "broken", visibility: core.VisibilityPublic, session: nil, }, { name: "drafts", visibility: core.VisibilityUnlisted, session: &fakeSession{ branches: []browse.Branch{{Name: "main", Head: "dddd"}}, commits: []browse.CommitInfo{{Hash: "dddd", Date: headTime}}, tables: []browse.TableInfo{{Name: "drafts"}}, }, }, { name: "secrets", visibility: core.VisibilityPrivate, acl: map[int]core.AccessMode{bobID: core.AccessRO}, session: &fakeSession{ branches: []browse.Branch{{Name: "release", Head: "eeee"}}, commits: []browse.CommitInfo{{Hash: "eeee", Date: headTime}}, tables: []browse.TableInfo{{Name: "secrets"}}, }, }, } } // headTime is the commit time every fixture head carries, fixed so that a test // can assert the value rather than merely that something was rendered. var headTime = time.Date(2026, 8, 1, 12, 0, 0, 0, time.UTC) // beadsTables is the minimum beads.Applies accepts: issues + dependencies, with // issues carrying id and status. The fingerprint is beads/'s and is not restated // here — this is a fixture that satisfies it, not a second copy of it. func beadsTables() []browse.TableInfo { return []browse.TableInfo{ {Name: "issues", Columns: []browse.ColumnInfo{{Name: "id"}, {Name: "status"}, {Name: "title"}}}, {Name: "dependencies", Columns: []browse.ColumnInfo{{Name: "from_id"}, {Name: "to_id"}}}, } } // listable is the listing rule of docs/DESIGN.mcp.md §4.3 spelled out // independently of the implementation under test: PUBLIC to everyone including // anonymity, plus whatever the viewer owns or holds an ACL entry on. UNLISTED // and PRIVATE are absent for everybody else. // // It is written here rather than derived from the fake so that the expectation // and the fake cannot drift into agreement with each other and away from the // rule. func listable(f fixture, viewer *core.Caller) bool { if f.visibility == core.VisibilityPublic { return true } if viewer == nil { return false } if viewer.UserID == aliceID { return true // alice owns every fixture } _, ok := f.acl[viewer.UserID] return ok } func listableNames(viewer *core.Caller) []string { var out []string for _, f := range fixtures() { if listable(f, viewer) { out = append(out, f.name) } } return out } // --- the fakes -------------------------------------------------------------- // fakeRepos is the metadata store: the fixture rows, plus the two listing // queries db/repos.go actually has and the ACL lookup. It applies db/'s SQL as // Go, and nothing else — no visibility rule beyond the one ListReposByOwner // documents, so a surface that leaned on the store to hide something would fail // here rather than pass by accident. type fakeRepos struct { repos []*core.Repo acl map[int]map[int]core.AccessMode // repoID -> userID -> mode // listErr, when set, is what both listings answer: a metadata store that // could not be read. listErr error } var _ mcpsrv.Repos = (*fakeRepos)(nil) func newFakeRepos() *fakeRepos { f := &fakeRepos{acl: map[int]map[int]core.AccessMode{}} for i, fx := range fixtures() { id := i + 1 f.repos = append(f.repos, &core.Repo{ ID: id, Name: fx.name, Description: "the " + fx.name + " database", OwnerID: aliceID, OwnerName: "alice", Path: storePath("alice", fx.name), Visibility: fx.visibility, }) for userID, mode := range fx.acl { if f.acl[id] == nil { f.acl[id] = map[int]core.AccessMode{} } f.acl[id][userID] = mode } } return f } func (f *fakeRepos) GetRepoByOwnerAndName(_ context.Context, owner, name string) (*core.Repo, error) { for _, r := range f.repos { if r.OwnerName == owner && r.Name == name { return r, nil } } return nil, db.ErrNotFound } func (f *fakeRepos) ListReposByOwner(_ context.Context, owner string, viewer *core.Caller) ([]*core.Repo, error) { if f.listErr != nil { return nil, f.listErr } 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 *fakeRepos) ListReposForDashboard(_ context.Context, userID int) ([]*core.Repo, error) { if f.listErr != nil { return nil, f.listErr } var out []*core.Repo for _, r := range f.repos { if r.OwnerID == userID || f.hasACL(r.ID, userID) { out = append(out, r) } } return out, nil } func (f *fakeRepos) EffectiveAccess(_ context.Context, userID, repoID int) (*core.AccessMode, error) { mode, ok := f.acl[repoID][userID] if !ok { return nil, nil } return &mode, nil } func (f *fakeRepos) hasACL(repoID, userID int) bool { _, ok := f.acl[repoID][userID] return ok } // fakeOpener resolves a store path to its fixture session. A path with no // session is a store that will not open, which is how the "broken" fixture // behaves. type fakeOpener struct { sessions map[string]*fakeSession opened []string } var _ mcpsrv.BrowseOpener = (*fakeOpener)(nil) func newFakeOpener() *fakeOpener { o := &fakeOpener{sessions: map[string]*fakeSession{}} for _, fx := range fixtures() { if fx.session != nil { o.sessions[storePath("alice", fx.name)] = fx.session } } return o } func (o *fakeOpener) Open(_ context.Context, diskPath string) (mcpsrv.BrowseSession, error) { o.opened = append(o.opened, diskPath) sess, ok := o.sessions[diskPath] if !ok { return nil, fmt.Errorf("no store at %s", diskPath) } return sess, nil } // fakeSession is one bare store as browse/ reads it. Only the three methods // list_databases calls answer; the rest are the seam's, waiting for the tools of // ch. 9.1, and a handler that reached one here would fail loudly rather than // read an empty result. type fakeSession struct { branches []browse.Branch commits []browse.CommitInfo tables []browse.TableInfo logErr error tablesErr error closes int } var _ mcpsrv.BrowseSession = (*fakeSession)(nil) 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) { if s.logErr != nil { return nil, "", s.logErr } return s.commits, "", nil } func (s *fakeSession) Tables(context.Context, string) ([]browse.TableInfo, error) { if s.tablesErr != nil { return nil, s.tablesErr } return s.tables, nil } func (s *fakeSession) TableHash(context.Context, string, string) (string, bool, error) { panic("TableHash: no tool of this phase reads a table hash") } func (s *fakeSession) Rows(context.Context, string, string, int, int) (*browse.RowPage, error) { panic("Rows: no tool of this phase reads rows") } func (s *fakeSession) CommitSummary(context.Context, string) (*browse.CommitDiff, error) { panic("CommitSummary: no tool of this phase reads a diff") } func (s *fakeSession) Close() error { s.closes++ return nil } // --- callers ---------------------------------------------------------------- // The three principals of the visibility matrix, as the *auth.AuthContext every // plane of this service produces. Anonymous is a nil one, and it is a caller // like any other. func alice() *auth.AuthContext { return user(aliceID, "alice") } func bob() *auth.AuthContext { return user(bobID, "bob") } func carol() *auth.AuthContext { return user(carolID, "carol") } func user(id int, name string) *auth.AuthContext { return &auth.AuthContext{UserID: id, Username: name, UserType: auth.USER_TYPE_USER} } // authnContext is a context carrying ac as the resolved caller, exactly as the // credential middleware leaves it — authn.WithCaller stores a nil one as-is and // CallerFromContext reads it back as anonymous, which is why an anonymous test // needs no special case. func authnContext(ac *auth.AuthContext) context.Context { return authn.WithCaller(context.Background(), ac) } func coreCaller(ac *auth.AuthContext) *core.Caller { if ac == nil { return nil } return &core.Caller{UserID: ac.UserID, Username: ac.Username, UserType: core.UserType(ac.UserType)} } // --- plumbing --------------------------------------------------------------- // newServer builds the surface over the fakes, failing the test on a wiring // error rather than returning one. func newServer(t *testing.T, repos mcpsrv.Repos, opener mcpsrv.BrowseOpener) *mcpsrv.Server { t.Helper() s, err := mcpsrv.New(repos, opener, nil, testOrigin) require.NoError(t, err) return s } // connect runs an in-process MCP client against the real server, with ac as the // caller. // // The caller is injected by connecting the session with a context carrying it, // which is exactly what happens in production: the SDK connects a session with // the context of the HTTP request, and every tool handler descends from it. Over // HTTP the credential middleware puts it there (http_test.go drives that path); // here the test puts it there directly, and the handlers cannot tell. func connect(t *testing.T, s *mcpsrv.Server, ac *auth.AuthContext) *mcp.ClientSession { t.Helper() ctx := authnContext(ac) serverTransport, clientTransport := mcp.NewInMemoryTransports() serverConn, err := s.Connect(ctx, serverTransport) require.NoError(t, err) t.Cleanup(func() { _ = serverConn.Close() }) client := mcp.NewClient(&mcp.Implementation{Name: "test-client", Version: "test"}, nil) session, err := client.Connect(context.Background(), clientTransport, nil) require.NoError(t, err) t.Cleanup(func() { _ = session.Close() }) return session } // call makes a tool call, failing the test on a protocol error — which is the // distinction errors.go draws: a missing database is a result, a store that // could not answer is a protocol error, and a test that conflated them would // pass for the wrong reason. func call(t *testing.T, s *mcp.ClientSession, name string, args map[string]any) *mcp.CallToolResult { t.Helper() res, err := s.CallTool(context.Background(), &mcp.CallToolParams{Name: name, Arguments: args}) require.NoError(t, err, "protocol-level failure calling %s", name) return res } // decode reads a successful tool result into out, asserting it is not an error // result on the way. func decode(t *testing.T, res *mcp.CallToolResult, out any) { t.Helper() require.False(t, res.IsError, "unexpected tool error: %s", errorText(res)) require.NotNil(t, res.StructuredContent, "no structured output") raw, err := json.Marshal(res.StructuredContent) require.NoError(t, err) require.NoError(t, json.Unmarshal(raw, out)) } // errorText is the message of an error result, which is where this surface's // refusals are written. func errorText(res *mcp.CallToolResult) string { var s string for _, c := range res.Content { if tc, ok := c.(*mcp.TextContent); ok { s += tc.Text } } return s } // resultJSON is the whole result as it went over the wire — content blocks // included, not just the structured half — for the tests that assert about // everything a client can see. func resultJSON(t *testing.T, res *mcp.CallToolResult) string { t.Helper() raw, err := json.Marshal(res) require.NoError(t, err) return string(raw) } // The shapes a client decodes into, spelled out here rather than exported from // the package: they are this surface's contract, and a test that reused the // production structs would pass no matter what those structs said. type ( listDatabasesResult struct { Databases []databaseResult `json:"databases"` } databaseResult struct { Owner string `json:"owner"` Name string `json:"name"` Description string `json:"description"` Visibility string `json:"visibility"` Content *databaseContent `json:"content"` ContentError string `json:"content_error"` } databaseContent struct { DefaultBranch string `json:"default_branch"` Head string `json:"head"` HeadTime *time.Time `json:"head_time"` IsBeads bool `json:"is_beads"` } ) func names(res listDatabasesResult) []string { out := make([]string, 0, len(res.Databases)) for _, d := range res.Databases { out = append(out, d.Name) } return out } func listDatabases(t *testing.T, session *mcp.ClientSession, args map[string]any) listDatabasesResult { t.Helper() var out listDatabasesResult decode(t, call(t, session, "list_databases", args), &out) return out } // --- the constructor -------------------------------------------------------- func TestNewRejectsAMissingSeam(t *testing.T) { opener := newFakeOpener() repos := newFakeRepos() _, err := mcpsrv.New(nil, opener, nil, testOrigin) require.Error(t, err, "a surface with no metadata store would answer every call internal error") assert.Contains(t, err.Error(), "Repos") _, err = mcpsrv.New(repos, nil, nil, testOrigin) require.Error(t, err, "a surface that cannot open a store cannot describe one") assert.Contains(t, err.Error(), "BrowseOpener") } // A nil InstanceValidator is a configuration and not a missing seam: an instance // with no [tokens.sr.ht] section still serves meta PATs and anonymous callers // (authn.ResolveBearer's documented contract, docs/DESIGN.mcp.md §10). func TestNewAcceptsNoTokensDaemon(t *testing.T) { s, err := mcpsrv.New(newFakeRepos(), newFakeOpener(), nil, testOrigin) require.NoError(t, err) require.NotNil(t, s) session := connect(t, s, nil) assert.NotEmpty(t, listDatabases(t, session, map[string]any{"owner": "alice"}).Databases, "an instance without a token daemon still answers an anonymous caller") } // The Host allowlist is derived from the origin, so an origin with no host is a // constructor error rather than a guessed "localhost" — which would make every // malformed origin agree with a local client on the one code path that decides // the allowlist. func TestNewRequiresAnOriginToGuardWith(t *testing.T) { for _, origin := range []string{"", " ", "not a url", "/relative/path"} { t.Run(fmt.Sprintf("%q", origin), func(t *testing.T) { _, err := mcpsrv.New(newFakeRepos(), newFakeOpener(), nil, origin) require.Error(t, err) assert.Contains(t, err.Error(), "no host") }) } } // --- the tool --------------------------------------------------------------- func TestServerAdvertisesTheToolsOfThisPhase(t *testing.T) { session := connect(t, newServer(t, newFakeRepos(), newFakeOpener()), nil) require.Equal(t, mcpsrv.ServerName, session.InitializeResult().ServerInfo.Name) res, err := session.ListTools(context.Background(), nil) require.NoError(t, err) var got []string for _, tool := range res.Tools { got = append(got, tool.Name) assert.NotEmpty(t, tool.Description, "%s: an agent reads the description as its documentation", tool.Name) require.NotNil(t, tool.Annotations, "%s: every tool here is a read and must say so", tool.Name) assert.True(t, tool.Annotations.ReadOnlyHint, "%s", tool.Name) require.NotNil(t, tool.InputSchema, "%s: the schema is derived from the Go struct", tool.Name) } assert.Equal(t, []string{"list_databases"}, got, "this phase registers exactly one tool; the rest arrive with their own commits") } func TestListDatabasesDescribesADatabase(t *testing.T) { session := connect(t, newServer(t, newFakeRepos(), newFakeOpener()), nil) byName := map[string]databaseResult{} for _, d := range listDatabases(t, session, map[string]any{"owner": "alice"}).Databases { byName[d.Name] = d } notes := byName["notes"] assert.Equal(t, "alice", notes.Owner) assert.Equal(t, "the notes database", notes.Description) assert.Equal(t, string(core.VisibilityPublic), notes.Visibility) require.NotNil(t, notes.Content) assert.Equal(t, "main", notes.Content.DefaultBranch) assert.Equal(t, "aaaa", notes.Content.Head) require.NotNil(t, notes.Content.HeadTime) assert.True(t, headTime.Equal(*notes.Content.HeadTime), "got %v", notes.Content.HeadTime) assert.False(t, notes.Content.IsBeads, "a database whose tables are not beads' is not a tracker") tracker := byName["tracker"] require.NotNil(t, tracker.Content) assert.True(t, tracker.Content.IsBeads, "the fingerprint is beads.Applies, and this fixture satisfies it") } // A database that exists and carries no commits is not a failure: its content is // null and there is no error beside it. The two nulls are distinguishable, which // is the whole reason content is one object rather than four fields. func TestADatabaseWithNoCommitsHasNoContentAndNoError(t *testing.T) { session := connect(t, newServer(t, newFakeRepos(), newFakeOpener()), nil) for _, d := range listDatabases(t, session, map[string]any{"owner": "alice"}).Databases { if d.Name != "empty" { continue } assert.Nil(t, d.Content, "nothing has been pushed, so there is nothing to describe") assert.Empty(t, d.ContentError, "and that is not an error") return } t.Fatal("the empty database was not listed") } // One unreadable store costs that database its content and nothing else. The // alternatives were failing the whole call — one broken store hiding every // database from every caller — and answering is_beads:false, which is a lie an // agent cannot detect. func TestAnUnreadableStoreDoesNotSinkTheListing(t *testing.T) { session := connect(t, newServer(t, newFakeRepos(), newFakeOpener()), nil) res := listDatabases(t, session, map[string]any{"owner": "alice"}) assert.Subset(t, names(res), []string{"notes", "tracker", "broken"}, "the other databases are still listed") for _, d := range res.Databases { if d.Name != "broken" { continue } assert.Nil(t, d.Content, "no content is claimed for a store that did not open") assert.NotEmpty(t, d.ContentError, "and the entry says so") assert.Equal(t, string(core.VisibilityPublic), d.Visibility, "the metadata beside it came from Postgres and is still true") return } t.Fatal("the broken database was not listed") } // The failure reaches the caller as a fixed sentence: the real one names on-disk // paths and dolt internals, and this endpoint is reachable by anyone. func TestAContentFailureDisclosesNoDetail(t *testing.T) { repos := newFakeRepos() opener := newFakeOpener() opener.sessions[storePath("alice", "notes")].tablesErr = errors.New("read /srv/dolt/~alice/notes/manifest: input/output error") session := connect(t, newServer(t, repos, opener), nil) body := resultJSON(t, call(t, session, "list_databases", map[string]any{"owner": "alice"})) assert.NotContains(t, body, "/srv/dolt", "no on-disk path reaches a caller") assert.NotContains(t, body, "input/output error") assert.Contains(t, body, "could not be read") } // Every session a listing opens is closed by the handler that opened it, which // is the browse discipline: a store held open across calls is a stale manifest // and a leaked handle. func TestEverySessionIsClosed(t *testing.T) { repos := newFakeRepos() opener := newFakeOpener() session := connect(t, newServer(t, repos, opener), alice()) listDatabases(t, session, nil) for path, sess := range opener.sessions { assert.Equal(t, 1, sess.closes, "%s: opened once, closed once", path) } } // A metadata store that could not answer is a protocol error and not an empty // listing: an agent must not read "Postgres is down" as "you have no // databases". func TestAStoreThatCouldNotAnswerIsAProtocolError(t *testing.T) { repos := newFakeRepos() repos.listErr = errors.New("dial tcp 127.0.0.1:5432: connection refused") session := connect(t, newServer(t, repos, newFakeOpener()), alice()) res, err := session.CallTool(context.Background(), &mcp.CallToolParams{ Name: "list_databases", Arguments: map[string]any{"owner": "alice"}, }) require.Error(t, err, "a broken metadata store is not an answer") assert.NotContains(t, err.Error(), "5432", "and the cause is logged, not sent") assert.Nil(t, res) } // --- the visibility matrix -------------------------------------------------- // The listing rule of docs/DESIGN.mcp.md §4.3 over every viewer: PUBLIC to // everyone, an owner sees all of their own, a grantee sees what they were // granted, and an UNLISTED database of somebody else is absent from the listing // while remaining readable by direct address. func TestListDatabasesAppliesTheListingRule(t *testing.T) { server := newServer(t, newFakeRepos(), newFakeOpener()) for _, tc := range []struct { name string caller *auth.AuthContext }{ {"anonymous", nil}, {"a stranger", carol()}, {"a grantee", bob()}, {"the owner", alice()}, } { t.Run(tc.name, func(t *testing.T) { res := listDatabases(t, connect(t, server, tc.caller), map[string]any{"owner": "alice"}) assert.ElementsMatch(t, listableNames(coreCaller(tc.caller)), names(res)) }) } } // Nothing a caller may not list leaks through the listing — not as a name, not // as a description, not in an error message. The private database's name is the // canary: a stranger who can see the string at all can enumerate what exists. func TestNothingLeaksToAViewerWhoMayNotList(t *testing.T) { server := newServer(t, newFakeRepos(), newFakeOpener()) for _, tc := range []struct { name string caller *auth.AuthContext }{ {"anonymous", nil}, {"a stranger", carol()}, } { t.Run(tc.name, func(t *testing.T) { session := connect(t, server, tc.caller) body := resultJSON(t, call(t, session, "list_databases", map[string]any{"owner": "alice"})) assert.NotContains(t, body, "secrets", "a private database is not named to a caller who may not list it") assert.NotContains(t, body, "drafts", "and neither is an unlisted one") }) } } // An owner named with the sigil is the address a link shows, so it is accepted // rather than answered with a sentence about punctuation. func TestTheOwnerArgumentToleratesTheSigil(t *testing.T) { session := connect(t, newServer(t, newFakeRepos(), newFakeOpener()), nil) with := listDatabases(t, session, map[string]any{"owner": "~alice"}) without := listDatabases(t, session, map[string]any{"owner": "alice"}) assert.Equal(t, names(without), names(with)) assert.NotEmpty(t, names(with)) } // An owner nobody has and an owner with nothing visible are one answer: an empty // listing. That is not a limitation to be fixed — a "no such user" would let an // agent enumerate accounts through a database listing. func TestAnUnknownOwnerIsAnEmptyListing(t *testing.T) { session := connect(t, newServer(t, newFakeRepos(), newFakeOpener()), nil) res := call(t, session, "list_databases", map[string]any{"owner": "nobody"}) assert.False(t, res.IsError, "not knowing an account is not an error") var out listDatabasesResult decode(t, res, &out) assert.Empty(t, out.Databases) } // Without an owner the tool answers about the caller: everything they own or // hold an ACL entry on, whatever its visibility. That is the dashboard query and // the only "everything I may see" this service can answer. func TestListDatabasesWithNoOwnerIsTheCallersOwn(t *testing.T) { server := newServer(t, newFakeRepos(), newFakeOpener()) t.Run("the owner sees all of their own", func(t *testing.T) { res := listDatabases(t, connect(t, server, alice()), nil) assert.ElementsMatch(t, listableNames(coreCaller(alice())), names(res)) }) t.Run("a grantee sees what they were granted", func(t *testing.T) { res := listDatabases(t, connect(t, server, bob()), nil) assert.Equal(t, []string{"secrets"}, names(res)) }) t.Run("a stranger owns nothing and is granted nothing", func(t *testing.T) { res := listDatabases(t, connect(t, server, carol()), nil) assert.Empty(t, res.Databases) }) } // An anonymous caller with no owner named has nothing to be answered about: // there is no "every public database on this instance" query (db/repos.go), so // the tool says what it cannot do and names the two ways out instead of // answering an empty listing that would read as "this instance is empty". func TestAnonymousWithNoOwnerIsToldWhatToPass(t *testing.T) { session := connect(t, newServer(t, newFakeRepos(), newFakeOpener()), nil) res := call(t, session, "list_databases", nil) require.True(t, res.IsError, "an empty listing here would be a false statement about the instance") text := errorText(res) assert.Contains(t, text, "owner") assert.Contains(t, text, "token") }