package console import ( "context" "encoding/json" "errors" "fmt" "html" "io" "net/http" "strings" "testing" "github.com/carlosframework/platform/internal/geo" "github.com/carlosframework/platform/internal/provision" ) func regionsURL(fx *consoleFixture) string { return fx.ts.URL + "/api/cli/apps/bab/titogo/regions" } func guessURL(fx *consoleFixture) string { return regionsURL(fx) + "/guess" } // corruptManifest replaces the manifest with unparsable bytes, under its // current etag (Put with "" is create-only and would not replace it). func corruptManifest(t *testing.T, fx *consoleFixture) { t.Helper() key := provision.ManifestKey("bab", "titogo") _, etag, err := fx.rel.Get(context.Background(), key) if err != nil { t.Fatal(err) } if _, err := fx.rel.Put(context.Background(), key, []byte("{not json"), etag); err != nil { t.Fatal(err) } } func TestRegionsKeepsLegacyFieldsAndAddsAppDefault(t *testing.T) { fx, token, _ := fillFixture(t, "uk") status, _, out := rawReq(t, http.MethodGet, regionsURL(fx), token, "") var res struct { Default string `json:"default"` AppDefault appDefaultView `json:"app_default"` } if err := json.Unmarshal([]byte(out), &res); err != nil || status != http.StatusOK || res.Default != "ie" { t.Fatalf("legacy default = %d %s", status, out) } if d := res.AppDefault; d.Region != "uk" || d.Effective != "uk" || d.Available == nil || !*d.Available || d.Name != ukPlace { t.Fatalf("app_default = %+v", d) } if !strings.Contains(out, `"aws_region":"eu-west-2"`) || !strings.Contains(out, ukPlace) { t.Fatalf("entries lack names: %s", out) } } // Every reason §9.3 names, each from its own condition (codex plan-R1 12). func TestAppDefaultReasons(t *testing.T) { acct := &Account{ID: OpsAccountID, Sqid: "bab"} cases := []struct { name string setup func(t *testing.T, fx *consoleFixture) reason string available *bool effective string }{ {"available", func(*testing.T, *consoleFixture) {}, "", bp(true), "uk"}, {"absent", func(t *testing.T, fx *consoleFixture) { retireBox(t, fx, "uk-box") }, "absent", bp(false), "uk"}, {"not-hosting", func(t *testing.T, fx *consoleFixture) { 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})}) }, "not-hosting", bp(false), "uk"}, {"no-secrets-key", func(t *testing.T, fx *consoleFixture) { setEdges(t, fx, map[string]string{"uk-box": ukNoKey}) }, "no-secrets-key", bp(false), "uk"}, {"not-eligible", func(t *testing.T, fx *consoleFixture) { 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})}) }, "not-eligible", bp(false), "uk"}, {"not-allowed", func(t *testing.T, fx *consoleFixture) { m := titoManifest() m.Pool, m.Regions = "", []string{"au"} enableInstances(t, fx, "titogo", m) }, "not-allowed", bp(false), "uk"}, {"pooled", func(t *testing.T, fx *consoleFixture) { m := titoManifest() m.Pool, m.Regions = "tito", nil enableInstances(t, fx, "titogo", m) }, "pooled", bp(true), "ie"}, {"fleet", func(t *testing.T, fx *consoleFixture) { fx.st.SetAppPlacement(OpsAccountID, "titogo", "fleet/acme") }, "fleet", nil, "fleet/acme"}, {"policy-unreadable", corruptManifest, "policy-unreadable", nil, "uk"}, {"edges-unreadable", func(t *testing.T, fx *consoleFixture) { fx.srv.cfg.Releases = failingList{fx.rel} }, "edges-unreadable", nil, "uk"}, } for _, c := range cases { fx, _, _ := fillFixture(t, "uk") c.setup(t, fx) d := fx.srv.appDefault(t.Context(), acct, "titogo") okAvail := (c.available == nil && d.Available == nil) || (c.available != nil && d.Available != nil && *c.available == *d.Available) if d.Reason != c.reason || !okAvail || d.Effective != c.effective || d.Region != "uk" { t.Errorf("%s: %+v, want reason %q effective %q available %v", c.name, d, c.reason, c.effective, c.available) } } } // codex plan-R2 3: a fact the console could not read is never presented as // known: no home-valued region, no non-fleet effective. func TestAppDefaultKnownFlags(t *testing.T) { acct := &Account{ID: OpsAccountID, Sqid: "bab"} fx, _, _ := fillFixture(t, "uk") if d := fx.srv.appDefault(t.Context(), acct, "titogo"); !d.RegionKnown || !d.PlacementKnown || d.Region != "uk" { t.Fatalf("readable = %+v", d) } fx.srv.appRegionOf = func(int64, string) (string, string, error) { return "", "", errors.New("db gone") } if d := fx.srv.appDefault(t.Context(), acct, "titogo"); d.RegionKnown || !d.PlacementKnown || d.Region != "" || d.Effective != "" || d.Available != nil { t.Fatalf("unreadable region = %+v, want region unknown and nothing presented as fact", d) } fx2, _, _ := fillFixture(t, "uk") corruptManifest(t, fx2) if d := fx2.srv.appDefault(t.Context(), acct, "titogo"); !d.RegionKnown || d.PlacementKnown || d.Reason != "policy-unreadable" { t.Fatalf("unreadable policy = %+v, want placement unknown", d) } } // codex plan-R3 2: a pooled default is available only when the create door // would take it. A no-region pooled create lands on the home, so the home // must pass the manifest's allow-list. func TestPooledAvailabilityMatchesTheCreateDoor(t *testing.T) { acct := &Account{ID: OpsAccountID, Sqid: "bab"} for _, c := range []struct { name string regions []string avail bool }{ {"no allow-list", nil, true}, {"allow-list without the home", []string{"uk"}, false}, } { fx, token, u := fillFixture(t, "uk") m := titoManifest() m.Pool, m.Regions = "tito", c.regions enableInstances(t, fx, "titogo", m) d := fx.srv.appDefault(t.Context(), acct, "titogo") if d.Reason != "pooled" || d.Effective != "ie" || d.Available == nil || *d.Available != c.avail { t.Fatalf("%s: app_default = %+v, want pooled, ie, available %v", c.name, d, c.avail) } status, _, out := create(t, u, token, `{"host":"p.go.tito.io"}`) if created := status == http.StatusCreated; created != c.avail { t.Fatalf("%s: create = %d %s, but app_default said available %v", c.name, status, out, c.avail) } } } // codex plan-R3 1: instance_domain is the domain the instances-manifest PUT // stamps on hosts, so the CLI can name a canary's host before any write. func TestAppDefaultInstanceDomain(t *testing.T) { acct := &Account{ID: OpsAccountID, Sqid: "bab"} fx, _, _ := fillFixture(t, "uk") if d := fx.srv.appDefault(t.Context(), acct, "titogo"); d.InstanceDomain != "go.tito.io" { t.Fatalf("with a manifest = %q, want the manifest's go.tito.io", d.InstanceDomain) } corruptManifest(t, fx) if d := fx.srv.appDefault(t.Context(), acct, "titogo"); d.InstanceDomain != "" { t.Fatalf("unreadable manifest = %q, want none", d.InstanceDomain) } fx2, token := instanceFixture(t, "code-inst-domain") fx2.srv.cfg.AppsDomain = "" if d := fx2.srv.appDefault(t.Context(), acct, "titogo"); d.InstanceDomain != "" { t.Fatalf("no apps domain = %q, want none (the PUT refuses)", d.InstanceDomain) } fx2.srv.cfg.AppsDomain = "oncarlos.test" before := fx2.srv.appDefault(t.Context(), acct, "titogo").InstanceDomain status, _, out := rawReq(t, http.MethodPut, fx2.ts.URL+"/api/cli/apps/bab/titogo/instances-manifest", token, `{}`) var m struct { Domain string `json:"domain"` } if err := json.Unmarshal([]byte(out), &m); err != nil || status != http.StatusOK { t.Fatalf("enable = %d %s", status, out) } if before == "" || before != m.Domain { t.Fatalf("before the PUT instance_domain = %q, the PUT stamped %q", before, m.Domain) } } // codex plan-R4 1, spec R9a: an unread stored region leaves region, name, // source and effective empty and available omitted, even when a fleet or a // pooled manifest would otherwise decide them. The placement the console // did read stays visible in reason. func TestAppDefaultUnreadRegionHidesEffective(t *testing.T) { acct := &Account{ID: OpsAccountID, Sqid: "bab"} for _, c := range []struct { name string setup func(t *testing.T, fx *consoleFixture) reason string }{ {"pooled", func(t *testing.T, fx *consoleFixture) { m := titoManifest() m.Pool, m.Regions = "tito", nil enableInstances(t, fx, "titogo", m) }, "pooled"}, {"fleet", func(t *testing.T, fx *consoleFixture) { if err := fx.st.SetAppPlacement(OpsAccountID, "titogo", "fleet/acme"); err != nil { t.Fatal(err) } }, "fleet"}, } { fx, _, _ := fillFixture(t, "uk") c.setup(t, fx) fx.srv.appRegionOf = func(int64, string) (string, string, error) { return "", "", errors.New("db gone") } d := fx.srv.appDefault(t.Context(), acct, "titogo") if d.RegionKnown || !d.PlacementKnown || d.Region != "" || d.Name != "" || d.Source != "" || d.Effective != "" || d.Available != nil || d.Reason != c.reason { t.Errorf("%s with an unread region = %+v, want only placement_known and reason %q", c.name, d, c.reason) } } } func TestRegionGuessEndpoint(t *testing.T) { fx, token, _ := fillFixture(t, "") status, _, out := rawReq(t, http.MethodPost, guessURL(fx), token, `{"tz":"Europe/London"}`) if status != http.StatusOK || !strings.Contains(out, `"code":"uk"`) || !strings.Contains(out, `"source":"timezone"`) { t.Fatalf("guess = %d %s", status, out) } // Review Focus 4: garbage hints are absent, never an error. status, _, out = rawReq(t, http.MethodPost, guessURL(fx), token, `{"ip":"not-an-ip","tz":"`+strings.Repeat("a", 10<<10)+`"}`) if status != http.StatusOK || !strings.Contains(out, `"source":"default"`) { t.Fatalf("garbage = %d %s", status, out) } status, _, out = rawReq(t, http.MethodPost, guessURL(fx), token, `{not json`) if status != http.StatusOK || !strings.Contains(out, `"source":"default"`) { t.Fatalf("malformed body = %d %s", status, out) } fx.st.SetAppPlacement(OpsAccountID, "titogo", "fleet/acme") if _, _, out := rawReq(t, http.MethodPost, guessURL(fx), token, `{"tz":"Europe/London"}`); !strings.Contains(out, `"guess":null`) { t.Fatalf("fleet guess = %s (codex R4-1)", out) } } func TestRegionGuessPooledNullAndUnreadable502(t *testing.T) { fx, token, _ := fillFixture(t, "") m := titoManifest() m.Pool = "tito" enableInstances(t, fx, "titogo", m) if _, _, out := rawReq(t, http.MethodPost, guessURL(fx), token, `{"tz":"Europe/London"}`); !strings.Contains(out, `"guess":null`) { t.Fatalf("pooled guess = %s", out) } corruptManifest(t, fx) if status, _, out := rawReq(t, http.MethodPost, guessURL(fx), token, `{"tz":"Europe/London"}`); status != http.StatusBadGateway || !strings.Contains(out, msgPolicyUnreadable) { t.Fatalf("unreadable manifest = %d %s, want 502 C27", status, out) } fx2, token2, _ := fillFixture(t, "") fx2.srv.cfg.Registry = t.TempDir() // a directory: the registry will not open if status, _, out := rawReq(t, http.MethodPost, guessURL(fx2), token2, `{"tz":"Europe/London"}`); status != http.StatusBadGateway || !strings.Contains(out, msgPolicyUnreadable) { t.Fatalf("unreadable placement = %d %s, want 502 C27", status, out) } } // codex 8: with a canonical-host redirect configured, a guess POST's hints // appear in no Location, no body, no log and no audit row. PF9: the endpoint // must first answer 2xx (a 4xx would leak nothing for the wrong reason), and // the no-timezone run drives the GeoIP locator path with the IP hint. func TestRegionGuessAPIHintsNeverLeak(t *testing.T) { for _, c := range []struct { name, hints string secrets []string wantSource string }{ {"timezone and ip", `{"tz":"Pacific/Chatham","ip":"203.0.113.77"}`, []string{"Chatham", "203.0.113.77"}, "timezone"}, {"ip only, no timezone", `{"ip":"203.0.113.77"}`, []string{"203.0.113.77"}, "ip"}, } { fx, token, _ := fillFixture(t, "") fx.srv.cfg.GeoIP = fakeLocator{"203.0.113.77": geo.Point{Lat: 51.5, Lon: -0.12}} fx.srv.redirects = map[string]bool{"old.example": true} var logs strings.Builder restore := captureLogTo(&logs) req, _ := http.NewRequest(http.MethodPost, guessURL(fx), strings.NewReader(c.hints)) req.Host = "old.example" req.Header.Set("Authorization", "Bearer "+token) res, err := (&http.Client{CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}).Do(req) if err != nil { restore() t.Fatal(err) } body, _ := io.ReadAll(res.Body) res.Body.Close() everything := res.Header.Get("Location") + string(body) status, _, out := rawReq(t, http.MethodPost, guessURL(fx), token, c.hints) restore() if status/100 != 2 || !strings.Contains(out, `"source":"`+c.wantSource+`"`) { t.Fatalf("%s: guess = %d %s, want 2xx from source %q", c.name, status, out, c.wantSource) } everything += out + logs.String() events, err := fx.st.Events(OpsAccountID, "titogo", 50) if err != nil { t.Fatal(err) } for _, e := range events { everything += e.Detail + e.Message } for _, secret := range c.secrets { if strings.Contains(everything, secret) { t.Fatalf("%s: hint %q leaked: %s", c.name, secret, everything) } } } } // PF7: when ONLY the stored region is unread, reason stays empty. // policy-unreadable is reserved for placement_known being false (spec §9.3). func TestAppDefaultUnreadRegionAloneLeavesReasonEmpty(t *testing.T) { acct := &Account{ID: OpsAccountID, Sqid: "bab"} fx, _, _ := fillFixture(t, "uk") fx.srv.appRegionOf = func(int64, string) (string, string, error) { return "", "", errors.New("db gone") } d := fx.srv.appDefault(t.Context(), acct, "titogo") if d.RegionKnown || !d.PlacementKnown || d.Reason != "" || d.Effective != "" || d.Available != nil || d.Region != "" { t.Fatalf("unread region alone = %+v, want empty reason, effective and available", d) } } // PF10: the legacy fields older clients and titogo read are unchanged for an // app with no stored region. The base answer was // // {"default":"ie","regions":[{"code":"ie","armed":true},{"code":"uk","armed":true}]} // // Entries gain aws_region and name (the brief's additive fields), so each // entry is compared on the two keys it had, in the order it had them. func TestRegionsLegacyFieldsGolden(t *testing.T) { fx, token, _ := fillFixture(t, "") status, _, out := rawReq(t, http.MethodGet, regionsURL(fx), token, "") var res struct { Default json.RawMessage `json:"default"` Regions []json.RawMessage `json:"regions"` } if err := json.Unmarshal([]byte(out), &res); err != nil || status != http.StatusOK { t.Fatalf("regions = %d %s", status, out) } var legacy []struct { Code string `json:"code"` Armed bool `json:"armed"` } for _, raw := range res.Regions { var e struct { Code string `json:"code"` Armed bool `json:"armed"` } if err := json.Unmarshal(raw, &e); err != nil { t.Fatal(err) } legacy = append(legacy, e) } got, _ := json.Marshal(legacy) if string(got) != `[{"code":"ie","armed":true},{"code":"uk","armed":true}]` || string(res.Default) != `"ie"` { t.Fatalf("legacy fields drifted: regions %s default %s", got, res.Default) } for _, raw := range res.Regions { if !strings.HasPrefix(string(raw), `{"code":"`) || !strings.Contains(string(raw), `","armed":true`) { t.Fatalf("entry %s lost its legacy key order", raw) } } } // C7 only when the default is CONFIRMED available; C8 whenever it is // confirmed unavailable; nothing for fleet, pooled, or a default that // could not be read (codex plan-R1 14). Assertions compare the HTML-escaped // copy, both ways (codex plan-R1 8). func TestInstancesTabRegionLines(t *testing.T) { starts := html.EscapeString(fmt.Sprintf(regionCopy.StartsIn, ukPlace)) cantUK := html.EscapeString(fmt.Sprintf(regionCopy.CantStart, "uk")) page := func(fx *consoleFixture) string { _, _, body := get(t, signInPaul(t, fx), fx.u("/apps/titogo/instances")) return body } anyLine := func(body string) bool { return strings.Contains(body, `id="app-region-line"`) } fx, _, _ := fillFixture(t, "uk") fx.srv.cfg.AppRegions = true body := page(fx) if !strings.Contains(body, starts) { t.Fatal("no C7 line for an available region") } if strings.Contains(body, `id="app-region-line" class="banner error"`) { t.Error("C7 carries the warning class") } retireBox(t, fx, "uk-box") // No box publishes uk, so its place falls back to the bare code (§14). body = page(fx) if !strings.Contains(body, cantUK) || strings.Contains(body, starts) { t.Fatal("want C8, not C7, when the region is absent (codex 7)") } if !strings.Contains(body, `id="app-region-line" class="banner error"`) { t.Error("C8 lacks the warning class") } for _, setup := range []func(*consoleFixture){ func(fx *consoleFixture) { fx.st.SetAppPlacement(OpsAccountID, "titogo", "fleet/acme") }, func(fx *consoleFixture) { m := titoManifest() m.Pool = "tito" enableInstances(t, fx, "titogo", m) }, func(fx *consoleFixture) { corruptManifest(t, fx) }, func(fx *consoleFixture) { fx.srv.cfg.Releases = failingList{fx.rel} }, // the stored region could not be read: never claimed available. func(fx *consoleFixture) { fx.srv.appRegionOf = func(int64, string) (string, string, error) { return "", "", errors.New("db gone") } }, } { fx, _, _ := fillFixture(t, "uk") setup(fx) if anyLine(page(fx)) { t.Error("a fleet, pooled or unreadable default showed a region line") } } } // PF9, PF10: an app whose region is the home (stored "", the record // vocabulary's home) gets C7 only when the home has a sibling to be told // apart from (two eligible regions); alone it says nothing. A home the // allow-list leaves out is still C8, whatever the sibling count. func TestInstancesTabRegionLineForAHomeApp(t *testing.T) { homePlace := geo.PlaceName("eu-west-1", "ie") starts := html.EscapeString(fmt.Sprintf(regionCopy.StartsIn, homePlace)) cantHome := html.EscapeString(fmt.Sprintf(regionCopy.CantStart, homePlace)) page := func(fx *consoleFixture) string { _, _, body := get(t, signInPaul(t, fx), fx.u("/apps/titogo/instances")) return body } fx, _, _ := fillFixture(t, "") if body := page(fx); !strings.Contains(body, starts) { t.Errorf("two eligible regions: want C7 naming the home %q", homePlace) } // Only the home is left: nothing to distinguish it from. fx, _, _ = fillFixture(t, "") retireBox(t, fx, "uk-box") if body := page(fx); strings.Contains(body, `id="app-region-line"`) { t.Error("one eligible region: a region line showed") } fx, _, _ = fillFixture(t, "") m := titoManifest() m.Pool, m.Regions = "", []string{"uk"} enableInstances(t, fx, "titogo", m) body := page(fx) if !strings.Contains(body, cantHome) || strings.Contains(body, starts) || !strings.Contains(body, `id="app-region-line" class="banner error"`) { t.Error("home left out of the allow-list: want C8 only") } }