diff --git a/package-lock.json b/package-lock.json index 538dda65..b7c97fff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,12 +44,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", @@ -7140,12 +7142,12 @@ } }, "node_modules/@types/node": { - "version": "24.0.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.3.tgz", - "integrity": "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg==", + "version": "24.8.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.8.1.tgz", + "integrity": "sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q==", "license": "MIT", "dependencies": { - "undici-types": "~7.8.0" + "undici-types": "~7.14.0" } }, "node_modules/@types/normalize-package-data": { @@ -11374,6 +11376,13 @@ "node": ">=6" } }, + "node_modules/dns2": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/dns2/-/dns2-2.1.0.tgz", + "integrity": "sha512-m27K11aQalRbmUs7RLaz6aPyceLjAoqjPRNTdE7qUouQpl+PC8Bi67O+i9SuJUPbQC8dxFrczAxfmTPuTKHNkw==", + "dev": true, + "license": "MIT" + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -31916,9 +31925,9 @@ } }, "node_modules/undici-types": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", - "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", + "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { diff --git a/package.json b/package.json index ca2087d1..009e4b64 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", diff --git a/test/dns-server.spec.ts b/test/dns-server.spec.ts new file mode 100644 index 00000000..5cd2adb8 --- /dev/null +++ b/test/dns-server.spec.ts @@ -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 + + 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 { + 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 { + 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) + }) +}) \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 49daee29..47f679c1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,7 +16,7 @@ }, "include": [ "src", - "types/svg.d.ts", + "types/**/*", "test", "test-e2e", "playwright.config.js", diff --git a/types/dns2.d.ts b/types/dns2.d.ts new file mode 100644 index 00000000..6b2c62c6 --- /dev/null +++ b/types/dns2.d.ts @@ -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 + } +} \ No newline at end of file