package console import ( "context" "encoding/json" "errors" "os" "testing" "time" "github.com/carlosframework/platform/internal/hostops" "github.com/carlosframework/platform/internal/mail" "github.com/carlosframework/platform/internal/release" ) func TestClaimFlagOffRevokeIgnoresCancelledContext(t *testing.T) { for _, byValue := range []bool{false, true} { t.Run(map[bool]string{false: "id", true: "value"}[byValue], func(t *testing.T) { st := openTestStore(t) raw := mintUserToken(t, st, "owner@example.com", time.Now()) ctx, cancel := context.WithCancel(t.Context()) cancel() var err error if byValue { err = st.RevokeCLITokenValueContext(ctx, raw) } else { err = st.RevokeCLITokenContext(ctx, "owner@example.com", hashToken(raw)) } if err != nil { t.Fatalf("flag-off revoke: %v", err) } rows, err := st.CLITokens("owner@example.com") if err != nil || len(rows) != 0 { t.Fatalf("token still live: %v %v", rows, err) } if byValue { err = st.RevokeCLITokenValueContext(ctx, raw) if err != nil { t.Fatal(err) } } else { err = st.RevokeCLITokenContext(ctx, "owner@example.com", hashToken(raw)) if !errors.Is(err, errNoSuchCLIToken) { t.Fatalf("missing token: %v", err) } } }) } } func TestClaimServiceCredentialLookupError(t *testing.T) { for _, gated := range []bool{false, true} { t.Run(map[bool]string{false: "off", true: "on"}[gated], func(t *testing.T) { st := openTestStore(t) if gated { st = newClaimCase(t).st } st.Close() _, want := st.ServicePrincipals(OpsAccountID) if want == nil { t.Fatal("closed DB lookup succeeded") } called := false err := (&Server{store: st}).serviceCredentialWrite(t.Context(), &Account{ID: OpsAccountID}, serviceHandle{name: "test"}, "test", func(*Store) error { called = true; return nil }) if gated { if called || err == nil || err.Error() != want.Error() { t.Fatalf("lookup error swallowed: called=%v err=%v want=%v", called, err, want) } } else if !called || err != nil { t.Fatalf("flag-off changed: called=%v err=%v", called, err) } }) } } type ambiguousDisassociate struct { mail.Provisioner err error disassociations, deletes int } func (p *ambiguousDisassociate) DisassociateTenantResource(ctx context.Context, tenant, resource string) error { p.disassociations++ if err := p.Provisioner.DisassociateTenantResource(ctx, tenant, resource); err != nil { return err } return p.err } func (p *ambiguousDisassociate) DeleteIdentity(ctx context.Context, region, domain string) error { p.deletes++ return p.Provisioner.DeleteIdentity(ctx, region, domain) } func TestClaimMailDiscardedBackendErrorRetainsBothLocks(t *testing.T) { cx := newClaimCase(t) ctx := t.Context() const domain = "remove.example.com" d := cx.srv.cfg.MailDeployment if _, _, err := mail.EnsureDomain(ctx, cx.srv.claimMailStore(), cx.srv.cfg.MailProvisioner, cx.srv.cfg.DNSWriter, d, "bab", "titogo", domain, time.Now()); err != nil { t.Fatal(err) } uncertain := errors.New("lost disassociation response") p := &ambiguousDisassociate{Provisioner: cx.srv.cfg.MailProvisioner, err: uncertain} _, err := mail.RemoveDomain(ctx, cx.srv.claimMailStore(), p, cx.srv.cfg.DNSWriter, d, "bab", "titogo", domain) if p.disassociations == 0 || p.deletes == 0 { t.Fatalf("did not reach failing and successful backend calls: %+v err=%v", p, err) } if rec, _, e := mail.GetDomain(ctx, cx.rel, "bab", domain); e != nil || rec != nil { t.Fatalf("later removal did not succeed: %v %v", rec, e) } a, e := hostops.ReadCanonicalAdmission(ctx, cx.rel, "bab", "titogo") if e != nil { t.Fatal(e) } if a.Value.Op == nil { t.Error("app admission released after ambiguous disassociation") } held := false for _, effect := range a.Value.Effects { if effect.State == "uncertain" { held = true } } if !held { t.Errorf("no uncertain effect: %+v", a.Value.Effects) } raw, _, e := cx.rel.Get(ctx, hostops.MailGateKey) if e != nil { t.Fatal(e) } var gate struct { State string `json:"state"` } if e = json.Unmarshal(raw, &gate); e != nil { t.Fatal(e) } if gate.State != "held" { t.Errorf("mail gate released: %s", raw) } } func TestClaimPurgeMailRecordRefusesBeforeEffects(t *testing.T) { for _, tc := range []struct{ name, key, body string }{ {"sender schema", "email/senders/app.json", `{"schema":2,"id":"app","scope":"app","source":"domain","provisioned":{"iam_user":"carlos-mail-bab-titogo"}}`}, {"sender identity", "email/senders/app.json", `{"schema":1,"id":"other","scope":"app","source":"domain","provisioned":{"iam_user":"carlos-mail-bab-titogo"}}`}, {"legacy schema", "email.json", `{"schema":2,"app":"titogo","provisioned":{"iam_user":"carlos-mail-bab-titogo"}}`}, {"legacy identity", "email.json", `{"schema":1,"app":"other","provisioned":{"iam_user":"carlos-mail-bab-titogo"}}`}, } { for _, stage := range []string{"readiness", "request", "teardown", "mail teardown"} { t.Run(tc.name+"/"+stage, func(t *testing.T) { cx := newClaimCase(t) cx.purgeWorld(t) p := cx.srv.cfg.ClaimPurge ctx := t.Context() const id = "strict-mail-purge" appID := cx.appIDs[claimAppRef{Account: "bab", App: "titogo"}] var h *hostops.OperationHandle if stage == "teardown" { got, err := p.Request(ctx, "bab", "titogo", id, "owner", claimFxOwner) if got != id || err != nil && !errors.Is(err, hostops.ErrPending) { t.Fatalf("request: %s %v", got, err) } var err2 error h, err2 = p.Authority.Control.ResumeExclusive(ctx, "bab", "titogo", appID, id, hostops.OpPurging, p.Owner) if err2 != nil { t.Fatal(err2) } j, _, err := hostops.ReadPurgeJournal(ctx, cx.rel, "bab", "titogo", id, appID) if err != nil { t.Fatal(err) } j.Phase = "claimed" // Stage fixture: host barriers are tested separately. key, _ := hostops.PurgeKey("bab", "titogo") putMailJSON(t, cx.rel, key, j) } key := release.AppPrefix("bab", "titogo") + tc.key if _, err := cx.rel.Put(ctx, key, []byte(tc.body), ""); err != nil { t.Fatal(err) } legacy, _, err := mail.Get(ctx, cx.rel, "bab", "titogo") if err != nil || legacy == nil || legacy.Schema != mail.Schema || legacy.App != "titogo" { t.Fatalf("flag-off conversion changed: %+v %v", legacy, err) } mark := cx.rec.mark() if stage == "mail teardown" { err = mail.Teardown(ctx, cx.srv.claimMailStore(), cx.srv.cfg.MailProvisioner, cx.srv.cfg.DNSWriter, cx.srv.cfg.MailDeployment, "bab", "titogo") } else if stage == "readiness" { err = p.mailReady(ctx, "bab", "titogo") } else if stage == "request" { _, err = p.Request(ctx, "bab", "titogo", id, "owner", claimFxOwner) journal, _ := hostops.PurgeKey("bab", "titogo") if _, _, e := cx.rel.Get(ctx, journal); !errors.Is(e, os.ErrNotExist) { t.Errorf("invalid inventory reserved purge: %v", e) } } else { err = p.stage(ctx, h, "teardown", func(ctx context.Context) error { scope := mail.ClaimMailScope{Account: "bab", App: "titogo", AppID: appID, Operation: id} return mail.WithClaimMailResource(mail.WithClaimMailScope(ctx, scope), cx.srv.claimMailStore(), func(held context.Context) error { return p.teardown(held, h, "bab", "titogo", id, appID) }) }) } if err == nil || errors.Is(err, hostops.ErrPending) { t.Errorf("invalid inventory was not refused: %v", err) } for _, e := range cx.rec.since(mark) { if !e.Control && (e.Unscoped || len(e.Scopes) > 0) { t.Errorf("effect before validation: %v", e) } } if _, _, e := cx.rel.Get(ctx, key); e != nil { t.Errorf("invalid estate record removed: %v", e) } }) } } }