Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"pretest:all": "run-s build",
"test:all": "cross-env SHOULD_BUILD=false run-s 'test:iso -- -b false' 'test:node -- -b false' test:browsers",
"test:iso": "aegir test -f dist-tsc/test/**/*.spec.js",
"test:dns": "aegir test -t node -f dist-tsc/test/dns-server.spec.js",
"test:browsers": "playwright test -c playwright.config.js --project chromium firefox safari no-service-worker",
"test:chrome": "playwright test -c playwright.config.js --project chromium",
"test:no-sw": "playwright test -c playwright.config.js --project no-service-worker",
Expand Down Expand Up @@ -86,12 +87,14 @@
"@babel/preset-react": "^7.27.1",
"@babel/preset-typescript": "^7.27.1",
"@playwright/test": "^1.53.2",
"@types/node": "^24.8.1",
"@types/react": "^19.1.6",
"@types/react-dom": "^19.1.9",
"@types/wait-on": "^5.3.4",
"aegir": "^47.0.19",
"concurrently": "^9.1.2",
"cross-env": "^7.0.3",
"dns2": "^2.1.0",
"esbuild": "^0.25.6",
"glob": "^11.0.1",
"http-server": "^14.1.1",
Expand Down
200 changes: 200 additions & 0 deletions test/dns-server.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import { expect } from 'aegir/chai'
import dns2 from 'dns2'
import dns from 'dns/promises'
import { platform } from 'os'

const { Packet } = dns2

/**
* Cross-platform test DNS server for DNSLink testing
*/
class TestDNSServer {
private port: number
private server: any
private records: Map<string, { type: number, data: string, ttl: number }>

constructor(port: number = 15353) {
this.port = port
this.server = null
this.records = new Map()
}

addDNSLink(domain: string, dnslink: string, ttl: number = 60): void {
const name = `_dnslink.${domain}`
this.records.set(name, {
type: Packet.TYPE.TXT,
data: `dnslink=${dnslink}`,
ttl
})
}

async start(): Promise<void> {
return await new Promise((resolve, reject) => {
this.server = dns2.createServer({
udp: true,
handle: (request: any, send: any) => {
try {
const response = Packet.createResponseFromRequest(request)
const [question] = request.questions
const record = this.records.get(question.name)

if (record != null) {
response.answers.push({
name: question.name,
type: record.type,
class: Packet.CLASS.IN,
ttl: record.ttl,
data: record.data
})
} else {
response.header.rcode = 3
}

send(response)
} catch (err) {
console.error('Error handling DNS request:', err)
}
}
})

this.server.on('error', (err: Error) => {
reject(err)
})

this.server.listen({
udp: {
port: this.port,
address: '127.0.0.1'
}
})

this.server.on('listening', () => {
console.log(`✅ DNS Server started on 127.0.0.1:${this.port} (${platform()})`)
resolve()
})
})
}

async stop(): Promise<void> {
return await new Promise((resolve) => {
if (this.server != null) {
try {
const timeout = setTimeout(() => {
console.log('🛑 DNS Server stopped (timeout)')
this.server = null
resolve()
}, 2000)

this.server.close(() => {
clearTimeout(timeout)
console.log('🛑 DNS Server stopped')
this.server = null
resolve()
})
} catch (err) {
console.error('Error stopping server:', err)
this.server = null
resolve()
}
} else {
resolve()
}
})
}

clear(): void {
this.records.clear()
}

getPort(): number {
return this.port
}
}

describe('DNS Query Server Testing', function () {
this.timeout(10000)

let dnsServer: TestDNSServer

before(async function () {
console.log(`🧪 Running DNS tests on ${platform()}`)

try {
dnsServer = new TestDNSServer(15353)

dnsServer.addDNSLink(
'example.test',
'/ipfs/bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi',
60
)

dnsServer.addDNSLink(
'docs.ipfs.tech',
'/ipfs/bafybeichmvxgznqta5d2wvlmvpvljmh3nwxk5svnlg2rbobvu3c6mtzzfy',
300
)

await dnsServer.start()
} catch (err: any) {
console.error('⚠️ Failed to start DNS server:', err.message)
console.log('Skipping DNS tests...')
this.skip()
}
})

after(async () => {
if (dnsServer != null) {
await dnsServer.stop()
}
})

it('should resolve DNSLink records', async () => {
const resolver = new dns.Resolver()
resolver.setServers([`127.0.0.1:${dnsServer.getPort()}`])

const records = await resolver.resolveTxt('_dnslink.example.test')

expect(records).to.be.an('array')
expect(records.length).to.be.greaterThan(0)
expect(records[0].join('')).to.include('dnslink=/ipfs/')
})

it('should handle multiple domains', async () => {
const resolver = new dns.Resolver()
resolver.setServers([`127.0.0.1:${dnsServer.getPort()}`])

const records1 = await resolver.resolveTxt('_dnslink.example.test')
const records2 = await resolver.resolveTxt('_dnslink.docs.ipfs.tech')

expect(records1).to.be.an('array')
expect(records2).to.be.an('array')
})

it('should return NXDOMAIN for non-existent domains', async () => {
const resolver = new dns.Resolver()
resolver.setServers([`127.0.0.1:${dnsServer.getPort()}`])

try {
await resolver.resolveTxt('_dnslink.does-not-exist.invalid')
expect.fail('Should have thrown')
} catch (err: any) {
expect(err.code).to.equal('ENOTFOUND')
}
})

it('should allow adding records dynamically', async () => {
dnsServer.addDNSLink('dynamic.test', '/ipfs/QmNewHash123')

const resolver = new dns.Resolver()
resolver.setServers([`127.0.0.1:${dnsServer.getPort()}`])

const records = await resolver.resolveTxt('_dnslink.dynamic.test')

expect(records[0].join('')).to.include('QmNewHash123')
})

it('should work across different platforms', async () => {
expect(['darwin', 'linux', 'win32']).to.include(platform())
expect(dnsServer.getPort()).to.equal(15353)
})
})
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
},
"include": [
"src",
"types/svg.d.ts",
"types/**/*",
"test",
"test-e2e",
"playwright.config.js",
Expand Down
85 changes: 85 additions & 0 deletions types/dns2.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
declare module 'dns2' {
export class Packet {
static TYPE: {
A: number
AAAA: number
CNAME: number
MX: number
NS: number
PTR: number
SOA: number
SRV: number
TXT: number
}

static CLASS: {
IN: number
CS: number
CH: number
HS: number
}

static RCODE: {
NO_ERROR: number
FORMAT_ERROR: number
SERVER_FAILURE: number
NAME_ERROR: number
NOT_IMPLEMENTED: number
REFUSED: number
}

static createResponseFromRequest(request: any): any

header: {
id: number
qr: number
opcode: number
aa: number
tc: number
rd: number
ra: number
rcode: number
qdcount: number
ancount: number
nscount: number
arcount: number
}

questions: Array<{
name: string
type: number
class: number
}>

answers: Array<{
name: string
type: number
class: number
ttl: number
data: string
}>
}

export interface ServerOptions {
udp?: boolean
tcp?: boolean
handle: (request: any, send: (response: any) => void) => void
}

export interface ListenOptions {
udp?: {
port: number
address: string
}
tcp?: {
port: number
address: string
}
}

export function createServer(options: ServerOptions): {
on: (event: string, callback: (...args: any[]) => void) => void
listen: (options: ListenOptions) => void
close: (callback?: () => void) => void
}
}