package graph
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"sourcecraft.dev/bigbes/sr-ht-core/auth"
"sourcecraft.dev/bigbes/sr-ht-ecore/ecoretest"
"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"
)
// TestMain seeds the process-global crypto state: the pagination cursor is
// core-go's, which encrypts itself with the instance keys.
func TestMain(m *testing.M) {
ecoretest.InitCrypto()
m.Run()
}
// --- fakes -------------------------------------------------------------------
type fakeRepos struct {
repos map[string]*core.Repo // "owner/name"
order []*core.Repo // listing order, newest first
acls map[int]map[int]core.AccessMode
names map[int]string // user id -> username, for ListACL
listErr error
getErr error
accessErr error
}
func newFakeRepos() *fakeRepos {
return &fakeRepos{
repos: map[string]*core.Repo{},
acls: map[int]map[int]core.AccessMode{},
names: map[int]string{},
}
}
func (f *fakeRepos) add(r *core.Repo) *core.Repo {
r.ID = len(f.order) + 1
r.Path = "/var/lib/dolt/~" + r.OwnerName + "/" + r.Name
if r.Created.IsZero() {
r.Created = time.Date(2026, 8, 15, 12, 0, 0, 0, time.UTC)
r.Updated = r.Created
}
f.repos[r.OwnerName+"/"+r.Name] = r
// Newest first, as the store's ORDER BY created DESC, id DESC yields.
f.order = append([]*core.Repo{r}, f.order...)
f.names[r.OwnerID] = r.OwnerName
return r
}
func (f *fakeRepos) grant(repoID, userID int, mode core.AccessMode, username string) {
if f.acls[repoID] == nil {
f.acls[repoID] = map[int]core.AccessMode{}
}
f.acls[repoID][userID] = mode
f.names[userID] = username
}
func (f *fakeRepos) GetRepoByOwnerAndName(_ context.Context, owner, name string) (*core.Repo, error) {
if f.getErr != nil {
return nil, f.getErr
}
r, ok := f.repos[owner+"/"+name]
if !ok {
return nil, db.ErrNotFound
}
return r, nil
}
// visible mirrors db.Store's listing rule: PUBLIC to everyone, plus whatever the
// viewer owns or holds an ACL entry on.
func (f *fakeRepos) visible(r *core.Repo, viewer *core.Caller) bool {
if r.Visibility == core.VisibilityPublic {
return true
}
if viewer == nil {
return false
}
if viewer.UserID == r.OwnerID {
return true
}
_, ok := f.acls[r.ID][viewer.UserID]
return ok
}
func (f *fakeRepos) ListReposForViewer(_ context.Context, viewer *core.Caller) ([]*core.Repo, error) {
if f.listErr != nil {
return nil, f.listErr
}
var out []*core.Repo
for _, r := range f.order {
if f.visible(r, viewer) {
out = append(out, r)
}
}
return out, nil
}
func (f *fakeRepos) ListReposByOwner(ctx context.Context, owner string, viewer *core.Caller) ([]*core.Repo, error) {
all, err := f.ListReposForViewer(ctx, viewer)
if err != nil {
return nil, err
}
var out []*core.Repo
for _, r := range all {
if r.OwnerName == owner {
out = append(out, r)
}
}
return out, nil
}
func (f *fakeRepos) EffectiveAccess(_ context.Context, userID, repoID int) (*core.AccessMode, error) {
if f.accessErr != nil {
return nil, f.accessErr
}
mode, ok := f.acls[repoID][userID]
if !ok {
return nil, nil
}
return &mode, nil
}
func (f *fakeRepos) 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: f.names[uid], Mode: mode})
}
return out, nil
}
type fakeSession struct {
branches []browse.Branch
commits []browse.CommitInfo
next string
tables []browse.TableInfo
err error
logRef string
logFrom string
logLimit int
closed bool
}
func (s *fakeSession) Branches(context.Context) ([]browse.Branch, error) {
return s.branches, s.err
}
func (s *fakeSession) Log(_ context.Context, ref, from string, limit int) ([]browse.CommitInfo, string, error) {
s.logRef, s.logFrom, s.logLimit = ref, from, limit
return s.commits, s.next, s.err
}
func (s *fakeSession) Tables(context.Context, string) ([]browse.TableInfo, error) {
return s.tables, s.err
}
func (s *fakeSession) Close() error { s.closed = true; return nil }
type fakeOpener struct {
sess *fakeSession
openErr error
mu sync.Mutex
opens []string
}
func (o *fakeOpener) Open(_ context.Context, diskPath string) (BrowseSession, error) {
o.mu.Lock()
o.opens = append(o.opens, diskPath)
o.mu.Unlock()
if o.openErr != nil {
return nil, o.openErr
}
if o.sess == nil {
o.sess = &fakeSession{}
}
return o.sess, nil
}
func (o *fakeOpener) count() int {
o.mu.Lock()
defer o.mu.Unlock()
return len(o.opens)
}
// --- harness -----------------------------------------------------------------
type harness struct {
server *Server
repos *fakeRepos
opener *fakeOpener
}
func newHarness(t *testing.T) *harness {
t.Helper()
repos, opener := newFakeRepos(), &fakeOpener{}
srv, err := New(Options{Repos: repos, Browse: opener})
require.NoError(t, err)
return &harness{server: srv, repos: repos, opener: opener}
}
// gqlResponse is the GraphQL envelope: data and errors travel together, and a
// resolver that refused shows up in errors with data still present.
type gqlResponse struct {
Data json.RawMessage `json:"data"`
Errors []struct {
Message string `json:"message"`
Path []any `json:"path"`
} `json:"errors"`
}
// query POSTs a query as caller (nil for anonymous) and returns the raw
// recorder plus the decoded envelope.
func (h *harness) query(t *testing.T, caller *auth.AuthContext, q string) (*httptest.ResponseRecorder, gqlResponse) {
t.Helper()
body, err := json.Marshal(map[string]any{"query": q})
require.NoError(t, err)
req := httptest.NewRequest(http.MethodPost, "/query", strings.NewReader(string(body)))
req.Header.Set("Content-Type", "application/json")
if caller != nil {
req = req.WithContext(authn.WithCaller(req.Context(), caller))
}
rec := httptest.NewRecorder()
h.server.ServeHTTP(rec, req)
var out gqlResponse
if rec.Body.Len() > 0 && strings.HasPrefix(rec.Header().Get("Content-Type"), "application/json") {
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &out), "body: %s", rec.Body.String())
}
return rec, out
}
// ok asserts the query succeeded and unmarshals data into v.
func (h *harness) ok(t *testing.T, caller *auth.AuthContext, q string, v any) {
t.Helper()
rec, resp := h.query(t, caller, q)
require.Equal(t, http.StatusOK, rec.Code)
require.Empty(t, resp.Errors, "unexpected GraphQL errors: %+v", resp.Errors)
require.NoError(t, json.Unmarshal(resp.Data, v))
}
func testCaller(id int, name string) *auth.AuthContext {
return &auth.AuthContext{UserID: id, Username: name, UserType: auth.USER_TYPE_USER}
}
// seed is one instance: alice owns a public, an unlisted and a private
// database; bob owns a public one and holds RO on alice's private one.
func (h *harness) seed() {
h.repos.add(&core.Repo{Name: "widgets", OwnerID: 1, OwnerName: "alice",
Description: "public things", Visibility: core.VisibilityPublic})
h.repos.add(&core.Repo{Name: "drafts", OwnerID: 1, OwnerName: "alice",
Visibility: core.VisibilityUnlisted})
priv := h.repos.add(&core.Repo{Name: "secrets", OwnerID: 1, OwnerName: "alice",
Visibility: core.VisibilityPrivate})
h.repos.add(&core.Repo{Name: "gadgets", OwnerID: 2, OwnerName: "bob",
Visibility: core.VisibilityPublic})
h.repos.grant(priv.ID, 2, core.AccessRO, "bob")
h.repos.names[1] = "alice"
}
// --- the listing --------------------------------------------------------------
// The query that started this: what an anonymous caller and each user see from
// the same endpoint. The listing rule is the store's, and the schema must not
// widen it.
func TestDatabasesListsWhatTheCallerMaySee(t *testing.T) {
type listing struct {
Databases struct {
Results []struct {
Name string `json:"name"`
Visibility string `json:"visibility"`
Owner struct {
Username string `json:"username"`
CanonicalName string `json:"canonicalName"`
} `json:"owner"`
} `json:"results"`
Cursor *string `json:"cursor"`
} `json:"databases"`
}
const q = `{ databases { results { name visibility owner { username canonicalName } } cursor } }`
for _, tc := range []struct {
name string
caller *auth.AuthContext
want []string
}{
{"anonymous sees only PUBLIC", nil, []string{"gadgets", "widgets"}},
{"a stranger sees the same as anonymous", testCaller(9, "carol"), []string{"gadgets", "widgets"}},
{"the owner sees all of their own", testCaller(1, "alice"), []string{"gadgets", "secrets", "drafts", "widgets"}},
{"an ACL holder additionally sees what they were granted", testCaller(2, "bob"), []string{"gadgets", "secrets", "widgets"}},
} {
t.Run(tc.name, func(t *testing.T) {
h := newHarness(t)
h.seed()
var got listing
h.ok(t, tc.caller, q, &got)
var names []string
for _, r := range got.Databases.Results {
names = append(names, r.Name)
}
assert.Equal(t, tc.want, names)
assert.Nil(t, got.Databases.Cursor, "one page holds this instance")
for _, r := range got.Databases.Results {
assert.Equal(t, "~"+r.Owner.Username, r.Owner.CanonicalName)
}
})
}
}
// A listing must not open a single bare store. This is what the hand-written
// model and the resolver:true fields in gqlgen.yml buy, and it is the kind of
// thing that regresses silently the day someone adds a field to the struct.
func TestAMetadataQueryOpensNoStore(t *testing.T) {
h := newHarness(t)
h.seed()
var got struct{}
h.ok(t, testCaller(1, "alice"), `{ databases { results { name visibility description created } } }`, &got)
assert.Zero(t, h.opener.count(), "a metadata-only query opened a store")
}
func TestDatabasesByOwner(t *testing.T) {
h := newHarness(t)
h.seed()
var got struct {
DatabasesByOwner struct {
Results []struct {
Name string `json:"name"`
} `json:"results"`
} `json:"databasesByOwner"`
}
h.ok(t, nil, `{ databasesByOwner(owner: "alice") { results { name } } }`, &got)
require.Len(t, got.DatabasesByOwner.Results, 1)
assert.Equal(t, "widgets", got.DatabasesByOwner.Results[0].Name)
}
// --- one database ------------------------------------------------------------
// A database the caller may not see is null and not an error: the two answers
// are one, so its existence cannot be read out of the shape of the refusal.
func TestDatabaseHidesWhatTheCallerMayNotSee(t *testing.T) {
const q = `{ database(owner: "alice", name: "%s") { name visibility } }`
for _, tc := range []struct {
name string
caller *auth.AuthContext
db string
want bool
}{
{"a private database is invisible to a stranger", testCaller(9, "carol"), "secrets", false},
{"and to an anonymous caller", nil, "secrets", false},
{"an unlisted one is readable by direct address", nil, "drafts", true},
{"the owner reads their private one", testCaller(1, "alice"), "secrets", true},
{"an ACL holder reads it too", testCaller(2, "bob"), "secrets", true},
{"a database that does not exist", testCaller(1, "alice"), "nope", false},
} {
t.Run(tc.name, func(t *testing.T) {
h := newHarness(t)
h.seed()
rec, resp := h.query(t, tc.caller, fmt.Sprintf(q, tc.db))
require.Equal(t, http.StatusOK, rec.Code)
require.Empty(t, resp.Errors, "a refusal must be null, not an error: %+v", resp.Errors)
var got struct {
Database *struct {
Name string `json:"name"`
} `json:"database"`
}
require.NoError(t, json.Unmarshal(resp.Data, &got))
if tc.want {
require.NotNil(t, got.Database)
assert.Equal(t, tc.db, got.Database.Name)
} else {
assert.Nil(t, got.Database)
}
})
}
}
// The ACL of a database you do not own is not yours to enumerate. It is the
// empty list rather than an error, so asking is not a way to learn who is on it.
func TestACLIsOwnerOnly(t *testing.T) {
const q = `{ database(owner: "alice", name: "secrets") { acl { mode user { username } } } }`
h := newHarness(t)
h.seed()
var owner struct {
Database struct {
ACL []struct {
Mode string `json:"mode"`
User struct {
Username string `json:"username"`
} `json:"user"`
} `json:"acl"`
} `json:"database"`
}
h.ok(t, testCaller(1, "alice"), q, &owner)
require.Len(t, owner.Database.ACL, 1)
assert.Equal(t, "bob", owner.Database.ACL[0].User.Username)
assert.Equal(t, "RO", owner.Database.ACL[0].Mode)
// bob may READ the database — he holds the grant — and still may not see
// who else does.
var grantee struct {
Database struct {
ACL []json.RawMessage `json:"acl"`
} `json:"database"`
}
h.ok(t, testCaller(2, "bob"), q, &grantee)
assert.Empty(t, grantee.Database.ACL)
}
func TestMeIsTheCallerOrNull(t *testing.T) {
h := newHarness(t)
h.seed()
var anon struct {
Me *struct{} `json:"me"`
}
h.ok(t, nil, `{ me { username } }`, &anon)
assert.Nil(t, anon.Me, "anonymous is an answer here, not an error")
var known struct {
Me struct {
Username string `json:"username"`
CanonicalName string `json:"canonicalName"`
} `json:"me"`
}
h.ok(t, testCaller(1, "alice"), `{ me { username canonicalName } }`, &known)
assert.Equal(t, "alice", known.Me.Username)
assert.Equal(t, "~alice", known.Me.CanonicalName)
}
// --- the browse fields --------------------------------------------------------
func TestBranchesLogAndTables(t *testing.T) {
h := newHarness(t)
h.seed()
when := time.Date(2026, 8, 14, 9, 30, 0, 0, time.UTC)
h.opener.sess = &fakeSession{
branches: []browse.Branch{{Name: "main", Head: "abc"}, {Name: "topic", Head: "def"}},
commits: []browse.CommitInfo{{
Hash: "abc", Author: "alice", Email: "a@b.test", Date: when,
Message: "init", ParentHashes: []string{"aaa"},
}},
next: "older",
tables: []browse.TableInfo{{
Name: "issues",
RowCount: 7,
Columns: []browse.ColumnInfo{{Name: "id", Type: "int", PrimaryKey: true}},
}},
}
var got struct {
Database struct {
DefaultBranch string `json:"defaultBranch"`
Branches []struct {
Name string `json:"name"`
Head string `json:"head"`
} `json:"branches"`
Log struct {
Results []struct {
Hash string `json:"hash"`
Author string `json:"author"`
Date time.Time `json:"date"`
Parents []string `json:"parents"`
} `json:"results"`
Cursor *string `json:"cursor"`
} `json:"log"`
Tables []struct {
Name string `json:"name"`
RowCount int `json:"rowCount"`
Columns []struct {
Name string `json:"name"`
PrimaryKey bool `json:"primaryKey"`
} `json:"columns"`
} `json:"tables"`
} `json:"database"`
}
h.ok(t, nil, `{ database(owner: "alice", name: "widgets") {
defaultBranch
branches { name head }
log { results { hash author date parents } cursor }
tables { name rowCount columns { name primaryKey } }
} }`, &got)
assert.Equal(t, "main", got.Database.DefaultBranch)
require.Len(t, got.Database.Branches, 2)
assert.Equal(t, "abc", got.Database.Branches[0].Head)
require.Len(t, got.Database.Log.Results, 1)
assert.Equal(t, when, got.Database.Log.Results[0].Date)
assert.Equal(t, []string{"aaa"}, got.Database.Log.Results[0].Parents)
assert.NotNil(t, got.Database.Log.Cursor, "browse reported more history, so the page must carry a cursor")
require.Len(t, got.Database.Tables, 1)
assert.Equal(t, 7, got.Database.Tables[0].RowCount)
assert.True(t, got.Database.Tables[0].Columns[0].PrimaryKey)
// The default branch is what a field with no ref reads.
assert.Equal(t, "main", h.opener.sess.logRef)
assert.Equal(t, defaultLogLimit, h.opener.sess.logLimit)
assert.True(t, h.opener.sess.closed, "every session a resolver opens is closed")
}
// A database nothing has been pushed to has no branches. That is a state, not a
// failure, and every field says so in its own vocabulary rather than erroring.
func TestAnEmptyDatabaseIsAStateAndNotAFailure(t *testing.T) {
h := newHarness(t)
h.seed()
h.opener.sess = &fakeSession{} // no branches, no commits, no tables
var got struct {
Database struct {
DefaultBranch *string `json:"defaultBranch"`
Branches []any `json:"branches"`
Log struct {
Results []any `json:"results"`
Cursor *string `json:"cursor"`
} `json:"log"`
Tables []any `json:"tables"`
} `json:"database"`
}
h.ok(t, nil, `{ database(owner: "alice", name: "widgets") {
defaultBranch branches { name } log { results { hash } cursor } tables { name }
} }`, &got)
assert.Nil(t, got.Database.DefaultBranch)
assert.Empty(t, got.Database.Branches)
assert.Empty(t, got.Database.Log.Results)
assert.Nil(t, got.Database.Log.Cursor)
assert.Empty(t, got.Database.Tables)
}
// A store that will not open is an error on that field — the database exists and
// the caller may read it — and the store layer's own text, which carries the
// on-disk path, never travels with it.
func TestAStoreThatWillNotOpenIsAnErrorAndSaysNothingAboutDisk(t *testing.T) {
h := newHarness(t)
h.seed()
h.opener.openErr = errors.New("open /var/lib/dolt/~alice/widgets: manifest is corrupt")
rec, resp := h.query(t, nil, `{ database(owner: "alice", name: "widgets") { name branches { name } } }`)
require.Equal(t, http.StatusOK, rec.Code)
require.NotEmpty(t, resp.Errors)
body := rec.Body.String()
assert.Contains(t, body, "could not be read")
assert.NotContains(t, body, "/var/lib/dolt", "the store's path must not reach the reader")
assert.NotContains(t, body, "manifest is corrupt")
}
// The metadata store being unreachable is not "there is nothing here". A
// listing that answered an empty set on a failed query would tell a client a
// false fact about the instance.
func TestAnUnreachableStoreIsAnErrorAndNotAnEmptyListing(t *testing.T) {
h := newHarness(t)
h.seed()
h.repos.listErr = errors.New("dial tcp 10.0.0.5:5432: connect: connection refused")
rec, resp := h.query(t, nil, `{ databases { results { name } } }`)
require.Equal(t, http.StatusOK, rec.Code)
require.NotEmpty(t, resp.Errors)
assert.NotContains(t, rec.Body.String(), "10.0.0.5", "the connection string must not reach the reader")
}
// An ACL lookup that fails is an error, not a fall-through to visibility: a
// caller holding a grant must never be told the database is not there because
// the grant could not be checked.
func TestAnUncheckableGrantIsNotADenial(t *testing.T) {
h := newHarness(t)
h.seed()
h.repos.accessErr = errors.New("dial tcp: connection refused")
rec, resp := h.query(t, testCaller(2, "bob"), `{ database(owner: "alice", name: "secrets") { name } }`)
require.Equal(t, http.StatusOK, rec.Code)
require.NotEmpty(t, resp.Errors, "an unreadable grant answered as 'no such database'")
}
// --- paging -------------------------------------------------------------------
// The cursor is opaque and encrypted, so a test walks it exactly as a client
// does: read a page, hand the cursor back, and check the pages tile the listing
// without gaps or repeats.
func TestTheListingPagesWithItsOwnCursor(t *testing.T) {
h := newHarness(t)
for i := range 5 {
h.repos.add(&core.Repo{
Name: fmt.Sprintf("db%d", i), OwnerID: 1, OwnerName: "alice",
Visibility: core.VisibilityPublic,
})
}
type pageResp struct {
Databases struct {
Results []struct {
Name string `json:"name"`
} `json:"results"`
Cursor *string `json:"cursor"`
} `json:"databases"`
}
var got pageResp
h.ok(t, nil, `{ databases { results { name } cursor } }`, &got)
require.Len(t, got.Databases.Results, 5, "the default page holds this instance")
assert.Nil(t, got.Databases.Cursor)
// Walk it two at a time, exactly as a client does: the page size is given
// once, and every later page is addressed by the cursor the previous one
// returned.
var seen []string
var pages int
h.ok(t, nil, `{ databases(filter: {count: 2}) { results { name } cursor } }`, &got)
for {
pages++
require.LessOrEqual(t, len(got.Databases.Results), 2, "a page overran the count it was given")
for _, r := range got.Databases.Results {
seen = append(seen, r.Name)
}
if got.Databases.Cursor == nil {
break
}
require.Less(t, pages, 10, "the walk did not terminate")
h.ok(t, nil, fmt.Sprintf(`{ databases(cursor: %q) { results { name } cursor } }`,
*got.Databases.Cursor), &got)
}
assert.Equal(t, 3, pages, "five rows, two at a time")
assert.Equal(t, []string{"db4", "db3", "db2", "db1", "db0"}, seen,
"the pages tile the listing, newest first, with no gap and no repeat")
}
// The count is the surface's to cap. A client asking for the whole instance in
// one page gets the cap, not the ask.
func TestThePageSizeIsCapped(t *testing.T) {
h := newHarness(t)
for i := range maxPageSize + 10 {
h.repos.add(&core.Repo{
Name: fmt.Sprintf("db%d", i), OwnerID: 1, OwnerName: "alice",
Visibility: core.VisibilityPublic,
})
}
var got struct {
Databases struct {
Results []struct {
Name string `json:"name"`
} `json:"results"`
Cursor *string `json:"cursor"`
} `json:"databases"`
}
h.ok(t, nil, `{ databases(filter: {count: 100000}) { results { name } cursor } }`, &got)
assert.Len(t, got.Databases.Results, maxPageSize)
assert.NotNil(t, got.Databases.Cursor, "the rest is still reachable, one page at a time")
}
// A cursor whose row is gone — deleted, or made invisible to this caller —
// resumes at the next row rather than failing or silently starting over.
func TestAStaleCursorResumesRatherThanRestarting(t *testing.T) {
h := newHarness(t)
for i := range 4 {
h.repos.add(&core.Repo{
Name: fmt.Sprintf("db%d", i), OwnerID: 1, OwnerName: "alice",
Visibility: core.VisibilityPublic,
})
}
var got struct {
Databases struct {
Results []struct {
Name string `json:"name"`
} `json:"results"`
Cursor *string `json:"cursor"`
} `json:"databases"`
}
h.ok(t, nil, `{ databases(filter: {count: 2}) { results { name } cursor } }`, &got)
require.NotNil(t, got.Databases.Cursor)
next := *got.Databases.Cursor
// db1 is the row that cursor points at; drop it between the two pages.
delete(h.repos.repos, "alice/db1")
kept := h.repos.order[:0]
for _, r := range h.repos.order {
if r.Name != "db1" {
kept = append(kept, r)
}
}
h.repos.order = kept
h.ok(t, nil, fmt.Sprintf(`{ databases(cursor: %q) { results { name } cursor } }`, next), &got)
var names []string
for _, r := range got.Databases.Results {
names = append(names, r.Name)
}
assert.Equal(t, []string{"db0"}, names,
"the page resumes past the vanished row instead of restarting at the newest")
}
// --- the credential plane -----------------------------------------------------
// A credential this endpoint cannot verify is refused, and refused with the
// challenge, so a client knows what to present. This is /mcp's middleware and
// the arms are the same.
func TestABadTokenIsRefusedWithAChallenge(t *testing.T) {
h := newHarness(t)
req := httptest.NewRequest(http.MethodPost, "/query",
strings.NewReader(`{"query":"{ __typename }"}`))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer not-a-real-token")
rec := httptest.NewRecorder()
h.server.ServeHTTP(rec, req)
require.Equal(t, http.StatusUnauthorized, rec.Code)
assert.NotEmpty(t, rec.Header().Get("WWW-Authenticate"))
assert.Contains(t, rec.Body.String(), "refused")
}
// Introspection is deliberately on: a client that cannot introspect cannot
// generate a typed client, and every field is gated per caller anyway.
func TestIntrospectionAnswers(t *testing.T) {
h := newHarness(t)
var got struct {
Schema struct {
QueryType struct {
Name string `json:"name"`
} `json:"queryType"`
} `json:"__schema"`
}
h.ok(t, nil, `{ __schema { queryType { name } } }`, &got)
assert.Equal(t, "Query", got.Schema.QueryType.Name)
}
// A GET is not a transport this endpoint offers: a query in a URL is a
// cross-origin-readable address for data that is often private.
func TestGetIsNotATransport(t *testing.T) {
h := newHarness(t)
rec := httptest.NewRecorder()
h.server.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/query?query={__typename}", nil))
assert.NotEqual(t, http.StatusOK, rec.Code)
}
func TestNewRequiresItsSeams(t *testing.T) {
_, err := New(Options{Browse: &fakeOpener{}})
require.Error(t, err)
assert.Contains(t, err.Error(), "Repos")
_, err = New(Options{Repos: newFakeRepos()})
require.Error(t, err)
assert.Contains(t, err.Error(), "BrowseOpener")
}