package view import ( "bytes" "net/http" "net/http/httptest" "strings" "testing" ) // Tests of the component kit's own code: the template functions, each partial // in each of its states and the shell's visibility rules. The design contract // itself (vocabulary, outlines, the stylesheet) is held by the guard tests, // which this file does not repeat. func kitEngine(t *testing.T) *Engine { t.Helper() e, err := New("9.9.9-test") if err != nil { t.Fatalf("New: %v", err) } e.SetInboundEnabled(true) e.SetDMARCEnabled(true) return e } // partial executes one named partial of components.html with data. func partial(t *testing.T, name string, data any) string { t.Helper() var buf bytes.Buffer if err := kitEngine(t).Page("components").ExecuteTemplate(&buf, name, data); err != nil { t.Fatalf("partial %s: %v", name, err) } return buf.String() } // squash removes the layout whitespace between tags, so a test can look for the // markup without caring about the line breaks the template source carries. func squash(s string) string { var out []string for _, line := range strings.Split(s, "\n") { if line = strings.TrimSpace(line); line != "" { out = append(out, line) } } return strings.Join(out, "") } func mustContain(t *testing.T, out string, wants ...string) { t.Helper() for _, w := range wants { if !strings.Contains(out, w) { t.Errorf("missing %q in:\n%s", w, out) } } } func mustNotContain(t *testing.T, out string, unwanted ...string) { t.Helper() for _, u := range unwanted { if strings.Contains(out, u) { t.Errorf("unexpected %q in:\n%s", u, out) } } } // status_tag is the one mapping of a status to Bulma's classes: the table of // box 03 of the kit page. A word it does not know is neutral, never an error. func TestStatusTagMapsEveryStatusOfTheKitTable(t *testing.T) { for status, want := range map[string]string{ "ok": "tag is-success is-light", "published": "tag is-success is-light", "running": "tag is-success is-light", "sent": "tag is-success is-light", "warn": "tag is-warning is-light", "deferred": "tag is-warning is-light", "quiet": "tag is-warning is-light", "error": "tag is-danger is-light", "fail": "tag is-danger is-light", "mismatch": "tag is-danger is-light", "bounced": "tag is-danger is-light", "quarantine": "tag is-primary is-light", "global": "tag is-primary is-light", "off": "tag is-light", "unknown": "tag is-light", } { if got := StatusTag(status); got != want { t.Errorf("StatusTag(%q) = %q, want %q", status, got, want) } } } func TestStatusTagTreatsAnUnknownStatusAsNeutral(t *testing.T) { for _, status := range []string{"", "rate-limited", "3", "DKIM", "something new"} { if got := StatusTag(status); got != "tag is-light" { t.Errorf("StatusTag(%q) = %q, want the neutral tag", status, got) } } // Case and surrounding space are not part of the word. if got := StatusTag(" Deferred "); got != "tag is-warning is-light" { t.Errorf("StatusTag(\" Deferred \") = %q", got) } } // The kit page's status table is drawn from the function, so the words shown // there cannot drift from what the function does. func TestKitStatusTableAgreesWithStatusTag(t *testing.T) { for _, row := range KitPage().StatusRows { out := partial(t, "tag", row.Tag) mustContain(t, out, `class="`+StatusTag(row.Tag.Status)+`"`) } } func TestWbrAtBreaksAfterTheAtSignAndEscapesTheRest(t *testing.T) { if got, want := string(wbrAt("shop@shop.example.org")), "shop@shop.example.org"; got != want { t.Errorf("wbrAt = %q, want %q", got, want) } got := string(wbrAt(`"a"@x&y`)) if strings.Contains(got, "") || !strings.Contains(got, "@x&y") { t.Errorf("wbrAt did not escape the address: %q", got) } if got := string(wbrAt("no-address")); got != "no-address" { t.Errorf("wbrAt changed text without an @: %q", got) } } // Both functions are reachable from a page template, which is how a table cell // uses them. func TestStatusTagAndWbrAtAreTemplateFunctions(t *testing.T) { funcs := templateFuncs() for _, name := range []string{"status_tag", "wbr_at"} { if _, ok := funcs[name]; !ok { t.Errorf("template function %s is missing", name) } } } func TestRichBuildsATextFromMixedParts(t *testing.T) { got := Rich("a ", Code("b"), Text{{Text: " c"}}, 4) if len(got) != 4 || got[0].Text != "a " || !got[1].Code || got[2].Text != " c" || got[3].Text != "4" { t.Errorf("Rich = %#v", got) } } func TestInlineRendersEveryKindAndEscapesText(t *testing.T) { out := partial(t, "inline", Rich( "1<2 ", Code("c"), Mono("m"), Strong("s"), Em("e"), Br(), Link("/x?a=1&b=2", "go"), TagOf("ok", "fine"), Inline{Text: "both", Href: "/y", Strong: true}, )) mustContain(t, out, "1<2 ", "c", `m`, "s", "e", "
", `go`, `fine`, `both`) } func TestTagLabelDefaultsToTheStatus(t *testing.T) { mustContain(t, partial(t, "tag", Tag{Status: "deferred"}), `deferred`) mustContain(t, partial(t, "tag", Tag{Status: "ok", Label: "DKIM"}), `DKIM`) } func TestPostmarkLevels(t *testing.T) { for level, class := range map[string]string{ LevelOK: `class="sp-postmark"`, "": `class="sp-postmark"`, LevelWarn: `class="sp-postmark sp-warn"`, LevelFail: `class="sp-postmark sp-fail"`, } { out := partial(t, "postmark", Postmark{Top: "DNS", Word: "FAIL", Bottom: "1 OF 3", Level: level}) mustContain(t, out, class, "DNSFAIL1 OF 3") } } func TestPageHeadStates(t *testing.T) { // Everything a head can carry. out := squash(partial(t, "page_head", Head{ Postmark: &Postmark{Top: "DNS", Word: "FAIL", Bottom: "1 OF 3 RECORDS", Level: LevelFail}, Crumbs: []Crumb{{Href: "/outbound/domains", Label: "Outbound domains"}, {Label: "Applications"}}, Title: "New password for", Code: "prod-server", Lead: Rich("Lead with ", Code("code")), Note: []string{"Updated 14:02:31 UTC", "refreshes every 10 s"}, Actions: []Action{{Label: "Re-check DNS", Icon: "ti-refresh", Post: "/x/dns-recheck"}, {Label: "Add", Icon: "ti-plus", Href: "/x/new", Primary: true}, {Label: "Delete", Href: "/x/delete", Danger: true}}, })) mustContain(t, out, `
`, `

