188 lines
5.3 KiB
JavaScript
188 lines
5.3 KiB
JavaScript
import { readFile } from 'node:fs/promises'
|
|
import path from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import test from 'brittle'
|
|
import b4a from 'b4a'
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor
|
|
|
|
async function loadWhoisBin() {
|
|
const runtime = await readFile(path.join(__dirname, '../lib/runtime.js'), 'utf8')
|
|
const body = await readFile(path.join(__dirname, '../src/whois.js'), 'utf8')
|
|
return new AsyncFunction(
|
|
'ctx',
|
|
'argv',
|
|
`${runtime}\n${body}\nif (typeof run === 'function') return await run(ctx, argv)\n`
|
|
)
|
|
}
|
|
|
|
function createCtx() {
|
|
const logs = []
|
|
const errs = []
|
|
return {
|
|
b4a,
|
|
exitCode: 0,
|
|
vfs: { env: {} },
|
|
console: {
|
|
log: (m) => logs.push(String(m)),
|
|
error: (m) => errs.push(String(m))
|
|
},
|
|
_logs: logs,
|
|
_errs: errs
|
|
}
|
|
}
|
|
|
|
function responseJson(body, status = 200) {
|
|
return new Response(JSON.stringify(body), {
|
|
status,
|
|
headers: { 'Content-Type': 'application/rdap+json' }
|
|
})
|
|
}
|
|
|
|
async function withPatchedFetch(stub, fn) {
|
|
const oldFetch = globalThis.fetch
|
|
globalThis.fetch = stub
|
|
try {
|
|
return await fn()
|
|
} finally {
|
|
globalThis.fetch = oldFetch
|
|
}
|
|
}
|
|
|
|
test('/bin/whois domain lookup prints summary', async (t) => {
|
|
const run = await loadWhoisBin()
|
|
const ctx = createCtx()
|
|
const calls = []
|
|
await withPatchedFetch(async (url) => {
|
|
calls.push(String(url))
|
|
if (String(url).includes('/rdap/dns.json')) {
|
|
return responseJson({
|
|
services: [[['com'], ['https://rdap.test/']]]
|
|
})
|
|
}
|
|
return responseJson({
|
|
objectClassName: 'domain',
|
|
ldhName: 'example.com',
|
|
handle: 'EXAMPLE1',
|
|
status: ['active'],
|
|
nameservers: [{ ldhName: 'ns1.example.com' }],
|
|
entities: [
|
|
{
|
|
roles: ['registrar'],
|
|
vcardArray: ['vcard', [['fn', {}, 'text', 'Test Registrar']]]
|
|
}
|
|
]
|
|
})
|
|
}, async () => {
|
|
await run(ctx, ['whois', 'example.com'])
|
|
})
|
|
t.is(ctx.exitCode, 0)
|
|
t.alike(calls, [
|
|
'https://data.iana.org/rdap/dns.json',
|
|
'https://rdap.test/domain/example.com'
|
|
])
|
|
t.ok(ctx._logs.join('\n').includes('Registrar: Test Registrar'))
|
|
t.ok(ctx._logs.join('\n').includes('Nameservers: ns1.example.com'))
|
|
})
|
|
|
|
test('/bin/whois IP lookup resolves via ipv4 bootstrap', async (t) => {
|
|
const run = await loadWhoisBin()
|
|
const ctx = createCtx()
|
|
await withPatchedFetch(async (url) => {
|
|
if (String(url).includes('/rdap/ipv4.json')) {
|
|
return responseJson({
|
|
services: [[['203.0.113.0/24'], ['https://arin.test/rdap']]]
|
|
})
|
|
}
|
|
return responseJson({
|
|
objectClassName: 'ip network',
|
|
startAddress: '203.0.113.0',
|
|
endAddress: '203.0.113.255',
|
|
ipVersion: 'v4',
|
|
handle: 'NET-203-0-113-0-1'
|
|
})
|
|
}, async () => {
|
|
await run(ctx, ['whois', '203.0.113.10'])
|
|
})
|
|
t.is(ctx.exitCode, 0)
|
|
t.ok(ctx._logs.join('\n').includes('IP Version: v4'))
|
|
t.ok(ctx._logs.join('\n').includes('Range: 203.0.113.0 - 203.0.113.255'))
|
|
})
|
|
|
|
test('/bin/whois ASN lookup resolves autnum and supports --json', async (t) => {
|
|
const run = await loadWhoisBin()
|
|
const ctx = createCtx()
|
|
await withPatchedFetch(async (url) => {
|
|
if (String(url).includes('/rdap/asn.json')) {
|
|
return responseJson({
|
|
services: [[['13335-13335'], ['https://asn.test/']]]
|
|
})
|
|
}
|
|
return responseJson({
|
|
objectClassName: 'autnum',
|
|
handle: 'AS13335',
|
|
startAutnum: 13335,
|
|
endAutnum: 13335,
|
|
name: 'CLOUDFLARENET'
|
|
})
|
|
}, async () => {
|
|
await run(ctx, ['whois', '--json', 'AS13335'])
|
|
})
|
|
t.is(ctx.exitCode, 0)
|
|
t.ok(ctx._logs.join('\n').includes('"objectClassName": "autnum"'))
|
|
t.absent(ctx._logs.join('\n').includes('ASN Range:'))
|
|
})
|
|
|
|
test('/bin/whois exits non-zero when no bootstrap service matches', async (t) => {
|
|
const run = await loadWhoisBin()
|
|
const ctx = createCtx()
|
|
await withPatchedFetch(async (url) => {
|
|
if (String(url).includes('/rdap/dns.json')) {
|
|
return responseJson({
|
|
services: [[['net'], ['https://rdap.test/']]]
|
|
})
|
|
}
|
|
return responseJson({})
|
|
}, async () => {
|
|
await run(ctx, ['whois', 'example.com'])
|
|
})
|
|
t.is(ctx.exitCode, 1)
|
|
t.ok(ctx._errs.join('\n').includes('no RDAP service found'))
|
|
})
|
|
|
|
test('/bin/whois handles upstream HTTP failures', async (t) => {
|
|
const run = await loadWhoisBin()
|
|
const ctx = createCtx()
|
|
await withPatchedFetch(async (url) => {
|
|
if (String(url).includes('/rdap/dns.json')) {
|
|
return responseJson({
|
|
services: [[['com'], ['https://rdap.test/']]]
|
|
})
|
|
}
|
|
return responseJson({ errorCode: 404 }, 404)
|
|
}, async () => {
|
|
await run(ctx, ['whois', 'example.com'])
|
|
})
|
|
t.is(ctx.exitCode, 1)
|
|
t.ok(ctx._errs.join('\n').includes('HTTP error'))
|
|
})
|
|
|
|
test('/bin/whois validates arguments and help output', async (t) => {
|
|
const run = await loadWhoisBin()
|
|
const helpCtx = createCtx()
|
|
await run(helpCtx, ['whois', '--help'])
|
|
t.is(helpCtx.exitCode, 0)
|
|
t.ok(helpCtx._logs.join('\n').includes('usage: whois'))
|
|
|
|
const badCtx = createCtx()
|
|
await run(badCtx, ['whois', '-Z'])
|
|
t.is(badCtx.exitCode, 1)
|
|
t.ok(badCtx._errs.join('\n').includes('unknown option'))
|
|
|
|
const missingCtx = createCtx()
|
|
await run(missingCtx, ['whois'])
|
|
t.is(missingCtx.exitCode, 1)
|
|
t.ok(missingCtx._errs.join('\n').includes('expected exactly one target'))
|
|
})
|