package view import ( "fmt" "regexp" "strings" "testing" ) // The tests of the Help page that depend on who is reading it. The one that // every link of every page lands on a topic is TestEveryHelpLinkLandsOnATopic // (server_test.go). // helpViewers are the readers of the Help page and the sections each is shown. var helpViewers = []struct { name string meta Meta inbound bool groups []string }{ {"global", Meta{User: "admin", IsGlobal: true}, true, []string{"Overview", "Outbound", "Inbound", "Server", "Account"}}, {"global, inbound feature off", Meta{User: "admin", IsGlobal: true}, false, []string{"Overview", "Outbound", "Server", "Account"}}, {"domain, outbound only", Meta{User: "ops", HasOutbound: true}, true, []string{"Outbound", "Account"}}, {"domain, inbound only", Meta{User: "ops", HasInbound: true}, true, []string{"Inbound", "Account"}}, {"domain, inbound only, feature off", Meta{User: "ops", HasInbound: true}, false, []string{"Account"}}, {"domain, both", Meta{User: "ops", HasOutbound: true, HasInbound: true}, true, []string{"Outbound", "Inbound", "Account"}}, {"domain, both, feature off", Meta{User: "ops", HasOutbound: true, HasInbound: true}, false, []string{"Outbound", "Account"}}, } // Each reader is shown the sections of the pages they can open and no other, // numbered one after the other, and the menu lists the same sections. func TestHelpShowsEachRoleOnlyItsSections(t *testing.T) { groupTitle := regexp.MustCompile(`(\d+)

([^<]+)

`) menuLabel := regexp.MustCompile(``) for _, v := range helpViewers { out := renderSignedIn(t, "help", NewHelp(v.meta, v.inbound)) var got, numbers, menu []string for _, m := range groupTitle.FindAllStringSubmatch(out, -1) { numbers, got = append(numbers, m[1]), append(got, m[2]) } for _, m := range menuLabel.FindAllStringSubmatch(out, -1) { menu = append(menu, m[1]) } if strings.Join(got, ",") != strings.Join(v.groups, ",") { t.Errorf("%s: sections %v, want %v", v.name, got, v.groups) } if strings.Join(menu, ",") != strings.Join(v.groups, ",") { t.Errorf("%s: the menu lists %v, the page shows %v", v.name, menu, v.groups) } for i, n := range numbers { if want := fmt.Sprintf("%02d", i+1); n != want { t.Errorf("%s: section %d is numbered %s, want %s", v.name, i+1, n, want) } } } } // The menu of the Help page jumps to topics that exist, in the order they are // shown, and every topic shown is in it; no id is used twice on the page. func TestHelpMenuAndTopicsAgree(t *testing.T) { menuLink := regexp.MustCompile(``) for _, v := range helpViewers { out := renderSignedIn(t, "help", NewHelp(v.meta, v.inbound)) topics := helpTopic.FindAllStringSubmatch(out, -1) menu := menuLink.FindAllStringSubmatch(out, -1) if len(topics) != len(menu) || len(topics) == 0 { t.Fatalf("%s: %d topics, %d menu entries", v.name, len(topics), len(menu)) } for i := range topics { if topics[i][1] != menu[i][1] { t.Errorf("%s: topic %d is %q, its menu entry points at %q", v.name, i, topics[i][1], menu[i][1]) } } unique(t, "Help ("+v.name+")", ids(out)) } } // The topics about Overview and Health, and about the Server pages, are for the // role that sees those pages; a domain administrator is not shown them, nor a // link to them. func TestHelpLeavesOutWhatADomainAdministratorCannotOpen(t *testing.T) { out := renderSignedIn(t, "help", NewHelp(Meta{User: "ops", HasOutbound: true, HasInbound: true}, true)) pageHas(t, "Help", out, `id="dns"`, `id="apps"`, `id="limits"`, `id="log"`, `id="queue"`, `id="dmarc"`, `id="inbound"`, `id="upstream"`, `id="recipients"`, `id="filter"`, `id="account"`, `

Outbound

`, `

Inbound

`, `

Account

`) pageLacks(t, "Help", out, `id="checks"`, `id="certificate"`, `id="rdns"`, `id="backup"`, `id="users"`, `id="settings"`, `

Overview

`, `

Server

