carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3import (
4 "net/http"
5 "testing"
6 "time"
7)
8
9func accountPlanURL(fx *consoleFixture, sqid string) string {
10 return fx.ts.URL + "/api/cli/accounts/" + sqid + "/plan"
11}
12
13func planOperatorToken(t *testing.T, fx *consoleFixture, address, code string) string {
14 t.Helper()
15 if err := fx.st.SetOperator(address, true); err != nil {
16 t.Fatal(err)
17 }
18 return cliToken(t, fx, address, code)
19}
20
21func planNum(t *testing.T, m map[string]any, key string) float64 {
22 t.Helper()
23 v, ok := m[key].(float64)
24 if !ok {
25 t.Fatalf("%s missing or not a number in %v", key, m)
26 }
27 return v
28}
29
30func TestAccountPlanDoorIsOperatorOnly(t *testing.T) {
31 fx := newConsoleFixture(t)
32 fx.srv.cfg.PlanCatalog = examplePlanCatalog(t)
33 acct := planAccount(t, fx, 501)
34 owner := "owner" + acct.Sqid + "@example.com"
35 token := cliToken(t, fx, owner, "code-plan-owner")
36 if status, body := getRaw(t, accountPlanURL(fx, acct.Sqid), token); status != http.StatusNotFound || body != "404 page not found\n" {
37 t.Fatalf("GET by the account's owner = %d %q, want the stdlib 404", status, body)
38 }
39 body := map[string]any{"plan": "personal", "reason": "r"}
40 if status, out := postRaw(t, accountPlanURL(fx, acct.Sqid), body, token); status != http.StatusNotFound || out != "404 page not found\n" {
41 t.Fatalf("POST by the account's owner = %d %q, want the stdlib 404", status, out)
42 }
43 if row, _ := fx.st.AccountPlan(acct.ID); row != nil {
44 t.Fatalf("refused POST wrote %+v", row)
45 }
46}
47
48func TestAccountPlanDoorWithoutACatalog(t *testing.T) {
49 fx := newConsoleFixture(t)
50 acct := planAccount(t, fx, 501)
51 token := planOperatorToken(t, fx, "ops@keymail.dev", "code-plan-nocat")
52 status, out := getJSON(t, accountPlanURL(fx, acct.Sqid), token)
53 if status != http.StatusConflict || out["error"] == nil {
54 t.Fatalf("GET with no catalog = %d %v, want 409 with an error", status, out)
55 }
56 if status, _ := postJSON(t, accountPlanURL(fx, acct.Sqid), map[string]any{"plan": "free", "reason": "r"}, token); status != http.StatusConflict {
57 t.Fatalf("POST with no catalog = %d, want 409", status)
58 }
59}
60
61func TestAccountPlanDoorUnknownAccount(t *testing.T) {
62 fx := newConsoleFixture(t)
63 fx.srv.cfg.PlanCatalog = examplePlanCatalog(t)
64 token := planOperatorToken(t, fx, "ops@keymail.dev", "code-plan-unknown")
65 if status, _ := getRaw(t, accountPlanURL(fx, "zzzzzz"), token); status != http.StatusNotFound {
66 t.Fatalf("unknown account = %d, want 404", status)
67 }
68}
69
70func TestAccountPlanGetDefaults(t *testing.T) {
71 fx := newConsoleFixture(t)
72 fx.srv.cfg.PlanCatalog = examplePlanCatalog(t)
73 acct := planAccount(t, fx, 501)
74 token := planOperatorToken(t, fx, "ops@keymail.dev", "code-plan-get")
75 status, out := getJSON(t, accountPlanURL(fx, acct.Sqid), token)
76 if status != http.StatusOK {
77 t.Fatalf("GET = %d %v", status, out)
78 }
79 if out["plan"] != "free" || out["source"] != PlanSourceDefault || planNum(t, out, "revision") != 0 ||
80 out["catalog_id"] != fx.srv.cfg.PlanCatalog.ID() || out["account"] != acct.Sqid {
81 t.Fatalf("reply %v", out)
82 }
83 if limits := block(t, out, "limits"); planNum(t, limits, "running_things") != 3 {
84 t.Fatalf("limits %v", limits)
85 }
86}
87
88func TestAccountPlanSetAndPublish(t *testing.T) {
89 fx := newConsoleFixture(t)
90 fx.srv.cfg.PlanCatalog = examplePlanCatalog(t)
91 acct := planAccount(t, fx, 501)
92 token := planOperatorToken(t, fx, "ops@keymail.dev", "code-plan-set")
93 status, out := postJSON(t, accountPlanURL(fx, acct.Sqid),
94 map[string]any{"plan": "personal", "packs": map[string]int{"apps": 1}, "reason": "upgraded"}, token)
95 if status != http.StatusOK {
96 t.Fatalf("POST = %d %v", status, out)
97 }
98 if out["source"] != PlanSourceOperator || planNum(t, out, "revision") != 1 || planNum(t, out, "published_revision") != 1 ||
99 out["reason"] != "upgraded" {
100 t.Fatalf("reply %v", out)
101 }
102 if limits := block(t, out, "limits"); planNum(t, limits, "running_things") != 30 {
103 t.Fatalf("limits %v", limits)
104 }
105 r, _ := readLimits(t, fx, acct.Sqid)
106 if r == nil || r.Revision != 1 || r.Plan != "personal" || r.Limits.RunningThings != 30 {
107 t.Fatalf("bucket record %+v", r)
108 }
109 row, _ := fx.st.AccountPlan(acct.ID)
110 if row.UpdatedBy != "ops@keymail.dev" {
111 t.Fatalf("updated_by %q", row.UpdatedBy)
112 }
113}
114
115func TestAccountPlanSetSponsored(t *testing.T) {
116 fx := newConsoleFixture(t)
117 fx.srv.cfg.PlanCatalog = examplePlanCatalog(t)
118 acct := planAccount(t, fx, 501)
119 token := planOperatorToken(t, fx, "ops@keymail.dev", "code-plan-sponsor")
120 status, out := postJSON(t, accountPlanURL(fx, acct.Sqid),
121 map[string]any{"plan": "free", "sponsored": map[string]any{"running_things": 12}, "reason": "existing account"}, token)
122 if status != http.StatusOK {
123 t.Fatalf("POST = %d %v", status, out)
124 }
125 if out["source"] != PlanSourceSponsored {
126 t.Fatalf("source %v", out["source"])
127 }
128 if limits := block(t, out, "limits"); planNum(t, limits, "running_things") != 12 {
129 t.Fatalf("limits %v", limits)
130 }
131 if sp := block(t, out, "sponsored"); planNum(t, sp, "running_things") != 12 {
132 t.Fatalf("sponsored %v", sp)
133 }
134}
135
136func TestAccountPlanSetRefuses(t *testing.T) {
137 fx := newConsoleFixture(t)
138 fx.srv.cfg.PlanCatalog = examplePlanCatalog(t)
139 acct := planAccount(t, fx, 501)
140 token := planOperatorToken(t, fx, "ops@keymail.dev", "code-plan-refuse")
141 cases := map[string]map[string]any{
142 "unknown plan": {"plan": "gold", "reason": "r"},
143 "unknown pack": {"plan": "personal", "packs": map[string]int{"seats": 1}, "reason": "r"},
144 "pack not offered": {"plan": "free", "packs": map[string]int{"apps": 1}, "reason": "r"},
145 "zero quantity": {"plan": "personal", "packs": map[string]int{"apps": 0}, "reason": "r"},
146 "negative": {"plan": "personal", "packs": map[string]int{"apps": -1}, "reason": "r"},
147 "above max": {"plan": "personal", "packs": map[string]int{"apps": 21}, "reason": "r"},
148 "no reason": {"plan": "personal"},
149 "blank reason": {"plan": "personal", "reason": " "},
150 }
151 for name, body := range cases {
152 status, out := postJSON(t, accountPlanURL(fx, acct.Sqid), body, token)
153 if status != http.StatusBadRequest || out["error"] == nil {
154 t.Errorf("%s: %d %v, want 400 with an error", name, status, out)
155 }
156 }
157 if row, _ := fx.st.AccountPlan(acct.ID); row != nil {
158 t.Fatalf("a refused POST wrote %+v", row)
159 }
160}
161
162func TestAccountPlanSetSurvivesABucketFailure(t *testing.T) {
163 fx := newConsoleFixture(t)
164 fx.srv.cfg.PlanCatalog = examplePlanCatalog(t)
165 acct := planAccount(t, fx, 501)
166 token := planOperatorToken(t, fx, "ops@keymail.dev", "code-plan-bucket")
167 fp := &planFailingPuts{Store: fx.rel}
168 fp.n.Store(1000)
169 fx.srv.cfg.Releases = fp
170 status, out := postJSON(t, accountPlanURL(fx, acct.Sqid), map[string]any{"plan": "personal", "reason": "r"}, token)
171 if status != http.StatusOK {
172 t.Fatalf("POST = %d %v", status, out)
173 }
174 if planNum(t, out, "published_revision") >= planNum(t, out, "revision") {
175 t.Fatalf("claimed published although the write failed: %v", out)
176 }
177 fp.n.Store(0)
178 fx.srv.planLimitsPass(t.Context(), time.Now())
179 if r, _ := readLimits(t, fx, acct.Sqid); r == nil || r.Plan != "personal" {
180 t.Fatalf("the next pass did not publish: %+v", r)
181 }
182}
183
184// A misspelt sponsored limit must be refused, not silently dropped: an
185// override that quietly vanished would lower a sponsored account's limits.
186func TestAccountPlanSetRefusesUnknownFields(t *testing.T) {
187 fx := newConsoleFixture(t)
188 fx.srv.cfg.PlanCatalog = examplePlanCatalog(t)
189 acct := planAccount(t, fx, 501)
190 token := planOperatorToken(t, fx, "ops@keymail.dev", "code-plan-strict")
191 for name, body := range map[string]map[string]any{
192 "misspelt sponsored limit": {"plan": "free", "sponsored": map[string]any{"running_thing": 12}, "reason": "r"},
193 "misspelt top-level field": {"plan": "free", "pack": map[string]int{"apps": 1}, "reason": "r"},
194 } {
195 if status, out := postJSON(t, accountPlanURL(fx, acct.Sqid), body, token); status != http.StatusBadRequest {
196 t.Errorf("%s: %d %v, want 400", name, status, out)
197 }
198 }
199 if row, _ := fx.st.AccountPlan(acct.ID); row != nil {
200 t.Fatalf("a refused POST wrote %+v", row)
201 }
202}
203
204// Astra plans-1 finding 3: an account deleted between the door's lookup and
205// its write gets no plan row and no published record.
206func TestAccountPlanSetOnADeletedAccountIs404(t *testing.T) {
207 fx := newConsoleFixture(t)
208 fx.srv.cfg.PlanCatalog = examplePlanCatalog(t)
209 acct := planAccount(t, fx, 501)
210 token := planOperatorToken(t, fx, "ops@keymail.dev", "code-plan-deleted")
211 if _, err := fx.st.db.Exec(`UPDATE accounts SET deleted_at = ? WHERE id = ?`, planNow.Unix(), acct.ID); err != nil {
212 t.Fatal(err)
213 }
214 status, _ := postRaw(t, accountPlanURL(fx, acct.Sqid), map[string]any{"plan": "free", "reason": "r"}, token)
215 if status != http.StatusNotFound {
216 t.Fatalf("POST on a deleted account = %d, want 404", status)
217 }
218 if r, _ := readLimits(t, fx, acct.Sqid); r != nil {
219 t.Fatalf("published %+v for a deleted account", r)
220 }
221}
222