package web import ( "bytes" "io" "log" "net/http/httptest" "path/filepath" "strings" "testing" "time" "gitea.dooplex.hu/admin/felhom-hub/internal/store" ) // Catches template SYNTAX errors (template.Must in New panics) and that the per-customer floor renders // on the Customers list. Field-level execution errors surface as a non-nil ExecuteTemplate error. func TestTemplates_FloorRender(t *testing.T) { st, err := store.New(filepath.Join(t.TempDir(), "t.db"), log.New(io.Discard, "", 0)) if err != nil { t.Fatalf("store.New: %v", err) } t.Cleanup(func() { st.Close() }) s := New(st, "", "", "test", time.Hour, log.New(io.Discard, "", 0)) // template.Must runs here // configs.html — the list page data shape used by handleConfigList (global floor/artifacts moved // to the Configuration tab; the per-customer effective floor still renders here). cfgData := struct { Customers []customerListEntry ActiveNav string Flash string }{ Customers: []customerListEntry{{ CustomerID: "c", EffectiveFloor: "0.87.0", FloorOverride: "0.87.0", BelowFloor: true, ControllerVersion: "0.86.0", HasConfig: true, }}, ActiveNav: "configs", } var buf bytes.Buffer if err := s.templates.ExecuteTemplate(&buf, "configs.html", cfgData); err != nil { t.Fatalf("render configs.html: %v", err) } if !bytes.Contains(buf.Bytes(), []byte("0.87.0")) { t.Errorf("configs.html missing per-customer floor content") } // The global floor + artifact cards must NOT be on the Customers page anymore. if bytes.Contains(buf.Bytes(), []byte("global floor")) || bytes.Contains(buf.Bytes(), []byte("Day-0 artifacts")) { t.Errorf("configs.html still renders global settings that moved to Configuration") } // configuration.html — the Configuration tab now carries the global floor + artifact settings. confData := map[string]interface{}{ "CSRFToken": "tok", "CSRFField": s.csrfField(httptest.NewRequest("GET", "/", nil)), "AssetCount": 0, "AssetLastSync": "", "GlobalFloor": "0.86.0", "Artifacts": store.ArtifactManifest{AgentVersion: "0.43.0", GoldenVersion: "0.85.1"}, "Flash": "artifacts_set", } buf.Reset() if err := s.templates.ExecuteTemplate(&buf, "configuration.html", confData); err != nil { t.Fatalf("render configuration.html: %v", err) } body := buf.String() for _, want := range []string{"global floor", "Day-0 artifacts", "0.86.0", "0.43.0", "/configuration/global-floor", "/configuration/artifacts", "Artifact manifest saved"} { if !strings.Contains(body, want) { t.Errorf("configuration.html missing %q", want) } } } // Scenario D of the v0.31.0 critical-severity fix: a customer with critical events must render a // distinct severity-critical count badge on the dashboard, ordered BEFORE the error badge. // (Pre-fix consumer gap: criticals were accepted but invisible in the 24h summary counts.) func TestTemplates_DashboardCriticalBadge(t *testing.T) { st, err := store.New(filepath.Join(t.TempDir(), "t.db"), log.New(io.Discard, "", 0)) if err != nil { t.Fatalf("store.New: %v", err) } t.Cleanup(func() { st.Close() }) s := New(st, "", "", "test", time.Hour, log.New(io.Discard, "", 0)) // Mirrors the anonymous dashboardCustomer struct in handleDashboard. type dashboardCustomer struct { store.CustomerSummary OverallStatus string BackupAge string EventCriticals int EventErrors int EventWarnings int } data := []dashboardCustomer{{ CustomerSummary: store.CustomerSummary{CustomerID: "c1", CustomerName: "Acme", ReceivedAt: time.Now()}, OverallStatus: "ok", BackupAge: "–", EventCriticals: 2, EventErrors: 1, EventWarnings: 0, }} var buf bytes.Buffer if err := s.templates.ExecuteTemplate(&buf, "dashboard.html", data); err != nil { t.Fatalf("render dashboard.html: %v", err) } body := buf.String() critIdx := strings.Index(body, `severity-badge severity-critical">2<`) errIdx := strings.Index(body, `severity-badge severity-error">1<`) if critIdx < 0 { t.Fatalf("dashboard.html missing the severity-critical count badge") } if errIdx < 0 { t.Fatalf("dashboard.html missing the severity-error count badge") } if critIdx > errIdx { t.Errorf("critical badge renders AFTER the error badge (crit@%d, err@%d) — must be first", critIdx, errIdx) } }