package console import ( "context" "path/filepath" "strings" "testing" "time" "github.com/carlosframework/platform/internal/mail" "github.com/carlosframework/platform/internal/release" ) // The two thresholds SES itself reviews an account on. They are per-tenant // here so one tenant's bad list cannot take the estate down (spec §9). func TestRatesAreComputedOverSends(t *testing.T) { c := MailCounts{Sends: 1000, Bounces: 60, Complaints: 2} if got := c.BounceRate(); got < 0.059 || got > 0.061 { t.Fatalf("BounceRate = %v, want ~0.06", got) } if got := c.ComplaintRate(); got < 0.0019 || got > 0.0021 { t.Fatalf("ComplaintRate = %v, want ~0.002", got) } } // Zero sends must be zero rates, not a divide-by-zero or a NaN that // compares false against every threshold and silently never trips. func TestRatesWithNoSendsAreZeroNotNaN(t *testing.T) { c := MailCounts{} if c.BounceRate() != 0 || c.ComplaintRate() != 0 { t.Fatalf("rates on an empty day = %v/%v, want 0/0", c.BounceRate(), c.ComplaintRate()) } } // An untiered account defaults to free rather than to unlimited. This is // what lets the primitive ship before the billing work on #226 lands // (spec §10) — and getting it backwards would hand every unknown account // an uncapped estate. func TestUntieredAccountsGetTheFreeCap(t *testing.T) { if TierCap("") != TierCap("free") { t.Fatal("an untiered account did not default to the free cap") } if TierCap("free") != 50 { t.Fatalf("free cap = %d, want 50", TierCap("free")) } if TierCap("unknown-tier-name") != TierCap("free") { t.Fatal("an unrecognised tier did not fall back to free") } } // mailFixture seeds a declared, granted, provisioned app so enforce has // something to pause. Mirrors storeFixture in apistore_test.go. func mailFixture(t *testing.T) (*consoleFixture, time.Time) { t.Helper() fx := newConsoleFixture(t) now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC) fx.srv.cfg.MailProvisioner = mail.NewMemProvisioner("1") if _, err := fx.st.CreateAppIn(OpsAccountID, "harbourmail", "paul@keymail.dev", now); err != nil { t.Fatal(err) } if _, err := mail.Declare(context.Background(), fx.rel, release.OpsAccount, "harbourmail", "HARBOURMAIL_SMTP", "noreply@x.com", "owner@example.test", now); err != nil { t.Fatal(err) } if _, err := mail.ApplyGrant(context.Background(), fx.rel, release.OpsAccount, "harbourmail", []string{"x.com"}, 50, "tier:free", "tier", now); err != nil { t.Fatal(err) } // Provision by hand: this test is about the limiter, not the walk. rec, etag, _ := mail.Get(context.Background(), fx.rel, release.OpsAccount, "harbourmail") rec.Provisioned = &mail.Provisioned{IAMUser: "u", KeyID: "AKIA1", Active: true, At: now} if _, err := mail.Put(context.Background(), fx.rel, release.OpsAccount, "harbourmail", rec, etag); err != nil { t.Fatal(err) } p := fx.srv.cfg.MailProvisioner.(*mail.MemProvisioner) p.EnsureUser(context.Background(), "u", mail.UserPath, []string{"arn:aws:ses:eu-west-1:1:identity/x.com"}, nil, []string{"x.com"}) return fx, now } func seedCounts(t *testing.T, fx *consoleFixture, day string, c MailCounts) { t.Helper() ctx := context.Background() for i := 0; i < c.Sends; i++ { if err := fx.st.RecordMailEvent(ctx, release.OpsAccount, "harbourmail", mail.SenderAppID, day, mail.EventSend); err != nil { t.Fatal(err) } } for i := 0; i < c.Bounces; i++ { fx.st.RecordMailEvent(ctx, release.OpsAccount, "harbourmail", mail.SenderAppID, day, mail.EventBounce) } for i := 0; i < c.Complaints; i++ { fx.st.RecordMailEvent(ctx, release.OpsAccount, "harbourmail", mail.SenderAppID, day, mail.EventComplaint) } } func TestEnforcePausesOnCapBreach(t *testing.T) { fx, now := mailFixture(t) seedCounts(t, fx, now.UTC().Format("2006-01-02"), MailCounts{Sends: 50}) paused, reason, err := fx.srv.enforceMailLimits(context.Background(), release.OpsAccount, "harbourmail", mail.SenderAppID, now) if err != nil { t.Fatal(err) } if !paused { t.Fatal("cap reached and nothing paused") } if !strings.Contains(reason, "daily cap 50") { t.Fatalf("reason = %q, want it to name the cap", reason) } rec, _, _ := mail.Get(context.Background(), fx.rel, release.OpsAccount, "harbourmail") if rec.Provisioned.Active { t.Fatal("record still says active") } } // Below the send floor, a bad ratio is noise. One bounce on a two-message // morning is a 50% bounce rate and must NOT pause a healthy tenant. func TestEnforceIgnoresRatesBelowTheSendFloor(t *testing.T) { fx, now := mailFixture(t) seedCounts(t, fx, now.UTC().Format("2006-01-02"), MailCounts{Sends: 2, Bounces: 1}) paused, _, err := fx.srv.enforceMailLimits(context.Background(), release.OpsAccount, "harbourmail", mail.SenderAppID, now) if err != nil { t.Fatal(err) } if paused { t.Fatal("paused on a 2-message day — the send floor is not being applied") } } // Bounces only — Complaints stays at 0, so this cannot be mistaken for a // complaint breach (its reason must name "bounce rate", never "complaint // rate"). 6% clears BounceThreshold (5%); Sends=1000 comfortably clears // MinSendsForRates. func TestEnforcePausesOnBounceRate(t *testing.T) { fx, now := mailFixture(t) // Cap is 50, so raise it first: this test is about the RATE, and a // cap breach would mask it. mail.ApplyGrant(context.Background(), fx.rel, release.OpsAccount, "harbourmail", []string{"x.com"}, 100000, "operator", "owner@example.test", now) seedCounts(t, fx, now.UTC().Format("2006-01-02"), MailCounts{Sends: 1000, Bounces: 60}) paused, reason, err := fx.srv.enforceMailLimits(context.Background(), release.OpsAccount, "harbourmail", mail.SenderAppID, now) if err != nil { t.Fatal(err) } if !paused { t.Fatal("a 6% bounce rate did not pause") } if !strings.Contains(reason, "bounce rate") { t.Fatalf("reason = %q, want it to name the bounce rate", reason) } if strings.Contains(reason, "complaint rate") { t.Fatalf("reason = %q names the complaint rate on a bounce-only breach", reason) } } // Complaints only — Bounces stays at 0, so this cannot be mistaken for a // bounce breach (its reason must name "complaint rate", never "bounce // rate"). 0.5% clears ComplaintThreshold (0.1%) while staying far under // BounceThreshold (5%), so a threshold swap (BounceThreshold and // ComplaintThreshold exchanged) makes this scenario fail to pause at all: // the swapped ComplaintThreshold becomes 5%, which 0.5% no longer clears, // and BounceRate is 0 so the bounce case never fires either. That is // exactly the silent-swap failure mode this test exists to catch. func TestEnforcePausesOnComplaintRate(t *testing.T) { fx, now := mailFixture(t) // Cap is 50, so raise it first: this test is about the RATE, and a // cap breach would mask it. mail.ApplyGrant(context.Background(), fx.rel, release.OpsAccount, "harbourmail", []string{"x.com"}, 100000, "operator", "owner@example.test", now) seedCounts(t, fx, now.UTC().Format("2006-01-02"), MailCounts{Sends: 1000, Complaints: 5}) paused, reason, err := fx.srv.enforceMailLimits(context.Background(), release.OpsAccount, "harbourmail", mail.SenderAppID, now) if err != nil { t.Fatal(err) } if !paused { t.Fatal("a 0.5% complaint rate did not pause") } if !strings.Contains(reason, "complaint rate") { t.Fatalf("reason = %q, want it to name the complaint rate", reason) } if strings.Contains(reason, "bounce rate") { t.Fatalf("reason = %q names the bounce rate on a complaint-only breach", reason) } } // BounceThreshold and ComplaintThreshold are SES's own published // account-review thresholds, not values this package chose — pinned here so // a later edit that swaps or "tidies" them is caught even if the scenario // numbers above are changed too. func TestSESThresholdConstants(t *testing.T) { if BounceThreshold != 0.05 { t.Fatalf("BounceThreshold = %v, want 0.05 (SES's published bounce-rate review threshold)", BounceThreshold) } if ComplaintThreshold != 0.001 { t.Fatalf("ComplaintThreshold = %v, want 0.001 (SES's published complaint-rate review threshold)", ComplaintThreshold) } } // A pause must not lift itself. Re-running enforce on a paused app with a // clean slate must leave it paused — resume is a decision, not a timer. // I3 (review follow-up): a CAP pause is a PER-DAY limit — the counts it was // computed from are per-day — so it MUST clear on the UTC day roll, or a // free-tier member's ordinary first day over 50 messages stops their email // forever, with resume gated behind an operator allowlist that ships empty // (CARLOS_MAIL_OPERATORS, fail-closed). This test used to assert the // OPPOSITE of what it asserts now — see the note SetPaused's own doc // comment carries about the day-roll check this behaviour depends on. func TestEnforceCapPauseResumesOnTheNextUTCDay(t *testing.T) { fx, now := mailFixture(t) seedCounts(t, fx, now.UTC().Format("2006-01-02"), MailCounts{Sends: 50}) if _, _, err := fx.srv.enforceMailLimits(context.Background(), release.OpsAccount, "harbourmail", mail.SenderAppID, now); err != nil { t.Fatal(err) } rec, _, _ := mail.Get(context.Background(), fx.rel, release.OpsAccount, "harbourmail") if rec.Provisioned.Active { t.Fatal("did not pause on the cap breach") } if rec.Provisioned.PauseKind != mail.PauseKindCap { t.Fatalf("PauseKind = %q, want %q", rec.Provisioned.PauseKind, mail.PauseKindCap) } next := now.AddDate(0, 0, 1) // a fresh UTC day, no counts at all paused, _, err := fx.srv.enforceMailLimits(context.Background(), release.OpsAccount, "harbourmail", mail.SenderAppID, next) if err != nil { t.Fatal(err) } if paused { t.Fatal("enforce reports a NEW pause on the day-roll resume — nothing should trip on an empty day") } rec, _, _ = mail.Get(context.Background(), fx.rel, release.OpsAccount, "harbourmail") if !rec.Provisioned.Active { t.Fatal("a cap pause from yesterday did not resume on the UTC day roll") } if rec.Provisioned.PauseKind != "" { t.Fatalf("PauseKind = %q after the auto-resume, want cleared", rec.Provisioned.PauseKind) } entries, err := fx.st.Events(OpsAccountID, "harbourmail", 10) if err != nil { t.Fatal(err) } found := false for _, e := range entries { if strings.Contains(e.Action, "mail.resumed") { found = true } } if !found { t.Fatal("no audit entry for the automatic day-roll resume") } } // The symmetric case: a RATE pause (a bounce/complaint threshold breach) // never auto-resumes, day roll or not — SES's account-review thresholds // are not a daily allowance, and lifting the brake on a schedule would let // an abusive tenant breach it again every day forever. func TestEnforceRatePauseNeverAutoResumes(t *testing.T) { fx, now := mailFixture(t) // Cap is 50, so raise it first: this test is about the RATE pause, and // a cap breach would mask which kind actually paused it. if _, err := mail.ApplyGrant(context.Background(), fx.rel, release.OpsAccount, "harbourmail", []string{"x.com"}, 100000, "operator", "owner@example.test", now); err != nil { t.Fatal(err) } seedCounts(t, fx, now.UTC().Format("2006-01-02"), MailCounts{Sends: 1000, Bounces: 60}) if _, _, err := fx.srv.enforceMailLimits(context.Background(), release.OpsAccount, "harbourmail", mail.SenderAppID, now); err != nil { t.Fatal(err) } rec, _, _ := mail.Get(context.Background(), fx.rel, release.OpsAccount, "harbourmail") if rec.Provisioned.Active { t.Fatal("did not pause on the bounce-rate breach") } if rec.Provisioned.PauseKind != mail.PauseKindRate { t.Fatalf("PauseKind = %q, want %q", rec.Provisioned.PauseKind, mail.PauseKindRate) } next := now.AddDate(0, 0, 1) // a fresh UTC day paused, _, err := fx.srv.enforceMailLimits(context.Background(), release.OpsAccount, "harbourmail", mail.SenderAppID, next) if err != nil { t.Fatal(err) } if paused { t.Fatal("enforce re-paused an already-paused app — it should have returned early") } rec, _, _ = mail.Get(context.Background(), fx.rel, release.OpsAccount, "harbourmail") if rec.Provisioned.Active { t.Fatal("a rate pause auto-resumed on the UTC day roll — it must not") } if rec.Provisioned.PauseKind != mail.PauseKindRate { t.Fatalf("PauseKind = %q after a no-op day roll, want it to survive as %q", rec.Provisioned.PauseKind, mail.PauseKindRate) } } // The other half of "nothing auto-resumes": TestEnforceNeverAutoResumes above // re-checks on a day with zero counts, which an early-return guard and a // mutant that DROPS that guard both pass — a day with nothing to trip trips // nothing either way, so that test alone does not prove the guard exists. // This test keeps the cap breached (even further breached) on the SAME day // and re-runs enforce: only the early-return guard stops a second pass, a // second SetPaused call and a second audit line. (Mutation testing caught // this gap — see the task report.) func TestEnforceAlreadyPausedDoesNotReprocess(t *testing.T) { fx, now := mailFixture(t) day := now.UTC().Format("2006-01-02") seedCounts(t, fx, day, MailCounts{Sends: 50}) if _, _, err := fx.srv.enforceMailLimits(context.Background(), release.OpsAccount, "harbourmail", mail.SenderAppID, now); err != nil { t.Fatal(err) } // More sends land on the SAME day, well past the cap — a re-evaluating // enforce would trip again. seedCounts(t, fx, day, MailCounts{Sends: 25}) paused, reason, err := fx.srv.enforceMailLimits(context.Background(), release.OpsAccount, "harbourmail", mail.SenderAppID, now) if err != nil { t.Fatal(err) } if paused { t.Fatalf("enforce re-processed an already-paused app (reason %q) — the early-return guard is not applied", reason) } entries, err := fx.st.Events(OpsAccountID, "harbourmail", 10) if err != nil { t.Fatal(err) } pauses := 0 for _, e := range entries { if strings.Contains(e.Action, "mail.paused") { pauses++ } } if pauses != 1 { t.Fatalf("got %d mail.paused audit entries, want exactly 1 (a second pass must not re-audit)", pauses) } } // A pause nobody can see presents to the member as "email stopped working". // The real audit reader is Store.Events(accountID, app, limit) — see F7: // Store has no AuditEntries method, and s.audit(acct *Account, app, addr, // action, detail string) is what production code calls. func TestEnforceRecordsTheReasonInTheAuditLog(t *testing.T) { fx, now := mailFixture(t) seedCounts(t, fx, now.UTC().Format("2006-01-02"), MailCounts{Sends: 50}) fx.srv.enforceMailLimits(context.Background(), release.OpsAccount, "harbourmail", mail.SenderAppID, now) entries, err := fx.st.Events(OpsAccountID, "harbourmail", 10) if err != nil { t.Fatal(err) } for _, e := range entries { if strings.Contains(e.Action, "mail.paused") && strings.Contains(e.Detail, "daily cap 50") { return } } t.Fatalf("no audit entry naming the pause reason: %+v", entries) } // pauseDirectly writes rec.Provisioned's Active/PauseKind/At by hand — the // way SetPaused already would, without needing to breach a threshold // first. Used by the resumeStaleCapPause tests below, which pin the shared // day-roll function's contract directly (R2, review follow-up) rather than // only through enforceMailLimits' caller. func pauseDirectly(t *testing.T, fx *consoleFixture, app, kind string, at time.Time) { t.Helper() rec, etag, err := mail.Get(context.Background(), fx.rel, release.OpsAccount, app) if err != nil { t.Fatal(err) } rec.Provisioned.Active = false rec.Provisioned.PauseKind = kind rec.Provisioned.At = at if _, err := mail.Put(context.Background(), fx.rel, release.OpsAccount, app, rec, etag); err != nil { t.Fatal(err) } } // R2: resumeStaleCapPause is the ONE function both enforceMailLimits (the // SES event path) and handleAPIEmailStatus (the status READ path) share, // so these four cases pin its contract directly. func TestResumeStaleCapPauseClearsACapPauseFromYesterday(t *testing.T) { fx, now := mailFixture(t) pauseDirectly(t, fx, "harbourmail", mail.PauseKindCap, now.AddDate(0, 0, -1)) sn, _, err := mail.GetSender(context.Background(), fx.rel, release.OpsAccount, "harbourmail", mail.SenderAppID) if err != nil { t.Fatal(err) } got, resumed, err := fx.srv.resumeStaleCapPause(context.Background(), release.OpsAccount, "harbourmail", mail.SenderAppID, sn, now) if err != nil { t.Fatal(err) } if !resumed { t.Fatal("did not resume a cap pause applied on an earlier UTC day") } if !got.Provisioned.Active { t.Fatal("resumed record still reports Active == false") } if got.Provisioned.PauseKind != "" { t.Fatalf("PauseKind = %q after resume, want cleared", got.Provisioned.PauseKind) } } func TestResumeStaleCapPauseLeavesATodayCapPauseAlone(t *testing.T) { fx, now := mailFixture(t) pauseDirectly(t, fx, "harbourmail", mail.PauseKindCap, now) // paused earlier the SAME UTC day sn, _, err := mail.GetSender(context.Background(), fx.rel, release.OpsAccount, "harbourmail", mail.SenderAppID) if err != nil { t.Fatal(err) } got, resumed, err := fx.srv.resumeStaleCapPause(context.Background(), release.OpsAccount, "harbourmail", mail.SenderAppID, sn, now) if err != nil { t.Fatal(err) } if resumed { t.Fatal("resumed a cap pause applied the SAME UTC day — that makes the cap meaningless") } if got.Provisioned.Active { t.Fatal("same-day cap pause was lifted") } } func TestResumeStaleCapPauseNeverTouchesARatePause(t *testing.T) { fx, now := mailFixture(t) pauseDirectly(t, fx, "harbourmail", mail.PauseKindRate, now.AddDate(0, 0, -1)) sn, _, err := mail.GetSender(context.Background(), fx.rel, release.OpsAccount, "harbourmail", mail.SenderAppID) if err != nil { t.Fatal(err) } got, resumed, err := fx.srv.resumeStaleCapPause(context.Background(), release.OpsAccount, "harbourmail", mail.SenderAppID, sn, now) if err != nil { t.Fatal(err) } if resumed { t.Fatal("resumed a RATE pause — a rate pause must never auto-resume, day roll or not") } if got.Provisioned.Active { t.Fatal("a rate pause from yesterday was lifted") } if got.Provisioned.PauseKind != mail.PauseKindRate { t.Fatalf("PauseKind = %q, want it to survive as %q", got.Provisioned.PauseKind, mail.PauseKindRate) } } func TestResumeStaleCapPauseNeverTouchesAKindlessPause(t *testing.T) { fx, now := mailFixture(t) pauseDirectly(t, fx, "harbourmail", "", now.AddDate(0, 0, -1)) sn, _, err := mail.GetSender(context.Background(), fx.rel, release.OpsAccount, "harbourmail", mail.SenderAppID) if err != nil { t.Fatal(err) } got, resumed, err := fx.srv.resumeStaleCapPause(context.Background(), release.OpsAccount, "harbourmail", mail.SenderAppID, sn, now) if err != nil { t.Fatal(err) } if resumed { t.Fatal("resumed a kind-less pause (pre-existing record or manual pause) — fail safe means these never auto-resume") } if got.Provisioned.Active { t.Fatal("a kind-less pause from yesterday was lifted") } } // SQLite cannot widen a primary key, so per-sender counts are a new table, // backfilled once from mail_counts with sender = 'app'. The backfill runs at // most once for the life of the database: a second migrate must not // resurrect rows a purge or a rename has since removed. func TestMailSenderCountsAreBackfilledExactlyOnce(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "console.db") st, err := Open(path) if err != nil { t.Fatal(err) } if _, err := st.db.Exec(`INSERT INTO mail_counts (account, app, day, sends, bounces, complaints, rejects, deliveries) VALUES ('bag', 'harbourmail', '2026-09-14', 7, 1, 0, 0, 6)`); err != nil { t.Fatal(err) } // Force the backfill to run again by winding the marker back, which is // what a console rolled back and forward would look like. if _, err := st.db.Exec(`PRAGMA user_version = 0`); err != nil { t.Fatal(err) } if err := st.migrate(); err != nil { t.Fatal(err) } var sends, deliveries int if err := st.db.QueryRow(`SELECT sends, deliveries FROM mail_sender_counts WHERE account = 'bag' AND app = 'harbourmail' AND sender = 'app' AND day = '2026-09-14'`).Scan(&sends, &deliveries); err != nil { t.Fatalf("the backfill did not land: %v", err) } if sends != 7 || deliveries != 6 { t.Fatalf("backfilled %d sends and %d deliveries, want 7 and 6", sends, deliveries) } // A row removed after the backfill stays removed across a restart. if _, err := st.db.Exec(`DELETE FROM mail_sender_counts WHERE account = 'bag'`); err != nil { t.Fatal(err) } if err := st.migrate(); err != nil { t.Fatal(err) } var n int if err := st.db.QueryRow(`SELECT count(*) FROM mail_sender_counts WHERE account = 'bag'`).Scan(&n); err != nil { t.Fatal(err) } if n != 0 { t.Fatalf("the backfill resurrected %d deleted rows", n) } // The legacy table is dropped only when an operator asks, a release later. if err := st.DropLegacyMailCounts(false); err != nil { t.Fatal(err) } if err := st.db.QueryRow(`SELECT count(*) FROM mail_counts`).Scan(&n); err != nil { t.Fatalf("mail_counts was dropped while the flag was off: %v", err) } if err := st.DropLegacyMailCounts(true); err != nil { t.Fatal(err) } if err := st.db.QueryRow(`SELECT count(*) FROM mail_counts`).Scan(&n); err == nil { t.Fatal("mail_counts survived its own drop") } if err := st.Close(); err != nil { t.Fatal(err) } } // Counts are per sender from here on. An empty sender reads the app's whole // day across every sender, which is what the status reply's top-level // figures and the roll-up both need. func TestMailCountsAreKeptAndReadPerSender(t *testing.T) { fx := newConsoleFixture(t) ctx := context.Background() const day = "2026-09-15" for i := 0; i < 3; i++ { if err := fx.st.RecordMailEvent(ctx, release.OpsAccount, "harbourmail", mail.SenderAppID, day, mail.EventSend); err != nil { t.Fatal(err) } } for i := 0; i < 2; i++ { if err := fx.st.RecordMailEvent(ctx, release.OpsAccount, "harbourmail", "book.example.com", day, mail.EventSend); err != nil { t.Fatal(err) } } if err := fx.st.RecordMailEvent(ctx, release.OpsAccount, "harbourmail", "book.example.com", day, mail.EventBounce); err != nil { t.Fatal(err) } app, err := fx.st.MailCounts(ctx, release.OpsAccount, "harbourmail", mail.SenderAppID, day) if err != nil { t.Fatal(err) } if app.Sends != 3 || app.Bounces != 0 { t.Fatalf("app sender = %+v, want 3 sends and no bounces", app) } one, err := fx.st.MailCounts(ctx, release.OpsAccount, "harbourmail", "book.example.com", day) if err != nil { t.Fatal(err) } if one.Sends != 2 || one.Bounces != 1 { t.Fatalf("instance sender = %+v, want 2 sends and 1 bounce", one) } all, err := fx.st.MailCounts(ctx, release.OpsAccount, "harbourmail", "", day) if err != nil { t.Fatal(err) } if all.Sends != 5 || all.Bounces != 1 { t.Fatalf("every sender = %+v, want 5 sends and 1 bounce", all) } // A day with no rows is a legitimate zero, not an error and not an // ErrNoRows the caller has to know about. quiet, err := fx.st.MailCounts(ctx, release.OpsAccount, "harbourmail", "", "2026-01-01") if err != nil || quiet != (MailCounts{}) { t.Fatalf("a quiet day = %+v %v", quiet, err) } } // The cap applies per sender, so one sender's breach pauses that sender and // leaves its siblings sending. The counts are chosen to discriminate a // summed read from a per-sender one: 25 (app) + 40 (sibling) = 65, over the // cap of 50, so a summed MailCounts read would trip here even though // neither sender has yet reached 50 on its own. func TestTheCapIsEnforcedPerSender(t *testing.T) { fx, now := mailFixture(t) ctx := context.Background() day := now.UTC().Format("2006-01-02") // A provisioned, active sibling sender — checked below so a pause is // proven to leave it alone, not just left unread. sibling := &mail.Sender{ ID: "book.example.com", Scope: mail.ScopeInstance, Source: mail.SourceDomain, Provisioned: &mail.Provisioned{IAMUser: "book-u", KeyID: "AKIA2", Active: true, At: now}, } if _, err := mail.PutSender(ctx, fx.rel, release.OpsAccount, "harbourmail", sibling, ""); err != nil { t.Fatal(err) } for i := 0; i < 25; i++ { if err := fx.st.RecordMailEvent(ctx, release.OpsAccount, "harbourmail", mail.SenderAppID, day, mail.EventSend); err != nil { t.Fatal(err) } } for i := 0; i < 40; i++ { if err := fx.st.RecordMailEvent(ctx, release.OpsAccount, "harbourmail", "book.example.com", day, mail.EventSend); err != nil { t.Fatal(err) } } paused, _, err := fx.srv.enforceMailLimits(ctx, release.OpsAccount, "harbourmail", mail.SenderAppID, now) if err != nil { t.Fatal(err) } if paused { t.Fatal("the app sender paused on 25 of its own sends plus a sibling's 40 — the cap is being read summed across senders") } // Push the app sender's OWN count past its own cap of 50. The sibling's // count is untouched at 40, still under its own cap. for i := 0; i < 26; i++ { if err := fx.st.RecordMailEvent(ctx, release.OpsAccount, "harbourmail", mail.SenderAppID, day, mail.EventSend); err != nil { t.Fatal(err) } } paused, reason, err := fx.srv.enforceMailLimits(ctx, release.OpsAccount, "harbourmail", mail.SenderAppID, now) if err != nil { t.Fatal(err) } if !paused || !strings.Contains(reason, "daily cap 50") { t.Fatalf("app sender = %v %q, want a cap pause", paused, reason) } sn, _, err := mail.GetSender(ctx, fx.rel, release.OpsAccount, "harbourmail", mail.SenderAppID) if err != nil || sn.Provisioned.Active || sn.Provisioned.PauseKind != mail.PauseKindCap { t.Fatalf("the app sender's record = %+v %v", sn.Provisioned, err) } // The pause must land on the sender that breached, never a sibling that // did not: the sibling is still under its own cap and must still be // Active. sib, _, err := mail.GetSender(ctx, fx.rel, release.OpsAccount, "harbourmail", "book.example.com") if err != nil || sib == nil || !sib.Provisioned.Active { t.Fatalf("the sibling sender's record = %+v %v, want it left Active", sib, err) } // The day roll clears a cap pause, still, and only a cap pause. fresh, resumed, err := fx.srv.resumeStaleCapPause(ctx, release.OpsAccount, "harbourmail", mail.SenderAppID, sn, now.AddDate(0, 0, 1)) if err != nil || !resumed || fresh == nil || !fresh.Provisioned.Active { t.Fatalf("the day roll did not clear the cap pause: %+v %v %v", fresh, resumed, err) } // The sibling is still untouched by the day roll on the app sender's // pause — resumeStaleCapPause was given the app sender, not the // sibling, so the sibling's own state must not have moved. sib, _, err = mail.GetSender(ctx, fx.rel, release.OpsAccount, "harbourmail", "book.example.com") if err != nil || sib == nil || !sib.Provisioned.Active { t.Fatalf("the sibling sender's record = %+v %v after the day roll, want it left Active", sib, err) } } // The pause targets the sender that actually breached its cap, never a // hard-coded stand-in for it: if enforceMailLimits paused the app sender // regardless of which sender was asked for, a sibling's own breach would // leave the sibling sending and silently pause the app sender instead. func TestTheCapPauseTargetsTheBreachingSenderNotTheAppSender(t *testing.T) { fx, now := mailFixture(t) ctx := context.Background() day := now.UTC().Format("2006-01-02") sibling := &mail.Sender{ ID: "book.example.com", Scope: mail.ScopeInstance, Source: mail.SourceDomain, Provisioned: &mail.Provisioned{IAMUser: "book-u", KeyID: "AKIA2", Active: true, At: now}, } if _, err := mail.PutSender(ctx, fx.rel, release.OpsAccount, "harbourmail", sibling, ""); err != nil { t.Fatal(err) } // Only the SIBLING breaches its cap; the app sender sends nothing. for i := 0; i < 50; i++ { if err := fx.st.RecordMailEvent(ctx, release.OpsAccount, "harbourmail", "book.example.com", day, mail.EventSend); err != nil { t.Fatal(err) } } paused, reason, err := fx.srv.enforceMailLimits(ctx, release.OpsAccount, "harbourmail", "book.example.com", now) if err != nil { t.Fatal(err) } if !paused || !strings.Contains(reason, "daily cap 50") { t.Fatalf("sibling sender = %v %q, want a cap pause", paused, reason) } sib, _, err := mail.GetSender(ctx, fx.rel, release.OpsAccount, "harbourmail", "book.example.com") if err != nil || sib.Provisioned.Active || sib.Provisioned.PauseKind != mail.PauseKindCap { t.Fatalf("the sibling sender's record = %+v %v, want it paused", sib.Provisioned, err) } // The app sender never breached anything and must still be Active — a // pause hard-coded to the app sender would show up here instead. app, _, err := mail.GetSender(ctx, fx.rel, release.OpsAccount, "harbourmail", mail.SenderAppID) if err != nil || app == nil || !app.Provisioned.Active { t.Fatalf("the app sender's record = %+v %v, want it left Active", app, err) } } // resumeStaleCapPause must never resume a PerInstance sender's stale cap // pause: resuming reactivates every key it touches, including the app // credential every instance still holds, which is exactly what per-instance // mode exists to prevent (design section 6.3). Nothing sets PerInstance in // this release; this pins the guard directly so it cannot regress silently. func TestResumeStaleCapPauseNeverTouchesAPerInstanceSender(t *testing.T) { fx, now := mailFixture(t) pauseDirectly(t, fx, "harbourmail", mail.PauseKindCap, now.AddDate(0, 0, -1)) sn, etag, err := mail.GetSender(context.Background(), fx.rel, release.OpsAccount, "harbourmail", mail.SenderAppID) if err != nil { t.Fatal(err) } sn.PerInstance = true if _, err := mail.PutSender(context.Background(), fx.rel, release.OpsAccount, "harbourmail", sn, etag); err != nil { t.Fatal(err) } got, resumed, err := fx.srv.resumeStaleCapPause(context.Background(), release.OpsAccount, "harbourmail", mail.SenderAppID, sn, now) if err != nil { t.Fatal(err) } if resumed { t.Fatal("resumed a PerInstance sender's stale cap pause — re-arms a credential every instance still holds") } if got.Provisioned.Active { t.Fatal("a PerInstance sender's cap pause from yesterday was lifted") } } // mailAuditSubject is the naming rule directly: every app has exactly one // sender in this release (the app sender), so naming it would add "/app" to // a member-facing audit line for no information a member could use — and // that "/app" form never went through this repo's copy-review gate. Only a // sender that is NOT the app sender earns the "/sender" suffix. func TestMailAuditSubjectNamesOnlyANonAppSender(t *testing.T) { if got := mailAuditSubject("harbourmail", mail.SenderAppID); got != "harbourmail" { t.Fatalf("mailAuditSubject(app sender) = %q, want the bare app name", got) } if got := mailAuditSubject("harbourmail", "book.example.com"); got != "harbourmail/book.example.com" { t.Fatalf("mailAuditSubject(non-app sender) = %q, want app/sender", got) } } // The app sender's pause line must be byte-identical to what every live app // already prints today (pre-dating per-sender senders): "harbourmail: daily // cap 50 reached (50 sent)". Naming the sender here would be a member-facing // copy change this repo's copy-review gate never saw. func TestEnforceAuditDetailIsByteIdenticalForTheAppSender(t *testing.T) { fx, now := mailFixture(t) seedCounts(t, fx, now.UTC().Format("2006-01-02"), MailCounts{Sends: 50}) if _, _, err := fx.srv.enforceMailLimits(context.Background(), release.OpsAccount, "harbourmail", mail.SenderAppID, now); err != nil { t.Fatal(err) } entries, err := fx.st.Events(OpsAccountID, "harbourmail", 10) if err != nil { t.Fatal(err) } want := "harbourmail: daily cap 50 reached (50 sent)" for _, e := range entries { if e.Action == "mail.paused" { if e.Detail != want { t.Fatalf("pause detail = %q, want byte-identical %q", e.Detail, want) } return } } t.Fatalf("no mail.paused audit entry found: %+v", entries) } // A non-app sender's pause line DOES carry the sender, since "harbourmail" // alone would not tell a member which of two senders paused. func TestEnforceAuditDetailNamesANonAppSender(t *testing.T) { fx, now := mailFixture(t) ctx := context.Background() day := now.UTC().Format("2006-01-02") sibling := &mail.Sender{ ID: "book.example.com", Scope: mail.ScopeInstance, Source: mail.SourceDomain, Provisioned: &mail.Provisioned{IAMUser: "book-u", KeyID: "AKIA2", Active: true, At: now}, } if _, err := mail.PutSender(ctx, fx.rel, release.OpsAccount, "harbourmail", sibling, ""); err != nil { t.Fatal(err) } for i := 0; i < 50; i++ { if err := fx.st.RecordMailEvent(ctx, release.OpsAccount, "harbourmail", "book.example.com", day, mail.EventSend); err != nil { t.Fatal(err) } } if _, _, err := fx.srv.enforceMailLimits(ctx, release.OpsAccount, "harbourmail", "book.example.com", now); err != nil { t.Fatal(err) } entries, err := fx.st.Events(OpsAccountID, "harbourmail", 10) if err != nil { t.Fatal(err) } want := "harbourmail/book.example.com: daily cap 50 reached (50 sent)" for _, e := range entries { if e.Action == "mail.paused" { if e.Detail != want { t.Fatalf("pause detail = %q, want %q", e.Detail, want) } return } } t.Fatalf("no mail.paused audit entry found: %+v", entries) } // The resume line follows the same rule: byte-identical for the app sender. func TestResumeAuditDetailIsByteIdenticalForTheAppSender(t *testing.T) { fx, now := mailFixture(t) pauseDirectly(t, fx, "harbourmail", mail.PauseKindCap, now.AddDate(0, 0, -1)) sn, _, err := mail.GetSender(context.Background(), fx.rel, release.OpsAccount, "harbourmail", mail.SenderAppID) if err != nil { t.Fatal(err) } if _, resumed, err := fx.srv.resumeStaleCapPause(context.Background(), release.OpsAccount, "harbourmail", mail.SenderAppID, sn, now); err != nil || !resumed { t.Fatalf("resume = %v %v, want it to resume", resumed, err) } entries, err := fx.st.Events(OpsAccountID, "harbourmail", 10) if err != nil { t.Fatal(err) } want := "harbourmail: daily cap pause cleared on the UTC day roll" for _, e := range entries { if e.Action == "mail.resumed" { if e.Detail != want { t.Fatalf("resume detail = %q, want byte-identical %q", e.Detail, want) } return } } t.Fatalf("no mail.resumed audit entry found: %+v", entries) }