UNPKG

@bbc/http-transport

Version:

A flexible, modular REST client built for ease-of-use and resilience.

508 lines (428 loc) 13.7 kB
'use strict'; const assert = require('chai').assert; const nock = require('nock'); const context = require('../../lib/context'); const sinon = require('sinon'); const sandbox = sinon.sandbox.create(); const FetchTransport = require('../../lib/transport/node-fetch'); // const setContextProperty const url = 'http://www.example.com/'; const httpsUrl = 'https://www.example.com/'; const proxyUrl = 'http://forward-proxy.ibl.test.api.bbci.co.uk'; const host = 'http://www.example.com'; const httpsHost = 'https://www.example.com'; const api = nock(host); const httpsApi = nock(httpsHost); const path = '/'; const responseBody = 'Illegitimi non carborundum'; const JSONResponseBody = { body: 'Illegitimi non carborundum' }; const requestBody = { foo: 'bar' }; const header = { 'Content-Type': 'text/html' }; const jsonHeader = { 'Content-Type': 'application/json' }; const postResponseBody = requestBody; function createContext(url, method) { method = method || 'get'; const ctx = context.create(); ctx.req.method(method).baseUrl(url); return ctx; } describe('Request HTTP transport', () => { beforeEach(() => { nock.disableNetConnect(); nock.cleanAll(); api.get(path).reply(200, responseBody, header); httpsApi.get(path).reply(200, responseBody, header); }); afterEach(() => { sandbox.restore(); }); describe('.createRequest', () => { it('makes a GET request', () => { const ctx = createContext(url); const request = new FetchTransport(); return request .execute(ctx) .catch(assert.ifError) .then((ctx) => { assert.equal(ctx.res.statusCode, 200); assert.equal(ctx.res.body, responseBody); }); }); it('makes a GET request with headers', () => { nock.cleanAll(); nock(host, { reqheaders: { test: 'qui curat' } }) .get(path) .reply(200, responseBody, header); const ctx = createContext(url); ctx.req.addHeader('test', 'qui curat'); const request = new FetchTransport(); return request .execute(ctx) .catch(assert.ifError) .then((ctx) => { assert.equal(ctx.res.statusCode, 200); assert.equal(ctx.res.body, responseBody); }); }); it('makes a GET request with query strings', () => { api.get('/?a=1').reply(200, responseBody, header); const ctx = createContext(url); ctx.req.addQuery('a', 1); const request = new FetchTransport(); return request .execute(ctx) .catch(assert.ifError) .then((ctx) => { assert.equal(ctx.res.statusCode, 200); assert.equal(ctx.res.body, responseBody); }); }); it('does not allow adding an empty query string', () => { const ctx = createContext(url); ctx.req.addQuery(); const request = new FetchTransport(); return request .execute(ctx) .catch(assert.ifError) .then((ctx) => { const keys = Object.keys(ctx.req.getQueries()).length; assert.equal(keys, 0); }); }); it('does not allow adding an empty header', () => { const ctx = createContext(url); ctx.req.addHeader(); const request = new FetchTransport(); return request .execute(ctx) .catch(assert.ifError) .then((ctx) => { const keys = Object.keys(ctx.req.getHeaders()).length; assert.equal(keys, 0); }); }); it('makes a POST request with a JSON body', () => { api.post(path, requestBody).reply(201, postResponseBody); const ctx = createContext(url, 'post'); ctx.req.body(requestBody); return new FetchTransport() .execute(ctx) .catch(assert.ifError) .then((ctx) => { assert.equal(ctx.res.statusCode, 201); assert.deepEqual(ctx.res.body, postResponseBody); }); }); it('makes a PUT request with a JSON body', () => { api.put(path, requestBody).reply(201, postResponseBody); const ctx = createContext(url, 'put'); ctx.req.body(requestBody); return new FetchTransport() .execute(ctx) .catch(assert.ifError) .then((ctx) => { assert.equal(ctx.res.statusCode, 201); assert.deepEqual(ctx.res.body, postResponseBody); }); }); it('makes a DELETE request with a JSON body', () => { api.delete(path).reply(204); const ctx = createContext(url, 'delete'); ctx.req.body(requestBody); return new FetchTransport() .execute(ctx) .catch(assert.ifError) .then((ctx) => { assert.equal(ctx.res.statusCode, 204); }); }); it('makes a PATCH request with a JSON body', () => { api.patch(path).reply(204); const ctx = createContext(url, 'patch'); ctx.req.body(requestBody); return new FetchTransport() .execute(ctx) .catch(assert.ifError) .then((ctx) => { assert.equal(ctx.res.statusCode, 204); }); }); it('sets a timeout', () => { nock.cleanAll(); api .get('/') .delay(500) .reply(200, responseBody); const ctx = createContext(url); ctx.req.timeout(20); return new FetchTransport() .execute(ctx) .then(() => { assert.fail('Expected request to timeout'); }) .catch((e) => { assert.ok(e); assert.equal(e.message, 'Request failed for get http://www.example.com/: ESOCKETTIMEDOUT'); }); }); it('sets a default timeout', () => { nock.cleanAll(); api .get('/') .delay(500) .reply(200, responseBody); const ctx = createContext(url); return new FetchTransport({ defaults: { timeout: 50 } }) .execute(ctx) .then(() => { assert.fail('Expected request to timeout'); }) .catch((e) => { assert.ok(e); assert.equal(e.message, 'Request failed for get http://www.example.com/: ESOCKETTIMEDOUT'); }); }); it('sets a default redirect', () => { nock.cleanAll(); api .get('/') .reply(303, '', { Location: `${url}new-path` }); const ctx = createContext(url); return new FetchTransport({ defaults: { redirect: 'manual' } }) .execute(ctx) .then(() => { assert.equal(ctx.res.statusCode, 303); assert.equal(ctx.res.headers.location, `${url}new-path`); }); }); it('enables timing request by default', () => { nock.cleanAll(); api.get('/').reply(200, responseBody); const ctx = createContext(url); return new FetchTransport() .execute(ctx) .then((ctx) => { const timeTaken = ctx.res.elapsedTime; assert.isNumber(timeTaken); }) .catch(assert.ifError); }); it('allows disabling of timing request', () => { nock.cleanAll(); api.get('/').reply(200, responseBody); const ctx = createContext(url); const options = { defaults: { time: false } }; return new FetchTransport(options) .execute(ctx) .then((ctx) => { const timeTaken = ctx.res.elapsedTime; assert.isUndefined(timeTaken); }) .catch(assert.ifError); }); it('sets redirect', () => { nock.cleanAll(); api .get('/') .reply(303, '', { Location: `${url}new-path` }); const ctx = createContext(url); ctx.req.redirect('manual'); return new FetchTransport() .execute(ctx) .then((ctx) => { assert.equal(ctx.res.statusCode, 303); assert.equal(ctx.res.headers.location, `${url}new-path`); }); }); describe('JSON parsing', () => { it('if json default option is passed in as true, parse body as json', () => { nock.cleanAll(); api.get(path).reply(200, JSONResponseBody); const ctx = createContext(url); const options = { defaults: { json: true } }; const fetchTransport = new FetchTransport(options); return fetchTransport .execute(ctx) .catch(assert.ifError) .then(() => { assert.typeOf(ctx.res.body, 'object', 'we have an object'); }); }); it('if json default option is passed in as true, send an accept: application/json header', () => { nock.cleanAll(); nock(host, { reqheaders: { accept: 'application/json' } }) .get(path) .reply(200, responseBody, header); const ctx = createContext(url); const options = { defaults: { json: true } }; const fetchTransport = new FetchTransport(options); return fetchTransport .execute(ctx) .catch(assert.ifError) .then((ctx) => { assert.equal(ctx.res.statusCode, 200); }); }); it('if there is no json default option passed in, but the content type header includes application/json, then parse body as json', () => { nock.cleanAll(); api.get(path).reply(200, JSONResponseBody, jsonHeader); const ctx = createContext(url); const fetchTransport = new FetchTransport(); return fetchTransport .execute(ctx) .catch(assert.ifError) .then(() => { assert.typeOf(ctx.res.body, 'object', 'we have an object'); }); }); it('if there is no json default option passed in, and no content type application/json header, then parse body as text', () => { nock.cleanAll(); api.get(path).reply(200, responseBody); const ctx = createContext(url); const fetchTransport = new FetchTransport(); return fetchTransport .execute(ctx) .catch(assert.ifError) .then(() => { assert.typeOf(ctx.res.body, 'string', 'we have text'); }); }); it('if the context options have json set to true, then parse the body as json', () => { nock.cleanAll(); api.get(path).reply(200, JSONResponseBody); const ctx = createContext(url); const fetchTransport = new FetchTransport(); ctx.opts = { json: true }; return fetchTransport .execute(ctx) .catch(assert.ifError) .then(() => { assert.typeOf(ctx.res.body, 'object', 'we have an object'); }); }); }); describe('HTTP Agent', () => { it('selects httpAgent when protocol is http and agent options have been provided', () => { const ctx = createContext(url); const options = { agentOpts: { keepAlive: true, maxSockets: 1000 } }; const fetchTransport = new FetchTransport(options); const spy = sinon.spy(fetchTransport, '_fetch'); return fetchTransport .execute(ctx) .catch(assert.ifError) .then(() => { sinon.assert.calledWithMatch(spy, url, { agent: { protocol: 'http:', keepAlive: true, maxSockets: 1000 } }); }); }); it('selects httpsAgent when protocol is https and agent options have been provided', () => { const ctx = createContext(httpsUrl); const options = { agentOpts: { keepAlive: true, maxSockets: 1000 } }; const fetchTransport = new FetchTransport(options); const spy = sinon.spy(fetchTransport, '_fetch'); return fetchTransport .execute(ctx) .catch(assert.ifError) .then(() => { sinon.assert.calledWithMatch(spy, httpsUrl, { agent: { protocol: 'https:', keepAlive: true, maxSockets: 1000 } }); }); }); it('selects httpProxyAgent when proxy has been provided', () => { const ctx = createContext(url); const options = { defaults: { proxy: proxyUrl } }; const fetchTransport = new FetchTransport(options); const spy = sinon.spy(fetchTransport, '_fetch'); return fetchTransport .execute(ctx) .catch(assert.ifError) .then(() => { sinon.assert.calledWithMatch(spy, url, { agent: { proxy: new URL(proxyUrl), protocol: 'http:' } }); }); }); it('selects httpProxyAgent when proxy has been provided and applies agent options', () => { const ctx = createContext(url); const options = { agentOpts: { keepAlive: true, maxSockets: 1000 }, defaults: { proxy: proxyUrl } }; const fetchTransport = new FetchTransport(options); const spy = sinon.spy(fetchTransport, '_fetch'); return fetchTransport .execute(ctx) .catch(assert.ifError) .then(() => { sinon.assert.calledWithMatch(spy, url, { agent: { proxy: new URL(proxyUrl), keepAlive: true, maxSockets: 1000 } }); }); }); }); }); });