package console import ( "net/http" "testing" "time" ) func accountPlanURL(fx *consoleFixture, sqid string) string { return fx.ts.URL + "/api/cli/accounts/" + sqid + "/plan" } func planOperatorToken(t *testing.T, fx *consoleFixture, address, code string) string { t.Helper() if err := fx.st.SetOperator(address, true); err != nil { t.Fatal(err) } return cliToken(t, fx, address, code) } func planNum(t *testing.T, m map[string]any, key string) float64 { t.Helper() v, ok := m[key].(float64) if !ok { t.Fatalf("%s missing or not a number in %v", key, m) } return v } func TestAccountPlanDoorIsOperatorOnly(t *testing.T) { fx := newConsoleFixture(t) fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) acct := planAccount(t, fx, 501) owner := "owner" + acct.Sqid + "@example.com" token := cliToken(t, fx, owner, "code-plan-owner") if status, body := getRaw(t, accountPlanURL(fx, acct.Sqid), token); status != http.StatusNotFound || body != "404 page not found\n" { t.Fatalf("GET by the account's owner = %d %q, want the stdlib 404", status, body) } body := map[string]any{"plan": "personal", "reason": "r"} if status, out := postRaw(t, accountPlanURL(fx, acct.Sqid), body, token); status != http.StatusNotFound || out != "404 page not found\n" { t.Fatalf("POST by the account's owner = %d %q, want the stdlib 404", status, out) } if row, _ := fx.st.AccountPlan(acct.ID); row != nil { t.Fatalf("refused POST wrote %+v", row) } } func TestAccountPlanDoorWithoutACatalog(t *testing.T) { fx := newConsoleFixture(t) acct := planAccount(t, fx, 501) token := planOperatorToken(t, fx, "ops@keymail.dev", "code-plan-nocat") status, out := getJSON(t, accountPlanURL(fx, acct.Sqid), token) if status != http.StatusConflict || out["error"] == nil { t.Fatalf("GET with no catalog = %d %v, want 409 with an error", status, out) } if status, _ := postJSON(t, accountPlanURL(fx, acct.Sqid), map[string]any{"plan": "free", "reason": "r"}, token); status != http.StatusConflict { t.Fatalf("POST with no catalog = %d, want 409", status) } } func TestAccountPlanDoorUnknownAccount(t *testing.T) { fx := newConsoleFixture(t) fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) token := planOperatorToken(t, fx, "ops@keymail.dev", "code-plan-unknown") if status, _ := getRaw(t, accountPlanURL(fx, "zzzzzz"), token); status != http.StatusNotFound { t.Fatalf("unknown account = %d, want 404", status) } } func TestAccountPlanGetDefaults(t *testing.T) { fx := newConsoleFixture(t) fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) acct := planAccount(t, fx, 501) token := planOperatorToken(t, fx, "ops@keymail.dev", "code-plan-get") status, out := getJSON(t, accountPlanURL(fx, acct.Sqid), token) if status != http.StatusOK { t.Fatalf("GET = %d %v", status, out) } if out["plan"] != "free" || out["source"] != PlanSourceDefault || planNum(t, out, "revision") != 0 || out["catalog_id"] != fx.srv.cfg.PlanCatalog.ID() || out["account"] != acct.Sqid { t.Fatalf("reply %v", out) } if limits := block(t, out, "limits"); planNum(t, limits, "running_things") != 3 { t.Fatalf("limits %v", limits) } } func TestAccountPlanSetAndPublish(t *testing.T) { fx := newConsoleFixture(t) fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) acct := planAccount(t, fx, 501) token := planOperatorToken(t, fx, "ops@keymail.dev", "code-plan-set") status, out := postJSON(t, accountPlanURL(fx, acct.Sqid), map[string]any{"plan": "personal", "packs": map[string]int{"apps": 1}, "reason": "upgraded"}, token) if status != http.StatusOK { t.Fatalf("POST = %d %v", status, out) } if out["source"] != PlanSourceOperator || planNum(t, out, "revision") != 1 || planNum(t, out, "published_revision") != 1 || out["reason"] != "upgraded" { t.Fatalf("reply %v", out) } if limits := block(t, out, "limits"); planNum(t, limits, "running_things") != 30 { t.Fatalf("limits %v", limits) } r, _ := readLimits(t, fx, acct.Sqid) if r == nil || r.Revision != 1 || r.Plan != "personal" || r.Limits.RunningThings != 30 { t.Fatalf("bucket record %+v", r) } row, _ := fx.st.AccountPlan(acct.ID) if row.UpdatedBy != "ops@keymail.dev" { t.Fatalf("updated_by %q", row.UpdatedBy) } } func TestAccountPlanSetSponsored(t *testing.T) { fx := newConsoleFixture(t) fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) acct := planAccount(t, fx, 501) token := planOperatorToken(t, fx, "ops@keymail.dev", "code-plan-sponsor") status, out := postJSON(t, accountPlanURL(fx, acct.Sqid), map[string]any{"plan": "free", "sponsored": map[string]any{"running_things": 12}, "reason": "existing account"}, token) if status != http.StatusOK { t.Fatalf("POST = %d %v", status, out) } if out["source"] != PlanSourceSponsored { t.Fatalf("source %v", out["source"]) } if limits := block(t, out, "limits"); planNum(t, limits, "running_things") != 12 { t.Fatalf("limits %v", limits) } if sp := block(t, out, "sponsored"); planNum(t, sp, "running_things") != 12 { t.Fatalf("sponsored %v", sp) } } func TestAccountPlanSetRefuses(t *testing.T) { fx := newConsoleFixture(t) fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) acct := planAccount(t, fx, 501) token := planOperatorToken(t, fx, "ops@keymail.dev", "code-plan-refuse") cases := map[string]map[string]any{ "unknown plan": {"plan": "gold", "reason": "r"}, "unknown pack": {"plan": "personal", "packs": map[string]int{"seats": 1}, "reason": "r"}, "pack not offered": {"plan": "free", "packs": map[string]int{"apps": 1}, "reason": "r"}, "zero quantity": {"plan": "personal", "packs": map[string]int{"apps": 0}, "reason": "r"}, "negative": {"plan": "personal", "packs": map[string]int{"apps": -1}, "reason": "r"}, "above max": {"plan": "personal", "packs": map[string]int{"apps": 21}, "reason": "r"}, "no reason": {"plan": "personal"}, "blank reason": {"plan": "personal", "reason": " "}, } for name, body := range cases { status, out := postJSON(t, accountPlanURL(fx, acct.Sqid), body, token) if status != http.StatusBadRequest || out["error"] == nil { t.Errorf("%s: %d %v, want 400 with an error", name, status, out) } } if row, _ := fx.st.AccountPlan(acct.ID); row != nil { t.Fatalf("a refused POST wrote %+v", row) } } func TestAccountPlanSetSurvivesABucketFailure(t *testing.T) { fx := newConsoleFixture(t) fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) acct := planAccount(t, fx, 501) token := planOperatorToken(t, fx, "ops@keymail.dev", "code-plan-bucket") fp := &planFailingPuts{Store: fx.rel} fp.n.Store(1000) fx.srv.cfg.Releases = fp status, out := postJSON(t, accountPlanURL(fx, acct.Sqid), map[string]any{"plan": "personal", "reason": "r"}, token) if status != http.StatusOK { t.Fatalf("POST = %d %v", status, out) } if planNum(t, out, "published_revision") >= planNum(t, out, "revision") { t.Fatalf("claimed published although the write failed: %v", out) } fp.n.Store(0) fx.srv.planLimitsPass(t.Context(), time.Now()) if r, _ := readLimits(t, fx, acct.Sqid); r == nil || r.Plan != "personal" { t.Fatalf("the next pass did not publish: %+v", r) } } // A misspelt sponsored limit must be refused, not silently dropped: an // override that quietly vanished would lower a sponsored account's limits. func TestAccountPlanSetRefusesUnknownFields(t *testing.T) { fx := newConsoleFixture(t) fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) acct := planAccount(t, fx, 501) token := planOperatorToken(t, fx, "ops@keymail.dev", "code-plan-strict") for name, body := range map[string]map[string]any{ "misspelt sponsored limit": {"plan": "free", "sponsored": map[string]any{"running_thing": 12}, "reason": "r"}, "misspelt top-level field": {"plan": "free", "pack": map[string]int{"apps": 1}, "reason": "r"}, } { if status, out := postJSON(t, accountPlanURL(fx, acct.Sqid), body, token); status != http.StatusBadRequest { t.Errorf("%s: %d %v, want 400", name, status, out) } } if row, _ := fx.st.AccountPlan(acct.ID); row != nil { t.Fatalf("a refused POST wrote %+v", row) } } // Astra plans-1 finding 3: an account deleted between the door's lookup and // its write gets no plan row and no published record. func TestAccountPlanSetOnADeletedAccountIs404(t *testing.T) { fx := newConsoleFixture(t) fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) acct := planAccount(t, fx, 501) token := planOperatorToken(t, fx, "ops@keymail.dev", "code-plan-deleted") if _, err := fx.st.db.Exec(`UPDATE accounts SET deleted_at = ? WHERE id = ?`, planNow.Unix(), acct.ID); err != nil { t.Fatal(err) } status, _ := postRaw(t, accountPlanURL(fx, acct.Sqid), map[string]any{"plan": "free", "reason": "r"}, token) if status != http.StatusNotFound { t.Fatalf("POST on a deleted account = %d, want 404", status) } if r, _ := readLimits(t, fx, acct.Sqid); r != nil { t.Fatalf("published %+v for a deleted account", r) } }