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:
2025-10-23 14:51:16 -05:00
parent 5646fd8006
commit 9350864929
2 changed files with 303 additions and 5 deletions

View File

@@ -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);
});
});
});

View File

@@ -68,11 +68,17 @@ const TypeValidators = new Map<TargetType, (args: ValidatorArgs) => Promise<Vali
"ip6", "ip6",
({ value }) => { ({ value }) => {
// Basic format check (hex characters, colons, optional CIDR) // 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})?$/); const match = value.match(/^([0-9a-fA-F:]+)(\/\d{1,3})?$/);
if (!match) return Promise.resolve(false); if (!match) return Promise.resolve(false);
const ipPart = match[1] ?? ""; const ipPart = match[1] ?? "";
// Require at least one colon (essential for IPv6 structure)
if (!ipPart.includes(":")) {
return Promise.resolve(false);
}
// Check for invalid characters // Check for invalid characters
if (!/^[0-9a-fA-F:]+$/.test(ipPart)) { if (!/^[0-9a-fA-F:]+$/.test(ipPart)) {
return Promise.resolve("Invalid IPv6 address: contains invalid characters"); 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"); 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 // Validate CIDR prefix if present
if (match[2]) { if (match[2]) {
const prefix = parseInt(match[2].substring(1), 10); const prefix = parseInt(match[2].substring(1), 10);
@@ -131,11 +144,21 @@ const TypeValidators = new Map<TargetType, (args: ValidatorArgs) => Promise<Vali
({ value }) => { ({ value }) => {
// Case-insensitive domain matching with support for multiple labels // Case-insensitive domain matching with support for multiple labels
// Matches: example.com, www.example.com, a.b.c.d.example.net, etc. // Matches: example.com, www.example.com, a.b.c.d.example.net, etc.
return Promise.resolve( 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.test( /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/i;
value
) 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)], ["registrar", () => Promise.resolve(false)],