package mail import ( "context" "errors" "reflect" "testing" "github.com/aws/aws-sdk-go-v2/aws" iamtypes "github.com/aws/aws-sdk-go-v2/service/iam/types" "github.com/aws/aws-sdk-go-v2/service/sesv2" sesv2types "github.com/aws/aws-sdk-go-v2/service/sesv2/types" ) // "Already exists" is a SUCCESS. Every walk re-runs after a half-failure, // and a second EnsureIdentity that errors would make a crashed run // unrepairable except by hand. func TestAlreadyExistsIsRecognised(t *testing.T) { if !isAlreadyExists(&sesv2types.AlreadyExistsException{}) { t.Fatal("SES AlreadyExistsException not recognised") } if !isAlreadyExists(&iamtypes.EntityAlreadyExistsException{}) { t.Fatal("IAM EntityAlreadyExistsException not recognised") } if isAlreadyExists(errors.New("some other failure")) { t.Fatal("an unrelated error was treated as already-exists") } } // "Already gone" is a SUCCESS, for the same reason in the other direction: // teardown must be re-runnable. func TestNotFoundIsRecognised(t *testing.T) { if !isNotFound(&sesv2types.NotFoundException{}) { t.Fatal("SES NotFoundException not recognised") } if !isNotFound(&iamtypes.NoSuchEntityException{}) { t.Fatal("IAM NoSuchEntityException not recognised") } if isNotFound(errors.New("some other failure")) { t.Fatal("an unrelated error was treated as not-found") } } // BehaviorOnMxFailure must be REJECT_MESSAGE, not USE_DEFAULT_VALUE. // // USE_DEFAULT_VALUE silently falls back to amazonses.com when the custom // MAIL FROM MX is missing — which means a misconfigured tenant keeps // sending, from an unaligned envelope domain, failing DMARC, and nothing // surfaces the fault. Rejecting is what turns a DNS mistake into an error // somebody sees. func TestMailFromFailureRejectsRatherThanFallsBack(t *testing.T) { if mailFromBehaviour != sesv2types.BehaviorOnMxFailureRejectMessage { t.Fatalf("BehaviorOnMxFailure = %v, want REJECT_MESSAGE", mailFromBehaviour) } } // stubSES is a minimal sesAPI double: only the methods a given test cares // about are wired via the function fields; everything else answers with a // zero value and a nil error, which is fine because no test below relies on // those paths. type stubSES struct { createTenant func(*sesv2.CreateTenantInput) (*sesv2.CreateTenantOutput, error) createEmailIdentity func(*sesv2.CreateEmailIdentityInput) (*sesv2.CreateEmailIdentityOutput, error) getEmailIdentity func(*sesv2.GetEmailIdentityInput) (*sesv2.GetEmailIdentityOutput, error) putEmailIdentityMailFromAttributes func(*sesv2.PutEmailIdentityMailFromAttributesInput) (*sesv2.PutEmailIdentityMailFromAttributesOutput, error) putEmailIdentityConfigurationSet func(*sesv2.PutEmailIdentityConfigurationSetAttributesInput) (*sesv2.PutEmailIdentityConfigurationSetAttributesOutput, error) } func (s *stubSES) CreateTenant(_ context.Context, in *sesv2.CreateTenantInput, _ ...func(*sesv2.Options)) (*sesv2.CreateTenantOutput, error) { if s.createTenant != nil { return s.createTenant(in) } return &sesv2.CreateTenantOutput{}, nil } func (s *stubSES) CreateEmailIdentity(_ context.Context, in *sesv2.CreateEmailIdentityInput, _ ...func(*sesv2.Options)) (*sesv2.CreateEmailIdentityOutput, error) { if s.createEmailIdentity != nil { return s.createEmailIdentity(in) } return &sesv2.CreateEmailIdentityOutput{}, nil } func (s *stubSES) GetEmailIdentity(_ context.Context, in *sesv2.GetEmailIdentityInput, _ ...func(*sesv2.Options)) (*sesv2.GetEmailIdentityOutput, error) { if s.getEmailIdentity != nil { return s.getEmailIdentity(in) } return &sesv2.GetEmailIdentityOutput{}, nil } func (s *stubSES) PutEmailIdentityMailFromAttributes(_ context.Context, in *sesv2.PutEmailIdentityMailFromAttributesInput, _ ...func(*sesv2.Options)) (*sesv2.PutEmailIdentityMailFromAttributesOutput, error) { if s.putEmailIdentityMailFromAttributes != nil { return s.putEmailIdentityMailFromAttributes(in) } return &sesv2.PutEmailIdentityMailFromAttributesOutput{}, nil } func (s *stubSES) DeleteEmailIdentity(_ context.Context, in *sesv2.DeleteEmailIdentityInput, _ ...func(*sesv2.Options)) (*sesv2.DeleteEmailIdentityOutput, error) { return &sesv2.DeleteEmailIdentityOutput{}, nil } func (s *stubSES) CreateConfigurationSet(_ context.Context, in *sesv2.CreateConfigurationSetInput, _ ...func(*sesv2.Options)) (*sesv2.CreateConfigurationSetOutput, error) { return &sesv2.CreateConfigurationSetOutput{}, nil } func (s *stubSES) CreateConfigurationSetEventDestination(_ context.Context, in *sesv2.CreateConfigurationSetEventDestinationInput, _ ...func(*sesv2.Options)) (*sesv2.CreateConfigurationSetEventDestinationOutput, error) { return &sesv2.CreateConfigurationSetEventDestinationOutput{}, nil } func (s *stubSES) PutEmailIdentityConfigurationSetAttributes(_ context.Context, in *sesv2.PutEmailIdentityConfigurationSetAttributesInput, _ ...func(*sesv2.Options)) (*sesv2.PutEmailIdentityConfigurationSetAttributesOutput, error) { if s.putEmailIdentityConfigurationSet != nil { return s.putEmailIdentityConfigurationSet(in) } return &sesv2.PutEmailIdentityConfigurationSetAttributesOutput{}, nil } func (s *stubSES) CreateTenantResourceAssociation(_ context.Context, in *sesv2.CreateTenantResourceAssociationInput, _ ...func(*sesv2.Options)) (*sesv2.CreateTenantResourceAssociationOutput, error) { return &sesv2.CreateTenantResourceAssociationOutput{}, nil } func (s *stubSES) DeleteTenantResourceAssociation(_ context.Context, in *sesv2.DeleteTenantResourceAssociationInput, _ ...func(*sesv2.Options)) (*sesv2.DeleteTenantResourceAssociationOutput, error) { return &sesv2.DeleteTenantResourceAssociationOutput{}, nil } var _ sesAPI = (*stubSES)(nil) // TestEnsureIdentityAlreadyExistsReadsBackTokensAndComposesTheARN pins the // F3-critical path: the ruling's own words are that getting this wrong means // every IAM policy names a resource that does not exist, and this was // otherwise untested until Task 12. It exercises the already-exists branch // (CreateEmailIdentity fails, GetEmailIdentity is read back for the DKIM // tokens already published in DNS), checks the exact composed ARN, and — // the assertion that actually matters, because it pins aws.go and policy.go // together — checks that ARN is ACCEPTED by SendPolicyJSON. func TestEnsureIdentityAlreadyExistsReadsBackTokensAndComposesTheARN(t *testing.T) { wantTokens := []string{"tok1", "tok2", "tok3"} stub := &stubSES{ createEmailIdentity: func(*sesv2.CreateEmailIdentityInput) (*sesv2.CreateEmailIdentityOutput, error) { return nil, &sesv2types.AlreadyExistsException{Message: aws.String("identity already exists")} }, getEmailIdentity: func(*sesv2.GetEmailIdentityInput) (*sesv2.GetEmailIdentityOutput, error) { return &sesv2.GetEmailIdentityOutput{ DkimAttributes: &sesv2types.DkimAttributes{Tokens: wantTokens}, }, nil }, } p := &awsProvisioner{ses: stub, account: "123456789012", region: "eu-west-1"} got, err := p.EnsureIdentity(context.Background(), "eu-west-1", "example.com", "mail.example.com", true) if err != nil { t.Fatalf("EnsureIdentity: %v", err) } if !reflect.DeepEqual(got.DKIMTokens, wantTokens) { t.Fatalf("DKIMTokens = %v, want the ones read back from GetEmailIdentity %v", got.DKIMTokens, wantTokens) } const wantARN = "arn:aws:ses:eu-west-1:123456789012:identity/example.com" if got.ARN != wantARN { t.Fatalf("ARN = %q, want %q", got.ARN, wantARN) } // The assertion that actually matters: the composed ARN must satisfy // SendPolicyJSON's shape validator, since EnsureIdentity's return value // flows straight into EnsureUser -> SendPolicyJSON in production. A // composed ARN that validator rejects would make every send fail with // no way to notice until Task 12. if _, err := SendPolicyJSON([]string{got.ARN}, nil, []string{"example.com"}); err != nil { t.Fatalf("composed ARN %q was rejected by SendPolicyJSON: %v", got.ARN, err) } } // TestEnsureIdentityWithoutAdoptLeavesAnExistingIdentityAlone (#216): with // adopt false, an identity that already exists may belong to another product // in this AWS account, so it is refused before it is read or its MAIL FROM // rewritten. func TestEnsureIdentityWithoutAdoptLeavesAnExistingIdentityAlone(t *testing.T) { touched := false stub := &stubSES{ createEmailIdentity: func(*sesv2.CreateEmailIdentityInput) (*sesv2.CreateEmailIdentityOutput, error) { return nil, &sesv2types.AlreadyExistsException{Message: aws.String("identity already exists")} }, getEmailIdentity: func(*sesv2.GetEmailIdentityInput) (*sesv2.GetEmailIdentityOutput, error) { touched = true return &sesv2.GetEmailIdentityOutput{}, nil }, putEmailIdentityMailFromAttributes: func(*sesv2.PutEmailIdentityMailFromAttributesInput) (*sesv2.PutEmailIdentityMailFromAttributesOutput, error) { touched = true return &sesv2.PutEmailIdentityMailFromAttributesOutput{}, nil }, } p := &awsProvisioner{ses: stub, account: "123456789012", region: "eu-west-1"} if _, err := p.EnsureIdentity(context.Background(), "eu-west-1", "example.com", "mail.example.com", false); !errors.Is(err, ErrIdentityExists) { t.Fatalf("EnsureIdentity(adopt=false) on an existing identity = %v, want ErrIdentityExists", err) } if touched { t.Fatal("an existing identity was read or rewritten without adopt") } stub.createEmailIdentity = func(*sesv2.CreateEmailIdentityInput) (*sesv2.CreateEmailIdentityOutput, error) { return &sesv2.CreateEmailIdentityOutput{DkimAttributes: &sesv2types.DkimAttributes{Tokens: []string{"a", "b", "c"}}}, nil } got, err := p.EnsureIdentity(context.Background(), "eu-west-1", "example.com", "mail.example.com", false) if err != nil || !got.Created { t.Fatalf("a fresh create = %+v, %v; want Created", got, err) } } // TestEnsureIdentityRefusesNoTokens is the other half of the same path: an // identity that reads back with no DKIM tokens (BYODKIM, an email-address // identity, or a nil DkimAttributes) must not be reported as success — the // walk would publish no CNAMEs and the domain could never converge, with // nothing surfacing why. func TestEnsureIdentityRefusesNoTokens(t *testing.T) { stub := &stubSES{ createEmailIdentity: func(*sesv2.CreateEmailIdentityInput) (*sesv2.CreateEmailIdentityOutput, error) { return &sesv2.CreateEmailIdentityOutput{}, nil }, } p := &awsProvisioner{ses: stub, account: "123456789012", region: "eu-west-1"} if _, err := p.EnsureIdentity(context.Background(), "eu-west-1", "example.com", "mail.example.com", true); err == nil { t.Fatal("EnsureIdentity with zero DKIM tokens returned success, want an error") } } // TestSetIdentityConfigSetCallsThroughWithTheRightNames (C1, review // follow-up): this is the ONLY call in the whole package that actually // binds an identity to the configuration set carrying SES's event // destination — without it SES never emits an event for that identity, no // matter how many messages it sends. Pinned here at the AWS-facing layer, // not just against the in-memory fake, so a call built with the identity // and set-name arguments swapped (or dropped) would fail this test. func TestSetIdentityConfigSetCallsThroughWithTheRightNames(t *testing.T) { var gotIdentity, gotConfigSet string stub := &stubSES{ putEmailIdentityConfigurationSet: func(in *sesv2.PutEmailIdentityConfigurationSetAttributesInput) (*sesv2.PutEmailIdentityConfigurationSetAttributesOutput, error) { gotIdentity = aws.ToString(in.EmailIdentity) gotConfigSet = aws.ToString(in.ConfigurationSetName) return &sesv2.PutEmailIdentityConfigurationSetAttributesOutput{}, nil }, } p := &awsProvisioner{ses: stub, account: "123456789012", region: "eu-west-1"} if err := p.SetIdentityConfigSet(context.Background(), "eu-west-1", "example.com", "carlos-bag"); err != nil { t.Fatalf("SetIdentityConfigSet: %v", err) } if gotIdentity != "example.com" { t.Fatalf("EmailIdentity = %q, want %q", gotIdentity, "example.com") } if gotConfigSet != "carlos-bag" { t.Fatalf("ConfigurationSetName = %q, want %q", gotConfigSet, "carlos-bag") } } // A backend failure (e.g. the identity does not exist) must propagate, not // be swallowed as though the binding succeeded — a silently-not-bound // identity is exactly the C1 defect this whole verb exists to close. func TestSetIdentityConfigSetPropagatesBackendError(t *testing.T) { stub := &stubSES{ putEmailIdentityConfigurationSet: func(*sesv2.PutEmailIdentityConfigurationSetAttributesInput) (*sesv2.PutEmailIdentityConfigurationSetAttributesOutput, error) { return nil, &sesv2types.NotFoundException{Message: aws.String("no such identity")} }, } p := &awsProvisioner{ses: stub, account: "123456789012", region: "eu-west-1"} if err := p.SetIdentityConfigSet(context.Background(), "eu-west-1", "example.com", "carlos-bag"); err == nil { t.Fatal("SetIdentityConfigSet swallowed a backend error") } } // wellFormedIdentityStub returns a stubSES whose CreateEmailIdentity and // PutEmailIdentityMailFromAttributes both succeed, with three non-empty // DKIM tokens. Used by the region-guard tests below so that success or // failure turns ENTIRELY on checkRegion and never on fix #3's zero-token // guard — round 1's TestEnsureIdentityRefusesAMismatchedRegion used a bare // &stubSES{}, whose zero-value CreateEmailIdentity success carries no // DkimAttributes; the zero-token guard fired and satisfied "an error came // back" even with checkRegion's call site deleted entirely, so the test // passed for the wrong reason. Confirmed empirically below (see the // round-2 fix report) by removing the checkRegion call and rerunning. func wellFormedIdentityStub() *stubSES { return &stubSES{ createEmailIdentity: func(*sesv2.CreateEmailIdentityInput) (*sesv2.CreateEmailIdentityOutput, error) { return &sesv2.CreateEmailIdentityOutput{ DkimAttributes: &sesv2types.DkimAttributes{Tokens: []string{"t1", "t2", "t3"}}, }, nil }, } } // TestRegionGuardRefusesAMismatch covers all four SES-facing methods that // take a region parameter (EnsureConfigSet, EnsureIdentity, IdentityStatus, // DeleteIdentity): a caller naming a region other than the one the // provisioner is bound to must be refused, and the refusal must be // checkRegion's — not an error that happens to fire for an unrelated // reason. Every stub here answers with an unconditional success for // whatever SES call the guard is supposed to prevent from ever being // reached, so if the guard is silently removed the call SUCCEEDS instead of // merely "erroring somehow" — which is what makes the errors.Is assertion // meaningful rather than vacuous. func TestRegionGuardRefusesAMismatch(t *testing.T) { const boundRegion = "eu-west-1" const otherRegion = "us-east-1" cases := []struct { name string call func(p *awsProvisioner) error }{ {"EnsureConfigSet", func(p *awsProvisioner) error { return p.EnsureConfigSet(context.Background(), otherRegion, "cfg", "arn:aws:sns:eu-west-1:123456789012:topic") }}, {"EnsureIdentity", func(p *awsProvisioner) error { _, err := p.EnsureIdentity(context.Background(), otherRegion, "example.com", "mail.example.com", true) return err }}, {"IdentityStatus", func(p *awsProvisioner) error { _, err := p.IdentityStatus(context.Background(), otherRegion, "example.com") return err }}, {"DeleteIdentity", func(p *awsProvisioner) error { return p.DeleteIdentity(context.Background(), otherRegion, "example.com") }}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { p := &awsProvisioner{ses: wellFormedIdentityStub(), account: "123456789012", region: boundRegion} err := tc.call(p) if err == nil { t.Fatalf("%s accepted region %q against a provisioner bound to %q", tc.name, otherRegion, boundRegion) } if !errors.Is(err, errRegionMismatch) { t.Fatalf("%s error = %v, want it to wrap errRegionMismatch", tc.name, err) } }) } } // TestRegionGuardPassesAnEmptyRegionThrough pins the deliberate escape: // checkRegion treats "" as "caller doesn't care" rather than a mismatch. // Each case uses a stub that succeeds unconditionally, so a passing test // here means the guard did not fire — not that some other stub default // happened to swallow an error. func TestRegionGuardPassesAnEmptyRegionThrough(t *testing.T) { const boundRegion = "eu-west-1" cases := []struct { name string call func(p *awsProvisioner) error }{ {"EnsureConfigSet", func(p *awsProvisioner) error { return p.EnsureConfigSet(context.Background(), "", "cfg", "arn:aws:sns:eu-west-1:123456789012:topic") }}, {"EnsureIdentity", func(p *awsProvisioner) error { _, err := p.EnsureIdentity(context.Background(), "", "example.com", "mail.example.com", true) return err }}, {"IdentityStatus", func(p *awsProvisioner) error { _, err := p.IdentityStatus(context.Background(), "", "example.com") return err }}, {"DeleteIdentity", func(p *awsProvisioner) error { return p.DeleteIdentity(context.Background(), "", "example.com") }}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { p := &awsProvisioner{ses: wellFormedIdentityStub(), account: "123456789012", region: boundRegion} if err := tc.call(p); err != nil { t.Fatalf("%s with an empty region was refused: %v", tc.name, err) } }) } } // TestEnsureTenantCallsCreateTenantAndAcceptsAlreadyExists exercises // EnsureTenant, which added CreateTenant to sesAPI and was otherwise never // stubbed anywhere. func TestEnsureTenantCallsCreateTenantAndAcceptsAlreadyExists(t *testing.T) { var gotName string stub := &stubSES{ createTenant: func(in *sesv2.CreateTenantInput) (*sesv2.CreateTenantOutput, error) { gotName = aws.ToString(in.TenantName) return nil, &sesv2types.AlreadyExistsException{Message: aws.String("tenant already exists")} }, } p := &awsProvisioner{ses: stub, account: "123456789012", region: "eu-west-1"} if err := p.EnsureTenant(context.Background(), "acct-42"); err != nil { t.Fatalf("EnsureTenant: %v", err) } if gotName != "acct-42" { t.Fatalf("CreateTenant called with TenantName %q, want %q", gotName, "acct-42") } }