//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) }