summaryrefslogtreecommitdiff
path: root/tests/node_compat/test/parallel/test-net-autoselectfamily.js
diff options
context:
space:
mode:
authorYoshiya Hinosawa <stibium121@gmail.com>2024-11-12 19:54:47 +0900
committerGitHub <noreply@github.com>2024-11-12 19:54:47 +0900
commitc3c2b379669b17e5fdcbe5e62662404ca22c71c6 (patch)
tree91aae17a3c9dbad8aad35d41d52767b6df4dd24b /tests/node_compat/test/parallel/test-net-autoselectfamily.js
parent90236d67c591d4344a9ca0e5d23a4906d08308e5 (diff)
fix(ext/node): add autoSelectFamily option to net.createConnection (#26661)
Diffstat (limited to 'tests/node_compat/test/parallel/test-net-autoselectfamily.js')
-rw-r--r--tests/node_compat/test/parallel/test-net-autoselectfamily.js312
1 files changed, 312 insertions, 0 deletions
diff --git a/tests/node_compat/test/parallel/test-net-autoselectfamily.js b/tests/node_compat/test/parallel/test-net-autoselectfamily.js
new file mode 100644
index 000000000..3b520e6c8
--- /dev/null
+++ b/tests/node_compat/test/parallel/test-net-autoselectfamily.js
@@ -0,0 +1,312 @@
+// deno-fmt-ignore-file
+// deno-lint-ignore-file
+
+// Copyright Joyent and Node contributors. All rights reserved. MIT license.
+// Taken from Node 18.12.1
+// This file is automatically generated by `tests/node_compat/runner/setup.ts`. Do not modify this file manually.
+
+'use strict';
+
+const common = require('../common');
+const { parseDNSPacket, writeDNSPacket } = require('../common/dns');
+
+const assert = require('assert');
+const dgram = require('dgram');
+const { Resolver } = require('dns');
+const { createConnection, createServer } = require('net');
+
+// Test that happy eyeballs algorithm is properly implemented.
+
+// Purposely not using setDefaultAutoSelectFamilyAttemptTimeout here to test the
+// parameter is correctly used in options.
+//
+// Some of the machines in the CI need more time to establish connection
+const autoSelectFamilyAttemptTimeout = common.defaultAutoSelectFamilyAttemptTimeout;
+
+function _lookup(resolver, hostname, options, cb) {
+ resolver.resolve(hostname, 'ANY', (err, replies) => {
+ assert.notStrictEqual(options.family, 4);
+
+ if (err) {
+ return cb(err);
+ }
+
+ const hosts = replies
+ .map((r) => ({ address: r.address, family: r.type === 'AAAA' ? 6 : 4 }))
+ .sort((a, b) => b.family - a.family);
+
+ if (options.all === true) {
+ return cb(null, hosts);
+ }
+
+ return cb(null, hosts[0].address, hosts[0].family);
+ });
+}
+
+function createDnsServer(ipv6Addrs, ipv4Addrs, cb) {
+ if (!Array.isArray(ipv6Addrs)) {
+ ipv6Addrs = [ipv6Addrs];
+ }
+
+ if (!Array.isArray(ipv4Addrs)) {
+ ipv4Addrs = [ipv4Addrs];
+ }
+
+ // Create a DNS server which replies with a AAAA and a A record for the same host
+ const socket = dgram.createSocket('udp4');
+
+ // TODO(kt3k): We use common.mustCallAtLeast instead of common.mustCall
+ // because Deno sends multiple requests to the DNS server.
+ // This can be addressed if Deno.resolveDns supports ANY record type.
+ // See https://github.com/denoland/deno/issues/14492
+ socket.on('message', common.mustCallAtLeast((msg, { address, port }) => {
+ const parsed = parseDNSPacket(msg);
+ const domain = parsed.questions[0].domain;
+ assert.strictEqual(domain, 'example.org');
+
+ socket.send(writeDNSPacket({
+ id: parsed.id,
+ questions: parsed.questions,
+ answers: [
+ ...ipv6Addrs.map((address) => ({ type: 'AAAA', address, ttl: 123, domain: 'example.org' })),
+ ...ipv4Addrs.map((address) => ({ type: 'A', address, ttl: 123, domain: 'example.org' })),
+ ]
+ }), port, address);
+ }));
+
+ socket.bind(0, () => {
+ const resolver = new Resolver();
+ resolver.setServers([`127.0.0.1:${socket.address().port}`]);
+
+ cb({ dnsServer: socket, lookup: _lookup.bind(null, resolver) });
+ });
+}
+
+// Test that IPV4 is reached if IPV6 is not reachable
+{
+ createDnsServer('::1', '127.0.0.1', common.mustCall(function({ dnsServer, lookup }) {
+ const ipv4Server = createServer((socket) => {
+ socket.on('data', common.mustCall(() => {
+ socket.write('response-ipv4');
+ socket.end();
+ }));
+ });
+
+ ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
+ const port = ipv4Server.address().port;
+
+ const connection = createConnection({
+ host: 'example.org',
+ port: port,
+ lookup,
+ autoSelectFamily: true,
+ autoSelectFamilyAttemptTimeout,
+ });
+
+ let response = '';
+ connection.setEncoding('utf-8');
+
+ connection.on('ready', common.mustCall(() => {
+ assert.deepStrictEqual(connection.autoSelectFamilyAttemptedAddresses, [`::1:${port}`, `127.0.0.1:${port}`]);
+ }));
+
+ connection.on('data', (chunk) => {
+ response += chunk;
+ });
+
+ connection.on('end', common.mustCall(() => {
+ assert.strictEqual(response, 'response-ipv4');
+ ipv4Server.close();
+ dnsServer.close();
+ }));
+
+ connection.write('request');
+ }));
+ }));
+}
+
+// Test that only the last successful connection is established.
+{
+ createDnsServer(
+ ['2606:4700::6810:85e5', '2606:4700::6810:84e5', "::1"],
+ // TODO(kt3k): Comment out ipv4 addresses to make the test pass faster.
+ // Enable this when Deno.connect() call becomes cancellable.
+ // See https://github.com/denoland/deno/issues/26819
+ // ['104.20.22.46', '104.20.23.46', '127.0.0.1'],
+ ['127.0.0.1'],
+ common.mustCall(function({ dnsServer, lookup }) {
+ const ipv4Server = createServer((socket) => {
+ socket.on('data', common.mustCall(() => {
+ socket.write('response-ipv4');
+ socket.end();
+ }));
+ });
+
+ ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
+ const port = ipv4Server.address().port;
+
+ const connection = createConnection({
+ host: 'example.org',
+ port: port,
+ lookup,
+ autoSelectFamily: true,
+ autoSelectFamilyAttemptTimeout,
+ });
+
+ let response = '';
+ connection.setEncoding('utf-8');
+
+ connection.on('ready', common.mustCall(() => {
+ assert.deepStrictEqual(
+ connection.autoSelectFamilyAttemptedAddresses,
+ [
+ `2606:4700::6810:85e5:${port}`,
+ `104.20.22.46:${port}`,
+ `2606:4700::6810:84e5:${port}`,
+ `104.20.23.46:${port}`,
+ `::1:${port}`,
+ `127.0.0.1:${port}`,
+ ]
+ );
+ }));
+
+ connection.on('data', (chunk) => {
+ response += chunk;
+ });
+
+ connection.on('end', common.mustCall(() => {
+ assert.strictEqual(response, 'response-ipv4');
+ ipv4Server.close();
+ dnsServer.close();
+ }));
+
+ connection.write('request');
+ }));
+ })
+ );
+}
+
+// Test that IPV4 is NOT reached if IPV6 is reachable
+if (common.hasIPv6) {
+ createDnsServer('::1', '127.0.0.1', common.mustCall(function({ dnsServer, lookup }) {
+ const ipv4Server = createServer((socket) => {
+ socket.on('data', common.mustNotCall(() => {
+ socket.write('response-ipv4');
+ socket.end();
+ }));
+ });
+
+ const ipv6Server = createServer((socket) => {
+ socket.on('data', common.mustCall(() => {
+ socket.write('response-ipv6');
+ socket.end();
+ }));
+ });
+
+ ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
+ const port = ipv4Server.address().port;
+
+ ipv6Server.listen(port, '::1', common.mustCall(() => {
+ const connection = createConnection({
+ host: 'example.org',
+ port,
+ lookup,
+ autoSelectFamily: true,
+ autoSelectFamilyAttemptTimeout,
+ });
+
+ let response = '';
+ connection.setEncoding('utf-8');
+
+ connection.on('ready', common.mustCall(() => {
+ assert.deepStrictEqual(connection.autoSelectFamilyAttemptedAddresses, [`::1:${port}`]);
+ }));
+
+ connection.on('data', (chunk) => {
+ response += chunk;
+ });
+
+ connection.on('end', common.mustCall(() => {
+ assert.strictEqual(response, 'response-ipv6');
+ ipv4Server.close();
+ ipv6Server.close();
+ dnsServer.close();
+ }));
+
+ connection.write('request');
+ }));
+ }));
+ }));
+}
+
+// Test that when all errors are returned when no connections succeeded
+{
+ createDnsServer('::1', '127.0.0.1', common.mustCall(function({ dnsServer, lookup }) {
+ const connection = createConnection({
+ host: 'example.org',
+ port: 10,
+ lookup,
+ autoSelectFamily: true,
+ autoSelectFamilyAttemptTimeout,
+ });
+
+ connection.on('ready', common.mustNotCall());
+ connection.on('error', common.mustCall((error) => {
+ assert.deepStrictEqual(connection.autoSelectFamilyAttemptedAddresses, ['::1:10', '127.0.0.1:10']);
+ assert.strictEqual(error.constructor.name, 'AggregateError');
+ assert.strictEqual(error.errors.length, 2);
+
+ const errors = error.errors.map((e) => e.message);
+ assert.ok(errors.includes('connect ECONNREFUSED 127.0.0.1:10'));
+
+ if (common.hasIPv6) {
+ assert.ok(errors.includes('connect ECONNREFUSED ::1:10'));
+ }
+
+ dnsServer.close();
+ }));
+ }));
+}
+
+// Test that the option can be disabled
+{
+ createDnsServer('::1', '127.0.0.1', common.mustCall(function({ dnsServer, lookup }) {
+ const ipv4Server = createServer((socket) => {
+ socket.on('data', common.mustCall(() => {
+ socket.write('response-ipv4');
+ socket.end();
+ }));
+ });
+
+ ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
+ const port = ipv4Server.address().port;
+
+ const connection = createConnection({
+ host: 'example.org',
+ port,
+ lookup,
+ autoSelectFamily: false,
+ });
+
+ connection.on('ready', common.mustNotCall());
+ connection.on('error', common.mustCall((error) => {
+ assert.strictEqual(connection.autoSelectFamilyAttemptedAddresses, undefined);
+
+ if (common.hasIPv6) {
+ assert.strictEqual(error.code, 'ECONNREFUSED');
+ assert.strictEqual(error.message, `connect ECONNREFUSED ::1:${port}`);
+ } else if (error.code === 'EAFNOSUPPORT') {
+ assert.strictEqual(error.message, `connect EAFNOSUPPORT ::1:${port} - Local (undefined:undefined)`);
+ } else if (error.code === 'EUNATCH') {
+ assert.strictEqual(error.message, `connect EUNATCH ::1:${port} - Local (:::0)`);
+ } else {
+ assert.strictEqual(error.code, 'EADDRNOTAVAIL');
+ assert.strictEqual(error.message, `connect EADDRNOTAVAIL ::1:${port} - Local (:::0)`);
+ }
+
+ ipv4Server.close();
+ dnsServer.close();
+ }));
+ }));
+ }));
+}