Outbound domains / Applications /

`, `

New password for prod-server

`, `

Lead with code

`, `
Updated 14:02:31 UTC
refreshes every 10 s
`, `
`, `Add`, `Delete`) // The plainest head: a kicker, a title, a lead — and nothing else. out = squash(partial(t, "page_head", Head{Kicker: "Outbound", Title: "Outbound domains", Lead: Plain("Lead.")})) mustContain(t, out, `

Outbound

`, `

Outbound domains

`) mustNotContain(t, out, "sp-postmark", `class="buttons"`, "sp-muted sp-small") // A message: a compact mono title and a route in the place of the lead. out = squash(partial(t, "page_head", Head{ Title: "[FIRING] disk", Compact: true, Mono: true, Route: Route{{Text: "a@example.org"}, {Arrow: true}, {Text: "b@example.org"}, {Tag: &Tag{Status: "deferred"}}}, Lead: Plain("never shown beside a route"), })) mustContain(t, out, `

[FIRING] disk

`, `

a@example.orgb@example.orgdeferred

`) mustNotContain(t, out, "sp-lead") } func TestFlashSuccessAndError(t *testing.T) { mustContain(t, partial(t, "flash", Flash{Text: Rich(Strong("Saved"), " — done")}), `class="notification is-success is-light"`, `Saved — done`) mustContain(t, partial(t, "flash", Flash{Error: true, Text: Plain("It failed")}), `class="notification is-danger is-light"`, `It failed`) } func TestBoxOpensWithItsHeadInEveryVariant(t *testing.T) { out := squash(partial(t, "box_open", Box{No: "02", Title: "Applications", ID: "apps", End: Plain("note"), Help: &HelpLink{Href: "/help#apps", Title: "What an application is"}})) mustContain(t, out, `
02

