mirror of
https://github.com/Xevion/rdap.git
synced 2025-12-06 15:16:07 -06:00
build: migrate to pnpm and add vitest testing infrastructure
- Replace Yarn with pnpm as package manager - Add Vitest with React Testing Library and happy-dom - Configure test setup and add test scripts (test, test:ui, test:run) - Add IPCard component and update lookup components - Remove unused react-ogp dependency - Add type-check script
This commit is contained in:
@@ -1 +0,0 @@
|
||||
nodeLinker: node-modules
|
||||
16
package.json
16
package.json
@@ -6,7 +6,11 @@
|
||||
"build": "next build",
|
||||
"dev": "next dev",
|
||||
"lint": "next lint",
|
||||
"start": "next start"
|
||||
"start": "next start",
|
||||
"test": "vitest",
|
||||
"test:ui": "vitest --ui",
|
||||
"test:run": "vitest run",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^2.0.3",
|
||||
@@ -19,7 +23,6 @@
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hook-form": "^7.42.1",
|
||||
"react-ogp": "^0.0.3",
|
||||
"react-timeago": "^7.2.0",
|
||||
"sass": "^1.57.1",
|
||||
"true-myth": "^7.1.0",
|
||||
@@ -27,6 +30,8 @@
|
||||
"zod": "^3.20.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/prettier": "^2.7.2",
|
||||
"@types/react": "^18.0.26",
|
||||
@@ -34,18 +39,21 @@
|
||||
"@types/react-timeago": "^4.1.7",
|
||||
"@typescript-eslint/eslint-plugin": "^5.47.1",
|
||||
"@typescript-eslint/parser": "^5.47.1",
|
||||
"@vitest/ui": "^3.2.4",
|
||||
"autoprefixer": "^10.4.7",
|
||||
"eslint": "^8.30.0",
|
||||
"eslint-config-next": "13.1.1",
|
||||
"happy-dom": "^20.0.8",
|
||||
"postcss": "^8.4.14",
|
||||
"prettier": "^2.8.1",
|
||||
"prettier-plugin-tailwindcss": "^0.2.1",
|
||||
"tailwindcss": "^3.2.0",
|
||||
"type-fest": "^4.18.2",
|
||||
"typescript": "^4.9.4"
|
||||
"typescript": "^4.9.4",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"ct3aMetadata": {
|
||||
"initVersion": "7.2.0"
|
||||
},
|
||||
"packageManager": "yarn@4.2.2"
|
||||
"packageManager": "pnpm@9.0.0"
|
||||
}
|
||||
|
||||
5517
pnpm-lock.yaml
generated
Normal file
5517
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
import type { FunctionComponent } from "react";
|
||||
import DomainCard from "@/components/lookup/DomainCard";
|
||||
import IPCard from "@/components/lookup/IPCard";
|
||||
import type {
|
||||
Domain,
|
||||
AutonomousNumber,
|
||||
@@ -25,9 +26,10 @@ 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":
|
||||
case "entity":
|
||||
case "ip network":
|
||||
case "nameserver":
|
||||
default:
|
||||
return (
|
||||
|
||||
61
src/components/lookup/IPCard.tsx
Normal file
61
src/components/lookup/IPCard.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { FunctionComponent } from "react";
|
||||
import React from "react";
|
||||
import type { IpNetwork } 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 IPCardProps = {
|
||||
data: IpNetwork;
|
||||
url?: string;
|
||||
};
|
||||
|
||||
const IPCard: FunctionComponent<IPCardProps> = ({
|
||||
data,
|
||||
url,
|
||||
}: IPCardProps) => {
|
||||
return (
|
||||
<AbstractCard
|
||||
data={data}
|
||||
url={url}
|
||||
header={
|
||||
<>
|
||||
<span className="font-mono tracking-tighter">IP NETWORK</span>
|
||||
<span className="font-mono tracking-wide">
|
||||
{data.startAddress}
|
||||
{data.startAddress !== data.endAddress && ` - ${data.endAddress}`}
|
||||
</span>
|
||||
<span className="whitespace-nowrap">({data.handle})</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<dl>
|
||||
<Property title="Name">{data.name}</Property>
|
||||
<Property title="Handle">{data.handle}</Property>
|
||||
<Property title="IP Version">{data.ipVersion.toUpperCase()}</Property>
|
||||
<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.parentHandle && (
|
||||
<Property title="Parent Handle">{data.parentHandle}</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 IPCard;
|
||||
@@ -40,3 +40,92 @@ export function truncated(input: string, maxLength: number, ellipsis = "...") {
|
||||
export function preventDefault(event: SyntheticEvent | Event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an IPv4 address string to a 32-bit integer
|
||||
*/
|
||||
function ipv4ToInt(ip: string): number {
|
||||
const parts = ip.split('.').map(Number);
|
||||
if (parts.length !== 4) return 0;
|
||||
const [a, b, c, d] = parts;
|
||||
if (a === undefined || b === undefined || c === undefined || d === undefined) return 0;
|
||||
return ((a << 24) | (b << 16) | (c << 8) | d) >>> 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an IPv4 address falls within a CIDR range
|
||||
* @param ip The IP address to check (e.g., "192.168.1.1")
|
||||
* @param cidr The CIDR range to check against (e.g., "192.168.0.0/16")
|
||||
* @returns true if the IP is within the CIDR range
|
||||
*/
|
||||
export function ipv4InCIDR(ip: string, cidr: string): boolean {
|
||||
const [rangeIp, prefixLenStr] = cidr.split('/');
|
||||
const prefixLen = parseInt(prefixLenStr ?? '', 10);
|
||||
|
||||
if (!rangeIp || isNaN(prefixLen) || prefixLen < 0 || prefixLen > 32) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const ipInt = ipv4ToInt(ip);
|
||||
const rangeInt = ipv4ToInt(rangeIp);
|
||||
const mask = (0xFFFFFFFF << (32 - prefixLen)) >>> 0;
|
||||
|
||||
return (ipInt & mask) === (rangeInt & mask);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an IPv6 address to a BigInt representation
|
||||
*/
|
||||
function ipv6ToBigInt(ip: string): bigint {
|
||||
// Expand :: notation
|
||||
const expandedIp = expandIPv6(ip);
|
||||
const parts = expandedIp.split(':');
|
||||
|
||||
let result = BigInt(0);
|
||||
for (const part of parts) {
|
||||
result = (result << BigInt(16)) | BigInt(parseInt(part, 16));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand IPv6 address shorthand notation
|
||||
*/
|
||||
function expandIPv6(ip: string): string {
|
||||
if (ip.includes('::')) {
|
||||
const [left, right] = ip.split('::');
|
||||
const leftParts = left ? left.split(':') : [];
|
||||
const rightParts = right ? right.split(':') : [];
|
||||
const missingParts = 8 - leftParts.length - rightParts.length;
|
||||
const middleParts: string[] = Array(missingParts).fill('0') as string[];
|
||||
const allParts = [...leftParts, ...middleParts, ...rightParts];
|
||||
return allParts.map((p: string) => p.padStart(4, '0')).join(':');
|
||||
}
|
||||
return ip.split(':').map((p: string) => p.padStart(4, '0')).join(':');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an IPv6 address falls within a CIDR range
|
||||
* @param ip The IPv6 address to check (e.g., "2001:db8::1")
|
||||
* @param cidr The CIDR range to check against (e.g., "2001:db8::/32")
|
||||
* @returns true if the IP is within the CIDR range
|
||||
*/
|
||||
export function ipv6InCIDR(ip: string, cidr: string): boolean {
|
||||
const [rangeIp, prefixLenStr] = cidr.split('/');
|
||||
const prefixLen = parseInt(prefixLenStr ?? '', 10);
|
||||
|
||||
if (!rangeIp || isNaN(prefixLen) || prefixLen < 0 || prefixLen > 128) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const ipInt = ipv6ToBigInt(ip);
|
||||
const rangeInt = ipv6ToBigInt(rangeIp);
|
||||
const maxMask = BigInt('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF');
|
||||
const mask = (maxMask << BigInt(128 - prefixLen)) & maxMask;
|
||||
|
||||
return (ipInt & mask) === (rangeInt & mask);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
RegisterSchema,
|
||||
RootRegistryEnum,
|
||||
} from "@/schema";
|
||||
import { truncated } from "@/helpers";
|
||||
import { truncated, ipv4InCIDR, ipv6InCIDR } from "@/helpers";
|
||||
import type { ZodSchema } from "zod";
|
||||
import type { ParsedGeneric } from "@/components/lookup/Generic";
|
||||
import { Maybe, Result } from "true-myth";
|
||||
@@ -118,10 +118,30 @@ const useLookup = (warningHandler?: WarningHandler) => {
|
||||
}
|
||||
}
|
||||
throw new Error(`No matching domain found.`);
|
||||
case "ip4":
|
||||
throw new Error(`No matching ip4 found.`);
|
||||
case "ip6":
|
||||
throw new Error(`No matching ip6 found.`);
|
||||
case "ip4": {
|
||||
// Extract the IP address without CIDR suffix for matching
|
||||
const ipAddress = lookupTarget.split('/')[0] ?? lookupTarget;
|
||||
for (const bootstrapItem of bootstrap.services) {
|
||||
// bootstrapItem[0] contains CIDR ranges like ["1.0.0.0/8", "2.0.0.0/8"]
|
||||
if (bootstrapItem[0].some((cidr) => ipv4InCIDR(ipAddress, cidr))) {
|
||||
url = getBestURL(bootstrapItem[1] as [string, ...string[]]);
|
||||
break typeSwitch;
|
||||
}
|
||||
}
|
||||
throw new Error(`No matching IPv4 registry found for ${lookupTarget}.`);
|
||||
}
|
||||
case "ip6": {
|
||||
// Extract the IP address without CIDR suffix for matching
|
||||
const ipAddress = lookupTarget.split('/')[0] ?? lookupTarget;
|
||||
for (const bootstrapItem of bootstrap.services) {
|
||||
// bootstrapItem[0] contains CIDR ranges like ["2001:0200::/23", "2001:0400::/23"]
|
||||
if (bootstrapItem[0].some((cidr) => ipv6InCIDR(ipAddress, cidr))) {
|
||||
url = getBestURL(bootstrapItem[1] as [string, ...string[]]);
|
||||
break typeSwitch;
|
||||
}
|
||||
}
|
||||
throw new Error(`No matching IPv6 registry found for ${lookupTarget}.`);
|
||||
}
|
||||
case "entity":
|
||||
throw new Error(`No matching entity found.`);
|
||||
case "autnum":
|
||||
@@ -132,7 +152,11 @@ const useLookup = (warningHandler?: WarningHandler) => {
|
||||
|
||||
if (url == null) throw new Error("No lookup target was resolved.");
|
||||
|
||||
return `${url}${type}/${lookupTarget}`;
|
||||
// Map internal types to RDAP endpoint paths
|
||||
// ip4 and ip6 both use the 'ip' endpoint in RDAP
|
||||
const rdapPath = type === "ip4" || type === "ip6" ? "ip" : type;
|
||||
|
||||
return `${url}${rdapPath}/${lookupTarget}`;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useState } from "react";
|
||||
import Generic from "@/components/lookup/Generic";
|
||||
import type { MetaParsedGeneric } from "@/hooks/useLookup";
|
||||
import useLookup from "@/hooks/useLookup";
|
||||
import { OGP } from "react-ogp";
|
||||
import LookupInput from "@/components/form/LookupInput";
|
||||
import ErrorCard from "@/components/common/ErrorCard";
|
||||
import { Maybe } from "true-myth";
|
||||
@@ -22,13 +21,12 @@ const Index: NextPage = () => {
|
||||
<>
|
||||
<Head>
|
||||
<title>rdap.xevion.dev</title>
|
||||
<OGP
|
||||
url="https://rdap.xevion.dev"
|
||||
title="RDAP | by Xevion.dev"
|
||||
description="A custom, private RDAP lookup client built by Xevion."
|
||||
siteName="rdap.xevion.dev"
|
||||
type="website"
|
||||
/>
|
||||
<meta name="description" content="A custom, private RDAP lookup client built by Xevion." />
|
||||
<meta property="og:url" content="https://rdap.xevion.dev" />
|
||||
<meta property="og:title" content="RDAP | by Xevion.dev" />
|
||||
<meta property="og:description" content="A custom, private RDAP lookup client built by Xevion." />
|
||||
<meta property="og:site_name" content="rdap.xevion.dev" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta
|
||||
name="keywords"
|
||||
|
||||
@@ -107,12 +107,12 @@ export const IpNetworkSchema = z.object({
|
||||
ipVersion: z.enum(["v4", "v6"]),
|
||||
name: z.string(),
|
||||
type: z.string(),
|
||||
country: z.string(),
|
||||
parentHandle: z.string(),
|
||||
country: z.string().optional(),
|
||||
parentHandle: z.string().optional(),
|
||||
status: z.string().array(),
|
||||
entities: z.array(EntitySchema),
|
||||
remarks: z.any(),
|
||||
links: z.any(),
|
||||
entities: z.array(EntitySchema).optional(),
|
||||
remarks: z.any().optional(),
|
||||
links: z.any().optional(),
|
||||
port43: z.any().optional(),
|
||||
events: z.array(EventSchema),
|
||||
});
|
||||
|
||||
1
src/test/setup.ts
Normal file
1
src/test/setup.ts
Normal file
@@ -0,0 +1 @@
|
||||
import '@testing-library/jest-dom';
|
||||
15
vitest.config.ts
Normal file
15
vitest.config.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import path from 'path';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'happy-dom',
|
||||
setupFiles: ['./src/test/setup.ts'],
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user