From 3dfbcbe394e07b07b009bf76389517a5f85aadba Mon Sep 17 00:00:00 2001 From: Marco Tranchino Date: Mon, 29 Jul 2024 11:56:59 +0100 Subject: [PATCH] Use axios or request retry for Apple Pay Merchant validation With this change, we are introducing a new environment variable to check whether the pay-frontend application should use axios for the Apple Pay Merchant Validation. By default the new variable will not exist on any environment, therefore pay-frontend will keep using request retry to operate the Apple Pay Merchant Validation. When the new variable is created and set to true, then pay-frontend will use Axios, with or without the use of HttpsProxyAgent, in order to operate the Apple Pay Merchant Validation. This will allow us to safely deploy this change to all environments including production, and crucially it will allow us to test the Apple Pay Merchant Validation using axios on the Test environment only by creating the new environment variable there and setting it to 'true'. Further work will be needed to remove the old code and the use of request retry once the validation will be successfully operated with axios in all environments. Note that for simplicity the old tests for the Apple Pay Merchant Validation using request retry have been saved in a new file called old-merchant-validation.controller.test.js, so that it will be easy to simply delete this file once the current way using request retry will be fully discontinued. Further information in Jira. https://payments-platform.atlassian.net/browse/PP-12853 --- .secrets.baseline | 28 ++- .../merchant-validation.controller.js | 90 ++++++--- .../merchant-validation.controller.test.js | 1 + ...old-merchant-validation.controller.test.js | 184 ++++++++++++++++++ 4 files changed, 260 insertions(+), 43 deletions(-) create mode 100644 test/controllers/web-payments/apple-pay/old-merchant-validation.controller.test.js diff --git a/.secrets.baseline b/.secrets.baseline index 7fba76be2..4fba8e27e 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -26,9 +26,6 @@ { "name": "GitHubTokenDetector" }, - { - "name": "GitLabTokenDetector" - }, { "name": "HexHighEntropyString", "limit": 3.0 @@ -39,9 +36,6 @@ { "name": "IbmCosHmacDetector" }, - { - "name": "IPPublicDetector" - }, { "name": "JwtTokenDetector" }, @@ -55,15 +49,9 @@ { "name": "NpmDetector" }, - { - "name": "OpenAIDetector" - }, { "name": "PrivateKeyDetector" }, - { - "name": "PypiTokenDetector" - }, { "name": "SendGridDetector" }, @@ -79,9 +67,6 @@ { "name": "StripeDetector" }, - { - "name": "TelegramBotTokenDetector" - }, { "name": "TwilioKeyDetector" } @@ -129,7 +114,7 @@ "filename": "app/controllers/web-payments/apple-pay/merchant-validation.controller.js", "hashed_secret": "1348b145fa1a555461c1b790a2f66614781091e9", "is_verified": false, - "line_number": 18 + "line_number": 20 } ], "test/controllers/web-payments/apple-pay/normalise-apple-pay-payload.test.js": [ @@ -155,6 +140,15 @@ "line_number": 44 } ], + "test/controllers/web-payments/apple-pay/old-merchant-validation.controller.test.js": [ + { + "type": "Private Key", + "filename": "test/controllers/web-payments/apple-pay/old-merchant-validation.controller.test.js", + "hashed_secret": "1348b145fa1a555461c1b790a2f66614781091e9", + "is_verified": false, + "line_number": 69 + } + ], "test/controllers/web-payments/google-pay/normalise-google-pay-payload.test.js": [ { "type": "Base64 High Entropy String", @@ -391,5 +385,5 @@ } ] }, - "generated_at": "2024-07-24T08:52:06Z" + "generated_at": "2024-07-29T10:56:11Z" } diff --git a/app/controllers/web-payments/apple-pay/merchant-validation.controller.js b/app/controllers/web-payments/apple-pay/merchant-validation.controller.js index b8367ec5c..d7b7b09f1 100644 --- a/app/controllers/web-payments/apple-pay/merchant-validation.controller.js +++ b/app/controllers/web-payments/apple-pay/merchant-validation.controller.js @@ -1,11 +1,13 @@ 'use strict' +const request = require('requestretry') // to be removed once axios is in use const logger = require('../../../utils/logger')(__filename) const { getLoggingFields } = require('../../../utils/logging-fields-helper') const axios = require('axios') const https = require('https') const { HttpsProxyAgent } = require('https-proxy-agent') const proxyUrl = process.env.HTTPS_PROXY +const applePayMerchantValidationViaAxios = process.env.APPLE_PAY_MERCHANT_VALIDATION_VIA_AXIOS === 'true' function getCertificateMultiline (cert) { @@ -60,35 +62,71 @@ module.exports = async (req, res) => { const proxyAgent = proxyUrl ? new HttpsProxyAgent(proxyUrl) : null - const options = { - url: url, - method: 'post', - headers: { 'Content-Type': 'application/json' }, - data: { - merchantIdentifier: merchantIdentityVars.merchantIdentifier, - displayName: 'GOV.UK Pay', - initiative: 'web', - initiativeContext: process.env.APPLE_PAY_MERCHANT_DOMAIN - }, - httpsAgent: proxyUrl ? proxyAgent : httpsAgent - } + const options = applePayMerchantValidationViaAxios ? + { + url: url, + method: 'post', + headers: { 'Content-Type': 'application/json' }, + data: { + merchantIdentifier: merchantIdentityVars.merchantIdentifier, + displayName: 'GOV.UK Pay', + initiative: 'web', + initiativeContext: process.env.APPLE_PAY_MERCHANT_DOMAIN + }, + httpsAgent: proxyUrl ? proxyAgent : httpsAgent + } : + { + url: url, + cert: merchantIdentityVars.cert, + key: merchantIdentityVars.key, + method: 'post', + body: { + merchantIdentifier: merchantIdentityVars.merchantIdentifier, + displayName: 'GOV.UK Pay', + initiative: 'web', + initiativeContext: process.env.APPLE_PAY_MERCHANT_DOMAIN + }, + json: true + } - try { - let response + if (applePayMerchantValidationViaAxios) { + logger.info('Generating Apple Pay session via axios') + try { + let response - if (proxyUrl) { - response = await axios(options, httpsAgent) - } else { - response = await axios(options) + if (proxyUrl) { + response = await axios(options, httpsAgent) + } else { + response = await axios(options) + } + logger.info('Apple Pay session successfully generated via axios') + res.status(200).send(response.data) + } catch (error) { + const errorResponseData = error.response ? error.response.data : null + logger.info('Error generating Apple Pay session', { + ...getLoggingFields(req), + error: error, + response: error.response, + data: errorResponseData + }) + logger.info('Apple Pay session via axios failed', errorResponseData ? errorResponseData : 'Apple Pay Error') + res.status(500).send(errorResponseData ? errorResponseData : 'Apple Pay Error') } - res.status(200).send(response.data) - } catch (error) { - logger.info('Error generating Apple Pay session', { - ...getLoggingFields(req), - error: error, - response: error.response, - data: error.response ? error.response.data : null + } else { + logger.info('Generating Apple Pay session via request retry') + request(options, (err, response, body) => { + if (err) { + logger.info('Error generating Apple Pay session', { + ...getLoggingFields(req), + error: err, + response: response, + body: body + }) + logger.info('Apple Pay session via request retry failed', body) + return res.status(500).send(body) + } + logger.info('Apple Pay session successfully generated via request retry') + res.status(200).send(body) }) - res.status(500).send(error.response ? error.response.data : 'Apple Pay Error') } } diff --git a/test/controllers/web-payments/apple-pay/merchant-validation.controller.test.js b/test/controllers/web-payments/apple-pay/merchant-validation.controller.test.js index 0084dc630..bae859538 100644 --- a/test/controllers/web-payments/apple-pay/merchant-validation.controller.test.js +++ b/test/controllers/web-payments/apple-pay/merchant-validation.controller.test.js @@ -35,6 +35,7 @@ describe('Validate with Apple the merchant is legitimate', () => { process.env.STRIPE_APPLE_PAY_MERCHANT_ID = stripeMerchantId process.env.STRIPE_APPLE_PAY_MERCHANT_ID_CERTIFICATE = stripeCertificate process.env.STRIPE_APPLE_PAY_MERCHANT_ID_CERTIFICATE_KEY = stripeKey + process.env.APPLE_PAY_MERCHANT_VALIDATION_VIA_AXIOS = 'true' sendSpy = sinon.spy() res = { diff --git a/test/controllers/web-payments/apple-pay/old-merchant-validation.controller.test.js b/test/controllers/web-payments/apple-pay/old-merchant-validation.controller.test.js new file mode 100644 index 000000000..3ba68ef8a --- /dev/null +++ b/test/controllers/web-payments/apple-pay/old-merchant-validation.controller.test.js @@ -0,0 +1,184 @@ +'use strict' + +const sinon = require('sinon') +const proxyquire = require('proxyquire') + +const merchantDomain = 'www.pymnt.uk' +const worldpayMerchantId = 'worldpay.merchant.id' +const worldpayCertificate = 'A-WORLDPAY-CERTIFICATE' +const worldpayKey = 'A-WORLDPAY-KEY' +const stripeMerchantId = 'stripe.merchant.id' +const stripeCertificate = 'A-STRIPE-CERTIFICATE' +const stripeKey = 'A-STRIPE-KEY' +const url = 'https://fakeapple.url' + +const oldAppleResponse = { status: 200 } +const oldAppleResponseBody = { foo: 'bar' } + +function getControllerWithMocks (requestMock) { + return proxyquire('../../../../app/controllers/web-payments/apple-pay/merchant-validation.controller', { + requestretry: requestMock + }) +} + +describe('Validate with Apple the merchant is legitimate', () => { + let res, sendSpy + + beforeEach(() => { + process.env.APPLE_PAY_MERCHANT_DOMAIN = merchantDomain + process.env.WORLDPAY_APPLE_PAY_MERCHANT_ID = worldpayMerchantId + process.env.WORLDPAY_APPLE_PAY_MERCHANT_ID_CERTIFICATE = worldpayCertificate + process.env.WORLDPAY_APPLE_PAY_MERCHANT_ID_CERTIFICATE_KEY = worldpayKey + process.env.STRIPE_APPLE_PAY_MERCHANT_ID = stripeMerchantId + process.env.STRIPE_APPLE_PAY_MERCHANT_ID_CERTIFICATE = stripeCertificate + process.env.STRIPE_APPLE_PAY_MERCHANT_ID_CERTIFICATE_KEY = stripeKey + process.env.APPLE_PAY_MERCHANT_VALIDATION_VIA_AXIOS = 'false' + + sendSpy = sinon.spy() + res = { + status: sinon.spy(() => ({ send: sendSpy })), + sendStatus: sinon.spy() + } + }) + + it('should return a payload for a Worldpay payment if Merchant is valid', async () => { + const mockRequest = sinon.stub().yields(null, oldAppleResponse, oldAppleResponseBody) + const controller = getControllerWithMocks(mockRequest) + + const req = { + body: { + url, + paymentProvider: 'worldpay' + } + } + await controller(req, res) + + sinon.assert.calledWith(mockRequest, { + url, + body: { + merchantIdentifier: worldpayMerchantId, + displayName: 'GOV.UK Pay', + initiative: 'web', + initiativeContext: merchantDomain + }, + method: 'post', + json: true, + cert: `-----BEGIN CERTIFICATE----- +${worldpayCertificate} +-----END CERTIFICATE-----`, + key: `-----BEGIN PRIVATE KEY----- +${worldpayKey} +-----END PRIVATE KEY-----` + }) + sinon.assert.calledWith(res.status, 200) + sinon.assert.calledWith(sendSpy, oldAppleResponseBody) + }) + + it('should return a payload for a Stripe payment if Merchant is valid', async () => { + const mockRequest = sinon.stub().yields(null, oldAppleResponse, oldAppleResponseBody) + const controller = getControllerWithMocks(mockRequest) + + const req = { + body: { + url, + paymentProvider: 'stripe' + } + } + await controller(req, res) + + sinon.assert.calledWith(mockRequest, { + url, + body: { + merchantIdentifier: stripeMerchantId, + displayName: 'GOV.UK Pay', + initiative: 'web', + initiativeContext: merchantDomain + }, + method: 'post', + json: true, + cert: `-----BEGIN CERTIFICATE----- +${stripeCertificate} +-----END CERTIFICATE-----`, + key: `-----BEGIN PRIVATE KEY----- +${stripeKey} +-----END PRIVATE KEY-----` + }) + sinon.assert.calledWith(res.status, 200) + sinon.assert.calledWith(sendSpy, oldAppleResponseBody) + }) + + it('should return 400 if no url is provided', async () => { + const mockRequest = sinon.stub().yields(null, oldAppleResponse, oldAppleResponseBody) + const controller = getControllerWithMocks(mockRequest) + + const req = { + body: { + paymentProvider: 'worldpay' + } + } + await controller(req, res) + sinon.assert.calledWith(res.sendStatus, 400) + }) + + it('should return a payload for a Sandbox payment if Merchant is valid', async () => { + const mockRequest = sinon.stub().yields(null, oldAppleResponse, oldAppleResponseBody) + const controller = getControllerWithMocks(mockRequest) + + const req = { + body: { + url, + paymentProvider: 'sandbox' + } + } + await controller(req, res) + + sinon.assert.calledWith(mockRequest, { + url, + body: { + merchantIdentifier: worldpayMerchantId, + displayName: 'GOV.UK Pay', + initiative: 'web', + initiativeContext: merchantDomain + }, + method: 'post', + json: true, + cert: `-----BEGIN CERTIFICATE----- +${worldpayCertificate} +-----END CERTIFICATE-----`, + key: `-----BEGIN PRIVATE KEY----- +${worldpayKey} +-----END PRIVATE KEY-----` + }) + sinon.assert.calledWith(res.status, 200) + sinon.assert.calledWith(sendSpy, oldAppleResponseBody) + }) + + it('should return 400 for unexpected payment provider', async () => { + const mockRequest = sinon.stub().yields(null, oldAppleResponse, oldAppleResponseBody) + const controller = getControllerWithMocks(mockRequest) + + const req = { + body: { + url, + paymentProvider: 'mystery' + } + } + await controller(req, res) + sinon.assert.calledWith(res.sendStatus, 400) + }) + + it('should return an error if Apple Pay returns an error', async () => { + const mockRequest = sinon.stub().yields(new Error(), oldAppleResponse, oldAppleResponseBody) + const controller = getControllerWithMocks(mockRequest) + + const req = { + body: { + url, + paymentProvider: 'worldpay' + } + } + await controller(req, res) + sinon.assert.calledWith(res.status, 500) + sinon.assert.calledWith(sendSpy, oldAppleResponseBody) + }) +})