Files
Reef/dmarc/dmarc_test.go
T

158 lines
5.0 KiB
Go

package dmarc
import (
"archive/zip"
"bytes"
"compress/gzip"
"encoding/base64"
"strings"
"testing"
)
const sample = `<?xml version="1.0" encoding="UTF-8" ?>
<feedback>
<report_metadata>
<org_name>google.com</org_name>
<email>noreply-dmarc-support@google.com</email>
<report_id>12345</report_id>
<date_range><begin>1700000000</begin><end>1700086399</end></date_range>
</report_metadata>
<policy_published>
<domain>Example.COM</domain>
<adkim>r</adkim><aspf>r</aspf><p>none</p><sp>none</sp><pct>100</pct>
</policy_published>
<record>
<row>
<source_ip>203.0.113.7</source_ip><count>12</count>
<policy_evaluated><disposition>none</disposition><dkim>pass</dkim><spf>fail</spf></policy_evaluated>
</row>
<identifiers><header_from>example.com</header_from><envelope_from>bounce.example.com</envelope_from></identifiers>
<auth_results>
<dkim><domain>example.com</domain><selector>s1</selector><result>pass</result></dkim>
<spf><domain>bounce.example.com</domain><result>pass</result></spf>
</auth_results>
</record>
<record>
<row>
<source_ip>198.51.100.9</source_ip><count>3</count>
<policy_evaluated><disposition>quarantine</disposition><dkim>fail</dkim><spf>fail</spf></policy_evaluated>
</row>
<identifiers><header_from>example.com</header_from></identifiers>
<auth_results>
<spf><domain>example.com</domain><result>softfail</result></spf>
</auth_results>
</record>
</feedback>`
func check(t *testing.T, r *Report) {
t.Helper()
if r.Org != "google.com" || r.ReportID != "12345" || r.Domain != "example.com" {
t.Fatalf("metadata: %+v", r)
}
if r.Begin.Unix() != 1700000000 || r.End.Unix() != 1700086399 {
t.Fatalf("range: %v %v", r.Begin, r.End)
}
if r.Policy.P != "none" || r.Policy.Pct != 100 || r.Policy.ADKIM != "r" {
t.Fatalf("policy: %+v", r.Policy)
}
if len(r.Records) != 2 {
t.Fatalf("records: %d", len(r.Records))
}
a, b := r.Records[0], r.Records[1]
if a.SourceIP != "203.0.113.7" || a.Count != 12 || !a.Passed() || a.EnvelopeFrom != "bounce.example.com" {
t.Fatalf("first record: %+v", a)
}
if len(a.DKIMAuth) != 1 || a.DKIMAuth[0].Selector != "s1" || len(a.SPFAuth) != 1 {
t.Fatalf("auth results: %+v", a)
}
if b.Passed() || b.Disposition != "quarantine" || b.SPFAuth[0].Result != "softfail" {
t.Fatalf("second record: %+v", b)
}
}
func gz(t *testing.T, s string) []byte {
var buf bytes.Buffer
w := gzip.NewWriter(&buf)
w.Write([]byte(s))
w.Close()
return buf.Bytes()
}
func zipped(t *testing.T, s string) []byte {
var buf bytes.Buffer
w := zip.NewWriter(&buf)
f, _ := w.Create("report.xml")
f.Write([]byte(s))
w.Close()
return buf.Bytes()
}
func email(contentType, encoding string, body []byte) []byte {
b64 := base64.StdEncoding.EncodeToString(body)
var wrapped strings.Builder
for len(b64) > 76 {
wrapped.WriteString(b64[:76] + "\r\n")
b64 = b64[76:]
}
wrapped.WriteString(b64 + "\r\n")
return []byte("From: noreply@google.com\r\nSubject: Report Domain: example.com\r\nMIME-Version: 1.0\r\n" +
"Content-Type: multipart/mixed; boundary=\"XX\"\r\n\r\n--XX\r\nContent-Type: text/plain\r\n\r\nSee attached.\r\n" +
"--XX\r\nContent-Type: " + contentType + "\r\nContent-Transfer-Encoding: " + encoding + "\r\n\r\n" + wrapped.String() + "--XX--\r\n")
}
func TestParseForms(t *testing.T) {
forms := map[string][]byte{
"xml": []byte("\n" + sample),
"gzip": gz(t, sample),
"zip": zipped(t, sample),
"mail+gzip": email("application/gzip", "base64", gz(t, sample)),
"mail+zip": email("application/zip; name=r.zip", "base64", zipped(t, sample)),
"mail+xml": email("text/xml", "base64", []byte(sample)),
"mail+octet": email("application/octet-stream", "base64", gz(t, sample)),
}
for name, data := range forms {
t.Run(name, func(t *testing.T) {
r, err := Parse(data)
if err != nil {
t.Fatal(err)
}
check(t, r)
})
}
}
func TestParseRejects(t *testing.T) {
for name, data := range map[string][]byte{
"empty": nil,
"text": []byte("hello, this is not a report"),
"other xml": []byte("<html><body/></html>"),
"no id": []byte(strings.Replace(sample, "<report_id>12345</report_id>", "", 1)),
"mail no att": []byte("Subject: x\r\n\r\njust text\r\n"),
"bad gzip": {0x1f, 0x8b, 0, 0},
} {
t.Run(name, func(t *testing.T) {
if r, err := Parse(data); err == nil {
t.Fatalf("expected an error, got %+v", r)
}
})
}
}
func TestParseBombIsRefused(t *testing.T) {
big := "<feedback>" + strings.Repeat("x", maxXML+10) + "</feedback>"
if _, err := Parse(gz(t, big)); err == nil || !strings.Contains(err.Error(), "too large") {
t.Fatalf("want too large, got %v", err)
}
}
func TestPctDefaultsAndClamps(t *testing.T) {
r, err := Parse([]byte(strings.Replace(sample, "<pct>100</pct>", "<pct>25</pct>", 1)))
if err != nil || r.Policy.Pct != 25 {
t.Fatalf("pct 25: %v %+v", err, r)
}
r, err = Parse([]byte(strings.Replace(sample, "<pct>100</pct>", "", 1)))
if err != nil || r.Policy.Pct != 100 {
t.Fatalf("missing pct: %v %+v", err, r)
}
}