// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial // SPDX-FileCopyrightText: 2025-2026 Christian Galo package server import ( "bytes" "context" "errors" "html/template" "io" "io/fs" "log/slog" "regexp" "strconv" "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/identity" "git.coopcloud.tech/wiki-cafe/member-console/internal/organization" "git.coopcloud.tech/wiki-cafe/member-console/internal/web" "github.com/google/uuid" ) // Render coverage for the operator landing surface's overview regions ("At a // glance" + "System"). Template-level only: it executes operator.html against // hand-built OperatorPageData, so it needs no database and runs in every // environment. The loader that fills that data (loadOverview) is exercised // separately by the pure-function tests at the bottom of this file. // overviewTemplate parses the operator template set the same way // NewOperatorPartialsHandler does — partials first, then the shell — so this // test fails on the same parse errors production would hit. func overviewTemplate(t *testing.T) *template.Template { t.Helper() sub, err := fs.Sub(embeds.Templates, "templates") if err != nil { t.Fatalf("fs.Sub: %v", err) } partialsSub, err := fs.Sub(embeds.Templates, "templates/partials") if err != nil { t.Fatalf("fs.Sub partials: %v", err) } tmpl := template.New("operator").Funcs(template.FuncMap{ "renderBody": func(string, any) (template.HTML, error) { return "", nil }, "routeURL": web.RouteURL, "fieldErr": func(_, _, _, _ string, _, _ any) string { return "" }, "stripeEntityURL": func(string, string) string { return "" }, "deploymentName": config.DeploymentName, "pageTitle": pageTitle, "supportURL": config.SupportURL, "helpIcon": helpIcon, }) if tmpl, err = web.ParseUIPartials(template.Must(tmpl.ParseFS(partialsSub, "operator_*.html"))); err != nil { t.Fatalf("ParseFS partials: %v", err) } if tmpl, err = tmpl.ParseFS(sub, "operator.html"); err != nil { t.Fatalf("ParseFS operator.html: %v", err) } // The operator surface's root crumb (design D18); see server.go's own // override for why this must run after ParseUIPartials. tmpl = tmpl.Funcs(template.FuncMap{"surfaceRoot": OperatorSurfaceRoot}) return tmpl } func renderOperator(t *testing.T, data OperatorPageData) string { t.Helper() var buf bytes.Buffer if err := overviewTemplate(t).ExecuteTemplate(&buf, "operator.html", data); err != nil { t.Fatalf("ExecuteTemplate: %v", err) } return buf.String() } // landingRegion slices the rendered document down to the landing surface // itself. The navbar, sidebar, toast container and confirmation modal are // outside
(or outside #operator-body) and carry their own markup — // including an
in the modal — so assertions about this page's structure // must not see them. func landingRegion(t *testing.T, out string) string { t.Helper() const openTag = `
` start := strings.Index(out, openTag) if start < 0 { t.Fatalf("rendered output has no #operator-body region") } end := strings.Index(out, "
") if end < start { t.Fatalf("rendered output has no after #operator-body") } return out[start+len(openTag) : end] } // populatedOverview is a representative landing payload: all three tiles // available, and an Integrations card mixing a configured provider, an // unconfigured one, and Stripe (configured, draining queue work) — the // same mix the "Integrations card lists every provider with one status" // scenario describes (operator-panel-navigation). func populatedOverview() OperatorPageData { return OperatorPageData{ IAPosition: "runtime:landing", Overview: OverviewData{ Stats: []OverviewStat{ {Label: "People", Value: "1,204", Caption: "212 joined in the last 30 days", Href: "/operator/persons", Available: true}, {Label: "Team organizations", Value: "38", Caption: "Personal orgs excluded", Href: "/operator/organizations", Available: true}, {Label: "Monthly recurring", Value: "$2,295.00", Caption: "205 active subscriptions; 9 trialing", Href: "/operator/billing/subscriptions", Available: true}, }, Integrations: OverviewIntegrationsCard{ Rows: []OverviewIntegrationRow{ {IntegrationRow: IntegrationRow{ Key: "fedwiki", DisplayName: "FedWiki", Kind: "provisioning", Status: "active", SurfacePath: "/operator/integrations/fedwiki", Configured: true, }}, {IntegrationRow: IntegrationRow{ Key: "discourse", DisplayName: "Discourse", Kind: "provisioning", Status: "active", SettingsPath: "/operator/integrations/discourse/settings", Configured: false, }}, { IntegrationRow: IntegrationRow{ Key: "stripe", DisplayName: "Stripe", Kind: "payment", Status: "active", SurfacePath: "/operator/integrations/stripe", Configured: true, }, StripeFacts: &OverviewStripeFacts{ModeLabel: "Test mode", Queued: 4, Attention: 0, Available: true}, }, }, }, }, Activity: []ActivityEvent{{ EventType: "grant_issued", Timestamp: "Jan 2, 2026 3:04 PM", OrgID: "org-1", OrgName: "Example Org", Summary: "Issued Example Plan (promo)", Href: "/operator/organizations/org-1", }}, // ActivityNav pages the feed like the composite's Tier changes // (overview-consistency D5, round 2); the fixture's one row is the // whole (unpaged) window's length. ActivityNav: ListNav{ BasePath: "/operator", ParamPrefix: "act_", Page: 1, Total: 1, PerPage: embeddedListDefaultPerPage, DefaultPerPage: embeddedListDefaultPerPage, PerPageOptions: perPageOptions, Target: "#activity-panel", }, } } // The landing surface's lookup is a wide bar: one input group spanning the // content column, with the magnifier, the control and an outline "Look up" // on one line (design D20). Round 3 rendered it stacked and filled, a // width and a weight no other search had (maintainer, 2026-09-04). func TestOperatorOverviewLookupIsAWideBar(t *testing.T) { data := populatedOverview() data.LookupForm = lookupForm("") out := landingRegion(t, renderOperator(t, data)) for _, want := range []string{ `class="app-form app-form-bar-wide"`, `role="search"`, `
`, `At a glance`, // Each tile is a readout part inside a linked card // (overview-consistency D3, round 2): the card is the anchor, // marked by the chevron glyph, and the value is at the console's // one readout size. The label is the eyebrow, never a link. ``, `
`, `
People
`, `1,204`, // grouped value, formatted in Go `212 joined in the last 30 days`, "Team organizations", // deliberately created orgs, not personal ones `href="/operator/organizations"`, // tile drills into its section "Monthly recurring", // the money tile (design D3, round 2 restore) `
$2,295.00
`, `href="/operator/billing/subscriptions"`, } { if !strings.Contains(out, want) { t.Errorf("landing surface missing %q", want) } } // The retired tile markup: the label carrying the link style, a number // bigger than the page title, and an anchor inside the card's own // anchor. for _, banned := range []string{"link-primary", "display-6"} { if strings.Contains(out, banned) { t.Errorf("landing surface still carries the retired tile markup %q", banned) } } // The headline row is reserved for the three daily reads // (operator-panel-navigation "Headline row carries only the three daily // reads"): open-invoice, delivery, and catalog counts must not render // here. for _, forbidden := range []string{"Open invoices", "Delivering grants", "Catalog products"} { if strings.Contains(out, forbidden) { t.Errorf("landing surface still renders the removed tile %q", forbidden) } } // A tile with no Href must not render as a link (an unresolvable count // destination renders a static card, never an empty link target). if strings.Contains(out, `href=""`) { t.Errorf("hrefless stat tile rendered an empty link target") } } // TestOperatorOverviewTileChevronMarksOnlyLinkedTiles covers page-anatomy // "A linked card is marked by a glyph and the row's hover": the chevron // glyph marks a tile that is a link; a tile with no browse surface (no // .Href) carries no chevron (overview-consistency D3, round 2). func TestOperatorOverviewTileChevronMarksOnlyLinkedTiles(t *testing.T) { data := OperatorPageData{ Overview: OverviewData{Stats: []OverviewStat{ {Label: "Linked", Value: "1", Href: "/operator/persons", Available: true}, {Label: "Unlinked", Value: "2", Available: true}, }}, } out := landingRegion(t, renderOperator(t, data)) if n := strings.Count(out, ``) { t.Errorf("the unlinked tile must render a plain, unmarked card, got:\n%s", out) } } // An unavailable count must render an em dash. Rendering 0 would be an // actively misleading answer to "how many organizations are there". func TestOperatorOverviewUnavailableStatIsNotZero(t *testing.T) { data := OperatorPageData{ Overview: OverviewData{Stats: []OverviewStat{ {Label: "Organizations", Caption: "Active organizations", Href: "/operator/organizations", Available: false}, }}, } out := landingRegion(t, renderOperator(t, data)) if !strings.Contains(out, "—") { t.Errorf("unavailable stat did not render an em dash") } if !strings.Contains(out, "Count unavailable") { t.Errorf("unavailable stat did not explain itself") } // The tile still renders its label and link; only the number is withheld. if !strings.Contains(out, "Organizations") { t.Errorf("unavailable stat dropped its label") } if regexp.MustCompile(`fs-3[^>]*>\s*0\s*<`).MatchString(out) { t.Errorf("unavailable stat rendered a zero value") } } // TestOperatorOverviewFeedIsARecordTable covers overview-consistency D5: // the activity feed is a record table like every other, with a plain header // row, body-size cells, and the Summary as the row's primary cell placed // after the event kind. func TestOperatorOverviewFeedIsARecordTable(t *testing.T) { out := landingRegion(t, renderOperator(t, populatedOverview())) for _, want := range []string{ `

Recent activity

`, `
`, ``, ``, ``, // The feed pages through the list scaffold's pager, the same shape // as the composite's Tier changes (overview-consistency D5, round // 2), replacing the retired ?limit parameter. "Showing 1–1 of 1", } { if !strings.Contains(out, want) { t.Errorf("the activity feed is missing %q:\n%s", want, out) } } // The header row is plain, like every other table's (F5b): the styled // uppercase row is gone. if !regexp.MustCompile(`(?s)\s*`).MatchString(out) { t.Errorf("the activity feed's header row still carries classes:\n%s", out) } table := out[strings.Index(out, "([^<]+)`).FindAllStringSubmatch(out, -1) got := make([]string, 0, len(cols)) for _, c := range cols { got = append(got, c[1]) } want := []string{"Summary", "Event", "Organization", "Actor", "When"} if strings.Join(got, ",") != strings.Join(want, ",") { t.Errorf("activity columns = %v, want %v", got, want) } } // TestOperatorOverviewSystemPanel covers design D3's System region: one // Integrations card listing every registered provider, in registry order, // each with its name link and single Status; Stripe's row alone adds its // mode and outbox summary. func TestOperatorOverviewSystemPanel(t *testing.T) { out := landingRegion(t, renderOperator(t, populatedOverview())) for _, want := range []string{ `

