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 "context"
5 "fmt"
6 "net/http"
7 "net/url"
8 "strings"
9 "sync"
10 "testing"
11
12 "github.com/carlosframework/platform/internal/plans"
13 "github.com/carlosframework/platform/internal/provision"
14)
15
16// atRunningCap turns plans on and leaves the fixture's ops account with
17// exactly capN running things against a cap of capN (capN >= the free
18// plan's 3).
19func atRunningCap(t *testing.T, fx *consoleFixture, capN int) *Account {
20 t.Helper()
21 fx.srv.cfg.PlanCatalog = examplePlanCatalog(t)
22 acct, err := fx.st.AccountBySqid(fx.sqid)
23 if err != nil || acct == nil {
24 t.Fatalf("ops account: %v", err)
25 }
26 for i := 0; ; i++ {
27 n, err := fx.srv.runningThings(context.Background(), acct)
28 if err != nil {
29 t.Fatal(err)
30 }
31 if n > capN {
32 capN = n
33 }
34 if n == capN {
35 break
36 }
37 capsApp(t, fx, acct, fmt.Sprintf("filler%02d", i))
38 }
39 if _, err := fx.st.SetAccountPlan(acct.ID, "free", nil, PlanSourceSponsored, &plans.Limits{RunningThings: capN}, "test", "ops@example.com", planNow); err != nil {
40 t.Fatal(err)
41 }
42 return acct
43}
44
45func raiseRunningCap(t *testing.T, fx *consoleFixture, acct *Account, by int) {
46 t.Helper()
47 n, err := fx.srv.runningThings(context.Background(), acct)
48 if err != nil {
49 t.Fatal(err)
50 }
51 if _, err := fx.st.SetAccountPlan(acct.ID, "free", nil, PlanSourceSponsored, &plans.Limits{RunningThings: n + by}, "test", "ops@example.com", planNow); err != nil {
52 t.Fatal(err)
53 }
54}
55
56func TestCreateAppRefusedAtTheRunningThingsCap(t *testing.T) {
57 fx := newConsoleFixture(t)
58 acct := atRunningCap(t, fx, 3)
59 token := cliToken(t, fx, "paul@keymail.dev", "code-cap-create")
60 status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/"+fx.sqid+"/new", map[string]string{"name": "onemore"}, token)
61 if status != http.StatusConflict || out["code"] != "cap" || out["error"] == nil {
62 t.Fatalf("CLI create at the cap = %d %v, want 409 cap", status, out)
63 }
64 c, csrf := signInAs(t, fx, "paul@keymail.dev", "code-cap-create-web")
65 if st, _, _ := postForm(t, c, fx.u("/apps/new"), url.Values{"csrf": {csrf}, "name": {"webmore"}}); st != http.StatusSeeOther {
66 t.Fatalf("browser create at the cap = %d, want the flash redirect", st)
67 }
68 for _, app := range []string{"onemore", "webmore"} {
69 if has, _ := fx.st.HasAppIn(acct.ID, app); has {
70 t.Fatalf("%s was created past the cap", app)
71 }
72 }
73}
74
75func TestCreateAppBelowTheCapStillWorks(t *testing.T) {
76 fx := newConsoleFixture(t)
77 acct := atRunningCap(t, fx, 3)
78 raiseRunningCap(t, fx, acct, 1)
79 token := cliToken(t, fx, "paul@keymail.dev", "code-cap-below")
80 if status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/"+fx.sqid+"/new", map[string]string{"name": "fits"}, token); status != http.StatusOK {
81 t.Fatalf("create below the cap = %d %v", status, out)
82 }
83}
84
85func TestTemporaryAppCountsTowardRunningThings(t *testing.T) {
86 fx := newConsoleFixture(t)
87 fx.srv.cfg.TemporaryApps = true
88 atRunningCap(t, fx, 3)
89 token := cliToken(t, fx, "paul@keymail.dev", "code-cap-temp")
90 status, out := createTemporary(t, fx, token, "tmp-capcheck", 3600)
91 if status != http.StatusConflict || out["code"] != "cap" {
92 t.Fatalf("temporary create at the running cap = %d %v, want 409 cap", status, out)
93 }
94}
95
96func TestRestoreRefusedAtTheRunningThingsCap(t *testing.T) {
97 fx := newConsoleFixture(t)
98 token := cliToken(t, fx, "paul@keymail.dev", "code-cap-restore")
99 if status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/"+fx.sqid+"/kass/delete", map[string]string{}, token); status != http.StatusOK {
100 t.Fatalf("delete kass = %d %v", status, out)
101 }
102 atRunningCap(t, fx, 3)
103 status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/"+fx.sqid+"/kass/restore", map[string]string{}, token)
104 if status != http.StatusConflict || out["code"] != "cap" {
105 t.Fatalf("CLI restore at the cap = %d %v, want 409 cap", status, out)
106 }
107 c, csrf := signInAs(t, fx, "paul@keymail.dev", "code-cap-restore-web")
108 if st, _, _ := postForm(t, c, fx.u("/trash/restore"), url.Values{"csrf": {csrf}, "app": {"kass"}}); st != http.StatusSeeOther {
109 t.Fatalf("browser restore at the cap = %d, want the flash redirect", st)
110 }
111 if markerOf(t, fx, "kass") == nil {
112 t.Fatal("a refused restore removed the trash marker")
113 }
114}
115
116func TestRunningThingsCapIsExactUnderConcurrentCreates(t *testing.T) {
117 fx := newConsoleFixture(t)
118 acct := atRunningCap(t, fx, 3)
119 raiseRunningCap(t, fx, acct, 1)
120 token := cliToken(t, fx, "paul@keymail.dev", "code-cap-race")
121 var mu sync.Mutex
122 created := 0
123 var wg sync.WaitGroup
124 for i := range 8 {
125 wg.Add(1)
126 go func() {
127 defer wg.Done()
128 status, _ := postJSON(t, fx.ts.URL+"/api/cli/apps/"+fx.sqid+"/new", map[string]string{"name": fmt.Sprintf("race%d", i)}, token)
129 if status == http.StatusOK {
130 mu.Lock()
131 created++
132 mu.Unlock()
133 }
134 }()
135 }
136 wg.Wait()
137 if created != 1 {
138 t.Fatalf("%d creates succeeded for one free slot, want exactly 1", created)
139 }
140}
141
142func TestCreateAppWithoutACatalogUnchanged(t *testing.T) {
143 fx := newConsoleFixture(t)
144 acct, _ := fx.st.AccountBySqid(fx.sqid)
145 for i := range 5 {
146 capsApp(t, fx, acct, fmt.Sprintf("many%d", i))
147 }
148 token := cliToken(t, fx, "paul@keymail.dev", "code-cap-nocat")
149 if status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/"+fx.sqid+"/new", map[string]string{"name": "stillfine"}, token); status != http.StatusOK {
150 t.Fatalf("create with no catalog = %d %v", status, out)
151 }
152}
153
154// sponsorCustomDomains keeps the cap as atRunningCap left it and adds custom
155// domains, so a first-route attach meets the running-things cap rather than
156// the custom-domain gate.
157func sponsorCustomDomains(t *testing.T, fx *consoleFixture, acct *Account) {
158 t.Helper()
159 row, err := fx.st.AccountPlan(acct.ID)
160 if err != nil || row == nil || row.Sponsored == nil {
161 t.Fatalf("plan row: %+v, %v", row, err)
162 }
163 sp := *row.Sponsored
164 sp.CustomDomains = true
165 if _, err := fx.st.SetAccountPlan(acct.ID, "free", nil, PlanSourceSponsored, &sp, "test", "ops@example.com", planNow); err != nil {
166 t.Fatal(err)
167 }
168}
169
170func TestInstanceCreateRefusedAtTheRunningThingsCap(t *testing.T) {
171 fx, token := instanceFixture(t, "code-cap-instance")
172 enableInstances(t, fx, "titogo", titoManifest())
173 atRunningCap(t, fx, 3)
174 status, _, out := rawReq(t, http.MethodPost, instancesURL(fx, "bab", "titogo"), token, `{"host":"acme.go.tito.io","region":"ie"}`)
175 if status != http.StatusConflict || !strings.Contains(out, `"code":"cap"`) {
176 t.Fatalf("instance create at the cap = %d %s, want 409 cap", status, out)
177 }
178 if _, err := provision.GetRecord(context.Background(), fx.rel, "bab", "titogo", "acme.go.tito.io"); err == nil {
179 t.Fatal("an instance record was written past the cap")
180 }
181}
182
183func TestFirstRouteAttachRefusedAtTheRunningThingsCap(t *testing.T) {
184 fx, token := instanceFixture(t, "code-cap-firstroute")
185 shipVersion(t, fx, "titogo", "v1")
186 seedChannel(t, fx, "titogo", "canary/live", "v1")
187 acct := atRunningCap(t, fx, 3)
188 sponsorCustomDomains(t, fx, acct)
189 status, _, out := rawReq(t, http.MethodPost, domainsURL(fx, "bab", "titogo"), token, `{"host":"shop.example.com","kind":"static"}`)
190 if status != http.StatusConflict {
191 t.Fatalf("first-route attach at the cap = %d %s, want 409", status, out)
192 }
193 if _, err := provision.GetRecord(context.Background(), fx.rel, "bab", "titogo", "shop.example.com"); err == nil {
194 t.Fatal("a first-route record was written past the cap")
195 }
196}
197
198func TestSidecarCreateRefusedAtTheSidecarCap(t *testing.T) {
199 fx, token := instanceFixture(t, "code-cap-sidecar")
200 enableSidecars(t, fx, "titogo", titoSidecarManifest())
201 fx.srv.cfg.PlanCatalog = examplePlanCatalog(t)
202 acct, _ := fx.st.AccountBySqid("bab")
203 capsSidecar(t, fx, acct, "titogo", "first")
204 status, _, out := rawReq(t, http.MethodPost, sidecarsURL(fx, "bab", "titogo"), token, `{"name":"bot","region":"ie"}`)
205 if status != http.StatusConflict || !strings.Contains(out, `"code":"cap"`) {
206 t.Fatalf("second sidecar on free = %d %s, want 409 cap", status, out)
207 }
208 if _, _, err := fx.rel.Get(context.Background(), provision.SidecarRecordKey("bab", "titogo", "bot")); err == nil {
209 t.Fatal("a sidecar record was written past the cap")
210 }
211}
212
213func TestInstanceAndSidecarCreateWithoutACatalogUnchanged(t *testing.T) {
214 fx, token := instanceFixture(t, "code-cap-inst-nocat")
215 enableInstances(t, fx, "titogo", titoManifest())
216 if status, _, out := rawReq(t, http.MethodPost, instancesURL(fx, "bab", "titogo"), token, `{"host":"acme.go.tito.io","region":"ie"}`); status != http.StatusCreated {
217 t.Fatalf("instance create with no catalog = %d %s", status, out)
218 }
219 enableSidecars(t, fx, "titogo", titoSidecarManifest())
220 acct, _ := fx.st.AccountBySqid("bab")
221 capsSidecar(t, fx, acct, "titogo", "first")
222 if status, _, out := rawReq(t, http.MethodPost, sidecarsURL(fx, "bab", "titogo"), token, `{"name":"bot","region":"ie"}`); status != http.StatusCreated {
223 t.Fatalf("sidecar create with no catalog = %d %s", status, out)
224 }
225}
226
227func accountCount(t *testing.T, fx *consoleFixture) int {
228 t.Helper()
229 all, err := fx.st.Accounts()
230 if err != nil {
231 t.Fatal(err)
232 }
233 return len(all)
234}
235
236func TestNewAccountRefusedForASecondFreeAccount(t *testing.T) {
237 fx := newConsoleFixture(t)
238 fx.srv.cfg.PlanCatalog = examplePlanCatalog(t)
239 before := accountCount(t, fx)
240 token := cliToken(t, fx, "paul@keymail.dev", "code-cap-acct")
241 status, out := postJSON(t, fx.ts.URL+"/auth/cli/accounts", map[string]string{"name": "second"}, token)
242 if status != http.StatusConflict || out["code"] != "cap" {
243 t.Fatalf("CLI second free account = %d %v, want 409 cap", status, out)
244 }
245 c := signInPaul(t, fx)
246 st, loc, _ := postForm(t, c, fx.ts.URL+"/accounts/new", url.Values{"csrf": {sessionToken(t, fx, c)}, "name": {"thirdtry"}})
247 if st != http.StatusSeeOther || loc != "/accounts/new" {
248 t.Fatalf("browser second free account = %d -> %q, want the flash redirect back to /accounts/new", st, loc)
249 }
250 if after := accountCount(t, fx); after != before {
251 t.Fatalf("accounts %d -> %d: one was minted past the cap", before, after)
252 }
253}
254
255func TestFirstAccountIsNeverRefused(t *testing.T) {
256 fx := newConsoleFixture(t)
257 fx.srv.cfg.PlanCatalog = examplePlanCatalog(t)
258 signInAs(t, fx, "newcomer@keymail.dev", "code-cap-first")
259 accts, err := fx.st.AccountsFor("newcomer@keymail.dev")
260 if err != nil || len(accts) != 1 {
261 t.Fatalf("first sign-in with plans on: %d accounts, %v; want the auto-minted one", len(accts), err)
262 }
263}
264
265func TestNewAccountAllowedWhenTheOtherIsPaid(t *testing.T) {
266 fx := newConsoleFixture(t)
267 fx.srv.cfg.PlanCatalog = examplePlanCatalog(t)
268 ops, _ := fx.st.AccountBySqid(fx.sqid)
269 if _, err := fx.st.SetAccountPlan(ops.ID, "personal", nil, PlanSourceOperator, nil, "r", "ops@example.com", planNow); err != nil {
270 t.Fatal(err)
271 }
272 token := cliToken(t, fx, "paul@keymail.dev", "code-cap-acct-paid")
273 if status, out := postJSON(t, fx.ts.URL+"/auth/cli/accounts", map[string]string{"name": "second"}, token); status != http.StatusOK {
274 t.Fatalf("a first free account beside a paid one = %d %v", status, out)
275 }
276}
277
278func TestNewAccountWithoutACatalogUnchanged(t *testing.T) {
279 fx := newConsoleFixture(t)
280 token := cliToken(t, fx, "paul@keymail.dev", "code-cap-acct-nocat")
281 for _, name := range []string{"one", "two"} {
282 if status, out := postJSON(t, fx.ts.URL+"/auth/cli/accounts", map[string]string{"name": name}, token); status != http.StatusOK {
283 t.Fatalf("account %s with no catalog = %d %v", name, status, out)
284 }
285 }
286}
287
288func TestFleetCreateRefusedOnAPlanWithoutFleets(t *testing.T) {
289 fx, token := fleetFixture(t, "code-cap-fleet")
290 fx.srv.cfg.PlanCatalog = examplePlanCatalog(t)
291 status, out := postJSON(t, fleetsURL(fx, fx.sqid)+"/new", map[string]string{"name": "acme", "bucket": "bkt-1"}, token)
292 if status != http.StatusConflict || out["code"] != "cap" {
293 t.Fatalf("fleet create on free = %d %v, want 409 cap", status, out)
294 }
295}
296
297func TestCustomDomainRefusedOnAFreePlan(t *testing.T) {
298 fx, member := instanceFixture(t, "code-cap-domain")
299 shipVersion(t, fx, "titogo", "v1")
300 seedChannel(t, fx, "titogo", "canary/live", "v1")
301 fx.srv.cfg.PlanCatalog = examplePlanCatalog(t)
302 for name, body := range map[string]string{
303 "alias": `{"host":"tickets.example.com","route":"go.tito.io"}`,
304 "first route": `{"host":"fresh.example.com","kind":"static"}`,
305 } {
306 code, _, out := rawReq(t, http.MethodPost, domainsURL(fx, "bab", "titogo"), member, body)
307 if code != http.StatusConflict {
308 t.Errorf("%s on free = %d %s, want 409", name, code, out)
309 }
310 }
311 claimAbsent(t, fx, "tickets.example.com")
312 claimAbsent(t, fx, "fresh.example.com")
313}
314
315func TestFleetsAndDomainsWithoutACatalogUnchanged(t *testing.T) {
316 fx, member := instanceFixture(t, "code-cap-domain-nocat")
317 if code, _, out := rawReq(t, http.MethodPost, domainsURL(fx, "bab", "titogo"), member, `{"host":"tickets.example.com","route":"go.tito.io"}`); code != http.StatusOK {
318 t.Fatalf("alias attach with no catalog = %d %s", code, out)
319 }
320 fx2, token := fleetFixture(t, "code-cap-fleet-nocat")
321 if status, out := postJSON(t, fleetsURL(fx2, fx2.sqid)+"/new", map[string]string{"name": "acme", "bucket": "bkt-1"}, token); status != http.StatusOK {
322 t.Fatalf("fleet create with no catalog = %d %v", status, out)
323 }
324}
325
326// Review Focus 5: an address under the deployment's apps domain is the
327// account's own platform address, not a custom domain, so free plans keep it.
328func TestPlatformHostnamesStayFreeOnFreePlans(t *testing.T) {
329 fx, member := instanceFixture(t, "code-cap-platform")
330 fx.srv.cfg.AppsDomain = "apps.example.com"
331 fx.srv.cfg.PlanCatalog = examplePlanCatalog(t)
332 acct, _ := fx.st.AccountBySqid("bab")
333 if fx.srv.customDomain("shop.bab.apps.example.com", acct.Sqid) {
334 t.Fatal("an address under the apps domain was treated as a custom domain")
335 }
336 if !fx.srv.customDomain("shop.example.com", acct.Sqid) {
337 t.Fatal("a customer domain was not treated as a custom domain")
338 }
339 if ref := fx.srv.customDomainRefusal(acct, "shop.bab.apps.example.com"); ref != nil {
340 t.Fatalf("free plan refused its own platform address: %+v", ref)
341 }
342 _ = member
343}
344
345// Astra plans-3a finding 2: concurrent creates by one person who owns only
346// paid accounts must not mint more free accounts than the allowance.
347func TestFreeAccountCapIsExactUnderConcurrentCreates(t *testing.T) {
348 fx := newConsoleFixture(t)
349 fx.srv.cfg.PlanCatalog = examplePlanCatalog(t)
350 ops, _ := fx.st.AccountBySqid(fx.sqid)
351 if _, err := fx.st.SetAccountPlan(ops.ID, "personal", nil, PlanSourceOperator, nil, "r", "ops@example.com", planNow); err != nil {
352 t.Fatal(err)
353 }
354 token := cliToken(t, fx, "paul@keymail.dev", "code-cap-acct-race")
355 var mu sync.Mutex
356 created := 0
357 var wg sync.WaitGroup
358 for i := range 6 {
359 wg.Add(1)
360 go func() {
361 defer wg.Done()
362 status, _ := postJSON(t, fx.ts.URL+"/auth/cli/accounts", map[string]string{"name": fmt.Sprintf("race%d", i)}, token)
363 if status == http.StatusOK {
364 mu.Lock()
365 created++
366 mu.Unlock()
367 }
368 }()
369 }
370 wg.Wait()
371 if created != 1 {
372 t.Fatalf("%d free accounts minted concurrently, want exactly 1", created)
373 }
374}
375