package server import ( "html/template" "io/fs" "net/http/httptest" "strings" "testing" "git.coopcloud.tech/wiki-cafe/member-console/internal/config" "git.coopcloud.tech/wiki-cafe/member-console/internal/embeds" "git.coopcloud.tech/wiki-cafe/member-console/internal/web" "log/slog" ) // indexPageData mirrors the anonymous struct the "/" handler feeds // index.html (server.go); keep the field sets in sync. type indexPageData struct { Name string Username string Email string KeycloakAccountURL string CSRFToken string IsOperator bool HasMultipleWorkspaces bool CheckoutStatus string DashboardCards []DashboardCardView Workspaces template.HTML PendingDomainClaims []PendingDomainClaim Shell Shell } func renderIndex(t *testing.T, data indexPageData) string { t.Helper() sub, err := fs.Sub(embeds.Templates, "templates") if err != nil { t.Fatalf("sub templates FS: %v", err) } // index.html references no other templates, so it parses standalone; the // pending-domain notice uses routeURL, registered for real. tmpl, err := template.New("root").Funcs(template.FuncMap{ "routeURL": web.RouteURL, "deploymentName": config.DeploymentName, }).ParseFS(sub, "index.html") if err != nil { t.Fatalf("parse index.html: %v", err) } tmpl = template.Must(web.ParseUIPartials(tmpl)) rec := httptest.NewRecorder() NewSafeTemplates(tmpl, slog.Default()).Render(rec, "index.html", data) if rec.Code != 200 { t.Fatalf("render = %d, body: %s", rec.Code, rec.Body.String()) } return rec.Body.String() } // The raw template bytes must be integration-free too — a rendered-output // check could miss a reference hidden behind an untaken template branch. func TestIndexTemplateNamesNoIntegration(t *testing.T) { raw, err := fs.ReadFile(embeds.Templates, "templates/index.html") if err != nil { t.Fatalf("read embedded index.html: %v", err) } for _, banned := range []string{"fedwiki", "discourse", "stripe"} { if strings.Contains(strings.ToLower(string(raw)), banned) { t.Errorf("core index.html template references integration %q", banned) } } } // The dashboard is registry-driven: core's index.html must not name any // integration, and with no declared cards it must render no card section // chrome and no integration script tags. func TestIndexNoDashboardCards(t *testing.T) { body := renderIndex(t, indexPageData{CSRFToken: "tok"}) if strings.Contains(strings.ToLower(body), "fedwiki") { t.Errorf("index.html without cards still references fedwiki") } if strings.Contains(strings.ToLower(body), "discourse") { t.Errorf("index.html without cards references discourse") } if strings.Contains(body, "hx-trigger=") { t.Errorf("card shell rendered despite zero declared cards") } } // Declared cards render as generic shells in declaration (registry) order, // with the declared partial path, trigger spec, and page-level deferred // script tags. func TestIndexRendersDeclaredCards(t *testing.T) { body := renderIndex(t, indexPageData{ CSRFToken: "tok", DashboardCards: []DashboardCardView{ {DashboardCard: DashboardCard{ Title: "Stub Sites", PartialPath: "/partials/stub/sites", RefreshEvent: "refreshStub", Scripts: []string{"/static/stub/stub-form.js"}, }, Body: "
stub body
"}, {DashboardCard: DashboardCard{ Title: "Other Service", PartialPath: "/partials/other/status", }, Body: "other body
"}, }, }) first := strings.Index(body, "Stub Sites") second := strings.Index(body, "Other Service") if first == -1 || second == -1 { t.Fatalf("declared card titles missing (first=%d second=%d)", first, second) } if first > second { t.Errorf("cards rendered out of declaration order") } // Bodies are composed on the server (page-anatomy "A page arrives // complete"): both render inline; only the card with a refresh event // keeps its partial route as an HTMX swap source, and nothing fetches // after load. if !strings.Contains(body, "stub body
") || !strings.Contains(body, "other body
") { t.Errorf("composed card bodies missing") } if !strings.Contains(body, `stub body