gates / gates (push) Successful in 1m58s
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.
67 lines
2.7 KiB
Go
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))
|
|
}
|
|
}
|