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
3// The account-level Fleet tab (accountfleet.go): GET /a/{sqid}/fleet, a
4// MEMBER-readable page that answers "where does this account's work run?" in
5// two halves — the shared platform fleet, presented purely from the public
6// status/fleet.json aggregate, and the account's own fleets from the
7// registry. The properties that matter, in the order they'd hurt if lost:
8// the page never 500s because one of its three independent reads had a bad
9// moment (the loadFleetPage/overview.go posture), it never shows another
10// account's fleets, and the shared card's "Active" claim tracks placement —
11// an account whose every route sits on its own fleet must not be told its
12// apps run on the shared one.
13
14import (
15 "context"
16 "errors"
17 "net/http"
18 "path/filepath"
19 "strings"
20 "testing"
21 "time"
22
23 "github.com/carlosframework/platform/internal/registry"
24 "github.com/carlosframework/platform/internal/status"
25)
26
27// seedSharedBoxes publishes a status/fleet.json carrying one box per region
28// name given, through the same BuildFleet/PublishFleet path production uses
29// (seedFleetJSON's shape, with the regions the tab renders as chips).
30func seedSharedBoxes(t *testing.T, fx *consoleFixture, regions ...string) {
31 t.Helper()
32 ctx := context.Background()
33 for i, region := range regions {
34 if err := status.WriteBox(ctx, fx.rel, status.Box{
35 GeneratedAt: time.Now(),
36 Label: []string{"ada-stargazing", "grace-moonlit", "alan-seaside"}[i],
37 Region: region,
38 }); err != nil {
39 t.Fatal(err)
40 }
41 }
42 reg, err := registry.Open(fx.regPath)
43 if err != nil {
44 t.Fatal(err)
45 }
46 routes, err := reg.List()
47 if err != nil {
48 t.Fatal(err)
49 }
50 f, err := status.BuildFleet(ctx, fx.rel, routes, time.Now(), 5*time.Minute)
51 if err != nil {
52 t.Fatal(err)
53 }
54 if err := status.PublishFleet(ctx, fx.rel, f); err != nil {
55 t.Fatal(err)
56 }
57}
58
59// fixtureAccount is the account value the assemble-level tests pass in — the
60// same {ID, Sqid} pair the gated handler would hand it.
61func fixtureAccount(fx *consoleFixture) *Account {
62 return &Account{ID: OpsAccountID, Sqid: fx.sqid, Name: "ops"}
63}
64
65// TestAccountFleetTabMemberReadable: the tab is a READ, so a plain member
66// sees it exactly as the owner does; a non-member gets the account gate's
67// byte-identical 404, the same answer an unrouted URL gives.
68func TestAccountFleetTabMemberReadable(t *testing.T) {
69 fx, token := fleetFixture(t, "code-acctfleet-gate")
70 if st, body := postJSON(t, fleetsURL(fx, fx.sqid)+"/new",
71 map[string]string{"name": "acme", "bucket": "acme-data"}, token); st != http.StatusOK {
72 t.Fatalf("create fleet = %d %v", st, body)
73 }
74 if err := fx.st.AddMember(OpsAccountID, "sam@keymail.dev", RoleMember, time.Now()); err != nil {
75 t.Fatal(err)
76 }
77
78 owner := newBrowser(t)
79 signUp(t, fx, owner, "code-acctfleet-owner")
80 st, _, body := get(t, owner, fx.u("/fleet"))
81 if st != http.StatusOK {
82 t.Fatalf("owner GET /fleet = %d, want 200:\n%s", st, body)
83 }
84 for _, want := range []string{"Shared fleet", "Your fleets", "acme", "Bring your own fleet"} {
85 if !strings.Contains(body, want) {
86 t.Fatalf("owner fleet tab missing %q in:\n%s", want, body)
87 }
88 }
89
90 fx.setUser("sam@keymail.dev")
91 member := newBrowser(t)
92 signUp(t, fx, member, "code-acctfleet-member")
93 st, _, memberBody := get(t, member, fx.u("/fleet"))
94 if st != http.StatusOK {
95 t.Fatalf("member GET /fleet = %d, want 200:\n%s", st, memberBody)
96 }
97 for _, want := range []string{"Shared fleet", "acme"} {
98 if !strings.Contains(memberBody, want) {
99 t.Fatalf("member fleet tab missing %q in:\n%s", want, memberBody)
100 }
101 }
102
103 fx.setUser("bob@keymail.dev")
104 stranger := newBrowser(t)
105 signUp(t, fx, stranger, "code-acctfleet-bob")
106 nonMemberStatus, _, nonMemberBody := get(t, stranger, fx.u("/fleet"))
107 ghostStatus, _, ghostBody := get(t, stranger, fx.ts.URL+"/definitely-not-a-page")
108 if nonMemberStatus != http.StatusNotFound || nonMemberStatus != ghostStatus {
109 t.Fatalf("non-member fleet tab = %d, unrouted page = %d; want both 404", nonMemberStatus, ghostStatus)
110 }
111 if nonMemberBody != ghostBody {
112 t.Fatalf("non-member fleet tab body differs from an unrouted page's:\n%q\n%q", nonMemberBody, ghostBody)
113 }
114}
115
116// TestAccountFleetTabNavLink: the tab is reachable — every account page
117// carries the Fleet nav entry, and it marks itself current on this one.
118func TestAccountFleetTabNavLink(t *testing.T) {
119 fx := newConsoleFixture(t)
120 c := signInPaul(t, fx)
121
122 // Nav LINKS carry the preferred segment — the account's name, "ops" for
123 // the fixture's built-in account — while forms keep posting to the sqid
124 // path (accountPage: Nav vs Base).
125 st, _, apps := get(t, c, fx.u("/"))
126 if st != http.StatusOK {
127 t.Fatalf("account home = %d:\n%s", st, apps)
128 }
129 if !strings.Contains(apps, `href="/a/ops/fleet"`) {
130 t.Fatalf("account home nav has no Fleet link:\n%s", apps)
131 }
132 st, _, body := get(t, c, fx.u("/fleet"))
133 if st != http.StatusOK {
134 t.Fatalf("GET /fleet = %d:\n%s", st, body)
135 }
136 if !strings.Contains(body, `href="/a/ops/fleet" aria-current="page"`) {
137 t.Fatalf("Fleet tab does not mark its own nav entry current:\n%s", body)
138 }
139}
140
141// TestAccountFleetSharedCardFromFleetJSON: the shared card is a PRESENTATION
142// of the public aggregate — box count and the distinct regions, deduped and
143// sorted, straight off status/fleet.json.
144func TestAccountFleetSharedCardFromFleetJSON(t *testing.T) {
145 fx := newConsoleFixture(t)
146 seedSharedBoxes(t, fx, "UK · London", "EU · Ireland")
147
148 card, _, _, warn := fx.srv.assembleAccountFleet(context.Background(), fixtureAccount(fx))
149 if warn != "" {
150 t.Fatalf("warn = %q, want none on a healthy read", warn)
151 }
152 if card.Boxes != 2 {
153 t.Fatalf("card.Boxes = %d, want 2", card.Boxes)
154 }
155 if len(card.Regions) != 2 || card.Regions[0] != "EU · Ireland" || card.Regions[1] != "UK · London" {
156 t.Fatalf("card.Regions = %v, want both regions, sorted", card.Regions)
157 }
158
159 // And they reach the page.
160 c := signInPaul(t, fx)
161 st, _, body := get(t, c, fx.u("/fleet"))
162 if st != http.StatusOK {
163 t.Fatalf("GET /fleet = %d:\n%s", st, body)
164 }
165 for _, want := range []string{"EU · Ireland", "UK · London", "2 box"} {
166 if !strings.Contains(body, want) {
167 t.Fatalf("fleet tab missing %q in:\n%s", want, body)
168 }
169 }
170}
171
172// TestAccountFleetActiveFlipsWithPlacement: "Active" is a claim about THIS
173// account's routes — true while any of its apps still runs on the shared
174// fleet, false once every one of them is placed on a fleet of its own. An
175// account told "your apps run here" when they do not is the failure this
176// test exists to prevent.
177func TestAccountFleetActiveFlipsWithPlacement(t *testing.T) {
178 fx, token := fleetFixture(t, "code-acctfleet-active")
179 reg := openTestReg(t, fx)
180 if err := reg.Upsert(registry.Route{
181 Host: "kass.example.dev", Kind: "instance", App: "kass", Account: fx.sqid,
182 }); err != nil {
183 t.Fatal(err)
184 }
185
186 acct := fixtureAccount(fx)
187 card, _, _, _ := fx.srv.assembleAccountFleet(context.Background(), acct)
188 if !card.Active || card.PlatformApps != 1 {
189 t.Fatalf("unplaced route: Active=%v PlatformApps=%d, want true/1", card.Active, card.PlatformApps)
190 }
191
192 if st, body := postJSON(t, fleetsURL(fx, fx.sqid)+"/new",
193 map[string]string{"name": "acme", "bucket": "acme-data"}, token); st != http.StatusOK {
194 t.Fatalf("create fleet = %d %v", st, body)
195 }
196 if st, body := postJSON(t, placeURL(fx, "kass"), map[string]string{"target": "fleet/acme"}, token); st != http.StatusOK {
197 t.Fatalf("place = %d %v, want 200", st, body)
198 }
199
200 card, own, _, _ := fx.srv.assembleAccountFleet(context.Background(), acct)
201 if card.Active || card.PlatformApps != 0 {
202 t.Fatalf("every route placed: Active=%v PlatformApps=%d, want false/0", card.Active, card.PlatformApps)
203 }
204 if len(own) != 1 || own[0].Apps != 1 {
205 t.Fatalf("own fleets = %+v, want acme carrying the one placed app", own)
206 }
207}
208
209// TestAccountFleetListsOnlyOwnFleets: fleet names are one flat cross-account
210// namespace (fleets.go's package doc), so the index MUST filter by owner —
211// a fleet belonging to another account must not appear here any more than it
212// resolves on the detail page.
213func TestAccountFleetListsOnlyOwnFleets(t *testing.T) {
214 fx, token := fleetFixture(t, "code-acctfleet-own")
215 reg := openTestReg(t, fx)
216 if _, err := reg.CreateFleet(registry.Fleet{Name: "stranger", Account: "zzz", Bucket: "not-yours"}); err != nil {
217 t.Fatal(err)
218 }
219 for _, name := range []string{"acme", "beta"} {
220 if st, body := postJSON(t, fleetsURL(fx, fx.sqid)+"/new",
221 map[string]string{"name": name, "bucket": name + "-data"}, token); st != http.StatusOK {
222 t.Fatalf("create fleet %s = %d %v", name, st, body)
223 }
224 }
225 if st, body := postJSON(t, fleetsURL(fx, fx.sqid)+"/acme/boxes",
226 map[string]string{"label": "pi-1"}, token); st != http.StatusOK {
227 t.Fatalf("add-box = %d %v", st, body)
228 }
229 writeFleetHeartbeat(t, fx, "acme", status.Box{GeneratedAt: time.Now(), Label: "pi-1"})
230
231 _, own, ownKnown, warn := fx.srv.assembleAccountFleet(context.Background(), fixtureAccount(fx))
232 if warn != "" {
233 t.Fatalf("warn = %q, want none", warn)
234 }
235 if !ownKnown {
236 t.Fatalf("fleetsKnown = false on a healthy registry read")
237 }
238 if len(own) != 2 || own[0].Name != "acme" || own[1].Name != "beta" {
239 t.Fatalf("own fleets = %+v, want exactly acme and beta", own)
240 }
241 if own[0].Boxes != 1 || own[0].Reachable != 1 {
242 t.Fatalf("acme = %+v, want 1 box, 1 reachable", own[0])
243 }
244 if own[1].Boxes != 0 || own[1].Reachable != 0 {
245 t.Fatalf("beta = %+v, want no boxes", own[1])
246 }
247
248 c := signInPaul(t, fx)
249 st, _, body := get(t, c, fx.u("/fleet"))
250 if st != http.StatusOK {
251 t.Fatalf("GET /fleet = %d:\n%s", st, body)
252 }
253 if strings.Contains(body, "stranger") {
254 t.Fatalf("another account's fleet leaked onto the tab:\n%s", body)
255 }
256 for _, want := range []string{`href="/a/ops/fleets/acme"`, `href="/a/ops/fleets/beta"`} {
257 if !strings.Contains(body, want) {
258 t.Fatalf("fleet tab missing link %q in:\n%s", want, body)
259 }
260 }
261}
262
263// TestAccountFleetDegradesWithoutSnapshot: a deployment whose status-lease
264// holder has never published status/fleet.json still serves the tab — the
265// shared card renders its empty state, and the account's own fleets (a
266// completely independent read) are unaffected.
267func TestAccountFleetDegradesWithoutSnapshot(t *testing.T) {
268 fx, token := fleetFixture(t, "code-acctfleet-nosnapshot")
269 if st, body := postJSON(t, fleetsURL(fx, fx.sqid)+"/new",
270 map[string]string{"name": "acme", "bucket": "acme-data"}, token); st != http.StatusOK {
271 t.Fatalf("create fleet = %d %v", st, body)
272 }
273
274 c := signInPaul(t, fx)
275 st, _, body := get(t, c, fx.u("/fleet"))
276 if st != http.StatusOK {
277 t.Fatalf("GET /fleet with no published snapshot = %d, want 200 (degrade, don't 500):\n%s", st, body)
278 }
279 if !strings.Contains(body, "No shared boxes are reporting right now.") {
280 t.Fatalf("missing the shared card's empty state:\n%s", body)
281 }
282 if !strings.Contains(body, "acme") {
283 t.Fatalf("the account's own fleets (an independent read) went missing too:\n%s", body)
284 }
285}
286
287// TestAccountFleetUnreadableRegistryClaimsNothing: one failed registry read
288// takes out BOTH registry-fed sections, and neither may fall through to its
289// zero value and make a positive claim from it — not the card ("none of your
290// apps run here", about routes it never looked at) and not the index ("no
291// fleets of your own yet", about a list it never got). "Could not check" and
292// "checked, and no" are different sentences in both places.
293func TestAccountFleetUnreadableRegistryClaimsNothing(t *testing.T) {
294 fx := newConsoleFixture(t)
295 seedSharedBoxes(t, fx, "EU · Ireland")
296 c := signInPaul(t, fx)
297 // A path whose parent directory does not exist: registry.Open fails, and
298 // a failed open is never cached (openRegistry, view.go).
299 fx.srv.cfg.Registry = filepath.Join(t.TempDir(), "gone", "registry.db")
300
301 st, _, body := get(t, c, fx.u("/fleet"))
302 if st != http.StatusOK {
303 t.Fatalf("GET /fleet with an unreadable registry = %d, want 200 (degrade, don't 500):\n%s", st, body)
304 }
305 if strings.Contains(body, "None of your apps run on the shared fleet") ||
306 strings.Contains(body, "Not used") {
307 t.Fatalf("an unread registry rendered as a positive \"not used\" verdict:\n%s", body)
308 }
309 if !strings.Contains(body, "Whether your apps run here could not be read just now.") {
310 t.Fatalf("missing the honest unknown-placement state:\n%s", body)
311 }
312 if strings.Contains(body, "No fleets of your own yet") {
313 t.Fatalf("an unread registry rendered as a positive \"you have no fleets\" verdict:\n%s", body)
314 }
315 if !strings.Contains(body, "Your fleets could not be read just now.") {
316 t.Fatalf("missing the honest unknown-fleets state:\n%s", body)
317 }
318 // The half that DID read is still shown.
319 if !strings.Contains(body, "EU · Ireland") {
320 t.Fatalf("the shared card's own (successful) read went missing:\n%s", body)
321 }
322}
323
324// TestAccountFleetEmptyStatesAreClaimsOnlyWhenEarned is the positive half of
325// the test above: a HEALTHY registry that genuinely holds no fleets for this
326// account does get the flat "none" wording — the honest-unknown copy must not
327// have swallowed the ordinary empty state along with the lie.
328func TestAccountFleetEmptyStatesAreClaimsOnlyWhenEarned(t *testing.T) {
329 fx := newConsoleFixture(t)
330 c := signInPaul(t, fx)
331
332 st, _, body := get(t, c, fx.u("/fleet"))
333 if st != http.StatusOK {
334 t.Fatalf("GET /fleet = %d:\n%s", st, body)
335 }
336 if !strings.Contains(body, "No fleets of your own yet.") {
337 t.Fatalf("an account with no fleets and a healthy registry lost its empty state:\n%s", body)
338 }
339 if strings.Contains(body, "could not be read just now") {
340 t.Fatalf("a healthy read rendered as unknown:\n%s", body)
341 }
342}
343
344// TestFleetBYOPageMemberReadable: the BYO instructions page is member-
345// readable, the same gate as the Fleet tab itself — a member deciding
346// whether to bring a box needs to read the recipe as much as the owner
347// does. It must carry the exact commands and env var an operator will
348// actually type/set, and the Hardening section the checklist lives under.
349func TestFleetBYOPageMemberReadable(t *testing.T) {
350 fx := newConsoleFixture(t)
351 c := signInPaul(t, fx)
352
353 st, _, body := get(t, c, fx.u("/fleet/byo"))
354 if st != http.StatusOK {
355 t.Fatalf("GET /fleet/byo = %d, want 200:\n%s", st, body)
356 }
357 for _, want := range []string{"carlos fleets create", "CARLOS_TUNNEL_NAME", "Hardening"} {
358 if !strings.Contains(body, want) {
359 t.Fatalf("BYO page missing %q in:\n%s", want, body)
360 }
361 }
362 // The primary env contract is bucketless: no platform-bucket var or
363 // credential belongs there (the box reaches releases/heartbeats through
364 // the console's edge with its tunnel token). CARLOS_DEPLOYMENT_BUCKET
365 // still appears, but only inside the demoted legacy-direct-S3 note.
366 if !strings.Contains(body, "Legacy direct-S3") {
367 t.Fatalf("BYO page missing the legacy direct-S3 note in:\n%s", body)
368 }
369}
370
371// TestAccountFleetDegradesOnStoreFailure: a bucket having a moment must cost
372// the shared card, never the page — the fleets index is registry-side and
373// keeps rendering, with a flash that says which half is unknown.
374func TestAccountFleetDegradesOnStoreFailure(t *testing.T) {
375 fx, token := fleetFixture(t, "code-acctfleet-storefail")
376 if st, body := postJSON(t, fleetsURL(fx, fx.sqid)+"/new",
377 map[string]string{"name": "acme", "bucket": "acme-data"}, token); st != http.StatusOK {
378 t.Fatalf("create fleet = %d %v", st, body)
379 }
380 seedSharedBoxes(t, fx, "EU · Ireland")
381
382 c := signInPaul(t, fx)
383 // Only the aggregate itself is unreadable: the per-fleet heartbeat reads
384 // still work, so a degraded shared card is the ONLY thing this costs.
385 fx.srv.cfg.Releases = errOnKeyStore{Store: fx.rel,
386 key: "status/fleet.json", err: errors.New("bucket having a moment")}
387
388 st, _, body := get(t, c, fx.u("/fleet"))
389 if st != http.StatusOK {
390 t.Fatalf("GET /fleet with an unreadable store = %d, want 200 (degrade, don't 500):\n%s", st, body)
391 }
392 if !strings.Contains(body, "acme") {
393 t.Fatalf("the registry-side fleets index went missing on a BUCKET failure:\n%s", body)
394 }
395 if !strings.Contains(body, "Could not read the shared fleet") {
396 t.Fatalf("a failed shared-fleet read rendered as if nothing were wrong:\n%s", body)
397 }
398 if strings.Contains(body, "EU · Ireland") {
399 t.Fatalf("a failed read still showed the last-known regions:\n%s", body)
400 }
401}
402