package mail import ( "context" "testing" "time" "github.com/carlosframework/platform/internal/release" ) // An operator's deliberate cap must survive a tier recompute. Source is // what distinguishes "this account is on the free tier" from "someone // decided this account may send more", and a tier pass that flattened the // second into the first would silently revoke a decision (spec §10). func TestTierRecomputeDoesNotClobberAnOperatorGrant(t *testing.T) { ctx, s := context.Background(), release.NewDirStore(t.TempDir()) now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC) if _, err := Declare(ctx, s, "bag", "harbourmail", "HARBOURMAIL_SMTP", "n@x.com", "owner@example.test", now); err != nil { t.Fatal(err) } if _, err := ApplyGrant(ctx, s, "bag", "harbourmail", []string{"x.com"}, 5000, "operator", "owner@example.test", now); err != nil { t.Fatal(err) } rec, err := ApplyGrant(ctx, s, "bag", "harbourmail", []string{"x.com"}, 50, "tier:free", "tier", now) if err != nil { t.Fatal(err) } if rec.Grant.DailyCap != 5000 || rec.Grant.Source != "operator" { t.Fatalf("tier recompute clobbered the operator grant: %+v", rec.Grant) } } // A tier change DOES move a tier-sourced cap — that is the whole point of // recording the source. func TestTierRecomputeMovesATierGrant(t *testing.T) { ctx, s := context.Background(), release.NewDirStore(t.TempDir()) now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC) Declare(ctx, s, "bag", "harbourmail", "HARBOURMAIL_SMTP", "n@x.com", "owner@example.test", now) ApplyGrant(ctx, s, "bag", "harbourmail", []string{"x.com"}, 50, "tier:free", "tier", now) rec, err := ApplyGrant(ctx, s, "bag", "harbourmail", []string{"x.com"}, 1000, "tier:starter", "tier", now) if err != nil { t.Fatal(err) } if rec.Grant.DailyCap != 1000 { t.Fatalf("DailyCap = %d, want 1000", rec.Grant.DailyCap) } } func TestGrantRefusesAnUndeclaredApp(t *testing.T) { ctx, s := context.Background(), release.NewDirStore(t.TempDir()) if _, err := ApplyGrant(ctx, s, "bag", "harbourmail", []string{"x.com"}, 50, "tier:free", "tier", time.Now().UTC()); err != ErrNotDeclared { t.Fatalf("= %v, want ErrNotDeclared", err) } } // An operator must be able to override a tier grant. This is how an operator // raises a cap on a tiered account — the entire point of recording Source. func TestOperatorCanOverrideTierGrant(t *testing.T) { ctx, s := context.Background(), release.NewDirStore(t.TempDir()) now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC) Declare(ctx, s, "bag", "harbourmail", "HARBOURMAIL_SMTP", "n@x.com", "owner@example.test", now) ApplyGrant(ctx, s, "bag", "harbourmail", []string{"x.com"}, 50, "tier:free", "tier", now) rec, err := ApplyGrant(ctx, s, "bag", "harbourmail", []string{"x.com"}, 5000, "operator", "owner@example.test", now) if err != nil { t.Fatal(err) } if rec.Grant.DailyCap != 5000 || rec.Grant.Source != "operator" { t.Fatalf("operator override failed: got DailyCap=%d Source=%q, want 5000/operator", rec.Grant.DailyCap, rec.Grant.Source) } } // Domains are sorted before storage for consistency, so grant lookups are // stable regardless of the order they were supplied in. func TestGrantSortsDomains(t *testing.T) { ctx, s := context.Background(), release.NewDirStore(t.TempDir()) now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC) Declare(ctx, s, "bag", "harbourmail", "HARBOURMAIL_SMTP", "n@x.com", "owner@example.test", now) rec, err := ApplyGrant(ctx, s, "bag", "harbourmail", []string{"z.com", "a.com", "m.com"}, 50, "tier:free", "tier", now) if err != nil { t.Fatal(err) } if rec.Grant.Domains[0] != "a.com" || rec.Grant.Domains[1] != "m.com" || rec.Grant.Domains[2] != "z.com" { t.Fatalf("domains not sorted: %v", rec.Grant.Domains) } }