carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package main
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "strings"
8 "testing"
9
10 "github.com/aws/aws-sdk-go-v2/service/sesv2"
11
12 "github.com/carlosframework/platform/internal/dnsrec"
13 "github.com/carlosframework/platform/internal/mail"
14 "github.com/carlosframework/platform/internal/release"
15)
16
17// failIfCalled builds a newProvisioner/newWriter seam that fails the test if
18// it is ever invoked — used to prove mailBackends does not make a
19// construction call (and therefore no network I/O) when the coordinate that
20// would justify it is unset.
21
22func failProvisionerIfCalled(t *testing.T) func(context.Context, string) (mail.Provisioner, error) {
23 t.Helper()
24 return func(ctx context.Context, region string) (mail.Provisioner, error) {
25 t.Fatalf("newProvisioner called with region %q — mailBackends must not construct a Provisioner when Region is unset", region)
26 return nil, nil
27 }
28}
29
30func failWriterIfCalled(t *testing.T) func(context.Context) (dnsrec.Writer, error) {
31 t.Helper()
32 return func(ctx context.Context) (dnsrec.Writer, error) {
33 t.Fatal("newWriter called — mailBackends must not construct a DNSWriter when PlatformSuffix is unset")
34 return nil, nil
35 }
36}
37
38// TestMailBackendsNoEnvNilBoth is property 1 from the wiring brief: with no
39// mail coordinates configured (the shape of every deployment before this
40// change, and every deployment today that has not set CARLOS_MAIL_REGION /
41// CARLOS_MAIL_PLATFORM_SUFFIX), both fields come back nil and NEITHER
42// constructor is invoked — no AWS call happens at boot.
43func TestMailBackendsNoEnvNilBoth(t *testing.T) {
44 provisioner, writer := mailBackends(mail.Deployment{}, failProvisionerIfCalled(t), failWriterIfCalled(t))
45 if provisioner != nil {
46 t.Errorf("provisioner = %v, want nil", provisioner)
47 }
48 if writer != nil {
49 t.Errorf("writer = %v, want nil", writer)
50 }
51}
52
53// TestMailBackendsProvisionerFailureDoesNotCrash is property 2: a region IS
54// configured, but construction fails (bad region, no AWS credentials, an
55// outage). Boot must not crash — mailBackends must return normally with a
56// nil provisioner rather than calling log.Fatal (which would os.Exit this
57// very test process).
58func TestMailBackendsProvisionerFailureDoesNotCrash(t *testing.T) {
59 dep := mail.Deployment{Region: "eu-west-1"}
60 failer := func(ctx context.Context, region string) (mail.Provisioner, error) {
61 return nil, errors.New("boom: no AWS credentials on this box")
62 }
63 provisioner, writer := mailBackends(dep, failer, failWriterIfCalled(t))
64 if provisioner != nil {
65 t.Errorf("provisioner = %v, want nil after a construction failure", provisioner)
66 }
67 if writer != nil {
68 t.Errorf("writer = %v, want nil — PlatformSuffix was unset regardless of the provisioner outcome", writer)
69 }
70 // Reaching this line at all is part of what is under test: log.Fatal
71 // inside mailBackends would have os.Exit'd the test binary before this
72 // assertion ever ran.
73}
74
75// TestMailBackendsWriterFailureDoesNotCrash is the DNS-writer half of
76// property 2: a platform suffix is configured, but Route53 writer
77// construction fails. Boot must not crash — nil writer, no exit.
78func TestMailBackendsWriterFailureDoesNotCrash(t *testing.T) {
79 dep := mail.Deployment{PlatformSuffix: "oncarlos.com"}
80 failer := func(ctx context.Context) (dnsrec.Writer, error) {
81 return nil, errors.New("boom: route53 unreachable")
82 }
83 provisioner, writer := mailBackends(dep, failProvisionerIfCalled(t), failer)
84 if provisioner != nil {
85 t.Errorf("provisioner = %v, want nil — Region was unset regardless of the writer outcome", provisioner)
86 }
87 if writer != nil {
88 t.Errorf("writer = %v, want nil after a construction failure", writer)
89 }
90}
91
92// TestMailBackendsWriterNilWithoutSuffix is property 3: a region IS
93// configured (so the provisioner constructs), but PlatformSuffix is not —
94// the writer must stay nil, and newWriter must not even be called, whether
95// or not the provisioner side succeeds.
96func TestMailBackendsWriterNilWithoutSuffix(t *testing.T) {
97 dep := mail.Deployment{Region: "eu-west-1"}
98 newProvisioner := func(ctx context.Context, region string) (mail.Provisioner, error) {
99 return mail.NewMemProvisioner("123456789012"), nil
100 }
101 provisioner, writer := mailBackends(dep, newProvisioner, failWriterIfCalled(t))
102 if provisioner == nil {
103 t.Error("provisioner = nil, want the constructed MemProvisioner — Region was set")
104 }
105 if writer != nil {
106 t.Errorf("writer = %v, want nil — PlatformSuffix was unset", writer)
107 }
108}
109
110// TestMailBackendsBothConfiguredBothConstructed is the positive path: both
111// coordinates set, both constructors succeed, both fields come back
112// populated with exactly what the constructor returned.
113func TestMailBackendsBothConfiguredBothConstructed(t *testing.T) {
114 dep := mail.Deployment{Region: "eu-west-1", PlatformSuffix: "oncarlos.com"}
115 wantP := mail.NewMemProvisioner("123456789012")
116 wantW := dnsrec.NewMemWriter()
117 newProvisioner := func(ctx context.Context, region string) (mail.Provisioner, error) {
118 if region != "eu-west-1" {
119 t.Errorf("newProvisioner region = %q, want eu-west-1", region)
120 }
121 return wantP, nil
122 }
123 newWriter := func(ctx context.Context) (dnsrec.Writer, error) {
124 return wantW, nil
125 }
126 provisioner, writer := mailBackends(dep, newProvisioner, newWriter)
127 if provisioner != mail.Provisioner(wantP) {
128 t.Errorf("provisioner = %v, want the constructed MemProvisioner", provisioner)
129 }
130 if writer != dnsrec.Writer(wantW) {
131 t.Errorf("writer = %v, want the constructed MemWriter", writer)
132 }
133}
134
135// TestConsoleMailerWiring covers consoleMailer's own coordinates (From,
136// Provisioner, SES client construction) — distinct from mailBackends' above,
137// since the console mailer additionally needs a From address and a working
138// SES client, neither of which mailBackends' Provisioner/Writer pair cover.
139func TestConsoleMailerWiring(t *testing.T) {
140 sesOK := func(context.Context, string) (mail.EmailSender, error) { return fakeSESClient{}, nil }
141 dep := mail.Deployment{Region: "eu-west-1", PlatformSuffix: "oncarlos.test"}
142 store := release.NewDirStore(t.TempDir())
143 prov := mail.NewMemProvisioner("1")
144
145 t.Setenv("CARLOS_CONSOLE_MAIL_FROM", "")
146 if m, _ := consoleMailer(dep, store, prov, nil, sesOK, false); m == nil {
147 t.Error("suffix set, no explicit From: want a mailer on the default address")
148 }
149 if m, _ := consoleMailer(mail.Deployment{Region: "eu-west-1"}, store, prov, nil, sesOK, false); m != nil {
150 t.Error("no suffix, no explicit From: want no mailer")
151 }
152 if m, _ := consoleMailer(dep, store, nil, nil, sesOK, false); m != nil {
153 t.Error("no provisioner: want no mailer")
154 }
155 t.Setenv("CARLOS_CONSOLE_MAIL_FROM", "invites@shop.example")
156 if m, _ := consoleMailer(mail.Deployment{Region: "eu-west-1"}, store, prov, nil, sesOK, false); m == nil {
157 t.Error("explicit From, no suffix: want a mailer")
158 }
159 sesFail := func(context.Context, string) (mail.EmailSender, error) { return nil, errors.New("no creds") }
160 if m, _ := consoleMailer(dep, store, prov, nil, sesFail, false); m != nil {
161 t.Error("SES client construction failed: want no mailer, not a crash")
162 }
163 // #250: a console under claims must not have console mail until a gate
164 // route exists — hostClaim true refuses even when every other coordinate
165 // (suffix, region, provisioner) is set, and must do so before touching
166 // SES at all.
167 sesMustNotBeCalled := func(context.Context, string) (mail.EmailSender, error) {
168 t.Fatal("newSES called — consoleMailer must not construct a SES client when hostClaim is true")
169 return nil, nil
170 }
171 if m, _ := consoleMailer(dep, store, prov, nil, sesMustNotBeCalled, true); m != nil {
172 t.Error("hostClaim true: want no mailer, even with suffix, region and provisioner set")
173 }
174}
175
176type fakeSESClient struct{}
177
178func (fakeSESClient) SendEmail(context.Context, *sesv2.SendEmailInput, ...func(*sesv2.Options)) (*sesv2.SendEmailOutput, error) {
179 return &sesv2.SendEmailOutput{}, nil
180}
181
182// TestMailUserBoundary is the console's startup check on
183// CARLOS_MAIL_USER_BOUNDARY, the mail twin of storeBackend's
184// CARLOS_STORE_USER_BOUNDARY: with CARLOS_MAIL_REGION set (the mail
185// provisioner on), a missing or malformed boundary is refused, because the
186// provisioner would otherwise create sending users with no ceiling. With no
187// region there is no provisioner, so nothing is required.
188func TestMailUserBoundary(t *testing.T) {
189 const arn = "arn:aws:iam::123456789012:policy/carlos-mail-boundary"
190 for _, tc := range []struct {
191 name, region, raw, want string
192 wantErr bool
193 }{
194 {name: "no region, no boundary", region: "", raw: ""},
195 {name: "no region, boundary ignored", region: "", raw: arn},
196 {name: "region, no boundary", region: "eu-west-1", raw: "", wantErr: true},
197 {name: "region, blank boundary", region: "eu-west-1", raw: " ", wantErr: true},
198 {name: "region, not a policy ARN", region: "eu-west-1", raw: "carlos-mail-boundary", wantErr: true},
199 {name: "region, account not 12 digits", region: "eu-west-1", raw: "arn:aws:iam::not-an-account:policy/example", wantErr: true},
200 {name: "region, tab inside", region: "eu-west-1", raw: "arn:aws:iam::123456789012:policy/exa\tmple", wantErr: true},
201 {name: "region, newline inside", region: "eu-west-1", raw: "arn:aws:iam::123456789012:policy/exa\nmple", wantErr: true},
202 {name: "region, AWS managed policy", region: "eu-west-1", raw: "arn:aws:iam::aws:policy/AdministratorAccess", wantErr: true},
203 {name: "region, boundary", region: "eu-west-1", raw: " " + arn + "\n", want: arn},
204 } {
205 t.Run(tc.name, func(t *testing.T) {
206 got, err := mailUserBoundary(tc.region, tc.raw)
207 if tc.wantErr {
208 if err == nil {
209 t.Fatalf("mailUserBoundary(%q, %q) = %q, want an error", tc.region, tc.raw, got)
210 }
211 if !strings.Contains(err.Error(), "CARLOS_MAIL_USER_BOUNDARY") {
212 t.Fatalf("error %q does not name CARLOS_MAIL_USER_BOUNDARY", err)
213 }
214 return
215 }
216 if err != nil {
217 t.Fatalf("mailUserBoundary(%q, %q): %v", tc.region, tc.raw, err)
218 }
219 if got != tc.want {
220 t.Fatalf("mailUserBoundary(%q, %q) = %q, want %q", tc.region, tc.raw, got, tc.want)
221 }
222 })
223 }
224}
225
226// fakeBoundarySweeper is a mail.Provisioner that also sweeps, answering
227// with a canned result, so sweepMailBoundaries' reporting can be read.
228type fakeBoundarySweeper struct {
229 *mail.MemProvisioner
230 res mail.BoundarySweep
231 err error
232 calls *int
233}
234
235func (f fakeBoundarySweeper) SweepUserBoundaries(context.Context) (mail.BoundarySweep, error) {
236 if f.calls != nil {
237 *f.calls++
238 }
239 return f.res, f.err
240}
241
242// TestSweepMailBoundariesReports: the operator's last line says either
243// that every mail user is bounded or which are not; a failure is never
244// reported as success.
245func TestSweepMailBoundariesReports(t *testing.T) {
246 run := func(p mail.Provisioner) []string {
247 var lines []string
248 sweepMailBoundaries(context.Background(), false, p, func(f string, a ...any) { lines = append(lines, fmt.Sprintf(f, a...)) })
249 return lines
250 }
251 last := func(lines []string) string { return lines[len(lines)-1] }
252 mem := mail.NewMemProvisioner("1")
253
254 ok := run(fakeBoundarySweeper{MemProvisioner: mem, res: mail.BoundarySweep{Total: 3, Bounded: 1, Fixed: []string{"carlos-mail-a-x", "carlos-mail-a-y"}}})
255 if len(ok) != 3 || !strings.Contains(ok[0], "carlos-mail-a-x") || last(ok) != "console: mail boundary sweep: all 3 mail users bounded (1 already, 2 fixed)" {
256 t.Fatalf("success lines = %q", ok)
257 }
258 bad := run(fakeBoundarySweeper{MemProvisioner: mem, res: mail.BoundarySweep{Total: 2, Bounded: 1,
259 Failed: []mail.BoundaryFailure{{User: "carlos-mail-a-z", Err: errors.New("AccessDenied")}}}})
260 if !strings.Contains(last(bad), "1 of 2 mail users NOT bounded: carlos-mail-a-z") {
261 t.Fatalf("failure lines = %q", bad)
262 }
263 stopped := run(fakeBoundarySweeper{MemProvisioner: mem, res: mail.BoundarySweep{Total: 5}, err: errors.New("list failed")})
264 if !strings.Contains(last(stopped), "stopped after 5 mail users") || strings.Contains(strings.Join(stopped, "\n"), "all ") {
265 t.Fatalf("stopped lines = %q", stopped)
266 }
267 none := run(mem)
268 if len(none) != 1 || !strings.Contains(none[0], "skipped") {
269 t.Fatalf("no sweeper lines = %q", none)
270 }
271 if run(nil)[0] != none[0] {
272 t.Fatal("a nil provisioner must skip the same way")
273 }
274}
275
276// TestSweepMailBoundariesRefusesUnderHostClaims: in claim mode the startup
277// sweep must not touch IAM at all (the raw provisioner bypasses the claim
278// mail gate and app permits), and must say why.
279func TestSweepMailBoundariesRefusesUnderHostClaims(t *testing.T) {
280 calls := 0
281 mem := mail.NewMemProvisioner("1")
282 p := fakeBoundarySweeper{MemProvisioner: mem, calls: &calls, res: mail.BoundarySweep{Total: 1, Fixed: []string{"carlos-mail-a-x"}}}
283 var lines []string
284 sweepMailBoundaries(context.Background(), true, p, func(f string, a ...any) { lines = append(lines, fmt.Sprintf(f, a...)) })
285 if calls != 0 {
286 t.Fatalf("the sweep made %d backend calls under host claims, want 0", calls)
287 }
288 if len(lines) != 1 || !strings.Contains(lines[0], "refused while CARLOS_HOST_CLAIM is on") || !strings.Contains(lines[0], mail.ErrClaimMailPending.Error()) {
289 t.Fatalf("lines = %q, want one refusal naming CARLOS_HOST_CLAIM", lines)
290 }
291}
292