- 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
182 lines
6.6 KiB
Go
182 lines
6.6 KiB
Go
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
|
|
// SPDX-FileCopyrightText: 2025-2026 Christian Galo
|
|
|
|
package server
|
|
|
|
import (
|
|
"bytes"
|
|
"html/template"
|
|
"io/fs"
|
|
"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"
|
|
)
|
|
|
|
// TestOperatorTemplateRendersBothBranches parses operator.html and renders
|
|
// both dispatch branches (curated landing surface and the MPA BodyTemplate
|
|
// dispatch) to catch Go-template syntax errors that would otherwise only
|
|
// surface at request time. The legacy tabstrip branch was retired in slice
|
|
// 8 of operator-mpa-conversion §5.
|
|
func TestOperatorTemplateRendersBothBranches(t *testing.T) {
|
|
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)
|
|
}
|
|
// operator.html references the `renderBody` and `routeURL` template
|
|
// funcs and includes partials like operator_lookup_result.html via
|
|
// `{{ template "..." . }}`. Mirror NewOperatorPartialsHandler: load
|
|
// partials first, then the shell.
|
|
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,
|
|
"helpIcon": helpIcon,
|
|
})
|
|
tmpl, err = web.ParseUIPartials(template.Must(tmpl.ParseFS(partialsSub, "operator_*.html")))
|
|
if err != nil {
|
|
t.Fatalf("ParseFS partials: %v", err)
|
|
}
|
|
tmpl, err = tmpl.ParseFS(sub, "operator.html")
|
|
if 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})
|
|
|
|
cases := []struct {
|
|
name string
|
|
data OperatorPageData
|
|
want string
|
|
}{
|
|
{
|
|
name: "landing",
|
|
data: OperatorPageData{
|
|
IAPosition: "runtime:landing",
|
|
},
|
|
// The lookup region names itself to screen readers with
|
|
// aria-label; it carries no heading of its own
|
|
// (overview-consistency D1).
|
|
want: `<section aria-label="Lookup">`,
|
|
},
|
|
{
|
|
// BodyTemplate dispatch branch must short-circuit the landing
|
|
// surface; the stub renderBody returns "" so the assertion is
|
|
// that the shell still renders (no landing markup leaks in).
|
|
name: "body-template-dispatch",
|
|
data: OperatorPageData{
|
|
IAPosition: "runtime:organizations",
|
|
ActiveCapability: "organizations",
|
|
BodyTemplate: "operator_organizations.html",
|
|
BodyData: nil,
|
|
},
|
|
want: `id="operator-main"`,
|
|
},
|
|
{
|
|
// Dedicated Integrations section: the sidebar renders a labeled
|
|
// "Integrations" home link to /operator/integrations. Provider
|
|
// surfaces are reached from that home's provider cards, not from
|
|
// sidebar children (maintainer 2026-08-23).
|
|
name: "integration-section-home",
|
|
data: OperatorPageData{
|
|
ActiveCapability: "integrations",
|
|
},
|
|
want: `href="/operator/integrations">Integrations</a>`,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
if err := tmpl.ExecuteTemplate(&buf, "operator.html", tc.data); err != nil {
|
|
t.Fatalf("ExecuteTemplate: %v", err)
|
|
}
|
|
out := buf.String()
|
|
if !strings.Contains(out, tc.want) {
|
|
t.Errorf("rendered output missing %q", tc.want)
|
|
}
|
|
if !strings.Contains(out, `hx-boost:inherited="true"`) {
|
|
t.Errorf("rendered output missing hx-boost:inherited on body (htmx 4 explicit inheritance)")
|
|
}
|
|
if !strings.Contains(out, `id="operator-main"`) {
|
|
t.Errorf("rendered output missing main#operator-main swap target")
|
|
}
|
|
// Slice 8 guard: legacy tabstrip markup must not reappear.
|
|
if strings.Contains(out, `id="operatorTabs"`) {
|
|
t.Errorf("legacy tabstrip leaked back into operator.html — re-check §5 retirement")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestOperatorIntegrationsTemplate renders the Integrations landing body
|
|
// partial directly (the operator.html smoke test stubs renderBody, so the body
|
|
// markup is not exercised there). Covers a populated provider row and the
|
|
// empty state.
|
|
func TestOperatorIntegrationsTemplate(t *testing.T) {
|
|
partialsSub, err := fs.Sub(embeds.Templates, "templates/partials")
|
|
if err != nil {
|
|
t.Fatalf("fs.Sub partials: %v", err)
|
|
}
|
|
tmpl := template.New("integrations").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 "" },
|
|
"helpIcon": helpIcon,
|
|
})
|
|
tmpl, err = web.ParseUIPartials(template.Must(tmpl.ParseFS(partialsSub, "operator_*.html")))
|
|
if err != nil {
|
|
t.Fatalf("ParseFS partials: %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})
|
|
|
|
t.Run("populated", func(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
data := IntegrationsData{Integrations: []IntegrationRow{{
|
|
Key: "example",
|
|
DisplayName: "Example",
|
|
Kind: "provisioning",
|
|
Status: "active",
|
|
Configured: true,
|
|
SurfacePath: "/operator/example-sites",
|
|
Operations: []string{"create", "delete", "set_status"},
|
|
ResourceKeys: []ResourceKeyOption{{ResourceKey: "example_sites", DisplayName: "Example sites"}},
|
|
}}}
|
|
if err := tmpl.ExecuteTemplate(&buf, "operator_integrations.html", data); err != nil {
|
|
t.Fatalf("ExecuteTemplate: %v", err)
|
|
}
|
|
out := buf.String()
|
|
for _, want := range []string{"Example", "example_sites", "Example sites", "set_status", `<th scope="row">`, `href="/operator/example-sites">Example</a>`, "Configured"} {
|
|
if !strings.Contains(out, want) {
|
|
t.Errorf("integrations landing missing %q", want)
|
|
}
|
|
}
|
|
if strings.Contains(out, ">Manage<") {
|
|
t.Errorf("integrations landing still renders a Manage control")
|
|
}
|
|
})
|
|
|
|
t.Run("empty", func(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
if err := tmpl.ExecuteTemplate(&buf, "operator_integrations.html", IntegrationsData{}); err != nil {
|
|
t.Fatalf("ExecuteTemplate empty: %v", err)
|
|
}
|
|
if !strings.Contains(buf.String(), "No integrations registered") {
|
|
t.Errorf("empty state not rendered")
|
|
}
|
|
})
|
|
}
|