package web import ( "strings" "testing" "gitea.dooplex.hu/admin/felhom-controller/internal/stacks" ) // Slice 4 Part 4 — the card while an update runs, while an app is held, and after an update that // failed without a hold. Rendered through the REAL stacks template. // // THE TRAP THESE PIN: isOperationalState counts `restarting` as operational — that is how the // 2026-09-01 spike saw a green Frissítés beside a crash loop. Both fixtures below are in // StateRestarting on purpose, so a template that checked isOperational FIRST would render all three // lifecycle buttons and fail here. // // COMPANION RED-PROOF (REPORT.md): move the `{{if .Updating}}…{{else if .HoldReason}}` branches below // `isOperational` in stacks.html — the held and updating tests fail with the buttons present. var slice4LifecycleButtons = []string{ `stackAction(event, 'bookstack', 'update')`, `stackAction(event, 'bookstack', 'restart')`, `stackAction(event, 'bookstack', 'stop')`, `stackAction(event, 'bookstack', 'start')`, } func TestSlice4_Page_UpdatingCardShowsThePhaseAndNoLifecycleButton(t *testing.T) { st := ubStack(nil, map[string]string{"web": "nginx:1.27"}, "2026-07-18") st.State = stacks.StateRestarting st.Updating, st.UpdatePhase, st.UpdatePhaseLabel = true, stacks.UpdatePhasePulling, stacks.UpdatePhaseLabel(stacks.UpdatePhasePulling) h := renderBackupPage(t, "stacks", ubStacksData(st)) if !strings.Contains(h, `data-update-phase="pulling"`) || !strings.Contains(h, stacks.UpdatePhaseLabel(stacks.UpdatePhasePulling)) { t.Error("an updating card must show its phase label") } for _, b := range slice4LifecycleButtons { if strings.Contains(h, b) { t.Errorf("an updating card must offer no lifecycle button, found %q", b) } } } func TestSlice4_Page_HeldCardShowsTheHoldAndTheWayBack(t *testing.T) { st := ubStack(nil, map[string]string{"web": "nginx:1.27"}, "2026-07-18") st.State = stacks.StateRestarting st.HoldReason = "A(z) bookstack frissítése 2026-09-13 10:00-kor nem sikerült — HOLD" h := renderBackupPage(t, "stacks", ubStacksData(st)) if !strings.Contains(h, `data-held="true"`) || !strings.Contains(h, "HOLD") || !strings.Contains(h, `href="/backups/apps"`) { t.Error("a held card must show the hold sentence and link Mentések") } for _, b := range slice4LifecycleButtons { if strings.Contains(h, b) { t.Errorf("a held card must offer nothing that starts or updates it, found %q", b) } } } func TestSlice4_Page_FailedUpdateWithoutHoldKeepsTheButtonsAndSaysWhy(t *testing.T) { st := ubStack(nil, map[string]string{"web": "nginx:1.27"}, "2026-07-18") st.UpdateError = stacks.MsgUpdatePullFailed h := renderBackupPage(t, "stacks", ubStacksData(st)) if !strings.Contains(h, `data-update-error="true"`) || !strings.Contains(h, stacks.MsgUpdatePullFailed) { t.Error("a failed update that held nothing must still say what happened") } for _, b := range slice4LifecycleButtons[:3] { if !strings.Contains(h, b) { t.Errorf("positive control: a running, unheld app keeps %q", b) } } }