mirror of
https://github.com/Xevion/rdap.git
synced 2025-12-05 23:15:58 -06:00
test: add comprehensive edge case tests and fix IPv6/domain validation
Enhances type detection validation to prevent false positives from ambiguous inputs. IPv6 validation now requires at least one colon and rejects invalid patterns like ":::" or single characters. Domain validation rejects pure numeric strings that are likely incomplete IP addresses. Includes 274 new test cases covering single characters, short hex strings, incomplete IPs, and boundary conditions.
This commit is contained in:
@@ -404,3 +404,278 @@ describe("getType - Case sensitivity", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("getType - Edge cases and false positives", () => {
|
||||
describe("Single character inputs (should fail)", () => {
|
||||
it("should NOT detect single hex character 'a' as IPv6", async () => {
|
||||
const result = await getType("a", mockGetRegistry);
|
||||
expect(result.isErr).toBe(true);
|
||||
if (result.isErr) {
|
||||
expect(result.error.message).toContain("No patterns matched");
|
||||
}
|
||||
});
|
||||
|
||||
it("should NOT detect single hex character 'f' as IPv6", async () => {
|
||||
const result = await getType("f", mockGetRegistry);
|
||||
expect(result.isErr).toBe(true);
|
||||
});
|
||||
|
||||
it("should NOT detect single digit '1' as any type", async () => {
|
||||
const result = await getType("1", mockGetRegistry);
|
||||
expect(result.isErr).toBe(true);
|
||||
});
|
||||
|
||||
it("should NOT detect single letter 'z' as any type", async () => {
|
||||
const result = await getType("z", mockGetRegistry);
|
||||
expect(result.isErr).toBe(true);
|
||||
});
|
||||
|
||||
it("should NOT detect single colon ':' as IPv6", async () => {
|
||||
const result = await getType(":", mockGetRegistry);
|
||||
expect(result.isErr).toBe(true);
|
||||
});
|
||||
|
||||
it("should NOT detect single dot '.' as any type", async () => {
|
||||
const result = await getType(".", mockGetRegistry);
|
||||
expect(result.isErr).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Short hex-only strings without colons (should fail)", () => {
|
||||
it("should NOT detect 'abc' as IPv6", async () => {
|
||||
const result = await getType("abc", mockGetRegistry);
|
||||
expect(result.isErr).toBe(true);
|
||||
if (result.isErr) {
|
||||
expect(result.error.message).toContain("No patterns matched");
|
||||
}
|
||||
});
|
||||
|
||||
it("should NOT detect 'def' as IPv6", async () => {
|
||||
const result = await getType("def", mockGetRegistry);
|
||||
expect(result.isErr).toBe(true);
|
||||
});
|
||||
|
||||
it("should NOT detect 'ff' as IPv6", async () => {
|
||||
const result = await getType("ff", mockGetRegistry);
|
||||
expect(result.isErr).toBe(true);
|
||||
});
|
||||
|
||||
it("should NOT detect '1234' as IPv6", async () => {
|
||||
const result = await getType("1234", mockGetRegistry);
|
||||
expect(result.isErr).toBe(true);
|
||||
});
|
||||
|
||||
it("should NOT detect 'abcdef' as IPv6", async () => {
|
||||
const result = await getType("abcdef", mockGetRegistry);
|
||||
expect(result.isErr).toBe(true);
|
||||
});
|
||||
|
||||
it("should NOT detect 'deadbeef' as IPv6", async () => {
|
||||
const result = await getType("deadbeef", mockGetRegistry);
|
||||
expect(result.isErr).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Incomplete IP-like inputs (should fail)", () => {
|
||||
it("should NOT detect '1.2' as IPv4", async () => {
|
||||
const result = await getType("1.2", mockGetRegistry);
|
||||
expect(result.isErr).toBe(true);
|
||||
});
|
||||
|
||||
it("should NOT detect '1.2.3' as IPv4", async () => {
|
||||
const result = await getType("1.2.3", mockGetRegistry);
|
||||
expect(result.isErr).toBe(true);
|
||||
});
|
||||
|
||||
it("should NOT detect '1.2.3.4.5' as IPv4", async () => {
|
||||
const result = await getType("1.2.3.4.5", mockGetRegistry);
|
||||
expect(result.isErr).toBe(true);
|
||||
});
|
||||
|
||||
it("should NOT detect '192.168' as IPv4", async () => {
|
||||
const result = await getType("192.168", mockGetRegistry);
|
||||
expect(result.isErr).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Dot-only and dot-related edge cases (should fail)", () => {
|
||||
it("should NOT detect '..' as any type", async () => {
|
||||
const result = await getType("..", mockGetRegistry);
|
||||
expect(result.isErr).toBe(true);
|
||||
});
|
||||
|
||||
it("should NOT detect '...' as any type", async () => {
|
||||
const result = await getType("...", mockGetRegistry);
|
||||
expect(result.isErr).toBe(true);
|
||||
});
|
||||
|
||||
it("should NOT detect 'a.' as domain (trailing dot without TLD)", async () => {
|
||||
const result = await getType("a.", mockGetRegistry);
|
||||
expect(result.isErr).toBe(true);
|
||||
});
|
||||
|
||||
it("should NOT detect '.a' as domain (it's a TLD)", async () => {
|
||||
const result = await getType(".a", mockGetRegistry);
|
||||
expect(result.isOk).toBe(true);
|
||||
if (result.isOk) {
|
||||
expect(result.value).toBe("tld");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Incomplete ASN formats (should fail)", () => {
|
||||
it("should NOT detect 'AS' as ASN", async () => {
|
||||
const result = await getType("AS", mockGetRegistry);
|
||||
expect(result.isErr).toBe(true);
|
||||
});
|
||||
|
||||
it("should NOT detect 'A1234' as ASN", async () => {
|
||||
const result = await getType("A1234", mockGetRegistry);
|
||||
expect(result.isErr).toBe(true);
|
||||
});
|
||||
|
||||
it("should NOT detect 'S1234' as ASN", async () => {
|
||||
const result = await getType("S1234", mockGetRegistry);
|
||||
expect(result.isErr).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Colon-only edge cases", () => {
|
||||
it("should NOT detect ':::' as IPv6", async () => {
|
||||
const result = await getType(":::", mockGetRegistry);
|
||||
expect(result.isErr).toBe(true);
|
||||
});
|
||||
|
||||
it("should NOT detect '::::' as IPv6", async () => {
|
||||
const result = await getType("::::", mockGetRegistry);
|
||||
expect(result.isErr).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Minimal valid inputs (lenient - should pass)", () => {
|
||||
it("should detect '::1' as IPv6 (localhost)", async () => {
|
||||
const result = await getType("::1", mockGetRegistry);
|
||||
expect(result.isOk).toBe(true);
|
||||
if (result.isOk) {
|
||||
expect(result.value).toBe("ip6");
|
||||
}
|
||||
});
|
||||
|
||||
it("should detect '::' as IPv6 (all zeros)", async () => {
|
||||
const result = await getType("::", mockGetRegistry);
|
||||
expect(result.isOk).toBe(true);
|
||||
if (result.isOk) {
|
||||
expect(result.value).toBe("ip6");
|
||||
}
|
||||
});
|
||||
|
||||
it("should detect 'a::' as IPv6", async () => {
|
||||
const result = await getType("a::", mockGetRegistry);
|
||||
expect(result.isOk).toBe(true);
|
||||
if (result.isOk) {
|
||||
expect(result.value).toBe("ip6");
|
||||
}
|
||||
});
|
||||
|
||||
it("should detect '::a' as IPv6", async () => {
|
||||
const result = await getType("::a", mockGetRegistry);
|
||||
expect(result.isOk).toBe(true);
|
||||
if (result.isOk) {
|
||||
expect(result.value).toBe("ip6");
|
||||
}
|
||||
});
|
||||
|
||||
it("should detect 'a.b' as domain (short labels)", async () => {
|
||||
const result = await getType("a.b", mockGetRegistry);
|
||||
expect(result.isOk).toBe(true);
|
||||
if (result.isOk) {
|
||||
expect(result.value).toBe("domain");
|
||||
}
|
||||
});
|
||||
|
||||
it("should detect 'AS1' as ASN (minimal ASN)", async () => {
|
||||
const result = await getType("AS1", mockGetRegistry);
|
||||
expect(result.isOk).toBe(true);
|
||||
if (result.isOk) {
|
||||
expect(result.value).toBe("autnum");
|
||||
}
|
||||
});
|
||||
|
||||
it("should detect 'as1' as ASN (lowercase minimal)", async () => {
|
||||
const result = await getType("as1", mockGetRegistry);
|
||||
expect(result.isOk).toBe(true);
|
||||
if (result.isOk) {
|
||||
expect(result.value).toBe("autnum");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Boundary IPv4 tests", () => {
|
||||
it("should detect '0.0.0.0' as IPv4", async () => {
|
||||
const result = await getType("0.0.0.0", mockGetRegistry);
|
||||
expect(result.isOk).toBe(true);
|
||||
if (result.isOk) {
|
||||
expect(result.value).toBe("ip4");
|
||||
}
|
||||
});
|
||||
|
||||
it("should detect '255.255.255.255' as IPv4", async () => {
|
||||
const result = await getType("255.255.255.255", mockGetRegistry);
|
||||
expect(result.isOk).toBe(true);
|
||||
if (result.isOk) {
|
||||
expect(result.value).toBe("ip4");
|
||||
}
|
||||
});
|
||||
|
||||
it("should detect IPv4 with /0 CIDR", async () => {
|
||||
const result = await getType("0.0.0.0/0", mockGetRegistry);
|
||||
expect(result.isOk).toBe(true);
|
||||
if (result.isOk) {
|
||||
expect(result.value).toBe("ip4");
|
||||
}
|
||||
});
|
||||
|
||||
it("should detect IPv4 with /32 CIDR", async () => {
|
||||
const result = await getType("192.168.1.1/32", mockGetRegistry);
|
||||
expect(result.isOk).toBe(true);
|
||||
if (result.isOk) {
|
||||
expect(result.value).toBe("ip4");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Boundary IPv6 tests", () => {
|
||||
it("should detect IPv6 with /0 CIDR", async () => {
|
||||
const result = await getType("::/0", mockGetRegistry);
|
||||
expect(result.isOk).toBe(true);
|
||||
if (result.isOk) {
|
||||
expect(result.value).toBe("ip6");
|
||||
}
|
||||
});
|
||||
|
||||
it("should detect IPv6 with /128 CIDR", async () => {
|
||||
const result = await getType("::1/128", mockGetRegistry);
|
||||
expect(result.isOk).toBe(true);
|
||||
if (result.isOk) {
|
||||
expect(result.value).toBe("ip6");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Ambiguous hex strings (should prioritize correctly)", () => {
|
||||
it("should NOT detect 'cafe' as IPv6 (missing colon)", async () => {
|
||||
const result = await getType("cafe", mockGetRegistry);
|
||||
expect(result.isErr).toBe(true);
|
||||
});
|
||||
|
||||
it("should NOT detect 'dead' as IPv6 (missing colon)", async () => {
|
||||
const result = await getType("dead", mockGetRegistry);
|
||||
expect(result.isErr).toBe(true);
|
||||
});
|
||||
|
||||
it("should NOT detect 'beef' as IPv6 (missing colon)", async () => {
|
||||
const result = await getType("beef", mockGetRegistry);
|
||||
expect(result.isErr).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -68,11 +68,17 @@ const TypeValidators = new Map<TargetType, (args: ValidatorArgs) => Promise<Vali
|
||||
"ip6",
|
||||
({ value }) => {
|
||||
// Basic format check (hex characters, colons, optional CIDR)
|
||||
// MUST contain at least one colon to be a valid IPv6 address
|
||||
const match = value.match(/^([0-9a-fA-F:]+)(\/\d{1,3})?$/);
|
||||
if (!match) return Promise.resolve(false);
|
||||
|
||||
const ipPart = match[1] ?? "";
|
||||
|
||||
// Require at least one colon (essential for IPv6 structure)
|
||||
if (!ipPart.includes(":")) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
// Check for invalid characters
|
||||
if (!/^[0-9a-fA-F:]+$/.test(ipPart)) {
|
||||
return Promise.resolve("Invalid IPv6 address: contains invalid characters");
|
||||
@@ -84,6 +90,13 @@ const TypeValidators = new Map<TargetType, (args: ValidatorArgs) => Promise<Vali
|
||||
return Promise.resolve("Invalid IPv6 address: :: can only appear once");
|
||||
}
|
||||
|
||||
// Reject invalid colon patterns (e.g., ":::")
|
||||
// Valid patterns: "::", "a::", "::a", "a::b", etc.
|
||||
// Invalid: ":::", "::::", single ":", etc.
|
||||
if (ipPart === ":" || /:{3,}/.test(ipPart)) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
// Validate CIDR prefix if present
|
||||
if (match[2]) {
|
||||
const prefix = parseInt(match[2].substring(1), 10);
|
||||
@@ -131,11 +144,21 @@ const TypeValidators = new Map<TargetType, (args: ValidatorArgs) => Promise<Vali
|
||||
({ value }) => {
|
||||
// Case-insensitive domain matching with support for multiple labels
|
||||
// Matches: example.com, www.example.com, a.b.c.d.example.net, etc.
|
||||
return Promise.resolve(
|
||||
/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/i.test(
|
||||
value
|
||||
)
|
||||
);
|
||||
const domainPattern =
|
||||
/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/i;
|
||||
|
||||
if (!domainPattern.test(value)) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
// Reject pure numeric domains (e.g., "1.2", "192.168")
|
||||
// These are likely incomplete IPs, not domains
|
||||
// Valid domains must have at least one letter somewhere
|
||||
if (/^[\d.]+$/.test(value)) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
],
|
||||
["registrar", () => Promise.resolve(false)],
|
||||
|
||||
Reference in New Issue
Block a user