System

`, // The card carries no title of its own: "Integrations" is a // category, and a card title names an instance // (overview-consistency D2). The providers are a flush list in the // card body, the setup checklist's idiom. `
`, `
    `, `
  • `, "FedWiki", `href="/operator/integrations/fedwiki"`, `Setup`) { t.Errorf("System header did not render its Setup action, got:\n%s", out) } } // Stripe not configured reads "Not configured" without alarm styling. func TestOperatorOverviewStripeNotConfigured(t *testing.T) { data := populatedOverview() data.Overview.Integrations.Rows[2] = OverviewIntegrationRow{ IntegrationRow: IntegrationRow{ Key: "stripe", DisplayName: "Stripe", Status: "active", SurfacePath: "/operator/integrations/stripe", Configured: false, }, StripeFacts: &OverviewStripeFacts{ModeLabel: "Test mode", Available: true}, } out := landingRegion(t, renderOperator(t, data)) if !strings.Contains(out, ``) { t.Errorf("empty landing surface dropped the lookup affordance") } // A zero-value Setup (no steps derived) has nothing to announce. if strings.Contains(out, `id="setup-banner"`) { t.Errorf("empty landing surface rendered the setup banner with no steps to report") } } // TestOperatorLandingRegionOrder covers operator-panel-navigation ("Landing // surface orders attention before totals"; maintainer 2026-08-23: the counts // an operator checks daily outrank the system cards): the lookup precedes At // a glance, At a glance precedes System, and System precedes Recent // activity — so attention-shaped content is still never the page's last // region. (The setup banner's own position, ahead of the lookup while it // shows, is covered separately in operator_setup_render_test.go.) func TestOperatorLandingRegionOrder(t *testing.T) { out := landingRegion(t, renderOperator(t, populatedOverview())) lookupIdx := strings.Index(out, `
    `) glanceIdx := strings.Index(out, `

    At a glance

    `) systemIdx := strings.Index(out, `

    System

    `) activityIdx := strings.Index(out, `

    Recent activity

    `) for name, idx := range map[string]int{"lookup": lookupIdx, "glance": glanceIdx, "system": systemIdx, "activity": activityIdx} { if idx < 0 { t.Fatalf("landing surface missing the %s region", name) } } if !(lookupIdx < glanceIdx && glanceIdx < systemIdx && systemIdx < activityIdx) { t.Errorf("landing regions out of order: lookup=%d glance=%d system=%d activity=%d, want lookup < glance < system < activity", lookupIdx, glanceIdx, systemIdx, activityIdx) } } // TestOperatorActivityFeedLinksToSubject covers operator-panel-navigation // ("Feed entries link to their subjects"): a grant/transition entry's // summary links to its organization's detail page, an invoice/payment // entry's summary links to the corresponding billing view, and an entry // with no addressable surface (Href empty) renders plain text rather than a // dead link. func TestOperatorActivityFeedLinksToSubject(t *testing.T) { data := OperatorPageData{ Activity: []ActivityEvent{ {EventType: "grant_issued", Timestamp: "t", OrgID: "org-1", OrgName: "Example Org", Summary: "Issued Example Plan", Href: "/operator/organizations/org-1"}, {EventType: "invoice_created", Timestamp: "t", OrgID: "org-1", OrgName: "Example Org", Summary: "Invoice $10.00", Href: "/operator/billing/invoices"}, {EventType: "payment_received", Timestamp: "t", OrgID: "org-1", OrgName: "Example Org", Summary: "Payment $10.00", Href: "/operator/billing/payments"}, {EventType: "grant_issued", Timestamp: "t", Summary: "No addressable subject"}, }, } out := landingRegion(t, renderOperator(t, data)) if !strings.Contains(out, `Issued Example Plan`) { t.Errorf("grant entry summary must link to the org detail page, got:\n%s", out) } if !strings.Contains(out, `Invoice $10.00`) { t.Errorf("invoice entry summary must link to the billing invoices view, got:\n%s", out) } if !strings.Contains(out, `Payment $10.00`) { t.Errorf("payment entry summary must link to the billing payments view, got:\n%s", out) } if strings.Contains(out, `No addressable subject`) { t.Errorf("an entry with no addressable surface must not render a dead (empty-href) link, got:\n%s", out) } // The Summary is the row's primary cell (overview-consistency D5), so // an entry with no addressable surface renders it as plain text in // that cell, at body size. if !strings.Contains(out, `
