Files
member-console/internal/server/operator_overview_render_test.go
cgalo5758 257955c9d3 Add operator list-scale contract and People directory
Governed operator lists (organizations, grants, people, billing×4) gain
server-side search, status filters, and 50-row pages with true totals
from count(*) OVER(); state is URL-addressable, out-of-range pages
clamp,
and no-match is distinct from true-empty.

People is the eighth flat sidebar entry: /operator/persons lists persons
newest-joined first (excluding the reserved system person), rows linking
to the existing detail.

Billing gains an operator invoice detail at
/operator/billing/invoices/{invoiceID} reusing the member projection;
open invoices past due present as Overdue (derived, filterable, stored
status untouched); all four views lead with the linked organization and
mute object IDs.

Grants filter over the derived Live/Superseded/Inactive state, the SQL
HAVING predicate pinned to the Go derivation by test. Embedded lists
(org composite ledger, Tier changes) adopt the shared controls under
namespaced params with sibling-state-preserving URLs and scoped htmx
swaps that hold the viewport.

Review corrections: blocked ladder Delete renders disabled with tooltip
and mutations fire toasts; collapse triggers paint their open state;
sections use outside headings; plan topology drops the orphan-product
check; domains policy collapses behind a disclosure.
2026-08-24 03:58:18 -05:00

