carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package mail
2
3import (
4 "context"
5 "errors"
6 "reflect"
7 "testing"
8
9 "github.com/aws/aws-sdk-go-v2/aws"
10 iamtypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
11 "github.com/aws/aws-sdk-go-v2/service/sesv2"
12 sesv2types "github.com/aws/aws-sdk-go-v2/service/sesv2/types"
13)
14
15// "Already exists" is a SUCCESS. Every walk re-runs after a half-failure,
16// and a second EnsureIdentity that errors would make a crashed run
17// unrepairable except by hand.
18func TestAlreadyExistsIsRecognised(t *testing.T) {
19 if !isAlreadyExists(&sesv2types.AlreadyExistsException{}) {
20 t.Fatal("SES AlreadyExistsException not recognised")
21 }
22 if !isAlreadyExists(&iamtypes.EntityAlreadyExistsException{}) {
23 t.Fatal("IAM EntityAlreadyExistsException not recognised")
24 }
25 if isAlreadyExists(errors.New("some other failure")) {
26 t.Fatal("an unrelated error was treated as already-exists")
27 }
28}
29
30// "Already gone" is a SUCCESS, for the same reason in the other direction:
31// teardown must be re-runnable.
32func TestNotFoundIsRecognised(t *testing.T) {
33 if !isNotFound(&sesv2types.NotFoundException{}) {
34 t.Fatal("SES NotFoundException not recognised")
35 }
36 if !isNotFound(&iamtypes.NoSuchEntityException{}) {
37 t.Fatal("IAM NoSuchEntityException not recognised")
38 }
39 if isNotFound(errors.New("some other failure")) {
40 t.Fatal("an unrelated error was treated as not-found")
41 }
42}
43
44// BehaviorOnMxFailure must be REJECT_MESSAGE, not USE_DEFAULT_VALUE.
45//
46// USE_DEFAULT_VALUE silently falls back to amazonses.com when the custom
47// MAIL FROM MX is missing — which means a misconfigured tenant keeps
48// sending, from an unaligned envelope domain, failing DMARC, and nothing
49// surfaces the fault. Rejecting is what turns a DNS mistake into an error
50// somebody sees.
51func TestMailFromFailureRejectsRatherThanFallsBack(t *testing.T) {
52 if mailFromBehaviour != sesv2types.BehaviorOnMxFailureRejectMessage {
53 t.Fatalf("BehaviorOnMxFailure = %v, want REJECT_MESSAGE", mailFromBehaviour)
54 }
55}
56
57// stubSES is a minimal sesAPI double: only the methods a given test cares
58// about are wired via the function fields; everything else answers with a
59// zero value and a nil error, which is fine because no test below relies on
60// those paths.
61type stubSES struct {
62 createTenant func(*sesv2.CreateTenantInput) (*sesv2.CreateTenantOutput, error)
63 createEmailIdentity func(*sesv2.CreateEmailIdentityInput) (*sesv2.CreateEmailIdentityOutput, error)
64 getEmailIdentity func(*sesv2.GetEmailIdentityInput) (*sesv2.GetEmailIdentityOutput, error)
65 putEmailIdentityMailFromAttributes func(*sesv2.PutEmailIdentityMailFromAttributesInput) (*sesv2.PutEmailIdentityMailFromAttributesOutput, error)
66 putEmailIdentityConfigurationSet func(*sesv2.PutEmailIdentityConfigurationSetAttributesInput) (*sesv2.PutEmailIdentityConfigurationSetAttributesOutput, error)
67}
68
69func (s *stubSES) CreateTenant(_ context.Context, in *sesv2.CreateTenantInput, _ ...func(*sesv2.Options)) (*sesv2.CreateTenantOutput, error) {
70 if s.createTenant != nil {
71 return s.createTenant(in)
72 }
73 return &sesv2.CreateTenantOutput{}, nil
74}
75
76func (s *stubSES) CreateEmailIdentity(_ context.Context, in *sesv2.CreateEmailIdentityInput, _ ...func(*sesv2.Options)) (*sesv2.CreateEmailIdentityOutput, error) {
77 if s.createEmailIdentity != nil {
78 return s.createEmailIdentity(in)
79 }
80 return &sesv2.CreateEmailIdentityOutput{}, nil
81}
82
83func (s *stubSES) GetEmailIdentity(_ context.Context, in *sesv2.GetEmailIdentityInput, _ ...func(*sesv2.Options)) (*sesv2.GetEmailIdentityOutput, error) {
84 if s.getEmailIdentity != nil {
85 return s.getEmailIdentity(in)
86 }
87 return &sesv2.GetEmailIdentityOutput{}, nil
88}
89
90func (s *stubSES) PutEmailIdentityMailFromAttributes(_ context.Context, in *sesv2.PutEmailIdentityMailFromAttributesInput, _ ...func(*sesv2.Options)) (*sesv2.PutEmailIdentityMailFromAttributesOutput, error) {
91 if s.putEmailIdentityMailFromAttributes != nil {
92 return s.putEmailIdentityMailFromAttributes(in)
93 }
94 return &sesv2.PutEmailIdentityMailFromAttributesOutput{}, nil
95}
96
97func (s *stubSES) DeleteEmailIdentity(_ context.Context, in *sesv2.DeleteEmailIdentityInput, _ ...func(*sesv2.Options)) (*sesv2.DeleteEmailIdentityOutput, error) {
98 return &sesv2.DeleteEmailIdentityOutput{}, nil
99}
100
101func (s *stubSES) CreateConfigurationSet(_ context.Context, in *sesv2.CreateConfigurationSetInput, _ ...func(*sesv2.Options)) (*sesv2.CreateConfigurationSetOutput, error) {
102 return &sesv2.CreateConfigurationSetOutput{}, nil
103}
104
105func (s *stubSES) CreateConfigurationSetEventDestination(_ context.Context, in *sesv2.CreateConfigurationSetEventDestinationInput, _ ...func(*sesv2.Options)) (*sesv2.CreateConfigurationSetEventDestinationOutput, error) {
106 return &sesv2.CreateConfigurationSetEventDestinationOutput{}, nil
107}
108
109func (s *stubSES) PutEmailIdentityConfigurationSetAttributes(_ context.Context, in *sesv2.PutEmailIdentityConfigurationSetAttributesInput, _ ...func(*sesv2.Options)) (*sesv2.PutEmailIdentityConfigurationSetAttributesOutput, error) {
110 if s.putEmailIdentityConfigurationSet != nil {
111 return s.putEmailIdentityConfigurationSet(in)
112 }
113 return &sesv2.PutEmailIdentityConfigurationSetAttributesOutput{}, nil
114}
115
116func (s *stubSES) CreateTenantResourceAssociation(_ context.Context, in *sesv2.CreateTenantResourceAssociationInput, _ ...func(*sesv2.Options)) (*sesv2.CreateTenantResourceAssociationOutput, error) {
117 return &sesv2.CreateTenantResourceAssociationOutput{}, nil
118}
119
120func (s *stubSES) DeleteTenantResourceAssociation(_ context.Context, in *sesv2.DeleteTenantResourceAssociationInput, _ ...func(*sesv2.Options)) (*sesv2.DeleteTenantResourceAssociationOutput, error) {
121 return &sesv2.DeleteTenantResourceAssociationOutput{}, nil
122}
123
124var _ sesAPI = (*stubSES)(nil)
125
126// TestEnsureIdentityAlreadyExistsReadsBackTokensAndComposesTheARN pins the
127// F3-critical path: the ruling's own words are that getting this wrong means
128// every IAM policy names a resource that does not exist, and this was
129// otherwise untested until Task 12. It exercises the already-exists branch
130// (CreateEmailIdentity fails, GetEmailIdentity is read back for the DKIM
131// tokens already published in DNS), checks the exact composed ARN, and —
132// the assertion that actually matters, because it pins aws.go and policy.go
133// together — checks that ARN is ACCEPTED by SendPolicyJSON.
134func TestEnsureIdentityAlreadyExistsReadsBackTokensAndComposesTheARN(t *testing.T) {
135 wantTokens := []string{"tok1", "tok2", "tok3"}
136 stub := &stubSES{
137 createEmailIdentity: func(*sesv2.CreateEmailIdentityInput) (*sesv2.CreateEmailIdentityOutput, error) {
138 return nil, &sesv2types.AlreadyExistsException{Message: aws.String("identity already exists")}
139 },
140 getEmailIdentity: func(*sesv2.GetEmailIdentityInput) (*sesv2.GetEmailIdentityOutput, error) {
141 return &sesv2.GetEmailIdentityOutput{
142 DkimAttributes: &sesv2types.DkimAttributes{Tokens: wantTokens},
143 }, nil
144 },
145 }
146 p := &awsProvisioner{ses: stub, account: "123456789012", region: "eu-west-1"}
147
148 got, err := p.EnsureIdentity(context.Background(), "eu-west-1", "example.com", "mail.example.com", true)
149 if err != nil {
150 t.Fatalf("EnsureIdentity: %v", err)
151 }
152 if !reflect.DeepEqual(got.DKIMTokens, wantTokens) {
153 t.Fatalf("DKIMTokens = %v, want the ones read back from GetEmailIdentity %v", got.DKIMTokens, wantTokens)
154 }
155
156 const wantARN = "arn:aws:ses:eu-west-1:123456789012:identity/example.com"
157 if got.ARN != wantARN {
158 t.Fatalf("ARN = %q, want %q", got.ARN, wantARN)
159 }
160
161 // The assertion that actually matters: the composed ARN must satisfy
162 // SendPolicyJSON's shape validator, since EnsureIdentity's return value
163 // flows straight into EnsureUser -> SendPolicyJSON in production. A
164 // composed ARN that validator rejects would make every send fail with
165 // no way to notice until Task 12.
166 if _, err := SendPolicyJSON([]string{got.ARN}, nil, []string{"example.com"}); err != nil {
167 t.Fatalf("composed ARN %q was rejected by SendPolicyJSON: %v", got.ARN, err)
168 }
169}
170
171// TestEnsureIdentityWithoutAdoptLeavesAnExistingIdentityAlone (#216): with
172// adopt false, an identity that already exists may belong to another product
173// in this AWS account, so it is refused before it is read or its MAIL FROM
174// rewritten.
175func TestEnsureIdentityWithoutAdoptLeavesAnExistingIdentityAlone(t *testing.T) {
176 touched := false
177 stub := &stubSES{
178 createEmailIdentity: func(*sesv2.CreateEmailIdentityInput) (*sesv2.CreateEmailIdentityOutput, error) {
179 return nil, &sesv2types.AlreadyExistsException{Message: aws.String("identity already exists")}
180 },
181 getEmailIdentity: func(*sesv2.GetEmailIdentityInput) (*sesv2.GetEmailIdentityOutput, error) {
182 touched = true
183 return &sesv2.GetEmailIdentityOutput{}, nil
184 },
185 putEmailIdentityMailFromAttributes: func(*sesv2.PutEmailIdentityMailFromAttributesInput) (*sesv2.PutEmailIdentityMailFromAttributesOutput, error) {
186 touched = true
187 return &sesv2.PutEmailIdentityMailFromAttributesOutput{}, nil
188 },
189 }
190 p := &awsProvisioner{ses: stub, account: "123456789012", region: "eu-west-1"}
191 if _, err := p.EnsureIdentity(context.Background(), "eu-west-1", "example.com", "mail.example.com", false); !errors.Is(err, ErrIdentityExists) {
192 t.Fatalf("EnsureIdentity(adopt=false) on an existing identity = %v, want ErrIdentityExists", err)
193 }
194 if touched {
195 t.Fatal("an existing identity was read or rewritten without adopt")
196 }
197
198 stub.createEmailIdentity = func(*sesv2.CreateEmailIdentityInput) (*sesv2.CreateEmailIdentityOutput, error) {
199 return &sesv2.CreateEmailIdentityOutput{DkimAttributes: &sesv2types.DkimAttributes{Tokens: []string{"a", "b", "c"}}}, nil
200 }
201 got, err := p.EnsureIdentity(context.Background(), "eu-west-1", "example.com", "mail.example.com", false)
202 if err != nil || !got.Created {
203 t.Fatalf("a fresh create = %+v, %v; want Created", got, err)
204 }
205}
206
207// TestEnsureIdentityRefusesNoTokens is the other half of the same path: an
208// identity that reads back with no DKIM tokens (BYODKIM, an email-address
209// identity, or a nil DkimAttributes) must not be reported as success — the
210// walk would publish no CNAMEs and the domain could never converge, with
211// nothing surfacing why.
212func TestEnsureIdentityRefusesNoTokens(t *testing.T) {
213 stub := &stubSES{
214 createEmailIdentity: func(*sesv2.CreateEmailIdentityInput) (*sesv2.CreateEmailIdentityOutput, error) {
215 return &sesv2.CreateEmailIdentityOutput{}, nil
216 },
217 }
218 p := &awsProvisioner{ses: stub, account: "123456789012", region: "eu-west-1"}
219
220 if _, err := p.EnsureIdentity(context.Background(), "eu-west-1", "example.com", "mail.example.com", true); err == nil {
221 t.Fatal("EnsureIdentity with zero DKIM tokens returned success, want an error")
222 }
223}
224
225// TestSetIdentityConfigSetCallsThroughWithTheRightNames (C1, review
226// follow-up): this is the ONLY call in the whole package that actually
227// binds an identity to the configuration set carrying SES's event
228// destination — without it SES never emits an event for that identity, no
229// matter how many messages it sends. Pinned here at the AWS-facing layer,
230// not just against the in-memory fake, so a call built with the identity
231// and set-name arguments swapped (or dropped) would fail this test.
232func TestSetIdentityConfigSetCallsThroughWithTheRightNames(t *testing.T) {
233 var gotIdentity, gotConfigSet string
234 stub := &stubSES{
235 putEmailIdentityConfigurationSet: func(in *sesv2.PutEmailIdentityConfigurationSetAttributesInput) (*sesv2.PutEmailIdentityConfigurationSetAttributesOutput, error) {
236 gotIdentity = aws.ToString(in.EmailIdentity)
237 gotConfigSet = aws.ToString(in.ConfigurationSetName)
238 return &sesv2.PutEmailIdentityConfigurationSetAttributesOutput{}, nil
239 },
240 }
241 p := &awsProvisioner{ses: stub, account: "123456789012", region: "eu-west-1"}
242
243 if err := p.SetIdentityConfigSet(context.Background(), "eu-west-1", "example.com", "carlos-bag"); err != nil {
244 t.Fatalf("SetIdentityConfigSet: %v", err)
245 }
246 if gotIdentity != "example.com" {
247 t.Fatalf("EmailIdentity = %q, want %q", gotIdentity, "example.com")
248 }
249 if gotConfigSet != "carlos-bag" {
250 t.Fatalf("ConfigurationSetName = %q, want %q", gotConfigSet, "carlos-bag")
251 }
252}
253
254// A backend failure (e.g. the identity does not exist) must propagate, not
255// be swallowed as though the binding succeeded — a silently-not-bound
256// identity is exactly the C1 defect this whole verb exists to close.
257func TestSetIdentityConfigSetPropagatesBackendError(t *testing.T) {
258 stub := &stubSES{
259 putEmailIdentityConfigurationSet: func(*sesv2.PutEmailIdentityConfigurationSetAttributesInput) (*sesv2.PutEmailIdentityConfigurationSetAttributesOutput, error) {
260 return nil, &sesv2types.NotFoundException{Message: aws.String("no such identity")}
261 },
262 }
263 p := &awsProvisioner{ses: stub, account: "123456789012", region: "eu-west-1"}
264
265 if err := p.SetIdentityConfigSet(context.Background(), "eu-west-1", "example.com", "carlos-bag"); err == nil {
266 t.Fatal("SetIdentityConfigSet swallowed a backend error")
267 }
268}
269
270// wellFormedIdentityStub returns a stubSES whose CreateEmailIdentity and
271// PutEmailIdentityMailFromAttributes both succeed, with three non-empty
272// DKIM tokens. Used by the region-guard tests below so that success or
273// failure turns ENTIRELY on checkRegion and never on fix #3's zero-token
274// guard — round 1's TestEnsureIdentityRefusesAMismatchedRegion used a bare
275// &stubSES{}, whose zero-value CreateEmailIdentity success carries no
276// DkimAttributes; the zero-token guard fired and satisfied "an error came
277// back" even with checkRegion's call site deleted entirely, so the test
278// passed for the wrong reason. Confirmed empirically below (see the
279// round-2 fix report) by removing the checkRegion call and rerunning.
280func wellFormedIdentityStub() *stubSES {
281 return &stubSES{
282 createEmailIdentity: func(*sesv2.CreateEmailIdentityInput) (*sesv2.CreateEmailIdentityOutput, error) {
283 return &sesv2.CreateEmailIdentityOutput{
284 DkimAttributes: &sesv2types.DkimAttributes{Tokens: []string{"t1", "t2", "t3"}},
285 }, nil
286 },
287 }
288}
289
290// TestRegionGuardRefusesAMismatch covers all four SES-facing methods that
291// take a region parameter (EnsureConfigSet, EnsureIdentity, IdentityStatus,
292// DeleteIdentity): a caller naming a region other than the one the
293// provisioner is bound to must be refused, and the refusal must be
294// checkRegion's — not an error that happens to fire for an unrelated
295// reason. Every stub here answers with an unconditional success for
296// whatever SES call the guard is supposed to prevent from ever being
297// reached, so if the guard is silently removed the call SUCCEEDS instead of
298// merely "erroring somehow" — which is what makes the errors.Is assertion
299// meaningful rather than vacuous.
300func TestRegionGuardRefusesAMismatch(t *testing.T) {
301 const boundRegion = "eu-west-1"
302 const otherRegion = "us-east-1"
303
304 cases := []struct {
305 name string
306 call func(p *awsProvisioner) error
307 }{
308 {"EnsureConfigSet", func(p *awsProvisioner) error {
309 return p.EnsureConfigSet(context.Background(), otherRegion, "cfg", "arn:aws:sns:eu-west-1:123456789012:topic")
310 }},
311 {"EnsureIdentity", func(p *awsProvisioner) error {
312 _, err := p.EnsureIdentity(context.Background(), otherRegion, "example.com", "mail.example.com", true)
313 return err
314 }},
315 {"IdentityStatus", func(p *awsProvisioner) error {
316 _, err := p.IdentityStatus(context.Background(), otherRegion, "example.com")
317 return err
318 }},
319 {"DeleteIdentity", func(p *awsProvisioner) error {
320 return p.DeleteIdentity(context.Background(), otherRegion, "example.com")
321 }},
322 }
323
324 for _, tc := range cases {
325 t.Run(tc.name, func(t *testing.T) {
326 p := &awsProvisioner{ses: wellFormedIdentityStub(), account: "123456789012", region: boundRegion}
327 err := tc.call(p)
328 if err == nil {
329 t.Fatalf("%s accepted region %q against a provisioner bound to %q", tc.name, otherRegion, boundRegion)
330 }
331 if !errors.Is(err, errRegionMismatch) {
332 t.Fatalf("%s error = %v, want it to wrap errRegionMismatch", tc.name, err)
333 }
334 })
335 }
336}
337
338// TestRegionGuardPassesAnEmptyRegionThrough pins the deliberate escape:
339// checkRegion treats "" as "caller doesn't care" rather than a mismatch.
340// Each case uses a stub that succeeds unconditionally, so a passing test
341// here means the guard did not fire — not that some other stub default
342// happened to swallow an error.
343func TestRegionGuardPassesAnEmptyRegionThrough(t *testing.T) {
344 const boundRegion = "eu-west-1"
345
346 cases := []struct {
347 name string
348 call func(p *awsProvisioner) error
349 }{
350 {"EnsureConfigSet", func(p *awsProvisioner) error {
351 return p.EnsureConfigSet(context.Background(), "", "cfg", "arn:aws:sns:eu-west-1:123456789012:topic")
352 }},
353 {"EnsureIdentity", func(p *awsProvisioner) error {
354 _, err := p.EnsureIdentity(context.Background(), "", "example.com", "mail.example.com", true)
355 return err
356 }},
357 {"IdentityStatus", func(p *awsProvisioner) error {
358 _, err := p.IdentityStatus(context.Background(), "", "example.com")
359 return err
360 }},
361 {"DeleteIdentity", func(p *awsProvisioner) error {
362 return p.DeleteIdentity(context.Background(), "", "example.com")
363 }},
364 }
365
366 for _, tc := range cases {
367 t.Run(tc.name, func(t *testing.T) {
368 p := &awsProvisioner{ses: wellFormedIdentityStub(), account: "123456789012", region: boundRegion}
369 if err := tc.call(p); err != nil {
370 t.Fatalf("%s with an empty region was refused: %v", tc.name, err)
371 }
372 })
373 }
374}
375
376// TestEnsureTenantCallsCreateTenantAndAcceptsAlreadyExists exercises
377// EnsureTenant, which added CreateTenant to sesAPI and was otherwise never
378// stubbed anywhere.
379func TestEnsureTenantCallsCreateTenantAndAcceptsAlreadyExists(t *testing.T) {
380 var gotName string
381 stub := &stubSES{
382 createTenant: func(in *sesv2.CreateTenantInput) (*sesv2.CreateTenantOutput, error) {
383 gotName = aws.ToString(in.TenantName)
384 return nil, &sesv2types.AlreadyExistsException{Message: aws.String("tenant already exists")}
385 },
386 }
387 p := &awsProvisioner{ses: stub, account: "123456789012", region: "eu-west-1"}
388
389 if err := p.EnsureTenant(context.Background(), "acct-42"); err != nil {
390 t.Fatalf("EnsureTenant: %v", err)
391 }
392 if gotName != "acct-42" {
393 t.Fatalf("CreateTenant called with TenantName %q, want %q", gotName, "acct-42")
394 }
395}
396