From 60636fd354121527aa63a9e3fb0fe84c3124a61b Mon Sep 17 00:00:00 2001 From: John Shields Date: Fri, 16 Feb 2024 15:39:43 +0000 Subject: [PATCH] =?UTF-8?q?PP-11681=20Update=20=E2=80=98webhooks.client?= =?UTF-8?q?=E2=80=99=20with=20=E2=80=98axios-base-client=E2=80=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Change ‘webhooks.client’ methods to use ‘axios-base-client’. - Refactor call parameters initialisation. - Use ‘async/await’ syntax. - Add pact tests for webhooks messages. - Use baseUrl from options parameter if present. Otherwise use default envvar. --- app/services/clients/webhooks.client.js | 202 ++++++--------- app/services/clients/webhooks.client.test.js | 234 ++++++++++++++++++ .../webhooks-client/webhooks.pact.test.js | 28 +++ test/test.env | 1 + 4 files changed, 342 insertions(+), 123 deletions(-) create mode 100644 app/services/clients/webhooks.client.test.js diff --git a/app/services/clients/webhooks.client.js b/app/services/clients/webhooks.client.js index 489d3cd50f..570a1aa588 100644 --- a/app/services/clients/webhooks.client.js +++ b/app/services/clients/webhooks.client.js @@ -1,6 +1,7 @@ 'use strict' -const baseClient = require('./base-client/base.client') +const { Client } = require('@govuk-pay/pay-js-commons/lib/utils/axios-base-client/axios-base-client') +const { configureClient } = require('./base/config') const urlJoin = require('url-join') const defaultRequestOptions = { @@ -9,125 +10,89 @@ const defaultRequestOptions = { service: 'webhooks' } -function webhook (id, serviceId, gatewayAccountId, options = {}) { - const url = urlJoin('/v1/webhook', id) - const request = { - url, - qs: { - service_id: serviceId, - gateway_account_id: gatewayAccountId - }, - description: 'Get one webhook', - ...defaultRequestOptions, - ...options - } - return baseClient.get(request) +const client = new Client(defaultRequestOptions.service) + +async function webhook (id, serviceId, gatewayAccountId, options = {}) { + const baseUrl = options.baseUrl ? options.baseUrl : defaultRequestOptions.baseUrl + const url = urlJoin(baseUrl, '/v1/webhook', id) + const fullUrl = `${url}?service_id=${serviceId}&gateway_account_id=${gatewayAccountId}` + configureClient(client, fullUrl) + const response = await client.get(fullUrl, 'Get one webhook') + return response.data } -function signingSecret (webhookId, serviceId, gatewayAccountId, options = {}) { - const url = urlJoin('/v1/webhook/', webhookId, '/signing-key') - const request = { - url, - qs: { - service_id: serviceId, - gateway_account_id: gatewayAccountId - }, - description: 'Get a Webhook signing secret', - ...defaultRequestOptions, - ...options - } - return baseClient.get(request) +async function signingSecret (webhookId, serviceId, gatewayAccountId, options = {}) { + const baseUrl = options.baseUrl ? options.baseUrl : defaultRequestOptions.baseUrl + const url = urlJoin(baseUrl, '/v1/webhook/', webhookId, '/signing-key') + const fullUrl = `${url}?service_id=${serviceId}&gateway_account_id=${gatewayAccountId}` + configureClient(client, fullUrl) + const response = await client.get(fullUrl, 'Get a Webhook signing secret') + return response.data } -function resetSigningSecret (webhookId, serviceId, gatewayAccountId, options = {}) { - const url = urlJoin('/v1/webhook/', webhookId, '/signing-key') - const request = { - url, - qs: { - service_id: serviceId, - gateway_account_id: gatewayAccountId - }, - description: 'Reset a Webhook signing secret', - ...defaultRequestOptions, - ...options - } - return baseClient.post(request) +async function resetSigningSecret (webhookId, serviceId, gatewayAccountId, options = {}) { + const baseUrl = options.baseUrl ? options.baseUrl : defaultRequestOptions.baseUrl + const url = urlJoin(baseUrl, '/v1/webhook/', webhookId, '/signing-key') + const fullUrl = `${url}?service_id=${serviceId}&gateway_account_id=${gatewayAccountId}` + configureClient(client, fullUrl) + const response = await client.post(fullUrl, 'Reset a Webhook signing secret') + return response.data } -function webhooks (serviceId, gatewayAccountId, isLive, options = {}) { - const url = '/v1/webhook' - const request = { - url, - qs: { - service_id: serviceId, - gateway_account_id: gatewayAccountId, - live: isLive - }, - description: 'List webhooks for service', - ...defaultRequestOptions, - ...options - } - return baseClient.get(request) +async function webhooks (serviceId, gatewayAccountId, isLive, options = {}) { + const baseUrl = options.baseUrl ? options.baseUrl : defaultRequestOptions.baseUrl + const url = urlJoin(baseUrl, '/v1/webhook') + const fullUrl = `${url}?service_id=${serviceId}&gateway_account_id=${gatewayAccountId}&live=${isLive}` + configureClient(client, fullUrl) + const response = await client.get(fullUrl, 'List webhooks for service') + return response.data } -function message (id, webhookId, options = {}) { - const url = urlJoin('/v1/webhook', webhookId, 'message', id) - const request = { - url, - description: 'Get webhook message', - ...defaultRequestOptions, - ...options - } - return baseClient.get(request) +async function message (id, webhookId, options = {}) { + const baseUrl = options.baseUrl ? options.baseUrl : defaultRequestOptions.baseUrl + const url = urlJoin(baseUrl, '/v1/webhook', webhookId, 'message', id) + configureClient(client, url) + const response = await client.get(url, 'Get webhook message') + return response.data } -function attempts (messageId, webhookId, options = {}) { - const url = urlJoin('/v1/webhook', webhookId, 'message', messageId, 'attempt') - const request = { - url, - description: 'Get webhook message delivery attempts', - ...defaultRequestOptions, - ...options - } - return baseClient.get(request) +async function attempts (messageId, webhookId, options = {}) { + const baseUrl = options.baseUrl ? options.baseUrl : defaultRequestOptions.baseUrl + const url = urlJoin(baseUrl, '/v1/webhook', webhookId, 'message', messageId, 'attempt') + configureClient(client, url) + const response = await client.get(url, 'Get webhook message delivery attempts') + return response.data } -function messages (id, options = {}) { - const url = urlJoin('/v1/webhook', id, 'message') - const request = { - url, - qs: { - page: options.page, - ...options.status && { status: options.status.toUpperCase() } - }, - description: 'List messages for webhook', - ...defaultRequestOptions, - ...options +async function messages (id, options = {}) { + const baseUrl = options.baseUrl ? options.baseUrl : defaultRequestOptions.baseUrl + const url = urlJoin(baseUrl, '/v1/webhook', id, 'message') + let fullUrl = `${url}?page=${options.page}` + if (options.status) { + fullUrl = `${fullUrl}&status=${options.status.toUpperCase()}` } - return baseClient.get(request) + configureClient(client, fullUrl) + const response = await client.get(fullUrl, 'List messages for webhook') + return response.data } -function createWebhook (serviceId, gatewayAccountId, isLive, options = {}) { - const url = '/v1/webhook' - const request = { - url, - body: { - service_id: serviceId, - gateway_account_id: gatewayAccountId, - live: isLive, - callback_url: options.callback_url, - subscriptions: options.subscriptions, - description: options.description - }, - ...defaultRequestOptions, - ...options, - description: 'Create a Webhook' +async function createWebhook (serviceId, gatewayAccountId, isLive, options = {}) { + const body = { + service_id: serviceId, + gateway_account_id: gatewayAccountId, + live: isLive, + callback_url: options.callback_url, + subscriptions: options.subscriptions, + description: options.description } - return baseClient.post(request) + const baseUrl = options.baseUrl ? options.baseUrl : defaultRequestOptions.baseUrl + const url = urlJoin(baseUrl, '/v1/webhook') + configureClient(client, url) + const response = await client.post(url, body, 'Create a Webhook') + return response.data } -function updateWebhook (id, serviceId, gatewayAccountId, options = {}) { - const url = urlJoin('/v1/webhook', id) +async function updateWebhook (id, serviceId, gatewayAccountId, options = {}) { const paths = [ 'callback_url', 'subscriptions', 'description', 'status' ] const body = [] paths.forEach((path) => { @@ -135,29 +100,20 @@ function updateWebhook (id, serviceId, gatewayAccountId, options = {}) { body.push({ op: 'replace', path, value: options[path] }) } }) - const request = { - url, - qs: { - service_id: serviceId, - gateway_account_id: gatewayAccountId - }, - body, - ...defaultRequestOptions, - ...options, - description: 'Update a Webhook' - } - return baseClient.patch(request) + const baseUrl = options.baseUrl ? options.baseUrl : defaultRequestOptions.baseUrl + const url = urlJoin(baseUrl, '/v1/webhook', id) + const fullUrl = `${url}?service_id=${serviceId}&gateway_account_id=${gatewayAccountId}` + configureClient(client, fullUrl) + const response = await client.patch(fullUrl, body, 'Create a Webhook') + return response.data } -function resendWebhookMessage (webhookId, messageId, options = {}) { - const url = urlJoin('/v1/webhook', webhookId, 'message', messageId, 'resend') - const request = { - url, - ...defaultRequestOptions, - ...options, - description: 'Schedule resending a message' - } - return baseClient.post(request) +async function resendWebhookMessage (webhookId, messageId, options = {}) { + const baseUrl = options.baseUrl ? options.baseUrl : defaultRequestOptions.baseUrl + const url = urlJoin(baseUrl, '/v1/webhook', webhookId, 'message', messageId, 'resend') + configureClient(client, url) + const response = await client.post(url, {}, 'Schedule resending a message') + return response.data } module.exports = { diff --git a/app/services/clients/webhooks.client.test.js b/app/services/clients/webhooks.client.test.js new file mode 100644 index 0000000000..cd526f7c68 --- /dev/null +++ b/app/services/clients/webhooks.client.test.js @@ -0,0 +1,234 @@ +'use strict' + +const sinon = require('sinon') +const proxyquire = require('proxyquire') +const { expect } = require('chai') + +const configureSpy = sinon.spy() + +class MockClient { + configure (baseUrl, options) { + configureSpy(baseUrl, options) + } + + async get (url, description) { + const dataResponse = {} + return Promise.resolve({ data: dataResponse }) + } + + async post (url, description) { + const dataResponse = {} + return Promise.resolve({ data: dataResponse }) + } + + async patch (url, description) { + const dataResponse = {} + return Promise.resolve({ data: dataResponse }) + } +} + +function getWebhooksClient () { + return proxyquire('./webhooks.client', { + '@govuk-pay/pay-js-commons/lib/utils/axios-base-client/axios-base-client': { Client: MockClient } + }) +} + +describe('Webhook client', () => { + describe('webhook function', () => { + beforeEach(() => { + configureSpy.resetHistory() + }) + + it('should use default base URL when base URL has not been set', async () => { + const webhooksClient = getWebhooksClient() + + await webhooksClient.webhook('id', 'a-service-id', 'a-gateway-account-id', {}) + + expect(configureSpy.getCall(0).args[0]).to.equal('http://127.0.0.1:8008/v1/webhook/id?service_id=a-service-id&gateway_account_id=a-gateway-account-id') + }) + + it('should use configured base url', async () => { + const webhooksClient = getWebhooksClient() + + await webhooksClient.webhook('id', 'a-service-id', 'a-gateway-account-id', { baseUrl: 'https://example.com' }) + + expect(configureSpy.getCall(0).args[0]).to.equal('https://example.com/v1/webhook/id?service_id=a-service-id&gateway_account_id=a-gateway-account-id') + }) + }) + + describe('signingSecret function', () => { + beforeEach(() => { + configureSpy.resetHistory() + }) + + it('should use default base URL when base URL has not been set', async () => { + const webhooksClient = getWebhooksClient() + + await webhooksClient.signingSecret('id', 'a-service-id', 'a-gateway-account-id', {}) + + expect(configureSpy.getCall(0).args[0]).to.equal('http://127.0.0.1:8008/v1/webhook/id/signing-key?service_id=a-service-id&gateway_account_id=a-gateway-account-id') + }) + + it('should use configured base url', async () => { + const webhooksClient = getWebhooksClient() + + await webhooksClient.signingSecret('id', 'a-service-id', 'a-gateway-account-id', { baseUrl: 'https://example.com' }) + + expect(configureSpy.getCall(0).args[0]).to.equal('https://example.com/v1/webhook/id/signing-key?service_id=a-service-id&gateway_account_id=a-gateway-account-id') + }) + }) + + describe('webhooks function', () => { + beforeEach(() => { + configureSpy.resetHistory() + }) + + it('should use default base URL when base URL has not been set', async () => { + const webhooksClient = getWebhooksClient() + + await webhooksClient.webhooks('a-service-id', 'a-gateway-account-id', 'isLive', {}) + + expect(configureSpy.getCall(0).args[0]).to.equal('http://127.0.0.1:8008/v1/webhook?service_id=a-service-id&gateway_account_id=a-gateway-account-id&live=isLive') + }) + + it('should use configured base url', async () => { + const webhooksClient = getWebhooksClient() + + await webhooksClient.webhooks('a-service-id', 'a-gateway-account-id', 'isLive', { baseUrl: 'https://example.com' }) + + expect(configureSpy.getCall(0).args[0]).to.equal('https://example.com/v1/webhook?service_id=a-service-id&gateway_account_id=a-gateway-account-id&live=isLive') + }) + }) + + describe('message function', () => { + beforeEach(() => { + configureSpy.resetHistory() + }) + + it('should use default base URL when base URL has not been set', async () => { + const webhooksClient = getWebhooksClient() + + await webhooksClient.message('id', 'a-webhook-id', {}) + + expect(configureSpy.getCall(0).args[0]).to.equal('http://127.0.0.1:8008/v1/webhook/a-webhook-id/message/id') + }) + + it('should use configured base url', async () => { + const webhooksClient = getWebhooksClient() + + await webhooksClient.message('id', 'a-webhook-id', { baseUrl: 'https://example.com' }) + + expect(configureSpy.getCall(0).args[0]).to.equal('https://example.com/v1/webhook/a-webhook-id/message/id') + }) + }) + + describe('attempts function', () => { + beforeEach(() => { + configureSpy.resetHistory() + }) + + it('should use default base URL when base URL has not been set', async () => { + const webhooksClient = getWebhooksClient() + + await webhooksClient.attempts('id', 'a-webhook-id', {}) + + expect(configureSpy.getCall(0).args[0]).to.equal('http://127.0.0.1:8008/v1/webhook/a-webhook-id/message/id/attempt') + }) + + it('should use configured base url', async () => { + const webhooksClient = getWebhooksClient() + + await webhooksClient.attempts('id', 'a-webhook-id', { baseUrl: 'https://example.com' }) + + expect(configureSpy.getCall(0).args[0]).to.equal('https://example.com/v1/webhook/a-webhook-id/message/id/attempt') + }) + }) + + describe('messages function', () => { + beforeEach(() => { + configureSpy.resetHistory() + }) + + it('should use default base URL when base URL has not been set', async () => { + const webhooksClient = getWebhooksClient() + + await webhooksClient.messages('id', { page: 1 }) + + expect(configureSpy.getCall(0).args[0]).to.equal('http://127.0.0.1:8008/v1/webhook/id/message?page=1') + }) + + it('should use configured base url', async () => { + const webhooksClient = getWebhooksClient() + + await webhooksClient.messages('id', { baseUrl: 'https://example.com', page: 1 }) + + expect(configureSpy.getCall(0).args[0]).to.equal('https://example.com/v1/webhook/id/message?page=1') + }) + }) + + describe('createWebhook function', () => { + beforeEach(() => { + configureSpy.resetHistory() + }) + + it('should use default base URL when base URL has not been set', async () => { + const webhooksClient = getWebhooksClient() + + await webhooksClient.createWebhook('a-service-id', 'a-gateway-account-id', 'isLive', {}) + + expect(configureSpy.getCall(0).args[0]).to.equal('http://127.0.0.1:8008/v1/webhook') + }) + + it('should use configured base url', async () => { + const webhooksClient = getWebhooksClient() + + await webhooksClient.createWebhook('a-service-id', 'a-gateway-account-id', 'isLive', { baseUrl: 'https://example.com' }) + + expect(configureSpy.getCall(0).args[0]).to.equal('https://example.com/v1/webhook') + }) + }) + + describe('updateWebhook function', () => { + beforeEach(() => { + configureSpy.resetHistory() + }) + + it('should use default base URL when base URL has not been set', async () => { + const webhooksClient = getWebhooksClient() + + await webhooksClient.updateWebhook('id', 'a-service-id', 'a-gateway-account-id', {}) + + expect(configureSpy.getCall(0).args[0]).to.equal('http://127.0.0.1:8008/v1/webhook/id?service_id=a-service-id&gateway_account_id=a-gateway-account-id') + }) + + it('should use configured base url', async () => { + const webhooksClient = getWebhooksClient() + + await webhooksClient.updateWebhook('id', 'a-service-id', 'a-gateway-account-id', { baseUrl: 'https://example.com' }) + + expect(configureSpy.getCall(0).args[0]).to.equal('https://example.com/v1/webhook/id?service_id=a-service-id&gateway_account_id=a-gateway-account-id') + }) + }) + + describe('resendWebhookMessage function', () => { + beforeEach(() => { + configureSpy.resetHistory() + }) + + it('should use default base URL when base URL has not been set', async () => { + const webhooksClient = getWebhooksClient() + + await webhooksClient.resendWebhookMessage('a-webhook-id', 'a-message-id', {}) + + expect(configureSpy.getCall(0).args[0]).to.equal('http://127.0.0.1:8008/v1/webhook/a-webhook-id/message/a-message-id/resend') + }) + + it('should use configured base url', async () => { + const webhooksClient = getWebhooksClient() + + await webhooksClient.resendWebhookMessage('a-webhook-id', 'a-message-id', { baseUrl: 'https://example.com' }) + + expect(configureSpy.getCall(0).args[0]).to.equal('https://example.com/v1/webhook/a-webhook-id/message/a-message-id/resend') + }) + }) +}) diff --git a/test/pact/webhooks-client/webhooks.pact.test.js b/test/pact/webhooks-client/webhooks.pact.test.js index 64fbfd41db..40637ad5d0 100644 --- a/test/pact/webhooks-client/webhooks.pact.test.js +++ b/test/pact/webhooks-client/webhooks.pact.test.js @@ -27,6 +27,8 @@ const serviceId = 'an-external-service-id' const gatewayAccountId = 'an-external-account-id' const isLive = true const webhookId = 'an-external-webhook-id' +const status = 'FAILED' +const page = 1 describe('webhooks client', function () { let webhooksUrl @@ -97,4 +99,30 @@ describe('webhooks client', function () { }) }) }) + + describe('messages', () => { + before(() => { + return provider.addInteraction( + new PactInteractionBuilder(`/v1/webhook/${serviceId}/message`) + .withQuery('page', page) + .withQuery('status', status) + .withUponReceiving('a valid list of messages for webhook') + .withState('webhooks exist for given service id') + .withMethod('GET') + .withStatusCode(200) + .withResponseBody(pactify(webhookFixtures.webhooksListResponse([{ external_id: webhookId }]))) + .build() + ) + }) + + afterEach(() => provider.verify()) + + it('should get list of messages for webhook for a given service', () => { + return webhooksClient.messages(serviceId, { baseUrl: webhooksUrl, page, status }) + .then((response) => { + // asserts that the client has correctly formatted the request to match the stubbed fixture provider + expect(response[0].external_id).to.equal(webhookId) + }) + }) + }) }) diff --git a/test/test.env b/test/test.env index 6e8da07bf0..74feef3fca 100644 --- a/test/test.env +++ b/test/test.env @@ -4,6 +4,7 @@ PUBLIC_AUTH_URL=http://127.0.0.1:8003/v1/frontend/auth ADMINUSERS_URL=http://127.0.0.1:8002 PRODUCTS_URL=http://127.0.0.1:8004 LEDGER_URL=http://127.0.0.1:8006 +WEBHOOKS_URL=http://127.0.0.1:8008 DISABLE_INTERNAL_HTTPS=true COOKIE_MAX_AGE=10800000 SESSION_ENCRYPTION_KEY=naskjwefvwei72rjkwfmjwfi72rfkjwefmjwefiuwefjkbwfiu24fmjbwfk