Files
rdapper/src/rdap/normalize.test.ts
T
jake 386006dc61 feat: add RFC 9537 redaction support, vCard address/fax parsing, and registrar address fields
- Parse the top-level RDAP `redacted` array (RFC 9537) into a new `redactions` field on `DomainRecord`; set `privacyEnabled` when any redaction references "registrant"
- Expose `street`, `city`, `state`, `postalCode`, `country`, and `countryCode` on both `RegistrarInfo` and `Contact`; extract ISO 3166-1 alpha-2 from the vCard `cc` parameter (RFC 8605)
- Split `tel` entries by fax type parameter (`"fax"` / `["work","fax"]`) into separate `tel` and `fax` lists; collect multiple email addresses as an array, collapsing singletons to a plain string
- Prefer an exact `registration` event match over `reregistration` when extracting `creationDate` (RFC 9083)
- Merge `redacted` arrays in `mergeRdapDocs`, deduplicating by JSON identity
- Split `adr` street on `\r?\n` only (not commas) to avoid splitting inline comma-separated addresses
2026-09-19 23:01:30 -04:00

270 lines
7.3 KiB
TypeScript

import { expect, test } from "vitest";
import { normalizeRdap } from "./normalize";
test("normalizeRdap maps registrar, contacts, nameservers, events, dnssec", () => {
const rdap = {
ldhName: "example.com",
unicodeName: "example.com",
entities: [
{
roles: ["registrar"],
vcardArray: [
"vcard",
[
["fn", {}, "text", "Registrar LLC"],
["email", {}, "text", "support@registrar.test"],
["tel", {}, "text", "+1.5555555555"],
["url", {}, "text", "https://registrar.example"],
],
],
publicIds: [{ type: "IANA Registrar ID", identifier: "9999" }],
},
{
roles: ["registrant"],
vcardArray: [
"vcard",
[
["fn", {}, "text", "Alice Registrant"],
["email", {}, "text", "alice@example.com"],
],
],
},
{
roles: ["administrative"],
vcardArray: ["vcard", [["fn", {}, "text", "Bob Admin"]]],
},
{
roles: ["technical"],
vcardArray: ["vcard", [["fn", {}, "text", "Carol Tech"]]],
},
],
nameservers: [
{
ldhName: "NS1.EXAMPLE.COM",
ipAddresses: { v4: ["192.0.2.1"], v6: ["2001:db8::1"] },
},
{ unicodeName: "ns2.example.com" },
],
secureDNS: {
delegationSigned: true,
dsData: [{ keyTag: 12345, algorithm: 13, digestType: 2, digest: "ABCDEF" }],
},
events: [
{ eventAction: "registration", eventDate: "2020-01-02T03:04:05Z" },
{ eventAction: "last changed", eventDate: "2021-01-02T03:04:05Z" },
{ eventAction: "expiration", eventDate: "2030-01-02T03:04:05Z" },
],
status: ["clientTransferProhibited"],
port43: "whois.example-registrar.test",
};
const rec = normalizeRdap("example.com", "com", rdap, ["https://rdap.example/"]);
expect(rec.domain).toBe("example.com");
expect(rec.tld).toBe("com");
expect(rec.registrar?.name).toBe("Registrar LLC");
expect(rec.registrar?.ianaId).toBe("9999");
expect(rec.contacts && rec.contacts.length >= 3).toBe(true);
expect(rec.nameservers && rec.nameservers.length === 2).toBe(true);
expect(rec.nameservers).toBeDefined();
expect(rec.nameservers?.[0]?.host).toBe("ns1.example.com");
expect(rec.dnssec?.enabled).toBeTruthy();
expect(rec.creationDate).toBe("2020-01-02T03:04:05Z");
expect(rec.expirationDate).toBe("2030-01-02T03:04:05Z");
expect(rec.transferLock).toBe(true);
expect(rec.whoisServer).toBe("whois.example-registrar.test");
expect(rec.source).toBe("rdap");
});
test("normalizeRdap derives privacyEnabled from registrant keywords", () => {
const rdap = {
ldhName: "example.com",
unicodeName: "example.com",
entities: [
{
roles: ["registrant"],
vcardArray: [
"vcard",
[
["fn", {}, "text", "REDACTED FOR PRIVACY"],
["org", {}, "text", "Example Org"],
],
],
},
],
};
const rec = normalizeRdap("example.com", "com", rdap, ["https://rdap.example/"]);
expect(rec.privacyEnabled).toBe(true);
});
test("normalizeRdap detects transfer lock with spaced status", () => {
const rdap = {
ldhName: "example.com",
status: ["client transfer prohibited"],
};
const rec = normalizeRdap("example.com", "com", rdap, []);
expect(rec.transferLock).toBe(true);
});
test("normalizeRdap detects transfer lock with camelCase status", () => {
const rdap = {
ldhName: "example.com",
status: ["clientTransferProhibited"],
};
const rec = normalizeRdap("example.com", "com", rdap, []);
expect(rec.transferLock).toBe(true);
});
test("normalizeRdap treats release-pending statuses as not registered", () => {
const rec = normalizeRdap(
"iba.com.br",
"com.br",
{
ldhName: "iba.com.br",
status: ["pending release"],
},
[],
);
expect(rec.isRegistered).toBe(false);
const active = normalizeRdap(
"example.com",
"com",
{
ldhName: "example.com",
status: ["active"],
},
[],
);
expect(active.isRegistered).toBe(true);
});
test("normalizeRdap reads vCard adr cc parameter into countryCode", () => {
const rec = normalizeRdap(
"example.com",
"com",
{
ldhName: "example.com",
entities: [
{
roles: ["registrant"],
vcardArray: [
"vcard",
[["adr", { cc: "us" }, "text", ["", "", "1 Main St", "Town", "CA", "90000", "USA"]]],
],
},
],
},
[],
);
expect(rec.contacts?.[0]?.country).toBe("USA");
expect(rec.contacts?.[0]?.countryCode).toBe("US");
});
test("normalizeRdap parses RFC 9537 redacted array and flags privacy", () => {
const rec = normalizeRdap(
"example.com",
"com",
{
ldhName: "example.com",
redacted: [
{
name: { description: "Registrant Email" },
prePath: "$.entities[?(@.roles[0]=='registrant')].vcardArray[1][?(@[0]=='email')]",
method: "emptyValue",
reason: { description: "Server policy" },
},
],
},
[],
);
expect(rec.redactions).toEqual([
expect.objectContaining({
name: "Registrant Email",
method: "emptyValue",
reason: "Server policy",
}),
]);
expect(rec.privacyEnabled).toBe(true);
});
test("normalizeRdap separates fax from tel and keeps multiple emails", () => {
const rec = normalizeRdap(
"example.com",
"com",
{
ldhName: "example.com",
entities: [
{
roles: ["registrant"],
vcardArray: [
"vcard",
[
["tel", { type: "voice" }, "text", "+1.111"],
["tel", { type: ["work", "fax"] }, "text", "+1.222"],
["email", {}, "text", "a@example.com"],
["email", {}, "text", "b@example.com"],
["adr", {}, "text", ["", "", "Suite 5, 1 Main St", "Town", "", "", ""]],
],
],
},
],
},
[],
);
const c = rec.contacts?.[0];
expect(c?.phone).toBe("+1.111");
expect(c?.fax).toBe("+1.222");
expect(c?.email).toEqual(["a@example.com", "b@example.com"]);
expect(c?.street).toEqual(["Suite 5, 1 Main St"]);
});
test("normalizeRdap prefers registration event over reregistration", () => {
const rec = normalizeRdap(
"example.com",
"com",
{
ldhName: "example.com",
events: [
{ eventAction: "reregistration", eventDate: "2024-01-01T00:00:00Z" },
{ eventAction: "registration", eventDate: "2020-01-01T00:00:00Z" },
],
},
[],
);
expect(rec.creationDate).toBe("2020-01-01T00:00:00Z");
});
test("normalizeRdap maps registrar adr and cc", () => {
const rec = normalizeRdap(
"example.com",
"com",
{
ldhName: "example.com",
entities: [
{
roles: ["registrar"],
vcardArray: [
"vcard",
[
["fn", {}, "text", "Registrar LLC"],
[
"adr",
{ cc: "CA" },
"text",
["", "", "5 King St", "Toronto", "ON", "M5H", "Canada"],
],
],
],
},
],
},
[],
);
expect(rec.registrar).toMatchObject({
street: ["5 King St"],
city: "Toronto",
state: "ON",
postalCode: "M5H",
country: "Canada",
countryCode: "CA",
});
});