diff --git a/app/lib/request.lib.js b/app/lib/request.lib.js index e3d72eac5b..939aec2731 100644 --- a/app/lib/request.lib.js +++ b/app/lib/request.lib.js @@ -34,6 +34,10 @@ async function get (url, additionalOptions = {}) { return await _sendRequest('get', url, additionalOptions) } +async function patch (url, additionalOptions = {}) { + return await _sendRequest('patch', url, additionalOptions) +} + async function post (url, additionalOptions = {}) { return await _sendRequest('post', url, additionalOptions) } @@ -162,5 +166,6 @@ function _requestOptions (additionalOptions) { module.exports = { get, + patch, post } diff --git a/test/lib/request.lib.test.js b/test/lib/request.lib.test.js index 5b4becffd6..199bed83e7 100644 --- a/test/lib/request.lib.test.js +++ b/test/lib/request.lib.test.js @@ -228,6 +228,201 @@ describe('RequestLib', () => { }) }) + describe('#patch()', () => { + describe('When a request succeeds', () => { + beforeEach(() => { + Nock(testDomain).patch('/').reply(200, { data: 'hello world' }) + }) + + describe('the result it returns', () => { + it("has a 'succeeded' property marked as true", async () => { + const result = await RequestLib.patch(testDomain) + + expect(result.succeeded).to.be.true() + }) + + it("has a 'response' property containing the web response", async () => { + const result = await RequestLib.patch(testDomain) + + expect(result.response).to.exist() + expect(result.response.statusCode).to.equal(200) + expect(result.response.body).to.equal('{"data":"hello world"}') + }) + }) + }) + + describe('When a request fails', () => { + describe('because the response was a 500', () => { + beforeEach(() => { + Nock(testDomain).patch('/').reply(500, { data: 'hello world' }) + }) + + it('logs the failure', async () => { + await RequestLib.patch(testDomain) + + const logDataArg = notifierStub.omg.args[0][1] + + expect(notifierStub.omg.calledWith('PATCH request failed')).to.be.true() + expect(logDataArg.method).to.equal('PATCH') + expect(logDataArg.url).to.equal('http://example.com') + expect(logDataArg.additionalOptions).to.equal({}) + expect(logDataArg.result.succeeded).to.be.false() + expect(logDataArg.result.response).to.equal({ + statusCode: 500, + body: '{"data":"hello world"}' + }) + }) + + describe('the result it returns', () => { + it("has a 'succeeded' property marked as false", async () => { + const result = await RequestLib.patch(testDomain) + + expect(result.succeeded).to.be.false() + }) + + it("has a 'response' property containing the web response", async () => { + const result = await RequestLib.patch(testDomain) + + expect(result.response).to.exist() + expect(result.response.statusCode).to.equal(500) + expect(result.response.body).to.equal('{"data":"hello world"}') + }) + }) + }) + + describe('because there was a network issue', () => { + beforeEach(() => { + Nock(testDomain).patch('/').replyWithError({ code: 'ECONNRESET' }) + }) + + it('logs and records the error', async () => { + await RequestLib.patch(testDomain) + + const logDataArg = notifierStub.omfg.args[0][1] + + expect(notifierStub.omfg.calledWith('PATCH request errored')).to.be.true() + expect(logDataArg.method).to.equal('PATCH') + expect(logDataArg.url).to.equal('http://example.com') + expect(logDataArg.additionalOptions).to.equal({}) + expect(logDataArg.result.succeeded).to.be.false() + expect(logDataArg.result.response).to.be.an.error() + }) + + describe('the result it returns', () => { + it("has a 'succeeded' property marked as false", async () => { + const result = await RequestLib.patch(testDomain) + + expect(result.succeeded).to.be.false() + }) + + it("has a 'response' property containing the error", async () => { + const result = await RequestLib.patch(testDomain) + + expect(result.response).to.exist() + expect(result.response).to.be.an.error() + expect(result.response.code).to.equal('ECONNRESET') + }) + }) + }) + + describe('because request timed out', () => { + beforeEach(async () => { + // Set the timeout value to 50ms for these tests + Sinon.replace(requestConfig, 'timeout', 50) + }) + + // Because of the fake delay in this test, Lab will timeout (by default tests have 2 seconds to finish). So, we + // have to override the timeout for this specific test to all it to complete + describe('and all retries fail', { timeout: 5000 }, () => { + beforeEach(async () => { + Nock(testDomain) + .patch(() => true) + .delay(100) + .reply(200, { data: 'hello world' }) + .persist() + }) + + describe('the result it returns', () => { + it("has a 'succeeded' property marked as false", async () => { + const result = await RequestLib.patch(testDomain) + + expect(result.succeeded).to.be.false() + }) + + it("has a 'response' property containing the error", async () => { + const result = await RequestLib.patch(testDomain) + + expect(result.response).to.exist() + expect(result.response).to.be.an.error() + expect(result.response.code).to.equal('ETIMEDOUT') + }) + }) + }) + + describe('and a retry succeeds', () => { + beforeEach(async () => { + // The first response will time out, the second response will return OK + Nock(testDomain) + .patch(() => true) + .delay(100) + .reply(200, { data: 'hello world' }) + .patch(() => true) + .reply(200, { data: 'delayed hello world' }) + }) + + describe('the result it returns', () => { + it("has a 'succeeded' property marked as true", async () => { + const result = await RequestLib.patch(testDomain) + + expect(result.succeeded).to.be.true() + }) + + it("has a 'response' property containing the web response", async () => { + const result = await RequestLib.patch(testDomain) + + expect(result.response).to.exist() + expect(result.response.statusCode).to.equal(200) + expect(result.response.body).to.equal('{"data":"delayed hello world"}') + }) + }) + }) + }) + }) + + describe('When I provide additional options', () => { + describe('and they expand the default options', () => { + beforeEach(() => { + Nock(testDomain).patch('/').reply(200, { data: 'hello world' }) + }) + + it('adds them to the options used when making the request', async () => { + // We tell Got to return the response as json rather than text. We can confirm the option has been applied by + // checking the result.response.body below. + const options = { responseType: 'json' } + const result = await RequestLib.patch(testDomain, options) + + expect(result.succeeded).to.be.true() + expect(result.response.body).to.equal({ data: 'hello world' }) + }) + }) + + describe('and they replace a default option', () => { + beforeEach(() => { + Nock(testDomain).patch('/').reply(500) + }) + + it('uses them in the options used when making the request', async () => { + // We tell Got throw an error if the response is not 2xx or 3xx + const options = { throwHttpErrors: true } + const result = await RequestLib.patch(testDomain, options) + + expect(result.succeeded).to.be.false() + expect(result.response).to.be.an.error() + }) + }) + }) + }) + describe('#post()', () => { describe('When a request succeeds', () => { beforeEach(() => {