package server import ( "bytes" "html/template" "io/fs" "strings" "testing" "git.coopcloud.tech/wiki-cafe/member-console/internal/embeds" "git.coopcloud.tech/wiki-cafe/member-console/internal/web" ) // renderOperatorBilling executes the real operator_billing.html wrapper // template, mirroring how NewOperatorPartialsHandler loads the operator // partials. renderBody is stubbed (as in other isolated partial render // tests): this test only exercises the wrapper's own markup (nav pills, the // Stripe Sync vocabulary legend, the webhook-recency stamp), not the inner // per-tab tables. func renderOperatorBilling(t *testing.T, data OperatorBillingWrapperData) string { t.Helper() 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 "" }, }) tmpl, err = tmpl.ParseFS(partialsSub, "operator_*.html") if err != nil { t.Fatalf("ParseFS: %v", err) } var buf bytes.Buffer if err := tmpl.ExecuteTemplate(&buf, "operator_billing.html", data); err != nil { t.Fatalf("ExecuteTemplate: %v", err) } return buf.String() } // TestOperatorBillingNotMappedVocabularyIsDefinedInPlace covers the // operator-billing-views delta: every one of the four billing sub-pages // shares this one wrapper, so a legend rendered here reaches all four // without per-tab duplication (design.md; tasks 4.1). func TestOperatorBillingNotMappedVocabularyIsDefinedInPlace(t *testing.T) { out := renderOperatorBilling(t, OperatorBillingWrapperData{ ActiveSection: "accounts", InnerTemplate: "operator_billing_accounts.html", DataAsOf: "Jan 2, 2026 15:04 MST", }) if !strings.Contains(out, "Not Mapped") { t.Error("legend missing the vocabulary term it defines") } if !strings.Contains(out, "no Stripe object is linked") { t.Error("legend does not state what an unmapped record means") } if !strings.Contains(out, "check its sync status") && !strings.Contains(out, "Stripe integration settings") { t.Error("legend does not say what to do about an unmapped record") } } // TestOperatorBillingRecencyStamp covers the operator-billing-views delta's // projection-recency requirement (design.md Decision 3; task 4.2): every // billing sub-page states when data was last projected, or states plainly // that nothing has been received yet, rather than presenting rows or // emptiness as if current. func TestOperatorBillingRecencyStamp(t *testing.T) { t.Run("recency stamp renders with data", func(t *testing.T) { out := renderOperatorBilling(t, OperatorBillingWrapperData{ ActiveSection: "invoices", InnerTemplate: "operator_invoices.html", DataAsOf: "Jan 2, 2026 15:04 MST", }) if !strings.Contains(out, "Data as of Jan 2, 2026 15:04 MST") { t.Errorf("recency stamp missing or malformed: %s", out) } if strings.Contains(out, "No billing events have been received") { t.Error("no-events copy rendered even though DataAsOf was set") } }) t.Run("no events ever processed is stated", func(t *testing.T) { out := renderOperatorBilling(t, OperatorBillingWrapperData{ ActiveSection: "payments", InnerTemplate: "operator_payments.html", NoEventsProcessed: true, }) if !strings.Contains(out, "No billing events have been received") { t.Errorf("missing the no-events-ever statement: %s", out) } if strings.Contains(out, "Data as of") { t.Error("stale 'Data as of' copy rendered with no processed events") } }) }