package mail import ( "context" "encoding/json" "errors" "os" "strings" "testing" "time" "github.com/carlosframework/platform/internal/appconfig" "github.com/carlosframework/platform/internal/dnsrec" "github.com/carlosframework/platform/internal/release" ) // Rotation leaves the OLD key ACTIVE. Adoption swaps a symlink; it does not // restart processes, so an instance may still be using the old credential // for minutes after delivery. Killing it at rotation time is an outage. func TestRotateKeepsTheOldKeyLive(t *testing.T) { ctx, s, p, pub, now := enableFixture(t) first, err := Enable(ctx, s, p, pub, testDeployment(), "bag", "harbourmail", now) if err != nil { t.Fatalf("Enable: %v", err) } old := first.Provisioned.KeyID rec, err := Rotate(ctx, s, p, pub, testDeployment(), "bag", "harbourmail", now) if err != nil { t.Fatalf("Rotate: %v", err) } if rec.Provisioned.KeyID == old { t.Fatal("Rotate did not mint a new key") } if rec.Provisioned.PrevKeyID != old { t.Fatalf("PrevKeyID = %q, want %q", rec.Provisioned.PrevKeyID, old) } keys := p.Users[UserName("bag", "harbourmail")].Keys if _, ok := keys[old]; !ok { t.Fatal("the old key was deleted at rotation — instances still using it would fail") } if !keys[old].Active { t.Fatal("the old key was deactivated at rotation") } } // C1: Rotate must not silently lift a standing pause at IAM. MintKey always // returns an ACTIVE key regardless of the record's own Active flag; without // the enforcement point, a paused app's record keeps saying paused (and the // console and audit log keep agreeing) while the freshly minted key is // live — an ordinary member `carlos email rotate` would defeat an abuse // pause. func TestRotateDoesNotLiftAPauseAtIAM(t *testing.T) { ctx, s, p, pub, now := enableFixture(t) if _, err := Enable(ctx, s, p, pub, testDeployment(), "bag", "harbourmail", now); err != nil { t.Fatalf("Enable: %v", err) } if _, err := SetPaused(ctx, s, p, "bag", "harbourmail", true, "", now); err != nil { t.Fatalf("SetPaused(true): %v", err) } rec, err := Rotate(ctx, s, p, pub, testDeployment(), "bag", "harbourmail", now) if err != nil { t.Fatalf("Rotate: %v", err) } if rec.Provisioned.Active { t.Fatal("rotate flipped the record back to active") } user := UserName("bag", "harbourmail") k, ok := p.Users[user].Keys[rec.Provisioned.KeyID] if !ok { t.Fatalf("minted key %s does not exist at all", rec.Provisioned.KeyID) } if k.Active { t.Fatal("rotate minted a LIVE key for a paused app — the pause was silently lifted at IAM") } } // Finish is the only walk that destroys a credential an app might still be // using, so it is explicit and separate. func TestRotateFinishClosesTheWindow(t *testing.T) { ctx, s, p, pub, now := enableFixture(t) Enable(ctx, s, p, pub, testDeployment(), "bag", "harbourmail", now) rec, _ := Rotate(ctx, s, p, pub, testDeployment(), "bag", "harbourmail", now) old := rec.Provisioned.PrevKeyID done, err := RotateFinish(ctx, s, p, "bag", "harbourmail") if err != nil { t.Fatalf("RotateFinish: %v", err) } if done.Provisioned.PrevKeyID != "" { t.Fatal("PrevKeyID survived the finish") } if _, ok := p.Users[UserName("bag", "harbourmail")].Keys[old]; ok { t.Fatal("the old key survived the finish") } } // I1: Enable's repair re-walk has the identical defect as Rotate. Task 12 // made Enable carry Active forward in the RECORD; that alone is half the // feature — a repair re-walk still reaps the paused key and mints a fresh // one, and MintKey always hands back a live key. func TestEnableRepairDoesNotLiftAPauseAtIAM(t *testing.T) { ctx, s, p, pub, now := enableFixture(t) if _, err := Enable(ctx, s, p, pub, testDeployment(), "bag", "harbourmail", now); err != nil { t.Fatalf("first Enable: %v", err) } if _, err := SetPaused(ctx, s, p, "bag", "harbourmail", true, "", now); err != nil { t.Fatalf("SetPaused(true): %v", err) } // Clear a delivered var so the idempotence short-circuit cannot fire — // this forces Enable to actually re-walk (reap + re-mint) rather than // return early. if _, err := appconfig.Unset(ctx, s, "bag", "harbourmail", []string{"HARBOURMAIL_SMTP_HOST"}); err != nil { t.Fatalf("Unset: %v", err) } rec, err := Enable(ctx, s, p, pub, testDeployment(), "bag", "harbourmail", now) if err != nil { t.Fatalf("repair Enable: %v", err) } if rec.Provisioned.Active { t.Fatal("repair re-walk flipped the record back to active") } user := UserName("bag", "harbourmail") k, ok := p.Users[user].Keys[rec.Provisioned.KeyID] if !ok { t.Fatalf("minted key %s does not exist at all", rec.Provisioned.KeyID) } if k.Active { t.Fatal("repair re-walk minted a LIVE key for a paused app — the pause was silently lifted at IAM") } } // The pause is SetKeyActive, and it must be reversible with no policy // rewrite — a policy rewrite would have to be undone correctly to resume // and could half-fail into a broken grant. func TestPauseAndResumeToggleTheKeyNotThePolicy(t *testing.T) { ctx, s, p, pub, now := enableFixture(t) rec, _ := Enable(ctx, s, p, pub, testDeployment(), "bag", "harbourmail", now) user := UserName("bag", "harbourmail") policyBefore := p.Users[user].Policy paused, err := SetPaused(ctx, s, p, "bag", "harbourmail", true, "", now) if err != nil { t.Fatalf("SetPaused(true): %v", err) } if paused.Provisioned.Active { t.Fatal("record still says active after a pause") } if p.Users[user].Keys[rec.Provisioned.KeyID].Active { t.Fatal("the key is still active after a pause") } if p.Users[user].Policy != policyBefore { t.Fatal("the pause rewrote the policy — it must only toggle the key") } resumed, err := SetPaused(ctx, s, p, "bag", "harbourmail", false, "", now) if err != nil { t.Fatalf("SetPaused(false): %v", err) } if !resumed.Provisioned.Active || !p.Users[user].Keys[rec.Provisioned.KeyID].Active { t.Fatal("resume did not reactivate") } } // SetPaused must toggle BOTH keys — an app mid-rotation still holds the // previous credential, so pausing only the current one leaves it sending. func TestPauseTogglesBothKeysDuringARotationOverlap(t *testing.T) { ctx, s, p, pub, now := enableFixture(t) Enable(ctx, s, p, pub, testDeployment(), "bag", "harbourmail", now) rec, err := Rotate(ctx, s, p, pub, testDeployment(), "bag", "harbourmail", now) if err != nil { t.Fatalf("Rotate: %v", err) } user := UserName("bag", "harbourmail") current := rec.Provisioned.KeyID prev := rec.Provisioned.PrevKeyID if prev == "" { t.Fatal("fixture broken: expected a PrevKeyID after Rotate") } paused, err := SetPaused(ctx, s, p, "bag", "harbourmail", true, "", now) if err != nil { t.Fatalf("SetPaused(true): %v", err) } if paused.Provisioned.Active { t.Fatal("record still says active after a pause") } if p.Users[user].Keys[current].Active { t.Fatal("the current key is still active after a pause") } if p.Users[user].Keys[prev].Active { t.Fatal("the previous key (mid-rotation overlap) is still active after a pause — it could still be sending") } } // I2: SetPaused must reach standalone principals too. Each is its own IAM // user, so there is no two-key ceiling interaction to worry about; skipping // them leaves a tenant paused for abuse still sending through, say, a // laptop credential on the very domain the pause was meant to protect. func TestPauseReachesStandaloneCredentials(t *testing.T) { ctx, s, p, pub, now := enableFixture(t) Enable(ctx, s, p, pub, testDeployment(), "bag", "harbourmail", now) if _, err := CreateStandalone(ctx, s, p, testDeployment(), "bag", "harbourmail", "laptop", "owner@example.test", now); err != nil { t.Fatalf("CreateStandalone: %v", err) } standaloneUser := StandaloneUserName("bag", "harbourmail", "laptop") rec, _, err := Get(ctx, s, "bag", "harbourmail") if err != nil { t.Fatalf("Get: %v", err) } var keyID string for _, sa := range rec.Standalone { if sa.Name == "laptop" { keyID = sa.KeyID } } if keyID == "" { t.Fatal("fixture broken: standalone entry missing from record") } if _, err := SetPaused(ctx, s, p, "bag", "harbourmail", true, "", now); err != nil { t.Fatalf("SetPaused(true): %v", err) } su, ok := p.Users[standaloneUser] if !ok { t.Fatalf("standalone user %s does not exist at all", standaloneUser) } k, ok := su.Keys[keyID] if !ok { t.Fatalf("standalone key %s does not exist at all", keyID) } if k.Active { t.Fatal("pause did not reach the standalone credential — it can still send") } if _, err := SetPaused(ctx, s, p, "bag", "harbourmail", false, "", now); err != nil { t.Fatalf("SetPaused(false): %v", err) } su, ok = p.Users[standaloneUser] if !ok { t.Fatalf("resume left the standalone user %s gone entirely", standaloneUser) } k, ok = su.Keys[keyID] if !ok { t.Fatalf("resume left the standalone key %s gone entirely", keyID) } if !k.Active { t.Fatal("resume did not reach the standalone credential") } } // Round 2, item 1: CreateStandalone is a THIRD mint path and must go // through the same enforcement point as Enable and Rotate. Without it, a // member verb (`carlos email credentials create`) could hand a paused // tenant a live sending credential on the same verified domain the pause // was meant to protect — exactly the C1 shape, on a path C1's original fix // did not cover. func TestCreateStandaloneDoesNotBypassAPause(t *testing.T) { ctx, s, p, pub, now := enableFixture(t) if _, err := Enable(ctx, s, p, pub, testDeployment(), "bag", "harbourmail", now); err != nil { t.Fatalf("Enable: %v", err) } if _, err := SetPaused(ctx, s, p, "bag", "harbourmail", true, "", now); err != nil { t.Fatalf("SetPaused(true): %v", err) } creds, err := CreateStandalone(ctx, s, p, testDeployment(), "bag", "harbourmail", "laptop", "owner@example.test", now) if err != nil { t.Fatalf("CreateStandalone: %v", err) } standaloneUser := StandaloneUserName("bag", "harbourmail", "laptop") su, ok := p.Users[standaloneUser] if !ok { t.Fatalf("standalone user %s does not exist at all", standaloneUser) } k, ok := su.Keys[creds.Username] if !ok { t.Fatalf("minted standalone key %s does not exist at all", creds.Username) } if k.Active { t.Fatal("CreateStandalone minted a LIVE key for a paused app — the pause was bypassed") } } // A re-mint for an existing standalone name while paused must not reactivate // it either, and a resume must reach a standalone credential created while // paused. func TestCreateStandaloneReMintWhilePausedStaysInactiveAndResumeReactivates(t *testing.T) { ctx, s, p, pub, now := enableFixture(t) if _, err := Enable(ctx, s, p, pub, testDeployment(), "bag", "harbourmail", now); err != nil { t.Fatalf("Enable: %v", err) } if _, err := SetPaused(ctx, s, p, "bag", "harbourmail", true, "", now); err != nil { t.Fatalf("SetPaused(true): %v", err) } if _, err := CreateStandalone(ctx, s, p, testDeployment(), "bag", "harbourmail", "laptop", "owner@example.test", now); err != nil { t.Fatalf("CreateStandalone: %v", err) } // Re-mint the same name while still paused. creds, err := CreateStandalone(ctx, s, p, testDeployment(), "bag", "harbourmail", "laptop", "owner@example.test", now) if err != nil { t.Fatalf("CreateStandalone (re-mint): %v", err) } standaloneUser := StandaloneUserName("bag", "harbourmail", "laptop") su, ok := p.Users[standaloneUser] if !ok { t.Fatalf("standalone user %s does not exist at all", standaloneUser) } k, ok := su.Keys[creds.Username] if !ok { t.Fatalf("minted standalone key %s does not exist at all", creds.Username) } if k.Active { t.Fatal("a re-mint while paused produced a LIVE key") } if _, err := SetPaused(ctx, s, p, "bag", "harbourmail", false, "", now); err != nil { t.Fatalf("SetPaused(false): %v", err) } su, ok = p.Users[standaloneUser] if !ok { t.Fatalf("resume left the standalone user %s gone entirely", standaloneUser) } k, ok = su.Keys[creds.Username] if !ok { t.Fatalf("resume left the standalone key %s gone entirely", creds.Username) } if !k.Active { t.Fatal("resume did not reach a standalone credential created while paused") } } // A standalone credential lives on its OWN IAM user, because the delivered // user's two key slots are spent on the delivered key and the rotation // overlap. Sharing would work until the first rotation and then break it. func TestStandaloneGetsItsOwnUserAndDoesNotDisturbDelivery(t *testing.T) { ctx, s, p, pub, now := enableFixture(t) rec, _ := Enable(ctx, s, p, pub, testDeployment(), "bag", "harbourmail", now) delivered := rec.Provisioned.KeyID creds, err := CreateStandalone(ctx, s, p, testDeployment(), "bag", "harbourmail", "laptop", "owner@example.test", now) if err != nil { t.Fatalf("CreateStandalone: %v", err) } if creds.Password == "" || creds.Username == "" { t.Fatalf("standalone creds incomplete: %+v", creds) } if creds.Host != "email-smtp.eu-west-1.amazonaws.com" || creds.Port != "587" { t.Fatalf("standalone coordinates wrong: %+v", creds) } own := StandaloneUserName("bag", "harbourmail", "laptop") if _, ok := p.Users[own]; !ok { t.Fatal("standalone credential did not get its own IAM user") } // The delivered user still holds exactly its own key. if len(p.Users[UserName("bag", "harbourmail")].Keys) != 1 { t.Fatal("standalone minting touched the delivered user's key slots") } if err := RevokeStandalone(ctx, s, p, "bag", "harbourmail", "laptop"); err != nil { t.Fatalf("RevokeStandalone: %v", err) } if _, ok := p.Users[own]; ok { t.Fatal("revoke left the standalone user behind") } after, _, _ := Get(ctx, s, "bag", "harbourmail") if after.Provisioned.KeyID != delivered { t.Fatal("revoking a standalone credential disturbed delivery") } } // CreateStandalone must refuse an invalid name — a credential name is not // an env prefix, so ValidEnvPrefix must play no part in the guard (F5). func TestCreateStandaloneRefusesAnInvalidName(t *testing.T) { ctx, s, p, _, now := enableFixture(t) if _, err := CreateStandalone(ctx, s, p, testDeployment(), "bag", "harbourmail", "Not Valid!", "owner@example.test", now); err == nil { t.Fatal("CreateStandalone accepted an invalid name") } } // Teardown removes the identity only when the LAST app referencing it goes. // Otherwise one app's teardown revokes a sibling's verified domain. func TestTeardownKeepsASharedDomainAlive(t *testing.T) { ctx, s, p, pub, now := enableFixture(t) Enable(ctx, s, p, pub, testDeployment(), "bag", "harbourmail", now) // A sibling app joins the same domain. if _, _, err := EnsureDomain(ctx, s, p, memWriterForTest(), testDeployment(), "bag", "marketing", "harbourmail.bag.oncarlos.com", now); err != nil { t.Fatalf("sibling EnsureDomain: %v", err) } // M3: thread a real, inspectable writer through (rather than a fresh // throwaway one) so a wrongly-triggered record deletion would actually // be visible to this test. w := dnsrec.NewMemWriter() if _, _, err := EnsureDomain(ctx, s, p, w, testDeployment(), "bag", "harbourmail", "harbourmail.bag.oncarlos.com", now); err != nil { t.Fatalf("re-publish into the inspectable writer: %v", err) } if len(w.Records) == 0 { t.Fatal("fixture broken: no records published to inspect") } if err := Teardown(ctx, s, p, w, testDeployment(), "bag", "harbourmail"); err != nil { t.Fatalf("Teardown: %v", err) } if _, ok := p.Identities[identityKey("eu-west-1", "harbourmail.bag.oncarlos.com")]; !ok { t.Fatal("teardown deleted an identity a sibling app still references") } dom, _, _ := GetDomain(ctx, s, "bag", "harbourmail.bag.oncarlos.com") if len(dom.Apps) != 1 || dom.Apps[0] != "marketing" { t.Fatalf("apps = %v, want [marketing]", dom.Apps) } if len(w.Records) == 0 { t.Fatal("teardown deleted DNS records for a domain a sibling app still references") } } // I4: the destroy branch's decision must be made on a FRESH read, not the // snapshot ListDomains took at the top of Teardown. A sibling that joins // this domain between that snapshot and the decision must not have its // identity, DKIM tokens, and (for a custom domain) hand-published DNS // destroyed out from under it. func TestTeardownDestroyDecisionSeesASiblingThatJoinsBetweenListAndDecision(t *testing.T) { ctx, s, p, pub, now := enableFixture(t) Enable(ctx, s, p, pub, testDeployment(), "bag", "harbourmail", now) domain := "harbourmail.bag.oncarlos.com" rs := &raceGetStore{ Store: s, key: DomainKey("bag", domain), trigger: 2, // 1st Get is ListDomains' own unmarshal pass; 2nd is Teardown's decision re-read. inject: func() error { cur, etag, err := GetDomain(ctx, s, "bag", domain) if err != nil { return err } cur.AddApp("marketing") _, err = PutDomain(ctx, s, "bag", cur, etag) return err }, } if err := Teardown(ctx, rs, p, memWriterForTest(), testDeployment(), "bag", "harbourmail"); err != nil { t.Fatalf("Teardown: %v", err) } if _, ok := p.Identities[identityKey("eu-west-1", domain)]; !ok { t.Fatal("teardown destroyed an identity a sibling that joined mid-teardown still references") } dom, _, err := GetDomain(ctx, s, "bag", domain) if err != nil { t.Fatalf("GetDomain: %v", err) } if dom == nil { t.Fatal("teardown deleted the domain record even though a sibling joined mid-teardown") } if len(dom.Apps) != 1 || dom.Apps[0] != "marketing" { t.Fatalf("apps = %v, want [marketing]", dom.Apps) } } // raceGetStore wraps a release.Store and, on the Nth Get call for one // specific key, runs inject() immediately before that call returns. It // models a write landing between an earlier snapshot read (ListDomains' own // pass) and a later re-read (Teardown's decision), inside one synchronous // call, without real concurrency. type raceGetStore struct { release.Store key string trigger int calls int inject func() error } func (r *raceGetStore) Get(ctx context.Context, key string) ([]byte, string, error) { if key == r.key { r.calls++ if r.calls == r.trigger && r.inject != nil { if err := r.inject(); err != nil { return nil, "", err } } } return r.Store.Get(ctx, key) } // Teardown removes the identity, every principal (delivered AND standalone — // I3), and every DNS record it published (M3), once it is the LAST // referrer. func TestTeardownRemovesTheIdentityWhenTheLastAppGoes(t *testing.T) { ctx, s, p, pub, now := enableFixture(t) Enable(ctx, s, p, pub, testDeployment(), "bag", "harbourmail", now) if _, err := CreateStandalone(ctx, s, p, testDeployment(), "bag", "harbourmail", "laptop", "owner@example.test", now); err != nil { t.Fatalf("CreateStandalone: %v", err) } standaloneUser := StandaloneUserName("bag", "harbourmail", "laptop") if _, ok := p.Users[standaloneUser]; !ok { t.Fatal("fixture broken: standalone user was not created") } // M3: thread a real, inspectable writer through. w := dnsrec.NewMemWriter() if _, _, err := EnsureDomain(ctx, s, p, w, testDeployment(), "bag", "harbourmail", "harbourmail.bag.oncarlos.com", now); err != nil { t.Fatalf("re-publish into the inspectable writer: %v", err) } if len(w.Records) == 0 { t.Fatal("fixture broken: no records published to inspect") } if err := Teardown(ctx, s, p, w, testDeployment(), "bag", "harbourmail"); err != nil { t.Fatalf("Teardown: %v", err) } if _, ok := p.Identities[identityKey("eu-west-1", "harbourmail.bag.oncarlos.com")]; ok { t.Fatal("teardown left the identity behind after the last app went") } if _, ok := p.Users[UserName("bag", "harbourmail")]; ok { t.Fatal("teardown left the delivered IAM user behind") } if _, ok := p.Users[standaloneUser]; ok { t.Fatal("teardown left the standalone IAM user behind") } if len(w.Records) != 0 { t.Fatalf("teardown left DNS records behind: %v", w.Records) } rec, _, err := Get(ctx, s, "bag", "harbourmail") if err != nil { t.Fatalf("Get: %v", err) } if rec != nil { t.Fatal("teardown left the app's email record behind") } } // I2 (review follow-up): a hard `return err` from DisassociateTenantResource // used to run before DeleteIdentity and before releaseDomainClaim. If the // control-plane role lacks ses:DeleteTenantResourceAssociation (spec §4.2), // AccessDenied is neither AlreadyExists nor NotFound, so it was never // swallowed — and that failed every teardown where the app was the last // referrer of a domain, stranding the domain's global claim, even for a // domain that predates this release and was never actually associated // (spec §4.2's first prerequisite). This test pins the fix: a failed // disassociate is logged, not fatal, and Teardown still reaches // DeleteIdentity and finishes when nothing is really associated. func TestTeardownReachesDeleteIdentityDespiteAFailedDisassociate(t *testing.T) { ctx, s, p, pub, now := enableFixture(t) if _, err := Enable(ctx, s, p, pub, testDeployment(), "bag", "harbourmail", now); err != nil { t.Fatalf("Enable: %v", err) } w := dnsrec.NewMemWriter() if _, _, err := EnsureDomain(ctx, s, p, w, testDeployment(), "bag", "harbourmail", "harbourmail.bag.oncarlos.com", now); err != nil { t.Fatalf("re-publish into the inspectable writer: %v", err) } // Stand in for a domain that predates this release: verified, but with // no real tenant association, because nothing before this release ever // created one. Clearing it after EnsureDomain (which did associate it, // same as it would for any domain added today) reaches that state // without a second fixture. ident := p.Identities[identityKey("eu-west-1", "harbourmail.bag.oncarlos.com")] delete(p.TenantResources[TenantName("bag")], ident.ARN) p.FailNext["DisassociateTenantResource"] = errors.New("AccessDenied: ses:DeleteTenantResourceAssociation") if err := Teardown(ctx, s, p, w, testDeployment(), "bag", "harbourmail"); err != nil { t.Fatalf("Teardown: %v, want the failed disassociate logged and teardown to still finish", err) } if _, ok := p.Identities[identityKey("eu-west-1", "harbourmail.bag.oncarlos.com")]; ok { t.Fatal("teardown left the identity behind after a failed but inconsequential disassociate") } } // The real protection stays: when the identity IS still associated, a // failed disassociate must not let DeleteIdentity through anyway — SES // itself refuses that delete for the same reason (confirmed against the // live API, spec §4.1), and this is what makes that refusal the hard // failure instead of the disassociate call. func TestTeardownStillRefusesADeleteWhileReallyAssociated(t *testing.T) { ctx, s, p, pub, now := enableFixture(t) if _, err := Enable(ctx, s, p, pub, testDeployment(), "bag", "harbourmail", now); err != nil { t.Fatalf("Enable: %v", err) } w := dnsrec.NewMemWriter() if _, _, err := EnsureDomain(ctx, s, p, w, testDeployment(), "bag", "harbourmail", "harbourmail.bag.oncarlos.com", now); err != nil { t.Fatalf("re-publish into the inspectable writer: %v", err) } // Left associated on purpose: EnsureDomain associated it above and // nothing has removed it, so this is the case DeleteIdentity's own // guard exists for. p.FailNext["DisassociateTenantResource"] = errors.New("AccessDenied: ses:DeleteTenantResourceAssociation") err := Teardown(ctx, s, p, w, testDeployment(), "bag", "harbourmail") if err == nil { t.Fatal("Teardown succeeded despite the identity still being associated — the real protection is gone") } if !strings.Contains(err.Error(), "still associated") { t.Fatalf("Teardown error = %v, want DeleteIdentity's own \"still associated\" refusal", err) } if _, ok := p.Identities[identityKey("eu-west-1", "harbourmail.bag.oncarlos.com")]; !ok { t.Fatal("the identity was deleted despite still being associated") } } // Teardown must delete BOTH objects, not just the current one. Every other // Teardown fixture builds its record through Enable/Declare, which write // only senders/app.json, so a fixture like those would pass whether or not // the legacy delete at walk.go's end is even there. This one seeds // email.json directly, the way an app that has never been migrated actually // has it, and checks the legacy key by reading the store directly rather // than through Get: Get falls back to email.json, so it would read back // nil-ish success even if the legacy object were left behind. func TestTeardownDeletesTheLegacyRecordToo(t *testing.T) { ctx := context.Background() s, p := release.NewDirStore(t.TempDir()), NewMemProvisioner("1") now := time.Date(2026, 9, 15, 12, 0, 0, 0, time.UTC) legacy := &Record{Schema: Schema, App: "harbourmail", Declared: &Declared{EnvPrefix: "HARBOURMAIL_SMTP", From: "noreply@x.com", At: now, By: "owner@example.test"}, Provisioned: &Provisioned{IAMUser: "carlos-mail-bag-harbourmail", KeyID: "AKIA1", Active: true, At: now}, } body, err := json.Marshal(legacy) if err != nil { t.Fatal(err) } if _, err := s.Put(ctx, LegacyKey("bag", "harbourmail"), body, ""); err != nil { t.Fatal(err) } w := dnsrec.NewMemWriter() if err := Teardown(ctx, s, p, w, testDeployment(), "bag", "harbourmail"); err != nil { t.Fatalf("Teardown: %v", err) } if _, _, err := s.Get(ctx, Key("bag", "harbourmail")); !errors.Is(err, os.ErrNotExist) { t.Fatalf("senders/app.json survived teardown: %v", err) } if _, _, err := s.Get(ctx, LegacyKey("bag", "harbourmail")); !errors.Is(err, os.ErrNotExist) { t.Fatalf("email.json survived teardown — a ghost record that would resurrect this app's email on the next Get: %v", err) } } // An app sender in per-instance mode refuses to resume. Nothing sets the // flag in this release, but the refusal has to be in place before anything // can: resumeStaleCapPause reactivates every key it touches, including every // standalone, so a cap-paused app sender would otherwise re-arm on the next // UTC day roll the credential every instance still holds. func TestPerInstanceModeRefusesAResume(t *testing.T) { ctx, s, p, pub, now := enableFixture(t) if _, err := Enable(ctx, s, p, pub, testDeployment(), "bag", "harbourmail", now); err != nil { t.Fatal(err) } if _, err := SetPaused(ctx, s, p, "bag", "harbourmail", true, PauseKindCap, now); err != nil { t.Fatal(err) } sn, etag, err := GetSender(ctx, s, "bag", "harbourmail", SenderAppID) if err != nil { t.Fatal(err) } sn.PerInstance = true sn.Provisioned.PauseKind = PauseKindPerInstance if _, err := PutSender(ctx, s, "bag", "harbourmail", sn, etag); err != nil { t.Fatal(err) } if _, err := SetPaused(ctx, s, p, "bag", "harbourmail", false, "", now); !errors.Is(err, ErrPerInstance) { t.Fatalf("resume in per-instance mode = %v, want ErrPerInstance", err) } // Pausing harder is still allowed: the refusal is about re-arming a // credential, not about turning one off. if _, err := SetPaused(ctx, s, p, "bag", "harbourmail", true, PauseKindRate, now); err != nil { t.Fatalf("pausing an app sender in per-instance mode was refused: %v", err) } } func memWriterForTest() dnsrec.Writer { return dnsrec.NewMemWriter() }