package mail import ( "context" "strings" "testing" "time" "github.com/carlosframework/platform/internal/dnsrec" "github.com/carlosframework/platform/internal/release" ) func seedDomain(t *testing.T, s release.Store, p *MemProvisioner) time.Time { t.Helper() now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC) if _, _, err := EnsureDomain(context.Background(), s, p, dnsrec.NewMemWriter(), testDeployment(), "bag", "harbourmail", "harbourmail.bag.oncarlos.com", now); err != nil { t.Fatalf("seed: %v", err) } return now } // All three axes must converge. An identity verified for SENDING whose // MAIL FROM has not converged still sends — from the wrong envelope domain, // failing DMARC alignment — so partial success must NOT read as verified. func TestVerifyRequiresEveryAxis(t *testing.T) { ctx := context.Background() s, p := release.NewDirStore(t.TempDir()), NewMemProvisioner("1") now := seedDomain(t, s, p) p.SetIdentityStatus("eu-west-1", "harbourmail.bag.oncarlos.com", Status{DKIM: "SUCCESS", MailFrom: "PENDING", Sending: "SUCCESS"}) rec, err := VerifyDomain(ctx, s, p, testDeployment(), "bag", "harbourmail.bag.oncarlos.com", "eu-west-1", now) if err != nil { t.Fatalf("VerifyDomain: %v", err) } if rec.Regions["eu-west-1"].Status != StatusPending { t.Fatal("a domain with an unconverged MAIL FROM was marked verified") } p.SetIdentityStatus("eu-west-1", "harbourmail.bag.oncarlos.com", Status{DKIM: "SUCCESS", MailFrom: "SUCCESS", Sending: "SUCCESS"}) rec, err = VerifyDomain(ctx, s, p, testDeployment(), "bag", "harbourmail.bag.oncarlos.com", "eu-west-1", now) if err != nil { t.Fatalf("VerifyDomain: %v", err) } if rec.Regions["eu-west-1"].Status != StatusVerified { t.Fatalf("status = %q, want verified", rec.Regions["eu-west-1"].Status) } } // SES reports a permanent DKIM failure as FAILED. That is a terminal state // the member must see, not a pending one they wait on forever. func TestVerifyRecordsAPermanentFailure(t *testing.T) { ctx := context.Background() s, p := release.NewDirStore(t.TempDir()), NewMemProvisioner("1") now := seedDomain(t, s, p) p.SetIdentityStatus("eu-west-1", "harbourmail.bag.oncarlos.com", Status{DKIM: "FAILED", MailFrom: "PENDING", Sending: "PENDING"}) rec, err := VerifyDomain(ctx, s, p, testDeployment(), "bag", "harbourmail.bag.oncarlos.com", "eu-west-1", now) if err != nil { t.Fatalf("VerifyDomain: %v", err) } if rec.Regions["eu-west-1"].Status != StatusFailed { t.Fatalf("status = %q, want failed", rec.Regions["eu-west-1"].Status) } } // A domain that recovers must be able to leave the failed state — SES can // report FAILED and later succeed once DNS is fixed. func TestVerifyCanRecoverFromFailed(t *testing.T) { ctx := context.Background() s, p := release.NewDirStore(t.TempDir()), NewMemProvisioner("1") now := seedDomain(t, s, p) p.SetIdentityStatus("eu-west-1", "harbourmail.bag.oncarlos.com", Status{DKIM: "FAILED", MailFrom: "PENDING", Sending: "PENDING"}) VerifyDomain(ctx, s, p, testDeployment(), "bag", "harbourmail.bag.oncarlos.com", "eu-west-1", now) p.SetIdentityStatus("eu-west-1", "harbourmail.bag.oncarlos.com", Status{DKIM: "SUCCESS", MailFrom: "SUCCESS", Sending: "SUCCESS"}) rec, err := VerifyDomain(ctx, s, p, testDeployment(), "bag", "harbourmail.bag.oncarlos.com", "eu-west-1", now) if err != nil { t.Fatalf("VerifyDomain: %v", err) } if rec.Regions["eu-west-1"].Status != StatusVerified { t.Fatal("a recovered domain stayed failed") } } // A domain must not stay verified once SES regresses — status is // recomputed from scratch every call, never latched on a prior verified // reading. Without this test, a VerifyDomain that short-circuits on an // already-verified region ("nothing to do, it converged") would pass every // other test in this file while silently leaving a de-verified domain // reading verified forever. func TestVerifyLeavesVerifiedWhenSESRegresses(t *testing.T) { ctx := context.Background() s, p := release.NewDirStore(t.TempDir()), NewMemProvisioner("1") now := seedDomain(t, s, p) p.SetIdentityStatus("eu-west-1", "harbourmail.bag.oncarlos.com", Status{DKIM: "SUCCESS", MailFrom: "SUCCESS", Sending: "SUCCESS"}) rec, err := VerifyDomain(ctx, s, p, testDeployment(), "bag", "harbourmail.bag.oncarlos.com", "eu-west-1", now) if err != nil { t.Fatalf("VerifyDomain: %v", err) } if rec.Regions["eu-west-1"].Status != StatusVerified { t.Fatalf("status = %q, want verified", rec.Regions["eu-west-1"].Status) } p.SetIdentityStatus("eu-west-1", "harbourmail.bag.oncarlos.com", Status{DKIM: "FAILED", MailFrom: "SUCCESS", Sending: "SUCCESS"}) rec, err = VerifyDomain(ctx, s, p, testDeployment(), "bag", "harbourmail.bag.oncarlos.com", "eu-west-1", now) if err != nil { t.Fatalf("VerifyDomain: %v", err) } if rec.Regions["eu-west-1"].Status != StatusFailed { t.Fatalf("status = %q, want failed — a previously verified domain must not stay latched verified once SES regresses", rec.Regions["eu-west-1"].Status) } } func TestVerifyRefusesAnUnknownRegion(t *testing.T) { ctx := context.Background() s, p := release.NewDirStore(t.TempDir()), NewMemProvisioner("1") now := seedDomain(t, s, p) if _, err := VerifyDomain(ctx, s, p, testDeployment(), "bag", "harbourmail.bag.oncarlos.com", "ap-south-1", now); err == nil { t.Fatal("verifying a region the domain has no identity in returned success") } } // ---- config-set reconciliation (issue #272, ruling 1) ---------------------- // TestVerifyDomainRebindsADriftedConfigSet: the record and SES can disagree, // and when they do the tooling goes quiet about a domain that emits nothing. // Reconciliation lands HERE because IdentityStatus already asks SES about // this identity on every verify — the drift check is free, where doing it on // every status read would put an API call on a path that answers from the // bucket alone and would fail whenever AWS is unreachable. func TestVerifyDomainRebindsADriftedConfigSet(t *testing.T) { ctx, s, p, _, now := enableFixture(t) d := testDeployment() // Something unbinds the set out of band — exactly what an operator // reaching for the AWS console does. if err := p.SetIdentityConfigSet(ctx, d.Region, "harbourmail.bag.oncarlos.com", ""); err != nil { t.Fatalf("unbind: %v", err) } got, err := p.IdentityStatus(ctx, d.Region, "harbourmail.bag.oncarlos.com") if err != nil || got.ConfigSet != "" { t.Fatalf("precondition: identity still reports config set %q (err %v)", got.ConfigSet, err) } if _, err := VerifyDomain(ctx, s, p, d, "bag", "harbourmail.bag.oncarlos.com", d.Region, now); err != nil { t.Fatalf("VerifyDomain: %v", err) } got, err = p.IdentityStatus(ctx, d.Region, "harbourmail.bag.oncarlos.com") if err != nil { t.Fatalf("IdentityStatus: %v", err) } if want := ConfigSetName("bag"); got.ConfigSet != want { t.Errorf("config set after verify = %q, want %q — verify saw the drift and left it", got.ConfigSet, want) } } // TestVerifyDomainUnbindsWhenTheDeploymentHasNoTopic is the reverse // direction, and it matters as much: a deployment that turns event ingest // OFF must not leave identities bound to a configuration set whose // destination no longer exists, because a send through a deleted set fails. func TestVerifyDomainUnbindsWhenTheDeploymentHasNoTopic(t *testing.T) { ctx, s, p, _, now := enableFixture(t) withSNS := testDeployment() noSNS := Deployment{Region: withSNS.Region, PlatformSuffix: withSNS.PlatformSuffix} if got, _ := p.IdentityStatus(ctx, withSNS.Region, "harbourmail.bag.oncarlos.com"); got.ConfigSet == "" { t.Fatal("precondition: the fixture never bound a config set") } if _, err := VerifyDomain(ctx, s, p, noSNS, "bag", "harbourmail.bag.oncarlos.com", noSNS.Region, now); err != nil { t.Fatalf("VerifyDomain: %v", err) } got, err := p.IdentityStatus(ctx, noSNS.Region, "harbourmail.bag.oncarlos.com") if err != nil { t.Fatalf("IdentityStatus: %v", err) } if got.ConfigSet != "" { t.Errorf("config set = %q, want empty — a deployment with no topic must not leave identities bound", got.ConfigSet) } } // TestVerifyDomainRefreshesPoliciesWhenItRebinds is the wiring half. A // repair that fixes SES and the record but not the IAM policy converts // silent-no-events into a 554 on every send — strictly worse than the drift // it repaired. Same lesson as #279, arriving through the other door. func TestVerifyDomainRefreshesPoliciesWhenItRebinds(t *testing.T) { ctx, s, p, pub, now := enableFixture(t) d := testDeployment() if _, err := Enable(ctx, s, p, pub, d, "bag", "harbourmail", now); err != nil { t.Fatalf("Enable: %v", err) } // Strip the config-set ARN back out of the policy, as it would be on an // app enabled before the set existed. dom, _, err := GetDomain(ctx, s, "bag", "harbourmail.bag.oncarlos.com") if err != nil { t.Fatalf("GetDomain: %v", err) } if _, err := p.EnsureUser(ctx, UserName("bag", "harbourmail"), UserPath, dom.VerifiedARNs(), nil, []string{"harbourmail.bag.oncarlos.com"}); err != nil { t.Fatalf("EnsureUser: %v", err) } if err := p.SetIdentityConfigSet(ctx, d.Region, "harbourmail.bag.oncarlos.com", ""); err != nil { t.Fatalf("unbind: %v", err) } if _, err := VerifyDomain(ctx, s, p, d, "bag", "harbourmail.bag.oncarlos.com", d.Region, now); err != nil { t.Fatalf("VerifyDomain: %v", err) } dom, _, err = GetDomain(ctx, s, "bag", "harbourmail.bag.oncarlos.com") if err != nil { t.Fatalf("GetDomain: %v", err) } want := dom.VerifiedConfigSetARNs() if len(want) == 0 { t.Fatal("nothing was bound, so this test proves nothing") } policy := p.Users[UserName("bag", "harbourmail")].Policy for _, arn := range want { if !strings.Contains(policy, arn) { t.Errorf("policy does not grant %s after the rebind — repairing the binding without the policy is a 554\npolicy = %s", arn, policy) } } }