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 /fleet/architecture page (archpage.go): its gate, its honest empty
4// state, and its refresh door. What these tests own is the DOOR's shape —
5// who may see the page at all, what it says when it has never had an
6// inventory, and that the refresh button cannot be driven from another
7// site. The graph itself is proven in archgraph_test.go and the drawing in
8// archlayout_test.go.
9
10import (
11 "context"
12 "errors"
13 "net/http"
14 "net/url"
15 "path/filepath"
16 "regexp"
17 "strconv"
18 "strings"
19 "testing"
20 "time"
21
22 "github.com/carlosframework/platform/internal/economics"
23 "github.com/carlosframework/platform/internal/infraview"
24 "github.com/carlosframework/platform/internal/registry"
25 "github.com/carlosframework/platform/internal/release"
26 "github.com/carlosframework/platform/internal/status"
27)
28
29// TestFleetOperatorOnly's shape against /fleet/architecture: signed out,
30// signed in but not an operator, and a revoked operator all get the exact
31// 404 an unrouted path gets — byte-identical, so the route's existence is
32// not probeable.
33func TestArchitectureOperatorOnly(t *testing.T) {
34 fx := newConsoleFixture(t)
35
36 out := newBrowser(t)
37 ghostStatus, _, ghost := get(t, out, fx.ts.URL+"/this-route-does-not-exist")
38 if ghostStatus != http.StatusNotFound {
39 t.Fatalf("sanity: missing route = %d", ghostStatus)
40 }
41 if st, _, body := get(t, out, fx.ts.URL+"/fleet/architecture"); st != http.StatusNotFound || body != ghost {
42 t.Fatalf("signed-out = %d %q, want the same 404 as a missing route", st, body)
43 }
44 owner := signInPaul(t, fx)
45 if st, _, body := get(t, owner, fx.ts.URL+"/fleet/architecture"); st != http.StatusNotFound || body != ghost {
46 t.Fatalf("non-operator = %d %q, want the same 404", st, body)
47 }
48 if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil {
49 t.Fatal(err)
50 }
51 st, _, body := get(t, owner, fx.ts.URL+"/fleet/architecture")
52 if st != 200 || !strings.Contains(body, "archsvg") {
53 t.Fatalf("operator view = %d, want 200 with the diagram:\n%s", st, body)
54 }
55 if err := fx.st.SetOperator("paul@keymail.dev", false); err != nil {
56 t.Fatal(err)
57 }
58 if st, _, body := get(t, owner, fx.ts.URL+"/fleet/architecture"); st != http.StatusNotFound || body != ghost {
59 t.Fatalf("revoked = %d %q, want the same 404", st, body)
60 }
61}
62
63// The refresh door's gate is the page's, and a wrong-method request is
64// byte-identical to a missing route: a "POST /path" registration would
65// answer a GET with 405 + Allow, which leaks that the route exists.
66func TestArchitectureRefreshOperatorOnly(t *testing.T) {
67 fx := newConsoleFixture(t)
68
69 out := newBrowser(t)
70 ghostStatus, _, ghost := get(t, out, fx.ts.URL+"/this-route-does-not-exist")
71 if ghostStatus != http.StatusNotFound {
72 t.Fatalf("sanity: missing route = %d", ghostStatus)
73 }
74 if st, _, body := postForm(t, out, fx.ts.URL+"/fleet/architecture/refresh", url.Values{}); st != http.StatusNotFound || body != ghost {
75 t.Fatalf("signed-out refresh = %d %q, want the same 404 as a missing route", st, body)
76 }
77 c := signInPaul(t, fx)
78 if st, _, body := postForm(t, c, fx.ts.URL+"/fleet/architecture/refresh", url.Values{}); st != http.StatusNotFound || body != ghost {
79 t.Fatalf("non-operator refresh = %d %q, want the same 404", st, body)
80 }
81 if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil {
82 t.Fatal(err)
83 }
84 // A GET on the door is a missing route too, for the same reason.
85 if st, _, body := get(t, c, fx.ts.URL+"/fleet/architecture/refresh"); st != http.StatusNotFound || body != ghost {
86 t.Fatalf("GET on the refresh door = %d %q, want the same 404", st, body)
87 }
88}
89
90// With no inventory ever taken, the page still renders and says WHICH of
91// the three states it is in — never a confident "no resources".
92func TestArchitecturePageWithoutInventory(t *testing.T) {
93 fx := newConsoleFixture(t)
94 c := signInPaul(t, fx)
95 if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil {
96 t.Fatal(err)
97 }
98 st, _, body := get(t, c, fx.ts.URL+"/fleet/architecture")
99 if st != 200 {
100 t.Fatalf("= %d, want 200", st)
101 }
102 if !strings.Contains(body, "No inventory has been taken yet") {
103 t.Errorf("page must name the never-taken state:\n%s", body)
104 }
105 if strings.Contains(body, "no resources") {
106 t.Errorf("page must not claim an empty estate it never read")
107 }
108}
109
110// A deployment whose inventory grant was refused says so in those words —
111// the second of the three states, and the one that must never be rendered
112// as "nothing here".
113func TestArchitecturePageUngranted(t *testing.T) {
114 fx := newConsoleFixture(t)
115 c := signInPaul(t, fx)
116 if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil {
117 t.Fatal(err)
118 }
119 fx.srv.infraDisabled.Store(true)
120
121 st, _, body := get(t, c, fx.ts.URL+"/fleet/architecture")
122 if st != 200 {
123 t.Fatalf("= %d, want 200", st)
124 }
125 if !strings.Contains(body, "no infrastructure read grant") {
126 t.Errorf("page must name the ungranted state:\n%s", body)
127 }
128 if strings.Contains(body, "No inventory has been taken yet") {
129 t.Errorf("ungranted must not be reported as never-taken")
130 }
131}
132
133// The refresh door: CSRF-checked, single-flighted, and it writes an
134// inventory the page then reads back.
135func TestArchitectureRefreshDoor(t *testing.T) {
136 fx := newConsoleFixture(t)
137 c := signInPaul(t, fx)
138 if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil {
139 t.Fatal(err)
140 }
141 called := 0
142 fx.srv.inventory = func(ctx context.Context) (infraview.Inventory, error) {
143 called++
144 return infraview.Inventory{Provider: "aws", Account: "123456789012", TakenAt: time.Now().UTC()}, nil
145 }
146
147 // No CSRF token: refused, and the pass must not have run.
148 st, _, _ := postForm(t, c, fx.ts.URL+"/fleet/architecture/refresh", url.Values{})
149 if st != http.StatusForbidden {
150 t.Fatalf("no-CSRF refresh = %d, want 403", st)
151 }
152 if called != 0 {
153 t.Fatalf("a refused refresh must not run the pass")
154 }
155
156 // With the token: runs once and redirects back to the page.
157 csrf := sessionToken(t, fx, c)
158 st, loc, body := postForm(t, c, fx.ts.URL+"/fleet/architecture/refresh", url.Values{"csrf": {csrf}})
159 if st != http.StatusSeeOther || loc != "/fleet/architecture" {
160 t.Fatalf("refresh = %d -> %q, want 303 back to the page:\n%s", st, loc, body)
161 }
162 if called != 1 {
163 t.Fatalf("pass ran %d times, want 1", called)
164 }
165
166 // And the page now reports the estate rather than one of the three
167 // missing states.
168 st, _, body = get(t, c, fx.ts.URL+"/fleet/architecture")
169 if st != 200 || !strings.Contains(body, "123456789012") {
170 t.Fatalf("page after refresh = %d, want the inventoried account:\n%s", st, body)
171 }
172 if strings.Contains(body, "No inventory has been taken yet") {
173 t.Errorf("page still claims no inventory after a successful refresh")
174 }
175}
176
177// The refresh door does NOT clear the auth latch: a deployment with no IAM
178// grant gets the same honest answer however many times the button is
179// pressed, rather than a fresh AWS attempt per click.
180func TestArchitectureRefreshKeepsLatch(t *testing.T) {
181 fx := newConsoleFixture(t)
182 c := signInPaul(t, fx)
183 if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil {
184 t.Fatal(err)
185 }
186 called := 0
187 fx.srv.inventory = func(ctx context.Context) (infraview.Inventory, error) {
188 called++
189 return infraview.Inventory{}, nil
190 }
191 fx.srv.infraDisabled.Store(true)
192
193 csrf := sessionToken(t, fx, c)
194 st, loc, body := postForm(t, c, fx.ts.URL+"/fleet/architecture/refresh", url.Values{"csrf": {csrf}})
195 if st != http.StatusSeeOther || loc != "/fleet/architecture" {
196 t.Fatalf("refresh = %d -> %q, want 303 back to the page:\n%s", st, loc, body)
197 }
198 if called != 0 {
199 t.Fatalf("a latched-off deployment must not call the provider (called %d)", called)
200 }
201 if !fx.srv.infraDisabled.Load() {
202 t.Errorf("the refresh door cleared the auth latch")
203 }
204 if fx.srv.infraRefreshing.Load() {
205 t.Errorf("the single-flight latch was left set after the door returned")
206 }
207}
208
209// The deployment bucket reaches the graph through Config, not the
210// environment: a console configured with one draws the box -> bucket edge
211// the spec mandates.
212func TestArchitectureDrawsConfiguredBucket(t *testing.T) {
213 fx := newConsoleFixture(t)
214 c := signInPaul(t, fx)
215 if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil {
216 t.Fatal(err)
217 }
218 fx.srv.cfg.Bucket = "carlos-deploy-fixture"
219
220 st, _, body := get(t, c, fx.ts.URL+"/fleet/architecture")
221 if st != 200 {
222 t.Fatalf("= %d, want 200", st)
223 }
224 if !strings.Contains(body, "carlos-deploy-fixture") {
225 t.Errorf("the configured deployment bucket is not drawn:\n%s", body)
226 }
227}
228
229// The page is two estates, both headed and both drawn — including on a
230// deployment where the second one is empty, which is what most accounts
231// are. A page that dropped the second heading when it had nothing under it
232// would be the page quietly deciding the account holds nothing else, which
233// is a claim rather than a read.
234func TestArchitectureDrawsBothEstates(t *testing.T) {
235 fx := newConsoleFixture(t)
236 c := signInPaul(t, fx)
237 if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil {
238 t.Fatal(err)
239 }
240 st, _, body := get(t, c, fx.ts.URL+"/fleet/architecture")
241 if st != 200 {
242 t.Fatalf("= %d, want 200", st)
243 }
244 if !strings.Contains(body, "<h2>CARLOS</h2>") {
245 t.Errorf("the CARLOS estate is not headed:\n%s", body)
246 }
247 if !strings.Contains(body, "<h2>Also in this account</h2>") {
248 t.Errorf("the second estate is not headed:\n%s", body)
249 }
250 // Estate-neutral wording: this console serves carloku, keymail and
251 // woodstar too, and "legacy" describes only Tito's own account.
252 if strings.Contains(body, "legacy") || strings.Contains(body, "Legacy") {
253 t.Errorf("the second bank must not be named for one deployment's history:\n%s", body)
254 }
255 // One diagram per REGION BAND, each under its own heading — not one per
256 // estate. That is what makes the page scroll down instead of across, so
257 // a page that had quietly gone back to one picture per estate would draw
258 // the same content and be the bug this shape exists to fix.
259 bands := strings.Count(body, `<h4 class="archband">`)
260 if bands < 2 {
261 t.Fatalf("page draws %d bands — a page back to one picture per estate is the bug this shape exists to fix:\n%s", bands, body)
262 }
263 if n := strings.Count(body, `class="archsvg"`); n != bands {
264 t.Errorf("page draws %d diagrams under %d band headings — every band has exactly one picture", n, bands)
265 }
266 // Every drawn picture fits the console's 64rem main column, which is the
267 // whole user-visible point: the page grows downwards, never sideways.
268 for _, w := range regexp.MustCompile(`<svg class="archsvg" width="(\d+)"`).FindAllStringSubmatch(body, -1) {
269 if px, _ := strconv.Atoi(w[1]); px > 1024 {
270 t.Errorf("a band is %dpx wide — the page would scroll sideways", px)
271 }
272 }
273}
274
275// A read that failed is SAID on the page. An empty Boxes section with
276// nothing beside it would read as "this deployment has no boxes" — the one
277// claim a page whose registry read just failed must not make.
278func TestArchitectureNamesAFailedRead(t *testing.T) {
279 fx := newConsoleFixture(t)
280 c := signInPaul(t, fx)
281 if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil {
282 t.Fatal(err)
283 }
284 fx.srv.cfg.Registry = filepath.Join(t.TempDir(), "no-such-dir", "registry.db")
285
286 st, _, body := get(t, c, fx.ts.URL+"/fleet/architecture")
287 if st != 200 {
288 t.Fatalf("= %d, want 200 — a failed registry read costs its section, not the page", st)
289 }
290 if !strings.Contains(body, "Reads that failed") || !strings.Contains(body, "route registry could not be opened") {
291 t.Errorf("the failed registry read is not named on the page:\n%s", body)
292 }
293}
294
295// A POST to the PAGE is the same 404 an unrouted path gives. A
296// "GET /fleet/architecture" mux pattern would answer 405 + Allow, which
297// distinguishes this route from a missing one to anyone who guesses the URL
298// — the same hole the refresh door is registered bare to avoid.
299func TestArchitecturePageRefusesOtherMethods(t *testing.T) {
300 fx := newConsoleFixture(t)
301 c := signInPaul(t, fx)
302 if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil {
303 t.Fatal(err)
304 }
305 _, _, ghost := postForm(t, newBrowser(t), fx.ts.URL+"/this-route-does-not-exist", url.Values{})
306
307 st, _, body := postForm(t, c, fx.ts.URL+"/fleet/architecture", url.Values{})
308 if st != http.StatusNotFound || body != ghost {
309 t.Fatalf("POST on the page = %d %q, want the same 404 as a missing route", st, body)
310 }
311}
312
313// archBrokenFleetStore fails exactly the one read the box list comes from,
314// leaving every other read on the page working — the shape of a real bad
315// moment, not a dead bucket.
316type archBrokenFleetStore struct{ release.Store }
317
318func (s archBrokenFleetStore) Get(ctx context.Context, key string) ([]byte, string, error) {
319 if key == "status/fleet.json" {
320 return nil, "", errors.New("bucket having a bad moment")
321 }
322 return s.Store.Get(ctx, key)
323}
324
325// An empty box list has two causes and the page must not confuse them. With
326// the fleet read broken, "No box has reported" would be a confident negative
327// claim about an estate nobody managed to look at.
328func TestArchitectureUnreadBoxesIsNotAnEmptyEstate(t *testing.T) {
329 fx := newConsoleFixture(t)
330 c := signInPaul(t, fx)
331 if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil {
332 t.Fatal(err)
333 }
334
335 // First, the honest empty state: nothing broken, no box reporting.
336 if st, _, body := get(t, c, fx.ts.URL+"/fleet/architecture"); st != 200 ||
337 !strings.Contains(body, "No box has reported to this deployment yet") {
338 t.Fatalf("a genuinely empty fleet = %d, want the empty-state sentence:\n%s", st, body)
339 }
340
341 fx.srv.cfg.Releases = archBrokenFleetStore{fx.rel}
342 st, _, body := get(t, c, fx.ts.URL+"/fleet/architecture")
343 if st != 200 {
344 t.Fatalf("= %d, want 200 — a failed fleet read costs its section, not the page", st)
345 }
346 if strings.Contains(body, "No box has reported to this deployment yet") {
347 t.Errorf("the page claims an empty estate it failed to read:\n%s", body)
348 }
349 if !strings.Contains(body, "The box list could not be read") {
350 t.Errorf("the page must say the box list is unread:\n%s", body)
351 }
352 if !strings.Contains(body, "published fleet status could not be read") {
353 t.Errorf("the failed read must also be named at the foot:\n%s", body)
354 }
355}
356
357// A box card with real content in it: what the estate says the machine is,
358// what the rate card says it costs, the apps and counts CARLOS reports, and
359// the drift chip where the two disagree. Without this every field inside
360// {{range .Boxes}} is unexercised — the fixture's fleet is empty.
361func TestArchitectureBoxCardShowsEstateAndDrift(t *testing.T) {
362 fx := newConsoleFixture(t)
363 c := signInPaul(t, fx)
364 if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil {
365 t.Fatal(err)
366 }
367 ctx := context.Background()
368
369 // The box CARLOS reports: two static routes, one resident, running a
370 // known build, carrying the fixture's own kass route.
371 if err := status.WriteBox(ctx, fx.rel, status.Box{
372 GeneratedAt: time.Now(),
373 Label: "ada-stargazing",
374 Region: "eu-west-1",
375 Routes: status.RouteCounts{Static: 2, Resident: 1},
376 Metrics: &status.BoxMetrics{
377 Release: "v9.9.9-test",
378 RouteVersions: map[string]string{"kass.carlosapp.dev": "abc123"},
379 },
380 }); err != nil {
381 t.Fatal(err)
382 }
383 reg, err := registry.Open(fx.regPath)
384 if err != nil {
385 t.Fatal(err)
386 }
387 routes, err := reg.List()
388 if err != nil {
389 t.Fatal(err)
390 }
391 f, err := status.BuildFleet(ctx, fx.rel, routes, time.Now(), 5*time.Minute)
392 if err != nil {
393 t.Fatal(err)
394 }
395 if err := status.PublishFleet(ctx, fx.rel, f); err != nil {
396 t.Fatal(err)
397 }
398
399 // The rate card prices this box explicitly at 30 GB of EBS...
400 sc := economics.DefaultScenario()
401 sc.Name = "test"
402 sc.RateCard.Boxes["ada-stargazing"] = economics.BoxSpec{HourUSD: 0.02, Cores: 2, MemGB: 4, EBSGB: 30}
403 putStoreJSON(t, fx.rel, economics.ScenarioKey("test"), sc)
404 putStoreJSON(t, fx.rel, economics.ActiveKey, economics.Active{Schema: 1, Name: "test"})
405
406 // ...while the estate has 40 GB attached to the machine behind it.
407 fx.srv.inventory = func(context.Context) (infraview.Inventory, error) {
408 return infraview.Inventory{Provider: "aws", Account: "1234", TakenAt: time.Now().UTC(),
409 Resources: []infraview.Resource{
410 {Provider: "aws", Kind: infraview.KindMachine, ID: "i-123", Name: "ada-stargazing", Region: "eu-west-1",
411 Attrs: map[string]string{"type": "t4g.medium", "state": "running"}},
412 {Provider: "aws", Kind: infraview.KindDisk, ID: "vol-1",
413 Attrs: map[string]string{"size_gb": "40", "attached_to": "i-123"}},
414 }}, nil
415 }
416 if err := fx.srv.infraPass(ctx); err != nil {
417 t.Fatal(err)
418 }
419
420 st, _, body := get(t, c, fx.ts.URL+"/fleet/architecture")
421 if st != 200 {
422 t.Fatalf("= %d, want 200", st)
423 }
424 for _, want := range []string{
425 "ada-stargazing", // the card is there at all
426 "eu-west-1", // region
427 "t4g.medium", // discovered instance type
428 "running", // discovered state
429 "2 cores", // rate-card cores (spec §7: cores/memory)
430 "4 GB", // rate-card memory
431 "40 GB disk", // discovered disk
432 "/mo", // rate-card price
433 "<code>kass</code>", // the app, joined through the registry
434 "static 2", // fleet counts
435 "resident 1", //
436 "<code>v9.9.9-test</code>", // the release the box reports
437 "chip chip-stale", // drift wears the fact-to-watch chip
438 "rate card says 30 GB of EB", // and says what disagrees
439 "the estate has 40 GB", //
440 } {
441 if !strings.Contains(body, want) {
442 t.Errorf("box card is missing %q in:\n%s", want, body)
443 }
444 }
445 // A box the rate card names explicitly must NOT also report the
446 // priced-by-the-default-spec drift.
447 if strings.Contains(body, "priced by the default spec") {
448 t.Errorf("an explicitly priced box reported default-spec drift:\n%s", body)
449 }
450}
451
452// The counts come from the published fleet aggregate and nowhere else. A box
453// whose own snapshot is on the bucket but which the last fleet merge did not
454// describe has NO counts — and "static 0 · resident 0 · hibernating 0" would
455// be three confident claims about a read that never happened, printed beside
456// an awake/asleep that correctly says it does not know.
457func TestArchitectureBoxWithoutAFleetRowSaysCountsUnknown(t *testing.T) {
458 fx := newConsoleFixture(t)
459 c := signInPaul(t, fx)
460 if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil {
461 t.Fatal(err)
462 }
463 // Written, but never merged into status/fleet.json: the box exists in the
464 // host table alone.
465 if err := status.WriteBox(context.Background(), fx.rel, status.Box{
466 GeneratedAt: time.Now(),
467 Label: "grace-unmerged",
468 Region: "eu-west-1",
469 Routes: status.RouteCounts{Static: 2, Resident: 1},
470 Metrics: &status.BoxMetrics{Release: "v9.9.9-test"},
471 }); err != nil {
472 t.Fatal(err)
473 }
474
475 st, _, body := get(t, c, fx.ts.URL+"/fleet/architecture")
476 if st != 200 {
477 t.Fatalf("= %d, want 200", st)
478 }
479 if !strings.Contains(body, "grace-unmerged") {
480 t.Fatalf("the host-table box has no card:\n%s", body)
481 }
482 if !strings.Contains(body, "counts unknown") {
483 t.Errorf("a box the fleet never described must say its counts are unknown:\n%s", body)
484 }
485 if strings.Contains(body, "static 0") || strings.Contains(body, "hibernating 0") {
486 t.Errorf("the page states a count for a read that never happened:\n%s", body)
487 }
488}
489