| 1 | package mail |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "testing" |
| 7 | ) |
| 8 | |
| 9 | // Idempotence is the contract, not a nicety: every walk re-runs after a |
| 10 | // half-failure, so a second EnsureIdentity must be a success that returns |
| 11 | // the SAME tokens — fresh tokens would orphan the DNS already published. |
| 12 | func TestMemEnsureIdentityIsIdempotentAndTokenStable(t *testing.T) { |
| 13 | ctx, p := context.Background(), NewMemProvisioner("1") |
| 14 | first, err := p.EnsureIdentity(ctx, "eu-west-1", "x.com", "mail-eu-west-1.x.com", true) |
| 15 | if err != nil { |
| 16 | t.Fatalf("EnsureIdentity: %v", err) |
| 17 | } |
| 18 | if len(first.DKIMTokens) != 3 { |
| 19 | t.Fatalf("want 3 DKIM tokens, got %d", len(first.DKIMTokens)) |
| 20 | } |
| 21 | second, err := p.EnsureIdentity(ctx, "eu-west-1", "x.com", "mail-eu-west-1.x.com", true) |
| 22 | if err != nil { |
| 23 | t.Fatalf("re-EnsureIdentity: %v", err) |
| 24 | } |
| 25 | if first.ARN != second.ARN || first.DKIMTokens[0] != second.DKIMTokens[0] { |
| 26 | t.Fatal("re-ensuring changed the identity — the published DNS would be orphaned") |
| 27 | } |
| 28 | } |
| 29 | |
| 30 | // Without adopt, the fake refuses an existing identity untouched, as SES |
| 31 | // does through awsProvisioner (#216). |
| 32 | func TestMemEnsureIdentityWithoutAdoptRefusesUntouched(t *testing.T) { |
| 33 | ctx, p := context.Background(), NewMemProvisioner("1") |
| 34 | first, err := p.EnsureIdentity(ctx, "eu-west-1", "x.com", "m1.x.com", false) |
| 35 | if err != nil || !first.Created { |
| 36 | t.Fatalf("create = %+v, %v", first, err) |
| 37 | } |
| 38 | if _, err := p.EnsureIdentity(ctx, "eu-west-1", "x.com", "m2.x.com", false); !errors.Is(err, ErrIdentityExists) { |
| 39 | t.Fatalf("re-ensure without adopt = %v, want ErrIdentityExists", err) |
| 40 | } |
| 41 | if got := p.Identities[identityKey("eu-west-1", "x.com")].MailFrom; got != "m1.x.com" { |
| 42 | t.Fatalf("MAIL FROM rewritten to %q without adopt", got) |
| 43 | } |
| 44 | again, err := p.EnsureIdentity(ctx, "eu-west-1", "x.com", "m1.x.com", true) |
| 45 | if err != nil || again.Created { |
| 46 | t.Fatalf("adopt = %+v, %v; want the existing identity, not Created", again, err) |
| 47 | } |
| 48 | } |
| 49 | |
| 50 | func TestMemIdentitiesAreRegionScoped(t *testing.T) { |
| 51 | ctx, p := context.Background(), NewMemProvisioner("1") |
| 52 | eu, _ := p.EnsureIdentity(ctx, "eu-west-1", "x.com", "mail-eu-west-1.x.com", true) |
| 53 | us, _ := p.EnsureIdentity(ctx, "us-east-1", "x.com", "mail-us-east-1.x.com", true) |
| 54 | if eu.ARN == us.ARN { |
| 55 | t.Fatal("one ARN served two regions") |
| 56 | } |
| 57 | if eu.DKIMTokens[0] == us.DKIMTokens[0] { |
| 58 | t.Fatal("two regions shared a DKIM token — they are independent identities") |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | func TestMemDeleteIsIdempotent(t *testing.T) { |
| 63 | ctx, p := context.Background(), NewMemProvisioner("1") |
| 64 | if err := p.DeleteIdentity(ctx, "eu-west-1", "absent.com"); err != nil { |
| 65 | t.Fatalf("deleting an absent identity = %v, want nil", err) |
| 66 | } |
| 67 | if err := p.DeleteUser(ctx, "absent-user"); err != nil { |
| 68 | t.Fatalf("deleting an absent user = %v, want nil", err) |
| 69 | } |
| 70 | } |
| 71 | |
| 72 | // The pause is SetKeyActive, and the fake must model it so a walk test can |
| 73 | // assert a paused credential really is inactive. |
| 74 | func TestMemSetKeyActiveTogglesTheKey(t *testing.T) { |
| 75 | ctx, p := context.Background(), NewMemProvisioner("1") |
| 76 | if _, err := p.EnsureUser(ctx, "u", UserPath, []string{"arn:aws:ses:eu-west-1:1:identity/x.com"}, nil, []string{"x.com"}); err != nil { |
| 77 | t.Fatalf("EnsureUser: %v", err) |
| 78 | } |
| 79 | id, _, err := p.MintKey(ctx, "u") |
| 80 | if err != nil { |
| 81 | t.Fatalf("MintKey: %v", err) |
| 82 | } |
| 83 | if !p.Users["u"].Keys[id].Active { |
| 84 | t.Fatal("a freshly minted key must be active") |
| 85 | } |
| 86 | if err := p.SetKeyActive(ctx, "u", id, false); err != nil { |
| 87 | t.Fatalf("SetKeyActive: %v", err) |
| 88 | } |
| 89 | if p.Users["u"].Keys[id].Active { |
| 90 | t.Fatal("SetKeyActive(false) did not deactivate the key") |
| 91 | } |
| 92 | } |
| 93 | |
| 94 | // ReapKeys is what finds a key stranded by a crash between mint and record — |
| 95 | // no record names it, so only list-and-delete can. |
| 96 | func TestMemReapKeysKeepsOnlyTheNamedOnes(t *testing.T) { |
| 97 | ctx, p := context.Background(), NewMemProvisioner("1") |
| 98 | p.EnsureUser(ctx, "u", UserPath, []string{"arn:aws:ses:eu-west-1:1:identity/x.com"}, nil, []string{"x.com"}) |
| 99 | keep, _, _ := p.MintKey(ctx, "u") |
| 100 | stranded, _, _ := p.MintKey(ctx, "u") |
| 101 | if err := p.ReapKeys(ctx, "u", []string{keep}); err != nil { |
| 102 | t.Fatalf("ReapKeys: %v", err) |
| 103 | } |
| 104 | if _, ok := p.Users["u"].Keys[stranded]; ok { |
| 105 | t.Fatal("ReapKeys left a key the record does not name") |
| 106 | } |
| 107 | if _, ok := p.Users["u"].Keys[keep]; !ok { |
| 108 | t.Fatal("ReapKeys deleted a key it was told to keep") |
| 109 | } |
| 110 | } |
| 111 | |
| 112 | // IAM hard-caps every user at two access keys. MintKey must enforce that |
| 113 | // cap in the fake too, since a rotation walk (Task 13) mints a second key |
| 114 | // while the first is still live, and a fake that let a third key through |
| 115 | // would pass a test that fails the first time it runs against real IAM. |
| 116 | // Both halves matter: the first two must succeed (a cap misapplied at two |
| 117 | // would break ordinary rotation), and only the third must fail. |
| 118 | func TestMemMintKeyEnforcesTheTwoKeyCap(t *testing.T) { |
| 119 | ctx, p := context.Background(), NewMemProvisioner("1") |
| 120 | if _, err := p.EnsureUser(ctx, "u", UserPath, []string{"arn:aws:ses:eu-west-1:1:identity/x.com"}, nil, []string{"x.com"}); err != nil { |
| 121 | t.Fatalf("EnsureUser: %v", err) |
| 122 | } |
| 123 | if _, _, err := p.MintKey(ctx, "u"); err != nil { |
| 124 | t.Fatalf("first MintKey: %v, want success", err) |
| 125 | } |
| 126 | if _, _, err := p.MintKey(ctx, "u"); err != nil { |
| 127 | t.Fatalf("second MintKey: %v, want success — the cap must not fire before the third key", err) |
| 128 | } |
| 129 | if _, _, err := p.MintKey(ctx, "u"); err == nil { |
| 130 | t.Fatal("third MintKey succeeded, want IAM's two-key-per-user limit enforced") |
| 131 | } |
| 132 | } |
| 133 | |
| 134 | // EnsureUser must refuse an ungranted principal rather than create one with |
| 135 | // no policy — an unscoped sending user is the failure this package exists to |
| 136 | // prevent. |
| 137 | func TestMemEnsureUserRefusesAnEmptyGrant(t *testing.T) { |
| 138 | if _, err := NewMemProvisioner("1").EnsureUser(context.Background(), "u", UserPath, nil, nil, nil); err == nil { |
| 139 | t.Fatal("EnsureUser with no ARNs returned success") |
| 140 | } |
| 141 | } |
| 142 | |
| 143 | func TestMemFailNextIsOneShot(t *testing.T) { |
| 144 | ctx, p := context.Background(), NewMemProvisioner("1") |
| 145 | boom := errors.New("boom") |
| 146 | p.FailNext["EnsureIdentity"] = boom |
| 147 | if _, err := p.EnsureIdentity(ctx, "eu-west-1", "x.com", "m.x.com", true); !errors.Is(err, boom) { |
| 148 | t.Fatalf("first call = %v, want boom", err) |
| 149 | } |
| 150 | if _, err := p.EnsureIdentity(ctx, "eu-west-1", "x.com", "m.x.com", true); err != nil { |
| 151 | t.Fatalf("second call = %v, want nil", err) |
| 152 | } |
| 153 | } |
| 154 | |
| 155 | // The fake refuses to delete an identity that is still associated with a |
| 156 | // tenant, because SES does. A walk that got the order wrong would pass |
| 157 | // against a permissive fake and fail on the first real teardown. |
| 158 | func TestMemProvisionerRefusesToDeleteAnAssociatedIdentity(t *testing.T) { |
| 159 | ctx := context.Background() |
| 160 | p := NewMemProvisioner("1") |
| 161 | id, err := p.EnsureIdentity(ctx, "eu-west-1", "x.com", "mail-eu-west-1.x.com", true) |
| 162 | if err != nil { |
| 163 | t.Fatal(err) |
| 164 | } |
| 165 | if err := p.AssociateTenantResource(ctx, "carlos-bag", id.ARN); err != nil { |
| 166 | t.Fatal(err) |
| 167 | } |
| 168 | if err := p.DeleteIdentity(ctx, "eu-west-1", "x.com"); err == nil { |
| 169 | t.Fatal("an associated identity was deleted") |
| 170 | } |
| 171 | if err := p.DisassociateTenantResource(ctx, "carlos-bag", id.ARN); err != nil { |
| 172 | t.Fatal(err) |
| 173 | } |
| 174 | // Disassociating twice is a success, like every other verb on this seam. |
| 175 | if err := p.DisassociateTenantResource(ctx, "carlos-bag", id.ARN); err != nil { |
| 176 | t.Fatal(err) |
| 177 | } |
| 178 | if err := p.DeleteIdentity(ctx, "eu-west-1", "x.com"); err != nil { |
| 179 | t.Fatal(err) |
| 180 | } |
| 181 | } |
| 182 | |