import { beforeAll, describe, expect, it, setDefaultTimeout, test } from "bun:test"; import { bunEnv, bunExe, isAndroid, isLinux, isWindows } from "harness"; import * as dgram from "node:dgram"; import * as dns from "node:dns"; import * as dns_promises from "node:dns/promises"; import { once } from "node:events"; import * as fs from "node:fs"; import * as os from "node:os"; import * as util from "node:util"; beforeAll(() => { setDefaultTimeout(1000 * 60 * 5); }); // TODO: test("it exists", () => { expect(dns).toBeDefined(); expect(dns.lookup).toBeDefined(); expect(dns.lookupService).toBeDefined(); expect(dns.resolve).toBeDefined(); expect(dns.resolve4).toBeDefined(); expect(dns.resolve6).toBeDefined(); expect(dns.resolveSrv).toBeDefined(); expect(dns.resolveTxt).toBeDefined(); expect(dns.resolveSoa).toBeDefined(); expect(dns.resolveNaptr).toBeDefined(); expect(dns.resolveMx).toBeDefined(); expect(dns.resolveCaa).toBeDefined(); expect(dns.resolveNs).toBeDefined(); expect(dns.resolvePtr).toBeDefined(); expect(dns.resolveCname).toBeDefined(); expect(dns.promises).toBeDefined(); expect(dns.promises.lookup).toBeDefined(); expect(dns.promises.lookupService).toBeDefined(); expect(dns.promises.resolve).toBeDefined(); expect(dns.promises.resolve4).toBeDefined(); expect(dns.promises.resolve6).toBeDefined(); expect(dns.promises.resolveSrv).toBeDefined(); expect(dns.promises.resolveTxt).toBeDefined(); expect(dns.promises.resolveSoa).toBeDefined(); expect(dns.promises.resolveNaptr).toBeDefined(); expect(dns.promises.resolveMx).toBeDefined(); expect(dns.promises.resolveCaa).toBeDefined(); expect(dns.promises.resolveNs).toBeDefined(); expect(dns.promises.resolvePtr).toBeDefined(); expect(dns.promises.resolveCname).toBeDefined(); expect(dns_promises).toBeDefined(); expect(dns_promises.lookup).toBeDefined(); expect(dns_promises.lookupService).toBeDefined(); expect(dns_promises.resolve).toBeDefined(); expect(dns_promises.resolve4).toBeDefined(); expect(dns_promises.resolve6).toBeDefined(); expect(dns_promises.resolveSrv).toBeDefined(); expect(dns_promises.resolveTxt).toBeDefined(); expect(dns_promises.resolveSoa).toBeDefined(); expect(dns_promises.resolveNaptr).toBeDefined(); expect(dns_promises.resolveMx).toBeDefined(); expect(dns_promises.resolveCaa).toBeDefined(); expect(dns_promises.resolveNs).toBeDefined(); expect(dns_promises.resolvePtr).toBeDefined(); expect(dns_promises.resolveCname).toBeDefined(); }); // //TODO: use a bun.sh SRV for testing test("dns.resolveSrv (_test._tcp.test.socketify.dev)", () => { const { promise, resolve, reject } = Promise.withResolvers(); dns.resolveSrv("_test._tcp.test.socketify.dev", (err, results) => { try { expect(err).toBeNull(); expect(results instanceof Array).toBe(true); expect(results[0].name).toBe("_dc-srv.130c90ab9de1._test._tcp.test.socketify.dev"); expect(results[0].priority).toBe(10); expect(results[0].weight).toBe(50); expect(results[0].port).toBe(80); resolve(); } catch (error) { reject(err || error); } }); return promise; }); test("dns.resolveSrv (_test._tcp.invalid.localhost)", () => { const { promise, resolve, reject } = Promise.withResolvers(); dns.resolveSrv("_test._tcp.invalid.localhost", (err, results) => { try { expect(err).toBeTruthy(); expect(results).toBeUndefined(true); resolve(); } catch (error) { reject(err || error); } }); return promise; }); // RFC 2782 says SRV targets must not be compressed, but a lot of deployed // resolvers (dnsmasq, mDNSResponder, older BIND, various corporate forwarders) // still compress them. c-ares 1.34.8 started rejecting these responses with // EBADRESP; make sure we keep accepting them. test.skipIf(isWindows)("dns.resolveSrv accepts compressed target in RDATA", async () => { const socket = dgram.createSocket("udp4"); try { socket.on("message", (query, rinfo) => { // Find end of question section: QNAME + QTYPE(2) + QCLASS(2). let off = 12; while (off < query.length && query[off] !== 0) off += query[off] + 1; off += 1 + 2 + 2; const question = query.subarray(12, off); const header = Buffer.alloc(12); header[0] = query[0]; header[1] = query[1]; header[2] = 0x81; // QR=1, RD=1 header[3] = 0x80; // RA=1 header[5] = 1; // QDCOUNT header[7] = 1; // ANCOUNT // RDATA: priority=10, weight=50, port=80, target = "srv" + pointer to // QNAME at offset 12. After decompression the target is // srv.. const rdata = Buffer.from([ 0x00, 0x0a, // priority 0x00, 0x32, // weight 0x00, 0x50, // port 0x03, 0x73, 0x72, 0x76, // "srv" 0xc0, 0x0c, // compression pointer -> offset 12 ]); const answer = Buffer.concat([ Buffer.from([0xc0, 0x0c, 0x00, 0x21, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3c]), Buffer.from([rdata.length >> 8, rdata.length & 0xff]), rdata, ]); socket.send(Buffer.concat([header, question, answer]), rinfo.port, rinfo.address); }); socket.bind(0, "127.0.0.1"); await once(socket, "listening"); const { port } = socket.address(); const resolver = new dns.Resolver({ timeout: 1000, tries: 1 }); resolver.setServers(["127.0.0.1:" + port]); const { promise, resolve, reject } = Promise.withResolvers(); resolver.resolveSrv("_test._tcp.example.test", (err, records) => (err ? reject(err) : resolve(records))); const records = await promise; expect(records).toEqual([{ name: "srv._test._tcp.example.test", priority: 10, weight: 50, port: 80 }]); } finally { socket.close(); } }); test("dns.resolveTxt (txt.socketify.dev)", () => { const { promise, resolve, reject } = Promise.withResolvers(); dns.resolveTxt("txt.socketify.dev", (err, results) => { try { expect(err).toBeNull(); expect(results instanceof Array).toBe(true); expect(results[0][0]).toBe("bun_test;test"); resolve(); } catch (error) { reject(err || error); } }); return promise; }); test("dns.resolveSoa (bun.sh)", () => { const { promise, resolve, reject } = Promise.withResolvers(); dns.resolveSoa("bun.sh", (err, result) => { try { expect(err).toBeNull(); expect(typeof result.serial).toBe("number"); expect(result.refresh).toBe(10000); expect(result.retry).toBe(2400); expect(result.expire).toBe(604800); // Cloudflare might randomly change min TTL expect(result.minttl).toBeNumber(); expect(result.nsname).toBe("hans.ns.cloudflare.com"); expect(result.hostmaster).toBe("dns.cloudflare.com"); resolve(); } catch (error) { reject(err || error); } }); return promise; }); test("dns.resolveSoa (empty string)", () => { const { promise, resolve, reject } = Promise.withResolvers(); dns.resolveSoa("", (err, result) => { try { expect(err).toBeNull(); // one of root server expect(result).not.toBeUndefined(); resolve(); } catch (error) { reject(err || error); } }); return promise; }); test("dns.resolveNaptr (naptr.socketify.dev)", () => { const { promise, resolve, reject } = Promise.withResolvers(); dns.resolveNaptr("naptr.socketify.dev", (err, results) => { try { expect(err).toBeNull(); expect(results instanceof Array).toBe(true); expect(results[0].flags).toBe("S"); expect(results[0].service).toBe("test"); expect(results[0].regexp).toBe(""); expect(results[0].replacement).toBe(""); expect(results[0].order).toBe(1); expect(results[0].preference).toBe(12); resolve(); } catch (error) { reject(err || error); } }); return promise; }); test("dns.resolveCaa (caa.socketify.dev)", () => { const { promise, resolve, reject } = Promise.withResolvers(); dns.resolveCaa("caa.socketify.dev", (err, results) => { try { expect(err).toBeNull(); expect(results instanceof Array).toBe(true); expect(results[0].critical).toBe(0); expect(results[0].issue).toBe("bun.sh"); resolve(); } catch (error) { reject(err || error); } }); return promise; }); test.skipIf(isWindows)("dns.Resolver#resolveCaa keeps a numeric CAA tag reachable by index", async () => { const socket = dgram.createSocket("udp4"); try { socket.on("message", (query, rinfo) => { let off = 12; while (off < query.length && query[off] !== 0) off += query[off] + 1; off += 1 + 2 + 2; const question = query.subarray(12, off); const header = Buffer.alloc(12); header[0] = query[0]; header[1] = query[1]; header[2] = 0x81; header[3] = 0x80; header[5] = 1; header[7] = 1; const tag = Buffer.from("128"); const value = Buffer.from("issue.example"); const rdata = Buffer.concat([Buffer.from([0x00, tag.length]), tag, value]); const answer = Buffer.concat([ Buffer.from([0xc0, 0x0c, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3c]), Buffer.from([rdata.length >> 8, rdata.length & 0xff]), rdata, ]); socket.send(Buffer.concat([header, question, answer]), rinfo.port, rinfo.address); }); socket.bind(0, "127.0.0.1"); await once(socket, "listening"); const { port } = socket.address(); const resolver = new dns.Resolver({ timeout: 1000, tries: 1 }); resolver.setServers(["127.0.0.1:" + port]); const { promise, resolve, reject } = Promise.withResolvers(); resolver.resolveCaa("caa.example.test", (err, records) => (err ? reject(err) : resolve(records))); const records = await promise; expect(records.length).toBe(1); expect(records[0].critical).toBe(0); expect(records[0][128]).toBe("issue.example"); expect(Object.keys(records[0])).toEqual(["128", "critical"]); } finally { socket.close(); } }); test("dns.resolveMx (bun.sh)", () => { const { promise, resolve, reject } = Promise.withResolvers(); dns.resolveMx("bun.sh", (err, results) => { try { expect(err).toBeNull(); expect(results instanceof Array).toBe(true); const priority = results[0].priority; expect(priority >= 0 && priority < 65535).toBe(true); expect(results[0].exchange.includes("aspmx.l.google.com")).toBe(true); resolve(); } catch (error) { reject(err || error); } }); return promise; }); test("dns.resolveNs (bun.sh) ", () => { const { promise, resolve, reject } = Promise.withResolvers(); dns.resolveNs("bun.sh", (err, results) => { try { expect(err).toBeNull(); expect(results instanceof Array).toBe(true); expect(results[0].includes(".ns.cloudflare.com")).toBe(true); resolve(); } catch (error) { reject(err || error); } }); return promise; }); test("dns.resolveNs (empty string) ", () => { const { promise, resolve, reject } = Promise.withResolvers(); dns.resolveNs("", (err, results) => { try { expect(err).toBeNull(); expect(results instanceof Array).toBe(true); // root servers expect(results.sort()).toStrictEqual( [ "e.root-servers.net", "h.root-servers.net", "l.root-servers.net", "i.root-servers.net", "a.root-servers.net", "d.root-servers.net", "c.root-servers.net", "b.root-servers.net", "j.root-servers.net", "k.root-servers.net", "g.root-servers.net", "m.root-servers.net", "f.root-servers.net", ].sort(), ); resolve(); } catch (error) { reject(err || error); } }); return promise; }); test("dns.resolvePtr (ptr.socketify.dev)", () => { const { promise, resolve, reject } = Promise.withResolvers(); dns.resolvePtr("ptr.socketify.dev", (err, results) => { try { expect(err).toBeNull(); expect(results instanceof Array).toBe(true); expect(results[0]).toBe("bun.sh"); resolve(); } catch (error) { reject(err || error); } }); return promise; }); test("dns.resolveCname (cname.socketify.dev)", () => { const { promise, resolve, reject } = Promise.withResolvers(); dns.resolveCname("cname.socketify.dev", (err, results) => { try { expect(err).toBeNull(); expect(results instanceof Array).toBe(true); expect(results[0]).toBe("bun.sh"); resolve(); } catch (error) { reject(err || error); } }); return promise; }); test("dns.lookup (example.com)", () => { const { promise, resolve, reject } = Promise.withResolvers(); dns.lookup("example.com", (err, address, family) => { try { expect(err).toBeNull(); expect(typeof address).toBe("string"); resolve(); } catch (error) { reject(err || error); } }); return promise; }); test("dns.lookup bad (qedjp3f4q4jgjh4d6vaf3fd2hbfhg6upt2bscrfe.com)", () => { const { promise, resolve, reject } = Promise.withResolvers(); dns.lookup("qedjp3f4q4jgjh4d6vaf3fd2hbfhg6upt2bscrfe.com", (err, address, family) => { try { expect(err).not.toBeNull(); expect(err.syscall).toEqual("getaddrinfo"); expect(err.code).toEqual("ENOTFOUND"); expect(address).toBeUndefined(); expect(family).toBeUndefined(); resolve(); } catch (error) { reject(err || error); } }); return promise; }); test("dns.lookup (example.com) with { all: true } #2675", () => { const { promise, resolve, reject } = Promise.withResolvers(); dns.lookup("example.com", { all: true }, (err, address, family) => { try { expect(err).toBeNull(); expect(Array.isArray(address)).toBe(true); resolve(); } catch (error) { reject(err || error); } }); return promise; }); test("dns.lookup (localhost)", () => { const { promise, resolve, reject } = Promise.withResolvers(); dns.lookup("localhost", (err, address, family) => { expect(err).toBeNull(); if (family === 6) { expect(address).toBe("::1"); } else { expect(address).toBe("127.0.0.1"); } err ? reject(err) : resolve(); }); return promise; }); // dns.lookup()'s contract is getaddrinfo(3), so it must use the system // resolver, not c-ares, which reads /etc/resolv.conf directly and bypasses // NSS/systemd-resolved on split-DNS hosts (#37378). "127.1" is a legacy IPv4 // literal: getaddrinfo resolves it locally with no DNS traffic, while c-ares // treats it as a hostname and queries the configured servers, here a local // responder that REFUSEs every query. Linux-only: it is the one platform // where the default Bun.dns backend is c-ares. test.skipIf(!isLinux)("dns.lookup uses getaddrinfo, not the c-ares resolver", async () => { const fixture = ` const dns = require("node:dns"); function refused(msg) { const r = Buffer.from(msg); r[2] = 0x81; // QR + RD r[3] = 0x85; // RA + rcode REFUSED r.fill(0, 6, 12); // zero the answer counts return r; } const server = await Bun.udpSocket({ hostname: "127.0.0.1", port: 0, socket: { data(sock, buf, port, addr) { sock.send(refused(buf), port, addr); } }, }); dns.setServers(["127.0.0.1:" + server.port]); const fromPromises = await dns.promises.lookup("127.1").then( ({ address, family }) => ({ address, family }), e => ({ code: e.code }), ); const fromCallback = await new Promise(resolve => { dns.lookup("127.1", (e, address, family) => resolve(e ? { code: e.code } : { address, family })); }); server.close(); console.log(JSON.stringify({ fromPromises, fromCallback })); `; await using proc = Bun.spawn({ cmd: [bunExe(), "-e", fixture], env: bunEnv, stderr: "pipe", }); const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); expect(stderr).toBe(""); expect(JSON.parse(stdout)).toEqual({ fromPromises: { address: "127.0.0.1", family: 4 }, fromCallback: { address: "127.0.0.1", family: 4 }, }); expect(exitCode).toBe(0); }); // Android's platform resolver does not expose its (per-network) servers, so // getServers() reports [] until setServers() hands the resolver explicit ones. test.skipIf(isAndroid)("dns.getServers", () => { function parseResolvConf() { const servers = []; if (isWindows) { const { stdout } = Bun.spawnSync(["node", "-e", "dns.getServers().forEach(x => console.log(x))"], { stdout: "pipe", }); return stdout.toString("utf8").trim().split("\n"); } try { const content = fs.readFileSync("/etc/resolv.conf", "utf-8"); const lines = content.split(os.EOL); for (const line of lines) { const parts = line.trim().split(/\s+/); if (parts.length >= 2 && parts[0] === "nameserver") { servers.push(parts[1]); } } } catch (err) { done(err); } return servers; } const expectServers = parseResolvConf(); const actualServers = dns.getServers(); for (const server of expectServers) { expect(actualServers).toContain(server); } }); describe("dns.reverse", () => { const inputs = [ ["8.8.8.8", "dns.google"], ["2606:4700:4700::1111", "one.one.one.one"], ["2606:4700:4700::1001", "one.one.one.one"], ["1.1.1.1", "one.one.one.one"], ]; it.each(inputs)("%s <- %s", (ip, expected) => { const { promise, resolve, reject } = Promise.withResolvers(); dns.reverse(ip, (err, hostnames) => { try { expect(err).toBeNull(); expect(hostnames).toContain(expected); resolve(); } catch (error) { reject(err || error); } }); return promise; }); }); // reverse()/lookupService() consult the hosts file before DNS on every platform // (c-ares reads it first; Android goes through bionic/netd, which does too), so // whatever they report for 127.0.0.1 must come from the hosts file's loopback // entries (reverse() reports the entry's aliases, which may be none). function hostsNamesFor(address) { const hostsPath = isWindows ? `${process.env.SystemRoot ?? "C:\\Windows"}\\System32\\drivers\\etc\\hosts` : isAndroid ? "/system/etc/hosts" : "/etc/hosts"; try { return fs .readFileSync(hostsPath, "utf8") .split("\n") .map(line => line.replace(/#.*/, "").trim().split(/\s+/)) .filter(parts => parts[0] === address) .flatMap(parts => parts.slice(1)); } catch (err) { if (err?.code === "ENOENT") return []; throw err; } } // c-ares merges hosts entries by name, so 127.0.0.1's "localhost" entry may // report ::1's aliases too; both loopback lines are acceptable answers. const loopbackNames = [...hostsNamesFor("127.0.0.1"), ...hostsNamesFor("::1")]; test.skipIf(!hostsNamesFor("127.0.0.1").includes("localhost"))( "dns.reverse of loopback comes from the hosts file", async () => { for (const name of await dns.promises.reverse("127.0.0.1")) { expect(loopbackNames).toContain(name); } const { hostname } = await dns.promises.lookupService("127.0.0.1", 22); expect(loopbackNames).toContain(hostname); }, ); // Answers every A query with 127.0.0.2, so a resolve4 that lands here proves // which servers the resolver is using without any outbound DNS. async function localDnsServer() { return await Bun.udpSocket({ hostname: "127.0.0.1", port: 0, socket: { data(sock, query, port, addr) { let end = 12; while (end < query.length && query[end] !== 0) end += query[end] + 1; end += 5; // root label + QTYPE + QCLASS if (end > query.length) return; const answer = Buffer.concat([ query.subarray(0, end), Buffer.from([0xc0, 0x0c, 0, 1, 0, 1, 0, 0, 0, 60, 0, 4, 127, 0, 0, 2]), ]); answer[2] = 0x81; // QR + RD answer[3] = 0x80; // RA answer.writeUInt16BE(1, 6); // ANCOUNT answer.writeUInt16BE(0, 8); answer.writeUInt16BE(0, 10); sock.send(answer, port, addr); }, }, }); } // A resolver with no explicit servers uses the platform resolver on Android 10+; // setServers([...]) switches it to those servers, setServers([]) switches back. test("Resolver getServers/setServers round-trip", async () => { const server = await localDnsServer(); try { const resolver = new dns.promises.Resolver(); // On Android 10+ a fresh resolver is on the platform resolver, which reports // no server list; before that (API 28) it is plain c-ares like everywhere else. const usesPlatformResolver = isAndroid && resolver.getServers().length === 0; resolver.setServers([`127.0.0.1:${server.port}`]); expect(resolver.getServers()).toEqual([`127.0.0.1:${server.port}`]); expect(await resolver.resolve4("example.com")).toEqual(["127.0.0.2"]); resolver.setServers([]); expect(resolver.getServers()).toEqual([]); if (usesPlatformResolver) { // back on the platform resolver: whatever it answers (or fails with), it // is not the local responder's 127.0.0.2 expect(await resolver.resolve4("example.com").catch(e => [e.code])).not.toEqual(["127.0.0.2"]); } } finally { server.close(); } }); test("Resolver#cancel rejects in-flight queries with ECANCELLED", async () => { const resolver = new dns.promises.Resolver(); const pending = resolver.resolveTxt("example.com").then( () => "resolved", e => e.code, ); resolver.cancel(); expect(await pending).toBe("ECANCELLED"); }); test("Resolver#setServers throws while that resolver has queries in flight", async () => { const resolver = new dns.promises.Resolver(); const pending = resolver.resolveMx("example.com").catch(e => e.code); expect(() => resolver.setServers(["1.1.1.1"])).toThrow( expect.objectContaining({ code: "ERR_DNS_SET_SERVERS_FAILED" }), ); resolver.cancel(); await pending; }); test("dns.promises.reverse", async () => { { let hostnames = await dns.promises.reverse("8.8.8.8"); expect(hostnames).toContain("dns.google"); } { let hostnames = await dns.promises.reverse("1.1.1.1"); expect(hostnames).toContain("one.one.one.one"); } { let hostnames = await dns.promises.reverse("2606:4700:4700::1111"); expect(hostnames).toContain("one.one.one.one"); } }); describe("test invalid arguments", () => { it.each([ // TODO: dns.resolveAny is not implemented yet ["dns.resolveCname", dns.resolveCname], ["dns.resolveCaa", dns.resolveCaa], ["dns.resolveMx", dns.resolveMx], ["dns.resolveNaptr", dns.resolveNaptr], ["dns.resolveNs", dns.resolveNs], ["dns.resolvePtr", dns.resolvePtr], ["dns.resolveSoa", dns.resolveSoa], ["dns.resolveSrv", dns.resolveSrv], ["dns.resolveTxt", dns.resolveTxt], ])("%s", (_, fn, done) => { fn("a".repeat(2000), (err, results) => { try { expect(err).not.toBeNull(); expect(results).toBeUndefined(); // Assert we convert our error codes to Node.js error codes expect(err.code).not.toStartWith("DNS_"); done(); } catch (e) { done(e); } }); }); // https://github.com/oven-sh/bun/issues/36892 describe.each([ ["dns.resolve", hostname => dns.resolve(hostname, undefined, () => {})], ["Resolver#resolve", hostname => new dns.Resolver().resolve(hostname, undefined, () => {})], ])("%s", (_, fn) => { it("with undefined rrtype throws ERR_INVALID_ARG_TYPE", () => { expect(() => fn("localhost")).toThrow( expect.objectContaining({ code: "ERR_INVALID_ARG_TYPE", message: expect.stringContaining('The "rrtype" argument must be of type string'), }), ); }); }); it("dns.promises.resolve with undefined rrtype does not throw", async () => { // Node's promises API treats undefined rrtype as "A" const promise = dns_promises.resolve("localhost", undefined); expect(promise).toBeInstanceOf(Promise); await promise.catch(() => {}); // result depends on the environment's resolver }); it("dns.lookupService", async () => { expect(() => { dns.lookupService("", 443, (err, hostname, service) => {}); }).toThrow("Expected address to be a non-empty string for 'lookupService'."); expect(() => { dns.lookupService("google.com", 443, (err, hostname, service) => {}); }).toThrow(`The "address" argument is invalid. Received type string ('google.com')`); }); }); describe("dns.lookupService", () => { it.each([ ["1.1.1.1", 53, ["one.one.one.one", "domain"]], ["2606:4700:4700::1111", 53, ["one.one.one.one", "domain"]], ["2606:4700:4700::1001", 53, ["one.one.one.one", "domain"]], ["1.1.1.1", 80, ["one.one.one.one", "http"]], ["1.1.1.1", 443, ["one.one.one.one", "https"]], ])("lookupService(%s, %d)", (address, port, expected) => { const { promise, resolve, reject } = Promise.withResolvers(); dns.lookupService(address, port, (err, hostname, service) => { try { expect(err).toBeNull(); expect(hostname).toStrictEqual(expected[0]); expect(service).toStrictEqual(expected[1]); resolve(); } catch (err) { reject(err); } }); return promise; }); it("lookupService(255.255.255.255, 443)", () => { const { promise, resolve, reject } = Promise.withResolvers(); dns.lookupService("255.255.255.255", 443, (err, hostname, service) => { if (process.platform == "darwin") { try { expect(err).toBeNull(); expect(hostname).toStrictEqual("broadcasthost"); expect(service).toStrictEqual("https"); resolve(); } catch (err) { reject(err); } } else { try { expect(err).not.toBeNull(); expect(hostname).toBeUndefined(); expect(service).toBeUndefined(); resolve(); } catch (err) { reject(err); } } }); }); it.each([ ["1.1.1.1", 53, ["one.one.one.one", "domain"]], ["2606:4700:4700::1111", 53, ["one.one.one.one", "domain"]], ["2606:4700:4700::1001", 53, ["one.one.one.one", "domain"]], ["1.1.1.1", 80, ["one.one.one.one", "http"]], ["1.1.1.1", 443, ["one.one.one.one", "https"]], ])("promises.lookupService(%s, %d)", async (address, port, expected) => { const { hostname, service } = await dns.promises.lookupService(address, port); expect(hostname).toStrictEqual(expected[0]); expect(service).toStrictEqual(expected[1]); }); // https://github.com/oven-sh/bun/issues/37486 it("treats an IPv4-mapped IPv6 address like its embedded IPv4 address", async () => { // getnameinfo for 127.0.0.1 may ENOTFOUND on Windows, so assert the // mapped form yields the same outcome rather than a specific result. const settle = p => p.then( v => ({ ok: v }), e => ({ err: e.code }), ); const [mapped, v4] = await Promise.all([ settle(dns_promises.lookupService("::ffff:127.0.0.1", 53)), settle(dns_promises.lookupService("127.0.0.1", 53)), ]); expect(mapped).toEqual(v4); }); }); // Deprecated reference: https://nodejs.org/api/deprecations.html#DEP0118 describe("lookup deprecated behavior", () => { it.each([undefined, false, null, NaN, ""])("dns.lookup", domain => { dns.lookup(domain, (error, address, family) => { expect(error).toBeNull(); expect(address).toBeNull(); expect(family).toBe(4); }); }); }); describe("uses `dns.promises` implementations for `util.promisify` factory", () => { it.each([ "lookup", "lookupService", "resolve", "reverse", "resolve4", "resolve6", "resolveAny", "resolveCname", "resolveCaa", "resolveMx", "resolveNs", "resolvePtr", "resolveSoa", "resolveSrv", "resolveTxt", "resolveNaptr", ])("%s", method => { expect(dns[method][util.promisify.custom]).toBe(dns_promises[method]); expect(dns.promises[method]).toBe(dns_promises[method]); }); it("util.promisify(dns.lookup) acts like dns.promises.lookup", async () => { // Use a name that resolves locally: a public name with several A records // behind round-robin DNS can return a different first address per call. expect(await util.promisify(dns.lookup)("localhost")).toEqual(await dns.promises.lookup("localhost")); }); }); describe("hostnames containing NUL bytes", () => { const hostnameWithNul = "localhost\0.example.invalid"; it("dns.promises.lookup rejects instead of truncating at the NUL", async () => { await expect(dns_promises.lookup(hostnameWithNul)).rejects.toThrow(); }); it("dns.lookup (callback) passes an error instead of truncating at the NUL", async () => { const { promise, resolve, reject } = Promise.withResolvers(); dns.lookup(hostnameWithNul, (err, address, family) => { try { expect(err).toBeTruthy(); expect(address).toBeUndefined(); resolve(); } catch (e) { reject(e); } }); await promise; }); it("plain localhost still resolves", async () => { const { address } = await dns_promises.lookup("localhost"); expect(["127.0.0.1", "::1"]).toContain(address); }); }); // Node treats a null option field the same as an absent one (its guards are // `!= null`), so e.g. `{ hints: null }` must not throw. https://github.com/oven-sh/bun/issues/37318 describe("dns.lookup null option fields are treated as unset", () => { const fields = ["hints", "all", "verbatim", "order", "family"]; it.each(fields)("dns.lookup with {%s: null} resolves", async field => { const { promise, resolve, reject } = Promise.withResolvers(); dns.lookup("localhost", { [field]: null }, (err, address, family) => { if (err) reject(err); else resolve({ address, family }); }); const { address } = await promise; expect(["127.0.0.1", "::1"]).toContain(address); }); it.each(fields)("dns.promises.lookup with {%s: null} resolves", async field => { const { address } = await dns_promises.lookup("localhost", { [field]: null }); expect(["127.0.0.1", "::1"]).toContain(address); }); }); describe("dns.Resolver options validation", () => { describe.each([ ["dns.Resolver", dns.Resolver], ["dns.promises.Resolver", dns_promises.Resolver], ])("%s", (_name, Resolver) => { it.each([0, -1, 2.5, -0.5, 2 ** 31])("{tries: %p} throws ERR_OUT_OF_RANGE", tries => { expect(() => new Resolver({ tries })).toThrow(expect.objectContaining({ code: "ERR_OUT_OF_RANGE" })); }); it("{tries: 'x'} throws ERR_INVALID_ARG_TYPE", () => { expect(() => new Resolver({ tries: "x" })).toThrow(expect.objectContaining({ code: "ERR_INVALID_ARG_TYPE" })); }); it.each([-2, 1.5, 2 ** 31])("{timeout: %p} throws ERR_OUT_OF_RANGE", timeout => { expect(() => new Resolver({ timeout })).toThrow(expect.objectContaining({ code: "ERR_OUT_OF_RANGE" })); }); it.each([1, 4, 2 ** 31 - 1])("{tries: %p} is accepted", tries => { expect(() => new Resolver({ tries })).not.toThrow(); }); it.each([-1, 0, 100, 2 ** 31 - 1])("{timeout: %p} is accepted", timeout => { expect(() => new Resolver({ timeout })).not.toThrow(); }); it.each([undefined, null, 42, "hello", true])("non-object options %p uses defaults", options => { expect(() => new Resolver(options)).not.toThrow(); }); }); }); describe("dns.Resolver#setServers with IPv6 zone identifiers", () => { describe.each([ ["dns.Resolver", dns.Resolver], ["dns.promises.Resolver", dns_promises.Resolver], ])("%s", (_name, Resolver) => { it("accepts and strips the zone id", () => { const r = new Resolver(); r.setServers(["fe80::1%lo"]); expect(r.getServers()).toEqual(["fe80::1"]); }); it("accepts a bracketed zone id with a port", () => { const r = new Resolver(); r.setServers(["[fe80::1%lo]:5353"]); expect(r.getServers()).toEqual(["[fe80::1]:5353"]); }); }); }); describe("dns.lookupService with a numeric-string port", () => { // getnameinfo for 127.0.0.1 resolves on Linux/macOS but may ENOTFOUND on // Windows, so assert that "22" yields the same outcome as 22 rather than a // specific result. const settle = p => p.then( v => ({ ok: v }), e => ({ err: e.code }), ); it("callback API coerces a numeric-string port", async () => { const run = port => { const { promise, resolve } = Promise.withResolvers(); dns.lookupService("127.0.0.1", port, (err, hostname, service) => { resolve(err ? { err: err.code } : { ok: { hostname, service } }); }); return promise; }; const [asString, asNumber] = await Promise.all([run("22"), run(22)]); expect(asString).toEqual(asNumber); expect(asString.err).not.toBe("ERR_SOCKET_BAD_PORT"); }); it("promises API coerces a numeric-string port", async () => { const [asString, asNumber] = await Promise.all([ settle(dns_promises.lookupService("127.0.0.1", "22")), settle(dns_promises.lookupService("127.0.0.1", 22)), ]); expect(asString).toEqual(asNumber); expect(asString.err).not.toBe("ERR_SOCKET_BAD_PORT"); }); it("promises API still throws ERR_SOCKET_BAD_PORT synchronously for a truly bad port", () => { expect(() => dns_promises.lookupService("127.0.0.1", "nope")).toThrow( expect.objectContaining({ code: "ERR_SOCKET_BAD_PORT" }), ); }); });