summaryrefslogtreecommitdiffstats
path: root/test/agent.js
diff options
context:
space:
mode:
Diffstat (limited to 'test/agent.js')
-rw-r--r--test/agent.js782
1 files changed, 782 insertions, 0 deletions
diff --git a/test/agent.js b/test/agent.js
new file mode 100644
index 0000000..65afd8b
--- /dev/null
+++ b/test/agent.js
@@ -0,0 +1,782 @@
+'use strict'
+
+const { test, teardown } = require('tap')
+const http = require('http')
+const { PassThrough } = require('stream')
+const { kRunning } = require('../lib/core/symbols')
+const {
+ Agent,
+ errors,
+ request,
+ stream,
+ pipeline,
+ Pool,
+ setGlobalDispatcher,
+ getGlobalDispatcher
+} = require('../')
+const importFresh = require('import-fresh')
+
+test('setGlobalDispatcher', t => {
+ t.plan(2)
+
+ t.test('fails if agent does not implement `get` method', t => {
+ t.plan(1)
+ t.throws(() => setGlobalDispatcher({ dispatch: 'not a function' }), errors.InvalidArgumentError)
+ })
+
+ t.test('sets global agent', t => {
+ t.plan(2)
+ t.doesNotThrow(() => setGlobalDispatcher(new Agent()))
+ t.doesNotThrow(() => setGlobalDispatcher({ dispatch: () => {} }))
+ })
+
+ t.teardown(() => {
+ // reset globalAgent to a fresh Agent instance for later tests
+ setGlobalDispatcher(new Agent())
+ })
+})
+
+test('Agent', t => {
+ t.plan(1)
+
+ t.doesNotThrow(() => new Agent())
+})
+
+test('agent should call callback after closing internal pools', t => {
+ t.plan(2)
+
+ const wanted = 'payload'
+
+ const server = http.createServer((req, res) => {
+ res.setHeader('Content-Type', 'text/plain')
+ res.end(wanted)
+ })
+
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const dispatcher = new Agent()
+
+ const origin = `http://localhost:${server.address().port}`
+
+ request(origin, { dispatcher })
+ .then(() => {
+ t.pass('first request should resolve')
+ })
+ .catch(err => {
+ t.fail(err)
+ })
+
+ dispatcher.once('connect', () => {
+ dispatcher.close(() => {
+ request(origin, { dispatcher })
+ .then(() => {
+ t.fail('second request should not resolve')
+ })
+ .catch(err => {
+ t.type(err, errors.ClientDestroyedError)
+ })
+ })
+ })
+ })
+})
+
+test('agent close throws when callback is not a function', t => {
+ t.plan(1)
+ const dispatcher = new Agent()
+ try {
+ dispatcher.close({})
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ }
+})
+
+test('agent should close internal pools', t => {
+ t.plan(2)
+
+ const wanted = 'payload'
+
+ const server = http.createServer((req, res) => {
+ res.setHeader('Content-Type', 'text/plain')
+ res.end(wanted)
+ })
+
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const dispatcher = new Agent()
+
+ const origin = `http://localhost:${server.address().port}`
+
+ request(origin, { dispatcher })
+ .then(() => {
+ t.pass('first request should resolve')
+ })
+ .catch(err => {
+ t.fail(err)
+ })
+
+ dispatcher.once('connect', () => {
+ dispatcher.close()
+ .then(() => request(origin, { dispatcher }))
+ .then(() => {
+ t.fail('second request should not resolve')
+ })
+ .catch(err => {
+ t.type(err, errors.ClientDestroyedError)
+ })
+ })
+ })
+})
+
+test('agent should destroy internal pools and call callback', t => {
+ t.plan(2)
+
+ const wanted = 'payload'
+
+ const server = http.createServer((req, res) => {
+ res.setHeader('Content-Type', 'text/plain')
+ res.end(wanted)
+ })
+
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const dispatcher = new Agent()
+
+ const origin = `http://localhost:${server.address().port}`
+
+ request(origin, { dispatcher })
+ .then(() => {
+ t.fail()
+ })
+ .catch(err => {
+ t.type(err, errors.ClientDestroyedError)
+ })
+
+ dispatcher.once('connect', () => {
+ dispatcher.destroy(() => {
+ request(origin, { dispatcher })
+ .then(() => {
+ t.fail()
+ })
+ .catch(err => {
+ t.type(err, errors.ClientDestroyedError)
+ })
+ })
+ })
+ })
+})
+
+test('agent destroy throws when callback is not a function', t => {
+ t.plan(1)
+ const dispatcher = new Agent()
+ try {
+ dispatcher.destroy(new Error('mock error'), {})
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ }
+})
+
+test('agent close/destroy callback with error', t => {
+ t.plan(4)
+ const dispatcher = new Agent()
+ t.equal(dispatcher.closed, false)
+ dispatcher.close()
+ t.equal(dispatcher.closed, true)
+ t.equal(dispatcher.destroyed, false)
+ dispatcher.destroy(new Error('mock error'))
+ t.equal(dispatcher.destroyed, true)
+})
+
+test('agent should destroy internal pools', t => {
+ t.plan(2)
+
+ const wanted = 'payload'
+
+ const server = http.createServer((req, res) => {
+ res.setHeader('Content-Type', 'text/plain')
+ res.end(wanted)
+ })
+
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const dispatcher = new Agent()
+
+ const origin = `http://localhost:${server.address().port}`
+
+ request(origin, { dispatcher })
+ .then(() => {
+ t.fail()
+ })
+ .catch(err => {
+ t.type(err, errors.ClientDestroyedError)
+ })
+
+ dispatcher.once('connect', () => {
+ dispatcher.destroy()
+ .then(() => request(origin, { dispatcher }))
+ .then(() => {
+ t.fail()
+ })
+ .catch(err => {
+ t.type(err, errors.ClientDestroyedError)
+ })
+ })
+ })
+})
+
+test('multiple connections', t => {
+ const connections = 3
+ t.plan(6 * connections)
+
+ const server = http.createServer((req, res) => {
+ res.writeHead(200, {
+ Connection: 'keep-alive',
+ 'Keep-Alive': 'timeout=1s'
+ })
+ res.end('ok')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const origin = `http://localhost:${server.address().port}`
+ const dispatcher = new Agent({ connections })
+
+ t.teardown(dispatcher.close.bind(dispatcher))
+
+ dispatcher.on('connect', (origin, [dispatcher]) => {
+ t.ok(dispatcher)
+ })
+ dispatcher.on('disconnect', (origin, [dispatcher], error) => {
+ t.ok(dispatcher)
+ t.type(error, errors.InformationalError)
+ t.equal(error.code, 'UND_ERR_INFO')
+ t.equal(error.message, 'reset')
+ })
+
+ for (let i = 0; i < connections; i++) {
+ await request(origin, { dispatcher })
+ .then(() => {
+ t.pass('should pass')
+ })
+ .catch(err => {
+ t.fail(err)
+ })
+ }
+ })
+})
+
+test('agent factory supports URL parameter', (t) => {
+ t.plan(2)
+
+ const noopHandler = {
+ onConnect () {},
+ onHeaders () {},
+ onData () {},
+ onComplete () {
+ server.close()
+ },
+ onError (err) {
+ throw err
+ }
+ }
+
+ const dispatcher = new Agent({
+ factory: (origin, opts) => {
+ t.ok(origin instanceof URL)
+ return new Pool(origin, opts)
+ }
+ })
+
+ const server = http.createServer((req, res) => {
+ res.setHeader('Content-Type', 'text/plain')
+ res.end('asd')
+ })
+
+ server.listen(0, () => {
+ t.doesNotThrow(() => dispatcher.dispatch({
+ origin: new URL(`http://localhost:${server.address().port}`),
+ path: '/',
+ method: 'GET'
+ }, noopHandler))
+ })
+})
+
+test('agent factory supports string parameter', (t) => {
+ t.plan(2)
+
+ const noopHandler = {
+ onConnect () {},
+ onHeaders () {},
+ onData () {},
+ onComplete () {
+ server.close()
+ },
+ onError (err) {
+ throw err
+ }
+ }
+
+ const dispatcher = new Agent({
+ factory: (origin, opts) => {
+ t.ok(typeof origin === 'string')
+ return new Pool(origin, opts)
+ }
+ })
+
+ const server = http.createServer((req, res) => {
+ res.setHeader('Content-Type', 'text/plain')
+ res.end('asd')
+ })
+
+ server.listen(0, () => {
+ t.doesNotThrow(() => dispatcher.dispatch({
+ origin: `http://localhost:${server.address().port}`,
+ path: '/',
+ method: 'GET'
+ }, noopHandler))
+ })
+})
+
+test('with globalAgent', t => {
+ t.plan(6)
+ const wanted = 'payload'
+
+ const server = http.createServer((req, res) => {
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ t.equal(`localhost:${server.address().port}`, req.headers.host)
+ res.setHeader('Content-Type', 'text/plain')
+ res.end(wanted)
+ })
+
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ request(`http://localhost:${server.address().port}`)
+ .then(({ statusCode, headers, body }) => {
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal(wanted, Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+ .catch(err => {
+ t.fail(err)
+ })
+ })
+})
+
+test('with local agent', t => {
+ t.plan(6)
+ const wanted = 'payload'
+
+ const server = http.createServer((req, res) => {
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ t.equal(`localhost:${server.address().port}`, req.headers.host)
+ res.setHeader('Content-Type', 'text/plain')
+ res.end(wanted)
+ })
+
+ t.teardown(server.close.bind(server))
+
+ const dispatcher = new Agent({
+ connect: {
+ servername: 'agent1'
+ }
+ })
+
+ server.listen(0, () => {
+ request(`http://localhost:${server.address().port}`, { dispatcher })
+ .then(({ statusCode, headers, body }) => {
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal(wanted, Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+ .catch(err => {
+ t.fail(err)
+ })
+ })
+})
+
+test('fails with invalid args', t => {
+ t.throws(() => request(), errors.InvalidArgumentError, 'throws on missing url argument')
+ t.throws(() => request(''), errors.InvalidArgumentError, 'throws on invalid url')
+ t.throws(() => request({}), errors.InvalidArgumentError, 'throws on missing url.origin argument')
+ t.throws(() => request({ origin: '' }), errors.InvalidArgumentError, 'throws on invalid url.origin argument')
+ t.throws(() => request('https://example.com', { path: 0 }), errors.InvalidArgumentError, 'throws on opts.path argument')
+ t.throws(() => request('https://example.com', { agent: new Agent() }), errors.InvalidArgumentError, 'throws on opts.path argument')
+ t.throws(() => request('https://example.com', 'asd'), errors.InvalidArgumentError, 'throws on non object opts argument')
+ t.end()
+})
+
+test('with globalAgent', t => {
+ t.plan(6)
+ const wanted = 'payload'
+
+ const server = http.createServer((req, res) => {
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ t.equal(`localhost:${server.address().port}`, req.headers.host)
+ res.setHeader('Content-Type', 'text/plain')
+ res.end(wanted)
+ })
+
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ stream(
+ `http://localhost:${server.address().port}`,
+ {
+ opaque: new PassThrough()
+ },
+ ({ statusCode, headers, opaque: pt }) => {
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ const bufs = []
+ pt.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ pt.on('end', () => {
+ t.equal(wanted, Buffer.concat(bufs).toString('utf8'))
+ })
+ pt.on('error', () => {
+ t.fail()
+ })
+ return pt
+ }
+ )
+ })
+})
+
+test('with a local agent', t => {
+ t.plan(9)
+ const wanted = 'payload'
+
+ const server = http.createServer((req, res) => {
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ t.equal(`localhost:${server.address().port}`, req.headers.host)
+ res.setHeader('Content-Type', 'text/plain')
+ res.end(wanted)
+ })
+
+ t.teardown(server.close.bind(server))
+
+ const dispatcher = new Agent()
+
+ dispatcher.on('connect', (origin, [dispatcher]) => {
+ t.ok(dispatcher)
+ t.equal(dispatcher[kRunning], 0)
+ process.nextTick(() => {
+ t.equal(dispatcher[kRunning], 1)
+ })
+ })
+
+ server.listen(0, () => {
+ stream(
+ `http://localhost:${server.address().port}`,
+ {
+ dispatcher,
+ opaque: new PassThrough()
+ },
+ ({ statusCode, headers, opaque: pt }) => {
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ const bufs = []
+ pt.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ pt.on('end', () => {
+ t.equal(wanted, Buffer.concat(bufs).toString('utf8'))
+ })
+ pt.on('error', () => {
+ t.fail()
+ })
+ return pt
+ }
+ )
+ })
+})
+
+test('stream: fails with invalid URL', t => {
+ t.plan(4)
+ t.throws(() => stream(), errors.InvalidArgumentError, 'throws on missing url argument')
+ t.throws(() => stream(''), errors.InvalidArgumentError, 'throws on invalid url')
+ t.throws(() => stream({}), errors.InvalidArgumentError, 'throws on missing url.origin argument')
+ t.throws(() => stream({ origin: '' }), errors.InvalidArgumentError, 'throws on invalid url.origin argument')
+})
+
+test('with globalAgent', t => {
+ t.plan(6)
+ const wanted = 'payload'
+
+ const server = http.createServer((req, res) => {
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ t.equal(`localhost:${server.address().port}`, req.headers.host)
+ res.setHeader('Content-Type', 'text/plain')
+ res.end(wanted)
+ })
+
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const bufs = []
+
+ pipeline(
+ `http://localhost:${server.address().port}`,
+ {},
+ ({ statusCode, headers, body }) => {
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ return body
+ }
+ )
+ .end()
+ .on('data', buf => {
+ bufs.push(buf)
+ })
+ .on('end', () => {
+ t.equal(wanted, Buffer.concat(bufs).toString('utf8'))
+ })
+ .on('error', () => {
+ t.fail()
+ })
+ })
+})
+
+test('with a local agent', t => {
+ t.plan(6)
+ const wanted = 'payload'
+
+ const server = http.createServer((req, res) => {
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ t.equal(`localhost:${server.address().port}`, req.headers.host)
+ res.setHeader('Content-Type', 'text/plain')
+ res.end(wanted)
+ })
+
+ t.teardown(server.close.bind(server))
+
+ const dispatcher = new Agent()
+
+ server.listen(0, () => {
+ const bufs = []
+
+ pipeline(
+ `http://localhost:${server.address().port}`,
+ { dispatcher },
+ ({ statusCode, headers, body }) => {
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ return body
+ }
+ )
+ .end()
+ .on('data', buf => {
+ bufs.push(buf)
+ })
+ .on('end', () => {
+ t.equal(wanted, Buffer.concat(bufs).toString('utf8'))
+ })
+ .on('error', () => {
+ t.fail()
+ })
+ })
+})
+
+test('pipeline: fails with invalid URL', t => {
+ t.plan(4)
+ t.throws(() => pipeline(), errors.InvalidArgumentError, 'throws on missing url argument')
+ t.throws(() => pipeline(''), errors.InvalidArgumentError, 'throws on invalid url')
+ t.throws(() => pipeline({}), errors.InvalidArgumentError, 'throws on missing url.origin argument')
+ t.throws(() => pipeline({ origin: '' }), errors.InvalidArgumentError, 'throws on invalid url.origin argument')
+})
+
+test('pipeline: fails with invalid onInfo', (t) => {
+ t.plan(2)
+ pipeline({ origin: 'http://localhost', path: '/', onInfo: 'foo' }, () => {}).on('error', (err) => {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'invalid onInfo callback')
+ })
+})
+
+test('request: fails with invalid onInfo', async (t) => {
+ try {
+ await request({ origin: 'http://localhost', path: '/', onInfo: 'foo' })
+ t.fail('should throw')
+ } catch (e) {
+ t.ok(e)
+ t.equal(e.message, 'invalid onInfo callback')
+ }
+ t.end()
+})
+
+test('stream: fails with invalid onInfo', async (t) => {
+ try {
+ await stream({ origin: 'http://localhost', path: '/', onInfo: 'foo' }, () => new PassThrough())
+ t.fail('should throw')
+ } catch (e) {
+ t.ok(e)
+ t.equal(e.message, 'invalid onInfo callback')
+ }
+ t.end()
+})
+
+test('constructor validations', t => {
+ t.plan(4)
+ t.throws(() => new Agent({ factory: 'ASD' }), errors.InvalidArgumentError, 'throws on invalid opts argument')
+ t.throws(() => new Agent({ maxRedirections: 'ASD' }), errors.InvalidArgumentError, 'throws on invalid opts argument')
+ t.throws(() => new Agent({ maxRedirections: -1 }), errors.InvalidArgumentError, 'throws on invalid opts argument')
+ t.throws(() => new Agent({ maxRedirections: null }), errors.InvalidArgumentError, 'throws on invalid opts argument')
+})
+
+test('dispatch validations', t => {
+ const dispatcher = new Agent()
+
+ const noopHandler = {
+ onConnect () {},
+ onHeaders () {},
+ onData () {},
+ onComplete () {
+ server.close()
+ },
+ onError (err) {
+ throw err
+ }
+ }
+
+ const server = http.createServer((req, res) => {
+ res.setHeader('Content-Type', 'text/plain')
+ res.end('asd')
+ })
+
+ t.plan(6)
+ t.throws(() => dispatcher.dispatch('ASD'), errors.InvalidArgumentError, 'throws on missing handler')
+ t.throws(() => dispatcher.dispatch('ASD', noopHandler), errors.InvalidArgumentError, 'throws on invalid opts argument type')
+ t.throws(() => dispatcher.dispatch({}, noopHandler), errors.InvalidArgumentError, 'throws on invalid opts.origin argument')
+ t.throws(() => dispatcher.dispatch({ origin: '' }, noopHandler), errors.InvalidArgumentError, 'throws on invalid opts.origin argument')
+ t.throws(() => dispatcher.dispatch({}, {}), errors.InvalidArgumentError, 'throws on invalid handler.onError')
+
+ server.listen(0, () => {
+ t.doesNotThrow(() => dispatcher.dispatch({
+ origin: new URL(`http://localhost:${server.address().port}`),
+ path: '/',
+ method: 'GET'
+ }, noopHandler))
+ })
+})
+
+test('drain', t => {
+ t.plan(2)
+
+ const dispatcher = new Agent({
+ connections: 1,
+ pipelining: 1
+ })
+
+ dispatcher.on('drain', () => {
+ t.pass()
+ })
+
+ class Handler {
+ onConnect () {}
+ onHeaders () {}
+ onData () {}
+ onComplete () {}
+ onError () {
+ t.fail()
+ }
+ }
+
+ const server = http.createServer((req, res) => {
+ res.setHeader('Content-Type', 'text/plain')
+ res.end('asd')
+ })
+
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ t.equal(dispatcher.dispatch({
+ origin: `http://localhost:${server.address().port}`,
+ method: 'GET',
+ path: '/'
+ }, new Handler()), false)
+ })
+})
+
+test('global api', t => {
+ t.plan(6 * 2)
+
+ const server = http.createServer((req, res) => {
+ if (req.url === '/bar') {
+ t.equal(req.method, 'PUT')
+ t.equal(req.url, '/bar')
+ } else {
+ t.equal(req.method, 'GET')
+ t.equal(req.url, '/foo')
+ }
+ req.pipe(res)
+ })
+
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const origin = `http://localhost:${server.address().port}`
+ await request(origin, { path: '/foo' })
+ await request(`${origin}/foo`)
+ await request({ origin, path: '/foo' })
+ await stream({ origin, path: '/foo' }, () => new PassThrough())
+ await request({ protocol: 'http:', hostname: 'localhost', port: server.address().port, path: '/foo' })
+ await request(`${origin}/bar`, { body: 'asd' })
+ })
+})
+
+test('global api throws', t => {
+ const origin = 'http://asd'
+ t.throws(() => request(`${origin}/foo`, { path: '/foo' }), errors.InvalidArgumentError)
+ t.throws(() => request({ origin, path: 0 }, { path: '/foo' }), errors.InvalidArgumentError)
+ t.throws(() => request({ origin, pathname: 0 }, { path: '/foo' }), errors.InvalidArgumentError)
+ t.throws(() => request({ origin: 0 }, { path: '/foo' }), errors.InvalidArgumentError)
+ t.throws(() => request(0), errors.InvalidArgumentError)
+ t.throws(() => request(1), errors.InvalidArgumentError)
+ t.end()
+})
+
+test('unreachable request rejects and can be caught', t => {
+ t.plan(1)
+
+ request('https://thisis.not/avalid/url').catch(() => {
+ t.pass()
+ })
+})
+
+test('connect is not valid', t => {
+ t.plan(1)
+
+ t.throws(() => new Agent({ connect: false }), errors.InvalidArgumentError, 'connect must be a function or an object')
+})
+
+test('the dispatcher is truly global', t => {
+ const agent = getGlobalDispatcher()
+ const undiciFresh = importFresh('../index.js')
+ t.equal(agent, undiciFresh.getGlobalDispatcher())
+ t.end()
+})
+
+teardown(() => process.exit())