`) { t.Errorf("an entry with no addressable surface must render plain text in the primary cell, got:\n%s", out) } // Organization column always links straight to the org, independent of // the row's own Href (which for invoice/payment rows points elsewhere). if n := strings.Count(out, `Example Org`); n != 3 { t.Errorf("expected 3 Organization-column links to the org, got %d in:\n%s", n, out) } } // The landing surface is the page screenshotted for the project README, so // its document outline has to be defensible: exactly one H1, no skipped // levels. The older operator pages run H1 straight to H6; that pattern must // not spread here. func TestOperatorOverviewHeadingHierarchy(t *testing.T) { out := landingRegion(t, renderOperator(t, populatedOverview())) matches := regexp.MustCompile(`(?i)]`).FindAllStringSubmatch(out, -1) if len(matches) == 0 { t.Fatalf("landing surface rendered no headings at all") } levels := make([]int, 0, len(matches)) for _, m := range matches { n, err := strconv.Atoi(m[1]) if err != nil { t.Fatalf("unparsable heading level %q", m[1]) } levels = append(levels, n) } if levels[0] != 1 { t.Errorf("first heading is h%d, want h1", levels[0]) } h1Count := 0 for _, l := range levels { if l == 1 { h1Count++ } } if h1Count != 1 { t.Errorf("found %d h1 elements, want exactly 1", h1Count) } for i := 1; i < len(levels); i++ { if levels[i] > levels[i-1]+1 { t.Errorf("heading level skips from h%d to h%d at position %d (levels: %v)", levels[i-1], levels[i], i, levels) } } // Guard the specific regression: no h4/h5/h6 on this surface at all. for _, l := range levels { if l > 3 { t.Errorf("landing surface uses h%d; the overview outline stops at h3 (levels: %v)", l, levels) } } } // TestOperatorOverviewCarriesNoTrail covers design D18: the operator // overview is one of exactly two surface roots (with the member dashboard) // that carry no location trail, so it renders no breadcrumb nav at all — // unlike every other operator page, which is rooted at the surface. func TestOperatorOverviewCarriesNoTrail(t *testing.T) { out := landingRegion(t, renderOperator(t, populatedOverview())) if strings.Contains(out, `aria-label="breadcrumb"`) || strings.Contains(out, "breadcrumb-item") { t.Errorf("the operator overview must render no breadcrumb nav, got:\n%s", out) } } // Strict CSP: nothing on this surface may rely on an inline script, an inline // event handler, or a style attribute. func TestOperatorOverviewIsCSPClean(t *testing.T) { out := landingRegion(t, renderOperator(t, populatedOverview())) if regexp.MustCompile(`(?i)]*)?>[^<]`).MatchString(out) { t.Errorf("landing surface contains an inline
Jan 2, 2026 3:04 PMIssued Example Plan (promo)
No addressable subject