test: add comprehensive testing infrastructure with critical bug fixes

- Add Vitest testing framework with 88 passing tests across 4 test files
- Fix critical entity validator bug (service array index)
- Implement validator architecture with 'matched but invalid' state support
- Add strict IPv4/IPv6 validation with detailed error messages
- Add case-insensitive domain and ASN matching
- Add explicit validator priority ordering (url→json→tld→ip→domain)
- Add integration tests with real IANA registry validation
- Add AutnumCard component for AS number display
- Update dependencies: prettier 2.8.1→2.8.8

Test coverage:
- helpers.test.ts: IPv4/IPv6 CIDR matching (27 tests)
- helpers.asn.test.ts: ASN range validation (22 tests)
- rdap.test.ts: Type detection with edge cases (36 tests)
- rdap.integration.test.ts: Real IANA registry tests (3 tests)

Bug fixes:
- Entity validator now correctly uses service[1] for tags (0=email, 1=tags, 2=urls)
- IPv4 validation rejects octets >255 with specific error messages
- IPv6 validation checks for invalid hex chars and multiple ::
- Domain regex supports multi-label domains (a.b.c.d.example.net)
- Type detection priority prevents URL/JSON false matches as domains
This commit is contained in:
2025-10-22 01:23:15 -05:00
parent 09cd0bf49b
commit 5fb095a498
27 changed files with 4559 additions and 1745 deletions

View File

