Files
at-container-registry/pkg/appview/authgate/testhelpers_test.go
T
Evan JarrettandClaude Fable 5.1 a01b08b924 atproto: gate local indigo behavior behind a testmode build tag
indigo's identity directory refuses HTTP and IP-hosted did:web, and its
OAuth client is growing an SSRF-guarded transport that refuses loopback
and private addresses. Local development and the test suites need both,
and the workarounds were scattered: two did:web fallbacks in the
resolver, a hand-rolled appview key fetch on the hold, and the OAuth
client left on indigo's defaults so any test driving it against an
httptest server depended on the transport staying permissive.

Move every departure from indigo's defaults into one file pair in
pkg/atproto: indigo_prod.go (!testmode) returns indigo's directory and
OAuth client unchanged; indigo_local.go (testmode) wraps the directory
so a did:web naming an IP, localhost, or a host with a port resolves
over plain HTTP, and gives the OAuth client plain HTTP clients. All six
identity and OAuth constructor call sites go through NewDirectory and
NewOAuthClientApp. The resolver fallbacks, DIDWebToURL, and the hold's
scheme-guessing key fetch are gone; the hold resolves the appview key
through the directory, preferring #appview, and purges and retries once
on a signature failure so a re-keyed appview is not masked by the
24-hour cache.

There is no runtime switch for this: a production binary cannot be
configured to resolve local DIDs. The runtime test_mode flag still
gates the remaining behavioral branches only.

Tests, the harness, make dev, Air, Dockerfile.dev, and docker-compose
build with the tag; fixtures that need loopback did:web fail fast
naming it. Test hold servers now serve a did.json via pkg/testpds so
they resolve as real holds under the tag.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UwYzaG3Yy7uA8FbZ5qk3tQ
2026-09-11 10:53:27 -05:00

137 lines
4.9 KiB
Go

package authgate
import (
"context"
"database/sql"
"net/http"
"net/http/httptest"
"testing"
"atcr.io/pkg/appview/db"
"atcr.io/pkg/atproto"
"atcr.io/pkg/auth"
"atcr.io/pkg/testpds"
)
// newTestDB returns an in-memory libsql DB with the full appview schema
// applied. Tears down on test completion.
func newTestDB(t *testing.T) *sql.DB {
t.Helper()
testDB, err := db.InitDB(":memory:", db.LibsqlConfig{})
if err != nil {
t.Fatalf("InitDB: %v", err)
}
t.Cleanup(func() { _ = testDB.Close() })
return testDB
}
// seedUser upserts a users row and optionally sets default_hold_did.
// Pass "" for defaultHold to leave it NULL.
func seedUser(t *testing.T, d *sql.DB, did, handle, defaultHold string) {
t.Helper()
if err := db.UpsertUser(d, &db.User{DID: did, Handle: handle, PDSEndpoint: "https://pds.example/" + did}); err != nil {
t.Fatalf("UpsertUser(%s): %v", did, err)
}
if defaultHold != "" {
if err := db.UpdateUserDefaultHold(d, did, defaultHold); err != nil {
t.Fatalf("UpdateUserDefaultHold(%s, %s): %v", did, defaultHold, err)
}
}
}
// seedCaptain inserts a single hold_captain_records row.
func seedCaptain(t *testing.T, d *sql.DB, holdDID, ownerDID string) {
t.Helper()
if err := db.BatchUpsertCaptainRecords(d, []db.HoldCaptainRecord{
{HoldDID: holdDID, OwnerDID: ownerDID, Public: false, AllowAllCrew: false},
}); err != nil {
t.Fatalf("BatchUpsertCaptainRecords(%s, %s): %v", holdDID, ownerDID, err)
}
}
// seedCrewMember inserts a single hold_crew_members row with the given
// permissions JSON (pass "" to leave permissions NULL — note that the
// underlying schema may coerce empty strings; pass `"[]"` for an empty
// permissions array).
func seedCrewMember(t *testing.T, d *sql.DB, holdDID, memberDID, permsJSON string) {
t.Helper()
if err := db.BatchUpsertCrewMembers(d, []db.CrewMember{
{HoldDID: holdDID, MemberDID: memberDID, Rkey: "rkey-" + memberDID, Role: "crew", Permissions: permsJSON},
}); err != nil {
t.Fatalf("BatchUpsertCrewMembers(%s, %s): %v", holdDID, memberDID, err)
}
}
// quotaServerResult captures HTTP traffic the server saw, for assertions.
type quotaServerResult struct {
server *httptest.Server
holdDID string // did:web:127.0.0.1%3APORT form
hits int
lastURL string
}
// quotaServer spins up an httptest.Server that responds to every request
// with the given status + body, records hit count + last URL, and returns
// both the server URL and the did:web:HOST form that resolves to it. The
// server also serves its own did:web document (not counted in hits) so the
// test-mode identity directory can resolve that DID back to the server.
func quotaServer(t *testing.T, status int, body string) *quotaServerResult {
t.Helper()
if !atproto.TestModeBuild {
t.Fatal("this test resolves a did:web on 127.0.0.1 and needs a `-tags testmode` build")
}
res := &quotaServerResult{}
mux := http.NewServeMux()
mux.HandleFunc("/.well-known/did.json", func(w http.ResponseWriter, r *http.Request) {
base := "http://" + r.Host
testpds.HoldDIDDocumentHandler(testpds.DIDWebForURL(base), base)(w, r)
})
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
res.hits++
res.lastURL = r.URL.String()
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_, _ = w.Write([]byte(body))
})
res.server = httptest.NewServer(mux)
t.Cleanup(res.server.Close)
// httptest.Server.URL has the form "http://127.0.0.1:PORT"; did:web
// percent-encodes the port colon.
res.holdDID = testpds.DIDWebForURL(res.server.URL)
return res
}
// httpClient returns the server's client, which trusts its TLS cert (n/a
// here since httptest.NewServer is HTTP) and routes to the loopback.
func (r *quotaServerResult) httpClient() *http.Client {
return r.server.Client()
}
// fakeHoldAuthorizer is a no-op auth.HoldAuthorizer stub. The Authorize
// orchestration tests don't exercise the reconciliation closure (the
// closure is nil for our purposes because we don't supply a refresher
// and don't go through ResolveIdentity), so we don't need atomic
// counters — just zero-value returns.
type fakeHoldAuthorizer struct{}
func (fakeHoldAuthorizer) CheckReadAccess(_ context.Context, _, _ string) (bool, error) {
return true, nil
}
func (fakeHoldAuthorizer) CheckWriteAccess(_ context.Context, _, _ string) (bool, error) {
return true, nil
}
func (fakeHoldAuthorizer) GetCaptainRecord(_ context.Context, _ string) (*atproto.CaptainRecord, error) {
return nil, nil
}
func (fakeHoldAuthorizer) IsCrewMember(_ context.Context, _, _ string) (bool, error) {
return false, nil
}
func (fakeHoldAuthorizer) ClearCrewDenial(_ context.Context, _, _ string) error { return nil }
func (fakeHoldAuthorizer) IsCachedCrewMember(_ context.Context, _, _ string) (bool, error) {
return false, nil
}
func (fakeHoldAuthorizer) RecordCrewApproval(_ context.Context, _, _ string) error { return nil }
var _ auth.HoldAuthorizer = fakeHoldAuthorizer{}