Skip to content

Commit

Permalink
Use axios or request retry for Apple Pay Merchant validation
Browse files Browse the repository at this point in the history
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
  • Loading branch information
marcotranchino committed Jul 29, 2024
1 parent b088df5 commit 3dfbcbe
Show file tree
Hide file tree
Showing 4 changed files with 260 additions and 43 deletions.
28 changes: 11 additions & 17 deletions .secrets.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,6 @@
{
"name": "GitHubTokenDetector"
},
{
"name": "GitLabTokenDetector"
},
{
"name": "HexHighEntropyString",
"limit": 3.0
Expand All @@ -39,9 +36,6 @@
{
"name": "IbmCosHmacDetector"
},
{
"name": "IPPublicDetector"
},
{
"name": "JwtTokenDetector"
},
Expand All @@ -55,15 +49,9 @@
{
"name": "NpmDetector"
},
{
"name": "OpenAIDetector"
},
{
"name": "PrivateKeyDetector"
},
{
"name": "PypiTokenDetector"
},
{
"name": "SendGridDetector"
},
Expand All @@ -79,9 +67,6 @@
{
"name": "StripeDetector"
},
{
"name": "TelegramBotTokenDetector"
},
{
"name": "TwilioKeyDetector"
}
Expand Down Expand Up @@ -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": [
Expand All @@ -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",
Expand Down Expand Up @@ -391,5 +385,5 @@
}
]
},
"generated_at": "2024-07-24T08:52:06Z"
"generated_at": "2024-07-29T10:56:11Z"
}
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down Expand Up @@ -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')
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
})
})

0 comments on commit 3dfbcbe

Please sign in to comment.