| 1 | package mail |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "testing" |
| 6 | "time" |
| 7 | |
| 8 | "github.com/carlosframework/platform/internal/release" |
| 9 | ) |
| 10 | |
| 11 | // An operator's deliberate cap must survive a tier recompute. Source is |
| 12 | // what distinguishes "this account is on the free tier" from "someone |
| 13 | // decided this account may send more", and a tier pass that flattened the |
| 14 | // second into the first would silently revoke a decision (spec §10). |
| 15 | func TestTierRecomputeDoesNotClobberAnOperatorGrant(t *testing.T) { |
| 16 | ctx, s := context.Background(), release.NewDirStore(t.TempDir()) |
| 17 | now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC) |
| 18 | if _, err := Declare(ctx, s, "bag", "harbourmail", "HARBOURMAIL_SMTP", |
| 19 | "n@x.com", "owner@example.test", now); err != nil { |
| 20 | t.Fatal(err) |
| 21 | } |
| 22 | if _, err := ApplyGrant(ctx, s, "bag", "harbourmail", []string{"x.com"}, 5000, "operator", "owner@example.test", now); err != nil { |
| 23 | t.Fatal(err) |
| 24 | } |
| 25 | rec, err := ApplyGrant(ctx, s, "bag", "harbourmail", []string{"x.com"}, 50, "tier:free", "tier", now) |
| 26 | if err != nil { |
| 27 | t.Fatal(err) |
| 28 | } |
| 29 | if rec.Grant.DailyCap != 5000 || rec.Grant.Source != "operator" { |
| 30 | t.Fatalf("tier recompute clobbered the operator grant: %+v", rec.Grant) |
| 31 | } |
| 32 | } |
| 33 | |
| 34 | // A tier change DOES move a tier-sourced cap — that is the whole point of |
| 35 | // recording the source. |
| 36 | func TestTierRecomputeMovesATierGrant(t *testing.T) { |
| 37 | ctx, s := context.Background(), release.NewDirStore(t.TempDir()) |
| 38 | now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC) |
| 39 | Declare(ctx, s, "bag", "harbourmail", "HARBOURMAIL_SMTP", "n@x.com", "owner@example.test", now) |
| 40 | ApplyGrant(ctx, s, "bag", "harbourmail", []string{"x.com"}, 50, "tier:free", "tier", now) |
| 41 | rec, err := ApplyGrant(ctx, s, "bag", "harbourmail", []string{"x.com"}, 1000, "tier:starter", "tier", now) |
| 42 | if err != nil { |
| 43 | t.Fatal(err) |
| 44 | } |
| 45 | if rec.Grant.DailyCap != 1000 { |
| 46 | t.Fatalf("DailyCap = %d, want 1000", rec.Grant.DailyCap) |
| 47 | } |
| 48 | } |
| 49 | |
| 50 | func TestGrantRefusesAnUndeclaredApp(t *testing.T) { |
| 51 | ctx, s := context.Background(), release.NewDirStore(t.TempDir()) |
| 52 | if _, err := ApplyGrant(ctx, s, "bag", "harbourmail", []string{"x.com"}, 50, "tier:free", "tier", time.Now().UTC()); err != ErrNotDeclared { |
| 53 | t.Fatalf("= %v, want ErrNotDeclared", err) |
| 54 | } |
| 55 | } |
| 56 | |
| 57 | // An operator must be able to override a tier grant. This is how an operator |
| 58 | // raises a cap on a tiered account — the entire point of recording Source. |
| 59 | func TestOperatorCanOverrideTierGrant(t *testing.T) { |
| 60 | ctx, s := context.Background(), release.NewDirStore(t.TempDir()) |
| 61 | now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC) |
| 62 | Declare(ctx, s, "bag", "harbourmail", "HARBOURMAIL_SMTP", "n@x.com", "owner@example.test", now) |
| 63 | ApplyGrant(ctx, s, "bag", "harbourmail", []string{"x.com"}, 50, "tier:free", "tier", now) |
| 64 | rec, err := ApplyGrant(ctx, s, "bag", "harbourmail", []string{"x.com"}, 5000, "operator", "owner@example.test", now) |
| 65 | if err != nil { |
| 66 | t.Fatal(err) |
| 67 | } |
| 68 | if rec.Grant.DailyCap != 5000 || rec.Grant.Source != "operator" { |
| 69 | t.Fatalf("operator override failed: got DailyCap=%d Source=%q, want 5000/operator", rec.Grant.DailyCap, rec.Grant.Source) |
| 70 | } |
| 71 | } |
| 72 | |
| 73 | // Domains are sorted before storage for consistency, so grant lookups are |
| 74 | // stable regardless of the order they were supplied in. |
| 75 | func TestGrantSortsDomains(t *testing.T) { |
| 76 | ctx, s := context.Background(), release.NewDirStore(t.TempDir()) |
| 77 | now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC) |
| 78 | Declare(ctx, s, "bag", "harbourmail", "HARBOURMAIL_SMTP", "n@x.com", "owner@example.test", now) |
| 79 | rec, err := ApplyGrant(ctx, s, "bag", "harbourmail", []string{"z.com", "a.com", "m.com"}, 50, "tier:free", "tier", now) |
| 80 | if err != nil { |
| 81 | t.Fatal(err) |
| 82 | } |
| 83 | if rec.Grant.Domains[0] != "a.com" || rec.Grant.Domains[1] != "m.com" || rec.Grant.Domains[2] != "z.com" { |
| 84 | t.Fatalf("domains not sorted: %v", rec.Grant.Domains) |
| 85 | } |
| 86 | } |
| 87 | |