Files
at-container-registry/pkg/auth/hold_remote_captain_verify_test.go
T
Evan JarrettandClaude Fable 5.1 0080957a21 remove the runtime test_mode switch; the testmode build tag is the only one
server.test_mode survived the build-tag refactor only to feed five
behavioral branches: the registry's fall-back to the default hold when
the user's hold is unreachable, backfill warning suppression for
external holds, the appview listener close on shutdown, the hold's
relay-crawl skip, and the hold's appview-issuer tolerance. Every one of
them is a "this is a local development build" decision, which is what
the tag already says, and local development has to build with the tag
or nothing resolves. So they read atproto.TestModeBuild now, and the
flag, SetTestMode, IsTestMode, the middleware option, the backfill
constructor parameter, the never-read field on RemoteHoldAuthorizer,
the example and template YAML lines, and the docker-compose env vars
are gone. The registry keeps the fallback as a field seeded from the
constant so the production-path tests can pin it off under the tag.

The 24 SetTestMode calls in tests were dead already: stripping them and
running the affected packages tagged changed nothing.

Tests that resolve a loopback did:web used to t.Fatal naming the tag,
which left a bare `go test ./...` permanently red in five packages.
They now live under `//go:build testmode`: whole-file constraints where
every test needs it, and sibling *_testmode_test.go files holding the
moved tests plus their fixtures where a file mixed. The harness carries
the constraint too, with its package doc in an untagged doc.go so the
package still exists without it. An untagged run compiles those tests
out and passes; make test keeps the tag and runs everything.

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

146 lines
4.9 KiB
Go

//go:build testmode
package auth
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"atcr.io/pkg/atproto"
"atcr.io/pkg/testpds"
)
// 69307c0 verified captain records against the publishing DID's atcr_hold
// service before caching them, but only on the two Jetstream writers. This
// covers the third: RemoteHoldAuthorizer.GetCaptainRecord, reached during blob
// authorization with a hold DID the user chose via their sailor profile.
//
// The row matters because GetAvailableHolds offers every hold_captain_records
// row with allow_all_crew=1 to every user's hold picker, so an unverified row
// puts an arbitrary DID in front of everyone as a storage option.
// captainServer serves a captain record for any repo, with allowAllCrew set —
// the value that makes a row visible to every user rather than just its author.
//
// It also serves its own did:web document so the test-mode identity directory
// can resolve the DID derived from its URL (see didFromServer) back to it.
func captainServer(t *testing.T) *httptest.Server {
t.Helper()
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) {
_ = json.NewEncoder(w).Encode(map[string]any{
"uri": "at://x/io.atcr.hold.captain/self",
"cid": "bafytest",
"value": map[string]any{
"$type": atproto.CaptainCollection,
"owner": "did:plc:attacker",
"public": true,
"allowAllCrew": true,
},
})
})
srv := httptest.NewServer(mux)
t.Cleanup(srv.Close)
return srv
}
func didFromServer(url string) string {
return "did:web:" + strings.ReplaceAll(strings.TrimPrefix(url, "http://"), ":", "%3A")
}
func captainRows(t *testing.T, a *RemoteHoldAuthorizer, holdDID string) int {
t.Helper()
var n int
if err := a.db.QueryRow(
`SELECT COUNT(*) FROM hold_captain_records WHERE hold_did = ?`, holdDID,
).Scan(&n); err != nil {
t.Fatalf("count captain rows: %v", err)
}
return n
}
func newVerifyAuthorizer(t *testing.T) *RemoteHoldAuthorizer {
t.Helper()
return &RemoteHoldAuthorizer{
db: setupTestDB(t),
httpClient: &http.Client{Timeout: 5 * time.Second},
cacheTTL: time.Hour,
}
}
// TestGetCaptainRecord_NonHoldDIDIsNotCached is the one that fails without the
// gate. A DID with no atcr_hold service still serves the record over its PDS
// endpoint, so the fetch succeeds and the caller gets an answer — but nothing
// durable may be written, or the picker inherits it.
func TestGetCaptainRecord_NonHoldDIDIsNotCached(t *testing.T) {
a := newVerifyAuthorizer(t)
srv := captainServer(t)
holdDID := didFromServer(srv.URL)
prev := hasHoldService
hasHoldService = func(context.Context, string) (bool, error) { return false, nil }
t.Cleanup(func() { hasHoldService = prev })
rec, err := a.GetCaptainRecord(context.Background(), holdDID)
if err != nil {
t.Fatalf("GetCaptainRecord: %v", err)
}
if rec == nil || !rec.AllowAllCrew {
t.Fatalf("expected the fetch itself to still succeed, got %+v", rec)
}
if n := captainRows(t, a, holdDID); n != 0 {
t.Errorf("hold_captain_records holds %d row(s) for a DID that runs no hold; "+
"GetAvailableHolds would offer it to every user's hold picker", n)
}
}
// TestGetCaptainRecord_RealHoldIsStillCached is the inverse, so the gate cannot
// degrade into "never cache anything" — which would look like a pass above
// while quietly costing an XRPC round trip on every authorization.
func TestGetCaptainRecord_RealHoldIsStillCached(t *testing.T) {
a := newVerifyAuthorizer(t)
srv := captainServer(t)
holdDID := didFromServer(srv.URL)
prev := hasHoldService
hasHoldService = func(context.Context, string) (bool, error) { return true, nil }
t.Cleanup(func() { hasHoldService = prev })
if _, err := a.GetCaptainRecord(context.Background(), holdDID); err != nil {
t.Fatalf("GetCaptainRecord: %v", err)
}
if n := captainRows(t, a, holdDID); n != 1 {
t.Errorf("hold_captain_records holds %d rows for a verified hold, want 1", n)
}
}
// TestGetCaptainRecord_UnresolvableDIDIsNotCached: a resolution failure is not
// evidence that the DID runs a hold, so it must not seed a durable row either.
func TestGetCaptainRecord_UnresolvableDIDIsNotCached(t *testing.T) {
a := newVerifyAuthorizer(t)
srv := captainServer(t)
holdDID := didFromServer(srv.URL)
prev := hasHoldService
hasHoldService = func(context.Context, string) (bool, error) {
return false, context.DeadlineExceeded
}
t.Cleanup(func() { hasHoldService = prev })
if _, err := a.GetCaptainRecord(context.Background(), holdDID); err != nil {
t.Fatalf("GetCaptainRecord should still answer from the live fetch: %v", err)
}
if n := captainRows(t, a, holdDID); n != 0 {
t.Errorf("cached %d row(s) for an unresolvable DID", n)
}
}