package handlers
import (
"net/http"
"net/http/httptest"
"net/url"
"regexp"
"strings"
"testing"
"github.com/mixeme/selfpost/internal/dmarc"
"github.com/mixeme/selfpost/internal/domain"
"github.com/mixeme/selfpost/internal/postfix"
"github.com/mixeme/selfpost/internal/store"
"github.com/mixeme/selfpost/internal/web/auth"
)
// postFormAs sends a form to a handler as the given principal.
func postFormAs(h http.HandlerFunc, p auth.Principal, target string, pathValues map[string]string, values url.Values) *httptest.ResponseRecorder {
req := httptest.NewRequest(http.MethodPost, target, strings.NewReader(values.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
for k, v := range pathValues {
req.SetPathValue(k, v)
}
rec := httptest.NewRecorder()
h(rec, auth.RequestWithPrincipal(req, p))
return rec
}
// dmarcRecordingMaps stands in for Postfix: the allow-lists a resync would write.
type dmarcRecordingMaps struct{ got []postfix.DMARCMapsConfig }
func (m *dmarcRecordingMaps) RebuildDMARCMaps(cfg postfix.DMARCMapsConfig) error {
m.got = append(m.got, cfg)
return nil
}
// hostedStand is an app stand whose server receives hosted DMARC reports.
func hostedStand(t *testing.T) (appStand, *dmarcRecordingMaps) {
t.Helper()
s := newAppStand(t)
maps := &dmarcRecordingMaps{}
s.h.cfg.DMARCEnabled, s.h.cfg.Hostname = true, "mail.example.org"
s.h.dmarc = dmarc.NewService(s.h.store, maps, "mail.example.org", true)
return s, maps
}
const hostedForExample = "dmarc-reports+example.org@mail.example.org"
// hostedEscaped is that address as html/template writes it into a page.
const hostedEscaped = "dmarc-reports+example.org@mail.example.org"
// saveReports posts the report-address form of domain 1 as the global
// administrator.
func (s appStand) saveReports(form url.Values) *httptest.ResponseRecorder {
return postFormAs(s.h.HandleDomainDMARC, globalPrincipal, "/outbound/domains/1/settings/reports", s.paths, form)
}
func (s appStand) reportAddress(t *testing.T) string {
t.Helper()
d, err := s.h.store.GetDomain(s.d.ID)
if err != nil {
t.Fatal(err)
}
return d.DMARCRua
}
func contains(list []string, want string) bool {
for _, s := range list {
if s == want {
return true
}
}
return false
}
// A new domain has no report address: its settings show "No reports" selected,
// and its DMARC record says so.
func TestNewDomainHasNoReports(t *testing.T) {
s, _ := hostedStand(t)
// A second OpenDKIM that reloads nothing: the stand's has no supervisor.
odk := domain.NewOpenDKIM(t.TempDir())
odk.SetReloadHook(func() error { return nil })
if _, err := odk.EnsureKey("example.org", "mail"); err != nil {
t.Fatal(err)
}
s.h.domains = domain.NewService(s.h.store, odk, s.h.apps, "mail")
rec := postFormAs(s.h.HandleAddDomain, globalPrincipal, "/outbound/domains", nil, url.Values{"name": {"new.example"}})
if rec.Code != http.StatusSeeOther {
t.Fatalf("adding a domain = %d:\n%s", rec.Code, rec.Body.String())
}
domains, _ := s.h.store.ListDomains()
var added store.Domain
for _, d := range domains {
if d.Name == "new.example" {
added = d
}
}
if added.ID == 0 || added.DMARCRua != "" {
t.Fatalf("a new domain reports to %q (%+v), want no reports", added.DMARCRua, added)
}
body := send(s.h.HandleDomainSettings, &globalPrincipal, "GET", "/outbound/domains/2/settings", map[string]string{"id": idStr(added.ID)}, nil).Body.String()
has(t, "settings of a new domain", body, ``)
lacks(t, "settings of a new domain", body, ``,
``,
``)
if rec := s.saveReports(url.Values{"dmarc_rua_mode": {"hosted"}}); rec.Code != http.StatusSeeOther {
t.Fatalf("hosted = %d:\n%s", rec.Code, rec.Body.String())
}
if got := s.reportAddress(t); got != hostedForExample {
t.Fatalf("hosted stored %q, want %q", got, hostedForExample)
}
has(t, "settings after hosted", settings(), ``, ``)
if err := s.h.store.SetDomainDMARCAddress(s.d.ID, "reports@hub.example"); err != nil {
t.Fatal(err)
}
for _, form := range []url.Values{
{"dmarc_rua_mode": {"hosted"}},
{"dmarc_rua_mode": {"inherit"}},
{"dmarc_rua_mode": {"keep"}},
{"dmarc_rua_mode": {"custom"}},
{"dmarc_rua_mode": {"custom"}, "dmarc_rua_email": {"not an address"}},
{},
} {
if rec := s.saveReports(form); rec.Code != http.StatusBadRequest {
t.Errorf("%v = %d, want 400", form, rec.Code)
}
if got := s.reportAddress(t); got != "reports@hub.example" {
t.Fatalf("%v changed the address to %q", form, got)
}
}
// A refused address comes back in the field, escaped, with its choice.
rec := s.saveReports(url.Values{"dmarc_rua_mode": {"custom"}, "dmarc_rua_email": {`x">@no`}})
has(t, "refusal", rec.Body.String(), `