Files
member-console/internal/server/index_render_test.go
T
cgalo5758 0b28a9dc29 Remediate security audit findings
- Replace gorilla/csrf with net/http CrossOriginProtection
- Require valkey-password and add TLS options for session store
- End session at /logout and revoke refresh tokens
- Re-derive identity and roles from provider every five minutes
- Process each Stripe webhook event in its own Temporal workflow
- Give each outbox entry its own workflow with Temporal retries
- Guard against stale Stripe events with provider timestamps
- Derive transport security from base-url scheme
2026-09-09 13:25:43 -05:00

211 lines
8.2 KiB
Go

// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
// SPDX-FileCopyrightText: 2025-2026 Christian Galo
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"
)
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{})
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{
DashboardCards: []DashboardCardView{
{DashboardCard: DashboardCard{
Title: "Stub Sites",
PartialPath: "/partials/stub/sites",
RefreshEvent: "refreshStub",
Scripts: []string{"/static/stub/stub-form.js"},
}, Body: "<p>stub body</p>"},
{DashboardCard: DashboardCard{
Title: "Other Service",
PartialPath: "/partials/other/status",
}, Body: "<p>other body</p>"},
},
})
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, "<p>stub body</p>") || !strings.Contains(body, "<p>other body</p>") {
t.Errorf("composed card bodies missing")
}
if !strings.Contains(body, `<div hx-get="/partials/stub/sites" hx-trigger="refreshStub from:body" hx-swap="innerHTML"><p>stub body</p></div>`) {
t.Errorf("refresh-event card must keep its partial route as the swap source")
}
if strings.Contains(body, `hx-get="/partials/other/status"`) {
t.Errorf("a card without a refresh event must not carry an hx-get")
}
// The confirm modal's submit indicator is a spinner too, but it is an
// action's loading state, hidden until a request; the first-paint
// spinner variant is what must be gone.
if strings.Contains(body, `hx-trigger="load`) || strings.Contains(body, "spinner-border text-primary") {
t.Errorf("nothing on the dashboard may fetch after load or show a spinner")
}
if !strings.Contains(body, `<script defer src="/static/stub/stub-form.js"></script>`) {
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")
}
}
// A card declaration's Description renders as the card's lead, in the same
// style pageHeader uses for its Lead (design D7); a card with no
// description renders no lead paragraph.
func TestIndexRendersCardDescription(t *testing.T) {
body := renderIndex(t, IndexPageData{
DashboardCards: []DashboardCardView{
{DashboardCard: DashboardCard{
Title: "FedWiki sites",
Description: "Your hosted wiki sites.",
PartialPath: "/partials/stub/sites",
}, Body: "<p>stub body</p>"},
{DashboardCard: DashboardCard{
Title: "Other Service",
PartialPath: "/partials/other/status",
}, Body: "<p>other body</p>"},
},
})
if !strings.Contains(body, `<p class="text-body-secondary mb-2">Your hosted wiki sites.</p>`) {
t.Errorf("card description did not render as the card's lead")
}
// The second card declares no description: no stray empty lead
// paragraph for it.
if strings.Count(body, `class="text-body-secondary mb-2"`) != 1 {
t.Errorf("a card with no description must render no lead paragraph")
}
}
// The member dashboard wraps its content in exactly one <main> (page-anatomy
// "Every page has one main landmark", ACC-40).
func TestIndexHasOneMainLandmark(t *testing.T) {
body := renderIndex(t, IndexPageData{})
if n := strings.Count(body, "<main"); n != 1 {
t.Errorf("index.html must render exactly one <main>, got %d", n)
}
if !strings.Contains(body, "</main>") {
t.Errorf("index.html must close its <main>")
}
}
// TestIndexEmptyState covers member-dashboard's ADDED requirement "The
// dashboard has one empty state" (design D25): with no declared card and
// no org entitlement, one emptyState renders the D25 sentence in place of
// the cards section, and no card chrome renders beside it.
func TestIndexEmptyState(t *testing.T) {
body := renderIndex(t, IndexPageData{})
if !strings.Contains(body, "Nothing to show yet.") {
t.Errorf("empty dashboard missing the D25 headline, got:\n%s", body)
}
if !strings.Contains(body, "Your services and entitlements appear here once your organization holds a plan or a grant.") {
t.Errorf("empty dashboard missing the D25 note, got:\n%s", body)
}
// The Account section always renders its own card; what must NOT
// render is dashboard-card chrome (an integration card's swap-source
// wrapper), the same marker TestIndexNoDashboardCards checks.
if strings.Contains(body, "hx-trigger=") {
t.Errorf("empty dashboard must render no dashboard-card chrome beside the empty state")
}
}
// TestIndexNoEmptyStateWithEntitlement covers the other half of the same
// requirement: an org that holds an entitlement is not "nothing to show",
// even with zero declared cards (no configured integration on this
// deployment) — the empty state must not render.
func TestIndexNoEmptyStateWithEntitlement(t *testing.T) {
body := renderIndex(t, IndexPageData{HasEntitlement: true})
if strings.Contains(body, "Nothing to show yet.") {
t.Errorf("dashboard with an org entitlement must not render the empty state, got:\n%s", body)
}
}
// TestIndexNoEmptyStateWithCards covers the same requirement from the
// other input: a declared card alone (regardless of HasEntitlement) is
// enough to skip the empty state.
func TestIndexNoEmptyStateWithCards(t *testing.T) {
body := renderIndex(t, IndexPageData{
DashboardCards: []DashboardCardView{
{DashboardCard: DashboardCard{Title: "Other Service", PartialPath: "/partials/other/status"}, Body: "<p>other body</p>"},
},
})
if strings.Contains(body, "Nothing to show yet.") {
t.Errorf("dashboard with a declared card must not render the empty state, got:\n%s", body)
}
}