package console import ( "bytes" "context" "encoding/json" "fmt" "net/http" "strings" "testing" "time" "github.com/carlosframework/platform/internal/geo" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/svcrole" ) func regionFixture(t *testing.T) (*consoleFixture, string) { t.Helper() fx := newConsoleFixture(t) fx.srv.cfg.AppRegions, fx.srv.cfg.HomeRegion, fx.srv.cfg.SecretsPub = true, "ie", testKeyPub setEdges(t, fx, map[string]string{"ie-box": homeBox, "uk-box": ukEligible}) return fx, cliToken(t, fx, "paul@keymail.dev", "code-region-claim") } func newAppURL(fx *consoleFixture) string { return fx.ts.URL + "/api/cli/apps/bab/new" } // postJSONFrom is postJSON with an X-Forwarded-For, which the console // trusts from the loopback edge (authcli.go clientIP). func postJSONFrom(t *testing.T, u string, body any, bearer, xff string) (int, map[string]any) { t.Helper() raw, _ := json.Marshal(body) req, err := http.NewRequest(http.MethodPost, u, bytes.NewReader(raw)) if err != nil { t.Fatal(err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+bearer) req.Header.Set("X-Forwarded-For", xff) return doJSON(t, req) } func errOf(res map[string]any) string { s, _ := res["error"].(string); return s } var ukPlace = geo.PlaceName("eu-west-2", "uk") func TestAPICreateAppResolvesRegion(t *testing.T) { fx, token := regionFixture(t) status, res := postJSON(t, newAppURL(fx), map[string]string{"name": "blog", "timezone": "Europe/London"}, token) if status != http.StatusOK || res["region"] != "uk" || res["region_source"] != "timezone" || res["region_name"] != ukPlace || res["regions_available"] != float64(2) { t.Fatalf("create = %d %v", status, res) } if r, src, _ := fx.st.AppRegion(OpsAccountID, "blog"); r != "uk" || src != "timezone" { t.Fatalf("stored %q %q", r, src) } // An old CLI's body, from loopback with no forwarded address. if _, res = postJSON(t, newAppURL(fx), map[string]string{"name": "old"}, token); res["region"] != "ie" || res["region_source"] != "default" { t.Fatalf("old body = %v", res) } // An explicit home name is a choice, stored as "". postJSON(t, newAppURL(fx), map[string]string{"name": "athome", "region": "ie"}, token) if r, src, _ := fx.st.AppRegion(OpsAccountID, "athome"); r != "" || src != "chosen" { t.Fatalf("explicit home = %q %q", r, src) } } // The old-body path through the real door: a person's forwarded address // is located; a service principal's never is (spec §6.2). func TestAPICreateAppLocatesOnlyAPerson(t *testing.T) { fx, token := regionFixture(t) fx.srv.cfg.GeoIP = fakeLocator{"81.2.69.142": {Lat: 51.5, Lon: -0.12}} if status, res := postJSONFrom(t, newAppURL(fx), map[string]string{"name": "person"}, token, "81.2.69.142"); status != http.StatusOK || res["region"] != "uk" || res["region_source"] != "ip" { t.Fatalf("person = %d %v", status, res) } svc := serviceToken(t, fx.st, OpsAccountID, "hub", "", svcrole.Publish) if status, res := postJSONFrom(t, newAppURL(fx), map[string]string{"name": "hubapp"}, svc, "81.2.69.142"); status != http.StatusOK || res["region"] != "ie" || res["region_source"] != "default" { t.Fatalf("service = %d %v, want no IP step", status, res) } } func TestAPICreateAppRefusesBadExplicitRegions(t *testing.T) { fx, token := regionFixture(t) // nz cannot host; au's current-writer box lacks the key (R9). auNoKey := edgeJSON("au-box", "9.9.9.8", "ap-southeast-2", "au", edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(true)}) setEdges(t, fx, map[string]string{"nz-box": nzNoHosting, "au-box": auNoKey}) // codex plan-R4 2: each list names exactly what this door would take. choices := []string{"ie", "uk"} cases := []struct{ name, region, want string }{ {"xa", "Bad/Code", msgRegionUnknown("Bad/Code", choices)}, {"xb", "nz", msgRegionNotArmed("nz", choices)}, {"xc", "au", msgNoSecretsKey("au")}, } for _, c := range cases { status, res := postJSON(t, newAppURL(fx), map[string]string{"name": c.name, "region": c.region}, token) if status != http.StatusBadRequest || errOf(res) != c.want { t.Errorf("region %q = %d %q, want 400 %q", c.region, status, errOf(res), c.want) } if has, _ := fx.st.HasAppIn(OpsAccountID, c.name); has { t.Errorf("a refused region still claimed %s", c.name) } } // Every advertised choice is accepted by the same door, same fixture. for i, code := range choices { name := fmt.Sprintf("alt%d", i) if status, res := postJSON(t, newAppURL(fx), map[string]string{"name": name, "region": code}, token); status != http.StatusOK { t.Errorf("advertised %q refused: %d %v", code, status, res) } } // R9: a region whose current-writer boxes lack the key. setEdges(t, fx, map[string]string{"uk-box": ukNoKey}) if status, res := postJSON(t, newAppURL(fx), map[string]string{"name": "xd", "region": "uk"}, token); status != http.StatusBadRequest || errOf(res) != msgNoSecretsKey("uk") { t.Fatalf("keyless uk = %d %v, want C31", status, res) } } func TestAPICreateAppSwitchOffStoresTheHome(t *testing.T) { fx, token := regionFixture(t) fx.srv.cfg.AppRegions = false _, res := postJSON(t, newAppURL(fx), map[string]string{"name": "off", "region": "uk", "timezone": "Europe/London"}, token) if r, src, _ := fx.st.AppRegion(OpsAccountID, "off"); r != "" || src != "default" || res["region"] != "ie" { t.Fatalf("switch off = %q %q %v", r, src, res) } fx.srv.cfg.AppRegions, fx.srv.cfg.HomeRegion = true, "" // PF9: the second half must not pass on a failed create. if status, res := postJSON(t, newAppURL(fx), map[string]string{"name": "unnamed", "region": "uk"}, token); status != http.StatusOK { t.Fatalf("unnamed-home create = %d %v", status, res) } if r, _, _ := fx.st.AppRegion(OpsAccountID, "unnamed"); r != "" { t.Fatalf("unnamed home stored %q", r) } } func TestClaimAdoptionDiscardsTheGuess(t *testing.T) { fx, _ := regionFixture(t) acct := &Account{ID: OpsAccountID, Sqid: "bab"} seedBucketApp(t, fx, "bab", "oldsite") if out, _, _ := fx.srv.claimApp(context.Background(), acct, "oldsite", "paul@keymail.dev", "", true, regionReq{tz: "Europe/London"}); out != claimOK { t.Fatalf("adoption = %v", out) } if r, src, _ := fx.st.AppRegion(OpsAccountID, "oldsite"); r != "" || src != "default" { t.Fatalf("adoption with a guess = %q %q", r, src) } seedBucketApp(t, fx, "bab", "oldsite2") fx.srv.claimApp(context.Background(), acct, "oldsite2", "paul@keymail.dev", "", true, regionReq{explicit: "uk", hasExplicit: true}) if r, src, _ := fx.st.AppRegion(OpsAccountID, "oldsite2"); r != "uk" || src != "chosen" { t.Fatalf("adoption with an explicit choice = %q %q", r, src) } } // codex plan-R6 1, spec §6.5: every successful creation logs exactly one // region line, naming what was stored, whatever the switch says: on, off, // and on with an unnamed home (which keeps assignment off). func TestClaimLogsOneRegionLineInEverySwitchState(t *testing.T) { for _, c := range []struct { name, app, home string on bool stored, source string line string }{ {"assignment on", "on1", "ie", true, "uk", "chosen", "console: app region bab/on1: uk (chosen)"}, {"assignment off", "off1", "ie", false, "", "default", "console: app region bab/off1: ie (default)"}, {"on, home unnamed", "unnamed1", "", true, "", "default", "console: app region bab/unnamed1: (default)"}, } { fx, token := regionFixture(t) fx.srv.cfg.AppRegions, fx.srv.cfg.HomeRegion = c.on, c.home var logs bytes.Buffer restore := captureLogTo(&logs) status, res := postJSON(t, newAppURL(fx), map[string]string{"name": c.app, "region": "uk"}, token) restore() if status != http.StatusOK { t.Fatalf("%s: create = %d %v", c.name, status, res) } if r, src, _ := fx.st.AppRegion(OpsAccountID, c.app); r != c.stored || src != c.source { t.Fatalf("%s: stored %q %q, want %q %q", c.name, r, src, c.stored, c.source) } if n := strings.Count(logs.String(), "console: app region bab/"+c.app+":"); n != 1 || !strings.Contains(logs.String(), c.line+"\n") { t.Errorf("%s: %d region line(s) in %q, want exactly %q", c.name, n, logs.String(), c.line) } } } // Review Focus 5: a competing insert that lands AFTER the region was // resolved loses the claim: no row of ours, no region log line. func TestClaimLostAfterResolutionWritesNothing(t *testing.T) { fx, token := regionFixture(t) fx.srv.beforeClaimInsert = func() { if _, err := fx.st.CreateAppInRegion(OpsAccountID, "racy", "rival@k.dev", "", "chosen", time.Now()); err != nil { t.Error(err) } } var logs bytes.Buffer restore := captureLogTo(&logs) status, _ := postJSON(t, newAppURL(fx), map[string]string{"name": "racy", "region": "uk"}, token) restore() if status != http.StatusConflict { t.Fatalf("lost race = %d, want 409", status) } if r, src, _ := fx.st.AppRegion(OpsAccountID, "racy"); r != "" || src != "chosen" { t.Fatalf("the rival's row changed: %q %q", r, src) } if strings.Contains(logs.String(), "app region bab/racy") { t.Fatalf("a lost claim logged a region line: %s", logs.String()) } } // Review Focus 5: a held apex refuses before resolution, so nothing is // written or logged. func TestClaimHeldApexWritesNothing(t *testing.T) { fx, token := regionFixture(t) fx.srv.cfg.AppsDomain = "oncarlos.test" fx.addRoute(t, registry.Route{Host: "beta." + fx.sqid + ".oncarlos.test", App: "kass", Account: fx.sqid, Kind: "instance"}) var logs bytes.Buffer restore := captureLogTo(&logs) status, _ := postJSON(t, newAppURL(fx), map[string]string{"name": "beta", "timezone": "Europe/London"}, token) restore() if status != http.StatusConflict { t.Fatalf("held apex = %d, want 409", status) } if has, _ := fx.st.HasAppIn(OpsAccountID, "beta"); has { t.Fatal("beta was created") } if strings.Contains(logs.String(), "app region bab/beta") { t.Fatalf("a refused claim logged a region line: %s", logs.String()) } } // Controller ruling: with the switch off the API door neither validates nor // refuses an explicit region (spec §9.6, C13b is the CLI's word for it). It // stores the home as "default", with no bucket reads for a refusal. func TestAPICreateAppSwitchOffIgnoresAnExplicitRegion(t *testing.T) { fx, token := regionFixture(t) fx.srv.cfg.AppRegions = false for _, region := range []string{"nz", "Bad/Code"} { name := "off" + strings.ToLower(strings.ReplaceAll(region, "/", "")) status, res := postJSON(t, newAppURL(fx), map[string]string{"name": name, "region": region}, token) if status != http.StatusOK || res["region"] != "ie" || res["region_source"] != "default" { t.Fatalf("switch off, region %q = %d %v, want 200 home/default", region, status, res) } if e := errOf(res); e != "" { t.Errorf("switch off refused: %q", e) } if r, src, _ := fx.st.AppRegion(OpsAccountID, name); r != "" || src != "default" { t.Fatalf("switch off, region %q stored %q %q", region, r, src) } } } // Final review, C11 on a switch-off console: regions_available is what the // create could actually use. With the switch off every create lands on the // home and --region is ignored (C13b), so the answer is 1, and a CLI, old // or new, that prints C11 at two or more never invites a flag this console // ignores. The same fleet with the switch on still answers 2. func TestAPICreateAppSwitchOffReportsOneRegionAvailable(t *testing.T) { fx, token := regionFixture(t) if status, res := postJSON(t, newAppURL(fx), map[string]string{"name": "on"}, token); status != http.StatusOK || res["regions_available"] != 2.0 { t.Fatalf("switch on = %d %v, want regions_available 2", status, res) } fx.srv.cfg.AppRegions = false if status, res := postJSON(t, newAppURL(fx), map[string]string{"name": "off"}, token); status != http.StatusOK || res["regions_available"] != 1.0 { t.Fatalf("switch off = %d %v, want regions_available 1", status, res) } // An unnamed home is the switch off too (appRegionsOn). fx.srv.cfg.AppRegions, fx.srv.cfg.HomeRegion = true, "" if status, res := postJSON(t, newAppURL(fx), map[string]string{"name": "unnamed"}, token); status != http.StatusOK || res["regions_available"] != 1.0 { t.Fatalf("unnamed home = %d %v, want regions_available 1", status, res) } } // Defence in depth for future doors: an explicit code that is not a valid // region never reaches the row. func TestResolveNewAppRegionRejectsAGarbageExplicitCode(t *testing.T) { fx, _ := regionFixture(t) for _, bad := range []string{"Bad/Code", "../x", "UK", "a b"} { if r, src := fx.srv.resolveNewAppRegion(context.Background(), regionReq{explicit: bad, hasExplicit: true}, false); r != "" || src != "default" { t.Errorf("explicit %q resolved to %q %q, want the home as default", bad, r, src) } } // Valid choices still stand: a coded region, and the home's "". if r, src := fx.srv.resolveNewAppRegion(context.Background(), regionReq{explicit: "uk", hasExplicit: true}, false); r != "uk" || src != "chosen" { t.Errorf("explicit uk = %q %q", r, src) } if r, src := fx.srv.resolveNewAppRegion(context.Background(), regionReq{explicit: "", hasExplicit: true}, false); r != "" || src != "chosen" { t.Errorf("explicit home = %q %q", r, src) } }