f181efd6a7
gates / gates (push) Successful in 18s
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/.
149 lines
6.6 KiB
Go
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")
|
|
}
|
|
}
|