| index 1c0ea4663..61a2a0ab0 100644 |
| --- a/internal/console/mailcounts_test.go |
| +++ b/internal/console/mailcounts_test.go |
| @@ -573,22 +573,50 @@ func TestMailCountsAreKeptAndReadPerSender(t *testing.T) { |
| } |
| |
| // The cap applies per sender, so one sender's breach pauses that sender and |
| -// leaves its siblings sending. |
| +// 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") |
| - for i := 0; i < 50; i++ { |
| + |
| + // 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) |
| } |
| } |
| - // A second sender's traffic does not count towards the first one's cap. |
| - for i := 0; i < 50; i++ { |
| + 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) |
| @@ -600,9 +628,94 @@ func TestTheCapIsEnforcedPerSender(t *testing.T) { |
| 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") |
| + } |
| } |