643 lines
26 KiB
Go

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/billing"
"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/entitlements"
"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"
)
// 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,
})
if tmpl, err = 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)
}
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 <main> (or outside #operator-body) and carry their own markup —
// including an <h5> 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 = `<div id="operator-body">`
start := strings.Index(out, openTag)
if start < 0 {
t.Fatalf("rendered output has no #operator-body region")
}
end := strings.Index(out, "</main>")
if end < start {
t.Fatalf("rendered output has no </main> after #operator-body")
}
return out[start+len(openTag) : end]
}
// populatedOverview is a representative landing payload: every tile
// available, a mix of healthy and unhealthy integrations, and a draining
// queue.
func populatedOverview() OperatorPageData {
return OperatorPageData{
CSRFToken: "csrf",
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},
{Label: "Open invoices", Value: "3", Caption: "$372.00 outstanding; issued and awaiting payment", Href: "/operator/billing/invoices", Available: true},
{Label: "Delivering grants", Value: "7", Caption: "Plus 80 signup defaults", Href: "/operator/grants", Available: true},
},
Providers: []OverviewProvider{
{Slug: "alpha", DisplayName: "Alpha", Kind: "provisioning", Status: "active", SurfacePath: "/operator/alpha-sites", Healthy: true, Configured: true},
{Slug: "beta", DisplayName: "Beta", Kind: "billing", Status: "disabled", MissingKeysText: "beta-api-key"},
},
Queue: OverviewQueue{Pending: 4, Retrying: 1, 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",
}},
}
}
func TestOperatorOverviewRendersStatTiles(t *testing.T) {
out := landingRegion(t, renderOperator(t, populatedOverview()))
for _, want := range []string{
"At a glance",
"People",
`href="/operator/persons"`, // tile drills into the directory (ux-operator-scale)
"1,204", // grouped value, formatted in Go
"Team organizations", // deliberately created orgs, not personal ones
`href="/operator/organizations"`, // tile drills into its section
"Monthly recurring", // commitment as money, not a bare count
"$2,295.00", // largest-currency-bucket value
`href="/operator/billing/subscriptions"`,
"Delivering grants", // operator-issued grants; defaults ride the caption
"Plus 80 signup defaults", // composition caption
`href="/operator/grants"`,
"Open invoices", // receivables tile
`href="/operator/billing/invoices"`, // drills into the invoices page
"$372.00 outstanding", // caption leads with the balance
} {
if !strings.Contains(out, want) {
t.Errorf("landing surface missing %q", want)
}
}
// 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")
}
}
// 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{
CSRFToken: "csrf",
Overview: OverviewData{Stats: []OverviewStat{
{Label: "Organizations", Caption: "Active organizations", Href: "/operator/organizations", Available: false},
}},
}
out := landingRegion(t, renderOperator(t, data))
if !strings.Contains(out, "&mdash;") {
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(`display-6[^>]*>\s*0\s*<`).MatchString(out) {
t.Errorf("unavailable stat rendered a zero value")
}
}
func TestOperatorOverviewSystemPanel(t *testing.T) {
out := landingRegion(t, renderOperator(t, populatedOverview()))
for _, want := range []string{
"System",
"Integrations",
"Alpha",
`href="/operator/alpha-sites"`, // provider with a surface links to it
"provisioning", // provider kind is shown
`<span class="badge text-bg-success`, // healthy provider
"Beta",
`<span class="badge text-bg-warning`, // unhealthy provider
"Not configured", // Beta's configuration-readiness signal
"Delivery queue",
"Stripe", // outbox coverage caption names what it tracks
"Pending",
"Retrying",
"Dead-letter",
"draining normally",
} {
if !strings.Contains(out, want) {
t.Errorf("system panel missing %q", want)
}
}
// A provider without a surface path must not become a dead link.
if strings.Contains(out, `href="">Beta</a>`) {
t.Errorf("surfaceless provider rendered an empty link")
}
// Alpha is fully configured; only Beta's row should carry the
// not-configured marker (ux-honest-surfaces: the signal is a negative
// marker, never a redundant positive claim).
if n := strings.Count(out, "Not configured"); n != 1 {
t.Errorf("system panel shows %d \"Not configured\" markers, want exactly 1 (Beta only)", n)
}
// The registry-status badge must not read as a live health/connectivity
// claim (integration-settings spec: "cannot read as a health check").
if strings.Contains(out, "read live") || strings.Contains(out, "Connected") || strings.Contains(out, "connected to") {
t.Errorf("system panel implies a live health/connectivity check")
}
}
// Dead-lettered outbox work is the one queue bucket nothing will clear on its
// own, so it must be called out rather than shown as another neutral number.
func TestOperatorOverviewDeadLetterRaisesAlarm(t *testing.T) {
data := populatedOverview()
data.Overview.Queue = OverviewQueue{Pending: 2, Retrying: 0, DeadLetter: 3, Available: true}
out := landingRegion(t, renderOperator(t, data))
if !strings.Contains(out, "text-danger") {
t.Errorf("dead-lettered work did not render with alarm styling")
}
if !strings.Contains(out, "exhausted its retries") {
t.Errorf("dead-letter state did not explain what an operator must do")
}
if strings.Contains(out, "draining normally") {
t.Errorf("dead-lettered queue still claimed to be draining normally")
}
}
// The landing surface is the first thing a fresh deployment shows, so every
// region needs a legible empty state rather than a blank panel.
func TestOperatorOverviewEmptyStates(t *testing.T) {
out := landingRegion(t, renderOperator(t, OperatorPageData{CSRFToken: "csrf"}))
for _, want := range []string{
"Counts are unavailable.",
"No activity recorded yet.",
"No integrations registered.",
"Queue health is unavailable.",
} {
if !strings.Contains(out, want) {
t.Errorf("empty landing surface missing %q", want)
}
}
// The lookup affordance is the one thing that must work on an empty
// deployment, so it renders regardless.
if !strings.Contains(out, "Find a person or organization") {
t.Errorf("empty landing surface dropped the lookup affordance")
}
}
// 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): setup (while required
// steps are incomplete) 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.
func TestOperatorLandingRegionOrder(t *testing.T) {
data := populatedOverview()
data.Setup = SetupState{Steps: []SetupStep{{Key: "step", Required: true, Complete: false}}}
out := landingRegion(t, renderOperator(t, data))
setupIdx := strings.Index(out, `id="overview-setup-heading"`)
systemIdx := strings.Index(out, `id="overview-system-heading"`)
glanceIdx := strings.Index(out, `id="overview-glance-heading"`)
activityIdx := strings.Index(out, `id="overview-activity-heading"`)
for name, idx := range map[string]int{"setup": setupIdx, "system": systemIdx, "glance": glanceIdx, "activity": activityIdx} {
if idx < 0 {
t.Fatalf("landing surface missing the %s region", name)
}
}
if !(setupIdx < glanceIdx && glanceIdx < systemIdx && systemIdx < activityIdx) {
t.Errorf("landing regions out of order: setup=%d glance=%d system=%d activity=%d, want setup < glance < system < activity",
setupIdx, glanceIdx, systemIdx, activityIdx)
}
}
// TestOperatorLandingGlanceBeforeSystemEvenWithoutSetup covers the same
// requirement's scenario directly (the metric tiles render before the
// system-status region) for the common case where setup is not showing.
func TestOperatorLandingGlanceBeforeSystemEvenWithoutSetup(t *testing.T) {
out := landingRegion(t, renderOperator(t, populatedOverview()))
systemIdx := strings.Index(out, `id="overview-system-heading"`)
glanceIdx := strings.Index(out, `id="overview-glance-heading"`)
if systemIdx < 0 || glanceIdx < 0 {
t.Fatalf("landing surface missing System or At a glance region")
}
if glanceIdx >= systemIdx {
t.Errorf("At a glance must render before System status (glance=%d system=%d)", glanceIdx, systemIdx)
}
}
// 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{
CSRFToken: "csrf",
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, `<a href="/operator/organizations/org-1">Issued Example Plan</a>`) {
t.Errorf("grant entry summary must link to the org detail page, got:\n%s", out)
}
if !strings.Contains(out, `<a href="/operator/billing/invoices">Invoice $10.00</a>`) {
t.Errorf("invoice entry summary must link to the billing invoices view, got:\n%s", out)
}
if !strings.Contains(out, `<a href="/operator/billing/payments">Payment $10.00</a>`) {
t.Errorf("payment entry summary must link to the billing payments view, got:\n%s", out)
}
if strings.Contains(out, `<a href="">No addressable subject</a>`) {
t.Errorf("an entry with no addressable surface must not render a dead (empty-href) link, got:\n%s", out)
}
if !strings.Contains(out, `<small>No addressable subject</small>`) {
t.Errorf("an entry with no addressable surface must render plain text, 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, `<a href="/operator/organizations/org-1">Example Org</a>`); 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)<h([1-6])[\s>]`).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)
}
}
}
// 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)<script(\s[^>]*)?>[^<]`).MatchString(out) {
t.Errorf("landing surface contains an inline <script> body")
}
if m := regexp.MustCompile(`(?i)\son(click|load|change|submit|input|focus|error)\s*=`).FindString(out); m != "" {
t.Errorf("landing surface contains an inline event handler: %q", strings.TrimSpace(m))
}
if regexp.MustCompile(`(?i)\sstyle\s*=\s*"`).MatchString(out) {
t.Errorf("landing surface contains an inline style attribute")
}
}
// Brand neutrality: this is general-purpose OSS and the operator surface is
// the page a stranger evaluating the project sees first.
func TestOperatorShellIsBrandNeutral(t *testing.T) {
out := renderOperator(t, populatedOverview())
for _, forbidden := range []string{"Wiki Cafe", "wiki.cafe", "WikiCafe"} {
if strings.Contains(out, forbidden) {
t.Errorf("operator shell names a specific deployment: %q", forbidden)
}
}
if !strings.Contains(out, "<title>Operator overview - Member Console</title>") {
t.Errorf("operator shell lost its brand-neutral title")
}
}
func TestFormatCount(t *testing.T) {
cases := []struct {
in int64
want string
}{
{0, "0"},
{7, "7"},
{999, "999"},
{1000, "1,000"},
{1204, "1,204"},
{12480, "12,480"},
{999999, "999,999"},
{1000000, "1,000,000"},
{-4200, "-4,200"},
}
for _, tc := range cases {
if got := formatCount(tc.in); got != tc.want {
t.Errorf("formatCount(%d) = %q, want %q", tc.in, got, tc.want)
}
}
}
// The "Delivering grants" caption states its composition (signup defaults
// vs deliberate grants) without echoing the headline, which the
// grant-backed provision count equals by construction.
func TestDeliveringGrantsCaptionStatesComposition(t *testing.T) {
cases := []struct {
name string
defaults int64
err error
want string
}{
{"plural", 2589, nil, "Plus 2,589 signup defaults"},
{"singular", 1, nil, "Plus 1 signup default"},
{"unavailable", 0, errors.New("boom"), "Signup defaults excluded"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := deliveringGrantsCaption(tc.defaults, tc.err)
if got != tc.want {
t.Errorf("deliveringGrantsCaption(%d, %v) = %q, want %q", tc.defaults, tc.err, got, tc.want)
}
})
}
}
// Money never crosses currencies on this surface: largest bucket wins the
// headline, the caption acknowledges the rest, and formatting matches the
// member-facing price renderer.
func TestMoneyStatPicksLargestBucket(t *testing.T) {
stat := newMoneyStat("Monthly recurring", nil, []moneyBucket{
{currency: "eur", cents: 4200},
{currency: "usd", cents: 229500},
}, "caption", "/operator/billing/subscriptions")
if !stat.Available || stat.Value != "$2,295.00" {
t.Errorf("money stat = %+v, want available $2,295.00 (largest bucket)", stat)
}
empty := newMoneyStat("Monthly recurring", nil, nil, "caption", "")
if !empty.Available || empty.Value != "0" {
t.Errorf("no-subscriptions money stat = %+v, want available bare 0", empty)
}
failed := newMoneyStat("Monthly recurring", errors.New("boom"), nil, "caption", "")
if failed.Available || failed.Value != "" {
t.Errorf("failed money stat = %+v, want unavailable with no value", failed)
}
}
func TestRecurringCaption(t *testing.T) {
if got, want := recurringCaption(205, 9, nil, 1),
"205 active subscriptions; 9 trialing"; got != want {
t.Errorf("recurringCaption = %q, want %q", got, want)
}
if got, want := recurringCaption(205, 0, nil, 3),
"205 active subscriptions; plus 2 more currencies"; got != want {
t.Errorf("multi-currency recurringCaption = %q, want %q", got, want)
}
if got, want := recurringCaption(0, 0, errors.New("boom"), 0),
"Active subscriptions"; got != want {
t.Errorf("failed recurringCaption = %q, want %q", got, want)
}
}
func TestOpenInvoicesCaption(t *testing.T) {
if got, want := openInvoicesCaption([]moneyBucket{{currency: "usd", cents: 37200}}, nil),
"$372.00 outstanding; issued and awaiting payment"; got != want {
t.Errorf("openInvoicesCaption = %q, want %q", got, want)
}
if got, want := openInvoicesCaption(nil, nil),
"Issued and awaiting payment"; got != want {
t.Errorf("empty openInvoicesCaption = %q, want %q", got, want)
}
}
func TestPeopleCaption(t *testing.T) {
if got, want := peopleCaption(212, nil), "212 joined in the last 30 days"; got != want {
t.Errorf("peopleCaption = %q, want %q", got, want)
}
if got := peopleCaption(0, errors.New("boom")); !strings.Contains(got, "search above") {
t.Errorf("failed peopleCaption = %q, want the lookup fallback", got)
}
}
// The headline row is reserved for core capability state
// (operator-overview-polish D5): pinning loadOverview's labels and order
// keeps a feature tile — the removed domain-claims one, or a future
// equivalent — from slipping back in without failing here. The fakes embed
// their Querier interfaces, so loadOverview calling any count not stubbed
// below panics, which doubles as a check on what the loader is allowed to
// read.
type overviewBillingQ struct{ billing.Querier }
func (overviewBillingQ) CountLiveSubscriptionsByStatus(context.Context) (billing.CountLiveSubscriptionsByStatusRow, error) {
return billing.CountLiveSubscriptionsByStatusRow{ActiveCount: 205, TrialingCount: 9}, nil
}
func (overviewBillingQ) SumMonthlyRecurringByCurrency(context.Context) ([]billing.SumMonthlyRecurringByCurrencyRow, error) {
return []billing.SumMonthlyRecurringByCurrencyRow{{Currency: "usd", MonthlyCents: 229500}}, nil
}
func (overviewBillingQ) CountOpenInvoices(context.Context) (int64, error) { return 3, nil }
func (overviewBillingQ) SumOpenInvoiceBalanceByCurrency(context.Context) ([]billing.SumOpenInvoiceBalanceByCurrencyRow, error) {
return []billing.SumOpenInvoiceBalanceByCurrencyRow{{Currency: "usd", OutstandingCents: 37200}}, nil
}
func (overviewBillingQ) CountPublishedProducts(context.Context) (int64, error) { return 5, nil }
type overviewIdentityQ struct{ identity.Querier }
func (overviewIdentityQ) CountActivePersons(context.Context) (int64, error) { return 1204, nil }
func (overviewIdentityQ) CountPersonsJoinedLast30Days(context.Context) (int64, error) {
return 212, nil
}
type overviewOrgQ struct{ organization.Querier }
func (overviewOrgQ) CountTeamOrganizations(context.Context) (int64, error) { return 38, nil }
type overviewEntitlementsQ struct{ entitlements.Querier }
func (overviewEntitlementsQ) CountDeliveringOperatorGrants(context.Context) (int64, error) {
return 7, nil
}
func (overviewEntitlementsQ) CountDeliveringDefaultGrants(context.Context) (int64, error) {
return 80, nil
}
func TestLoadOverviewTileSet(t *testing.T) {
h := &OperatorHandler{
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
BillingQ: overviewBillingQ{},
EntitlementsQ: overviewEntitlementsQ{},
OrgQ: overviewOrgQ{},
IdentityQ: overviewIdentityQ{},
}
stats := h.loadOverview(context.Background()).Stats
got := make([]string, len(stats))
for i, s := range stats {
got[i] = s.Label
}
want := []string{"People", "Team organizations", "Monthly recurring", "Open invoices", "Delivering grants", "Catalog products"}
if len(got) != len(want) {
t.Fatalf("tile labels = %v, want %v", got, want)
}
for i := range want {
if got[i] != want[i] {
t.Errorf("tile %d = %q, want %q (full set %v)", i, got[i], want[i], got)
}
}
if mrr := stats[2]; mrr.Value != "$2,295.00" || mrr.Href != "/operator/billing/subscriptions" ||
mrr.Caption != "205 active subscriptions; 9 trialing" {
t.Errorf("Monthly recurring tile = %+v, want $2,295.00 with the active/trialing caption", mrr)
}
if inv := stats[3]; inv.Value != "3" || inv.Caption != "$372.00 outstanding; issued and awaiting payment" {
t.Errorf("Open invoices tile = %+v, want count 3 with the outstanding caption", inv)
}
if ppl := stats[0]; ppl.Caption != "212 joined in the last 30 days" || ppl.Href != "/operator/persons" {
t.Errorf("People tile = %+v, want the 30-day trend caption and the directory link", ppl)
}
if g := stats[4]; g.Value != "7" || g.Caption != "Plus 80 signup defaults" {
t.Errorf("Delivering grants tile = %+v, want operator-issued count 7 with defaults in the caption", g)
}
// House style for this row: captions use no em dashes (the unavailable
// value marker is separate, spec-mandated semantics).
for _, s := range stats {
if strings.Contains(s.Caption, "—") {
t.Errorf("tile %q caption %q contains an em dash", s.Label, s.Caption)
}
}
}
// The console's card idiom is a plain bordered card. The landing surface
// shipped with a bespoke elevation/hover treatment and lost it in
// operator-overview-polish; this pins the whole rendered shell, hero
// included, so it cannot come back silently.
func TestOperatorOverviewUsesPlainCardIdiom(t *testing.T) {
out := renderOperator(t, populatedOverview())
for _, forbidden := range []string{"shadow-sm", "stat-tile"} {
if strings.Contains(out, forbidden) {
t.Errorf("landing surface carries bespoke card styling %q", forbidden)
}
}
}
func TestOverviewQueueNeedsAttention(t *testing.T) {
if (OverviewQueue{Pending: 99, Retrying: 12, Available: true}).NeedsAttention() {
t.Errorf("pending and retrying work must not raise an alarm on its own")
}
if !(OverviewQueue{DeadLetter: 1, Available: true}).NeedsAttention() {
t.Errorf("dead-lettered work must raise an alarm")
}
}
func TestNewOverviewStatWithholdsValueOnError(t *testing.T) {
ok := newOverviewStat("Organizations", nil, 42, "caption", "/operator/organizations")
if !ok.Available || ok.Value != "42" {
t.Errorf("successful stat = %+v, want available with value 42", ok)
}
bad := newOverviewStat("Organizations", errors.New("boom"), 42, "caption", "/operator/organizations")
if bad.Available {
t.Errorf("failed stat reported itself available")
}
if bad.Value != "" {
t.Errorf("failed stat carried a value %q; the template must have nothing to print", bad.Value)
}
}