carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "html"
9 "io"
10 "net/http"
11 "strings"
12 "testing"
13
14 "github.com/carlosframework/platform/internal/geo"
15 "github.com/carlosframework/platform/internal/provision"
16)
17
18func regionsURL(fx *consoleFixture) string { return fx.ts.URL + "/api/cli/apps/bab/titogo/regions" }
19func guessURL(fx *consoleFixture) string { return regionsURL(fx) + "/guess" }
20
21// corruptManifest replaces the manifest with unparsable bytes, under its
22// current etag (Put with "" is create-only and would not replace it).
23func corruptManifest(t *testing.T, fx *consoleFixture) {
24 t.Helper()
25 key := provision.ManifestKey("bab", "titogo")
26 _, etag, err := fx.rel.Get(context.Background(), key)
27 if err != nil {
28 t.Fatal(err)
29 }
30 if _, err := fx.rel.Put(context.Background(), key, []byte("{not json"), etag); err != nil {
31 t.Fatal(err)
32 }
33}
34
35func TestRegionsKeepsLegacyFieldsAndAddsAppDefault(t *testing.T) {
36 fx, token, _ := fillFixture(t, "uk")
37 status, _, out := rawReq(t, http.MethodGet, regionsURL(fx), token, "")
38 var res struct {
39 Default string `json:"default"`
40 AppDefault appDefaultView `json:"app_default"`
41 }
42 if err := json.Unmarshal([]byte(out), &res); err != nil || status != http.StatusOK || res.Default != "ie" {
43 t.Fatalf("legacy default = %d %s", status, out)
44 }
45 if d := res.AppDefault; d.Region != "uk" || d.Effective != "uk" || d.Available == nil || !*d.Available || d.Name != ukPlace {
46 t.Fatalf("app_default = %+v", d)
47 }
48 if !strings.Contains(out, `"aws_region":"eu-west-2"`) || !strings.Contains(out, ukPlace) {
49 t.Fatalf("entries lack names: %s", out)
50 }
51}
52
53// Every reason §9.3 names, each from its own condition (codex plan-R1 12).
54func TestAppDefaultReasons(t *testing.T) {
55 acct := &Account{ID: OpsAccountID, Sqid: "bab"}
56 cases := []struct {
57 name string
58 setup func(t *testing.T, fx *consoleFixture)
59 reason string
60 available *bool
61 effective string
62 }{
63 {"available", func(*testing.T, *consoleFixture) {}, "", bp(true), "uk"},
64 {"absent", func(t *testing.T, fx *consoleFixture) { retireBox(t, fx, "uk-box") }, "absent", bp(false), "uk"},
65 {"not-hosting", func(t *testing.T, fx *consoleFixture) {
66 setEdges(t, fx, map[string]string{"uk-box": edgeJSON("uk-box", "1.2.3.4", "eu-west-2", "uk", edgeOpts{hib: bp(false), shared: bp(true), inbound: bp(true), fp: testKeyFP})})
67 }, "not-hosting", bp(false), "uk"},
68 {"no-secrets-key", func(t *testing.T, fx *consoleFixture) { setEdges(t, fx, map[string]string{"uk-box": ukNoKey}) }, "no-secrets-key", bp(false), "uk"},
69 {"not-eligible", func(t *testing.T, fx *consoleFixture) {
70 setEdges(t, fx, map[string]string{"uk-box": edgeJSON("uk-box", "1.2.3.4", "eu-west-2", "uk", edgeOpts{hib: bp(true), shared: bp(false), inbound: bp(true), fp: testKeyFP})})
71 }, "not-eligible", bp(false), "uk"},
72 {"not-allowed", func(t *testing.T, fx *consoleFixture) {
73 m := titoManifest()
74 m.Pool, m.Regions = "", []string{"au"}
75 enableInstances(t, fx, "titogo", m)
76 }, "not-allowed", bp(false), "uk"},
77 {"pooled", func(t *testing.T, fx *consoleFixture) {
78 m := titoManifest()
79 m.Pool, m.Regions = "tito", nil
80 enableInstances(t, fx, "titogo", m)
81 }, "pooled", bp(true), "ie"},
82 {"fleet", func(t *testing.T, fx *consoleFixture) { fx.st.SetAppPlacement(OpsAccountID, "titogo", "fleet/acme") }, "fleet", nil, "fleet/acme"},
83 {"policy-unreadable", corruptManifest, "policy-unreadable", nil, "uk"},
84 {"edges-unreadable", func(t *testing.T, fx *consoleFixture) { fx.srv.cfg.Releases = failingList{fx.rel} }, "edges-unreadable", nil, "uk"},
85 }
86 for _, c := range cases {
87 fx, _, _ := fillFixture(t, "uk")
88 c.setup(t, fx)
89 d := fx.srv.appDefault(t.Context(), acct, "titogo")
90 okAvail := (c.available == nil && d.Available == nil) || (c.available != nil && d.Available != nil && *c.available == *d.Available)
91 if d.Reason != c.reason || !okAvail || d.Effective != c.effective || d.Region != "uk" {
92 t.Errorf("%s: %+v, want reason %q effective %q available %v", c.name, d, c.reason, c.effective, c.available)
93 }
94 }
95}
96
97// codex plan-R2 3: a fact the console could not read is never presented as
98// known: no home-valued region, no non-fleet effective.
99func TestAppDefaultKnownFlags(t *testing.T) {
100 acct := &Account{ID: OpsAccountID, Sqid: "bab"}
101 fx, _, _ := fillFixture(t, "uk")
102 if d := fx.srv.appDefault(t.Context(), acct, "titogo"); !d.RegionKnown || !d.PlacementKnown || d.Region != "uk" {
103 t.Fatalf("readable = %+v", d)
104 }
105 fx.srv.appRegionOf = func(int64, string) (string, string, error) { return "", "", errors.New("db gone") }
106 if d := fx.srv.appDefault(t.Context(), acct, "titogo"); d.RegionKnown || !d.PlacementKnown || d.Region != "" || d.Effective != "" || d.Available != nil {
107 t.Fatalf("unreadable region = %+v, want region unknown and nothing presented as fact", d)
108 }
109 fx2, _, _ := fillFixture(t, "uk")
110 corruptManifest(t, fx2)
111 if d := fx2.srv.appDefault(t.Context(), acct, "titogo"); !d.RegionKnown || d.PlacementKnown || d.Reason != "policy-unreadable" {
112 t.Fatalf("unreadable policy = %+v, want placement unknown", d)
113 }
114}
115
116// codex plan-R3 2: a pooled default is available only when the create door
117// would take it. A no-region pooled create lands on the home, so the home
118// must pass the manifest's allow-list.
119func TestPooledAvailabilityMatchesTheCreateDoor(t *testing.T) {
120 acct := &Account{ID: OpsAccountID, Sqid: "bab"}
121 for _, c := range []struct {
122 name string
123 regions []string
124 avail bool
125 }{
126 {"no allow-list", nil, true},
127 {"allow-list without the home", []string{"uk"}, false},
128 } {
129 fx, token, u := fillFixture(t, "uk")
130 m := titoManifest()
131 m.Pool, m.Regions = "tito", c.regions
132 enableInstances(t, fx, "titogo", m)
133 d := fx.srv.appDefault(t.Context(), acct, "titogo")
134 if d.Reason != "pooled" || d.Effective != "ie" || d.Available == nil || *d.Available != c.avail {
135 t.Fatalf("%s: app_default = %+v, want pooled, ie, available %v", c.name, d, c.avail)
136 }
137 status, _, out := create(t, u, token, `{"host":"p.go.tito.io"}`)
138 if created := status == http.StatusCreated; created != c.avail {
139 t.Fatalf("%s: create = %d %s, but app_default said available %v", c.name, status, out, c.avail)
140 }
141 }
142}
143
144// codex plan-R3 1: instance_domain is the domain the instances-manifest PUT
145// stamps on hosts, so the CLI can name a canary's host before any write.
146func TestAppDefaultInstanceDomain(t *testing.T) {
147 acct := &Account{ID: OpsAccountID, Sqid: "bab"}
148 fx, _, _ := fillFixture(t, "uk")
149 if d := fx.srv.appDefault(t.Context(), acct, "titogo"); d.InstanceDomain != "go.tito.io" {
150 t.Fatalf("with a manifest = %q, want the manifest's go.tito.io", d.InstanceDomain)
151 }
152 corruptManifest(t, fx)
153 if d := fx.srv.appDefault(t.Context(), acct, "titogo"); d.InstanceDomain != "" {
154 t.Fatalf("unreadable manifest = %q, want none", d.InstanceDomain)
155 }
156 fx2, token := instanceFixture(t, "code-inst-domain")
157 fx2.srv.cfg.AppsDomain = ""
158 if d := fx2.srv.appDefault(t.Context(), acct, "titogo"); d.InstanceDomain != "" {
159 t.Fatalf("no apps domain = %q, want none (the PUT refuses)", d.InstanceDomain)
160 }
161 fx2.srv.cfg.AppsDomain = "oncarlos.test"
162 before := fx2.srv.appDefault(t.Context(), acct, "titogo").InstanceDomain
163 status, _, out := rawReq(t, http.MethodPut, fx2.ts.URL+"/api/cli/apps/bab/titogo/instances-manifest", token, `{}`)
164 var m struct {
165 Domain string `json:"domain"`
166 }
167 if err := json.Unmarshal([]byte(out), &m); err != nil || status != http.StatusOK {
168 t.Fatalf("enable = %d %s", status, out)
169 }
170 if before == "" || before != m.Domain {
171 t.Fatalf("before the PUT instance_domain = %q, the PUT stamped %q", before, m.Domain)
172 }
173}
174
175// codex plan-R4 1, spec R9a: an unread stored region leaves region, name,
176// source and effective empty and available omitted, even when a fleet or a
177// pooled manifest would otherwise decide them. The placement the console
178// did read stays visible in reason.
179func TestAppDefaultUnreadRegionHidesEffective(t *testing.T) {
180 acct := &Account{ID: OpsAccountID, Sqid: "bab"}
181 for _, c := range []struct {
182 name string
183 setup func(t *testing.T, fx *consoleFixture)
184 reason string
185 }{
186 {"pooled", func(t *testing.T, fx *consoleFixture) {
187 m := titoManifest()
188 m.Pool, m.Regions = "tito", nil
189 enableInstances(t, fx, "titogo", m)
190 }, "pooled"},
191 {"fleet", func(t *testing.T, fx *consoleFixture) {
192 if err := fx.st.SetAppPlacement(OpsAccountID, "titogo", "fleet/acme"); err != nil {
193 t.Fatal(err)
194 }
195 }, "fleet"},
196 } {
197 fx, _, _ := fillFixture(t, "uk")
198 c.setup(t, fx)
199 fx.srv.appRegionOf = func(int64, string) (string, string, error) { return "", "", errors.New("db gone") }
200 d := fx.srv.appDefault(t.Context(), acct, "titogo")
201 if d.RegionKnown || !d.PlacementKnown || d.Region != "" || d.Name != "" || d.Source != "" ||
202 d.Effective != "" || d.Available != nil || d.Reason != c.reason {
203 t.Errorf("%s with an unread region = %+v, want only placement_known and reason %q", c.name, d, c.reason)
204 }
205 }
206}
207
208func TestRegionGuessEndpoint(t *testing.T) {
209 fx, token, _ := fillFixture(t, "")
210 status, _, out := rawReq(t, http.MethodPost, guessURL(fx), token, `{"tz":"Europe/London"}`)
211 if status != http.StatusOK || !strings.Contains(out, `"code":"uk"`) || !strings.Contains(out, `"source":"timezone"`) {
212 t.Fatalf("guess = %d %s", status, out)
213 }
214 // Review Focus 4: garbage hints are absent, never an error.
215 status, _, out = rawReq(t, http.MethodPost, guessURL(fx), token, `{"ip":"not-an-ip","tz":"`+strings.Repeat("a", 10<<10)+`"}`)
216 if status != http.StatusOK || !strings.Contains(out, `"source":"default"`) {
217 t.Fatalf("garbage = %d %s", status, out)
218 }
219 status, _, out = rawReq(t, http.MethodPost, guessURL(fx), token, `{not json`)
220 if status != http.StatusOK || !strings.Contains(out, `"source":"default"`) {
221 t.Fatalf("malformed body = %d %s", status, out)
222 }
223 fx.st.SetAppPlacement(OpsAccountID, "titogo", "fleet/acme")
224 if _, _, out := rawReq(t, http.MethodPost, guessURL(fx), token, `{"tz":"Europe/London"}`); !strings.Contains(out, `"guess":null`) {
225 t.Fatalf("fleet guess = %s (codex R4-1)", out)
226 }
227}
228
229func TestRegionGuessPooledNullAndUnreadable502(t *testing.T) {
230 fx, token, _ := fillFixture(t, "")
231 m := titoManifest()
232 m.Pool = "tito"
233 enableInstances(t, fx, "titogo", m)
234 if _, _, out := rawReq(t, http.MethodPost, guessURL(fx), token, `{"tz":"Europe/London"}`); !strings.Contains(out, `"guess":null`) {
235 t.Fatalf("pooled guess = %s", out)
236 }
237 corruptManifest(t, fx)
238 if status, _, out := rawReq(t, http.MethodPost, guessURL(fx), token, `{"tz":"Europe/London"}`); status != http.StatusBadGateway || !strings.Contains(out, msgPolicyUnreadable) {
239 t.Fatalf("unreadable manifest = %d %s, want 502 C27", status, out)
240 }
241 fx2, token2, _ := fillFixture(t, "")
242 fx2.srv.cfg.Registry = t.TempDir() // a directory: the registry will not open
243 if status, _, out := rawReq(t, http.MethodPost, guessURL(fx2), token2, `{"tz":"Europe/London"}`); status != http.StatusBadGateway || !strings.Contains(out, msgPolicyUnreadable) {
244 t.Fatalf("unreadable placement = %d %s, want 502 C27", status, out)
245 }
246}
247
248// codex 8: with a canonical-host redirect configured, a guess POST's hints
249// appear in no Location, no body, no log and no audit row. PF9: the endpoint
250// must first answer 2xx (a 4xx would leak nothing for the wrong reason), and
251// the no-timezone run drives the GeoIP locator path with the IP hint.
252func TestRegionGuessAPIHintsNeverLeak(t *testing.T) {
253 for _, c := range []struct {
254 name, hints string
255 secrets []string
256 wantSource string
257 }{
258 {"timezone and ip", `{"tz":"Pacific/Chatham","ip":"203.0.113.77"}`, []string{"Chatham", "203.0.113.77"}, "timezone"},
259 {"ip only, no timezone", `{"ip":"203.0.113.77"}`, []string{"203.0.113.77"}, "ip"},
260 } {
261 fx, token, _ := fillFixture(t, "")
262 fx.srv.cfg.GeoIP = fakeLocator{"203.0.113.77": geo.Point{Lat: 51.5, Lon: -0.12}}
263 fx.srv.redirects = map[string]bool{"old.example": true}
264 var logs strings.Builder
265 restore := captureLogTo(&logs)
266 req, _ := http.NewRequest(http.MethodPost, guessURL(fx), strings.NewReader(c.hints))
267 req.Host = "old.example"
268 req.Header.Set("Authorization", "Bearer "+token)
269 res, err := (&http.Client{CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}).Do(req)
270 if err != nil {
271 restore()
272 t.Fatal(err)
273 }
274 body, _ := io.ReadAll(res.Body)
275 res.Body.Close()
276 everything := res.Header.Get("Location") + string(body)
277 status, _, out := rawReq(t, http.MethodPost, guessURL(fx), token, c.hints)
278 restore()
279 if status/100 != 2 || !strings.Contains(out, `"source":"`+c.wantSource+`"`) {
280 t.Fatalf("%s: guess = %d %s, want 2xx from source %q", c.name, status, out, c.wantSource)
281 }
282 everything += out + logs.String()
283 events, err := fx.st.Events(OpsAccountID, "titogo", 50)
284 if err != nil {
285 t.Fatal(err)
286 }
287 for _, e := range events {
288 everything += e.Detail + e.Message
289 }
290 for _, secret := range c.secrets {
291 if strings.Contains(everything, secret) {
292 t.Fatalf("%s: hint %q leaked: %s", c.name, secret, everything)
293 }
294 }
295 }
296}
297
298// PF7: when ONLY the stored region is unread, reason stays empty.
299// policy-unreadable is reserved for placement_known being false (spec §9.3).
300func TestAppDefaultUnreadRegionAloneLeavesReasonEmpty(t *testing.T) {
301 acct := &Account{ID: OpsAccountID, Sqid: "bab"}
302 fx, _, _ := fillFixture(t, "uk")
303 fx.srv.appRegionOf = func(int64, string) (string, string, error) { return "", "", errors.New("db gone") }
304 d := fx.srv.appDefault(t.Context(), acct, "titogo")
305 if d.RegionKnown || !d.PlacementKnown || d.Reason != "" || d.Effective != "" || d.Available != nil || d.Region != "" {
306 t.Fatalf("unread region alone = %+v, want empty reason, effective and available", d)
307 }
308}
309
310// PF10: the legacy fields older clients and titogo read are unchanged for an
311// app with no stored region. The base answer was
312//
313// {"default":"ie","regions":[{"code":"ie","armed":true},{"code":"uk","armed":true}]}
314//
315// Entries gain aws_region and name (the brief's additive fields), so each
316// entry is compared on the two keys it had, in the order it had them.
317func TestRegionsLegacyFieldsGolden(t *testing.T) {
318 fx, token, _ := fillFixture(t, "")
319 status, _, out := rawReq(t, http.MethodGet, regionsURL(fx), token, "")
320 var res struct {
321 Default json.RawMessage `json:"default"`
322 Regions []json.RawMessage `json:"regions"`
323 }
324 if err := json.Unmarshal([]byte(out), &res); err != nil || status != http.StatusOK {
325 t.Fatalf("regions = %d %s", status, out)
326 }
327 var legacy []struct {
328 Code string `json:"code"`
329 Armed bool `json:"armed"`
330 }
331 for _, raw := range res.Regions {
332 var e struct {
333 Code string `json:"code"`
334 Armed bool `json:"armed"`
335 }
336 if err := json.Unmarshal(raw, &e); err != nil {
337 t.Fatal(err)
338 }
339 legacy = append(legacy, e)
340 }
341 got, _ := json.Marshal(legacy)
342 if string(got) != `[{"code":"ie","armed":true},{"code":"uk","armed":true}]` || string(res.Default) != `"ie"` {
343 t.Fatalf("legacy fields drifted: regions %s default %s", got, res.Default)
344 }
345 for _, raw := range res.Regions {
346 if !strings.HasPrefix(string(raw), `{"code":"`) || !strings.Contains(string(raw), `","armed":true`) {
347 t.Fatalf("entry %s lost its legacy key order", raw)
348 }
349 }
350}
351
352// C7 only when the default is CONFIRMED available; C8 whenever it is
353// confirmed unavailable; nothing for fleet, pooled, or a default that
354// could not be read (codex plan-R1 14). Assertions compare the HTML-escaped
355// copy, both ways (codex plan-R1 8).
356func TestInstancesTabRegionLines(t *testing.T) {
357 starts := html.EscapeString(fmt.Sprintf(regionCopy.StartsIn, ukPlace))
358 cantUK := html.EscapeString(fmt.Sprintf(regionCopy.CantStart, "uk"))
359 page := func(fx *consoleFixture) string {
360 _, _, body := get(t, signInPaul(t, fx), fx.u("/apps/titogo/instances"))
361 return body
362 }
363 anyLine := func(body string) bool { return strings.Contains(body, `id="app-region-line"`) }
364
365 fx, _, _ := fillFixture(t, "uk")
366 fx.srv.cfg.AppRegions = true
367 body := page(fx)
368 if !strings.Contains(body, starts) {
369 t.Fatal("no C7 line for an available region")
370 }
371 if strings.Contains(body, `id="app-region-line" class="banner error"`) {
372 t.Error("C7 carries the warning class")
373 }
374 retireBox(t, fx, "uk-box")
375 // No box publishes uk, so its place falls back to the bare code (§14).
376 body = page(fx)
377 if !strings.Contains(body, cantUK) || strings.Contains(body, starts) {
378 t.Fatal("want C8, not C7, when the region is absent (codex 7)")
379 }
380 if !strings.Contains(body, `id="app-region-line" class="banner error"`) {
381 t.Error("C8 lacks the warning class")
382 }
383
384 for _, setup := range []func(*consoleFixture){
385 func(fx *consoleFixture) { fx.st.SetAppPlacement(OpsAccountID, "titogo", "fleet/acme") },
386 func(fx *consoleFixture) {
387 m := titoManifest()
388 m.Pool = "tito"
389 enableInstances(t, fx, "titogo", m)
390 },
391 func(fx *consoleFixture) { corruptManifest(t, fx) },
392 func(fx *consoleFixture) { fx.srv.cfg.Releases = failingList{fx.rel} },
393 // the stored region could not be read: never claimed available.
394 func(fx *consoleFixture) {
395 fx.srv.appRegionOf = func(int64, string) (string, string, error) { return "", "", errors.New("db gone") }
396 },
397 } {
398 fx, _, _ := fillFixture(t, "uk")
399 setup(fx)
400 if anyLine(page(fx)) {
401 t.Error("a fleet, pooled or unreadable default showed a region line")
402 }
403 }
404}
405
406// PF9, PF10: an app whose region is the home (stored "", the record
407// vocabulary's home) gets C7 only when the home has a sibling to be told
408// apart from (two eligible regions); alone it says nothing. A home the
409// allow-list leaves out is still C8, whatever the sibling count.
410func TestInstancesTabRegionLineForAHomeApp(t *testing.T) {
411 homePlace := geo.PlaceName("eu-west-1", "ie")
412 starts := html.EscapeString(fmt.Sprintf(regionCopy.StartsIn, homePlace))
413 cantHome := html.EscapeString(fmt.Sprintf(regionCopy.CantStart, homePlace))
414 page := func(fx *consoleFixture) string {
415 _, _, body := get(t, signInPaul(t, fx), fx.u("/apps/titogo/instances"))
416 return body
417 }
418
419 fx, _, _ := fillFixture(t, "")
420 if body := page(fx); !strings.Contains(body, starts) {
421 t.Errorf("two eligible regions: want C7 naming the home %q", homePlace)
422 }
423
424 // Only the home is left: nothing to distinguish it from.
425 fx, _, _ = fillFixture(t, "")
426 retireBox(t, fx, "uk-box")
427 if body := page(fx); strings.Contains(body, `id="app-region-line"`) {
428 t.Error("one eligible region: a region line showed")
429 }
430
431 fx, _, _ = fillFixture(t, "")
432 m := titoManifest()
433 m.Pool, m.Regions = "", []string{"uk"}
434 enableInstances(t, fx, "titogo", m)
435 body := page(fx)
436 if !strings.Contains(body, cantHome) || strings.Contains(body, starts) ||
437 !strings.Contains(body, `id="app-region-line" class="banner error"`) {
438 t.Error("home left out of the allow-list: want C8 only")
439 }
440}
441