| 1 | package mail |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "testing" |
| 6 | "time" |
| 7 | |
| 8 | "github.com/carlosframework/platform/internal/release" |
| 9 | ) |
| 10 | |
| 11 | func TestDomainKeyIsAccountScoped(t *testing.T) { |
| 12 | got := DomainKey("bag", "harbourmail.bag.oncarlos.com") |
| 13 | want := "accounts/bag/email/domains/harbourmail.bag.oncarlos.com.json" |
| 14 | if got != want { |
| 15 | t.Fatalf("DomainKey = %q, want %q", got, want) |
| 16 | } |
| 17 | } |
| 18 | |
| 19 | func TestValidDomainRefusesWhatWouldEscapeTheKey(t *testing.T) { |
| 20 | for _, ok := range []string{"harbourmail.bag.oncarlos.com", "example.com", "a-b.example.co.uk"} { |
| 21 | if !ValidDomain(ok) { |
| 22 | t.Fatalf("ValidDomain(%q) = false, want true", ok) |
| 23 | } |
| 24 | } |
| 25 | // A domain becomes a bucket KEY and an IAM policy RESOURCE. Anything |
| 26 | // that could traverse or inject must be refused here, at the only |
| 27 | // place every caller passes through. |
| 28 | for _, bad := range []string{"", ".", "../etc", "a/b.com", "UPPER.com", "-lead.com", "trail-.com", "a..b.com", |
| 29 | // Wildcards are dangerous: the domain string becomes an IAM |
| 30 | // ses:FromAddress condition value, where * would match every |
| 31 | // address on earth. |
| 32 | "*", "*.example.com"} { |
| 33 | if ValidDomain(bad) { |
| 34 | t.Fatalf("ValidDomain(%q) = true, want false", bad) |
| 35 | } |
| 36 | } |
| 37 | } |
| 38 | |
| 39 | // Regions is a MAP, and this test is the reason: a domain verified in two |
| 40 | // regions holds two independent identities, ARNs and DKIM token sets, and |
| 41 | // a scalar region field would have to be migrated the day a second region |
| 42 | // appears (spec §6a, §8). |
| 43 | func TestRegionsAreIndependent(t *testing.T) { |
| 44 | ctx, s := context.Background(), release.NewDirStore(t.TempDir()) |
| 45 | now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC) |
| 46 | rec := &DomainRecord{ |
| 47 | Domain: "harbourmail.bag.oncarlos.com", |
| 48 | Kind: KindPlatform, |
| 49 | Apps: []string{"harbourmail"}, |
| 50 | Regions: map[string]*RegionState{ |
| 51 | "eu-west-1": {IdentityARN: "arn:aws:ses:eu-west-1:1:identity/harbourmail.bag.oncarlos.com", |
| 52 | DKIMTokens: []string{"a", "b", "c"}, |
| 53 | MailFrom: "mail-eu-west-1.harbourmail.bag.oncarlos.com", |
| 54 | Status: StatusVerified, CheckedAt: now}, |
| 55 | "us-east-1": {IdentityARN: "arn:aws:ses:us-east-1:1:identity/harbourmail.bag.oncarlos.com", |
| 56 | DKIMTokens: []string{"d", "e", "f"}, |
| 57 | MailFrom: "mail-us-east-1.harbourmail.bag.oncarlos.com", |
| 58 | Status: StatusPending, CheckedAt: now}, |
| 59 | }, |
| 60 | CreatedAt: now, |
| 61 | } |
| 62 | if _, err := PutDomain(ctx, s, "bag", rec, ""); err != nil { |
| 63 | t.Fatalf("PutDomain: %v", err) |
| 64 | } |
| 65 | out, _, err := GetDomain(ctx, s, "bag", "harbourmail.bag.oncarlos.com") |
| 66 | if err != nil { |
| 67 | t.Fatalf("GetDomain: %v", err) |
| 68 | } |
| 69 | if len(out.Regions) != 2 { |
| 70 | t.Fatalf("regions = %d, want 2", len(out.Regions)) |
| 71 | } |
| 72 | if out.Regions["us-east-1"].DKIMTokens[0] != "d" { |
| 73 | t.Fatal("per-region DKIM tokens did not round-trip independently") |
| 74 | } |
| 75 | } |
| 76 | |
| 77 | // Only a VERIFIED region contributes an ARN, so a pending domain grants |
| 78 | // nothing (spec §6a). |
| 79 | func TestVerifiedARNsSkipsPendingAndFailed(t *testing.T) { |
| 80 | rec := &DomainRecord{Regions: map[string]*RegionState{ |
| 81 | "eu-west-1": {IdentityARN: "arn-ok", Status: StatusVerified}, |
| 82 | "us-east-1": {IdentityARN: "arn-pending", Status: StatusPending}, |
| 83 | "ap-south-1": {IdentityARN: "arn-failed", Status: StatusFailed}, |
| 84 | }} |
| 85 | got := rec.VerifiedARNs() |
| 86 | if len(got) != 1 || got[0] != "arn-ok" { |
| 87 | t.Fatalf("VerifiedARNs = %v, want [arn-ok]", got) |
| 88 | } |
| 89 | } |
| 90 | |
| 91 | // The apps set is what makes account-scoping worth its cost: teardown of |
| 92 | // one app must never revoke a sibling's verified domain (spec §6a). |
| 93 | func TestAppsSetGuardsSiblingTeardown(t *testing.T) { |
| 94 | rec := &DomainRecord{Apps: []string{"harbourmail"}} |
| 95 | if !rec.AddApp("marketing") { |
| 96 | t.Fatal("AddApp returned false for a new app") |
| 97 | } |
| 98 | if rec.AddApp("marketing") { |
| 99 | t.Fatal("AddApp returned true for an app already present") |
| 100 | } |
| 101 | if !rec.RemoveApp("harbourmail") { |
| 102 | t.Fatal("RemoveApp returned false for a present app") |
| 103 | } |
| 104 | if len(rec.Apps) != 1 || rec.Apps[0] != "marketing" { |
| 105 | t.Fatalf("Apps = %v, want [marketing] — the sibling must survive", rec.Apps) |
| 106 | } |
| 107 | } |
| 108 | |