package jetstream import ( "context" "encoding/json" "net/http" "net/http/httptest" "testing" "time" "atcr.io/pkg/appview/db" "atcr.io/pkg/atproto" ) // When a profile's default hold can't be resolved, the processor must cache the // raw reference (non-empty) rather than leaving default_hold_did = "". The // billing gate treats "" as the operator's managed default, so an empty value // for a genuinely self-hosted (but unreachable) hold would fail OPEN. A raw, // non-managed value reads as non-managed → fails closed. func TestProcessSailorProfile_UnresolvableHold_CachesRawValue(t *testing.T) { database, err := db.InitDB(":memory:", db.LibsqlConfig{}) if err != nil { t.Fatalf("init db: %v", err) } defer database.Close() const did = "did:plc:failclosed" if err := db.UpsertUser(database, &db.User{ DID: did, Handle: "fc.test", PDSEndpoint: "https://pds", LastSeen: time.Now(), }); err != nil { t.Fatalf("upsert user: %v", err) } // A hold whose .well-known/atproto-did returns 404 → ResolveHoldDID fails // deterministically without depending on external DNS/network. holdSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.NotFound(w, r) })) defer holdSrv.Close() record, err := json.Marshal(atproto.SailorProfileRecord{ Type: "io.atcr.sailor.profile", DefaultHold: holdSrv.URL, // URL-form, unresolvable → resolution fails CreatedAt: time.Now().UTC(), }) if err != nil { t.Fatalf("marshal profile: %v", err) } p := NewProcessor(database, false, nil) if err := p.ProcessSailorProfile(context.Background(), did, record, nil); err != nil { t.Fatalf("ProcessSailorProfile: %v", err) } got := db.GetUserDefaultHoldDID(database, did) if got == "" { t.Fatal("default_hold_did is empty — would fail OPEN for a self-hosted user") } if got != holdSrv.URL { t.Errorf("default_hold_did = %q, want raw value %q", got, holdSrv.URL) } }