`, `href="/server/health"`) } // Help text is data, not markup: a topic is one paragraph of Plain, Rich and // Code runs with no raw HTML in it, and the page renders each topic as a // heading and exactly one paragraph. func TestHelpTopicsAreOneParagraphOfPlainText(t *testing.T) { p := NewHelp(admin(), true) seen := map[string]bool{} for _, g := range p.Groups { for _, topic := range g.Topics { if seen[topic.ID] { t.Errorf("topic id %q is used twice", topic.ID) } seen[topic.ID] = true if len(topic.Body) != 1 { t.Errorf("topic %q has %d paragraphs, want one: paragraph margins are reset, so several would run together", topic.ID, len(topic.Body)) } texts := []string{topic.Title} for _, text := range topic.Body { for _, in := range text { texts = append(texts, in.Text) } } for _, s := range texts { if strings.ContainsAny(s, "<>") || strings.Contains(s, "&") || strings.Contains(s, "<") { t.Errorf("topic %q holds markup: %q", topic.ID, s) } } } } out := renderSignedIn(t, "help", p) blocks := regexp.MustCompile(`(?s)
(.*?)
`).FindAllStringSubmatch(out, -1) if len(blocks) != len(seen) { t.Errorf("%d topics rendered, %d in the data", len(blocks), len(seen)) } for _, m := range blocks { if n, h := strings.Count(m[2], "

"), strings.Count(m[2], "

"); n != 1 || h != 1 { t.Errorf("topic %q renders %d paragraphs and %d headings, want one of each", m[1], n, h) } } pageLacks(t, "Help", out, `&lt;`, `<a `) } // A link leads only to a topic its reader is shown. Boxes on pages that more // than one role opens have their topic in a section only some are shown, so what // they link to depends on the viewer. func TestHelpLinksStayInsideWhatTheirViewerIsShown(t *testing.T) { for _, v := range helpViewers { shown := map[string]bool{} for _, m := range helpTopic.FindAllStringSubmatch(renderSignedIn(t, "help", NewHelp(v.meta, v.inbound)), -1) { shown[m[1]] = true } landsOn := func(where string, l *HelpLink) { t.Helper() if l == nil { return } if anchor := strings.TrimPrefix(l.Href, "/help#"); !shown[anchor] { t.Errorf("%s, %s: a box links to /help#%s, a topic that reader is not shown", v.name, where, anchor) } } if !v.meta.reachesOutbound() { continue } settings := NewOutDomainSettings(v.meta, 1, "example.org", 2, false) landsOn("Domain settings", settings.Reports.Help) landsOn("Domain settings", settings.Limit.Help) landsOn("Domain settings", settings.Export.Help) if has := settings.Export.Help != nil; has != v.meta.IsGlobal { t.Errorf("%s: the Export box has a Help link = %v, want %v (its topic is in Server)", v.name, has, v.meta.IsGlobal) } hub := NewDMARCHub(v.meta, IngestInput{}).WithoutIngest() if hub.Recent.Help == nil || hub.Recent.No != "01" { t.Errorf("%s: without the Ingest box the first box of the DMARC hub is %+v, want it numbered 01 with the Help link", v.name, hub.Recent) } landsOn("DMARC hub", hub.Recent.Help) } } // The Inbound log and Filter lists have their topics, after the spam filter's, in // the menu and on the page, for everyone who is shown Inbound; the Settings topic // mentions the second retention. func TestHelpHasTheInboundLogAndFilterListsTopics(t *testing.T) { out := renderSignedIn(t, "help", NewHelp(admin(), true)) pageHas(t, "Help", out, `
  • Inbound log
  • `, `
  • Filter lists
  • `, `

    Inbound log

    `, `quarantine — held by the filter. Why names who decided.`, `a message refused after its text was sent is one row however many recipients it had.`, `and never more than 10 000.`, `

    Filter lists

    `, `Deny wins.`, `files under /data/antispam/`, `With the inbound relay on there is a second number, for the Inbound log, with the same range.`) for _, v := range helpViewers { out := renderSignedIn(t, "help", NewHelp(v.meta, v.inbound)) shown := strings.Contains(out, `id="inbound-log"`) if want := v.inbound && (v.meta.IsGlobal || v.meta.HasInbound); shown != want { t.Errorf("%s: inbound log topic shown = %v, want %v", v.name, shown, want) } // The filter lists' page is the global role's alone (404 to anyone // else), and so is their topic and its menu entry. lists := strings.Contains(out, `id="filter-lists"`) || strings.Contains(out, `href="#filter-lists"`) if want := v.inbound && v.meta.IsGlobal; lists != want { t.Errorf("%s: filter lists topic shown = %v, want %v", v.name, lists, want) } } if HelpInboundLog != "inbound-log" || HelpFilterLists != "filter-lists" { t.Errorf("anchors %q and %q", HelpInboundLog, HelpFilterLists) } }