package view import ( "regexp" "strings" "testing" ) // ---- System log // The page shows the newest line first, and its one box is the element that // polls: the fragment answers with that same box. func TestSystemLogShowsNewestFirstAndPollsItsBox(t *testing.T) { out := renderSignedIn(t, "system-log", systemLogFixture()) pageHas(t, "System log", out, `

System log

`, `

Recent lines

`, `Last 200 lines · refreshes on its own`, `id="system-log" data-poll aria-live="polite" hx-get="/server/log/fragment" hx-trigger="load" hx-swap="outerHTML"`, `Sep 21 14:01:52 postfix/smtp[2131]`, `postfix/smtp[2123]`, `postfix/smtpd[2119]`, ) if strings.Index(out, "postfix/smtp[2131]") > strings.Index(out, "opendkim[39]") { t.Error("the log is not shown newest line first") } // What a line says is escaped, not interpreted. if !strings.Contains(out, "to=<m.keller@gmx.de>") || strings.Contains(out, "") { t.Error("an address in a log line is not escaped") } frag := renderFragment(t, "system_log_body", systemLogFixture()) if !strings.HasPrefix(frag, `
`, ) pageLacks(t, "Backup", out, `notification`) } // The box is ticked on arrival, as on the domain export: the secret file is // sealed unless the operator unticks it, and the page comes back the same way // after a refusal, with the password fields in view. func TestBackupEncryptionIsTickedLikeTheDomainExport(t *testing.T) { backup := renderSignedIn(t, "backup", backupFixture()) export := renderSignedIn(t, "out-domain-settings", outDomainSettingsFixture()) const box = `name="encrypt" value="1" checked data-encrypt-toggle>` pageHas(t, "Backup", backup, box) pageHas(t, "Domain settings", export, box) for _, hook := range []string{`data-encrypt-fields`, `name="password" type="password" autocomplete="new-password" minlength="12" placeholder="at least 12 characters"`, `name="password_confirm" type="password" autocomplete="new-password"`, `Keep this password: SelfPost does not store it, and the file cannot be opened without it.`} { pageHas(t, "Backup", backup, hook) pageHas(t, "Domain settings", export, hook) } } // A refusal is the flash above the boxes, with the encryption box still ticked. func TestBackupRefusalIsTheFlashAndKeepsTheTick(t *testing.T) { out := renderSignedIn(t, "backup", NewBackup(admin(), 12, "The two passwords do not match.")) pageHas(t, "Backup", out, `
`, `The two passwords do not match.`, `name="encrypt" value="1" checked`) if strings.Index(out, `class="notification`) > strings.Index(out, `class="box"`) { t.Error("the flash is not above the first box") } } // ---- Users func TestUsersListsTheReachOfEachUser(t *testing.T) { out := renderSignedIn(t, "users", usersFixture()) pageHas(t, "Users", out, `

Users

`, `01

Panel users

`, `3 users`, `href="/server/users/new"`, `Create user`, `
`, `User created.`, `admin you`, `global`, `domain`, `Outbound domainsInbound domains`, `shop.example.org—`, `Allacme.io`, `mix@example.org`, `team@shop.example.org`, `href="/server/users/3"`, ) if strings.Count(out, `>you<`) != 1 { t.Error("only the signed-in user's own row carries the mark") } // Without the inbound feature the list has no inbound column, nor a lead // that speaks of one. p := NewUsers(admin(), false, "").WithRows([]UserRow{ NewUserRow(UserRowInput{ID: 1, Username: "admin", Global: true, You: true}), }) out = renderSignedIn(t, "users", p) pageHas(t, "Users", out, `1 user<`, `Outbound domains`) pageLacks(t, "Users", out, `Inbound domains`, `outbound and inbound separately`, `notification`) // A user who has not set an address is a dash, not a gap. pageHas(t, "Users", out, `—`) } func TestReachWords(t *testing.T) { for _, tt := range []struct { all bool names []string want string }{ {true, []string{"ignored.example"}, "All"}, {false, []string{"a.example", "b.example"}, "a.example, b.example"}, {false, nil, "—"}, } { if got := reachWords(tt.all, tt.names); got != tt.want { t.Errorf("reachWords(%v, %v) = %q, want %q", tt.all, tt.names, got, tt.want) } } // A global user reaches everything whatever the lists say. row := NewUserRow(UserRowInput{ID: 1, Username: "root", Global: true}) if row.Outbound != "All" || row.Inbound != "All" || row.Role.Status != "global" { t.Errorf("a global user's row = %+v", row) } } // ---- The user form // The form for an existing user shows what is stored and sends everything the // handler reads under the names it has always read it by. Saving it untouched // sends the same values back: the role as selected, the All ticks as ticked, the // single domains as ticked, and a blank password, which the handler reads as // "keep". func TestUserFormShowsWhatIsStoredUnderTheHandlersFieldNames(t *testing.T) { out := renderSignedIn(t, "user", userFixture()) pageHas(t, "User", out, `

acme-ops

`, `href="/server/users">Users`, `href="/server/users/3/delete"`, `
`, `name="username" value="acme-ops"`, ``, ``, `name="password" type="password" autocomplete="new-password">`, `Leave empty to keep the current one.`, ``, ``) if n := strings.Count(out, `
`); n != 2 { t.Errorf("%d boxes are marked data-domain-pick, want the outbound and the inbound list", n) } out = renderSignedIn(t, "user", NewUserForm(admin(), UserFormInput{ID: 2, Name: "shop-team", Username: "shop-team", Role: "domain", PasswordMin: 12})) if n := strings.Count(out, "data-domain-pick"); n != 1 { t.Errorf("%d boxes are marked without the inbound feature, want the outbound list alone", n) } } // Inbound domains are offered only where the feature is on; the handler keeps // what the user already has there when the form has no such list. func TestUserFormHasNoInboundListWhenTheFeatureIsOff(t *testing.T) { p := NewUserForm(admin(), UserFormInput{ID: 2, Name: "shop-team", Username: "shop-team", Role: "domain", PasswordMin: 12}) out := renderSignedIn(t, "user", p) pageHas(t, "User", out, `

Outbound domains

`) pageLacks(t, "User", out, `Inbound domains`, `all_inbound_domains`, `inbound_domain_ids`) } // A form that was refused says so above the boxes, shows what was typed, and // names the user in the head as stored, not as typed. func TestUserFormRefusalKeepsWhatWasTyped(t *testing.T) { p := NewUserForm(admin(), UserFormInput{ID: 3, Name: "acme-ops", Username: "x", Email: `a"b@x`, Role: "global", PasswordMin: 12, Error: "That username is already in use."}) out := renderSignedIn(t, "user", p) pageHas(t, "User", out, `
`, `That username is already in use.`, `

acme-ops

`, `value="<b>x</b>"`, `value="a"b@x"`, `