Files
felhom.eu/hub/internal/web/declared_floor_test.go
T
admin f181efd6a7
gates / gates (push) Successful in 18s
hub v0.112.0: a floor carries a declared MinAgent past the golden (R-472)
Operator ruling 2026-09-13. Above the vouched golden, a floor saved with a
declared MinAgent is served under the same agent comparison; an undeclared
one is still held beyond the golden. The declaration is stored beside each
floor as FLOOR=MINAGENT so it never carries to a later floor. Both floor
forms require min_agent above the golden (flash floor_needs_min_agent,
nothing stored). The Hosts page and the API log name the source.

Vouch path and R-120 gate untouched. Scenarios A-E tested; red-proofs A
and C in documentation/audits/rulings-r472-r475-2026-09-13/.
2026-09-13 16:47:11 +02:00

149 lines
6.6 KiB
Go

package web
import (
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
)
// R-472 SCENARIO E — the floor forms, through the real handlers.
func postGlobalFloor(t *testing.T, s *Server, form url.Values) string {
t.Helper()
r := httptest.NewRequest(http.MethodPost, "/configuration/global-floor", strings.NewReader(form.Encode()))
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
s.handleSetGlobalFloor(w, r)
return w.Header().Get("Location")
}
func TestGlobalFloorForm_RefusesAboveGoldenWithoutMinAgent(t *testing.T) {
s, st := newTestServer(t)
_ = st.SetArtifactManifest(store.ArtifactManifest{GoldenVersion: "0.236.0", MinAgent: "0.129.0"})
_ = st.SetGlobalMinControllerVersion("0.236.0")
loc := postGlobalFloor(t, s, url.Values{"min_controller_version": {"0.239.0"}})
if !strings.Contains(loc, "flash=floor_needs_min_agent") {
t.Fatalf("a floor above the golden without min_agent must be refused, got %q", loc)
}
if got := st.GetGlobalMinControllerVersion(); got != "0.236.0" {
t.Errorf("a refused form must store NOTHING — floor is now %q", got)
}
loc = postGlobalFloor(t, s, url.Values{"min_controller_version": {"0.239.0"}, "min_agent": {"0.12x"}})
if !strings.Contains(loc, "flash=floor_min_agent_invalid") || st.GetGlobalMinControllerVersion() != "0.236.0" {
t.Fatalf("an unparseable min_agent must be refused with nothing stored, got %q", loc)
}
loc = postGlobalFloor(t, s, url.Values{"min_controller_version": {"0.239.0"}, "min_agent": {"0.129.0"}})
if !strings.Contains(loc, "flash=floor_set") {
t.Fatalf("a declared floor must be saved, got %q", loc)
}
if st.GetGlobalMinControllerVersion() != "0.239.0" || st.GlobalFloorDeclaredMinAgent() != "0.129.0" {
t.Errorf("floor=%q declared=%q", st.GetGlobalMinControllerVersion(), st.GlobalFloorDeclaredMinAgent())
}
// Inside the golden no declaration is needed — today's behaviour is unchanged.
if loc := postGlobalFloor(t, s, url.Values{"min_controller_version": {"0.230.0"}}); !strings.Contains(loc, "flash=floor_set") {
t.Errorf("a floor at/below the golden must save without min_agent, got %q", loc)
}
}
func TestCustomerFloorForm_RefusesAboveGoldenWithoutMinAgent(t *testing.T) {
s, st := newTestServer(t)
_ = st.SetArtifactManifest(store.ArtifactManifest{GoldenVersion: "0.236.0", MinAgent: "0.129.0"})
if err := st.SaveCustomerConfig(&store.CustomerConfig{CustomerID: "c1", RetrievalPassword: "x", APIKey: "y"}); err != nil {
t.Fatal(err)
}
post := func(form url.Values) string {
r := httptest.NewRequest(http.MethodPost, "/customers/c1/floor", strings.NewReader(form.Encode()))
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
s.handleSetCustomerFloor(w, r, "c1")
return w.Header().Get("Location")
}
if loc := post(url.Values{"min_controller_version": {"0.239.0"}}); !strings.Contains(loc, "flash=floor_needs_min_agent") {
t.Fatalf("got %q", loc)
}
if cfg, _ := st.GetCustomerConfig("c1"); cfg.MinControllerVersion != "" {
t.Errorf("a refused override must store nothing, got %q", cfg.MinControllerVersion)
}
if loc := post(url.Values{"min_controller_version": {"0.239.0"}, "min_agent": {"0.129.0"}}); !strings.Contains(loc, "flash=floor_set") {
t.Fatalf("got %q", loc)
}
if got := st.CustomerFloorDeclaredMinAgent("c1"); got != "0.129.0" {
t.Errorf("declared = %q", got)
}
}
// R-472 — the Hosts badge is a template gate, so each branch gets a render test (hub rules: seam-wiring
// covers template gates). Served-by-declaration shows the badge; undeclared above the golden shows
// "floor held" and NOT the badge; inside the golden (manifest governs) shows neither.
func renderHostsForFloor(t *testing.T, floor, declared string) string {
t.Helper()
s, st := newTestServer(t)
_ = st.SetArtifactManifest(store.ArtifactManifest{GoldenVersion: "0.236.0", MinAgent: "0.129.0"})
if err := st.UpsertHost(&store.Host{HostID: "demo-hp-01", CustomerID: "c1", APIKey: "k1"}); err != nil {
t.Fatal(err)
}
if err := st.SaveHostReport("demo-hp-01", "c1", []byte(testReportJSON), store.HostReportDenorm{AgentVersion: "0.130.0"}); err != nil {
t.Fatal(err)
}
if err := st.SetGlobalMinControllerVersion(floor); err != nil {
t.Fatal(err)
}
if err := st.SetGlobalFloorDeclaredMinAgent(floor, declared); err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
s.handleHostsList(rr, httptest.NewRequest(http.MethodGet, "/hosts", nil))
if rr.Code != http.StatusOK {
t.Fatalf("status = %d", rr.Code)
}
return rr.Body.String()
}
func TestHostsBadge_EachFloorBranchRenders(t *testing.T) {
const badge, held = "floor: declared MinAgent", "floor held"
if body := renderHostsForFloor(t, "0.239.0", "0.129.0"); !strings.Contains(body, badge) || strings.Contains(body, held) {
t.Errorf("declared floor above the golden: want the badge and no hold (badge=%v held=%v)",
strings.Contains(body, badge), strings.Contains(body, held))
}
if body := renderHostsForFloor(t, "0.239.0", ""); strings.Contains(body, badge) || !strings.Contains(body, held) {
t.Errorf("undeclared floor above the golden: want the hold and no badge (badge=%v held=%v)",
strings.Contains(body, badge), strings.Contains(body, held))
}
if body := renderHostsForFloor(t, "0.236.0", "0.129.0"); strings.Contains(body, badge) || strings.Contains(body, held) {
t.Errorf("floor at the golden: the manifest governs, want neither (badge=%v held=%v)",
strings.Contains(body, badge), strings.Contains(body, held))
}
}
// The customer page renders the declared value back into its min_agent input — the form is reachable
// and round-trips what was stored. (Its absence once truncated the whole page render.)
func TestCustomerPage_RendersDeclaredMinAgentInput(t *testing.T) {
s, st := newTestServer(t)
_ = st.SetArtifactManifest(store.ArtifactManifest{GoldenVersion: "0.236.0", MinAgent: "0.129.0"})
if err := st.SaveCustomerConfig(&store.CustomerConfig{CustomerID: "acme", CustomerName: "Acme", Domain: "acme.hu",
RetrievalPassword: "pw", APIKey: "k", Status: "active"}); err != nil {
t.Fatal(err)
}
// The Controller Update card (and its floor form) renders only for a reporting customer.
if err := st.SaveReport("acme", []byte(tabsTestReportJSON)); err != nil {
t.Fatal(err)
}
if err := st.SetMinControllerVersion("acme", "0.239.0"); err != nil {
t.Fatal(err)
}
if err := st.SetCustomerFloorDeclaredMinAgent("acme", "0.239.0", "0.129.0"); err != nil {
t.Fatal(err)
}
html := renderCustomerPage(t, s, "acme")
if !strings.Contains(html, `name="min_agent" value="0.129.0"`) {
t.Error("the per-customer floor form must render min_agent with the declared value")
}
}