Files
selfpost/internal/store/dmarc_test.go
T
mixandClaude Opus 5.5 b81ff9f135 domains: a domain's DMARC reports are deleted with it (audit A5/W2)
Security audit 2026-10-17, finding A5/W2 (low), stage 1; the mockup is
7effc98. dmarc_reports belong to a domain by name, outlive the domain (no
FK), and a domain deleted and added again handed its old reports to
whoever it was assigned to then - the logs were moved to ids in 2.5.0 for
exactly this case, reports were not.

The cheap form of the plan: store.DeleteDomain deletes the domain's
reports (their records by the table's cascade) in the same transaction as
the domain. The confirmation page says so when the domain has reports
("delete the 12 DMARC reports received for it;"), counted by the new
store.CountDMARCReports; NewOutDomainDelete takes the count.

Tests: TestDeletingADomainDeletesItsReports (the domain added again has
none, another domain keeps its report and record, a second delete is
ErrDomainNotFound); TestOutDomainDeleteNamesWhatGoesWithIt with 12, 1 and
0 reports.

Docs: security.md § Roles; CHANGELOG; plan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 21:52:06 +03:00

148 lines
4.8 KiB
Go

package store
import (
"testing"
"time"
)
func TestDMARCReportRoundTrip(t *testing.T) {
st, err := Open(t.TempDir() + "/test.db")
if err != nil {
t.Fatal(err)
}
defer st.Close()
now := time.Now().UTC().Truncate(time.Second)
_, err = st.InsertDMARCReport(DMARCReport{
Domain: "example.com",
Reporter: "google.com",
ReportID: "abc",
PeriodBegin: now.Add(-24 * time.Hour),
PeriodEnd: now,
ReceivedAt: now,
PassCount: 5,
FailCount: 1,
Records: []DMARCReportRecord{{
SourceIP: "203.0.113.1", Count: 5, SPFResult: "pass", DKIMResult: "pass",
}},
})
if err != nil {
t.Fatal(err)
}
list, err := st.ListDMARCReports(DMARCReportScope{Domains: []string{"example.com"}}, 10)
if err != nil || len(list) != 1 {
t.Fatalf("list: %v err=%v", list, err)
}
got, err := st.GetDMARCReport(list[0].ID)
if err != nil {
t.Fatal(err)
}
if got.PassCount != 5 || len(got.Records) != 1 {
t.Fatalf("got %+v", got)
}
}
func TestPruneDMARCReports(t *testing.T) {
st, err := Open(t.TempDir() + "/test.db")
if err != nil {
t.Fatal(err)
}
defer st.Close()
old := time.Now().UTC().AddDate(0, 0, -120)
for i := 0; i < 3; i++ {
if _, err := st.InsertDMARCReport(DMARCReport{
Domain: "example.com", Reporter: "r", ReportID: string(rune('a' + i)),
PeriodBegin: old, PeriodEnd: old, ReceivedAt: old,
}); err != nil {
t.Fatal(err)
}
}
if err := st.PruneDMARCReports("example.com"); err != nil {
t.Fatal(err)
}
list, _ := st.ListDMARCReports(DMARCReportScope{AllDomains: true}, 100)
if len(list) != 0 {
t.Fatalf("expected prune by age, got %d", len(list))
}
}
// The domains a list is limited to are the whole answer: an empty list is a
// viewer who reaches nothing, never a viewer who reaches everything.
func TestListDMARCReportsEmptyListIsNoRows(t *testing.T) {
st := openTestStore(t)
now := time.Now().UTC()
for _, domain := range []string{"a.example", "b.example"} {
if _, err := st.InsertDMARCReport(DMARCReport{
Domain: domain, Reporter: "r", ReportID: domain,
PeriodBegin: now.Add(-24 * time.Hour), PeriodEnd: now, ReceivedAt: now,
}); err != nil {
t.Fatal(err)
}
}
for name, scope := range map[string]DMARCReportScope{
"an empty list": {Domains: []string{}},
"no scope at all": {},
} {
list, err := st.ListDMARCReports(scope, 10)
if err != nil {
t.Fatalf("%s is an error: %v", name, err)
}
if len(list) != 0 {
t.Errorf("%s returned %d report(s), want none", name, len(list))
}
}
list, err := st.ListDMARCReports(DMARCReportScope{Domains: []string{"a.example"}}, 10)
if err != nil || len(list) != 1 || list[0].Domain != "a.example" {
t.Errorf("one domain = %+v, %v; want only a.example", list, err)
}
list, err = st.ListDMARCReports(DMARCReportScope{AllDomains: true}, 10)
if err != nil || len(list) != 2 {
t.Errorf("all domains = %d report(s), %v; want both", len(list), err)
}
list, err = st.ListDMARCReports(DMARCReportScope{AllDomains: true, Domains: []string{"a.example"}}, 10)
if err != nil || len(list) != 2 {
t.Errorf("all domains with a list = %d report(s), %v; want both (AllDomains wins)", len(list), err)
}
}
// A domain's DMARC reports go when the domain does, records included, and
// another domain's stay: a domain added again under the old name starts with
// none (security audit 2026-10-17, A5/W2).
func TestDeletingADomainDeletesItsReports(t *testing.T) {
st := openTestStore(t)
gone, _ := st.AddDomain("gone.example", "sel")
if _, err := st.AddDomain("kept.example", "sel"); err != nil {
t.Fatal(err)
}
now := time.Now().UTC().Truncate(time.Second)
for _, d := range []string{"gone.example", "kept.example"} {
if _, err := st.InsertDMARCReport(DMARCReport{
Domain: d, Reporter: "google.com", ReportID: "r-" + d, PeriodBegin: now.Add(-24 * time.Hour), PeriodEnd: now, ReceivedAt: now,
Records: []DMARCReportRecord{{SourceIP: "203.0.113.1", Count: 1, SPFResult: "pass", DKIMResult: "pass"}},
}); err != nil {
t.Fatal(err)
}
}
if err := st.DeleteDomain(gone.ID); err != nil {
t.Fatal(err)
}
if _, err := st.AddDomain("gone.example", "sel"); err != nil {
t.Fatal(err)
}
if list, err := st.ListDMARCReports(DMARCReportScope{Domains: []string{"gone.example"}}, 10); err != nil || len(list) != 0 {
t.Errorf("the domain added again has %d old reports (%v)", len(list), err)
}
if list, err := st.ListDMARCReports(DMARCReportScope{Domains: []string{"kept.example"}}, 10); err != nil || len(list) != 1 {
t.Errorf("the other domain has %d reports (%v), want 1", len(list), err)
}
var records int
if err := st.db.QueryRow("SELECT COUNT(*) FROM dmarc_report_records").Scan(&records); err != nil || records != 1 {
t.Errorf("%d report records left (%v), want the kept domain's one", records, err)
}
if err := st.DeleteDomain(gone.ID); err != ErrDomainNotFound {
t.Errorf("deleting it again = %v, want ErrDomainNotFound", err)
}
}