import { describe, expect, it } from "@suaveplan/testing/runner";
import { gzipSync, strToU8 } from "fflate";
import { DMARC_CODES, DmarcError } from "../errors/dmarc-error.js";
import { parseDmarcAggregate } from "./aggregate.js";
const MINIMAL_XML = `
1.0
Example Corp
dmarc@example.com
12345
16094592001609545600
example.com
r
r
none
100
192.0.2.1
5
none
pass
pass
example.com
example.compass
example.compassmfrom
`;
const MULTI_RECORD_XML = `
1.0
Multi Corp
dmarc@multi.com
99
16094592001609545600
multi.com
s
s
reject
quarantine
50
10.0.0.1
3
reject
fail
fail
multi.com
bounce@multi.com
multi.comfaildkim1
multi.comfail
10.0.0.2
1
none
pass
pass
multi.com
multi.compass
multi.compass
`;
const ERROR_XML = `
1.0
Example Corp
dmarc@example.com
12345
16094592001609545600
Something went wrong
example.com
r
r
none
100
`;
describe("parseDmarcAggregate", () => {
it("parses minimal valid XML", { timeout: 5000 }, () => {
const report = parseDmarcAggregate(MINIMAL_XML);
// fast-xml-parser coerces "1.0" to the number 1, which str() converts to "1"
expect(report.version).toBe("1");
expect(report.reportMetadata.orgName).toBe("Example Corp");
expect(report.reportMetadata.email).toBe("dmarc@example.com");
expect(report.reportMetadata.reportId).toBe("12345");
expect(report.reportMetadata.dateRange.begin).toBe(1609459200);
expect(report.reportMetadata.dateRange.end).toBe(1609545600);
});
it("maps policy_published fields correctly", { timeout: 5000 }, () => {
const report = parseDmarcAggregate(MINIMAL_XML);
expect(report.policyPublished.domain).toBe("example.com");
expect(report.policyPublished.adkim).toBe("r");
expect(report.policyPublished.aspf).toBe("r");
expect(report.policyPublished.p).toBe("none");
expect(report.policyPublished.pct).toBe(100);
});
it("maps record fields correctly", { timeout: 5000 }, () => {
const report = parseDmarcAggregate(MINIMAL_XML);
expect(report.record).toHaveLength(1);
const rec = report.record[0];
expect(rec.row.sourceIp).toBe("192.0.2.1");
expect(rec.row.count).toBe(5);
expect(rec.row.policyEvaluated.disposition).toBe("none");
expect(rec.row.policyEvaluated.dkim).toBe("pass");
expect(rec.row.policyEvaluated.spf).toBe("pass");
expect(rec.identifiers.headerFrom).toBe("example.com");
});
it("maps auth_results fields", { timeout: 5000 }, () => {
const report = parseDmarcAggregate(MINIMAL_XML);
const auth = report.record[0].authResults;
expect(auth.dkim).toBeDefined();
expect(auth.spf).toBeDefined();
});
it("parses gzip-compressed XML (Uint8Array)", { timeout: 5000 }, () => {
const compressed = gzipSync(strToU8(MINIMAL_XML));
const report = parseDmarcAggregate(compressed);
expect(report.reportMetadata.orgName).toBe("Example Corp");
});
it("parses plain XML as Uint8Array", { timeout: 5000 }, () => {
const bytes = new TextEncoder().encode(MINIMAL_XML);
const report = parseDmarcAggregate(bytes);
expect(report.reportMetadata.orgName).toBe("Example Corp");
});
it("handles multiple records", { timeout: 5000 }, () => {
const report = parseDmarcAggregate(MULTI_RECORD_XML);
expect(report.record).toHaveLength(2);
expect(report.policyPublished.sp).toBe("quarantine");
});
it("handles optional envelope_from", { timeout: 5000 }, () => {
const report = parseDmarcAggregate(MULTI_RECORD_XML);
expect(report.record[0].identifiers.envelopeFrom).toBe(
"bounce@multi.com"
);
});
it("handles dkim selector", { timeout: 5000 }, () => {
const report = parseDmarcAggregate(MULTI_RECORD_XML);
const dkim = report.record[0].authResults.dkim;
expect(dkim).toBeDefined();
});
it("parses report with error elements", { timeout: 5000 }, () => {
const report = parseDmarcAggregate(ERROR_XML);
expect(report.reportMetadata.errors).toEqual(["Something went wrong"]);
expect(report.record).toHaveLength(0);
});
it("throws DmarcError on missing ", { timeout: 5000 }, () => {
expect(() => parseDmarcAggregate("")).toThrowError(
DmarcError
);
try {
parseDmarcAggregate("");
} catch (err) {
expect(err).toBeInstanceOf(DmarcError);
expect((err as DmarcError).code).toBe(DMARC_CODES.INVALID_REPORT);
}
});
it("throws DmarcError on missing ", {
timeout: 5000,
}, () => {
const xml = "1.0";
expect(() => parseDmarcAggregate(xml)).toThrowError(DmarcError);
try {
parseDmarcAggregate(xml);
} catch (err) {
expect((err as DmarcError).code).toBe(DMARC_CODES.INVALID_REPORT);
}
});
it("throws DmarcError on missing ", {
timeout: 5000,
}, () => {
const xml = `
1.0
Xx@x.com1
01
`;
expect(() => parseDmarcAggregate(xml)).toThrowError(DmarcError);
try {
parseDmarcAggregate(xml);
} catch (err) {
expect((err as DmarcError).code).toBe(DMARC_CODES.INVALID_REPORT);
}
});
it("throws DmarcError on malformed XML (unclosed attribute)", {
timeout: 5000,
}, () => {
expect(() =>
parseDmarcAggregate('", {
timeout: 5000,
}, () => {
const xml = `
1.0
Xx@x.com1
01
x.comrrnone
100
x.com
`;
expect(() => parseDmarcAggregate(xml)).toThrowError(DmarcError);
try {
parseDmarcAggregate(xml);
} catch (err) {
expect((err as DmarcError).code).toBe(DMARC_CODES.INVALID_REPORT);
}
});
it("throws DmarcError when record is missing ", {
timeout: 5000,
}, () => {
const xml = `
1.0
Xx@x.com1
01
x.comrrnone
100
1.2.3.41
x.com
`;
expect(() => parseDmarcAggregate(xml)).toThrowError(DmarcError);
try {
parseDmarcAggregate(xml);
} catch (err) {
expect((err as DmarcError).code).toBe(DMARC_CODES.INVALID_REPORT);
}
});
it("accepts decompress: true option explicitly", { timeout: 5000 }, () => {
const compressed = gzipSync(strToU8(MINIMAL_XML));
const report = parseDmarcAggregate(compressed, { decompress: true });
expect(report.reportMetadata.orgName).toBe("Example Corp");
});
it("handles multiple DKIM entries in auth_results", {
timeout: 5000,
}, () => {
const xml = `
1.0
Xx@x.com1
01
x.comrrnone
100
1.2.3.41
nonepasspass
x.com
x.compasssel1
y.compass
`;
const report = parseDmarcAggregate(xml);
expect(Array.isArray(report.record[0].authResults.dkim)).toBe(true);
});
it("handles invalid disposition value (schema validation failure)", {
timeout: 5000,
}, () => {
const xml = `
1.0
Xx@x.com1
01
x.comrrinvalid_disposition
100
`;
expect(() => parseDmarcAggregate(xml)).toThrowError(DmarcError);
try {
parseDmarcAggregate(xml);
} catch (err) {
expect((err as DmarcError).code).toBe(DMARC_CODES.INVALID_REPORT);
}
});
it("throws DmarcError when record element is an empty string (non-object)", {
timeout: 5000,
}, () => {
// produces "" string in fast-xml-parser, not an object
const xml = `
1.0
Xx@x.com1
01
x.comrrnone
100
`;
expect(() => parseDmarcAggregate(xml)).toThrowError(DmarcError);
try {
parseDmarcAggregate(xml);
} catch (err) {
expect((err as DmarcError).code).toBe(DMARC_CODES.INVALID_REPORT);
}
});
it("num() handles non-numeric string (NaN -> 0) and empty string (0) date range", {
timeout: 5000,
}, () => {
// NaN path: non-parseable string -> NaN -> 0
const xmlNaN = `
1.0
Xx@x.com1
notanumberalsonotanumber
x.comrrnone
100
`;
const reportNaN = parseDmarcAggregate(xmlNaN);
expect(reportNaN.reportMetadata.dateRange.begin).toBe(0);
// Finite path: empty string -> Number("") = 0 (finite) -> returns 0
const xmlEmpty = `
1.0
Xx@x.com1
x.comrrnone
100
`;
const reportEmpty = parseDmarcAggregate(xmlEmpty);
expect(reportEmpty.reportMetadata.dateRange.begin).toBe(0);
});
it("adkim/aspf defaults to 'r' when absent from policy_published", {
timeout: 5000,
}, () => {
// Missing adkim/aspf elements triggers the || "r" fallback
const xml = `
1.0
Xx@x.com1
01
x.comnone
100
`;
const report = parseDmarcAggregate(xml);
expect(report.policyPublished.adkim).toBe("r");
expect(report.policyPublished.aspf).toBe("r");
});
it("handles multiple SPF entries with scope in auth_results", {
timeout: 5000,
}, () => {
const xml = `
1.0
Xx@x.com1
01
x.comrrnone
100
1.2.3.41
nonepasspass
x.com
x.compassmfrom
y.comfail
`;
const report = parseDmarcAggregate(xml);
const spf = report.record[0].authResults.spf;
expect(Array.isArray(spf)).toBe(true);
});
it("handles envelope_to in identifiers", { timeout: 5000 }, () => {
const xml = `
1.0
Xx@x.com1
01
x.comrrnone
100
1.2.3.41
nonepasspass
x.com
dest@x.com
`;
const report = parseDmarcAggregate(xml);
expect(report.record[0].identifiers.envelopeTo).toBe("dest@x.com");
});
});