@@ -33,7 +33,7 @@ const AbstractCard: FunctionComponent<AbstractCardProps> = ({
{url != undefined ? (
<div className="pr-2">
<a href={url} target="_blank" rel="noreferrer">
<LinkIcon className="h-5 w-5 mt-1 cursor-pointer" />
<LinkIcon className="mt-1 h-5 w-5 cursor-pointer" />
</a>
</div>
) : null}
@@ -43,14 +43,20 @@ const AbstractCard: FunctionComponent<AbstractCardProps> = ({
<ClipboardDocumentIcon
onClick={() => {
// stringify the JSON object, then begin the async clipboard write
navigator.clipboard.writeText(JSON.stringify(data, null, 4)).then(() => {
console.log('Copied to clipboard.');
}, (err) => {
if (err instanceof Error)
console.error(`Failed to copy to clipboard (${err.toString()}).`);
else
console.error("Failed to copy to clipboard.");
});
navigator.clipboard
.writeText(JSON.stringify(data, null, 4))
.then(
() => {
console.log("Copied to clipboard.");
},
(err) => {
if (err instanceof Error)
console.error(
`Failed to copy to clipboard (${err.toString()}).`
);
else console.error("Failed to copy to clipboard.");
}
);
}}
className="h-6 w-6 cursor-pointer"
/>
@@ -80,7 +86,7 @@ const AbstractCard: FunctionComponent<AbstractCardProps> = ({
) : null}
</div>
) : null}
<div className="max-w-full p-2 px-4 overflow-x-auto">
<div className="max-w-full overflow-x-auto p-2 px-4">
{showRaw ? (
<pre className="scrollbar-thin m-2 max-h-[40rem] max-w-full overflow-y-auto whitespace-pre-wrap rounded">
{JSON.stringify(data, null, 4)}

View File

@@ -23,9 +23,11 @@ const DynamicDate: FunctionComponent<DynamicDateProps> = ({
return (
<button onClick={toggleFormat}>
<span className="dashed" title={date.toISOString()}>
{showAbsolute
? format(date, absoluteFormat ?? "LLL do, y HH:mm:ss xxx")
: <TimeAgo date={date} />}
{showAbsolute ? (
format(date, absoluteFormat ?? "LLL do, y HH:mm:ss xxx")
) : (
<TimeAgo date={date} />
)}
</span>
</button>
);

View File

@@ -16,15 +16,22 @@ const ErrorCard: FunctionComponent<ErrorCardProps> = ({
className,
}) => {
return (
<div className={clsx(className, "rounded-md border border-red-700/30 bg-zinc-800 pt-3 px-3 pb-1")}>
<div
className={clsx(
className,
"rounded-md border border-red-700/30 bg-zinc-800 px-3 pb-1 pt-3"
)}
>
<div className="flex">
<div className="flex-shrink-0">
<XCircleIcon className="h-5 w-5 text-red-300" aria-hidden="true" />
</div>
<div className="ml-3 text-sm text-red-300 w-full">
<div className="ml-3 w-full text-sm text-red-300">
<h3 className="font-medium text-red-200">{title}</h3>
{description != undefined ? (
<div className="mt-2 whitespace-pre-wrap max-h-24 overflow-y-auto w-full" >{description}</div>
<div className="mt-2 max-h-24 w-full overflow-y-auto whitespace-pre-wrap">
{description}
</div>
) : null}
<div className="mt-2">
{issues != undefined ? (

View File

@@ -18,7 +18,7 @@ const Property: FunctionComponent<PropertyProps> = ({
return (
<>
<dt className={clsx("font-medium", titleClass)}>{title}:</dt>
<dd className={clsx("mt-2 mb-2 ml-6", valueClass)}>{children}</dd>
<dd className={clsx("mb-2 ml-6 mt-2", valueClass)}>{children}</dd>
</>
);
};

View File

@@ -8,7 +8,9 @@ const PropertyListItem: FunctionComponent<{
}> = ({ title, children }) => {
return (
<li>
<span className="dashed" title={title}>{children}</span>
<span className="dashed" title={title}>
{children}
</span>
</li>
);
};

View File

@@ -193,14 +193,20 @@ const LookupInput: FunctionComponent<LookupInputProps> = ({
{selected == "auto" ? (
// If the detected type was provided, then notate which in parentheses. Compact object naming might be better in the future.
detectedType.isJust ? (
<>Auto (<span className="animate-pulse">{targetShortNames[detectedType.value]}</span>)</>
<>
Auto (
<span className="animate-pulse">
{targetShortNames[detectedType.value]}
</span>
)
</>
) : (
objectNames["auto"]
)
) : (
<>
<LockClosedIcon
className="mr-2.5 mb-1 inline h-4 w-4 animate-pulse text-zinc-500"
className="mb-1 mr-2.5 inline h-4 w-4 animate-pulse text-zinc-500"
aria-hidden
/>
{objectNames[selected]}
@@ -292,7 +298,7 @@ const LookupInput: FunctionComponent<LookupInputProps> = ({
</div>
</div>
<div className="col">
<div className="flex flex-wrap pt-3 pb-1 text-sm">
<div className="flex flex-wrap pb-1 pt-3 text-sm">
<div className="whitespace-nowrap">
<input
className="ml-2 mr-1 whitespace-nowrap text-zinc-800 accent-blue-700"

View File

@@ -0,0 +1,60 @@
import type { FunctionComponent } from "react";
import React from "react";
import type { AutonomousNumber } from "@/types";
import Events from "@/components/lookup/Events";
import Property from "@/components/common/Property";
import PropertyList from "@/components/common/PropertyList";
import AbstractCard from "@/components/common/AbstractCard";
export type AutnumCardProps = {
data: AutonomousNumber;
url?: string;
};
const AutnumCard: FunctionComponent<AutnumCardProps> = ({
data,
url,
}: AutnumCardProps) => {
const asnRange =
data.startAutnum === data.endAutnum
? `AS${data.startAutnum}`
: `AS${data.startAutnum}-AS${data.endAutnum}`;
return (
<AbstractCard
data={data}
url={url}
header={
<>
<span className="font-mono tracking-tighter">AUTONOMOUS SYSTEM</span>
<span className="font-mono tracking-wide">{asnRange}</span>
<span className="whitespace-nowrap">({data.handle})</span>
</>
}
>
<dl>
<Property title="Name">{data.name}</Property>
<Property title="Handle">{data.handle}</Property>
<Property title="ASN Range">
{data.startAutnum === data.endAutnum
? `AS${data.startAutnum}`
: `AS${data.startAutnum} - AS${data.endAutnum}`}
</Property>
<Property title="Type">{data.type}</Property>
<Property title="Country">{data.country.toUpperCase()}</Property>
<Property title="Events">
<Events key={0} data={data.events} />
</Property>
<PropertyList title="Status">
{data.status.map((status, index) => (
<PropertyList.Item key={index} title={status}>
{status}
</PropertyList.Item>
))}
</PropertyList>
</dl>
</AbstractCard>
);
};
export default AutnumCard;

View File

@@ -23,7 +23,9 @@ const DomainCard: FunctionComponent<DomainProps> = ({
header={
<>
<span className="font-mono tracking-tighter">DOMAIN</span>
<span className="font-mono tracking-wide">{data.ldhName ?? data.unicodeName}</span>
<span className="font-mono tracking-wide">
{data.ldhName ?? data.unicodeName}
</span>
<span className="whitespace-nowrap">({data.handle})</span>
</>
}

View File

@@ -1,6 +1,7 @@
import type { FunctionComponent } from "react";
import DomainCard from "@/components/lookup/DomainCard";
import IPCard from "@/components/lookup/IPCard";
import AutnumCard from "@/components/lookup/AutnumCard";
import type {
Domain,
AutonomousNumber,
@@ -22,13 +23,17 @@ export type ObjectProps = {
url?: string;
};
const Generic: FunctionComponent<ObjectProps> = ({ data, url }: ObjectProps) => {
const Generic: FunctionComponent<ObjectProps> = ({
data,
url,
}: ObjectProps) => {
switch (data.objectClassName) {
case "domain":
return <DomainCard url={url} data={data} />;
case "ip network":
return <IPCard url={url} data={data} />;
case "autnum":
return <AutnumCard url={url} data={data} />;
case "entity":
case "nameserver":
default:

View File

@@ -11,10 +11,7 @@ export type IPCardProps = {
url?: string;
};
const IPCard: FunctionComponent<IPCardProps> = ({
data,
url,
}: IPCardProps) => {
const IPCard: FunctionComponent<IPCardProps> = ({ data, url }: IPCardProps) => {
return (
<AbstractCard
data={data}
@@ -37,9 +34,7 @@ const IPCard: FunctionComponent<IPCardProps> = ({
<Property title="Start Address">{data.startAddress}</Property>
<Property title="End Address">{data.endAddress}</Property>
<Property title="Type">{data.type}</Property>
{data.country && (
<Property title="Country">{data.country}</Property>
)}
{data.country && <Property title="Country">{data.country}</Property>}
{data.parentHandle && (
<Property title="Parent Handle">{data.parentHandle}</Property>
)}