Files
at-container-registry/test/integration/bootstrap_test.go
2026-05-11 19:53:13 -05:00

107 lines
4.4 KiB
Go

//go:build integration
package integration
import (
"encoding/json"
"net/http"
"net/url"
"testing"
"time"
"atcr.io/internal/testharness"
"atcr.io/pkg/atproto"
)
// TestAuthTokenBootstrapsLexicons verifies that a user who has never pushed
// or pulled — and therefore has no sailor profile in their PDS and no crew
// record in the hold's PDS — gets both written by the time their very first
// /auth/token call returns.
//
// The two writes happen on different code paths:
// - sailor profile: post-auth callback runs storage.EnsureProfile against
// the user's PDS (the fake testpds in this test).
// - hold crew: authgate.Authorize runs storage.EnsureCrewMembership, which
// mints a service-auth via the user's PDS and POSTs requestCrew to the
// hold, which writes io.atcr.hold.crew into its embedded PDS.
//
// Both run during the same /auth/token request (the post-auth callback is
// synchronous and the authgate goroutine is awaited before the handler
// returns), so a 200 from /auth/token is the signal that both records exist.
func TestAuthTokenBootstrapsLexicons(t *testing.T) {
h := testharness.New(t)
// Use AddStranger rather than AddSailor: a stranger has a PDS identity
// and a users-table row (so PDS resolution works) but no crew_members
// row anywhere. That's the closest thing to a brand-new user — no
// pre-seeded crew membership in either the AppView or the hold's PDS,
// and no sailor profile record in their PDS.
sailor := h.AddStranger("newcomer.test")
// Pre-condition: no sailor profile in the user's PDS.
if _, ok := h.PDS.GetRecord(sailor.DID(), atproto.SailorProfileCollection, "self"); ok {
t.Fatalf("expected no sailor profile before /auth/token, but found one for %s", sailor.DID())
}
// Pre-condition: no crew record for this user in the hold's PDS. The
// lookup wraps repomgr's "not found" without a sentinel, so any non-nil
// error here means "absent" — and a successful return is what we want
// to assert is impossible at this point.
if _, _, err := h.Hold.PDS.GetCrewMemberByDID(t.Context(), sailor.DID()); err == nil {
t.Fatalf("expected no crew record before /auth/token, but found one for %s", sailor.DID())
}
// Trigger /auth/token via Basic auth. Docker's login does this same call
// (no scope param) just to validate creds. The handler caches the access
// token, runs the post-auth callback (EnsureProfile), and waits on the
// authgate goroutine (EnsureCrewMembership) before returning.
tokenURL := h.AppViewURL + "/auth/token?service=" + url.QueryEscape("127.0.0.1")
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, tokenURL, nil)
if err != nil {
t.Fatalf("build token request: %v", err)
}
req.SetBasicAuth(sailor.Handle(), sailor.Identity.Password)
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("call /auth/token: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("/auth/token: want 200, got %d", resp.StatusCode)
}
// Post-condition: sailor profile now exists in the user's PDS, anchored
// to the AppView's default hold. EnsureProfile runs synchronously inside
// the post-auth callback, so it's done by the time the handler returns.
raw, ok := h.PDS.GetRecord(sailor.DID(), atproto.SailorProfileCollection, "self")
if !ok {
t.Fatalf("expected sailor profile in PDS after /auth/token, found none for %s", sailor.DID())
}
var profile atproto.SailorProfileRecord
if err := json.Unmarshal(raw, &profile); err != nil {
t.Fatalf("decode sailor profile: %v", err)
}
if profile.DefaultHold != h.HoldDID {
t.Errorf("sailor profile defaultHold = %q, want %q", profile.DefaultHold, h.HoldDID)
}
// Post-condition: hold's embedded PDS now has a crew record for this
// user. The authgate runs EnsureCrewMembership inside a goroutine that
// the handler awaits, so this should be visible by the time /auth/token
// returned. We allow a small poll window to absorb any IPC scheduling
// jitter (the records-index update on the hold side is its own goroutine
// behind the repomgr write).
deadline := time.Now().Add(2 * time.Second)
var lastErr error
for time.Now().Before(deadline) {
if _, _, err := h.Hold.PDS.GetCrewMemberByDID(t.Context(), sailor.DID()); err == nil {
return
} else {
lastErr = err
}
time.Sleep(25 * time.Millisecond)
}
t.Fatalf("expected hold crew record after /auth/token for %s, last lookup error: %v", sailor.DID(), lastErr)
}