package main import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" ) func TestCreateAppSendsRegionAndTimezone(t *testing.T) { var got map[string]any ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { json.NewDecoder(r.Body).Decode(&got) json.NewEncoder(w).Encode(map[string]any{"account": "a", "app": "blog", "region": "uk", "region_name": "UK · London", "region_source": "timezone", "regions_available": 3}) })) defer ts.Close() res, err := newTestSession(ts.URL).createApp(context.Background(), apiAcct{Seg: "a"}, "blog", "", false, "", "", "Europe/London") if err != nil { t.Fatal(err) } if got["timezone"] != "Europe/London" { t.Fatalf("body = %v", got) } if _, sent := got["region"]; sent { t.Fatal("region sent without --region") } if res.RegionName != "UK · London" || res.RegionsAvailable != 3 || res.Region != "uk" || res.RegionSource != "timezone" { t.Fatalf("result = %+v", res) } } // An explicit region travels in the body; an empty timezone is left out, so // a machine with no readable zone sends nothing. func TestCreateAppSendsExplicitRegionAndOmitsEmptyTimezone(t *testing.T) { var got map[string]any ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { json.NewDecoder(r.Body).Decode(&got) json.NewEncoder(w).Encode(map[string]any{"account": "a", "app": "blog"}) })) defer ts.Close() if _, err := newTestSession(ts.URL).createApp(context.Background(), apiAcct{Seg: "a"}, "blog", "", false, "", "us-east", ""); err != nil { t.Fatal(err) } if got["region"] != "us-east" { t.Fatalf("body = %v", got) } if _, sent := got["timezone"]; sent { t.Fatalf("empty timezone sent: %v", got) } } // A console that predates the region fields answers as it always did; the // new fields read as zero values. func TestCreateAppOldConsoleResponse(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(map[string]any{"account": "a", "app": "blog", "url": "https://x/a/a/apps/blog"}) })) defer ts.Close() res, err := newTestSession(ts.URL).createApp(context.Background(), apiAcct{Seg: "a"}, "blog", "", false, "", "", "Europe/London") if err != nil { t.Fatal(err) } if res.App != "blog" || res.Region != "" || res.RegionName != "" || res.RegionSource != "" || res.RegionsAvailable != 0 { t.Fatalf("result = %+v", res) } } func TestPrintAppRegion(t *testing.T) { cases := []struct { name string res apiCreateAppResult asked string want string }{ {"timezone", apiCreateAppResult{Region: "uk", RegionName: "UK · London", RegionSource: "timezone", RegionsAvailable: 2}, "", regionCreatedLine("UK · London", "timezone") + "\n"}, {"single region", apiCreateAppResult{Region: "ie", RegionName: "Ireland · Dublin", RegionSource: "default", RegionsAvailable: 1}, "", ""}, {"old console asked", apiCreateAppResult{}, "uk", regionNotRecordedOldConsole + "\n"}, {"switch off asked", apiCreateAppResult{Region: "ie", RegionName: "Ireland · Dublin", RegionSource: "default", RegionsAvailable: 2}, "uk", regionNotAppliedSwitchOff("Ireland · Dublin") + "\n"}, {"switch off, asked the home itself", apiCreateAppResult{Region: "ie", RegionName: "Ireland · Dublin", RegionSource: "default", RegionsAvailable: 1}, "ie", regionNotAppliedSwitchOff("Ireland · Dublin") + "\n"}, {"switch on, explicit region", apiCreateAppResult{Region: "uk", RegionName: "UK · London", RegionSource: "chosen", RegionsAvailable: 2}, "uk", regionCreatedLine("UK · London", "chosen") + "\n"}, {"old console not asked", apiCreateAppResult{}, "", ""}, } for _, c := range cases { var b bytes.Buffer printAppRegion(&b, c.res, c.asked, false) if b.String() != c.want { t.Errorf("%s: %q, want %q", c.name, b.String(), c.want) } } } // The full printed line for each source, with a place name and a region code // that differ, so a swapped argument shows. func TestPrintAppRegionFullLines(t *testing.T) { cases := []struct { source, asked, want string }{ {"timezone", "", "Region: UK · London (from your timezone). Use --region to choose another.\n"}, {"ip", "", "Region: UK · London (from your network location). Use --region to choose another.\n"}, {"default", "", "Region: UK · London (the platform default). Use --region to choose another.\n"}, {"chosen", "uk", "Region: UK · London.\n"}, } for _, c := range cases { var b bytes.Buffer printAppRegion(&b, apiCreateAppResult{Region: "uk", RegionName: "UK · London", RegionSource: c.source, RegionsAvailable: 3}, c.asked, false) if b.String() != c.want { t.Errorf("source %q: %q, want %q", c.source, b.String(), c.want) } } } // Controller ruling, switch off: the console ignores --region, and answers // the home with source "default". The CLI says so (C13b), in full. func TestPrintAppRegionSwitchOffFullLine(t *testing.T) { var b bytes.Buffer printAppRegion(&b, apiCreateAppResult{Region: "ie", RegionName: "Ireland · Dublin", RegionSource: "default", RegionsAvailable: 1}, "uk", false) want := "This console is not assigning app regions yet, so --region was not applied. This app's instances start in Ireland · Dublin.\n" if b.String() != want { t.Fatalf("got %q, want %q", b.String(), want) } } // Controller ruling, read-back failure: the console answers region = the home // and region_source = "". That is not "the console did not apply --region", // so the CLI prints neither C13a nor C13b, and no C9 to C12 line that would // claim a source it does not know. func TestPrintAppRegionReadBackFailureIsSilent(t *testing.T) { for _, asked := range []string{"", "uk", "ie"} { for _, avail := range []int{0, 1, 3} { var b bytes.Buffer printAppRegion(&b, apiCreateAppResult{Region: "ie", RegionName: "Ireland · Dublin", RegionSource: "", RegionsAvailable: avail}, asked, false) if b.String() != "" { t.Errorf("asked %q available %d: printed %q, want nothing", asked, avail, b.String()) } } } } // deploy's onboarding prints the region line after "created app", sends the // asked region, and never prints the timezone it sends. func TestOfferCreateAppPrintsRegionLine(t *testing.T) { useTempCredentials(t) out := captureStdout(t) forceTerminal(t, true) t.Setenv("TZ", "Europe/Secretville") var got map[string]any ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { json.NewDecoder(r.Body).Decode(&got) json.NewEncoder(w).Encode(map[string]any{"app": "taskmonger", "account": "bdk", "url": "https://x/a/bdk/taskmonger", "region": "uk", "region_name": "UK · London", "region_source": "chosen", "regions_available": 3}) })) t.Cleanup(ts.Close) dir := filepath.Join(t.TempDir(), "taskmonger") if err := os.MkdirAll(dir, 0o755); err != nil { t.Fatal(err) } t.Chdir(dir) swapChooserIn(t, "y\n") app := offerCreateApp(context.Background(), newTestSession(ts.URL), apiAcct{Seg: "bdk", Exact: true}, &noAppsError{Account: "tom@keymail.dev"}, "uk") if app != "taskmonger" { t.Fatalf("app = %q", app) } if got["region"] != "uk" || got["timezone"] != "Europe/Secretville" { t.Fatalf("body = %v", got) } if !strings.Contains(out.String(), "created app") || !strings.Contains(out.String(), "Region: UK · London.\n") { t.Fatalf("stdout = %q", out.String()) } if strings.Contains(out.String(), "Secretville") { t.Fatalf("the timezone was printed: %q", out.String()) } } // A create that named a fleet is placed by the fleet: C13b would be wrong, so // nothing prints. Every other case is unchanged by onFleet. func TestPrintAppRegionOnFleet(t *testing.T) { off := apiCreateAppResult{Region: "ie", RegionName: "Ireland · Dublin", RegionSource: "default", RegionsAvailable: 2} var b bytes.Buffer printAppRegion(&b, off, "uk", true) if b.String() != "" { t.Fatalf("on a fleet: %q, want nothing", b.String()) } b.Reset() printAppRegion(&b, off, "uk", false) if b.String() != regionNotAppliedSwitchOff("Ireland · Dublin")+"\n" { t.Fatalf("off a fleet: %q", b.String()) } // A console that did apply --region still says where the app starts. b.Reset() printAppRegion(&b, apiCreateAppResult{Region: "uk", RegionName: "UK · London", RegionSource: "chosen", RegionsAvailable: 2}, "uk", true) if b.String() != regionCreatedLine("UK · London", "chosen")+"\n" { t.Fatalf("chosen on a fleet: %q", b.String()) } }