| new file mode 100644 |
| index 000000000..df9ec54af |
| --- /dev/null |
| +++ b/internal/mail/walk_verify_test.go |
| @@ -0,0 +1,129 @@ |
| +package mail |
| + |
| +import ( |
| + "context" |
| + "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", "seapointish", "seapointish.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", "seapointish.bag.oncarlos.com", |
| + Status{DKIM: "SUCCESS", MailFrom: "PENDING", Sending: "SUCCESS"}) |
| + rec, err := VerifyDomain(ctx, s, p, "bag", "seapointish.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", "seapointish.bag.oncarlos.com", |
| + Status{DKIM: "SUCCESS", MailFrom: "SUCCESS", Sending: "SUCCESS"}) |
| + rec, err = VerifyDomain(ctx, s, p, "bag", "seapointish.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", "seapointish.bag.oncarlos.com", |
| + Status{DKIM: "FAILED", MailFrom: "PENDING", Sending: "PENDING"}) |
| + rec, err := VerifyDomain(ctx, s, p, "bag", "seapointish.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", "seapointish.bag.oncarlos.com", |
| + Status{DKIM: "FAILED", MailFrom: "PENDING", Sending: "PENDING"}) |
| + VerifyDomain(ctx, s, p, "bag", "seapointish.bag.oncarlos.com", "eu-west-1", now) |
| + p.SetIdentityStatus("eu-west-1", "seapointish.bag.oncarlos.com", |
| + Status{DKIM: "SUCCESS", MailFrom: "SUCCESS", Sending: "SUCCESS"}) |
| + rec, err := VerifyDomain(ctx, s, p, "bag", "seapointish.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", "seapointish.bag.oncarlos.com", |
| + Status{DKIM: "SUCCESS", MailFrom: "SUCCESS", Sending: "SUCCESS"}) |
| + rec, err := VerifyDomain(ctx, s, p, "bag", "seapointish.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", "seapointish.bag.oncarlos.com", |
| + Status{DKIM: "FAILED", MailFrom: "SUCCESS", Sending: "SUCCESS"}) |
| + rec, err = VerifyDomain(ctx, s, p, "bag", "seapointish.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, "bag", "seapointish.bag.oncarlos.com", "ap-south-1", now); err == nil { |
| + t.Fatal("verifying a region the domain has no identity in returned success") |
| + } |
| +} |