| index 68933f7..1c742ef 100644 |
| --- a/home/region_test.go |
| +++ b/home/region_test.go |
| @@ -181,6 +181,98 @@ func TestListSpacesSurvivesOneRegionDown(t *testing.T) { |
| } |
| } |
| |
| +// listSpaces returns the decoded /api/spaces response body. |
| +func listSpaces(t *testing.T, ts *httptest.Server, token string) map[string]any { |
| + t.Helper() |
| + req, _ := http.NewRequest("GET", ts.URL+"/api/spaces", nil) |
| + req.Header.Set("Authorization", "Bearer "+token) |
| + resp, err := http.DefaultClient.Do(req) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer resp.Body.Close() |
| + if resp.StatusCode != http.StatusOK { |
| + t.Fatalf("list status = %d, want 200", resp.StatusCode) |
| + } |
| + var m map[string]any |
| + json.NewDecoder(resp.Body).Decode(&m) |
| + return m |
| +} |
| + |
| +// tierKey digs the first tier's key out of a list response's billing block. |
| +func tierKey(t *testing.T, m map[string]any) string { |
| + t.Helper() |
| + b, ok := m["billing"].(map[string]any) |
| + if !ok { |
| + t.Fatalf("no billing in list response: %v", m) |
| + } |
| + tiers, _ := b["tiers"].([]any) |
| + if len(tiers) == 0 { |
| + t.Fatalf("billing has no tiers: %v", b) |
| + } |
| + first, _ := tiers[0].(map[string]any) |
| + key, _ := first["key"].(string) |
| + return key |
| +} |
| + |
| +func TestListSpacesCarriesBilling(t *testing.T) { |
| + ts, s := newTestServer(t) |
| + _, token := seedUser(t, s) |
| + |
| + ieCloud, _, _ := fakeCloud(t, http.StatusOK, map[string]any{ |
| + "spaces": []map[string]any{{"host": "a.ie.example.com"}}, |
| + "billing": map[string]any{"tiers": []map[string]any{{"key": "family"}}}, |
| + }) |
| + fiCloud, _, _ := fakeCloud(t, http.StatusOK, map[string]any{ |
| + "spaces": []map[string]any{}, |
| + "billing": map[string]any{"tiers": []map[string]any{{"key": "fi-only"}}}, |
| + }) |
| + |
| + // The production shape: one unpartitioned cloud with a price book. The book |
| + // must ride the list response — home.js reads r.billing, and dropping it |
| + // silently removes the Upgrade UI (the #1083 regression this test pins). |
| + s.cloud = newCloudProvisioner(ieCloud.URL, "tok") |
| + if got := tierKey(t, listSpaces(t, ts, token)); got != "family" { |
| + t.Errorf("single region: tier = %q, want family", got) |
| + } |
| + |
| + // Partitioned: the DEFAULT region's book wins, wherever it sits in the |
| + // fan-out order (ie deliberately listed last here). |
| + s.regions = []cloudRegion{ |
| + {Code: "fi", Name: "Finland", prov: newCloudProvisioner(fiCloud.URL, "fi-tok")}, |
| + {Code: "ie", Name: "Ireland", prov: newCloudProvisioner(ieCloud.URL, "ie-tok")}, |
| + } |
| + s.defaultRegion = "ie" |
| + s.cloud = s.cloudForRegion("ie") |
| + if got := tierKey(t, listSpaces(t, ts, token)); got != "family" { |
| + t.Errorf("two regions: tier = %q, want the default region's family", got) |
| + } |
| + |
| + // Default region billing-less (reports false) → another region's book is |
| + // the fallback rather than no Upgrade UI at all. |
| + noBilling, _, _ := fakeCloud(t, http.StatusOK, map[string]any{ |
| + "spaces": []map[string]any{}, "billing": false, |
| + }) |
| + s.regions = []cloudRegion{ |
| + {Code: "ie", Name: "Ireland", prov: newCloudProvisioner(noBilling.URL, "ie-tok")}, |
| + {Code: "fi", Name: "Finland", prov: newCloudProvisioner(fiCloud.URL, "fi-tok")}, |
| + } |
| + s.defaultRegion = "ie" |
| + s.cloud = s.cloudForRegion("ie") |
| + if got := tierKey(t, listSpaces(t, ts, token)); got != "fi-only" { |
| + t.Errorf("default billing-less: tier = %q, want the fi fallback", got) |
| + } |
| + |
| + // No cloud offers billing → the key is absent, not false/null (home.js |
| + // treats absence as billing off). |
| + s.regions = nil |
| + s.defaultRegion = "" |
| + s.cloud = newCloudProvisioner(noBilling.URL, "tok") |
| + if m := listSpaces(t, ts, token); m["billing"] != nil { |
| + t.Errorf("billing-less: billing = %v, want absent", m["billing"]) |
| + } |
| +} |
| + |
| func TestManageRoutesByHostRegion(t *testing.T) { |
| ts, s := newTestServer(t) |
| uid, _ := seedUser(t, s) |