Files
selfpost/internal/web/handlers/handlers_outbound_pages_test.go
T
mixandClaude Opus 5.5 acdfaaecd7 panel-redesign: Outbound domains, domain, settings, application on the kit
Stage 2, step 3. Written by Sonnet, reviewed by Opus.

  - Six pages named after their mockups: out-domains, out-domain,
    out-domain-settings, out-app, out-app-created, out-domain-delete,
    with typed page data (view/outbound.go, outbound_app.go), fixtures
    and tests. The engine pages dashboard, domain_detail and
    domain_delete are gone with their templates and are off
    legacy_pages.txt. GET .../settings has its own handler.
  - Handlers pass typed data; routes, POST targets, field names,
    validation, authorization (both roles, another tenant's id is a 404)
    and Cache-Control: no-store on the password page are unchanged.
  - Kit, attribute only: Action.Confirm writes data-confirm on a POST
    action's form (panel.js already reads it).
  - test/e2e/panel_client.go scrapes the new markup (copy fields, the
    application row's Edit link). Not run: no Docker here.

Fixes found on the way
  - Report address of a domain that follows another user's default: the
    select had one "inherit" option, selected, so saving the form
    untouched moved the domain to whoever saved it. It now offers that
    user's default as "keep" (selected, saves nothing) and "My default
    instead" as the change, as the mockup draws it; "keep" on a domain
    that does not follow someone else is refused.
  - panel.js: syncShowWhen stopped at the first matching rule, so the
    auto-limit fields were never toggled with the limit mode. Every
    matching rule applies now.

Differences from the mockups, for the owner:
  - application rows carry Edit only. New password and Delete are POSTs,
    and the kit has no link-styled POST control for a table row; both are
    in the application page's head with a confirmation. Drawing them in
    the row needs a design: change (as Sign out was);
  - the application login is read-only on edit (the handler refuses a
    rename); the submit says Create application on the empty form;
  - multiplier bounds are the store's 1.5-5, default 2.5;
  - "Recalculate" buttons exist where the stored limit is automatic;
  - a domain administrator has no add box and no Delete; Delete domain is
    global-only on the domain page and on settings;
  - a fourth record, Report authorization, appears when the report
    address is on another domain; record states missing / weak / not
    checked are tags with the explanation under the value;
  - refusals are the kit flash above the first box, and those of the
    settings forms now show on the settings page; successful saves still
    redirect to the domain page;
  - the export checkbox is ticked by default, as drawn.

Evidence
  - docs/assets/panel-redesign/evidence/out-domains.png, out-domain.png,
    out-domain-settings.png, out-app.png, out-app-created.png,
    out-domain-delete.png: 1440 px wide, headless Edge, real templates
    served locally with their fixtures - NOT from the container (no
    Docker on this machine). Taken before the report-address fix, which
    does not change what the fixture renders.
  - panel/audit.js on the six pages at 1440 and 1280 (text inside
    [hidden] blocks skipped): 866 texts checked, problems: [].
  - go test -count=1 ./internal/web/...
      ok  internal/web
      ok  internal/web/auth
      ok  internal/web/handlers
      ok  internal/web/validate
      ok  internal/web/view

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 00:49:20 +03:00

397 lines
20 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handlers
import (
"net/http"
"net/url"
"strings"
"testing"
"github.com/mixeme/selfpost/internal/app"
"github.com/mixeme/selfpost/internal/store"
)
// The pages of the Outbound group, rendered from the real stores and the
// handlers, with the refusals each form can come back with. Their markup is
// held to the mockups by the guards in internal/web/view; what is checked here
// is that the handlers fill the typed pages from what is stored and that every
// state a form can end in lands on the page that owns it.
func has(t *testing.T, where, body string, wants ...string) {
t.Helper()
for _, w := range wants {
if !strings.Contains(body, w) {
t.Errorf("%s is missing %q:\n%s", where, w, body)
}
}
}
func lacks(t *testing.T, where, body string, gone ...string) {
t.Helper()
for _, g := range gone {
if strings.Contains(body, g) {
t.Errorf("%s carries %q", where, g)
}
}
}
// pageOf reads a page of domain 1 as the global administrator, binding the
// path's {id} the way the router would.
func pageOf(t *testing.T, h http.HandlerFunc, target string) string {
t.Helper()
rec := send(h, &globalPrincipal, "GET", target, map[string]string{"id": "1"}, nil)
if rec.Code != http.StatusOK {
t.Fatalf("GET %s = %d, want 200:\n%s", target, rec.Code, rec.Body.String())
}
return rec.Body.String()
}
func TestDomainsListShowsTheAddFormToTheGlobalRoleOnly(t *testing.T) {
s := newAppStand(t)
body := getBody(t, s.h.HandleDashboard, "/outbound/domains")
has(t, "list", body, `<h1 class="title is-3">Outbound domains</h1>`, `<h2>Add a domain</h2>`, `action="/outbound/domains"`,
`<strong>example.org</strong>`, `href="/outbound/domains/1/delete"`, `1 domain · DNS cached`, `>DKIM</span>`, `>SPF</span>`, `>DMARC</span>`, `mail</td>`)
ops := domainAdmin(t, s.h.store, "ops", s.d.ID)
body = getBodyAs(t, s.h.HandleDashboard, "/outbound/domains", ops)
has(t, "list for a domain administrator", body, `<strong>example.org</strong>`, `href="/outbound/domains/1"`)
lacks(t, "list for a domain administrator", body, `Add a domain`, `/delete`)
// What the viewer does not reach is not listed.
other, err := s.h.store.AddDomain("other.example.net", "mail")
if err != nil {
t.Fatal(err)
}
lacks(t, "list for a domain administrator", getBodyAs(t, s.h.HandleDashboard, "/outbound/domains", ops), other.Name)
has(t, "list", getBody(t, s.h.HandleDashboard, "/outbound/domains"), other.Name)
}
// A refused name comes back on the list, with what was typed in the field.
func TestAddDomainRefusalKeepsWhatWasTyped(t *testing.T) {
s := newAppStand(t)
rec := postFormAs(s.h.HandleAddDomain, globalPrincipal, "/outbound/domains", nil, url.Values{"name": {`Not A Domain "<b>`}})
if rec.Code != http.StatusBadRequest {
t.Fatalf("a bad name = %d, want 400:\n%s", rec.Code, rec.Body.String())
}
body := rec.Body.String()
has(t, "refused add", body, `<div class="notification is-danger is-light">`, `<h2>Add a domain</h2>`, `<strong>example.org</strong>`)
lacks(t, "refused add", body, `<b>`)
if domains, _ := s.h.store.ListDomains(); len(domains) != 1 {
t.Errorf("a refused name left %d domains", len(domains))
}
}
func TestDomainPageShowsRecordsApplicationsAndConnection(t *testing.T) {
s := newAppStand(t)
s.h.cfg.Hostname, s.h.cfg.SubmissionEnabled = "mail.example.org", true
a, _, err := s.h.apps.CreateWithSettings(s.d.ID, "prod-server", app.Settings{
Mode: store.AddressModeList, Addresses: []string{"alerts@example.org"},
AuthIPRestrict: true, AuthAllowedIPs: []string{"203.0.113.10", "203.0.113.11"},
Limit: app.Limit{Mode: store.RateLimitModeManual, MaxMessages: 200, WindowSeconds: 3600},
}, nil)
if err != nil {
t.Fatal(err)
}
body := pageOf(t, s.h.HandleDomainDetail, "/outbound/domains/1")
has(t, "domain", body,
`<h1 class="title is-3">example.org</h1>`, `class="sp-postmark`, `<h3>DKIM</h3>`, `<h3>SPF</h3>`, `<h3>DMARC</h3>`,
`mail._domainkey.example.org`, `v=DKIM1`, `_dmarc.example.org`, `v=DMARC1; p=none`, `v=spf1 `,
`<td class="sp-mono"><strong>prod-server</strong></td>`, `alerts@<wbr>example.org`, `200 / h`, `2 IPs`,
`href="/outbound/domains/1/applications/`+idStr(a.ID)+`">Edit</a>`,
`mail.example.org`, `STARTTLS (submission)`, `action="/outbound/domains/1/dns-recheck"`,
`href="/outbound/log?domain=example.org"`, `href="/outbound/domains/1/settings#reports"`, `href="/outbound/domains/1/delete"`)
// The flash the redirects carry.
rec := send(s.h.HandleDomainDetail, &globalPrincipal, "GET", "/outbound/domains/1?appsaved=1", s.paths, nil)
has(t, "domain after a save", rec.Body.String(), `<div class="notification is-success is-light">`, `Application saved.`)
// A domain administrator does not get the global role's delete entry.
ops := domainAdmin(t, s.h.store, "ops", s.d.ID)
rec = send(s.h.HandleDomainDetail, &ops, "GET", "/outbound/domains/1", s.paths, nil)
if rec.Code != http.StatusOK {
t.Fatalf("domain page for its administrator = %d", rec.Code)
}
lacks(t, "domain for a domain administrator", rec.Body.String(), `href="/outbound/domains/1/delete"`)
}
func TestDomainPageWithoutApplicationsSaysSo(t *testing.T) {
s := newAppStand(t)
body := pageOf(t, s.h.HandleDomainDetail, "/outbound/domains/1")
has(t, "domain", body, `No applications yet.`)
lacks(t, "domain", body, `<th>Login</th>`)
}
// The report address form offers what the viewer can choose, and says whose
// default the domain follows.
func TestDomainSettingsReportAddressChoices(t *testing.T) {
s := newAppStand(t)
body := pageOf(t, s.h.HandleDomainSettings, "/outbound/domains/1/settings")
has(t, "settings", body, `<h1 class="title is-3">Domain settings</h1>`,
`action="/outbound/domains/1/settings/reports"`, `<option value="inherit">My default — no report address yet</option>`,
`<option value="none" selected>`, `<option value="custom">`,
`action="/outbound/domains/1/settings/ratelimit"`, `action="/outbound/domains/1/settings/export"`,
`name="encrypt" value="1" checked`, `minlength="12"`, `<h2>Delete domain</h2>`, `Removes the DKIM key.`)
lacks(t, "settings", body, `value="hosted"`, `name="clear"`, `ratelimit/recalc`)
// Following the signed-in user's default.
if err := s.h.store.SetDomainDMARCUser(s.d.ID, globalPrincipal.ID); err != nil {
t.Fatal(err)
}
if err := s.h.store.SetDMARCDefault(globalPrincipal.ID, store.DMARCDefaultCustom, "dmarc@hub.example"); err != nil {
t.Fatal(err)
}
body = pageOf(t, s.h.HandleDomainSettings, "/outbound/domains/1/settings")
has(t, "settings", body, `<option value="inherit" selected>admin&#39;s default — dmarc@hub.example</option>`)
// "keep" names nobody when the domain follows the person saving: refused.
if rec := postFormAs(s.h.HandleDomainDMARC, globalPrincipal, "/outbound/domains/1/settings/reports", map[string]string{"id": "1"},
url.Values{"dmarc_rua_mode": {"keep"}}); rec.Code != http.StatusBadRequest {
t.Errorf("keep on a domain that follows the saver = %d, want 400", rec.Code)
}
// Following someone else's: theirs is selected and is what an untouched form
// saves; one's own is offered as the change.
other := domainAdmin(t, s.h.store, "ops", s.d.ID)
if err := s.h.store.SetDMARCDefault(other.ID, store.DMARCDefaultCustom, "ops@hub.example"); err != nil {
t.Fatal(err)
}
if err := s.h.store.SetDomainDMARCUser(s.d.ID, other.ID); err != nil {
t.Fatal(err)
}
body = pageOf(t, s.h.HandleDomainSettings, "/outbound/domains/1/settings")
has(t, "settings", body, `<option value="keep" selected>ops&#39;s default — ops@hub.example</option>`,
`<option value="inherit">My default instead — dmarc@hub.example (you are admin)</option>`)
if rec := postFormAs(s.h.HandleDomainDMARC, globalPrincipal, "/outbound/domains/1/settings/reports", map[string]string{"id": "1"},
url.Values{"dmarc_rua_mode": {"keep"}}); rec.Code != http.StatusSeeOther {
t.Fatalf("saving the form untouched = %d, want 303:\n%s", rec.Code, rec.Body.String())
}
if got, err := s.h.store.GetDomain(s.d.ID); err != nil || !got.DMARCRuaUserID.Valid || got.DMARCRuaUserID.Int64 != other.ID {
t.Fatalf("an untouched form took the domain over: follows %v, %v; want user %d", got.DMARCRuaUserID, err, other.ID)
}
// An address of its own shows in the field.
if err := s.h.store.SetDomainDMARCAddress(s.d.ID, "reports@hub.example"); err != nil {
t.Fatal(err)
}
body = pageOf(t, s.h.HandleDomainSettings, "/outbound/domains/1/settings")
has(t, "settings", body, `<option value="custom" selected>`, `value="reports@hub.example"`)
}
func TestDomainSettingsShowsTheRateLimit(t *testing.T) {
s := newAppStand(t)
if err := s.h.domains.SaveRateLimit(s.d.ID, store.RateLimit{Mode: store.RateLimitModeManual, MaxMessages: 250, WindowSeconds: 3600}); err != nil {
t.Fatal(err)
}
body := pageOf(t, s.h.HandleDomainSettings, "/outbound/domains/1/settings")
has(t, "settings", body, `<option value="manual" selected>`, `value="250"`, `<span class="tag is-success is-light">active</span>`,
`name="clear" value="1"`, `max="600"`, `600 messages / 3600 s per client IP`, `Multiplier (1.5–5, default 2.5)`)
lacks(t, "settings", body, `ratelimit/recalc`)
}
// Each form of the settings page answers its refusal on the settings page, with
// the status it always had and the message in the flash.
func TestDomainSettingsRefusalsStayOnTheSettingsPage(t *testing.T) {
s := newAppStand(t)
post := func(h http.HandlerFunc, path string, form url.Values) (int, string) {
rec := postFormAs(h, globalPrincipal, "/outbound/domains/1/settings/"+path, s.paths, form)
return rec.Code, rec.Body.String()
}
code, body := post(s.h.HandleDomainRateLimit, "ratelimit", url.Values{"mode": {"manual"}, "max_messages": {"601"}})
if code != http.StatusBadRequest {
t.Fatalf("a limit above level 1 = %d", code)
}
has(t, "rate limit refusal", body, `<div class="notification is-danger is-light">`, `level-1 backstop (600)`, `<h1 class="title is-3">Domain settings</h1>`)
code, body = post(s.h.HandleDomainRateLimitRecalc, "ratelimit/recalc", url.Values{})
if code != http.StatusBadRequest {
t.Fatalf("recalculating a limit that is not automatic = %d", code)
}
has(t, "recalculation refusal", body, `rate limit not in auto mode`, `<h1 class="title is-3">Domain settings</h1>`)
code, body = post(s.h.HandleDomainDMARC, "reports", url.Values{"dmarc_rua_mode": {"custom"}, "dmarc_rua_email": {"not an address<b>"}})
if code != http.StatusBadRequest {
t.Fatalf("a bad report address = %d", code)
}
has(t, "report address refusal", body, `<div class="notification is-danger is-light">`, `<option value="custom" selected>`, `not an address&lt;b&gt;`)
lacks(t, "report address refusal", body, `<b>`)
code, body = post(s.h.HandleDomainDMARC, "reports", url.Values{"dmarc_rua_mode": {"custom"}})
has(t, "report address refusal", body, `Enter a custom report address or choose another mode.`)
code, body = post(s.h.HandleDomainDMARC, "reports", url.Values{"dmarc_rua_mode": {"hosted"}})
if code != http.StatusBadRequest {
t.Fatalf("hosted reports with ingest off = %d", code)
}
has(t, "report address refusal", body, `SelfPost-hosted reports are not enabled on this server.`)
code, body = post(s.h.HandleDomainDMARC, "reports", url.Values{"dmarc_rua_mode": {"bogus"}})
if code != http.StatusBadRequest {
t.Fatalf("an unknown mode = %d", code)
}
has(t, "report address refusal", body, `Choose how aggregate reports are addressed for this domain.`)
code, body = post(s.h.HandleExportDomain, "export", url.Values{"encrypt": {"1"}, "password": {"short"}, "password_confirm": {"short"}})
if code != http.StatusBadRequest {
t.Fatalf("a short export password = %d", code)
}
has(t, "export refusal", body, `The encryption password must be at least 12 characters.`, `<h1 class="title is-3">Domain settings</h1>`)
code, body = post(s.h.HandleExportDomain, "export", url.Values{"encrypt": {"1"}, "password": {"long-enough-password"}, "password_confirm": {"another-one-entirely"}})
has(t, "export refusal", body, `The two passwords do not match.`)
}
func TestApplicationNewFormIsBlank(t *testing.T) {
s := newAppStand(t)
body := pageOf(t, s.h.HandleApplicationNew, "/outbound/domains/1/applications/new")
has(t, "new application", body, `<h1 class="title is-3">New application</h1>`,
`action="/outbound/domains/1/applications/new"`, `Create application`, `name="login" value=""`,
`<option value="wildcard" selected>`, `<option value="domain" selected>`, `value="3600"`, `placeholder="alerts@example.org"`)
lacks(t, "new application", body, ` readonly`, `/password`, `/delete`, `Recalculate`)
}
func TestApplicationEditFormShowsWhatIsStored(t *testing.T) {
s := newAppStand(t)
a, _, err := s.h.apps.CreateWithSettings(s.d.ID, "prod-server", app.Settings{
Mode: store.AddressModeList, Addresses: []string{"alerts@example.org", "noc@example.org"},
AuthIPRestrict: true, AuthAllowedIPs: []string{"203.0.113.10"},
Limit: app.Limit{Mode: store.RateLimitModeManual, MaxMessages: 200, WindowSeconds: 1800},
}, nil)
if err != nil {
t.Fatal(err)
}
if err := s.h.domains.SaveRateLimit(s.d.ID, store.RateLimit{Mode: store.RateLimitModeManual, MaxMessages: 288, WindowSeconds: 3600}); err != nil {
t.Fatal(err)
}
own := "/outbound/domains/1/applications/" + idStr(a.ID)
rec := send(s.h.HandleApplicationEdit, &globalPrincipal, "GET", own, map[string]string{"id": "1", "aid": idStr(a.ID)}, nil)
body := rec.Body.String()
if rec.Code != http.StatusOK {
t.Fatalf("edit form = %d:\n%s", rec.Code, body)
}
has(t, "edit form", body, `<h1 class="title is-3 sp-mono">prod-server</h1>`, `action="`+own+`"`, `name="login" value="prod-server" readonly`,
`<option value="list" selected>`, "alerts@example.org\nnoc@example.org</textarea>", "203.0.113.10</textarea>", ` checked> Restrict to listed IPs`,
`<option value="manual" selected>`, `value="200"`, `value="1800"`, `Overrides the domain limit (288 / h)`,
`action="`+own+`/password" data-confirm=`, `action="`+own+`/delete" data-confirm=`, `Save application`)
lacks(t, "edit form", body, `Recalculate`)
// An automatic limit offers its recalculation.
if err := s.h.apps.SaveRateLimit(a.ID, store.RateLimit{Mode: store.RateLimitModeAuto, AutoMultiplier: 2, WindowSeconds: 3600, MaxMessages: 40}); err != nil {
t.Fatal(err)
}
rec = send(s.h.HandleApplicationEdit, &globalPrincipal, "GET", own, map[string]string{"id": "1", "aid": idStr(a.ID)}, nil)
has(t, "edit form", rec.Body.String(), `formaction="`+own+`/ratelimit/recalc"`, `<option value="auto" selected>`, `Computed limit: <strong>40</strong>`)
}
// A refusal of the application form comes back on the form with what was typed,
// the status it always had, and creates or changes nothing.
func TestApplicationFormRefusalsComeBackOnTheForm(t *testing.T) {
s := newAppStand(t)
typed := url.Values{
"login": {"has space"}, "mode": {"list"}, "addresses": {"a@example.org"}, "auth_ip_restrict": {"1"},
"auth_allowed_ips": {"203.0.113.10"}, "rl_mode": {"manual"}, "max_messages": {"601"}, "window_seconds": {"900"},
}
rec := postFormAs(s.h.HandleApplicationCreate, globalPrincipal, "/outbound/domains/1/applications/new", s.paths, typed)
if rec.Code != http.StatusBadRequest {
t.Fatalf("a limit above level 1 = %d:\n%s", rec.Code, rec.Body.String())
}
body := rec.Body.String()
has(t, "refused create", body, `<div class="notification is-danger is-light">`, `level-1 backstop (600)`,
`name="login" value="has space"`, `<option value="list" selected>`, "a@example.org</textarea>", "203.0.113.10</textarea>",
` checked> Restrict to listed IPs`, `<option value="manual" selected>`, `value="601"`, `value="900"`, `Create application`)
lacks(t, "refused create", body, `Shown once`)
if rec.Header().Get("Cache-Control") == "no-store" {
t.Error("a refusal is marked as a page that shows a secret")
}
typed.Set("max_messages", "100")
typed.Set("login", "taken")
if _, _, err := s.h.apps.CreateWithSettings(s.d.ID, "taken", app.Settings{Mode: store.AddressModeWildcard}, nil); err != nil {
t.Fatal(err)
}
rec = postFormAs(s.h.HandleApplicationCreate, globalPrincipal, "/outbound/domains/1/applications/new", s.paths, typed)
if rec.Code != http.StatusConflict {
t.Fatalf("a login that exists = %d, want 409:\n%s", rec.Code, rec.Body.String())
}
has(t, "refused create", rec.Body.String(), `That login is already in use. Choose another.`, `name="login" value="taken"`)
}
func TestApplicationSaveRefusalKeepsTheLoginFixed(t *testing.T) {
s := newAppStand(t)
a, _, err := s.h.apps.CreateWithSettings(s.d.ID, "prod-server", app.Settings{Mode: store.AddressModeWildcard}, nil)
if err != nil {
t.Fatal(err)
}
paths := map[string]string{"id": "1", "aid": idStr(a.ID)}
own := "/outbound/domains/1/applications/" + idStr(a.ID)
rec := postFormAs(s.h.HandleApplicationSave, globalPrincipal, own, paths, url.Values{
"login": {"renamed"}, "mode": {"wildcard"}, "rl_mode": {"manual"}, "max_messages": {"500"},
})
if rec.Code != http.StatusBadRequest {
t.Fatalf("a changed login = %d", rec.Code)
}
body := rec.Body.String()
has(t, "refused save", body, `prod-server: the login of an application cannot be changed`, `name="login" value="prod-server" readonly`,
`<option value="manual" selected>`, `value="500"`)
lacks(t, "refused save", body, `value="renamed"`)
rec = postFormAs(s.h.HandleApplicationSave, globalPrincipal, own, paths, url.Values{"mode": {"wildcard"}, "rl_mode": {"manual"}, "max_messages": {"601"}})
has(t, "refused save", rec.Body.String(), `prod-server: message limit cannot exceed the level-1 backstop (600)`)
// The recalculation of a limit that is not automatic is refused on the form too.
rec = postFormAs(s.h.HandleAppRateLimitRecalc, globalPrincipal, own+"/ratelimit/recalc", paths, url.Values{})
if rec.Code != http.StatusBadRequest {
t.Fatalf("recalculating a limit that is not automatic = %d", rec.Code)
}
has(t, "refused recalculation", rec.Body.String(), `prod-server: rate limit not in auto mode`, `Save application`)
}
// The password page is the response of its POST: shown once, in no cache, and
// the same for a new application and for a new password.
func TestPasswordIsShownOnceAndNeverCached(t *testing.T) {
s := newAppStand(t)
s.h.cfg.Hostname, s.h.cfg.SubmissionEnabled = "mail.example.org", true
rec := postFormAs(s.h.HandleApplicationCreate, globalPrincipal, "/outbound/domains/1/applications/new", s.paths, url.Values{
"login": {"prod-server"}, "mode": {"list"}, "addresses": {"alerts@example.org"},
})
if rec.Code != http.StatusCreated || rec.Header().Get("Cache-Control") != "no-store" || rec.Header().Get("Pragma") != "no-cache" {
t.Fatalf("create = %d, Cache-Control %q", rec.Code, rec.Header().Get("Cache-Control"))
}
password := s.sasl["prod-server"]
has(t, "created", rec.Body.String(), `<h2>Shown once</h2>`, `value="`+password+`"`, `value="prod-server"`,
`If this one is lost, generate another from the application&#39;s page.`, `alerts@example.org`, `465 SSL/TLS · 587 STARTTLS`,
`href="/outbound/domains/1">Done — back to example.org</a>`)
lacks(t, "created", rec.Body.String(), `stopped working`)
a, _, _ := s.app(t, "prod-server")
paths := map[string]string{"id": "1", "aid": idStr(a.ID)}
rec = postFormAs(s.h.HandleRegenPassword, globalPrincipal, "/outbound/domains/1/applications/1/password", paths, url.Values{})
if rec.Code != http.StatusOK || rec.Header().Get("Cache-Control") != "no-store" {
t.Fatalf("new password = %d, Cache-Control %q", rec.Code, rec.Header().Get("Cache-Control"))
}
renewed := s.sasl["prod-server"]
if renewed == password {
t.Fatal("the password did not change")
}
has(t, "new password", rec.Body.String(), `value="`+renewed+`"`, `The previous password stopped working when this one was generated.`)
lacks(t, "new password", rec.Body.String(), `value="`+password+`"`)
}
func TestDeleteConfirmNamesTheApplicationsThatGoWithTheDomain(t *testing.T) {
s := newAppStand(t)
rec := send(s.h.HandleDeleteConfirm, &globalPrincipal, "GET", "/outbound/domains/1/delete", s.paths, nil)
body := rec.Body.String()
if rec.Code != http.StatusOK {
t.Fatalf("delete confirmation = %d:\n%s", rec.Code, body)
}
has(t, "delete", body, `<h1 class="title is-3">Delete example.org</h1>`, `permanently delete its DKIM signing key;`,
`<form method="post" action="/outbound/domains/1/delete">`, `Delete example.org</button>`, `href="/outbound/domains/1/settings#export"`)
lacks(t, "delete", body, `credentials`)
for _, login := range []string{"prod-server", "alerts"} {
if _, _, err := s.h.apps.CreateWithSettings(s.d.ID, login, app.Settings{Mode: store.AddressModeWildcard}, nil); err != nil {
t.Fatal(err)
}
}
rec = send(s.h.HandleDeleteConfirm, &globalPrincipal, "GET", "/outbound/domains/1/delete", s.paths, nil)
has(t, "delete", rec.Body.String(), `<strong>both applications</strong>`, `<span class="sp-mono">alerts</span>, <span class="sp-mono">prod-server</span>`)
}