Files
felhom.eu/hub/internal/web/d53_held_apps_test.go
T
admin 7e57e824a2
gates / gates (push) Successful in 1m58s
hub(D53): held apps reach the operator — mirror, Held apps list (host + Hosts page), reminder after the grace
app_held / app_hold_cleared allow-listed and operator-only; handleEvent mirrors them in held_apps; HeldAppsChecker
sends ONE app_held_reminder (warning, operator-only, per-app cooldown) after the grace per reason (6 h / 1 d / 3 d),
then weekly; recorded before sending; stale rows not reminded; the hub never clears a hold. Test-only
alerting.held_app_grace_override. TestD53_* in api/monitor/notify/web, red-proved by six mutations. Commit only.
2026-10-26 18:25:00 +01:00

67 lines
2.7 KiB
Go

package web
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
)
// D53 (`08` §10): the „Held apps" list on the host page and on the Hosts page — both branches of each {{if}} rendered
// (the seam-wiring rule): a held app shows its app, reason and who may clear it; after the clear, the empty sentence.
func TestD53_HeldAppsListOnTheHostPageAndTheHostsPage(t *testing.T) {
s, st := newTestServer(t)
if err := st.UpsertHost(&store.Host{HostID: "demo-hp-01", CustomerID: "c1", APIKey: "k1"}); err != nil {
t.Fatal(err)
}
now := time.Now()
if err := st.UpsertHeldApp("c1", "kimai", "unhealthy_stop", now.Add(-3*24*time.Hour), "stopped by the box for a crash loop", now); err != nil {
t.Fatal(err)
}
render := func() (string, string) {
rr := httptest.NewRecorder()
s.handleHostsList(rr, httptest.NewRequest(http.MethodGet, "/hosts", nil))
hd := httptest.NewRecorder()
s.handleHostDetail(hd, httptest.NewRequest(http.MethodGet, "/hosts/demo-hp-01", nil), "demo-hp-01")
if rr.Code != 200 || hd.Code != 200 {
t.Fatalf("status: hosts %d, host %d", rr.Code, hd.Code)
}
return rr.Body.String(), hd.Body.String()
}
fleet, host := render()
for name, body := range map[string]string{"hosts": fleet, "host": host} {
if !strings.Contains(body, "Held apps") || !strings.Contains(body, `data-held-app=`) || !strings.Contains(body, "kimai") ||
!strings.Contains(body, "unhealthy_stop") || !strings.Contains(body, "Start (one more try)") {
t.Errorf("%s page: the held app is not listed with its reason and who may clear it", name)
}
}
if !strings.Contains(fleet, `href="/hosts/demo-hp-01#held-apps"`) {
t.Error("hosts page: the held app does not link to its host")
}
if !strings.Contains(host, "stopped by the box for a crash loop") {
t.Error("host page: the box's sentence is missing")
}
if _, err := st.ClearHeldApp("c1", "kimai"); err != nil {
t.Fatal(err)
}
fleet, host = render()
if strings.Contains(fleet, `data-held-app=`) || !strings.Contains(fleet, "No app is held on any box.") {
t.Error("hosts page: after the clear the list is not empty")
}
if strings.Contains(host, `data-held-app=`) || !strings.Contains(host, "No app is held on this box.") {
t.Error("host page: after the clear the list is not empty")
}
}
// A host with no customer shows no held apps — never the whole fleet's.
func TestD53_AHostWithNoCustomerShowsNoHeldApps(t *testing.T) {
s, st := newTestServer(t)
_ = st.UpsertHeldApp("c9", "x", "restore_failed", time.Now(), "y", time.Now())
if rows := s.hostHeldAppRows("", time.Now()); len(rows) != 0 {
t.Fatalf("a customer-less host lists %d held app(s)", len(rows))
}
}