Applications

note
`, `
`) mustContain(t, partial(t, "box_open", Box{No: "01", Title: "x", Variant: BoxDanger}), `
`) out = partial(t, "box_open", Box{Icon: "ti-eye", Title: "Shown once", Variant: BoxCredential}) mustContain(t, out, `
`, ``) mustNotContain(t, out, "id=") if got := partial(t, "box_close", nil); got != "
" { t.Errorf("box_close = %q", got) } if got := partial(t, "body_open", nil); got != `
` { t.Errorf("body_open = %q", got) } } func TestBoxHeadFilterFormReplacesTheEndSlot(t *testing.T) { out := squash(partial(t, "box_head", Box{No: "01", Title: "Messages", End: Plain("ignored"), Filter: &Filter{ Action: "/outbound/log", Selects: []Select{{Name: "status", Label: "Status", Selected: "sent", Options: []Option{{Value: "", Label: "Any status"}, {Value: "sent", Label: "sent"}}}}, Dates: []DateInput{{Name: "from", Label: "From date", Value: "2026-09-14"}}, }})) mustContain(t, out, `
`, `
`, ``, `
`) mustNotContain(t, out, "ignored") } func TestBoxFootVariants(t *testing.T) { mustContain(t, partial(t, "box_foot", Foot{Text: Plain("Page 1 of 9"), Link: &Anchor{Href: "/p?n=2", Label: "Older →"}, End: Plain("live")}), `
Page 1 of 9Older →live
`) mustContain(t, partial(t, "box_foot", Foot{Bare: true, Text: Plain("A legend.")}), `
A legend.
`) mustContain(t, partial(t, "box_foot", Foot{Text: Plain("ignored"), Button: &Action{Label: "Done", Href: "/x", Primary: true}}), ``) } func TestHealthCards(t *testing.T) { out := squash(partial(t, "health_cards", []HealthCard{ {Name: "Machine", Value: "CPU 12 %", Sub: "RAM 41 %", Icon: "ti-cpu", Href: "/server/health"}, {Name: "TLS certificate", Value: "Expires in 12 days", Sub: "renewal has not run", Icon: "ti-certificate", Href: "/server/health", Level: LevelWarn}, {Name: "Sockets", Value: "1 of 2", Sub: "x", Icon: "ti-plug-connected-x", Href: "/server/health", Level: LevelFail}, })) mustContain(t, out, `
MachineCPU 12 %RAM 41 %`, ``, ``) } func TestDNSRecordShowsInDNSNowOnlyOnAMismatch(t *testing.T) { ok := squash(partial(t, "dns_record", Record{ Name: "DKIM", Status: Tag{Status: "published"}, Note: Plain("selector"), Host: "sp2026._domainkey.example.org", Type: "TXT", Value: "v=DKIM1; p=AAA", ValueRows: 3, })) mustContain(t, ok, `

DKIM

publishedselector
`, `
`, ``, `
`, ``) mustNotContain(t, ok, "In DNS now", "is-danger") bad := squash(partial(t, "dns_record", Record{ Name: "SPF", Status: Tag{Status: "mismatch"}, Host: "example.org", Type: "TXT", Value: "v=spf1 ip4:203.0.113.25 ~all", InDNS: "v=spf1 include:_spf.google.com ~all", Problem: Rich("Add ", Code("ip4:203.0.113.25"), " before ~all."), })) mustContain(t, bad, ``, ``, `

Add ip4:203.0.113.25 before ~all.

`, ``) } func TestCopyFieldSingleLineAndMultiLine(t *testing.T) { one := squash(partial(t, "copy_field", CopyField{Label: "Login", Value: "prod-server", Help: Plain("Keep it.")})) mustContain(t, one, ``, `
`, ``, `

Keep it.

`) many := squash(partial(t, "copy_field", CopyField{Label: "Key", Value: "line <1>\nline 2", Rows: 2})) mustContain(t, many, `