package console import ( "context" "fmt" "html" "html/template" "net/http" "net/url" "os" "regexp" "slices" "strings" "testing" "time" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/status" "github.com/carlosframework/platform/internal/updatewords" ) // pageFx is zdd-6 Task 12's heldFixture (kass's web instance, on stable, with // box1 reporting it held: installed v2, serving v1) plus the store helpers the // instance page's Updates section tests use. Every helper writes the store // directly, bypassing the settings doors. type pageFx struct { *consoleFixture t *testing.T // report is box1's update report for heldHost, written by boxesEnforce. report *status.HostUpdates } func updatesPageFixture(t *testing.T) *pageFx { t.Helper() return &pageFx{consoleFixture: heldFixture(t), t: t} } // esc is a text as the instance page's html/template prints it. func esc(s string) string { return template.HTMLEscapeString(s) } // getPage GETs u, a full URL or a redirect's path. func (fx *pageFx) getPage(c *http.Client, u string) (int, string) { fx.t.Helper() if strings.HasPrefix(u, "/") { u = fx.ts.URL + u } code, _, body := get(fx.t, c, u) return code, body } // updatesSection is the instance page's Updates section, "" when none. func updatesSection(body string) string { i := strings.Index(body, `
`) if i < 0 { return "" } j := strings.Index(body[i:], "
") if j < 0 { return body[i:] } return body[i : i+j] } var ( formInputTag = regexp.MustCompile(`]*>`) formInputAttr = regexp.MustCompile(`([a-z_]+)="([^"]*)"`) ) // browserForm is what a browser submits from the Updates section's Save form // as rendered, untouched: every hidden, text and number input with its // value, and a checkbox only when it is checked. A test edits the result the // way a member edits the form. func browserForm(t *testing.T, sec string) url.Values { t.Helper() i := strings.Index(sec, `
")] out := url.Values{} for _, tag := range formInputTag.FindAllString(form, -1) { attrs := map[string]string{} for _, m := range formInputAttr.FindAllStringSubmatch(tag, -1) { attrs[m[1]] = html.UnescapeString(m[2]) } if attrs["type"] == "checkbox" && !strings.Contains(tag, " checked") { continue } out.Add(attrs["name"], attrs["value"]) } return out } // boxesEnforce rewrites box1, the deployment's only box, keeping its held // rollout report and adding heldHost to its routes, so the box join has an // update view for the host. on says whether box1's binary applies update // settings, which also opens or closes the console's fleet gate. func (fx *pageFx) boxesEnforce(on bool) { fx.t.Helper() box := status.CollectBox(nil, status.Probes{MachineID: "box1"}, time.Now()) m := &status.BoxMetrics{ Rollout: &status.RolloutReport{Hosts: map[string]status.HostRollout{ heldHost: {Account: fx.sqid, App: "kass", Installed: "v2", Serving: "v1", Good: "v1", Awake: true, HeldBy: "v2", HeldEpoch: heldEpoch, HeldBook: heldBook, Reason: status.ReasonHealth}, }}, RouteVersions: map[string]string{heldHost: "v2"}, } if on { m.UpdateSettings = status.UpdateSettingsProtocol } if fx.report != nil { m.Updates = map[string]status.HostUpdates{heldHost: *fx.report} } box.Metrics = m if err := status.WriteBox(context.Background(), fx.rel, box); err != nil { fx.t.Fatal(err) } } // writeBox1 replaces box1's snapshot with m: no rollout report unless m // carries one. func (fx *pageFx) writeBox1(m *status.BoxMetrics) { fx.t.Helper() box := status.CollectBox(nil, status.Probes{MachineID: "box1"}, time.Now()) box.Metrics = m if err := status.WriteBox(context.Background(), fx.rel, box); err != nil { fx.t.Fatal(err) } } func (fx *pageFx) record(host string) provision.Record { fx.t.Helper() rec, err := provision.GetRecord(context.Background(), fx.rel, fx.sqid, "kass", host) if err != nil { fx.t.Fatal(err) } return *rec } // setRecordSettings is a read-modify-write of host's record that bypasses // the settings doors (and so their fleet gate). func (fx *pageFx) setRecordSettings(host string, edit func(*provision.Record)) { fx.t.Helper() ctx := context.Background() rec, etag, err := provision.GetRecordETag(ctx, fx.rel, fx.sqid, "kass", host) if err != nil { fx.t.Fatal(err) } edit(rec) if err := provision.UpdateRecord(ctx, fx.rel, *rec, etag); err != nil { fx.t.Fatal(err) } } func (fx *pageFx) setRecordUpdates(host, updates string) { fx.setRecordSettings(host, func(r *provision.Record) { r.Updates = updates }) } func (fx *pageFx) pin(host, build string) { fx.setRecordSettings(host, func(r *provision.Record) { r.Pin = build }) } func (fx *pageFx) setChannel(host, channel string) { fx.setRecordSettings(host, func(r *provision.Record) { r.Channel = channel }) } // makeNamedUnit gives host's record a systemd unit of its own, as plan 3's // TestUpdatesDoorsRefuseNamedUnitsAndOtherKinds does. func (fx *pageFx) makeNamedUnit(host string) { fx.setRecordSettings(host, func(r *provision.Record) { r.Unit, r.Addr = "console.service", "127.0.0.1:8080" }) } // grant writes kass's instances manifest with an update policy whose // instance_control is names. func (fx *pageFx) grant(names ...string) { fx.t.Helper() enableInstances(fx.t, fx.consoleFixture, "kass", provision.Manifest{Schema: 1, App: "kass", Domain: "kass.carlosapp.dev", Channel: "stable", Updates: &provision.UpdatePolicy{InstanceControl: names}}) } // ageSession leaves c's session with no proof of presence, as the step-up // tests find a fresh sign-in: the owner has to confirm with a passkey. func (fx *pageFx) ageSession(c *http.Client) { fx.t.Helper() if _, err := fx.st.db.Exec(`DELETE FROM step_ups WHERE session_hash = ?`, hashToken(sessionToken(fx.t, fx.consoleFixture, c))); err != nil { fx.t.Fatal(err) } } func TestUpdatesSectionShowsDefaults(t *testing.T) { fx := updatesPageFixture(t) c := signInPaul(t, fx.consoleFixture) code, body := fx.getPage(c, fx.u("/apps/kass/instances/"+heldHost)) sec := updatesSection(body) if code != http.StatusOK || !strings.Contains(sec, esc(updatewords.Text("settings.page.heading"))) || !strings.Contains(sec, esc(updatewords.Text("settings.page.update-now"))) { t.Fatalf("page: %d\n%s", code, body) } } func TestPageSaveGoesThroughTheGate(t *testing.T) { fx := updatesPageFixture(t) fx.boxesEnforce(true) c := signInPaul(t, fx.consoleFixture) manual := url.Values{"form": {"updates"}, "was_auto": {"1"}} // the page showed auto; the checkbox unticked bad := url.Values{"csrf": {"wrong"}} for k, v := range manual { bad[k] = v } if code, _, _ := postForm(t, c, fx.u("/apps/kass/instances/"+heldHost+"/updates"), bad); code != http.StatusForbidden { t.Fatalf("bad csrf = %d", code) } csrf := sessionToken(t, fx.consoleFixture, c) with := func(v url.Values) url.Values { out := url.Values{"csrf": {csrf}} for k, x := range v { out[k] = x } return out } code, _, body := postForm(t, c, fx.u("/apps/kass/instances/nope.kass.carlosapp.dev/updates"), with(manual)) if code != http.StatusNotFound || body != "404 page not found\n" { t.Fatalf("unknown host = %d %q", code, body) } code, loc, _ := postForm(t, c, fx.u("/apps/kass/instances/"+heldHost+"/updates"), with(manual)) if code != http.StatusSeeOther || loc != fx.path("/apps/kass/instances/"+heldHost) || fx.record(heldHost).Updates != release.UpdatesManual { t.Fatalf("save = %d -> %q, record %+v", code, loc, fx.record(heldHost)) } if _, page := fx.getPage(c, loc); !strings.Contains(page, esc(updatewords.Text("settings.page.saved"))) { t.Fatalf("no saved notice:\n%s", page) } // An enabling change the fleet gate refuses comes back as plan 3's // reviewed refusal, verbatim, in the error flash; nothing is written. fx.setRecordUpdates(heldHost, "") fx.boxesEnforce(false) code, loc, _ = postForm(t, c, fx.u("/apps/kass/instances/"+heldHost+"/updates"), with(manual)) if code != http.StatusSeeOther || fx.record(heldHost).Updates != "" { t.Fatalf("refused save = %d, record %+v", code, fx.record(heldHost)) } refusal := fmt.Sprintf(updatewords.Text("settings.console.not-enforced"), 1) if _, page := fx.getPage(c, loc); !strings.Contains(page, ``) { t.Fatalf("refusal flash lacks %q:\n%s", refusal, page) } } // Review Focus 5. func TestPageUpdateNowRefusedWhenPinned(t *testing.T) { fx := updatesPageFixture(t) fx.pin(heldHost, "v1") c := signInPaul(t, fx.consoleFixture) csrf := sessionToken(t, fx.consoleFixture, c) code, loc, _ := postForm(t, c, fx.u("/apps/kass/instances/"+heldHost+"/updates/install"), url.Values{"csrf": {csrf}}) if code != http.StatusSeeOther { t.Fatalf("Update now = %d", code) } _, body := fx.getPage(c, loc) refusal := fmt.Sprintf(updatewords.Text("settings.console.pinned"), heldHost, "v1") if !strings.Contains(body, ``) || fx.record(heldHost).InstallRequest != nil { t.Fatalf("pinned Update now: %s / %+v", body, fx.record(heldHost).InstallRequest) } } func TestPageUpdateNowRequestsAnInstall(t *testing.T) { fx := updatesPageFixture(t) c := signInPaul(t, fx.consoleFixture) csrf := sessionToken(t, fx.consoleFixture, c) code, loc, _ := postForm(t, c, fx.u("/apps/kass/instances/"+heldHost+"/updates/install"), url.Values{"csrf": {csrf}}) rec := fx.record(heldHost) if code != http.StatusSeeOther || rec.InstallRequest == nil || rec.InstallRequest.ID != 1 || rec.InstallRequest.FastTrack || rec.InstallRequest.By != "paul@keymail.dev" { t.Fatalf("Update now = %d, record %+v", code, rec.InstallRequest) } if _, page := fx.getPage(c, loc); !strings.Contains(page, esc(updatewords.Text("settings.page.requested"))) { t.Fatalf("no requested notice:\n%s", page) } } // Review Focus 4, ruling R12: plan 3's approved named-unit words, no forms. func TestUpdatesSectionForANamedUnit(t *testing.T) { fx := updatesPageFixture(t) fx.grant("updates", "major", "pin") fx.makeNamedUnit(heldHost) c := signInPaul(t, fx.consoleFixture) _, body := fx.getPage(c, fx.u("/apps/kass/instances/"+heldHost)) sec := updatesSection(body) if !strings.Contains(sec, esc(fmt.Sprintf(updatewords.Text("settings.console.named-unit"), heldHost))) || strings.Contains(sec, "auto", "stable", "2", "v1", esc(updatewords.Text("settings.page.channel-hint")), esc(updatewords.Text("settings.page.update-now"))} { if !strings.Contains(sec, want) { t.Errorf("kept settings section lacks %q", want) } } if t.Failed() { t.Logf("section:\n%s", sec) } } // staleSave posts f to the Save door and fails on anything but a clean save. func (fx *pageFx) staleSave(c *http.Client, f url.Values) { fx.t.Helper() code, loc, _ := postForm(fx.t, c, fx.u("/apps/kass/instances/"+heldHost+"/updates"), f) if code != http.StatusSeeOther { fx.t.Fatalf("save = %d", code) } if _, page := fx.getPage(c, loc); !strings.Contains(page, esc(updatewords.Text("settings.page.saved"))) { fx.t.Fatalf("save not saved:\n%s", page) } } // updatesAudit is every instance-updates activity line for kass. func (fx *pageFx) updatesAudit() []string { fx.t.Helper() acts, err := fx.st.Events(OpsAccountID, "kass", 50) if err != nil { fx.t.Fatal(err) } var out []string for _, a := range acts { if a.Action == "instance-updates" { out = append(out, a.Detail) } } return out } // Review round 1 (R15): the form sends what the member changed on the page // they saw, not what differs from the record at post time. Member A's page // shows the instance pinned to v1; member B clears the pin behind it; A // unticks auto and saves. The pin A never touched stays cleared. func TestAStalePageDoesNotUndoAnotherMembersChange(t *testing.T) { fx := updatesPageFixture(t) fx.grant("updates", "major", "pin") fx.boxesEnforce(true) fx.pin(heldHost, "v1") c := signInPaul(t, fx.consoleFixture) _, body := fx.getPage(c, fx.u("/apps/kass/instances/"+heldHost)) f := browserForm(t, updatesSection(body)) if f.Get("pin") != "v1" || f.Get("auto") != "1" { t.Fatalf("rendered form %v", f) } fx.pin(heldHost, "") // member B, through the CLI, after A's page loaded f.Del("auto") // A unticks auto fx.staleSave(c, f) if r := fx.record(heldHost); r.Updates != release.UpdatesManual || r.Pin != "" { t.Fatalf("record after a stale save: updates %q pin %q", r.Updates, r.Pin) } want := []string{fmt.Sprintf(updatewords.Text("settings.console.audit-set"), heldHost, "updates manual")} if got := fx.updatesAudit(); !slices.Equal(got, want) { t.Fatalf("audit = %q, want %q", got, want) } } // The auto variant: A's page shows the instance manual and pinned; B switches // it to auto behind it; A only clears the pin. The unticked box A left alone // does not switch the instance back to manual. func TestAStalePageDoesNotUndoAnotherMembersSwitchToAuto(t *testing.T) { fx := updatesPageFixture(t) fx.grant("updates", "major", "pin") fx.boxesEnforce(true) fx.setRecordSettings(heldHost, func(r *provision.Record) { r.Updates, r.Pin = release.UpdatesManual, "v1" }) c := signInPaul(t, fx.consoleFixture) _, body := fx.getPage(c, fx.u("/apps/kass/instances/"+heldHost)) f := browserForm(t, updatesSection(body)) if f.Get("pin") != "v1" || f.Has("auto") { t.Fatalf("rendered form %v", f) } fx.setRecordUpdates(heldHost, release.UpdatesAuto) // member B f.Set("pin", "") // A clears the pin fx.staleSave(c, f) if r := fx.record(heldHost); r.Updates != release.UpdatesAuto || r.Pin != "" { t.Fatalf("record after a stale save: updates %q pin %q", r.Updates, r.Pin) } want := []string{fmt.Sprintf(updatewords.Text("settings.console.audit-set"), heldHost, "pin cleared")} if got := fx.updatesAudit(); !slices.Equal(got, want) { t.Fatalf("audit = %q, want %q", got, want) } } // Review round 1 (R16): both forms spend the shared write budget, and a // refusal is the page's reviewed words, not the API's. func TestPageSaveAndUpdateNowAreRateLimited(t *testing.T) { fx := updatesPageFixture(t) fx.srv.cli.write = newRateLimiter(1, time.Minute) c := signInPaul(t, fx.consoleFixture) csrf := sessionToken(t, fx.consoleFixture, c) limited := `` save := url.Values{"csrf": {csrf}, "form": {"updates"}} _, loc, _ := postForm(t, c, fx.u("/apps/kass/instances/"+heldHost+"/updates"), save) if _, page := fx.getPage(c, loc); !strings.Contains(page, esc(updatewords.Text("settings.page.saved"))) { t.Fatalf("first save:\n%s", page) } _, loc, _ = postForm(t, c, fx.u("/apps/kass/instances/"+heldHost+"/updates"), save) if _, page := fx.getPage(c, loc); !strings.Contains(page, limited) { t.Fatalf("second save lacks %q:\n%s", limited, page) } _, loc, _ = postForm(t, c, fx.u("/apps/kass/instances/"+heldHost+"/updates/install"), url.Values{"csrf": {csrf}}) if _, page := fx.getPage(c, loc); !strings.Contains(page, limited) || fx.record(heldHost).InstallRequest != nil { t.Fatalf("Update now on a spent budget lacks %q or wrote %+v:\n%s", limited, fx.record(heldHost).InstallRequest, page) } } // TestUpdatesSectionPreview writes the instance page, Updates section and // all, to $UPDATEWORDS_PREVIEW for Paul's copy review: an owner's view of a // pinned instance on stable whose app grants updates, major and pin, with a // box report naming the build it wants and the fleet gate closed. func TestUpdatesSectionPreview(t *testing.T) { out := os.Getenv("UPDATEWORDS_PREVIEW") if out == "" { t.Skip("UPDATEWORDS_PREVIEW is not set") } fx := updatesPageFixture(t) fx.grant("updates", "major", "pin") two := 2 fx.setRecordSettings(heldHost, func(r *provision.Record) { r.Channel, r.Major, r.Pin = "stable", &two, "v1" }) fx.report = &status.HostUpdates{Desired: "v1", Reason: "pin", Detail: "v1"} fx.boxesEnforce(true) seedEdgesRecord(t, fx.consoleFixture, "uk-1") // a box with no heartbeat closes the gate c := signInPaul(t, fx.consoleFixture) code, body := fx.getPage(c, fx.u("/apps/kass/instances/"+heldHost)) if code != http.StatusOK || !strings.Contains(updatesSection(body), esc(updatewords.Text("settings.page.refused-until-boxes"))) { t.Fatalf("preview page = %d:\n%s", code, updatesSection(body)) } if err := os.WriteFile(out, []byte(body), 0o644); err != nil { t.Fatal(err) } t.Logf("wrote %s", out) } // Astra r1 (R18), through the instance page: box1 is a platform box with no // rollout report (CARLOS_SWAP off) whose update report for heldHost is still // the previous tenant's. kass's page shows no status line, no reason and no // build from it. An entry naming kass is kass's; one naming anyone else is // never shown. func TestAReassignedHostShowsNoPreviousTenantsReportOnThePage(t *testing.T) { fx := updatesPageFixture(t) c := signInPaul(t, fx.consoleFixture) statusPrefix := esc(strings.SplitN(updatewords.Text("settings.page.status"), "%s", 2)[0]) box := func(tenant [2]string) *status.BoxMetrics { return &status.BoxMetrics{ UpdateSettings: status.UpdateSettingsProtocol, RouteVersions: map[string]string{heldHost: "v2"}, Updates: map[string]status.HostUpdates{heldHost: {Desired: "v-prev-desired", Reason: "pin", Detail: "v-prev-pin", Baking: "v-prev-baking", Account: tenant[0], App: tenant[1]}}, } } for _, tenant := range [][2]string{{"", ""}, {"cdf", "kass"}, {fx.sqid, "blog"}} { fx.writeBox1(box(tenant)) _, body := fx.getPage(c, fx.u("/apps/kass/instances/"+heldHost)) sec := updatesSection(body) if sec == "" { t.Fatalf("tenant %v: no Updates section:\n%s", tenant, body) } for _, leak := range []string{"v-prev-desired", "v-prev-pin", "v-prev-baking", statusPrefix} { if strings.Contains(body, leak) { t.Errorf("tenant %v: the page shows %q:\n%s", tenant, leak, sec) } } } fx.writeBox1(box([2]string{fx.sqid, "kass"})) _, body := fx.getPage(c, fx.u("/apps/kass/instances/"+heldHost)) reason := fmt.Sprintf(updatewords.Text("settings.box.reason.pin"), "v-prev-pin") + "; " + fmt.Sprintf(updatewords.Text("settings.box.reason.baking"), "v-prev-baking") if want := esc(fmt.Sprintf(updatewords.Text("settings.page.status"), "v2", updatewords.Text("settings.cli.none"), "v-prev-desired", reason)); !strings.Contains(updatesSection(body), want) { t.Fatalf("kass's own attributed report: status line %q not in:\n%s", want, updatesSection(body)) } } // R18: with no tenant to attribute the report to, an old box still says the // settings are not enforced; that is the box's, not the instance's. func TestATenantlessOldBoxStillSaysNotEnforced(t *testing.T) { fx := updatesPageFixture(t) fx.writeBox1(&status.BoxMetrics{RouteVersions: map[string]string{heldHost: "v2"}, Updates: map[string]status.HostUpdates{heldHost: {Desired: "v-prev-desired", Reason: "manual", Detail: "v-prev-desired"}}}) fx.setRecordUpdates(heldHost, "manual") c := signInPaul(t, fx.consoleFixture) _, body := fx.getPage(c, fx.u("/apps/kass/instances/"+heldHost)) sec := updatesSection(body) if !strings.Contains(sec, esc(updatewords.Text("settings.page.not-enforced"))) || strings.Contains(body, "v-prev-desired") { t.Fatalf("section:\n%s", sec) } } // Astra r1 (R20): the major box is the instance's own override, so it is // empty while the app's default holds the instance; the note beside it says // the app holds it and at which major. Clearing an override back to the // app's default works with the fleet gate closed, and the note comes back. func TestAnInheritedMajorHoldIsShownBesideTheBox(t *testing.T) { fx := updatesPageFixture(t) two, three := 2, 3 enableInstances(t, fx.consoleFixture, "kass", provision.Manifest{Schema: 1, App: "kass", Domain: "kass.carlosapp.dev", Channel: "stable", Updates: &provision.UpdatePolicy{InstanceControl: []string{"updates", "major", "pin"}, DefaultMajor: &two}}) fx.boxesEnforce(false) // the fleet gate is closed fx.setChannel(heldHost, "stable") note := esc(fmt.Sprintf(updatewords.Text("settings.page.major-inherited"), 2)) c := signInPaul(t, fx.consoleFixture) section := func() string { t.Helper() _, body := fx.getPage(c, fx.u("/apps/kass/instances/"+heldHost)) return updatesSection(body) } if sec := section(); !strings.Contains(sec, note) || !strings.Contains(sec, `name="major" value=""`) || !strings.Contains(sec, `name="was_major" value=""`) { t.Fatalf("inherited hold: section lacks the note %q or an empty override:\n%s", note, sec) } fx.setRecordSettings(heldHost, func(r *provision.Record) { r.Major = &three }) sec := section() if strings.Contains(sec, note) || !strings.Contains(sec, `name="major" value="3"`) || !strings.Contains(sec, `name="was_major" value="3"`) { t.Fatalf("explicit hold: the note shows or the override is missing:\n%s", sec) } f := browserForm(t, sec) f.Set("major", "") // back to the app's default code, loc, _ := postForm(t, c, fx.u("/apps/kass/instances/"+heldHost+"/updates"), f) if _, page := fx.getPage(c, loc); code != http.StatusSeeOther || !strings.Contains(page, esc(updatewords.Text("settings.page.saved"))) { t.Fatalf("clearing the override = %d:\n%s", code, page) } if r := fx.record(heldHost); r.Major != nil { t.Fatalf("the override survived: %+v", r) } if sec := section(); !strings.Contains(sec, note) || !strings.Contains(sec, `name="major" value=""`) { t.Fatalf("after clearing: section lacks the note:\n%s", sec) } // Off stable no major applies, so there is no box and no note. fx.setChannel(heldHost, "beta") if sec := section(); strings.Contains(sec, note) || strings.Contains(sec, `name="major"`) { t.Fatalf("off stable:\n%s", sec) } }