UNPKG

@heroku/http-call

Version:
363 lines (362 loc) 15.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const nock = require("nock"); const querystring = require("node:querystring"); const sinon = require("sinon"); const stripAnsi = require('strip-ansi'); const debug = require('debug'); const http_1 = require("./http"); nock.disableNetConnect(); let api; beforeEach(() => { api = nock('https://api.jdxcode.com'); }); afterEach(() => { api.done(); }); afterEach(() => { nock.cleanAll(); }); describe('HTTP.get()', () => { test('makes a GET request', async () => { api.get('/').reply(200, { message: 'ok' }); const { body } = await http_1.HTTP.get('https://api.jdxcode.com'); expect(body).toEqual({ message: 'ok' }); }); test('makes a GET request', async () => { api.get('/').reply(200, { message: 'ok' }, { 'content-type': 'application/json; charset=UTF-8', }); const { body } = await http_1.HTTP.get('https://api.jdxcode.com'); expect(body).toEqual({ message: 'ok' }); }); test('gets headers', async () => { api.get('/').reply(200, { message: 'ok' }, { myheader: 'ok' }); const { body, headers } = await http_1.HTTP.get('https://api.jdxcode.com'); expect(body).toEqual({ message: 'ok' }); expect(headers).toMatchObject({ myheader: 'ok' }); }); test('can build a new HTTP with defaults', async () => { const MyHTTP = http_1.HTTP.create({ host: 'api.jdxcode.com' }); api.get('/').reply(200, { message: 'ok' }); const { body } = await MyHTTP.get('/'); expect(body).toEqual({ message: 'ok' }); }); test('makes a request to a port', async () => { api = nock('https://api.jdxcode.com:3000'); api.get('/').reply(200, { message: 'ok' }); const { body } = await http_1.HTTP.get('https://api.jdxcode.com:3000'); expect(body).toEqual({ message: 'ok' }); }); test('allows specifying the port', async () => { api = nock('https://api.jdxcode.com:3000'); api.get('/').reply(200, { message: 'ok' }); const { body } = await http_1.HTTP.get('https://api.jdxcode.com', { port: 3000 }); expect(body).toEqual({ message: 'ok' }); }); test('makes a http GET request', async () => { api = nock('http://api.jdxcode.com'); api.get('/').reply(200, { message: 'ok' }); const { body } = await http_1.HTTP.get('http://api.jdxcode.com'); expect(body).toEqual({ message: 'ok' }); }); test('can set default user agent', async () => { http_1.HTTP.defaults.headers = { 'user-agent': 'mynewuseragent' }; api .matchHeader('user-agent', 'mynewuseragent') .get('/') .reply(200, { message: 'ok' }); const { body } = await http_1.HTTP.get('https://api.jdxcode.com/'); expect(body).toEqual({ message: 'ok' }); delete http_1.HTTP.defaults.headers['user-agent']; }); test('can set user agent as a global', async () => { global.httpCall = { userAgent: 'mynewuseragent' }; api .matchHeader('user-agent', 'mynewuseragent') .get('/') .reply(200, { message: 'ok' }); const { body } = await http_1.HTTP.get('https://api.jdxcode.com/'); expect(body).toEqual({ message: 'ok' }); delete global.httpCall; }); test('sets user-agent header', async () => { api .matchHeader('user-agent', `@heroku/http-call/${require('../package.json').version} node-${process.version}`) .get('/') .reply(200, { message: 'ok' }); await http_1.HTTP.get('https://api.jdxcode.com'); }); test('sets custom headers', async () => { api .matchHeader('foo', 'bar') .get('/') .reply(200); const headers = { foo: 'bar' }; await http_1.HTTP.get('https://api.jdxcode.com', { headers }); }); test('does not fail on undefined header', async () => { api.get('/').reply(200); const headers = { foo: undefined }; await http_1.HTTP.get('https://api.jdxcode.com', { headers }); }); describe('wait mocked out', () => { const wait = http_1.HTTP.prototype._wait; beforeAll(() => { http_1.HTTP.prototype._wait = jest.fn(); }); afterAll(() => { http_1.HTTP.prototype._wait = wait; }); test('retries then succeeds', async () => { api.get('/').replyWithError({ message: 'timed out 1', code: 'ETIMEDOUT' }); api.get('/').replyWithError({ message: 'timed out 2', code: 'ETIMEDOUT' }); api.get('/').replyWithError({ message: 'timed out 3', code: 'ETIMEDOUT' }); api.get('/').replyWithError({ message: 'timed out 4', code: 'ETIMEDOUT' }); api.get('/').reply(200, { message: 'foo' }); const { body } = await http_1.HTTP.get('https://api.jdxcode.com'); expect(body).toEqual({ message: 'foo' }); }); test('retries 5 times on ETIMEDOUT', async () => { expect.assertions(1); api.get('/').replyWithError({ message: 'timed out 1', code: 'ETIMEDOUT' }); api.get('/').replyWithError({ message: 'timed out 2', code: 'ETIMEDOUT' }); api.get('/').replyWithError({ message: 'timed out 3', code: 'ETIMEDOUT' }); api.get('/').replyWithError({ message: 'timed out 4', code: 'ETIMEDOUT' }); api.get('/').replyWithError({ message: 'timed out 5', code: 'ETIMEDOUT' }); api.get('/').replyWithError({ message: 'timed out 6', code: 'ETIMEDOUT' }); try { await http_1.HTTP.get('https://api.jdxcode.com'); } catch (error) { expect(error.message).toEqual('timed out 6'); } }); }); test('retries on ENOTFOUND', async () => { api.get('/').replyWithError({ message: 'not found', code: 'ENOTFOUND' }); api.get('/').reply(200, { message: 'foo' }); const { body } = await http_1.HTTP.get('https://api.jdxcode.com'); expect(body).toMatchObject({ message: 'foo' }); }); test('errors on EFOOBAR', async () => { expect.assertions(1); api.get('/').replyWithError({ message: 'oom', code: 'OUT_OF_MEM' }); try { await http_1.HTTP.get('https://api.jdxcode.com'); } catch (error) { expect(error.message).toEqual('oom'); } }); test('displays 404 error', async () => { expect.assertions(2); api.get('/').reply(404, 'oops! not found'); try { await http_1.HTTP.get('https://api.jdxcode.com'); } catch (error) { expect(error.statusCode).toEqual(404); expect(error.message).toEqual(`HTTP Error 404 for GET https://api.jdxcode.com/ oops! not found`); } }); test('displays error message', async () => { expect.assertions(3); api.get('/').reply(404, { message: 'uh oh', otherinfo: [1, 2, 3] }); try { await http_1.HTTP.get('https://api.jdxcode.com'); } catch (error) { expect(error.statusCode).toEqual(404); expect(error.message).toEqual(`HTTP Error 404 for GET https://api.jdxcode.com/ uh oh`); expect(error.body).toMatchObject({ otherinfo: [1, 2, 3] }); } }); test('displays object error', async () => { expect.assertions(3); api.get('/').reply(404, { otherinfo: [1, 2, 3] }); try { await http_1.HTTP.get('https://api.jdxcode.com'); } catch (error) { expect(error.statusCode).toEqual(404); expect(error.message).toEqual(`HTTP Error 404 for GET https://api.jdxcode.com/ { otherinfo: [ 1, 2, 3 ] }`); expect(error.body).toMatchObject({ otherinfo: [1, 2, 3] }); } }); test('follows redirect', async () => { api.get('/foo1').reply(302, undefined, { Location: 'https://api.jdxcode.com/foo2' }); api.get('/foo2').reply(302, undefined, { Location: 'https://api.jdxcode.com/foo3' }); api.get('/foo3').reply(200, { success: true }); await http_1.HTTP.get('https://api.jdxcode.com/foo1'); }); test('follows redirect only 10 times', async () => { api.get('/foo1').reply(302, undefined, { Location: 'https://api.jdxcode.com/foo2' }); api.get('/foo2').reply(302, undefined, { Location: 'https://api.jdxcode.com/foo3' }); api.get('/foo3').reply(302, undefined, { Location: 'https://api.jdxcode.com/foo4' }); api.get('/foo4').reply(302, undefined, { Location: 'https://api.jdxcode.com/foo5' }); api.get('/foo5').reply(302, undefined, { Location: 'https://api.jdxcode.com/foo6' }); api.get('/foo6').reply(302, undefined, { Location: 'https://api.jdxcode.com/foo7' }); api.get('/foo7').reply(302, undefined, { Location: 'https://api.jdxcode.com/foo8' }); api.get('/foo8').reply(302, undefined, { Location: 'https://api.jdxcode.com/foo9' }); api.get('/foo9').reply(302, undefined, { Location: 'https://api.jdxcode.com/foo10' }); api.get('/foo10').reply(302, undefined, { Location: 'https://api.jdxcode.com/foo11' }); api.get('/foo11').reply(302, undefined, { Location: 'https://api.jdxcode.com/foo12' }); expect.assertions(1); try { await http_1.HTTP.get('https://api.jdxcode.com/foo1'); } catch (error) { expect(error.message).toEqual('Redirect loop at https://api.jdxcode.com/foo11'); } }); }); describe('HTTP.post()', () => { test('makes a POST request', async () => { api.post('/', { foo: 'bar' }).reply(200, { message: 'ok' }); const { body } = await http_1.HTTP.post('https://api.jdxcode.com', { body: { foo: 'bar' } }); expect(body).toEqual({ message: 'ok' }); }); test('does not include a body if no body is passed in', async () => { api.post('/').reply(200, { message: 'ok' }); const { body } = await http_1.HTTP.post('https://api.jdxcode.com'); expect(body).toEqual({ message: 'ok' }); }); test('faithfully passes custom-encoded content-types', async () => { const apiEncoded = nock('https://api.jdxcode.com', { reqheaders: { 'Content-Type': 'application/x-www-form-urlencoded', }, }); const body = { karate: 'chop', judo: 'throw', taewkondo: 'kick', jujitsu: 'strangle', }; const options = { headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: querystring.stringify(body), }; apiEncoded.post('/', querystring.stringify(body)).reply(200, { message: 'ok' }); const rsp = await http_1.HTTP.post('https://api.jdxcode.com/', options); expect(rsp.body).toEqual({ message: 'ok' }); }); }); describe('HTTP.parseBody()', () => { let body; let http; beforeEach(() => { body = { karate: 'chop', judo: 'throw', taewkondo: 'kick', jujitsu: 'strangle', }; http = new http_1.HTTP('www.duckduckgo.com', { body }); }); it('sets the Content-Length', () => { expect(http.options.headers['Content-Length']).toEqual(Buffer.byteLength(JSON.stringify(body)).toString()); }); it('sets the Content-Type to JSON when Content-Type is unspecified', () => { expect(http.options.headers['content-type']).toEqual('application/json'); }); it('does not set the Content Type if it already exists', () => { const options = { headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: querystring.stringify(body), }; http = new http_1.HTTP('www.duckduckgo.com', options); expect(http.options.headers['content-type']).toEqual('application/x-www-form-urlencoded'); }); it('resets the value for http.body object', () => { expect(http.body).toBe(undefined); }); it('sets the requestBody to the body contents', () => { expect(http.options.body).toBe(JSON.stringify(body)); }); describe('with next-range header', () => { beforeEach(() => { api .get('/') .reply(206, [1, 2, 3], { 'next-range': '4', }) .get('/') // .matchHeader('range', '4') .reply(206, [4, 5, 6], { 'next-range': '7', }) .get('/') // .matchHeader('range', '7') .reply(206, [7, 8, 9]); }); test('gets next body when next-range is set', async () => { const { body } = await http_1.HTTP.get('https://api.jdxcode.com'); expect(body).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]); }); }); }); describe('debug logs', () => { let debugSpy; beforeEach(() => { debugSpy = sinon.spy(debug, 'log'); debug.enable('*'); }); afterEach(() => { debug.disable('*'); debugSpy.restore(); }); it('redacts authorization header from debug logs', async () => { api.get('/').reply(200, { message: 'ok' }, { authorization: '1234567890' }); await http_1.HTTP.get('https://api.jdxcode.com'); expect(stripAnsi(debugSpy.secondCall.firstArg)).toContain('authorization: \'[REDACTED]\''); }); it('redacts x-addon-sso header from debug logs', async () => { api.get('/').reply(200, { message: 'ok' }, { 'x-addon-sso': '1234567890' }); await http_1.HTTP.get('https://api.jdxcode.com'); expect(stripAnsi(debugSpy.secondCall.firstArg)).toContain('x-addon-sso: \'[REDACTED]\''); }); it('redacts the response from endpoints ending in /sso from debug logs', async () => { api.get('/sso').reply(200, { message: 'ok' }); await http_1.HTTP.get('https://api.jdxcode.com/sso'); expect(stripAnsi(debugSpy.secondCall.firstArg)).toContain('[REDACTED]'); }); }); describe('HTTP.put()', () => { test('makes a PUT request', async () => { api.put('/', { foo: 'bar' }).reply(200, { message: 'ok' }); const { body } = await http_1.HTTP.put('https://api.jdxcode.com', { body: { foo: 'bar' } }); expect(body).toEqual({ message: 'ok' }); }); }); describe('HTTP.patch()', () => { test('makes a PATCH request', async () => { api.patch('/', { foo: 'bar' }).reply(200, { message: 'ok' }); const { body } = await http_1.HTTP.patch('https://api.jdxcode.com', { body: { foo: 'bar' } }); expect(body).toEqual({ message: 'ok' }); }); }); describe('HTTP.delete()', () => { test('makes a DELETE request', async () => { api.delete('/', { foo: 'bar' }).reply(200, { message: 'ok' }); const { body } = await http_1.HTTP.delete('https://api.jdxcode.com', { body: { foo: 'bar' } }); expect(body).toEqual({ message: 'ok' }); }); }); describe('HTTP.stream()', () => { test('streams a response', async () => { api = nock('http://api.jdxcode.com'); api.get('/').reply(200, { message: 'ok' }); const { response } = await http_1.HTTP.stream('http://api.jdxcode.com'); response.setEncoding('utf8'); response.on('data', data => expect(data).toEqual('{"message":"ok"}')); }); });