Files
felhom.eu/hub/internal/web/r604_floor_held_back_test.go
T

142 lines
5.9 KiB
Go

package web
import (
"bytes"
"log"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
)
// R-604 (hub v0.135.0): a global floor raise says which boxes it did NOT move — one log line per customer whose
// own floor is LOWER, and ONE operator mail naming them. An override at or above the global is not named; a raise
// that moves everyone sends nothing.
// RED-PROOF (audits/hub-safety-2026-10-05/partD/red-proof.txt): remove the s.reportFloorHeldBack(v) call in
// handleSetGlobalFloor → TestR604_GlobalRaiseNamesHeldBackBoxes fails (no log line, no mail).
type emitted struct{ customer, typ, sev, msg string }
func r604Server(t *testing.T) (*Server, *store.Store, *bytes.Buffer, *[]emitted) {
t.Helper()
s, st := newTestServer(t)
var buf bytes.Buffer
s.logger = log.New(&buf, "", 0)
var got []emitted
s.SetEventEmitter(func(c, typ, sev, msg, _, _ string) { got = append(got, emitted{c, typ, sev, msg}) })
// vouch a golden ABOVE the floors used here, so a floor needs no declared MinAgent (R-472 is not under test)
if err := st.SetArtifactManifest(store.ArtifactManifest{GoldenVersion: "0.400.0", AgentVersion: "0.145.0"}); err != nil {
t.Fatal(err)
}
for _, c := range []struct{ id, floor string }{{"c-low", "0.240.0"}, {"c-high", "0.300.0"}, {"c-none", ""}} {
if err := st.SaveCustomerConfig(&store.CustomerConfig{CustomerID: c.id, CustomerName: c.id, RetrievalPassword: "x", APIKey: "k-" + c.id, ConfigJSON: "{}"}); err != nil {
t.Fatal(err)
}
if c.floor != "" {
if err := st.SetMinControllerVersion(c.id, c.floor); err != nil {
t.Fatal(err)
}
}
}
return s, st, &buf, &got
}
func setGlobal(t *testing.T, s *Server, v string) {
t.Helper()
r := httptest.NewRequest(http.MethodPost, "/configuration/global-floor", strings.NewReader(url.Values{"min_controller_version": {v}}.Encode()))
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
s.ServeHTTP(w, r)
if w.Code != http.StatusSeeOther || !strings.Contains(w.Header().Get("Location"), "flash=floor_set") {
t.Fatalf("global floor %s: %d %s", v, w.Code, w.Header().Get("Location"))
}
}
func TestR604_GlobalRaiseNamesHeldBackBoxes(t *testing.T) {
s, _, buf, got := r604Server(t)
setGlobal(t, s, "0.295.0")
logs := buf.String()
if !strings.Contains(logs, "does NOT move customer c-low: its own floor 0.240.0 wins") {
t.Fatalf("no log line for the held-back box:\n%s", logs)
}
if strings.Contains(logs, "customer c-high") || strings.Contains(logs, "customer c-none") {
t.Fatalf("a box that is NOT held back was named:\n%s", logs)
}
if len(*got) != 1 {
t.Fatalf("want exactly ONE operator mail, got %d: %+v", len(*got), *got)
}
e := (*got)[0]
if e.typ != "floor_raise_skipped" || e.sev != "warning" || e.customer != "" ||
!strings.Contains(e.msg, "c-low (own floor 0.240.0") || strings.Contains(e.msg, "c-high") {
t.Fatalf("the mail does not name exactly the held-back box: %+v", e)
}
if !strings.Contains(e.msg, "set 20") {
t.Fatalf("the mail must give the override's age (set time): %q", e.msg)
}
}
func TestR604_RaiseThatMovesEveryoneSendsNothing(t *testing.T) {
s, _, buf, got := r604Server(t)
setGlobal(t, s, "0.200.0") // below both overrides: nobody is held back by a LOWER own floor
if len(*got) != 0 || strings.Contains(buf.String(), "does NOT move") {
t.Fatalf("nothing held back, yet: mails %+v, log %q", *got, buf.String())
}
}
// The System page shows every per-customer floor with its age, and flags the one the global floor cannot move.
func TestR604_SystemPageListsFloorsWithAge(t *testing.T) {
s, st, _ := systemServer(t)
if err := st.SetArtifactManifest(store.ArtifactManifest{GoldenVersion: "0.400.0", AgentVersion: "0.145.0"}); err != nil {
t.Fatal(err)
}
if err := st.SetGlobalMinControllerVersion("0.295.0"); err != nil {
t.Fatal(err)
}
if err := st.SaveCustomerConfig(&store.CustomerConfig{CustomerID: "c-full", CustomerName: "Full", RetrievalPassword: "x", APIKey: "k", ConfigJSON: "{}"}); err != nil {
t.Fatal(err)
}
if err := st.SetMinControllerVersion("c-full", "0.243.0"); err != nil {
t.Fatal(err)
}
b := getSystem(t, s)
for _, want := range []string{`id="version-floors"`, "Global controller floor: <strong>0.295.0", ">c-full<", "0.243.0",
time.Now().UTC().Format("2006-01-02"), "NO — its own floor is lower and wins"} {
if !strings.Contains(b, want) {
t.Errorf("System page lacks %q", want)
}
}
// the other branch of the gate: no override → the plain sentence
_ = st.SetMinControllerVersion("c-full", "")
if b := getSystem(t, s); !strings.Contains(b, "No per-customer floors") {
t.Error("no overrides: the page must say every box follows the global floor")
}
}
// R-530 on the page: the Agent cell shows box → vouched and is amber; red once the alarm's wait has passed.
func TestR530_SystemPageAgentCell(t *testing.T) {
s, st, svc := systemServer(t) // every box reports agent 0.142.0
if err := st.SetArtifactManifest(store.ArtifactManifest{AgentVersion: "0.145.0", AgentSHA256: "x"}); err != nil {
t.Fatal(err)
}
if _, err := svc.Alarms(); err != nil { // starts the behind-clock for every box
t.Fatal(err)
}
b := getSystem(t, s)
if !strings.Contains(b, "0.142.0 → 0.145.0") || !strings.Contains(b, `class="c-warn" title="3 minor releases behind`) {
t.Fatalf("the Agent cell does not show the box behind the vouched agent")
}
_ = st.SetAgentBehindSince("full-1", time.Now().Add(-8*24*time.Hour))
if b := getSystem(t, s); !strings.Contains(b, `class="c-bad" title="3 minor releases behind`) {
t.Fatal("past the alarm's wait the cell must be red")
}
// current branch
_ = st.SetArtifactManifest(store.ArtifactManifest{AgentVersion: "0.142.0", AgentSHA256: "x"})
if b := getSystem(t, s); strings.Contains(b, "→ 0.142.0") || !strings.Contains(b, `title="current (vouched 0.142.0)"`) {
t.Fatal("a current box must read current, not behind")
}
}