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 []DashboardCard PendingDomainClaims []PendingDomainClaim } 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) } 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=\"load, ") { 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: []DashboardCard{ { Title: "Stub Sites", PartialPath: "/partials/stub/sites", RefreshEvent: "refreshStub", Scripts: []string{"/static/stub/stub-form.js"}, }, { Title: "Other Service", PartialPath: "/partials/other/status", }, }, }) 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") } if !strings.Contains(body, `hx-get="/partials/stub/sites"`) || !strings.Contains(body, `hx-get="/partials/other/status"`) { t.Errorf("card body hx-get partial paths missing") } if !strings.Contains(body, `hx-trigger="load, refreshStub from:body"`) { t.Errorf("refresh-event trigger spec missing") } if !strings.Contains(body, `hx-trigger="load"`) { t.Errorf("plain load trigger missing for card without refresh event") } if !strings.Contains(body, ``) { t.Errorf("declared card script not rendered as page-level deferred tag") } if strings.Contains(strings.ToLower(body), "fedwiki") { t.Errorf("generic shell leaked an integration name") } }