// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial // SPDX-FileCopyrightText: 2025-2026 Christian Galo package server import ( "bytes" "html/template" "io/fs" "regexp" "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" ) // checklistPageTemplate parses the same template set NewOperatorHandler // builds, renderBody included — unlike overviewTemplate (in // operator_overview_render_test.go), whose renderBody is a landing-only // no-op stub. GetSetupPage dispatches through BodyTemplate, so exercising it // needs the real dispatcher. func checklistPageTemplate(t *testing.T) *template.Template { t.Helper() sub, err := fs.Sub(embeds.Templates, "templates") if err != nil { t.Fatalf("fs.Sub: %v", err) } var tmpl *template.Template tmpl = template.New("operator").Funcs(template.FuncMap{ "renderBody": func(name string, data any) (template.HTML, error) { var buf bytes.Buffer if err := tmpl.ExecuteTemplate(&buf, name, data); err != nil { return "", err } return template.HTML(buf.String()), nil }, "deploymentName": config.DeploymentName, "pageTitle": pageTitle, }) if tmpl, err = web.ParseUIPartials(template.Must(tmpl.ParseFS(sub, "operator.html", "partials/operator_lookup_result.html", "partials/operator_setup.html"))); err != nil { t.Fatalf("ParseFS: %v", err) } return tmpl } func renderChecklistPage(t *testing.T, data OperatorPageData) string { t.Helper() var buf bytes.Buffer if err := checklistPageTemplate(t).ExecuteTemplate(&buf, "operator.html", data); err != nil { t.Fatalf("ExecuteTemplate: %v", err) } return buf.String() } // Render coverage for the setup checklist (setup_state.go, operator_setup.go, // partials/operator_setup.html; design D12 / operator-setup-checklist) and // the operator overview's setup banner (operator.html; design D12's "The // overview carries a dismissible setup banner"). Template-level only, // executed against hand-built OperatorPageData/SetupState the same way // operator_overview_render_test.go covers the landing surface's other // regions — reuses that file's overviewTemplate/renderOperator/ // landingRegion/populatedOverview helpers (same package, same test binary). // Live-state derivation is covered separately by setup_state_test.go's // DB-backed tests. // mixedSetupState is a representative checklist mid-progress: two steps // done (entitlement-set, product), four still open, including the two // whose relevance depends on the deployment's model (price-sync, // org-type-default) and the price step's Stripe-not-configured note. Every // step is equal now (design D12, round 2): no Required/Condition fields, // no "Required" label, one tone per completion state. func mixedSetupState() SetupState { return SetupState{ Steps: []SetupStep{ { Key: "integrations", Label: "Configure integrations", Description: "Integrations deliver what a product confers through a connected service. Needed when a product delivers through a connected service.", Complete: false, Href: "/operator/integrations", LinkText: "Go to integrations", Unlocks: "Selling or granting a product that delivers through that service.", }, { Key: "entitlement-set", Label: "Create an entitlement set", Description: "Entitlement sets define what a product grants; every product needs one.", Complete: true, Href: "/operator/entitlement-sets", LinkText: "Go to entitlement sets", Unlocks: "Creating a product that uses it.", }, { Key: "product", Label: "Create a product", Description: "Products are what this deployment grants or sells; each product uses an entitlement set and is published as soon as it is created.", Complete: true, Href: "/operator/products", LinkText: "Go to products", Unlocks: "Adding the product to a plan ladder, and selling it.", }, { Key: "price-sync", Label: "Add an active price and sync it to Stripe", Description: "A price makes a product chargeable; syncing it creates the matching Stripe price. Needed only when products are sold for money.", Complete: false, Href: "/operator/products", LinkText: "Go to products", Unlocks: "Charging members for this product.", Note: "Stripe integration is not configured yet; missing stripe-api-key.", NoteHref: "/operator/integrations/stripe/settings", NoteLinkText: "Go to Stripe settings", }, { Key: "ladder", Label: "Build a plan ladder with this product as a tier", Description: "Plan ladders group the products members move between; the rank-0 tier is the deployment's base plan and is what completes this step.", Complete: false, Href: "/operator/plan-ladders", LinkText: "Go to plan ladders", Unlocks: "Choosing an org-type default plan.", }, { Key: "org-type-default", Label: "Choose an org-type default plan", Description: "An org-type default plan is conferred automatically on signup. Needed only when signup should confer a plan automatically.", Complete: false, Href: "/operator/org-types", LinkText: "Go to org types", }, }, } } func TestSetupChecklistPageRendersAllSteps(t *testing.T) { out := renderChecklistPage(t, OperatorPageData{ IAPosition: "runtime:setup", BodyTemplate: "operator_setup.html", BodyData: mixedSetupState(), }) for _, want := range []string{ "