mirror of
https://github.com/Xevion/rdap.git
synced 2026-01-31 04:25:26 -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
|
|
||||||
+12
-4
@@ -6,7 +6,11 @@
|
|||||||
"build": "next build",
|
"build": "next build",
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"start": "next start"
|
"start": "next start",
|
||||||
|
"test": "vitest",
|
||||||
|
"test:ui": "vitest --ui",
|
||||||
|
"test:run": "vitest run",
|
||||||
|
"type-check": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^2.0.3",
|
"@headlessui/react": "^2.0.3",
|
||||||
@@ -19,7 +23,6 @@
|
|||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-hook-form": "^7.42.1",
|
"react-hook-form": "^7.42.1",
|
||||||
"react-ogp": "^0.0.3",
|
|
||||||
"react-timeago": "^7.2.0",
|
"react-timeago": "^7.2.0",
|
||||||
"sass": "^1.57.1",
|
"sass": "^1.57.1",
|
||||||
"true-myth": "^7.1.0",
|
"true-myth": "^7.1.0",
|
||||||
@@ -27,6 +30,8 @@
|
|||||||
"zod": "^3.20.2"
|
"zod": "^3.20.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
|
"@testing-library/react": "^16.3.0",
|
||||||
"@types/node": "^18.11.18",
|
"@types/node": "^18.11.18",
|
||||||
"@types/prettier": "^2.7.2",
|
"@types/prettier": "^2.7.2",
|
||||||
"@types/react": "^18.0.26",
|
"@types/react": "^18.0.26",
|
||||||
@@ -34,18 +39,21 @@
|
|||||||
"@types/react-timeago": "^4.1.7",
|
"@types/react-timeago": "^4.1.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.47.1",
|
"@typescript-eslint/eslint-plugin": "^5.47.1",
|
||||||
"@typescript-eslint/parser": "^5.47.1",
|
"@typescript-eslint/parser": "^5.47.1",
|
||||||
|
"@vitest/ui": "^3.2.4",
|
||||||
"autoprefixer": "^10.4.7",
|
"autoprefixer": "^10.4.7",
|
||||||
"eslint": "^8.30.0",
|
"eslint": "^8.30.0",
|
||||||
"eslint-config-next": "13.1.1",
|
"eslint-config-next": "13.1.1",
|
||||||
|
"happy-dom": "^20.0.8",
|
||||||
"postcss": "^8.4.14",
|
"postcss": "^8.4.14",
|
||||||
"prettier": "^2.8.1",
|
"prettier": "^2.8.1",
|
||||||
"prettier-plugin-tailwindcss": "^0.2.1",
|
"prettier-plugin-tailwindcss": "^0.2.1",
|
||||||
"tailwindcss": "^3.2.0",
|
"tailwindcss": "^3.2.0",
|
||||||
"type-fest": "^4.18.2",
|
"type-fest": "^4.18.2",
|
||||||
"typescript": "^4.9.4"
|
"typescript": "^4.9.4",
|
||||||
|
"vitest": "^3.2.4"
|
||||||
},
|
},
|
||||||
"ct3aMetadata": {
|
"ct3aMetadata": {
|
||||||
"initVersion": "7.2.0"
|
"initVersion": "7.2.0"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.2.2"
|
"packageManager": "pnpm@9.0.0"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+5517
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
|||||||
import type { FunctionComponent } from "react";
|
import type { FunctionComponent } from "react";
|
||||||
import DomainCard from "@/components/lookup/DomainCard";
|
import DomainCard from "@/components/lookup/DomainCard";
|
||||||
|
import IPCard from "@/components/lookup/IPCard";
|
||||||
import type {
|
import type {
|
||||||
Domain,
|
Domain,
|
||||||
AutonomousNumber,
|
AutonomousNumber,
|
||||||
@@ -25,9 +26,10 @@ const Generic: FunctionComponent<ObjectProps> = ({ data, url }: ObjectProps) =>
|
|||||||
switch (data.objectClassName) {
|
switch (data.objectClassName) {
|
||||||
case "domain":
|
case "domain":
|
||||||
return <DomainCard url={url} data={data} />;
|
return <DomainCard url={url} data={data} />;
|
||||||
|
case "ip network":
|
||||||
|
return <IPCard url={url} data={data} />;
|
||||||
case "autnum":
|
case "autnum":
|
||||||
case "entity":
|
case "entity":
|
||||||
case "ip network":
|
|
||||||
case "nameserver":
|
case "nameserver":
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -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) {
|
export function preventDefault(event: SyntheticEvent | Event) {
|
||||||
event.preventDefault();
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+30
-6
@@ -17,7 +17,7 @@ import {
|
|||||||
RegisterSchema,
|
RegisterSchema,
|
||||||
RootRegistryEnum,
|
RootRegistryEnum,
|
||||||
} from "@/schema";
|
} from "@/schema";
|
||||||
import { truncated } from "@/helpers";
|
import { truncated, ipv4InCIDR, ipv6InCIDR } from "@/helpers";
|
||||||
import type { ZodSchema } from "zod";
|
import type { ZodSchema } from "zod";
|
||||||
import type { ParsedGeneric } from "@/components/lookup/Generic";
|
import type { ParsedGeneric } from "@/components/lookup/Generic";
|
||||||
import { Maybe, Result } from "true-myth";
|
import { Maybe, Result } from "true-myth";
|
||||||
@@ -118,10 +118,30 @@ const useLookup = (warningHandler?: WarningHandler) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new Error(`No matching domain found.`);
|
throw new Error(`No matching domain found.`);
|
||||||
case "ip4":
|
case "ip4": {
|
||||||
throw new Error(`No matching ip4 found.`);
|
// Extract the IP address without CIDR suffix for matching
|
||||||
case "ip6":
|
const ipAddress = lookupTarget.split('/')[0] ?? lookupTarget;
|
||||||
throw new Error(`No matching ip6 found.`);
|
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":
|
case "entity":
|
||||||
throw new Error(`No matching entity found.`);
|
throw new Error(`No matching entity found.`);
|
||||||
case "autnum":
|
case "autnum":
|
||||||
@@ -132,7 +152,11 @@ const useLookup = (warningHandler?: WarningHandler) => {
|
|||||||
|
|
||||||
if (url == null) throw new Error("No lookup target was resolved.");
|
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(() => {
|
useEffect(() => {
|
||||||
|
|||||||
+6
-8
@@ -4,7 +4,6 @@ import { useState } from "react";
|
|||||||
import Generic from "@/components/lookup/Generic";
|
import Generic from "@/components/lookup/Generic";
|
||||||
import type { MetaParsedGeneric } from "@/hooks/useLookup";
|
import type { MetaParsedGeneric } from "@/hooks/useLookup";
|
||||||
import useLookup from "@/hooks/useLookup";
|
import useLookup from "@/hooks/useLookup";
|
||||||
import { OGP } from "react-ogp";
|
|
||||||
import LookupInput from "@/components/form/LookupInput";
|
import LookupInput from "@/components/form/LookupInput";
|
||||||
import ErrorCard from "@/components/common/ErrorCard";
|
import ErrorCard from "@/components/common/ErrorCard";
|
||||||
import { Maybe } from "true-myth";
|
import { Maybe } from "true-myth";
|
||||||
@@ -22,13 +21,12 @@ const Index: NextPage = () => {
|
|||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>rdap.xevion.dev</title>
|
<title>rdap.xevion.dev</title>
|
||||||
<OGP
|
<meta name="description" content="A custom, private RDAP lookup client built by Xevion." />
|
||||||
url="https://rdap.xevion.dev"
|
<meta property="og:url" content="https://rdap.xevion.dev" />
|
||||||
title="RDAP | by Xevion.dev"
|
<meta property="og:title" content="RDAP | by Xevion.dev" />
|
||||||
description="A custom, private RDAP lookup client built by Xevion."
|
<meta property="og:description" content="A custom, private RDAP lookup client built by Xevion." />
|
||||||
siteName="rdap.xevion.dev"
|
<meta property="og:site_name" content="rdap.xevion.dev" />
|
||||||
type="website"
|
<meta property="og:type" content="website" />
|
||||||
/>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta
|
<meta
|
||||||
name="keywords"
|
name="keywords"
|
||||||
|
|||||||
+5
-5
@@ -107,12 +107,12 @@ export const IpNetworkSchema = z.object({
|
|||||||
ipVersion: z.enum(["v4", "v6"]),
|
ipVersion: z.enum(["v4", "v6"]),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
type: z.string(),
|
type: z.string(),
|
||||||
country: z.string(),
|
country: z.string().optional(),
|
||||||
parentHandle: z.string(),
|
parentHandle: z.string().optional(),
|
||||||
status: z.string().array(),
|
status: z.string().array(),
|
||||||
entities: z.array(EntitySchema),
|
entities: z.array(EntitySchema).optional(),
|
||||||
remarks: z.any(),
|
remarks: z.any().optional(),
|
||||||
links: z.any(),
|
links: z.any().optional(),
|
||||||
port43: z.any().optional(),
|
port43: z.any().optional(),
|
||||||
events: z.array(EventSchema),
|
events: z.array(EventSchema),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
import '@testing-library/jest-dom';
|
||||||
@@ -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