package mail import ( "context" "errors" "testing" "time" "github.com/carlosframework/platform/internal/dnsrec" "github.com/carlosframework/platform/internal/release" ) func testDeployment() Deployment { return Deployment{Region: "eu-west-1", PlatformSuffix: "oncarlos.com", SNSTopicARN: "arn:aws:sns:eu-west-1:1:carlos-mail"} } func TestDomainKindSplitsOnWhoOwnsTheZone(t *testing.T) { d := testDeployment() if got := DomainKindFor(d, "harbourmail.bag.oncarlos.com"); got != KindPlatform { t.Fatalf("kind = %q, want platform", got) } if got := DomainKindFor(d, "harbourmail.com"); got != KindCustom { t.Fatalf("kind = %q, want custom", got) } // A lookalike must NOT be treated as ours — suffix matching has to be // label-aware, or "evil-oncarlos.com" would have CARLOS try to write // DNS into a zone it does not own. if got := DomainKindFor(d, "evil-oncarlos.com"); got != KindCustom { t.Fatalf("kind for a lookalike = %q, want custom", got) } } // A platform domain is published for the member: CARLOS owns the zone, so // the walk writes every record itself and the member does nothing. func TestEnsureDomainPublishesPlatformDNS(t *testing.T) { ctx := context.Background() s, p, w := release.NewDirStore(t.TempDir()), NewMemProvisioner("1"), dnsrec.NewMemWriter() now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC) rec, pending, err := EnsureDomain(ctx, s, p, w, testDeployment(), "bag", "harbourmail", "harbourmail.bag.oncarlos.com", now) if err != nil { t.Fatalf("EnsureDomain: %v", err) } if rec.Kind != KindPlatform { t.Fatalf("kind = %q, want platform", rec.Kind) } if len(pending) != 0 { t.Fatalf("a platform domain must leave nothing for the member to publish, got %d", len(pending)) } // 3 DKIM + MX + SPF + DMARC if len(w.Records) != 6 { t.Fatalf("published %d records, want 6: %+v", len(w.Records), w.Records) } st := rec.Regions["eu-west-1"] if st.Status != StatusPending { t.Fatalf("status = %q, want pending — DNS is written, SES has not converged", st.Status) } if st.MailFrom != "mail-eu-west-1.harbourmail.bag.oncarlos.com" { t.Fatalf("MailFrom = %q", st.MailFrom) } } // A custom domain is EMITTED for the customer: CARLOS cannot write their // zone, so the same record set comes back for them to publish and nothing // is written. func TestEnsureDomainEmitsCustomRecordsAndWritesNothing(t *testing.T) { ctx := context.Background() s, p, w := release.NewDirStore(t.TempDir()), NewMemProvisioner("1"), dnsrec.NewMemWriter() now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC) rec, pending, err := EnsureDomain(ctx, s, p, w, testDeployment(), "bag", "harbourmail", "harbourmail.com", now) if err != nil { t.Fatalf("EnsureDomain: %v", err) } if rec.Kind != KindCustom { t.Fatalf("kind = %q, want custom", rec.Kind) } if len(pending) != 6 { t.Fatalf("emitted %d records for the customer, want 6", len(pending)) } if len(w.Records) != 0 { t.Fatal("wrote into a zone CARLOS does not own") } } // Re-running is a no-op that returns the SAME tokens. This is the crash // repair path: a walk that half-failed is fixed by running it again, and // fresh tokens would orphan the DNS the first run published. func TestEnsureDomainIsIdempotent(t *testing.T) { ctx := context.Background() s, p, w := release.NewDirStore(t.TempDir()), NewMemProvisioner("1"), dnsrec.NewMemWriter() now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC) d := testDeployment() first, _, err := EnsureDomain(ctx, s, p, w, d, "bag", "harbourmail", "harbourmail.bag.oncarlos.com", now) if err != nil { t.Fatalf("first: %v", err) } second, _, err := EnsureDomain(ctx, s, p, w, d, "bag", "harbourmail", "harbourmail.bag.oncarlos.com", now) if err != nil { t.Fatalf("second: %v", err) } f, sec := first.Regions["eu-west-1"], second.Regions["eu-west-1"] if f.DKIMTokens[0] != sec.DKIMTokens[0] || f.IdentityARN != sec.IdentityARN { t.Fatal("re-running changed the identity — published DNS would be orphaned") } if len(w.Records) != 6 { t.Fatalf("re-running duplicated records: %d", len(w.Records)) } } // A second app on one domain joins the apps set rather than re-creating the // identity. This is the whole reason the record is account-scoped. func TestEnsureDomainAddsASecondAppWithoutRecreating(t *testing.T) { ctx := context.Background() s, p, w := release.NewDirStore(t.TempDir()), NewMemProvisioner("1"), dnsrec.NewMemWriter() now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC) d := testDeployment() first, _, err := EnsureDomain(ctx, s, p, w, d, "bag", "harbourmail", "shared.com", now) if err != nil { t.Fatalf("first app: %v", err) } rec, _, err := EnsureDomain(ctx, s, p, w, d, "bag", "marketing", "shared.com", now) if err != nil { t.Fatalf("second app: %v", err) } if len(rec.Apps) != 2 { t.Fatalf("apps = %v, want both", rec.Apps) } if len(p.Identities) != 1 { t.Fatalf("identities = %d, want 1 — a second app must join, not re-create", len(p.Identities)) } if first.Regions["eu-west-1"].IdentityARN != rec.Regions["eu-west-1"].IdentityARN { t.Fatal("identity ARN changed between apps — the identity was re-created") } } // A half-failure after the identity exists but before DNS is published must // be repaired by a re-run, not left stuck. func TestEnsureDomainRepairsAHalfFailedPublish(t *testing.T) { ctx := context.Background() s, p, w := release.NewDirStore(t.TempDir()), NewMemProvisioner("1"), dnsrec.NewMemWriter() now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC) d := testDeployment() w.FailNext["EnsureRecord"] = errors.New("route53 down") if _, _, err := EnsureDomain(ctx, s, p, w, d, "bag", "harbourmail", "harbourmail.bag.oncarlos.com", now); err == nil { t.Fatal("EnsureDomain succeeded despite a DNS write failure") } // A failed publish must leave NO record — the record must never claim // DNS that was not actually written. This is what pins the ordering: // create-identity, then publish, then record, in that order. if rec, _, err := GetDomain(ctx, s, "bag", "harbourmail.bag.oncarlos.com"); err != nil || rec != nil { t.Fatalf("a failed publish left a record claiming DNS that is not there: %+v", rec) } if _, _, err := EnsureDomain(ctx, s, p, w, d, "bag", "harbourmail", "harbourmail.bag.oncarlos.com", now); err != nil { t.Fatalf("re-run did not repair: %v", err) } if len(w.Records) != 6 { t.Fatalf("after repair, published %d records, want 6", len(w.Records)) } } // A region already verified must not be knocked back to pending by a // re-run — only a freshly-ensured region starts pending. StatusFailed is // deliberately NOT preserved this way (a re-run is a fresh attempt), so // this test targets verified specifically. func TestEnsureDomainPreservesVerifiedStatus(t *testing.T) { ctx := context.Background() s, p, w := release.NewDirStore(t.TempDir()), NewMemProvisioner("1"), dnsrec.NewMemWriter() now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC) d := testDeployment() if _, _, err := EnsureDomain(ctx, s, p, w, d, "bag", "harbourmail", "harbourmail.bag.oncarlos.com", now); err != nil { t.Fatalf("first: %v", err) } // Simulate SES having converged: mark the region verified directly on // the persisted record, the way a status-check walk would. got, etag, err := GetDomain(ctx, s, "bag", "harbourmail.bag.oncarlos.com") if err != nil { t.Fatalf("GetDomain: %v", err) } got.Regions["eu-west-1"].Status = StatusVerified if _, err := PutDomain(ctx, s, "bag", got, etag); err != nil { t.Fatalf("PutDomain: %v", err) } second, _, err := EnsureDomain(ctx, s, p, w, d, "bag", "harbourmail", "harbourmail.bag.oncarlos.com", now) if err != nil { t.Fatalf("second: %v", err) } if got := second.Regions["eu-west-1"].Status; got != StatusVerified { t.Fatalf("status = %q, want verified — a re-run must not knock back a converged region", got) } } // A deployment with no SNS topic must not record a configuration set name // for a set that was never created — a later send against a phantom set // fails with "Configuration set does not exist" on every attempt. func TestEnsureDomainLeavesConfigSetEmptyWithoutSNS(t *testing.T) { ctx := context.Background() s, p, w := release.NewDirStore(t.TempDir()), NewMemProvisioner("1"), dnsrec.NewMemWriter() now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC) d := testDeployment() d.SNSTopicARN = "" rec, _, err := EnsureDomain(ctx, s, p, w, d, "bag", "harbourmail", "harbourmail.bag.oncarlos.com", now) if err != nil { t.Fatalf("EnsureDomain: %v", err) } if got := rec.Regions["eu-west-1"].ConfigSet; got != "" { t.Fatalf("ConfigSet = %q, want empty — no SNS topic means no config set was created", got) } } // C1 (review follow-up): recording a config set NAME in the domain record // is not the same as SES actually emitting events for that identity — SES // only does that for a send carrying a configuration set, and nothing // before this task ever bound the identity to one. A domain ensured // against a deployment WITH an SNS topic must have its identity's default // configuration set stamped AT THE PROVISIONER, not just recorded in the // bucket. func TestEnsureDomainWithSNSStampsTheIdentitysConfigSet(t *testing.T) { ctx := context.Background() s, p, w := release.NewDirStore(t.TempDir()), NewMemProvisioner("1"), dnsrec.NewMemWriter() now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC) d := testDeployment() // carries an SNSTopicARN if _, _, err := EnsureDomain(ctx, s, p, w, d, "bag", "harbourmail", "harbourmail.bag.oncarlos.com", now); err != nil { t.Fatalf("EnsureDomain: %v", err) } id, ok := p.Identities[identityKey(d.Region, "harbourmail.bag.oncarlos.com")] if !ok { t.Fatal("no identity recorded at the provisioner") } if want := ConfigSetName("bag"); id.ConfigSet != want { t.Fatalf("identity ConfigSet = %q, want %q — SetIdentityConfigSet was never called, so SES "+ "would never emit an event for this identity and the daily cap/abuse pause would never fire", id.ConfigSet, want) } } // The symmetric SNS-less case: without a configuration set to bind to, // SetIdentityConfigSet must not be called at all — calling it with an // empty name would either error at a real backend or silently bind the // identity to nothing. func TestEnsureDomainWithoutSNSStampsNoIdentityConfigSet(t *testing.T) { ctx := context.Background() s, p, w := release.NewDirStore(t.TempDir()), NewMemProvisioner("1"), dnsrec.NewMemWriter() now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC) d := testDeployment() d.SNSTopicARN = "" if _, _, err := EnsureDomain(ctx, s, p, w, d, "bag", "harbourmail", "harbourmail.bag.oncarlos.com", now); err != nil { t.Fatalf("EnsureDomain: %v", err) } id, ok := p.Identities[identityKey(d.Region, "harbourmail.bag.oncarlos.com")] if !ok { t.Fatal("no identity recorded at the provisioner") } if id.ConfigSet != "" { t.Fatalf("identity ConfigSet = %q, want empty — no SNS topic means no config set to bind to", id.ConfigSet) } } // acct reaches EnsureTenant — a real backend call — before anything checks // it, unless validation gates the walk before that call. app is persisted // into the record and later turned into an IAM principal name by other // walks, so it must be validated too. func TestEnsureDomainRefusesInvalidAccount(t *testing.T) { ctx := context.Background() s, p, w := release.NewDirStore(t.TempDir()), NewMemProvisioner("1"), dnsrec.NewMemWriter() now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC) if _, _, err := EnsureDomain(ctx, s, p, w, testDeployment(), "../evil", "harbourmail", "harbourmail.bag.oncarlos.com", now); err == nil { t.Fatal("EnsureDomain accepted an invalid account") } if p.Tenants["../evil"] || p.Tenants[TenantName("../evil")] { t.Fatal("an invalid account reached EnsureTenant before validation") } } // EnsureDomain must create the tenant under its PREFIXED name, never the // bare account sqid — the SES tenant namespace is shared with other // products in the same AWS account (TenantName's doc comment in policy.go // has the live collision this guards against), so a bare sqid landing in // p.Tenants would be the exact defect this closes. func TestEnsureDomainCreatesPrefixedTenant(t *testing.T) { ctx := context.Background() s, p, w := release.NewDirStore(t.TempDir()), NewMemProvisioner("1"), dnsrec.NewMemWriter() now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC) if _, _, err := EnsureDomain(ctx, s, p, w, testDeployment(), "bag", "harbourmail", "harbourmail.bag.oncarlos.com", now); err != nil { t.Fatalf("EnsureDomain: %v", err) } if !p.Tenants["carlos-bag"] { t.Fatal("EnsureDomain did not create the prefixed tenant carlos-bag") } if p.Tenants["bag"] { t.Fatal("EnsureDomain created a bare-sqid tenant — this collides with another product's namespace") } } func TestEnsureDomainRefusesInvalidApp(t *testing.T) { ctx := context.Background() s, p, w := release.NewDirStore(t.TempDir()), NewMemProvisioner("1"), dnsrec.NewMemWriter() now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC) if _, _, err := EnsureDomain(ctx, s, p, w, testDeployment(), "bag", "../evil", "harbourmail.bag.oncarlos.com", now); err == nil { t.Fatal("EnsureDomain accepted an invalid app") } } // failAfterN wraps a Writer and fails the (n+1)th EnsureRecord, so a // PARTIAL publish can be exercised — MemWriter.FailNext is one-shot on // the first call, which is the least realistic crash point for this walk // (three published, then a crash, is the realistic one). After the one // failure it behaves like the wrapped Writer for good, matching how a // transient DNS-provider failure would look on a retry. type failAfterN struct { dnsrec.Writer n int err error failed bool } func (f *failAfterN) EnsureRecord(ctx context.Context, r dnsrec.Record) error { if !f.failed && f.n <= 0 { f.failed = true return f.err } if !f.failed { f.n-- } return f.Writer.EnsureRecord(ctx, r) } // A crash after SOME records are published but before all six is the // realistic failure mode this walk must survive. A re-run must converge to // the full set with no duplicates, and the half-published attempt must not // have persisted a record claiming DNS that is not fully there. func TestEnsureDomainRepairsAPartialPublish(t *testing.T) { ctx := context.Background() s, p := release.NewDirStore(t.TempDir()), NewMemProvisioner("1") inner := dnsrec.NewMemWriter() w := &failAfterN{Writer: inner, n: 3, err: errors.New("route53 down mid-publish")} now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC) d := testDeployment() if _, _, err := EnsureDomain(ctx, s, p, w, d, "bag", "harbourmail", "harbourmail.bag.oncarlos.com", now); err == nil { t.Fatal("EnsureDomain succeeded despite a mid-publish failure") } if len(inner.Records) != 3 { t.Fatalf("published %d records before the failure, want 3", len(inner.Records)) } if rec, _, err := GetDomain(ctx, s, "bag", "harbourmail.bag.oncarlos.com"); err != nil || rec != nil { t.Fatalf("a partial publish left a record claiming DNS that is not fully there: %+v", rec) } if _, _, err := EnsureDomain(ctx, s, p, w, d, "bag", "harbourmail", "harbourmail.bag.oncarlos.com", now); err != nil { t.Fatalf("re-run did not repair: %v", err) } if len(inner.Records) != 6 { t.Fatalf("after repair, published %d records, want 6 with no duplicates", len(inner.Records)) } } // Step 2 associates each account's EXISTING tenant with its identities and // its configuration set (design §7, "Associations are wired in step 2, not // here"). Associating a resource does not modify it and changes no // behaviour on its own: no send carries X-SES-TENANT yet. It is done now so // that step 3 can rely on it, and because SES validates the association on // every tenant-attributed send. func TestEnsureDomainAssociatesTheIdentityAndTheSetWithTheAccountsTenant(t *testing.T) { ctx := context.Background() s, p := release.NewDirStore(t.TempDir()), NewMemProvisioner("1") now := time.Date(2026, 9, 15, 12, 0, 0, 0, time.UTC) if _, err := Declare(ctx, s, "bag", "harbourmail", "HARBOURMAIL_SMTP", "noreply@harbourmail.bag.oncarlos.com", "owner@example.test", now); err != nil { t.Fatal(err) } rec, _, err := EnsureDomain(ctx, s, p, dnsrec.NewMemWriter(), testDeployment(), "bag", "harbourmail", "harbourmail.bag.oncarlos.com", now) if err != nil { t.Fatal(err) } tenant := TenantName("bag") identity := rec.Regions["eu-west-1"].IdentityARN set, ok := ConfigSetARN(identity, ConfigSetName("bag")) if !ok { t.Fatalf("no configuration-set ARN composed from %q", identity) } for _, arn := range []string{identity, set} { if !p.TenantResources[tenant][arn] { t.Errorf("%s is not associated with %s: %v", arn, tenant, p.TenantResources[tenant]) } } // A re-run associates the same pair again without error: every verb on // this seam is idempotent, which is what makes the walk re-runnable. if _, _, err := EnsureDomain(ctx, s, p, dnsrec.NewMemWriter(), testDeployment(), "bag", "harbourmail", "harbourmail.bag.oncarlos.com", now); err != nil { t.Fatalf("re-run: %v", err) } // An identity cannot be deleted while an association exists (design §3), // so teardown removes the association first. MemProvisioner models that // refusal, because without it the first real teardown would fail. if err := Teardown(ctx, s, p, dnsrec.NewMemWriter(), testDeployment(), "bag", "harbourmail"); err != nil { t.Fatalf("Teardown: %v", err) } if p.TenantResources[tenant][identity] { t.Error("the identity association survived teardown, so the identity could never be deleted") } // The configuration set is per ACCOUNT and outlives any one app, so its // association is never removed by an app's teardown. if !p.TenantResources[tenant][set] { t.Error("an app's teardown removed the account's configuration-set association") } }