From 8f7886a8289a2413f8f75c34707e8d43882e5c06 Mon Sep 17 00:00:00 2001 From: "ola.tomoloju" Date: Fri, 20 Oct 2023 11:57:57 +0100 Subject: [PATCH 01/18] PP-10451 Handle Selfservice 504 on Ledger Gateway Timeout Code change is done, but the entire historic test to do with transaction listing will need converting --- .../transaction-list.controller.it.test.js | 11 +++++++++++ .../transactions/transaction-list.controller.js | 8 ++++++-- app/services/transaction.service.js | 9 ++++++++- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/app/controllers/transactions/transaction-list.controller.it.test.js b/app/controllers/transactions/transaction-list.controller.it.test.js index 900b54c82f..063d224f23 100644 --- a/app/controllers/transactions/transaction-list.controller.it.test.js +++ b/app/controllers/transactions/transaction-list.controller.it.test.js @@ -70,6 +70,17 @@ describe('The /transactions endpoint', () => { sinon.assert.calledWith(next, expectedError) }) + it('should show a gateway timeout message while retrieving the list of transactions', async () => { + ledgerMockResponds(504, + { 'message': 'Gateway Timeout' }, + ledgerSearchParameters) + + await transactionListController(req, res, next) + const expectedError = sinon.match.instanceOf(Error) + .and(sinon.match.has('message', 'Your request has timed out. Please apply more filters and try again')) + sinon.assert.calledWith(next, expectedError) + }) + it('should show internal error message if any error happens while retrieving the list of transactions', async () => { // No ledgerMock defined on purpose to mock a network failure diff --git a/app/controllers/transactions/transaction-list.controller.js b/app/controllers/transactions/transaction-list.controller.js index 7934b470d0..cf8ed9b2c5 100644 --- a/app/controllers/transactions/transaction-list.controller.js +++ b/app/controllers/transactions/transaction-list.controller.js @@ -31,8 +31,12 @@ module.exports = async function showTransactionList (req, res, next) { transactionService.search([accountId], filters.result), client.getAllCardTypes() ]) - } catch (err) { - return next(new Error('Unable to retrieve list of transactions or card types')) + } catch (error) { + if (error.message === 'GATEWAY_TIMED_OUT') { + return next(new Error('Your request has timed out. Please apply more filters and try again')) + } else { + return next(new Error('Unable to retrieve list of transactions or card types')) + } } const transactionsDownloadLink = formatAccountPathsFor(router.paths.account.transactions.download, req.account.external_id) diff --git a/app/services/transaction.service.js b/app/services/transaction.service.js index 3e942162b2..644d676b46 100644 --- a/app/services/transaction.service.js +++ b/app/services/transaction.service.js @@ -25,7 +25,7 @@ const searchLedger = async function searchLedger (gatewayAccountIds = [], filter const transactions = await Ledger.transactions(gatewayAccountIds, filters) return transactions } catch (error) { - throw new Error('GET_FAILED') + throw getStatusCodeForFailedSearch(error) } } @@ -148,6 +148,13 @@ function getStatusCodeForError (err, response) { return status } +function getStatusCodeForFailedSearch (err, response) { + let status = 'GET_FAILED' + const code = (response || {}).statusCode || (err || {}).errorCode + if (code === 504) status = 'GATEWAY_TIMED_OUT' + return new Error(status) +} + module.exports = { search: searchLedger, csvSearchUrl, From 8a38f4b6f9616759f53d7d0292c34cc587a295ce Mon Sep 17 00:00:00 2001 From: "ola.tomoloju" Date: Tue, 24 Oct 2023 12:36:41 +0100 Subject: [PATCH 02/18] PP-10451 Handling Gateway timeout error in the self-service front-end, when thrown from the Ledger service during search. Rewriting Nock based 500 and 504 integration tests in Cypress. --- .../transaction-list.controller.it.test.js | 20 ---- app/middleware/error-handler.js | 11 ++ .../transactions/transaction-search.cy.js | 110 ++++++++++++++++++ test/cypress/stubs/transaction-stubs.js | 17 ++- 4 files changed, 137 insertions(+), 21 deletions(-) diff --git a/app/controllers/transactions/transaction-list.controller.it.test.js b/app/controllers/transactions/transaction-list.controller.it.test.js index 063d224f23..619c0fa78d 100644 --- a/app/controllers/transactions/transaction-list.controller.it.test.js +++ b/app/controllers/transactions/transaction-list.controller.it.test.js @@ -61,26 +61,6 @@ describe('The /transactions endpoint', () => { sinon.assert.calledWith(next, expectedError) }) - it('should show a generic error message on a ledger service error while retrieving the list of transactions', async () => { - ledgerMockResponds(500, { 'message': 'some error from connector' }, ledgerSearchParameters) - - await transactionListController(req, res, next) - const expectedError = sinon.match.instanceOf(Error) - .and(sinon.match.has('message', 'Unable to retrieve list of transactions or card types')) - sinon.assert.calledWith(next, expectedError) - }) - - it('should show a gateway timeout message while retrieving the list of transactions', async () => { - ledgerMockResponds(504, - { 'message': 'Gateway Timeout' }, - ledgerSearchParameters) - - await transactionListController(req, res, next) - const expectedError = sinon.match.instanceOf(Error) - .and(sinon.match.has('message', 'Your request has timed out. Please apply more filters and try again')) - sinon.assert.calledWith(next, expectedError) - }) - it('should show internal error message if any error happens while retrieving the list of transactions', async () => { // No ledgerMock defined on purpose to mock a network failure diff --git a/app/middleware/error-handler.js b/app/middleware/error-handler.js index 6742a6c5e9..d1870b2b86 100644 --- a/app/middleware/error-handler.js +++ b/app/middleware/error-handler.js @@ -95,6 +95,17 @@ module.exports = function errorHandler (err, req, res, next) { stack: err.stack }) } + + if (err && err.message === 'Your request has timed out. Please apply more filters and try again') { + logger.info('Gateway Time out Error occurred on Transactions Search Page. Rendering error page') + return renderErrorView(req, res, err.message, 504) + } + + if (err && err.message === 'Unable to retrieve list of transactions or card types') { + logger.info('General Error occurred on Transactions Search Page. Rendering error page') + return renderErrorView(req, res, err.message, 500) + } + Sentry.captureException(err) renderErrorView(req, res, 'There is a problem with the payments platform. Please contact the support team.', 500) } diff --git a/test/cypress/integration/transactions/transaction-search.cy.js b/test/cypress/integration/transactions/transaction-search.cy.js index 92f42ee66f..1d94561aaa 100644 --- a/test/cypress/integration/transactions/transaction-search.cy.js +++ b/test/cypress/integration/transactions/transaction-search.cy.js @@ -461,4 +461,114 @@ describe('Transactions List', () => { cy.get('#download-transactions-link').should('exist') }) }) + + describe('Should display error pages on search failures ', () => { + it.only('should display a generic error page, if a 500 error response is returned', () => { + cy.task('setupStubs', [ + ...sharedStubs(), + transactionsStubs.getLedgerTransactionsSuccess({ gatewayAccountId, transactions: unfilteredTransactions }) + ]) + cy.visit(transactionsUrl, { failOnStatusCode: false }) + + // 1. Filtering FROM + // Ensure both the date/time pickers aren't showing + cy.get('.datepicker').should('not.exist') + cy.get('.ui-timepicker-wrapper').should('not.exist') + + // Fill in a from date + cy.get('#fromDate').type('03/5/2018') + + // Fill in a from time + cy.get('#fromTime').type('01:00:00') + + // 2. Filtering TO + + // Fill in a to date + cy.get('#toDate').type('03/5/2023') + + // Fill in a to time + cy.get('#toTime').type('01:00:00') + + cy.task('clearStubs') + + cy.task('setupStubs', [ + ...sharedStubs(), + transactionsStubs.getLedgerTransactionsFailure( + { + account_id: gatewayAccountId, + limit_total: 'true', + limit_total_size: '5001', + from_date: '2018-05-03T00:00:00.000Z', + to_date: '2023-05-03T00:00:01.000Z', + page: '1', + display_size: '100' + }, + 500) + ]) + + // Click the filter button + cy.get('#filter').click() + + // Ensure that transaction list is not displayed + cy.get('#transactions-list tbody').should('not.exist') + + // Ensure a generic error message is displayed + cy.get('h1').contains('An error occurred') + cy.get('#errorMsg').contains('Unable to retrieve list of transactions or card types') + }) + + it('should display a gateway timeout error page, if a 504 error response is returned', () => { + cy.task('setupStubs', [ + ...sharedStubs(), + transactionsStubs.getLedgerTransactionsSuccess({ gatewayAccountId, transactions: unfilteredTransactions }) + ]) + cy.visit(transactionsUrl, { failOnStatusCode: false }) + + // 1. Filtering FROM + // Ensure both the date/time pickers aren't showing + cy.get('.datepicker').should('not.exist') + cy.get('.ui-timepicker-wrapper').should('not.exist') + + // Fill in a from date + cy.get('#fromDate').type('03/5/2018') + + // Fill in a from time + cy.get('#fromTime').type('01:00:00') + + // 2. Filtering TO + + // Fill in a to date + cy.get('#toDate').type('03/5/2023') + + // Fill in a to time + cy.get('#toTime').type('01:00:00') + + cy.task('clearStubs') + + cy.task('setupStubs', [ + ...sharedStubs(), + transactionsStubs.getLedgerTransactionsFailure( + { + account_id: gatewayAccountId, + limit_total: 'true', + limit_total_size: '5001', + from_date: '2018-05-03T00:00:00.000Z', + to_date: '2023-05-03T00:00:01.000Z', + page: '1', + display_size: '100' + }, + 504) + ]) + + // Click the filter button + cy.get('#filter').click() + + // Ensure that transaction list is not displayed + cy.get('#transactions-list tbody').should('not.exist') + + // Ensure a gateway timeout error message is displayed + cy.get('h1').contains('An error occurred') + cy.get('#errorMsg').contains('Your request has timed out. Please apply more filters and try again') + }) + }) }) diff --git a/test/cypress/stubs/transaction-stubs.js b/test/cypress/stubs/transaction-stubs.js index 228c93ee44..e9378653b7 100644 --- a/test/cypress/stubs/transaction-stubs.js +++ b/test/cypress/stubs/transaction-stubs.js @@ -91,6 +91,20 @@ function getTransactionsSummarySuccess (opts) { }) } +function getLedgerTransactionsFailure (opts, responseCode) { + const path = `/v1/transaction` + return stubBuilder('GET', path, responseCode, { + query: { + account_id: opts.account_id, + limit_total: opts.limit_total, + limit_total_size: opts.limit_total_size, + from_date: opts.from_date, + to_date: opts.to_date, + page: opts.page, + display_size: opts.display_size + } }) +} + module.exports = { getLedgerEventsSuccess, getLedgerTransactionSuccess, @@ -98,5 +112,6 @@ module.exports = { getLedgerDisputeTransactionsSuccess, postRefundSuccess, postRefundAmountNotAvailable, - getTransactionsSummarySuccess + getTransactionsSummarySuccess, + getLedgerTransactionsFailure } From 122fa7c65ee5983656ba40094774c07b4e6c806f Mon Sep 17 00:00:00 2001 From: "ola.tomoloju" Date: Tue, 24 Oct 2023 12:50:05 +0100 Subject: [PATCH 03/18] PP-10451 Handle gateway timeout error during transaction search. Convert Nock 500 and 504 Integration Test scenarios to Cypress Tests --- .../transaction-list.controller.js | 6 +----- app/errors.js | 20 ++++++++++++++++++- app/middleware/error-handler.js | 6 +++--- app/services/transaction.service.js | 16 ++++++++------- .../demo-payment/mock-cards-stripe.cy.js | 6 +++--- .../transactions/transaction-search.cy.js | 6 +++--- 6 files changed, 38 insertions(+), 22 deletions(-) diff --git a/app/controllers/transactions/transaction-list.controller.js b/app/controllers/transactions/transaction-list.controller.js index cf8ed9b2c5..f01caf4ef8 100644 --- a/app/controllers/transactions/transaction-list.controller.js +++ b/app/controllers/transactions/transaction-list.controller.js @@ -32,11 +32,7 @@ module.exports = async function showTransactionList (req, res, next) { client.getAllCardTypes() ]) } catch (error) { - if (error.message === 'GATEWAY_TIMED_OUT') { - return next(new Error('Your request has timed out. Please apply more filters and try again')) - } else { - return next(new Error('Unable to retrieve list of transactions or card types')) - } + return next(error) } const transactionsDownloadLink = formatAccountPathsFor(router.paths.account.transactions.download, req.account.external_id) diff --git a/app/errors.js b/app/errors.js index a99120d616..ec17b01992 100644 --- a/app/errors.js +++ b/app/errors.js @@ -19,6 +19,22 @@ class DomainError extends Error { } } +class GatewayTimeoutError extends Error { + constructor (message) { + super(message) + this.name = this.constructor.name + Error.captureStackTrace(this, this.constructor) + } +} + +class GenericServerError extends Error { + constructor (message) { + super(message) + this.name = this.constructor.name + Error.captureStackTrace(this, this.constructor) + } +} + /** * Thrown when there is no authentication session for the user. */ @@ -96,5 +112,7 @@ module.exports = { RegistrationSessionMissingError, InvalidRegistationStateError, InvalidConfigurationError, - ExpiredInviteError + ExpiredInviteError, + GatewayTimeoutError, + GenericServerError } diff --git a/app/middleware/error-handler.js b/app/middleware/error-handler.js index d1870b2b86..50ddb140d1 100644 --- a/app/middleware/error-handler.js +++ b/app/middleware/error-handler.js @@ -15,7 +15,7 @@ const { InvalidRegistationStateError, InvalidConfigurationError, ExpiredInviteError, - RESTClientError + RESTClientError, GatewayTimeoutError, GenericServerError } = require('../errors') const paths = require('../paths') const { renderErrorView, response } = require('../utils/response') @@ -96,12 +96,12 @@ module.exports = function errorHandler (err, req, res, next) { }) } - if (err && err.message === 'Your request has timed out. Please apply more filters and try again') { + if (err instanceof GatewayTimeoutError) { logger.info('Gateway Time out Error occurred on Transactions Search Page. Rendering error page') return renderErrorView(req, res, err.message, 504) } - if (err && err.message === 'Unable to retrieve list of transactions or card types') { + if (err instanceof GenericServerError) { logger.info('General Error occurred on Transactions Search Page. Rendering error page') return renderErrorView(req, res, err.message, 500) } diff --git a/app/services/transaction.service.js b/app/services/transaction.service.js index 644d676b46..0d45c243da 100644 --- a/app/services/transaction.service.js +++ b/app/services/transaction.service.js @@ -11,6 +11,7 @@ const getQueryStringForParams = require('../utils/get-query-string-for-params') const userService = require('../services/user.service') const transactionView = require('../utils/transaction-view') const errorIdentifier = require('../models/error-identifier') +const { GatewayTimeoutError, GenericServerError } = require('../errors') const connector = new ConnectorClient(process.env.CONNECTOR_URL) @@ -22,10 +23,9 @@ const connectorRefundFailureReasons = { const searchLedger = async function searchLedger (gatewayAccountIds = [], filters) { try { - const transactions = await Ledger.transactions(gatewayAccountIds, filters) - return transactions + return await Ledger.transactions(gatewayAccountIds, filters) } catch (error) { - throw getStatusCodeForFailedSearch(error) + throw handleErrorForFailedSearch(error) } } @@ -148,11 +148,13 @@ function getStatusCodeForError (err, response) { return status } -function getStatusCodeForFailedSearch (err, response) { - let status = 'GET_FAILED' +function handleErrorForFailedSearch (err, response) { const code = (response || {}).statusCode || (err || {}).errorCode - if (code === 504) status = 'GATEWAY_TIMED_OUT' - return new Error(status) + if (code === 504) { + return new GatewayTimeoutError('Your request has timed out. Please apply more filters and try again') + } else { + return new GenericServerError('Unable to retrieve list of transactions or card types') + } } module.exports = { diff --git a/test/cypress/integration/demo-payment/mock-cards-stripe.cy.js b/test/cypress/integration/demo-payment/mock-cards-stripe.cy.js index e6984ce97c..72206035e6 100644 --- a/test/cypress/integration/demo-payment/mock-cards-stripe.cy.js +++ b/test/cypress/integration/demo-payment/mock-cards-stripe.cy.js @@ -14,17 +14,17 @@ function setupYourPspStubs (opts = {}) { gatewayAccountId, gatewayAccountExternalId, type: 'test', - paymentProvider: 'stripe', + paymentProvider: 'stripe' }) const stripeAccountSetup = stripeAccountSetupStubs.getGatewayAccountStripeSetupSuccess({ - gatewayAccountId, + gatewayAccountId }) const stubs = [ user, gatewayAccountByExternalId, - stripeAccountSetup, + stripeAccountSetup ] cy.task('setupStubs', stubs) diff --git a/test/cypress/integration/transactions/transaction-search.cy.js b/test/cypress/integration/transactions/transaction-search.cy.js index 1d94561aaa..7f95c01b0d 100644 --- a/test/cypress/integration/transactions/transaction-search.cy.js +++ b/test/cypress/integration/transactions/transaction-search.cy.js @@ -462,8 +462,8 @@ describe('Transactions List', () => { }) }) - describe('Should display error pages on search failures ', () => { - it.only('should display a generic error page, if a 500 error response is returned', () => { + describe('Should display an error pages on search failure ', () => { + it('should display a generic error page, if a 500 error response is returned when search is done', () => { cy.task('setupStubs', [ ...sharedStubs(), transactionsStubs.getLedgerTransactionsSuccess({ gatewayAccountId, transactions: unfilteredTransactions }) @@ -517,7 +517,7 @@ describe('Transactions List', () => { cy.get('#errorMsg').contains('Unable to retrieve list of transactions or card types') }) - it('should display a gateway timeout error page, if a 504 error response is returned', () => { + it('should display a gateway timeout error page, if a 504 error response is returned when search is done', () => { cy.task('setupStubs', [ ...sharedStubs(), transactionsStubs.getLedgerTransactionsSuccess({ gatewayAccountId, transactions: unfilteredTransactions }) From c4d013c1ad39085046a3993d832e2dfd57cf97ac Mon Sep 17 00:00:00 2001 From: "ola.tomoloju" Date: Wed, 25 Oct 2023 13:00:20 +0100 Subject: [PATCH 04/18] PP-10451 Handle gateway timeout error during transactions download. Writing test scenario as Cypress Tests --- .../transaction-download.controller.js | 9 +++- app/services/transaction.service.js | 4 +- .../transactions/transaction-search.cy.js | 46 ++++++++++++++++++- test/cypress/stubs/transaction-stubs.js | 10 +++- 4 files changed, 64 insertions(+), 5 deletions(-) diff --git a/app/controllers/transactions/transaction-download.controller.js b/app/controllers/transactions/transaction-download.controller.js index fe43274846..fffa4e1ba0 100644 --- a/app/controllers/transactions/transaction-download.controller.js +++ b/app/controllers/transactions/transaction-download.controller.js @@ -22,7 +22,14 @@ const fetchTransactionCsvWithHeader = function fetchTransactionCsvWithHeader (re transactionService.logCsvFileStreamComplete(timestampStreamStart, filters, [accountId], req.user, false, req.account.type === 'live') res.end() } - const error = () => renderErrorView(req, res, 'Unable to download list of transactions.') + const error = () => { + const code = (res || {}).statusCode + if (code === 504) { + renderErrorView(req, res, 'Your request has timed out. Please apply more filters and try again.') + } else { + renderErrorView(req, res, 'Unable to download list of transactions.') + } + } const client = new Stream(data, complete, error) res.setHeader('Content-disposition', `attachment; filename="${name}"`) diff --git a/app/services/transaction.service.js b/app/services/transaction.service.js index 0d45c243da..cd46da5012 100644 --- a/app/services/transaction.service.js +++ b/app/services/transaction.service.js @@ -151,9 +151,9 @@ function getStatusCodeForError (err, response) { function handleErrorForFailedSearch (err, response) { const code = (response || {}).statusCode || (err || {}).errorCode if (code === 504) { - return new GatewayTimeoutError('Your request has timed out. Please apply more filters and try again') + return new GatewayTimeoutError('Your request has timed out. Please apply more filters and try again.') } else { - return new GenericServerError('Unable to retrieve list of transactions or card types') + return new GenericServerError('Unable to retrieve list of transactions or card types.') } } diff --git a/test/cypress/integration/transactions/transaction-search.cy.js b/test/cypress/integration/transactions/transaction-search.cy.js index 7f95c01b0d..39d8c3f687 100644 --- a/test/cypress/integration/transactions/transaction-search.cy.js +++ b/test/cypress/integration/transactions/transaction-search.cy.js @@ -462,7 +462,7 @@ describe('Transactions List', () => { }) }) - describe('Should display an error pages on search failure ', () => { + describe('Should display relevant error page on search failure ', () => { it('should display a generic error page, if a 500 error response is returned when search is done', () => { cy.task('setupStubs', [ ...sharedStubs(), @@ -571,4 +571,48 @@ describe('Transactions List', () => { cy.get('#errorMsg').contains('Your request has timed out. Please apply more filters and try again') }) }) + + describe('Should display relevant error page, when failure occurs when downloading transactions', () => { + it.only('Should display gateway timeout error page, when failure occurs when downloading transactions', () => { + cy.task('setupStubs', [ + ...sharedStubs(), + transactionsStubs.getLedgerTransactionsSuccess({ + gatewayAccountId, + transactions: unfilteredTransactions, + transactionLength: 1000 + }) + ]) + cy.visit(transactionsUrl) + + // Ensure the transactions list has the right number of items + cy.get('#transactions-list tbody').find('tr').should('have.length', unfilteredTransactions.length) + + // Ensure the values are displayed correctly + cy.get('#transactions-list tbody').first().find('td').eq(1).should('have.text', convertPenceToPoundsFormatted(unfilteredTransactions[0].amount)) + cy.get('#transactions-list tbody').find('tr').eq(1).find('td').eq(1).should('have.text', convertPenceToPoundsFormatted(unfilteredTransactions[1].amount)) + + // Ensure the card fee is displayed correctly + cy.get('#transactions-list tbody').find('tr').eq(2).find('td').eq(1).should('contain', convertPenceToPoundsFormatted(unfilteredTransactions[2].total_amount)).and('contain', '(with card fee)') + + cy.task('clearStubs') + + cy.task('setupStubs', [ + ...sharedStubs(), + transactionsStubs.getLedgerTransactionsDownloadFailure( + { account_id: gatewayAccountId }, + 504) + ]) + + // TODO Results in Cypress Timeout error + // cy.get('#download-transactions-link').click() + + // TODO Assertions + // Ensure that transaction list is not displayed + // cy.get('#transactions-list tbody').should('not.exist') + + // Ensure a gateway timeout error message is displayed + // cy.get('h1').contains('An error occurred') + // cy.get('#errorMsg').contains('Your request has timed out. Please apply more filters and try again') + }) + }) }) diff --git a/test/cypress/stubs/transaction-stubs.js b/test/cypress/stubs/transaction-stubs.js index e9378653b7..11f1103dc0 100644 --- a/test/cypress/stubs/transaction-stubs.js +++ b/test/cypress/stubs/transaction-stubs.js @@ -105,6 +105,13 @@ function getLedgerTransactionsFailure (opts, responseCode) { } }) } +function getLedgerTransactionsDownloadFailure (opts, responseCode) { + const path = `/v1/transaction` + return stubBuilder('GET', path, responseCode, { + query: { + account_id: opts.account_id + } }) +} module.exports = { getLedgerEventsSuccess, getLedgerTransactionSuccess, @@ -113,5 +120,6 @@ module.exports = { postRefundSuccess, postRefundAmountNotAvailable, getTransactionsSummarySuccess, - getLedgerTransactionsFailure + getLedgerTransactionsFailure, + getLedgerTransactionsDownloadFailure } From a2d9566e31878613dcf412cf4fd6e503e9b7337e Mon Sep 17 00:00:00 2001 From: "ola.tomoloju" Date: Thu, 26 Oct 2023 13:22:25 +0100 Subject: [PATCH 05/18] PP-10451 Handle gateway timeout error during transactions download. Writing test scenario as Cypress Tests for 400, 500 and 504 responses and removing nock mocking logic --- .../transaction-list.controller.it.test.js | 32 +------- .../transactions/transaction-search.cy.js | 80 +++++++++++-------- test/cypress/stubs/transaction-stubs.js | 10 +-- 3 files changed, 49 insertions(+), 73 deletions(-) diff --git a/app/controllers/transactions/transaction-list.controller.it.test.js b/app/controllers/transactions/transaction-list.controller.it.test.js index 619c0fa78d..ef810a9e48 100644 --- a/app/controllers/transactions/transaction-list.controller.it.test.js +++ b/app/controllers/transactions/transaction-list.controller.it.test.js @@ -1,28 +1,16 @@ 'use strict' -const nock = require('nock') const sinon = require('sinon') - const paths = require('../../paths') -const getQueryStringForParams = require('../../utils/get-query-string-for-params') const formatAccountPathsFor = require('../../utils/format-account-paths-for') const { validGatewayAccountResponse } = require('../../../test/fixtures/gateway-account.fixtures') const transactionListController = require('./transaction-list.controller') // Setup const gatewayAccountId = '651342' -const ledgerSearchParameters = {} const EXTERNAL_GATEWAY_ACCOUNT_ID = 'an-external-id' -const LEDGER_TRANSACTION_PATH = '/v1/transaction?account_id=' + gatewayAccountId const requestId = 'unique-request-id' const headers = { 'x-request-id': requestId } -const ledgerMock = nock(process.env.LEDGER_URL, { reqheaders: headers }) - -function ledgerMockResponds (code, data, searchParameters) { - const queryString = getQueryStringForParams(searchParameters) - return ledgerMock.get(LEDGER_TRANSACTION_PATH + '&' + queryString) - .reply(code, data) -} describe('The /transactions endpoint', () => { const account = validGatewayAccountResponse( @@ -42,31 +30,17 @@ describe('The /transactions endpoint', () => { const res = {} let next - afterEach(() => { - nock.cleanAll() - }) - beforeEach(() => { next = sinon.spy() }) describe('Error getting transactions', () => { - it('should show error message on a bad request while retrieving the list of transactions', async () => { - const errorMessage = 'There is a problem with the payments platform. Please contact the support team.' - ledgerMockResponds(400, { 'message': errorMessage }, ledgerSearchParameters) - - await transactionListController(req, res, next) - const expectedError = sinon.match.instanceOf(Error) - .and(sinon.match.has('message', 'Unable to retrieve list of transactions or card types')) - sinon.assert.calledWith(next, expectedError) - }) - it('should show internal error message if any error happens while retrieving the list of transactions', async () => { - // No ledgerMock defined on purpose to mock a network failure - + // No mocking defined on purpose to mock a network failure, + // This integration test will cover server errors outside the 500 and 504 defined in the Cypress test await transactionListController(req, res, next) const expectedError = sinon.match.instanceOf(Error) - .and(sinon.match.has('message', 'Unable to retrieve list of transactions or card types')) + .and(sinon.match.has('message', 'Unable to retrieve list of transactions or card types.')) sinon.assert.calledWith(next, expectedError) }) }) diff --git a/test/cypress/integration/transactions/transaction-search.cy.js b/test/cypress/integration/transactions/transaction-search.cy.js index 39d8c3f687..9b6ba2ffb3 100644 --- a/test/cypress/integration/transactions/transaction-search.cy.js +++ b/test/cypress/integration/transactions/transaction-search.cy.js @@ -430,6 +430,7 @@ describe('Transactions List', () => { cy.get('#download-transactions-link').should('have.attr', 'href', `/account/a-valid-external-id/transactions/download?dispute_states=needs_response&dispute_states=under_review`) }) }) + describe('csv download link', () => { it('should not display csv download link when results >5k and no filter applied', function () { cy.task('setupStubs', [ @@ -463,7 +464,7 @@ describe('Transactions List', () => { }) describe('Should display relevant error page on search failure ', () => { - it('should display a generic error page, if a 500 error response is returned when search is done', () => { + it('should show error message on a bad request while retrieving the list of transactions', () => { cy.task('setupStubs', [ ...sharedStubs(), transactionsStubs.getLedgerTransactionsSuccess({ gatewayAccountId, transactions: unfilteredTransactions }) @@ -476,7 +477,7 @@ describe('Transactions List', () => { cy.get('.ui-timepicker-wrapper').should('not.exist') // Fill in a from date - cy.get('#fromDate').type('03/5/2018') + cy.get('#fromDate').type('33/18/2018') // Fill in a from time cy.get('#fromTime').type('01:00:00') @@ -484,7 +485,7 @@ describe('Transactions List', () => { // 2. Filtering TO // Fill in a to date - cy.get('#toDate').type('03/5/2023') + cy.get('#toDate').type('33/23/2023') // Fill in a to time cy.get('#toTime').type('01:00:00') @@ -498,12 +499,12 @@ describe('Transactions List', () => { account_id: gatewayAccountId, limit_total: 'true', limit_total_size: '5001', - from_date: '2018-05-03T00:00:00.000Z', - to_date: '2023-05-03T00:00:01.000Z', + from_date: '2018-18-03T00:00:00.000Z', + to_date: '2023-23-03T00:00:01.000Z', page: '1', display_size: '100' }, - 500) + 400) ]) // Click the filter button @@ -516,8 +517,7 @@ describe('Transactions List', () => { cy.get('h1').contains('An error occurred') cy.get('#errorMsg').contains('Unable to retrieve list of transactions or card types') }) - - it('should display a gateway timeout error page, if a 504 error response is returned when search is done', () => { + it('should display the generic error page, if an internal server error occurs while retrieving the list of transactions', () => { cy.task('setupStubs', [ ...sharedStubs(), transactionsStubs.getLedgerTransactionsSuccess({ gatewayAccountId, transactions: unfilteredTransactions }) @@ -557,7 +557,7 @@ describe('Transactions List', () => { page: '1', display_size: '100' }, - 504) + 500) ]) // Click the filter button @@ -566,53 +566,63 @@ describe('Transactions List', () => { // Ensure that transaction list is not displayed cy.get('#transactions-list tbody').should('not.exist') - // Ensure a gateway timeout error message is displayed + // Ensure a generic error message is displayed cy.get('h1').contains('An error occurred') - cy.get('#errorMsg').contains('Your request has timed out. Please apply more filters and try again') + cy.get('#errorMsg').contains('Unable to retrieve list of transactions or card types') }) - }) - describe('Should display relevant error page, when failure occurs when downloading transactions', () => { - it.only('Should display gateway timeout error page, when failure occurs when downloading transactions', () => { + it('should display the gateway timeout error page, if a gateway timeout error occurs while retrieving the list of transactions', () => { cy.task('setupStubs', [ ...sharedStubs(), - transactionsStubs.getLedgerTransactionsSuccess({ - gatewayAccountId, - transactions: unfilteredTransactions, - transactionLength: 1000 - }) + transactionsStubs.getLedgerTransactionsSuccess({ gatewayAccountId, transactions: unfilteredTransactions }) ]) - cy.visit(transactionsUrl) + cy.visit(transactionsUrl, { failOnStatusCode: false }) - // Ensure the transactions list has the right number of items - cy.get('#transactions-list tbody').find('tr').should('have.length', unfilteredTransactions.length) + // 1. Filtering FROM + // Ensure both the date/time pickers aren't showing + cy.get('.datepicker').should('not.exist') + cy.get('.ui-timepicker-wrapper').should('not.exist') - // Ensure the values are displayed correctly - cy.get('#transactions-list tbody').first().find('td').eq(1).should('have.text', convertPenceToPoundsFormatted(unfilteredTransactions[0].amount)) - cy.get('#transactions-list tbody').find('tr').eq(1).find('td').eq(1).should('have.text', convertPenceToPoundsFormatted(unfilteredTransactions[1].amount)) + // Fill in a from date + cy.get('#fromDate').type('03/5/2018') - // Ensure the card fee is displayed correctly - cy.get('#transactions-list tbody').find('tr').eq(2).find('td').eq(1).should('contain', convertPenceToPoundsFormatted(unfilteredTransactions[2].total_amount)).and('contain', '(with card fee)') + // Fill in a from time + cy.get('#fromTime').type('01:00:00') + + // 2. Filtering TO + + // Fill in a to date + cy.get('#toDate').type('03/5/2023') + + // Fill in a to time + cy.get('#toTime').type('01:00:00') cy.task('clearStubs') cy.task('setupStubs', [ ...sharedStubs(), - transactionsStubs.getLedgerTransactionsDownloadFailure( - { account_id: gatewayAccountId }, + transactionsStubs.getLedgerTransactionsFailure( + { + account_id: gatewayAccountId, + limit_total: 'true', + limit_total_size: '5001', + from_date: '2018-05-03T00:00:00.000Z', + to_date: '2023-05-03T00:00:01.000Z', + page: '1', + display_size: '100' + }, 504) ]) - // TODO Results in Cypress Timeout error - // cy.get('#download-transactions-link').click() + // Click the filter button + cy.get('#filter').click() - // TODO Assertions // Ensure that transaction list is not displayed - // cy.get('#transactions-list tbody').should('not.exist') + cy.get('#transactions-list tbody').should('not.exist') // Ensure a gateway timeout error message is displayed - // cy.get('h1').contains('An error occurred') - // cy.get('#errorMsg').contains('Your request has timed out. Please apply more filters and try again') + cy.get('h1').contains('An error occurred') + cy.get('#errorMsg').contains('Your request has timed out. Please apply more filters and try again') }) }) }) diff --git a/test/cypress/stubs/transaction-stubs.js b/test/cypress/stubs/transaction-stubs.js index 11f1103dc0..e9378653b7 100644 --- a/test/cypress/stubs/transaction-stubs.js +++ b/test/cypress/stubs/transaction-stubs.js @@ -105,13 +105,6 @@ function getLedgerTransactionsFailure (opts, responseCode) { } }) } -function getLedgerTransactionsDownloadFailure (opts, responseCode) { - const path = `/v1/transaction` - return stubBuilder('GET', path, responseCode, { - query: { - account_id: opts.account_id - } }) -} module.exports = { getLedgerEventsSuccess, getLedgerTransactionSuccess, @@ -120,6 +113,5 @@ module.exports = { postRefundSuccess, postRefundAmountNotAvailable, getTransactionsSummarySuccess, - getLedgerTransactionsFailure, - getLedgerTransactionsDownloadFailure + getLedgerTransactionsFailure } From 7bcfcc4b476c53990f1b87ebe66c7bede7869bde Mon Sep 17 00:00:00 2001 From: "ola.tomoloju" Date: Thu, 26 Oct 2023 13:42:13 +0100 Subject: [PATCH 06/18] PP-10451 Handle gateway timeout error during transactions download. Writing transaction search test scenarios as Cypress Tests for 400, 500 and 504 responses and removing nock mock references and associated test scenarios from non-cypress integration tests. --- app/controllers/invite-user.controller.js | 4 +- .../invite-user.controller.test.js | 23 --- .../transaction-download.controller.js | 9 +- .../transaction-list.controller.it.test.js | 41 +---- .../transaction-list.controller.js | 4 +- app/errors.js | 20 ++- app/middleware/error-handler.js | 13 +- app/services/transaction.service.js | 15 +- app/views/team-members/team-member-invite.njk | 4 +- package.json | 2 +- .../demo-payment/mock-cards-stripe.cy.js | 6 +- .../transactions/transaction-search.cy.js | 164 ++++++++++++++++++ test/cypress/stubs/transaction-stubs.js | 17 +- .../invite-users.controller.ft.test.js | 22 +++ test/ui/invite-user.ui.test.js | 4 +- 15 files changed, 267 insertions(+), 81 deletions(-) diff --git a/app/controllers/invite-user.controller.js b/app/controllers/invite-user.controller.js index 44c03ff02a..0c978b3ca4 100644 --- a/app/controllers/invite-user.controller.js +++ b/app/controllers/invite-user.controller.js @@ -60,9 +60,7 @@ async function invite (req, res, next) { lodash.set(req, 'session.pageData', { invitee }) res.redirect(303, formatServicePathsFor(paths.service.teamMembers.invite, externalServiceId)) } else if (!role) { - req.flash('genericError', 'Select the team member’s permission level') - lodash.set(req, 'session.pageData', { invitee }) - res.redirect(303, formatServicePathsFor(paths.service.teamMembers.invite, externalServiceId)) + next(new Error(`Cannot identify role from user input ${roleId}`)) } else { try { await userService.createInviteToJoinService(invitee, senderId, externalServiceId, role.name) diff --git a/app/controllers/invite-user.controller.test.js b/app/controllers/invite-user.controller.test.js index a6995fa695..0c292be909 100644 --- a/app/controllers/invite-user.controller.test.js +++ b/app/controllers/invite-user.controller.test.js @@ -23,27 +23,4 @@ describe('invite user controller', () => { sinon.assert.calledWith(req.flash, 'genericError', 'Enter a valid email address') sinon.assert.calledWith(res.redirect, 303, `/service/${externalServiceId}/team-members/invite`) }) - - it('should error if a role is not recognised', async () => { - const externalServiceId = 'some-external-service-id' - const unknownRoleId = '999' - const req = { - user: { externalId: 'some-ext-id', serviceIds: ['1'] }, - body: { - 'invitee-email': 'valid@example.com', - 'role-input': unknownRoleId - }, - service: { - externalId: externalServiceId - }, - flash: sinon.stub() - } - const res = { - redirect: sinon.stub() - } - - await inviteUserController.invite(req, res) - sinon.assert.calledWith(req.flash, 'genericError', 'Select the team member’s permission level') - sinon.assert.calledWith(res.redirect, 303, `/service/${externalServiceId}/team-members/invite`) - }) }) diff --git a/app/controllers/transactions/transaction-download.controller.js b/app/controllers/transactions/transaction-download.controller.js index fe43274846..fffa4e1ba0 100644 --- a/app/controllers/transactions/transaction-download.controller.js +++ b/app/controllers/transactions/transaction-download.controller.js @@ -22,7 +22,14 @@ const fetchTransactionCsvWithHeader = function fetchTransactionCsvWithHeader (re transactionService.logCsvFileStreamComplete(timestampStreamStart, filters, [accountId], req.user, false, req.account.type === 'live') res.end() } - const error = () => renderErrorView(req, res, 'Unable to download list of transactions.') + const error = () => { + const code = (res || {}).statusCode + if (code === 504) { + renderErrorView(req, res, 'Your request has timed out. Please apply more filters and try again.') + } else { + renderErrorView(req, res, 'Unable to download list of transactions.') + } + } const client = new Stream(data, complete, error) res.setHeader('Content-disposition', `attachment; filename="${name}"`) diff --git a/app/controllers/transactions/transaction-list.controller.it.test.js b/app/controllers/transactions/transaction-list.controller.it.test.js index 900b54c82f..ef810a9e48 100644 --- a/app/controllers/transactions/transaction-list.controller.it.test.js +++ b/app/controllers/transactions/transaction-list.controller.it.test.js @@ -1,28 +1,16 @@ 'use strict' -const nock = require('nock') const sinon = require('sinon') - const paths = require('../../paths') -const getQueryStringForParams = require('../../utils/get-query-string-for-params') const formatAccountPathsFor = require('../../utils/format-account-paths-for') const { validGatewayAccountResponse } = require('../../../test/fixtures/gateway-account.fixtures') const transactionListController = require('./transaction-list.controller') // Setup const gatewayAccountId = '651342' -const ledgerSearchParameters = {} const EXTERNAL_GATEWAY_ACCOUNT_ID = 'an-external-id' -const LEDGER_TRANSACTION_PATH = '/v1/transaction?account_id=' + gatewayAccountId const requestId = 'unique-request-id' const headers = { 'x-request-id': requestId } -const ledgerMock = nock(process.env.LEDGER_URL, { reqheaders: headers }) - -function ledgerMockResponds (code, data, searchParameters) { - const queryString = getQueryStringForParams(searchParameters) - return ledgerMock.get(LEDGER_TRANSACTION_PATH + '&' + queryString) - .reply(code, data) -} describe('The /transactions endpoint', () => { const account = validGatewayAccountResponse( @@ -42,40 +30,17 @@ describe('The /transactions endpoint', () => { const res = {} let next - afterEach(() => { - nock.cleanAll() - }) - beforeEach(() => { next = sinon.spy() }) describe('Error getting transactions', () => { - it('should show error message on a bad request while retrieving the list of transactions', async () => { - const errorMessage = 'There is a problem with the payments platform. Please contact the support team.' - ledgerMockResponds(400, { 'message': errorMessage }, ledgerSearchParameters) - - await transactionListController(req, res, next) - const expectedError = sinon.match.instanceOf(Error) - .and(sinon.match.has('message', 'Unable to retrieve list of transactions or card types')) - sinon.assert.calledWith(next, expectedError) - }) - - it('should show a generic error message on a ledger service error while retrieving the list of transactions', async () => { - ledgerMockResponds(500, { 'message': 'some error from connector' }, ledgerSearchParameters) - - await transactionListController(req, res, next) - const expectedError = sinon.match.instanceOf(Error) - .and(sinon.match.has('message', 'Unable to retrieve list of transactions or card types')) - sinon.assert.calledWith(next, expectedError) - }) - it('should show internal error message if any error happens while retrieving the list of transactions', async () => { - // No ledgerMock defined on purpose to mock a network failure - + // No mocking defined on purpose to mock a network failure, + // This integration test will cover server errors outside the 500 and 504 defined in the Cypress test await transactionListController(req, res, next) const expectedError = sinon.match.instanceOf(Error) - .and(sinon.match.has('message', 'Unable to retrieve list of transactions or card types')) + .and(sinon.match.has('message', 'Unable to retrieve list of transactions or card types.')) sinon.assert.calledWith(next, expectedError) }) }) diff --git a/app/controllers/transactions/transaction-list.controller.js b/app/controllers/transactions/transaction-list.controller.js index 7934b470d0..f01caf4ef8 100644 --- a/app/controllers/transactions/transaction-list.controller.js +++ b/app/controllers/transactions/transaction-list.controller.js @@ -31,8 +31,8 @@ module.exports = async function showTransactionList (req, res, next) { transactionService.search([accountId], filters.result), client.getAllCardTypes() ]) - } catch (err) { - return next(new Error('Unable to retrieve list of transactions or card types')) + } catch (error) { + return next(error) } const transactionsDownloadLink = formatAccountPathsFor(router.paths.account.transactions.download, req.account.external_id) diff --git a/app/errors.js b/app/errors.js index a99120d616..ec17b01992 100644 --- a/app/errors.js +++ b/app/errors.js @@ -19,6 +19,22 @@ class DomainError extends Error { } } +class GatewayTimeoutError extends Error { + constructor (message) { + super(message) + this.name = this.constructor.name + Error.captureStackTrace(this, this.constructor) + } +} + +class GenericServerError extends Error { + constructor (message) { + super(message) + this.name = this.constructor.name + Error.captureStackTrace(this, this.constructor) + } +} + /** * Thrown when there is no authentication session for the user. */ @@ -96,5 +112,7 @@ module.exports = { RegistrationSessionMissingError, InvalidRegistationStateError, InvalidConfigurationError, - ExpiredInviteError + ExpiredInviteError, + GatewayTimeoutError, + GenericServerError } diff --git a/app/middleware/error-handler.js b/app/middleware/error-handler.js index 6742a6c5e9..50ddb140d1 100644 --- a/app/middleware/error-handler.js +++ b/app/middleware/error-handler.js @@ -15,7 +15,7 @@ const { InvalidRegistationStateError, InvalidConfigurationError, ExpiredInviteError, - RESTClientError + RESTClientError, GatewayTimeoutError, GenericServerError } = require('../errors') const paths = require('../paths') const { renderErrorView, response } = require('../utils/response') @@ -95,6 +95,17 @@ module.exports = function errorHandler (err, req, res, next) { stack: err.stack }) } + + if (err instanceof GatewayTimeoutError) { + logger.info('Gateway Time out Error occurred on Transactions Search Page. Rendering error page') + return renderErrorView(req, res, err.message, 504) + } + + if (err instanceof GenericServerError) { + logger.info('General Error occurred on Transactions Search Page. Rendering error page') + return renderErrorView(req, res, err.message, 500) + } + Sentry.captureException(err) renderErrorView(req, res, 'There is a problem with the payments platform. Please contact the support team.', 500) } diff --git a/app/services/transaction.service.js b/app/services/transaction.service.js index 3e942162b2..cd46da5012 100644 --- a/app/services/transaction.service.js +++ b/app/services/transaction.service.js @@ -11,6 +11,7 @@ const getQueryStringForParams = require('../utils/get-query-string-for-params') const userService = require('../services/user.service') const transactionView = require('../utils/transaction-view') const errorIdentifier = require('../models/error-identifier') +const { GatewayTimeoutError, GenericServerError } = require('../errors') const connector = new ConnectorClient(process.env.CONNECTOR_URL) @@ -22,10 +23,9 @@ const connectorRefundFailureReasons = { const searchLedger = async function searchLedger (gatewayAccountIds = [], filters) { try { - const transactions = await Ledger.transactions(gatewayAccountIds, filters) - return transactions + return await Ledger.transactions(gatewayAccountIds, filters) } catch (error) { - throw new Error('GET_FAILED') + throw handleErrorForFailedSearch(error) } } @@ -148,6 +148,15 @@ function getStatusCodeForError (err, response) { return status } +function handleErrorForFailedSearch (err, response) { + const code = (response || {}).statusCode || (err || {}).errorCode + if (code === 504) { + return new GatewayTimeoutError('Your request has timed out. Please apply more filters and try again.') + } else { + return new GenericServerError('Unable to retrieve list of transactions or card types.') + } +} + module.exports = { search: searchLedger, csvSearchUrl, diff --git a/app/views/team-members/team-member-invite.njk b/app/views/team-members/team-member-invite.njk index 67e18f47c6..cd63a29429 100644 --- a/app/views/team-members/team-member-invite.njk +++ b/app/views/team-members/team-member-invite.njk @@ -78,7 +78,7 @@ Invite a new team member - GOV.UK Pay label: { classes: "govuk-label--s" }, - checked: false, + checked: true, hint: { html: "View transactions
Cannot refund payments
@@ -155,7 +155,7 @@ Invite a new team member - GOV.UK Pay label: { classes: "govuk-label--s" }, - checked: false, + checked: true, hint: { html: "View transactions
Cannot refund payments
diff --git a/package.json b/package.json index 48bb7113f8..fbbcd9936d 100644 --- a/package.json +++ b/package.json @@ -128,7 +128,7 @@ "chai-arrays": "2.2.0", "chai-as-promised": "7.1.1", "cheerio": "1.0.0-rc.12", - "chokidar-cli": "*", + "chokidar-cli": "latest", "csrf": "^3.1.0", "cypress": "^12.16.0", "dotenv": "16.3.1", diff --git a/test/cypress/integration/demo-payment/mock-cards-stripe.cy.js b/test/cypress/integration/demo-payment/mock-cards-stripe.cy.js index e6984ce97c..72206035e6 100644 --- a/test/cypress/integration/demo-payment/mock-cards-stripe.cy.js +++ b/test/cypress/integration/demo-payment/mock-cards-stripe.cy.js @@ -14,17 +14,17 @@ function setupYourPspStubs (opts = {}) { gatewayAccountId, gatewayAccountExternalId, type: 'test', - paymentProvider: 'stripe', + paymentProvider: 'stripe' }) const stripeAccountSetup = stripeAccountSetupStubs.getGatewayAccountStripeSetupSuccess({ - gatewayAccountId, + gatewayAccountId }) const stubs = [ user, gatewayAccountByExternalId, - stripeAccountSetup, + stripeAccountSetup ] cy.task('setupStubs', stubs) diff --git a/test/cypress/integration/transactions/transaction-search.cy.js b/test/cypress/integration/transactions/transaction-search.cy.js index 92f42ee66f..9b6ba2ffb3 100644 --- a/test/cypress/integration/transactions/transaction-search.cy.js +++ b/test/cypress/integration/transactions/transaction-search.cy.js @@ -430,6 +430,7 @@ describe('Transactions List', () => { cy.get('#download-transactions-link').should('have.attr', 'href', `/account/a-valid-external-id/transactions/download?dispute_states=needs_response&dispute_states=under_review`) }) }) + describe('csv download link', () => { it('should not display csv download link when results >5k and no filter applied', function () { cy.task('setupStubs', [ @@ -461,4 +462,167 @@ describe('Transactions List', () => { cy.get('#download-transactions-link').should('exist') }) }) + + describe('Should display relevant error page on search failure ', () => { + it('should show error message on a bad request while retrieving the list of transactions', () => { + cy.task('setupStubs', [ + ...sharedStubs(), + transactionsStubs.getLedgerTransactionsSuccess({ gatewayAccountId, transactions: unfilteredTransactions }) + ]) + cy.visit(transactionsUrl, { failOnStatusCode: false }) + + // 1. Filtering FROM + // Ensure both the date/time pickers aren't showing + cy.get('.datepicker').should('not.exist') + cy.get('.ui-timepicker-wrapper').should('not.exist') + + // Fill in a from date + cy.get('#fromDate').type('33/18/2018') + + // Fill in a from time + cy.get('#fromTime').type('01:00:00') + + // 2. Filtering TO + + // Fill in a to date + cy.get('#toDate').type('33/23/2023') + + // Fill in a to time + cy.get('#toTime').type('01:00:00') + + cy.task('clearStubs') + + cy.task('setupStubs', [ + ...sharedStubs(), + transactionsStubs.getLedgerTransactionsFailure( + { + account_id: gatewayAccountId, + limit_total: 'true', + limit_total_size: '5001', + from_date: '2018-18-03T00:00:00.000Z', + to_date: '2023-23-03T00:00:01.000Z', + page: '1', + display_size: '100' + }, + 400) + ]) + + // Click the filter button + cy.get('#filter').click() + + // Ensure that transaction list is not displayed + cy.get('#transactions-list tbody').should('not.exist') + + // Ensure a generic error message is displayed + cy.get('h1').contains('An error occurred') + cy.get('#errorMsg').contains('Unable to retrieve list of transactions or card types') + }) + it('should display the generic error page, if an internal server error occurs while retrieving the list of transactions', () => { + cy.task('setupStubs', [ + ...sharedStubs(), + transactionsStubs.getLedgerTransactionsSuccess({ gatewayAccountId, transactions: unfilteredTransactions }) + ]) + cy.visit(transactionsUrl, { failOnStatusCode: false }) + + // 1. Filtering FROM + // Ensure both the date/time pickers aren't showing + cy.get('.datepicker').should('not.exist') + cy.get('.ui-timepicker-wrapper').should('not.exist') + + // Fill in a from date + cy.get('#fromDate').type('03/5/2018') + + // Fill in a from time + cy.get('#fromTime').type('01:00:00') + + // 2. Filtering TO + + // Fill in a to date + cy.get('#toDate').type('03/5/2023') + + // Fill in a to time + cy.get('#toTime').type('01:00:00') + + cy.task('clearStubs') + + cy.task('setupStubs', [ + ...sharedStubs(), + transactionsStubs.getLedgerTransactionsFailure( + { + account_id: gatewayAccountId, + limit_total: 'true', + limit_total_size: '5001', + from_date: '2018-05-03T00:00:00.000Z', + to_date: '2023-05-03T00:00:01.000Z', + page: '1', + display_size: '100' + }, + 500) + ]) + + // Click the filter button + cy.get('#filter').click() + + // Ensure that transaction list is not displayed + cy.get('#transactions-list tbody').should('not.exist') + + // Ensure a generic error message is displayed + cy.get('h1').contains('An error occurred') + cy.get('#errorMsg').contains('Unable to retrieve list of transactions or card types') + }) + + it('should display the gateway timeout error page, if a gateway timeout error occurs while retrieving the list of transactions', () => { + cy.task('setupStubs', [ + ...sharedStubs(), + transactionsStubs.getLedgerTransactionsSuccess({ gatewayAccountId, transactions: unfilteredTransactions }) + ]) + cy.visit(transactionsUrl, { failOnStatusCode: false }) + + // 1. Filtering FROM + // Ensure both the date/time pickers aren't showing + cy.get('.datepicker').should('not.exist') + cy.get('.ui-timepicker-wrapper').should('not.exist') + + // Fill in a from date + cy.get('#fromDate').type('03/5/2018') + + // Fill in a from time + cy.get('#fromTime').type('01:00:00') + + // 2. Filtering TO + + // Fill in a to date + cy.get('#toDate').type('03/5/2023') + + // Fill in a to time + cy.get('#toTime').type('01:00:00') + + cy.task('clearStubs') + + cy.task('setupStubs', [ + ...sharedStubs(), + transactionsStubs.getLedgerTransactionsFailure( + { + account_id: gatewayAccountId, + limit_total: 'true', + limit_total_size: '5001', + from_date: '2018-05-03T00:00:00.000Z', + to_date: '2023-05-03T00:00:01.000Z', + page: '1', + display_size: '100' + }, + 504) + ]) + + // Click the filter button + cy.get('#filter').click() + + // Ensure that transaction list is not displayed + cy.get('#transactions-list tbody').should('not.exist') + + // Ensure a gateway timeout error message is displayed + cy.get('h1').contains('An error occurred') + cy.get('#errorMsg').contains('Your request has timed out. Please apply more filters and try again') + }) + }) }) diff --git a/test/cypress/stubs/transaction-stubs.js b/test/cypress/stubs/transaction-stubs.js index 228c93ee44..e9378653b7 100644 --- a/test/cypress/stubs/transaction-stubs.js +++ b/test/cypress/stubs/transaction-stubs.js @@ -91,6 +91,20 @@ function getTransactionsSummarySuccess (opts) { }) } +function getLedgerTransactionsFailure (opts, responseCode) { + const path = `/v1/transaction` + return stubBuilder('GET', path, responseCode, { + query: { + account_id: opts.account_id, + limit_total: opts.limit_total, + limit_total_size: opts.limit_total_size, + from_date: opts.from_date, + to_date: opts.to_date, + page: opts.page, + display_size: opts.display_size + } }) +} + module.exports = { getLedgerEventsSuccess, getLedgerTransactionSuccess, @@ -98,5 +112,6 @@ module.exports = { getLedgerDisputeTransactionsSuccess, postRefundSuccess, postRefundAmountNotAvailable, - getTransactionsSummarySuccess + getTransactionsSummarySuccess, + getLedgerTransactionsFailure } diff --git a/test/integration/invite-users.controller.ft.test.js b/test/integration/invite-users.controller.ft.test.js index 2d546e26dc..9f1b0b1b32 100644 --- a/test/integration/invite-users.controller.ft.test.js +++ b/test/integration/invite-users.controller.ft.test.js @@ -85,5 +85,27 @@ describe('invite user controller', function () { }) .end(done) }) + + it('should error on unknown role externalId', function (done) { + const unknownRoleId = '999' + + const app = session.getAppWithLoggedInUser(getApp(), userInSession) + + supertest(app) + .post(formatServicePathsFor(paths.service.teamMembers.invite, EXTERNAL_SERVICE_ID)) + .set('Accept', 'application/json') + .set('Content-Type', 'application/x-www-form-urlencoded') + .set('x-request-id', 'bob') + .send({ + 'invitee-email': 'invitee@example.com', + 'role-input': unknownRoleId, + csrfToken: csrf().create('123') + }) + .expect(500) + .expect((res) => { + expect(res.body.message).to.equal('There is a problem with the payments platform. Please contact the support team.') + }) + .end(done) + }) }) }) diff --git a/test/ui/invite-user.ui.test.js b/test/ui/invite-user.ui.test.js index 44a3d9110b..3ca589c8ff 100644 --- a/test/ui/invite-user.ui.test.js +++ b/test/ui/invite-user.ui.test.js @@ -36,7 +36,7 @@ describe('Invite a team member view', function () { body.should.containSelector('#role-input-3') .withAttribute('type', 'radio') .withAttribute('value', '4') - .withNoAttribute('checked') + .withAttribute('checked') body.should.not.containSelector('#role-input-4') body.should.not.containSelector('#role-input-5') }) @@ -72,7 +72,7 @@ describe('Invite a team member view', function () { body.should.containSelector('#role-input-3') .withAttribute('type', 'radio') .withAttribute('value', '4') - .withNoAttribute('checked') + .withAttribute('checked') body.should.containSelector('#role-input-4') .withAttribute('type', 'radio') .withAttribute('value', '5') From bf0df4a23ba74caf496677cf5ccae9ccaec7f905 Mon Sep 17 00:00:00 2001 From: "ola.tomoloju" Date: Fri, 27 Oct 2023 18:30:56 +0100 Subject: [PATCH 07/18] PP-10451 Handle gateway timeout error during transactions download. This change is for the single service transaction search and not the all services functionality. Converting test scenarios as Cypress Tests for 400, 500 and 504 responses and removing nock mocking logic. Download of transactions list by clicking the download link was de-scoped, so no tests were written for the download functionality. --- .../transaction-download.controller.js | 9 +---- app/services/transaction.service.js | 5 ++- package.json | 2 +- .../transactions/transaction-search.cy.js | 35 ++++++------------- 4 files changed, 15 insertions(+), 36 deletions(-) diff --git a/app/controllers/transactions/transaction-download.controller.js b/app/controllers/transactions/transaction-download.controller.js index fffa4e1ba0..fe43274846 100644 --- a/app/controllers/transactions/transaction-download.controller.js +++ b/app/controllers/transactions/transaction-download.controller.js @@ -22,14 +22,7 @@ const fetchTransactionCsvWithHeader = function fetchTransactionCsvWithHeader (re transactionService.logCsvFileStreamComplete(timestampStreamStart, filters, [accountId], req.user, false, req.account.type === 'live') res.end() } - const error = () => { - const code = (res || {}).statusCode - if (code === 504) { - renderErrorView(req, res, 'Your request has timed out. Please apply more filters and try again.') - } else { - renderErrorView(req, res, 'Unable to download list of transactions.') - } - } + const error = () => renderErrorView(req, res, 'Unable to download list of transactions.') const client = new Stream(data, complete, error) res.setHeader('Content-disposition', `attachment; filename="${name}"`) diff --git a/app/services/transaction.service.js b/app/services/transaction.service.js index cd46da5012..80dfb77016 100644 --- a/app/services/transaction.service.js +++ b/app/services/transaction.service.js @@ -148,9 +148,8 @@ function getStatusCodeForError (err, response) { return status } -function handleErrorForFailedSearch (err, response) { - const code = (response || {}).statusCode || (err || {}).errorCode - if (code === 504) { +function handleErrorForFailedSearch (err) { + if (err.errorCode === 504) { return new GatewayTimeoutError('Your request has timed out. Please apply more filters and try again.') } else { return new GenericServerError('Unable to retrieve list of transactions or card types.') diff --git a/package.json b/package.json index fbbcd9936d..48bb7113f8 100644 --- a/package.json +++ b/package.json @@ -128,7 +128,7 @@ "chai-arrays": "2.2.0", "chai-as-promised": "7.1.1", "cheerio": "1.0.0-rc.12", - "chokidar-cli": "latest", + "chokidar-cli": "*", "csrf": "^3.1.0", "cypress": "^12.16.0", "dotenv": "16.3.1", diff --git a/test/cypress/integration/transactions/transaction-search.cy.js b/test/cypress/integration/transactions/transaction-search.cy.js index 9b6ba2ffb3..7304f6abaa 100644 --- a/test/cypress/integration/transactions/transaction-search.cy.js +++ b/test/cypress/integration/transactions/transaction-search.cy.js @@ -471,25 +471,6 @@ describe('Transactions List', () => { ]) cy.visit(transactionsUrl, { failOnStatusCode: false }) - // 1. Filtering FROM - // Ensure both the date/time pickers aren't showing - cy.get('.datepicker').should('not.exist') - cy.get('.ui-timepicker-wrapper').should('not.exist') - - // Fill in a from date - cy.get('#fromDate').type('33/18/2018') - - // Fill in a from time - cy.get('#fromTime').type('01:00:00') - - // 2. Filtering TO - - // Fill in a to date - cy.get('#toDate').type('33/23/2023') - - // Fill in a to time - cy.get('#toTime').type('01:00:00') - cy.task('clearStubs') cy.task('setupStubs', [ @@ -499,8 +480,8 @@ describe('Transactions List', () => { account_id: gatewayAccountId, limit_total: 'true', limit_total_size: '5001', - from_date: '2018-18-03T00:00:00.000Z', - to_date: '2023-23-03T00:00:01.000Z', + from_date: '', + to_date: '', page: '1', display_size: '100' }, @@ -513,8 +494,10 @@ describe('Transactions List', () => { // Ensure that transaction list is not displayed cy.get('#transactions-list tbody').should('not.exist') - // Ensure a generic error message is displayed + // Ensure an error message header is displayed cy.get('h1').contains('An error occurred') + + // Ensure a generic error message is displayed cy.get('#errorMsg').contains('Unable to retrieve list of transactions or card types') }) it('should display the generic error page, if an internal server error occurs while retrieving the list of transactions', () => { @@ -566,8 +549,10 @@ describe('Transactions List', () => { // Ensure that transaction list is not displayed cy.get('#transactions-list tbody').should('not.exist') - // Ensure a generic error message is displayed + // Ensure an error message header is displayed cy.get('h1').contains('An error occurred') + + // Ensure a generic error message is displayed cy.get('#errorMsg').contains('Unable to retrieve list of transactions or card types') }) @@ -620,8 +605,10 @@ describe('Transactions List', () => { // Ensure that transaction list is not displayed cy.get('#transactions-list tbody').should('not.exist') - // Ensure a gateway timeout error message is displayed + // Ensure an error message header is displayed cy.get('h1').contains('An error occurred') + + // Ensure a gateway timeout error message is displayed cy.get('#errorMsg').contains('Your request has timed out. Please apply more filters and try again') }) }) From 90ec07593f0d830223718ef6ae261cc0cf0f5cdb Mon Sep 17 00:00:00 2001 From: "ola.tomoloju" Date: Tue, 31 Oct 2023 17:43:06 +0000 Subject: [PATCH 08/18] PP-10451 Handle gateway timeout error during transactions download. This change is for the single service transaction search and not the all services functionality. Converting test scenarios as Cypress Tests for 400, 500 and 504 responses and removing nock mocking logic. Download of transactions list by clicking the download link was de-scoped, so no tests were written for the download functionality. --- app/errors.js | 11 +---- app/middleware/error-handler.js | 19 ++++----- app/services/transaction.service.js | 16 +++----- .../transactions/transaction-search.cy.js | 40 +++---------------- 4 files changed, 19 insertions(+), 67 deletions(-) diff --git a/app/errors.js b/app/errors.js index ec17b01992..360448470e 100644 --- a/app/errors.js +++ b/app/errors.js @@ -27,14 +27,6 @@ class GatewayTimeoutError extends Error { } } -class GenericServerError extends Error { - constructor (message) { - super(message) - this.name = this.constructor.name - Error.captureStackTrace(this, this.constructor) - } -} - /** * Thrown when there is no authentication session for the user. */ @@ -113,6 +105,5 @@ module.exports = { InvalidRegistationStateError, InvalidConfigurationError, ExpiredInviteError, - GatewayTimeoutError, - GenericServerError + GatewayTimeoutError } diff --git a/app/middleware/error-handler.js b/app/middleware/error-handler.js index 50ddb140d1..72caff8d74 100644 --- a/app/middleware/error-handler.js +++ b/app/middleware/error-handler.js @@ -15,7 +15,8 @@ const { InvalidRegistationStateError, InvalidConfigurationError, ExpiredInviteError, - RESTClientError, GatewayTimeoutError, GenericServerError + RESTClientError, + GatewayTimeoutError } = require('../errors') const paths = require('../paths') const { renderErrorView, response } = require('../utils/response') @@ -80,6 +81,11 @@ module.exports = function errorHandler (err, req, res, next) { return renderErrorView(req, res, 'There is a problem with the payments platform. Please contact the support team', 400) } + if (err instanceof GatewayTimeoutError) { + logger.info('Gateway Time out Error occurred on Transactions Search Page. Rendering error page') + return renderErrorView(req, res, err.message, 504) + } + if (err instanceof RESTClientError) { logger.info(`Unhandled REST client error caught: ${err.message}`, { service: err.service, @@ -95,17 +101,6 @@ module.exports = function errorHandler (err, req, res, next) { stack: err.stack }) } - - if (err instanceof GatewayTimeoutError) { - logger.info('Gateway Time out Error occurred on Transactions Search Page. Rendering error page') - return renderErrorView(req, res, err.message, 504) - } - - if (err instanceof GenericServerError) { - logger.info('General Error occurred on Transactions Search Page. Rendering error page') - return renderErrorView(req, res, err.message, 500) - } - Sentry.captureException(err) renderErrorView(req, res, 'There is a problem with the payments platform. Please contact the support team.', 500) } diff --git a/app/services/transaction.service.js b/app/services/transaction.service.js index 80dfb77016..df68b761e3 100644 --- a/app/services/transaction.service.js +++ b/app/services/transaction.service.js @@ -11,7 +11,7 @@ const getQueryStringForParams = require('../utils/get-query-string-for-params') const userService = require('../services/user.service') const transactionView = require('../utils/transaction-view') const errorIdentifier = require('../models/error-identifier') -const { GatewayTimeoutError, GenericServerError } = require('../errors') +const { GatewayTimeoutError } = require('../errors') const connector = new ConnectorClient(process.env.CONNECTOR_URL) @@ -25,7 +25,11 @@ const searchLedger = async function searchLedger (gatewayAccountIds = [], filter try { return await Ledger.transactions(gatewayAccountIds, filters) } catch (error) { - throw handleErrorForFailedSearch(error) + if (error.errorCode === 504) { + throw new GatewayTimeoutError('Your request has timed out. Please apply more filters and try again.') + } else { + throw new Error('Unable to retrieve list of transactions or card types.') + } } } @@ -148,14 +152,6 @@ function getStatusCodeForError (err, response) { return status } -function handleErrorForFailedSearch (err) { - if (err.errorCode === 504) { - return new GatewayTimeoutError('Your request has timed out. Please apply more filters and try again.') - } else { - return new GenericServerError('Unable to retrieve list of transactions or card types.') - } -} - module.exports = { search: searchLedger, csvSearchUrl, diff --git a/test/cypress/integration/transactions/transaction-search.cy.js b/test/cypress/integration/transactions/transaction-search.cy.js index 7304f6abaa..23a8c8eeb3 100644 --- a/test/cypress/integration/transactions/transaction-search.cy.js +++ b/test/cypress/integration/transactions/transaction-search.cy.js @@ -507,25 +507,6 @@ describe('Transactions List', () => { ]) cy.visit(transactionsUrl, { failOnStatusCode: false }) - // 1. Filtering FROM - // Ensure both the date/time pickers aren't showing - cy.get('.datepicker').should('not.exist') - cy.get('.ui-timepicker-wrapper').should('not.exist') - - // Fill in a from date - cy.get('#fromDate').type('03/5/2018') - - // Fill in a from time - cy.get('#fromTime').type('01:00:00') - - // 2. Filtering TO - - // Fill in a to date - cy.get('#toDate').type('03/5/2023') - - // Fill in a to time - cy.get('#toTime').type('01:00:00') - cy.task('clearStubs') cy.task('setupStubs', [ @@ -535,8 +516,8 @@ describe('Transactions List', () => { account_id: gatewayAccountId, limit_total: 'true', limit_total_size: '5001', - from_date: '2018-05-03T00:00:00.000Z', - to_date: '2023-05-03T00:00:01.000Z', + from_date: '', + to_date: '', page: '1', display_size: '100' }, @@ -561,27 +542,16 @@ describe('Transactions List', () => { ...sharedStubs(), transactionsStubs.getLedgerTransactionsSuccess({ gatewayAccountId, transactions: unfilteredTransactions }) ]) - cy.visit(transactionsUrl, { failOnStatusCode: false }) - // 1. Filtering FROM - // Ensure both the date/time pickers aren't showing - cy.get('.datepicker').should('not.exist') - cy.get('.ui-timepicker-wrapper').should('not.exist') + cy.visit(transactionsUrl, { failOnStatusCode: false }) - // Fill in a from date + // Fill from and to date cy.get('#fromDate').type('03/5/2018') - - // Fill in a from time cy.get('#fromTime').type('01:00:00') - - // 2. Filtering TO - - // Fill in a to date cy.get('#toDate').type('03/5/2023') - - // Fill in a to time cy.get('#toTime').type('01:00:00') + // 1. Filtering cy.task('clearStubs') cy.task('setupStubs', [ From b7f7daf3948e8ed9909d5f022b4a62156adbac26 Mon Sep 17 00:00:00 2001 From: "ola.tomoloju" Date: Tue, 31 Oct 2023 18:58:05 +0000 Subject: [PATCH 09/18] PP-10451 Handle gateway timeout error during transactions download. This change is for the single service transaction search and not the all services functionality. Converting test scenarios as Cypress Tests for 400, 500 and 504 responses and removing nock mocking logic. Download of transactions list by clicking the download link was de-scoped, so no tests were written for the download functionality. --- .../integration/transactions/transaction-search.cy.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/cypress/integration/transactions/transaction-search.cy.js b/test/cypress/integration/transactions/transaction-search.cy.js index 23a8c8eeb3..9363e6a4d3 100644 --- a/test/cypress/integration/transactions/transaction-search.cy.js +++ b/test/cypress/integration/transactions/transaction-search.cy.js @@ -498,8 +498,9 @@ describe('Transactions List', () => { cy.get('h1').contains('An error occurred') // Ensure a generic error message is displayed - cy.get('#errorMsg').contains('Unable to retrieve list of transactions or card types') + cy.get('#errorMsg').contains('There is a problem with the payments platform. Please contact the support team.') }) + it('should display the generic error page, if an internal server error occurs while retrieving the list of transactions', () => { cy.task('setupStubs', [ ...sharedStubs(), @@ -534,7 +535,7 @@ describe('Transactions List', () => { cy.get('h1').contains('An error occurred') // Ensure a generic error message is displayed - cy.get('#errorMsg').contains('Unable to retrieve list of transactions or card types') + cy.get('#errorMsg').contains('There is a problem with the payments platform. Please contact the support team.') }) it('should display the gateway timeout error page, if a gateway timeout error occurs while retrieving the list of transactions', () => { From 3eb16049a27dd3abc6e8ce85b338439988cf90cc Mon Sep 17 00:00:00 2001 From: "ola.tomoloju" Date: Wed, 1 Nov 2023 10:16:35 +0000 Subject: [PATCH 10/18] PP-10451 Handle gateway timeout error during transactions download. This change is for the single service transaction search and not the all services functionality. Converting test scenarios as Cypress Tests for 400, 500 and 504 responses and removing nock mocking logic. Download of transactions list by clicking the download link was de-scoped, so no tests were written for the download functionality. Removed files pulled in from master for MR clarity. --- app/controllers/invite-user.controller.js | 87 --------- .../invite-user.controller.test.js | 26 --- app/views/team-members/team-member-invite.njk | 176 ------------------ .../demo-payment/mock-cards-stripe.cy.js | 49 ----- .../invite-users.controller.ft.test.js | 111 ----------- test/ui/invite-user.ui.test.js | 85 --------- 6 files changed, 534 deletions(-) delete mode 100644 app/controllers/invite-user.controller.js delete mode 100644 app/controllers/invite-user.controller.test.js delete mode 100644 app/views/team-members/team-member-invite.njk delete mode 100644 test/cypress/integration/demo-payment/mock-cards-stripe.cy.js delete mode 100644 test/integration/invite-users.controller.ft.test.js delete mode 100644 test/ui/invite-user.ui.test.js diff --git a/app/controllers/invite-user.controller.js b/app/controllers/invite-user.controller.js deleted file mode 100644 index 0c978b3ca4..0000000000 --- a/app/controllers/invite-user.controller.js +++ /dev/null @@ -1,87 +0,0 @@ -const lodash = require('lodash') -const { response } = require('../utils/response.js') -const userService = require('../services/user.service.js') -const paths = require('../paths.js') -const rolesModule = require('../utils/roles') -const { isValidEmail } = require('../utils/email-tools.js') - -const formatServicePathsFor = require('../utils/format-service-paths-for') - -const messages = { - emailAlreadyInUse: 'Email already in use', - inviteError: 'Unable to send invitation at this time', - emailConflict: (email, externalServiceId) => { - return { - error: { - title: 'This person has already been invited', - message: `You cannot send an invitation to ${email} because they have received one already, or may be an existing team member.` - }, - link: { - link: formatServicePathsFor(paths.service.teamMembers.index, externalServiceId), - text: 'View all team members' - }, - enable_link: true - } - } -} - -function index (req, res) { - let roles = rolesModule.roles - const externalServiceId = req.service.externalId - const teamMemberIndexLink = formatServicePathsFor(paths.service.teamMembers.index, externalServiceId) - const teamMemberInviteSubmitLink = formatServicePathsFor(paths.service.teamMembers.invite, externalServiceId) - const serviceHasAgentInitiatedMotoEnabled = req.service.agentInitiatedMotoEnabled - const invitee = lodash.get(req, 'session.pageData.invitee', '') - let data = { - teamMemberIndexLink: teamMemberIndexLink, - teamMemberInviteSubmitLink: teamMemberInviteSubmitLink, - serviceHasAgentInitiatedMotoEnabled: serviceHasAgentInitiatedMotoEnabled, - admin: { id: roles['admin'].extId }, - viewAndRefund: { id: roles['view-and-refund'].extId }, - view: { id: roles['view-only'].extId }, - viewAndInitiateMoto: { id: roles['view-and-initiate-moto'].extId }, - viewRefundAndInitiateMoto: { id: roles['view-refund-and-initiate-moto'].extId }, - invitee - } - - return response(req, res, 'team-members/team-member-invite', data) -} - -async function invite (req, res, next) { - const senderId = req.user.externalId - const externalServiceId = req.service.externalId - const invitee = req.body['invitee-email'].trim() - const roleId = parseInt(req.body['role-input']) - - const role = rolesModule.getRoleByExtId(roleId) - - if (!isValidEmail(invitee)) { - req.flash('genericError', 'Enter a valid email address') - lodash.set(req, 'session.pageData', { invitee }) - res.redirect(303, formatServicePathsFor(paths.service.teamMembers.invite, externalServiceId)) - } else if (!role) { - next(new Error(`Cannot identify role from user input ${roleId}`)) - } else { - try { - await userService.createInviteToJoinService(invitee, senderId, externalServiceId, role.name) - if (lodash.has(req, 'session.pageData.invitee')) { - delete req.session.pageData.invitee - } - req.flash('generic', `Invite sent to ${invitee}`) - res.redirect(303, formatServicePathsFor(paths.service.teamMembers.index, externalServiceId)) - } catch (err) { - switch (err.errorCode) { - case 412: - response(req, res, 'error-with-link', messages.emailConflict(invitee, externalServiceId)) - break - default: - next(err) - } - } - } -} - -module.exports = { - index, - invite -} diff --git a/app/controllers/invite-user.controller.test.js b/app/controllers/invite-user.controller.test.js deleted file mode 100644 index 0c292be909..0000000000 --- a/app/controllers/invite-user.controller.test.js +++ /dev/null @@ -1,26 +0,0 @@ -const sinon = require('sinon') -const inviteUserController = require('./invite-user.controller') - -describe('invite user controller', () => { - it('should error for an invalid email address', async () => { - const externalServiceId = 'some-external-service-id' - const req = { - user: { externalId: 'some-ext-id', serviceIds: ['1'] }, - body: { - 'invitee-email': 'invalid@examplecom', - 'role-input': '200' - }, - service: { - externalId: externalServiceId - }, - flash: sinon.stub() - } - const res = { - redirect: sinon.stub() - } - - await inviteUserController.invite(req, res) - sinon.assert.calledWith(req.flash, 'genericError', 'Enter a valid email address') - sinon.assert.calledWith(res.redirect, 303, `/service/${externalServiceId}/team-members/invite`) - }) -}) diff --git a/app/views/team-members/team-member-invite.njk b/app/views/team-members/team-member-invite.njk deleted file mode 100644 index cd63a29429..0000000000 --- a/app/views/team-members/team-member-invite.njk +++ /dev/null @@ -1,176 +0,0 @@ -{% extends "../layout.njk" %} - -{% block pageTitle %} -Invite a new team member - GOV.UK Pay -{% endblock %} - -{% block beforeContent %} - {{ super() }} - {{ - govukBackLink({ - text: "See all team members", - href: teamMemberIndexLink - }) - }} -{% endblock %} - -{% block mainContent %} -
-
-

- Team members - Invite a team member

-
-
-
- - {{ govukInput({ - label: { - text: "Email address" - }, - id: "invitee-email", - name: "invitee-email", - classes: "govuk-!-width-two-thirds", - type: "email", - autocomplete: "work email", - spellcheck: false - }) }} - {% if serviceHasAgentInitiatedMotoEnabled %} - {{ govukRadios({ - idPrefix: "role-input", - name: "role-input", - fieldset: { - legend: { - text: "Permission level", - classes: "govuk-label--s" - } - }, - items: [ - { - value: admin.id, - text: "Administrator", - label: { - classes: "govuk-label--s" - }, - hint: { - html: "View transactions
- Refund payments
- Take telephone payments
- Manage settings" - } - }, - { - value: viewAndRefund.id, - text: "View and refund", - label: { - classes: "govuk-label--s" - }, - hint: { - html: "View transactions
- Refund payments
- Cannot take telephone payments
- Cannot manage settings" - } - }, - { - value: view.id, - text: "View only", - label: { - classes: "govuk-label--s" - }, - checked: true, - hint: { - html: "View transactions
- Cannot refund payments
- Cannot take telephone payments
- Cannot manage settings" - } - }, - { - value: viewAndInitiateMoto.id, - text: "View and take telephone payments", - label: { - classes: "govuk-label--s" - }, - hint: { - html: "View transactions
- Cannot refund payments
- Take telephone payments
- Cannot manage settings" - } - }, - { - value: viewRefundAndInitiateMoto.id, - text: "View, refund and take telephone payments", - label: { - classes: "govuk-label--s" - }, - hint: { - html: "View transactions
- Refund payments
- Take telephone payments
- Cannot manage settings" - } - } - ] - }) }} - {% else %} - {{ govukRadios({ - idPrefix: "role-input", - name: "role-input", - fieldset: { - legend: { - text: "Permission level", - classes: "govuk-label--s" - } - }, - items: [ - { - value: admin.id, - text: "Administrator", - label: { - classes: "govuk-label--s" - }, - hint: { - html: "View transactions
- Refund payments
- Manage settings" - } - }, - { - value: viewAndRefund.id, - text: "View and refund", - label: { - classes: "govuk-label--s" - }, - hint: { - html: "View transactions
- Refund payments
- Cannot manage settings" - } - }, - { - value: view.id, - text: "View only", - label: { - classes: "govuk-label--s" - }, - checked: true, - hint: { - html: "View transactions
- Cannot refund payments
- Cannot manage settings" - } - } - ] - }) }} - {% endif %} - - {{ govukButton({ text: "Send invitation email" }) }} -

- - Cancel - -

-
-{% endblock %} diff --git a/test/cypress/integration/demo-payment/mock-cards-stripe.cy.js b/test/cypress/integration/demo-payment/mock-cards-stripe.cy.js deleted file mode 100644 index 72206035e6..0000000000 --- a/test/cypress/integration/demo-payment/mock-cards-stripe.cy.js +++ /dev/null @@ -1,49 +0,0 @@ -'use strict' - -const userStubs = require('../../stubs/user-stubs') -const gatewayAccountStubs = require('../../stubs/gateway-account-stubs') -const stripeAccountSetupStubs = require('../../stubs/stripe-account-setup-stub') -const userExternalId = 'cd0fa54cf3b7408a80ae2f1b93e7c16e' // pragma: allowlist secret -const gatewayAccountId = '42' -const gatewayAccountExternalId = 'a-valid-external-id' - -function setupYourPspStubs (opts = {}) { - const user = userStubs.getUserSuccess({ userExternalId, gatewayAccountId }) - - const gatewayAccountByExternalId = gatewayAccountStubs.getGatewayAccountByExternalIdSuccess({ - gatewayAccountId, - gatewayAccountExternalId, - type: 'test', - paymentProvider: 'stripe' - }) - - const stripeAccountSetup = stripeAccountSetupStubs.getGatewayAccountStripeSetupSuccess({ - gatewayAccountId - }) - - const stubs = [ - user, - gatewayAccountByExternalId, - stripeAccountSetup - ] - - cy.task('setupStubs', stubs) -} - -describe('Show Mock cards screen for stripe accounts', () => { - beforeEach(() => { - cy.setEncryptedCookies(userExternalId) - }) - - it('should display stripe settings page correctly', () => { - setupYourPspStubs() - cy.visit(`/account/${gatewayAccountExternalId}/settings`) - cy.log('Continue to Make a demo payment page via Dashboard') - cy.get('a').contains('Dashboard').click() - cy.get('a').contains('Make a demo payment').click() - cy.log('Continue to Mock Cards page') - cy.get('a').contains('Continue').click() - cy.get('h1').should('have.text', 'Mock card numbers') - cy.get('p').contains(/^4000058260000005/) - }) -}) diff --git a/test/integration/invite-users.controller.ft.test.js b/test/integration/invite-users.controller.ft.test.js deleted file mode 100644 index 9f1b0b1b32..0000000000 --- a/test/integration/invite-users.controller.ft.test.js +++ /dev/null @@ -1,111 +0,0 @@ -const path = require('path') -const nock = require('nock') -const getApp = require(path.join(__dirname, '/../../server.js')).getApp -const supertest = require('supertest') -const session = require(path.join(__dirname, '/../test-helpers/mock-session.js')) -const csrf = require('csrf') -const chai = require('chai') -const roles = require('../../app/utils/roles').roles -const paths = require(path.join(__dirname, '/../../app/paths.js')) -const inviteFixtures = require(path.join(__dirname, '/../fixtures/invite.fixtures')) - -const expect = chai.expect -const adminusersMock = nock(process.env.ADMINUSERS_URL) - -const formatServicePathsFor = require('../../app/utils/format-service-paths-for') - -describe('invite user controller', function () { - const userInSession = session.getUser({}) - const EXTERNAL_SERVICE_ID = userInSession.serviceRoles[0].service.externalId - userInSession.serviceRoles[0].role.permissions.push({ name: 'users-service:create' }) - const INVITE_RESOURCE = `/v1/api/invites/create-invite-to-join-service` - - describe('invite user index view', function () { - it('should display invite page', function (done) { - const app = session.getAppWithLoggedInUser(getApp(), userInSession) - - supertest(app) - .get(formatServicePathsFor(paths.service.teamMembers.invite, EXTERNAL_SERVICE_ID)) - .set('Accept', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body.teamMemberIndexLink).to.equal(formatServicePathsFor(paths.service.teamMembers.index, EXTERNAL_SERVICE_ID)) - expect(res.body.teamMemberInviteSubmitLink).to.equal(formatServicePathsFor(paths.service.teamMembers.invite, EXTERNAL_SERVICE_ID)) - expect(res.body.admin.id).to.equal(roles['admin'].extId) - expect(res.body.viewAndRefund.id).to.equal(roles['view-and-refund'].extId) - expect(res.body.view.id).to.equal(roles['view-only'].extId) - expect(res.body.viewAndInitiateMoto.id).to.equal(roles['view-and-initiate-moto'].extId) - expect(res.body.viewRefundAndInitiateMoto.id).to.equal(roles['view-refund-and-initiate-moto'].extId) - }) - .end(done) - }) - }) - - describe('invite user', function () { - it('should invite a new team member successfully', function (done) { - const validInvite = inviteFixtures.validCreateInviteToJoinServiceRequest() - adminusersMock.post(INVITE_RESOURCE) - .reply(201, inviteFixtures.validInviteResponse(validInvite)) - const app = session.getAppWithLoggedInUser(getApp(), userInSession) - - supertest(app) - .post(formatServicePathsFor(paths.service.teamMembers.invite, EXTERNAL_SERVICE_ID)) - .set('Accept', 'application/json') - .set('Content-Type', 'application/x-www-form-urlencoded') - .set('x-request-id', 'bob') - .send({ - 'invitee-email': 'invitee@example.com', - 'role-input': roles['admin'].extId, - csrfToken: csrf().create('123') - }) - .expect(303, {}) - .expect('Location', formatServicePathsFor(paths.service.teamMembers.index, EXTERNAL_SERVICE_ID)) - .end(done) - }) - - it('should error if the user is already invited/exists', function (done) { - const existingUser = 'existing-user@example.com' - adminusersMock.post(INVITE_RESOURCE) - .reply(412, inviteFixtures.conflictingInviteResponseWhenEmailUserAlreadyCreated(existingUser)) - const app = session.getAppWithLoggedInUser(getApp(), userInSession) - - supertest(app) - .post(formatServicePathsFor(paths.service.teamMembers.invite, EXTERNAL_SERVICE_ID)) - .set('Accept', 'application/json') - .set('Content-Type', 'application/x-www-form-urlencoded') - .set('x-request-id', 'bob') - .send({ - 'invitee-email': existingUser, - 'role-input': roles['admin'].extId, - csrfToken: csrf().create('123') - }) - .expect(200) - .expect((res) => { - expect(res.body.error.message).to.include(existingUser) - }) - .end(done) - }) - - it('should error on unknown role externalId', function (done) { - const unknownRoleId = '999' - - const app = session.getAppWithLoggedInUser(getApp(), userInSession) - - supertest(app) - .post(formatServicePathsFor(paths.service.teamMembers.invite, EXTERNAL_SERVICE_ID)) - .set('Accept', 'application/json') - .set('Content-Type', 'application/x-www-form-urlencoded') - .set('x-request-id', 'bob') - .send({ - 'invitee-email': 'invitee@example.com', - 'role-input': unknownRoleId, - csrfToken: csrf().create('123') - }) - .expect(500) - .expect((res) => { - expect(res.body.message).to.equal('There is a problem with the payments platform. Please contact the support team.') - }) - .end(done) - }) - }) -}) diff --git a/test/ui/invite-user.ui.test.js b/test/ui/invite-user.ui.test.js deleted file mode 100644 index 3ca589c8ff..0000000000 --- a/test/ui/invite-user.ui.test.js +++ /dev/null @@ -1,85 +0,0 @@ -let path = require('path') -let renderTemplate = require(path.join(__dirname, '/../test-helpers/html-assertions.js')).render -let paths = require('../../app/paths.js') - -const formatServicePathsFor = require('../../app/utils/format-service-paths-for') - -describe('Invite a team member view', function () { - it('should render the standard invite team member view', function () { - const externalServiceId = 'some-external-id' - const teamMemberIndexLink = formatServicePathsFor(paths.service.teamMembers.index, externalServiceId) - const teamMemberInviteSubmitLink = formatServicePathsFor(paths.service.teamMembers.invite, externalServiceId) - - let templateData = { - teamMemberIndexLink: teamMemberIndexLink, - teamMemberInviteSubmitLink: teamMemberInviteSubmitLink, - admin: { id: 2 }, - viewAndRefund: { id: 3 }, - view: { id: 4 }, - viewAndInitiateMoto: { id: 5 }, - viewRefundAndInitiateMoto: { id: 6 }, - serviceHasAgentInitiatedMotoEnabled: false - } - - let body = renderTemplate('team-members/team-member-invite', templateData) - - body.should.containSelector('.govuk-back-link').withAttribute('href', teamMemberIndexLink) - body.should.containSelector('form#invite-member-form').withAttribute('action', teamMemberInviteSubmitLink) - body.should.containSelector('#role-input') - .withAttribute('type', 'radio') - .withAttribute('value', '2') - .withNoAttribute('checked') - body.should.containSelector('#role-input-2') - .withAttribute('type', 'radio') - .withAttribute('value', '3') - .withNoAttribute('checked') - body.should.containSelector('#role-input-3') - .withAttribute('type', 'radio') - .withAttribute('value', '4') - .withAttribute('checked') - body.should.not.containSelector('#role-input-4') - body.should.not.containSelector('#role-input-5') - }) - - it('should render the agent-initiated-MOTO-enhanced invite team member view', function () { - const externalServiceId = 'some-external-id' - const teamMemberIndexLink = formatServicePathsFor(paths.service.teamMembers.index, externalServiceId) - const teamMemberInviteSubmitLink = formatServicePathsFor(paths.service.teamMembers.invite, externalServiceId) - - let templateData = { - teamMemberIndexLink: teamMemberIndexLink, - teamMemberInviteSubmitLink: teamMemberInviteSubmitLink, - admin: { id: 2 }, - viewAndRefund: { id: 3 }, - view: { id: 4 }, - viewAndInitiateMoto: { id: 5 }, - viewRefundAndInitiateMoto: { id: 6 }, - serviceHasAgentInitiatedMotoEnabled: true - } - - let body = renderTemplate('team-members/team-member-invite', templateData) - - body.should.containSelector('.govuk-back-link').withAttribute('href', teamMemberIndexLink) - body.should.containSelector('form#invite-member-form').withAttribute('action', teamMemberInviteSubmitLink) - body.should.containSelector('#role-input') - .withAttribute('type', 'radio') - .withAttribute('value', '2') - .withNoAttribute('checked') - body.should.containSelector('#role-input-2') - .withAttribute('type', 'radio') - .withAttribute('value', '3') - .withNoAttribute('checked') - body.should.containSelector('#role-input-3') - .withAttribute('type', 'radio') - .withAttribute('value', '4') - .withAttribute('checked') - body.should.containSelector('#role-input-4') - .withAttribute('type', 'radio') - .withAttribute('value', '5') - .withNoAttribute('checked') - body.should.containSelector('#role-input-5') - .withAttribute('type', 'radio') - .withAttribute('value', '6') - .withNoAttribute('checked') - }) -}) From e7412135226ae0026173b6b425763b2fa15ef885 Mon Sep 17 00:00:00 2001 From: "ola.tomoloju" Date: Wed, 1 Nov 2023 10:28:55 +0000 Subject: [PATCH 11/18] PP-10451 Handle gateway timeout error during transactions download. This change is for the single service transaction search and not the all services functionality. Converting test scenarios as Cypress Tests for 400, 500 and 504 responses and removing nock mocking logic. Download of transactions list by clicking the download link was de-scoped, so no tests were written for the download functionality. --- app/controllers/invite-user.controller.js | 87 +++++++++ .../invite-user.controller.test.js | 26 +++ app/views/team-members/team-member-invite.njk | 176 ++++++++++++++++++ .../demo-payment/mock-cards-stripe.cy.js | 49 +++++ .../invite-users.controller.ft.test.js | 111 +++++++++++ test/ui/invite-user.ui.test.js | 85 +++++++++ 6 files changed, 534 insertions(+) create mode 100644 app/controllers/invite-user.controller.js create mode 100644 app/controllers/invite-user.controller.test.js create mode 100644 app/views/team-members/team-member-invite.njk create mode 100644 test/cypress/integration/demo-payment/mock-cards-stripe.cy.js create mode 100644 test/integration/invite-users.controller.ft.test.js create mode 100644 test/ui/invite-user.ui.test.js diff --git a/app/controllers/invite-user.controller.js b/app/controllers/invite-user.controller.js new file mode 100644 index 0000000000..0c978b3ca4 --- /dev/null +++ b/app/controllers/invite-user.controller.js @@ -0,0 +1,87 @@ +const lodash = require('lodash') +const { response } = require('../utils/response.js') +const userService = require('../services/user.service.js') +const paths = require('../paths.js') +const rolesModule = require('../utils/roles') +const { isValidEmail } = require('../utils/email-tools.js') + +const formatServicePathsFor = require('../utils/format-service-paths-for') + +const messages = { + emailAlreadyInUse: 'Email already in use', + inviteError: 'Unable to send invitation at this time', + emailConflict: (email, externalServiceId) => { + return { + error: { + title: 'This person has already been invited', + message: `You cannot send an invitation to ${email} because they have received one already, or may be an existing team member.` + }, + link: { + link: formatServicePathsFor(paths.service.teamMembers.index, externalServiceId), + text: 'View all team members' + }, + enable_link: true + } + } +} + +function index (req, res) { + let roles = rolesModule.roles + const externalServiceId = req.service.externalId + const teamMemberIndexLink = formatServicePathsFor(paths.service.teamMembers.index, externalServiceId) + const teamMemberInviteSubmitLink = formatServicePathsFor(paths.service.teamMembers.invite, externalServiceId) + const serviceHasAgentInitiatedMotoEnabled = req.service.agentInitiatedMotoEnabled + const invitee = lodash.get(req, 'session.pageData.invitee', '') + let data = { + teamMemberIndexLink: teamMemberIndexLink, + teamMemberInviteSubmitLink: teamMemberInviteSubmitLink, + serviceHasAgentInitiatedMotoEnabled: serviceHasAgentInitiatedMotoEnabled, + admin: { id: roles['admin'].extId }, + viewAndRefund: { id: roles['view-and-refund'].extId }, + view: { id: roles['view-only'].extId }, + viewAndInitiateMoto: { id: roles['view-and-initiate-moto'].extId }, + viewRefundAndInitiateMoto: { id: roles['view-refund-and-initiate-moto'].extId }, + invitee + } + + return response(req, res, 'team-members/team-member-invite', data) +} + +async function invite (req, res, next) { + const senderId = req.user.externalId + const externalServiceId = req.service.externalId + const invitee = req.body['invitee-email'].trim() + const roleId = parseInt(req.body['role-input']) + + const role = rolesModule.getRoleByExtId(roleId) + + if (!isValidEmail(invitee)) { + req.flash('genericError', 'Enter a valid email address') + lodash.set(req, 'session.pageData', { invitee }) + res.redirect(303, formatServicePathsFor(paths.service.teamMembers.invite, externalServiceId)) + } else if (!role) { + next(new Error(`Cannot identify role from user input ${roleId}`)) + } else { + try { + await userService.createInviteToJoinService(invitee, senderId, externalServiceId, role.name) + if (lodash.has(req, 'session.pageData.invitee')) { + delete req.session.pageData.invitee + } + req.flash('generic', `Invite sent to ${invitee}`) + res.redirect(303, formatServicePathsFor(paths.service.teamMembers.index, externalServiceId)) + } catch (err) { + switch (err.errorCode) { + case 412: + response(req, res, 'error-with-link', messages.emailConflict(invitee, externalServiceId)) + break + default: + next(err) + } + } + } +} + +module.exports = { + index, + invite +} diff --git a/app/controllers/invite-user.controller.test.js b/app/controllers/invite-user.controller.test.js new file mode 100644 index 0000000000..0c292be909 --- /dev/null +++ b/app/controllers/invite-user.controller.test.js @@ -0,0 +1,26 @@ +const sinon = require('sinon') +const inviteUserController = require('./invite-user.controller') + +describe('invite user controller', () => { + it('should error for an invalid email address', async () => { + const externalServiceId = 'some-external-service-id' + const req = { + user: { externalId: 'some-ext-id', serviceIds: ['1'] }, + body: { + 'invitee-email': 'invalid@examplecom', + 'role-input': '200' + }, + service: { + externalId: externalServiceId + }, + flash: sinon.stub() + } + const res = { + redirect: sinon.stub() + } + + await inviteUserController.invite(req, res) + sinon.assert.calledWith(req.flash, 'genericError', 'Enter a valid email address') + sinon.assert.calledWith(res.redirect, 303, `/service/${externalServiceId}/team-members/invite`) + }) +}) diff --git a/app/views/team-members/team-member-invite.njk b/app/views/team-members/team-member-invite.njk new file mode 100644 index 0000000000..cd63a29429 --- /dev/null +++ b/app/views/team-members/team-member-invite.njk @@ -0,0 +1,176 @@ +{% extends "../layout.njk" %} + +{% block pageTitle %} +Invite a new team member - GOV.UK Pay +{% endblock %} + +{% block beforeContent %} + {{ super() }} + {{ + govukBackLink({ + text: "See all team members", + href: teamMemberIndexLink + }) + }} +{% endblock %} + +{% block mainContent %} +
+
+

+ Team members + Invite a team member

+
+
+
+ + {{ govukInput({ + label: { + text: "Email address" + }, + id: "invitee-email", + name: "invitee-email", + classes: "govuk-!-width-two-thirds", + type: "email", + autocomplete: "work email", + spellcheck: false + }) }} + {% if serviceHasAgentInitiatedMotoEnabled %} + {{ govukRadios({ + idPrefix: "role-input", + name: "role-input", + fieldset: { + legend: { + text: "Permission level", + classes: "govuk-label--s" + } + }, + items: [ + { + value: admin.id, + text: "Administrator", + label: { + classes: "govuk-label--s" + }, + hint: { + html: "View transactions
+ Refund payments
+ Take telephone payments
+ Manage settings" + } + }, + { + value: viewAndRefund.id, + text: "View and refund", + label: { + classes: "govuk-label--s" + }, + hint: { + html: "View transactions
+ Refund payments
+ Cannot take telephone payments
+ Cannot manage settings" + } + }, + { + value: view.id, + text: "View only", + label: { + classes: "govuk-label--s" + }, + checked: true, + hint: { + html: "View transactions
+ Cannot refund payments
+ Cannot take telephone payments
+ Cannot manage settings" + } + }, + { + value: viewAndInitiateMoto.id, + text: "View and take telephone payments", + label: { + classes: "govuk-label--s" + }, + hint: { + html: "View transactions
+ Cannot refund payments
+ Take telephone payments
+ Cannot manage settings" + } + }, + { + value: viewRefundAndInitiateMoto.id, + text: "View, refund and take telephone payments", + label: { + classes: "govuk-label--s" + }, + hint: { + html: "View transactions
+ Refund payments
+ Take telephone payments
+ Cannot manage settings" + } + } + ] + }) }} + {% else %} + {{ govukRadios({ + idPrefix: "role-input", + name: "role-input", + fieldset: { + legend: { + text: "Permission level", + classes: "govuk-label--s" + } + }, + items: [ + { + value: admin.id, + text: "Administrator", + label: { + classes: "govuk-label--s" + }, + hint: { + html: "View transactions
+ Refund payments
+ Manage settings" + } + }, + { + value: viewAndRefund.id, + text: "View and refund", + label: { + classes: "govuk-label--s" + }, + hint: { + html: "View transactions
+ Refund payments
+ Cannot manage settings" + } + }, + { + value: view.id, + text: "View only", + label: { + classes: "govuk-label--s" + }, + checked: true, + hint: { + html: "View transactions
+ Cannot refund payments
+ Cannot manage settings" + } + } + ] + }) }} + {% endif %} + + {{ govukButton({ text: "Send invitation email" }) }} +

+ + Cancel + +

+
+{% endblock %} diff --git a/test/cypress/integration/demo-payment/mock-cards-stripe.cy.js b/test/cypress/integration/demo-payment/mock-cards-stripe.cy.js new file mode 100644 index 0000000000..72206035e6 --- /dev/null +++ b/test/cypress/integration/demo-payment/mock-cards-stripe.cy.js @@ -0,0 +1,49 @@ +'use strict' + +const userStubs = require('../../stubs/user-stubs') +const gatewayAccountStubs = require('../../stubs/gateway-account-stubs') +const stripeAccountSetupStubs = require('../../stubs/stripe-account-setup-stub') +const userExternalId = 'cd0fa54cf3b7408a80ae2f1b93e7c16e' // pragma: allowlist secret +const gatewayAccountId = '42' +const gatewayAccountExternalId = 'a-valid-external-id' + +function setupYourPspStubs (opts = {}) { + const user = userStubs.getUserSuccess({ userExternalId, gatewayAccountId }) + + const gatewayAccountByExternalId = gatewayAccountStubs.getGatewayAccountByExternalIdSuccess({ + gatewayAccountId, + gatewayAccountExternalId, + type: 'test', + paymentProvider: 'stripe' + }) + + const stripeAccountSetup = stripeAccountSetupStubs.getGatewayAccountStripeSetupSuccess({ + gatewayAccountId + }) + + const stubs = [ + user, + gatewayAccountByExternalId, + stripeAccountSetup + ] + + cy.task('setupStubs', stubs) +} + +describe('Show Mock cards screen for stripe accounts', () => { + beforeEach(() => { + cy.setEncryptedCookies(userExternalId) + }) + + it('should display stripe settings page correctly', () => { + setupYourPspStubs() + cy.visit(`/account/${gatewayAccountExternalId}/settings`) + cy.log('Continue to Make a demo payment page via Dashboard') + cy.get('a').contains('Dashboard').click() + cy.get('a').contains('Make a demo payment').click() + cy.log('Continue to Mock Cards page') + cy.get('a').contains('Continue').click() + cy.get('h1').should('have.text', 'Mock card numbers') + cy.get('p').contains(/^4000058260000005/) + }) +}) diff --git a/test/integration/invite-users.controller.ft.test.js b/test/integration/invite-users.controller.ft.test.js new file mode 100644 index 0000000000..9f1b0b1b32 --- /dev/null +++ b/test/integration/invite-users.controller.ft.test.js @@ -0,0 +1,111 @@ +const path = require('path') +const nock = require('nock') +const getApp = require(path.join(__dirname, '/../../server.js')).getApp +const supertest = require('supertest') +const session = require(path.join(__dirname, '/../test-helpers/mock-session.js')) +const csrf = require('csrf') +const chai = require('chai') +const roles = require('../../app/utils/roles').roles +const paths = require(path.join(__dirname, '/../../app/paths.js')) +const inviteFixtures = require(path.join(__dirname, '/../fixtures/invite.fixtures')) + +const expect = chai.expect +const adminusersMock = nock(process.env.ADMINUSERS_URL) + +const formatServicePathsFor = require('../../app/utils/format-service-paths-for') + +describe('invite user controller', function () { + const userInSession = session.getUser({}) + const EXTERNAL_SERVICE_ID = userInSession.serviceRoles[0].service.externalId + userInSession.serviceRoles[0].role.permissions.push({ name: 'users-service:create' }) + const INVITE_RESOURCE = `/v1/api/invites/create-invite-to-join-service` + + describe('invite user index view', function () { + it('should display invite page', function (done) { + const app = session.getAppWithLoggedInUser(getApp(), userInSession) + + supertest(app) + .get(formatServicePathsFor(paths.service.teamMembers.invite, EXTERNAL_SERVICE_ID)) + .set('Accept', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body.teamMemberIndexLink).to.equal(formatServicePathsFor(paths.service.teamMembers.index, EXTERNAL_SERVICE_ID)) + expect(res.body.teamMemberInviteSubmitLink).to.equal(formatServicePathsFor(paths.service.teamMembers.invite, EXTERNAL_SERVICE_ID)) + expect(res.body.admin.id).to.equal(roles['admin'].extId) + expect(res.body.viewAndRefund.id).to.equal(roles['view-and-refund'].extId) + expect(res.body.view.id).to.equal(roles['view-only'].extId) + expect(res.body.viewAndInitiateMoto.id).to.equal(roles['view-and-initiate-moto'].extId) + expect(res.body.viewRefundAndInitiateMoto.id).to.equal(roles['view-refund-and-initiate-moto'].extId) + }) + .end(done) + }) + }) + + describe('invite user', function () { + it('should invite a new team member successfully', function (done) { + const validInvite = inviteFixtures.validCreateInviteToJoinServiceRequest() + adminusersMock.post(INVITE_RESOURCE) + .reply(201, inviteFixtures.validInviteResponse(validInvite)) + const app = session.getAppWithLoggedInUser(getApp(), userInSession) + + supertest(app) + .post(formatServicePathsFor(paths.service.teamMembers.invite, EXTERNAL_SERVICE_ID)) + .set('Accept', 'application/json') + .set('Content-Type', 'application/x-www-form-urlencoded') + .set('x-request-id', 'bob') + .send({ + 'invitee-email': 'invitee@example.com', + 'role-input': roles['admin'].extId, + csrfToken: csrf().create('123') + }) + .expect(303, {}) + .expect('Location', formatServicePathsFor(paths.service.teamMembers.index, EXTERNAL_SERVICE_ID)) + .end(done) + }) + + it('should error if the user is already invited/exists', function (done) { + const existingUser = 'existing-user@example.com' + adminusersMock.post(INVITE_RESOURCE) + .reply(412, inviteFixtures.conflictingInviteResponseWhenEmailUserAlreadyCreated(existingUser)) + const app = session.getAppWithLoggedInUser(getApp(), userInSession) + + supertest(app) + .post(formatServicePathsFor(paths.service.teamMembers.invite, EXTERNAL_SERVICE_ID)) + .set('Accept', 'application/json') + .set('Content-Type', 'application/x-www-form-urlencoded') + .set('x-request-id', 'bob') + .send({ + 'invitee-email': existingUser, + 'role-input': roles['admin'].extId, + csrfToken: csrf().create('123') + }) + .expect(200) + .expect((res) => { + expect(res.body.error.message).to.include(existingUser) + }) + .end(done) + }) + + it('should error on unknown role externalId', function (done) { + const unknownRoleId = '999' + + const app = session.getAppWithLoggedInUser(getApp(), userInSession) + + supertest(app) + .post(formatServicePathsFor(paths.service.teamMembers.invite, EXTERNAL_SERVICE_ID)) + .set('Accept', 'application/json') + .set('Content-Type', 'application/x-www-form-urlencoded') + .set('x-request-id', 'bob') + .send({ + 'invitee-email': 'invitee@example.com', + 'role-input': unknownRoleId, + csrfToken: csrf().create('123') + }) + .expect(500) + .expect((res) => { + expect(res.body.message).to.equal('There is a problem with the payments platform. Please contact the support team.') + }) + .end(done) + }) + }) +}) diff --git a/test/ui/invite-user.ui.test.js b/test/ui/invite-user.ui.test.js new file mode 100644 index 0000000000..3ca589c8ff --- /dev/null +++ b/test/ui/invite-user.ui.test.js @@ -0,0 +1,85 @@ +let path = require('path') +let renderTemplate = require(path.join(__dirname, '/../test-helpers/html-assertions.js')).render +let paths = require('../../app/paths.js') + +const formatServicePathsFor = require('../../app/utils/format-service-paths-for') + +describe('Invite a team member view', function () { + it('should render the standard invite team member view', function () { + const externalServiceId = 'some-external-id' + const teamMemberIndexLink = formatServicePathsFor(paths.service.teamMembers.index, externalServiceId) + const teamMemberInviteSubmitLink = formatServicePathsFor(paths.service.teamMembers.invite, externalServiceId) + + let templateData = { + teamMemberIndexLink: teamMemberIndexLink, + teamMemberInviteSubmitLink: teamMemberInviteSubmitLink, + admin: { id: 2 }, + viewAndRefund: { id: 3 }, + view: { id: 4 }, + viewAndInitiateMoto: { id: 5 }, + viewRefundAndInitiateMoto: { id: 6 }, + serviceHasAgentInitiatedMotoEnabled: false + } + + let body = renderTemplate('team-members/team-member-invite', templateData) + + body.should.containSelector('.govuk-back-link').withAttribute('href', teamMemberIndexLink) + body.should.containSelector('form#invite-member-form').withAttribute('action', teamMemberInviteSubmitLink) + body.should.containSelector('#role-input') + .withAttribute('type', 'radio') + .withAttribute('value', '2') + .withNoAttribute('checked') + body.should.containSelector('#role-input-2') + .withAttribute('type', 'radio') + .withAttribute('value', '3') + .withNoAttribute('checked') + body.should.containSelector('#role-input-3') + .withAttribute('type', 'radio') + .withAttribute('value', '4') + .withAttribute('checked') + body.should.not.containSelector('#role-input-4') + body.should.not.containSelector('#role-input-5') + }) + + it('should render the agent-initiated-MOTO-enhanced invite team member view', function () { + const externalServiceId = 'some-external-id' + const teamMemberIndexLink = formatServicePathsFor(paths.service.teamMembers.index, externalServiceId) + const teamMemberInviteSubmitLink = formatServicePathsFor(paths.service.teamMembers.invite, externalServiceId) + + let templateData = { + teamMemberIndexLink: teamMemberIndexLink, + teamMemberInviteSubmitLink: teamMemberInviteSubmitLink, + admin: { id: 2 }, + viewAndRefund: { id: 3 }, + view: { id: 4 }, + viewAndInitiateMoto: { id: 5 }, + viewRefundAndInitiateMoto: { id: 6 }, + serviceHasAgentInitiatedMotoEnabled: true + } + + let body = renderTemplate('team-members/team-member-invite', templateData) + + body.should.containSelector('.govuk-back-link').withAttribute('href', teamMemberIndexLink) + body.should.containSelector('form#invite-member-form').withAttribute('action', teamMemberInviteSubmitLink) + body.should.containSelector('#role-input') + .withAttribute('type', 'radio') + .withAttribute('value', '2') + .withNoAttribute('checked') + body.should.containSelector('#role-input-2') + .withAttribute('type', 'radio') + .withAttribute('value', '3') + .withNoAttribute('checked') + body.should.containSelector('#role-input-3') + .withAttribute('type', 'radio') + .withAttribute('value', '4') + .withAttribute('checked') + body.should.containSelector('#role-input-4') + .withAttribute('type', 'radio') + .withAttribute('value', '5') + .withNoAttribute('checked') + body.should.containSelector('#role-input-5') + .withAttribute('type', 'radio') + .withAttribute('value', '6') + .withNoAttribute('checked') + }) +}) From 3fbd411b18951102816a3405ca8db9a3b78049eb Mon Sep 17 00:00:00 2001 From: "ola.tomoloju" Date: Tue, 24 Oct 2023 12:50:05 +0100 Subject: [PATCH 12/18] PP-10451 Handle gateway timeout error during transaction search. Convert Nock 500 and 504 Integration Test scenarios to Cypress Tests --- app/errors.js | 11 ++- app/middleware/error-handler.js | 19 +++-- app/services/transaction.service.js | 17 ++-- .../transactions/transaction-search.cy.js | 82 ++++++++----------- 4 files changed, 68 insertions(+), 61 deletions(-) diff --git a/app/errors.js b/app/errors.js index 360448470e..ec17b01992 100644 --- a/app/errors.js +++ b/app/errors.js @@ -27,6 +27,14 @@ class GatewayTimeoutError extends Error { } } +class GenericServerError extends Error { + constructor (message) { + super(message) + this.name = this.constructor.name + Error.captureStackTrace(this, this.constructor) + } +} + /** * Thrown when there is no authentication session for the user. */ @@ -105,5 +113,6 @@ module.exports = { InvalidRegistationStateError, InvalidConfigurationError, ExpiredInviteError, - GatewayTimeoutError + GatewayTimeoutError, + GenericServerError } diff --git a/app/middleware/error-handler.js b/app/middleware/error-handler.js index 72caff8d74..50ddb140d1 100644 --- a/app/middleware/error-handler.js +++ b/app/middleware/error-handler.js @@ -15,8 +15,7 @@ const { InvalidRegistationStateError, InvalidConfigurationError, ExpiredInviteError, - RESTClientError, - GatewayTimeoutError + RESTClientError, GatewayTimeoutError, GenericServerError } = require('../errors') const paths = require('../paths') const { renderErrorView, response } = require('../utils/response') @@ -81,11 +80,6 @@ module.exports = function errorHandler (err, req, res, next) { return renderErrorView(req, res, 'There is a problem with the payments platform. Please contact the support team', 400) } - if (err instanceof GatewayTimeoutError) { - logger.info('Gateway Time out Error occurred on Transactions Search Page. Rendering error page') - return renderErrorView(req, res, err.message, 504) - } - if (err instanceof RESTClientError) { logger.info(`Unhandled REST client error caught: ${err.message}`, { service: err.service, @@ -101,6 +95,17 @@ module.exports = function errorHandler (err, req, res, next) { stack: err.stack }) } + + if (err instanceof GatewayTimeoutError) { + logger.info('Gateway Time out Error occurred on Transactions Search Page. Rendering error page') + return renderErrorView(req, res, err.message, 504) + } + + if (err instanceof GenericServerError) { + logger.info('General Error occurred on Transactions Search Page. Rendering error page') + return renderErrorView(req, res, err.message, 500) + } + Sentry.captureException(err) renderErrorView(req, res, 'There is a problem with the payments platform. Please contact the support team.', 500) } diff --git a/app/services/transaction.service.js b/app/services/transaction.service.js index df68b761e3..0d45c243da 100644 --- a/app/services/transaction.service.js +++ b/app/services/transaction.service.js @@ -11,7 +11,7 @@ const getQueryStringForParams = require('../utils/get-query-string-for-params') const userService = require('../services/user.service') const transactionView = require('../utils/transaction-view') const errorIdentifier = require('../models/error-identifier') -const { GatewayTimeoutError } = require('../errors') +const { GatewayTimeoutError, GenericServerError } = require('../errors') const connector = new ConnectorClient(process.env.CONNECTOR_URL) @@ -25,11 +25,7 @@ const searchLedger = async function searchLedger (gatewayAccountIds = [], filter try { return await Ledger.transactions(gatewayAccountIds, filters) } catch (error) { - if (error.errorCode === 504) { - throw new GatewayTimeoutError('Your request has timed out. Please apply more filters and try again.') - } else { - throw new Error('Unable to retrieve list of transactions or card types.') - } + throw handleErrorForFailedSearch(error) } } @@ -152,6 +148,15 @@ function getStatusCodeForError (err, response) { return status } +function handleErrorForFailedSearch (err, response) { + const code = (response || {}).statusCode || (err || {}).errorCode + if (code === 504) { + return new GatewayTimeoutError('Your request has timed out. Please apply more filters and try again') + } else { + return new GenericServerError('Unable to retrieve list of transactions or card types') + } +} + module.exports = { search: searchLedger, csvSearchUrl, diff --git a/test/cypress/integration/transactions/transaction-search.cy.js b/test/cypress/integration/transactions/transaction-search.cy.js index 9363e6a4d3..7f95c01b0d 100644 --- a/test/cypress/integration/transactions/transaction-search.cy.js +++ b/test/cypress/integration/transactions/transaction-search.cy.js @@ -430,7 +430,6 @@ describe('Transactions List', () => { cy.get('#download-transactions-link').should('have.attr', 'href', `/account/a-valid-external-id/transactions/download?dispute_states=needs_response&dispute_states=under_review`) }) }) - describe('csv download link', () => { it('should not display csv download link when results >5k and no filter applied', function () { cy.task('setupStubs', [ @@ -463,50 +462,32 @@ describe('Transactions List', () => { }) }) - describe('Should display relevant error page on search failure ', () => { - it('should show error message on a bad request while retrieving the list of transactions', () => { + describe('Should display an error pages on search failure ', () => { + it('should display a generic error page, if a 500 error response is returned when search is done', () => { cy.task('setupStubs', [ ...sharedStubs(), transactionsStubs.getLedgerTransactionsSuccess({ gatewayAccountId, transactions: unfilteredTransactions }) ]) cy.visit(transactionsUrl, { failOnStatusCode: false }) - cy.task('clearStubs') - - cy.task('setupStubs', [ - ...sharedStubs(), - transactionsStubs.getLedgerTransactionsFailure( - { - account_id: gatewayAccountId, - limit_total: 'true', - limit_total_size: '5001', - from_date: '', - to_date: '', - page: '1', - display_size: '100' - }, - 400) - ]) + // 1. Filtering FROM + // Ensure both the date/time pickers aren't showing + cy.get('.datepicker').should('not.exist') + cy.get('.ui-timepicker-wrapper').should('not.exist') - // Click the filter button - cy.get('#filter').click() + // Fill in a from date + cy.get('#fromDate').type('03/5/2018') - // Ensure that transaction list is not displayed - cy.get('#transactions-list tbody').should('not.exist') + // Fill in a from time + cy.get('#fromTime').type('01:00:00') - // Ensure an error message header is displayed - cy.get('h1').contains('An error occurred') + // 2. Filtering TO - // Ensure a generic error message is displayed - cy.get('#errorMsg').contains('There is a problem with the payments platform. Please contact the support team.') - }) + // Fill in a to date + cy.get('#toDate').type('03/5/2023') - it('should display the generic error page, if an internal server error occurs while retrieving the list of transactions', () => { - cy.task('setupStubs', [ - ...sharedStubs(), - transactionsStubs.getLedgerTransactionsSuccess({ gatewayAccountId, transactions: unfilteredTransactions }) - ]) - cy.visit(transactionsUrl, { failOnStatusCode: false }) + // Fill in a to time + cy.get('#toTime').type('01:00:00') cy.task('clearStubs') @@ -517,8 +498,8 @@ describe('Transactions List', () => { account_id: gatewayAccountId, limit_total: 'true', limit_total_size: '5001', - from_date: '', - to_date: '', + from_date: '2018-05-03T00:00:00.000Z', + to_date: '2023-05-03T00:00:01.000Z', page: '1', display_size: '100' }, @@ -531,28 +512,37 @@ describe('Transactions List', () => { // Ensure that transaction list is not displayed cy.get('#transactions-list tbody').should('not.exist') - // Ensure an error message header is displayed - cy.get('h1').contains('An error occurred') - // Ensure a generic error message is displayed - cy.get('#errorMsg').contains('There is a problem with the payments platform. Please contact the support team.') + cy.get('h1').contains('An error occurred') + cy.get('#errorMsg').contains('Unable to retrieve list of transactions or card types') }) - it('should display the gateway timeout error page, if a gateway timeout error occurs while retrieving the list of transactions', () => { + it('should display a gateway timeout error page, if a 504 error response is returned when search is done', () => { cy.task('setupStubs', [ ...sharedStubs(), transactionsStubs.getLedgerTransactionsSuccess({ gatewayAccountId, transactions: unfilteredTransactions }) ]) - cy.visit(transactionsUrl, { failOnStatusCode: false }) - // Fill from and to date + // 1. Filtering FROM + // Ensure both the date/time pickers aren't showing + cy.get('.datepicker').should('not.exist') + cy.get('.ui-timepicker-wrapper').should('not.exist') + + // Fill in a from date cy.get('#fromDate').type('03/5/2018') + + // Fill in a from time cy.get('#fromTime').type('01:00:00') + + // 2. Filtering TO + + // Fill in a to date cy.get('#toDate').type('03/5/2023') + + // Fill in a to time cy.get('#toTime').type('01:00:00') - // 1. Filtering cy.task('clearStubs') cy.task('setupStubs', [ @@ -576,10 +566,8 @@ describe('Transactions List', () => { // Ensure that transaction list is not displayed cy.get('#transactions-list tbody').should('not.exist') - // Ensure an error message header is displayed - cy.get('h1').contains('An error occurred') - // Ensure a gateway timeout error message is displayed + cy.get('h1').contains('An error occurred') cy.get('#errorMsg').contains('Your request has timed out. Please apply more filters and try again') }) }) From 208ec91572759d6bc79ff05fed9c7620774cd7ca Mon Sep 17 00:00:00 2001 From: "ola.tomoloju" Date: Wed, 25 Oct 2023 13:00:20 +0100 Subject: [PATCH 13/18] PP-10451 Handle gateway timeout error during transactions download. Writing test scenario as Cypress Tests --- .../transaction-download.controller.js | 9 +++- app/services/transaction.service.js | 4 +- .../transactions/transaction-search.cy.js | 46 ++++++++++++++++++- test/cypress/stubs/transaction-stubs.js | 10 +++- 4 files changed, 64 insertions(+), 5 deletions(-) diff --git a/app/controllers/transactions/transaction-download.controller.js b/app/controllers/transactions/transaction-download.controller.js index fe43274846..fffa4e1ba0 100644 --- a/app/controllers/transactions/transaction-download.controller.js +++ b/app/controllers/transactions/transaction-download.controller.js @@ -22,7 +22,14 @@ const fetchTransactionCsvWithHeader = function fetchTransactionCsvWithHeader (re transactionService.logCsvFileStreamComplete(timestampStreamStart, filters, [accountId], req.user, false, req.account.type === 'live') res.end() } - const error = () => renderErrorView(req, res, 'Unable to download list of transactions.') + const error = () => { + const code = (res || {}).statusCode + if (code === 504) { + renderErrorView(req, res, 'Your request has timed out. Please apply more filters and try again.') + } else { + renderErrorView(req, res, 'Unable to download list of transactions.') + } + } const client = new Stream(data, complete, error) res.setHeader('Content-disposition', `attachment; filename="${name}"`) diff --git a/app/services/transaction.service.js b/app/services/transaction.service.js index 0d45c243da..cd46da5012 100644 --- a/app/services/transaction.service.js +++ b/app/services/transaction.service.js @@ -151,9 +151,9 @@ function getStatusCodeForError (err, response) { function handleErrorForFailedSearch (err, response) { const code = (response || {}).statusCode || (err || {}).errorCode if (code === 504) { - return new GatewayTimeoutError('Your request has timed out. Please apply more filters and try again') + return new GatewayTimeoutError('Your request has timed out. Please apply more filters and try again.') } else { - return new GenericServerError('Unable to retrieve list of transactions or card types') + return new GenericServerError('Unable to retrieve list of transactions or card types.') } } diff --git a/test/cypress/integration/transactions/transaction-search.cy.js b/test/cypress/integration/transactions/transaction-search.cy.js index 7f95c01b0d..39d8c3f687 100644 --- a/test/cypress/integration/transactions/transaction-search.cy.js +++ b/test/cypress/integration/transactions/transaction-search.cy.js @@ -462,7 +462,7 @@ describe('Transactions List', () => { }) }) - describe('Should display an error pages on search failure ', () => { + describe('Should display relevant error page on search failure ', () => { it('should display a generic error page, if a 500 error response is returned when search is done', () => { cy.task('setupStubs', [ ...sharedStubs(), @@ -571,4 +571,48 @@ describe('Transactions List', () => { cy.get('#errorMsg').contains('Your request has timed out. Please apply more filters and try again') }) }) + + describe('Should display relevant error page, when failure occurs when downloading transactions', () => { + it.only('Should display gateway timeout error page, when failure occurs when downloading transactions', () => { + cy.task('setupStubs', [ + ...sharedStubs(), + transactionsStubs.getLedgerTransactionsSuccess({ + gatewayAccountId, + transactions: unfilteredTransactions, + transactionLength: 1000 + }) + ]) + cy.visit(transactionsUrl) + + // Ensure the transactions list has the right number of items + cy.get('#transactions-list tbody').find('tr').should('have.length', unfilteredTransactions.length) + + // Ensure the values are displayed correctly + cy.get('#transactions-list tbody').first().find('td').eq(1).should('have.text', convertPenceToPoundsFormatted(unfilteredTransactions[0].amount)) + cy.get('#transactions-list tbody').find('tr').eq(1).find('td').eq(1).should('have.text', convertPenceToPoundsFormatted(unfilteredTransactions[1].amount)) + + // Ensure the card fee is displayed correctly + cy.get('#transactions-list tbody').find('tr').eq(2).find('td').eq(1).should('contain', convertPenceToPoundsFormatted(unfilteredTransactions[2].total_amount)).and('contain', '(with card fee)') + + cy.task('clearStubs') + + cy.task('setupStubs', [ + ...sharedStubs(), + transactionsStubs.getLedgerTransactionsDownloadFailure( + { account_id: gatewayAccountId }, + 504) + ]) + + // TODO Results in Cypress Timeout error + // cy.get('#download-transactions-link').click() + + // TODO Assertions + // Ensure that transaction list is not displayed + // cy.get('#transactions-list tbody').should('not.exist') + + // Ensure a gateway timeout error message is displayed + // cy.get('h1').contains('An error occurred') + // cy.get('#errorMsg').contains('Your request has timed out. Please apply more filters and try again') + }) + }) }) diff --git a/test/cypress/stubs/transaction-stubs.js b/test/cypress/stubs/transaction-stubs.js index e9378653b7..11f1103dc0 100644 --- a/test/cypress/stubs/transaction-stubs.js +++ b/test/cypress/stubs/transaction-stubs.js @@ -105,6 +105,13 @@ function getLedgerTransactionsFailure (opts, responseCode) { } }) } +function getLedgerTransactionsDownloadFailure (opts, responseCode) { + const path = `/v1/transaction` + return stubBuilder('GET', path, responseCode, { + query: { + account_id: opts.account_id + } }) +} module.exports = { getLedgerEventsSuccess, getLedgerTransactionSuccess, @@ -113,5 +120,6 @@ module.exports = { postRefundSuccess, postRefundAmountNotAvailable, getTransactionsSummarySuccess, - getLedgerTransactionsFailure + getLedgerTransactionsFailure, + getLedgerTransactionsDownloadFailure } From 01b56bf585e5e74ae98c683bae0ae160eb8feb48 Mon Sep 17 00:00:00 2001 From: "ola.tomoloju" Date: Thu, 26 Oct 2023 13:22:25 +0100 Subject: [PATCH 14/18] PP-10451 Handle gateway timeout error during transactions download. Writing test scenario as Cypress Tests for 400, 500 and 504 responses and removing nock mocking logic --- .../transactions/transaction-search.cy.js | 80 +++++++++++-------- test/cypress/stubs/transaction-stubs.js | 10 +-- 2 files changed, 46 insertions(+), 44 deletions(-) diff --git a/test/cypress/integration/transactions/transaction-search.cy.js b/test/cypress/integration/transactions/transaction-search.cy.js index 39d8c3f687..9b6ba2ffb3 100644 --- a/test/cypress/integration/transactions/transaction-search.cy.js +++ b/test/cypress/integration/transactions/transaction-search.cy.js @@ -430,6 +430,7 @@ describe('Transactions List', () => { cy.get('#download-transactions-link').should('have.attr', 'href', `/account/a-valid-external-id/transactions/download?dispute_states=needs_response&dispute_states=under_review`) }) }) + describe('csv download link', () => { it('should not display csv download link when results >5k and no filter applied', function () { cy.task('setupStubs', [ @@ -463,7 +464,7 @@ describe('Transactions List', () => { }) describe('Should display relevant error page on search failure ', () => { - it('should display a generic error page, if a 500 error response is returned when search is done', () => { + it('should show error message on a bad request while retrieving the list of transactions', () => { cy.task('setupStubs', [ ...sharedStubs(), transactionsStubs.getLedgerTransactionsSuccess({ gatewayAccountId, transactions: unfilteredTransactions }) @@ -476,7 +477,7 @@ describe('Transactions List', () => { cy.get('.ui-timepicker-wrapper').should('not.exist') // Fill in a from date - cy.get('#fromDate').type('03/5/2018') + cy.get('#fromDate').type('33/18/2018') // Fill in a from time cy.get('#fromTime').type('01:00:00') @@ -484,7 +485,7 @@ describe('Transactions List', () => { // 2. Filtering TO // Fill in a to date - cy.get('#toDate').type('03/5/2023') + cy.get('#toDate').type('33/23/2023') // Fill in a to time cy.get('#toTime').type('01:00:00') @@ -498,12 +499,12 @@ describe('Transactions List', () => { account_id: gatewayAccountId, limit_total: 'true', limit_total_size: '5001', - from_date: '2018-05-03T00:00:00.000Z', - to_date: '2023-05-03T00:00:01.000Z', + from_date: '2018-18-03T00:00:00.000Z', + to_date: '2023-23-03T00:00:01.000Z', page: '1', display_size: '100' }, - 500) + 400) ]) // Click the filter button @@ -516,8 +517,7 @@ describe('Transactions List', () => { cy.get('h1').contains('An error occurred') cy.get('#errorMsg').contains('Unable to retrieve list of transactions or card types') }) - - it('should display a gateway timeout error page, if a 504 error response is returned when search is done', () => { + it('should display the generic error page, if an internal server error occurs while retrieving the list of transactions', () => { cy.task('setupStubs', [ ...sharedStubs(), transactionsStubs.getLedgerTransactionsSuccess({ gatewayAccountId, transactions: unfilteredTransactions }) @@ -557,7 +557,7 @@ describe('Transactions List', () => { page: '1', display_size: '100' }, - 504) + 500) ]) // Click the filter button @@ -566,53 +566,63 @@ describe('Transactions List', () => { // Ensure that transaction list is not displayed cy.get('#transactions-list tbody').should('not.exist') - // Ensure a gateway timeout error message is displayed + // Ensure a generic error message is displayed cy.get('h1').contains('An error occurred') - cy.get('#errorMsg').contains('Your request has timed out. Please apply more filters and try again') + cy.get('#errorMsg').contains('Unable to retrieve list of transactions or card types') }) - }) - describe('Should display relevant error page, when failure occurs when downloading transactions', () => { - it.only('Should display gateway timeout error page, when failure occurs when downloading transactions', () => { + it('should display the gateway timeout error page, if a gateway timeout error occurs while retrieving the list of transactions', () => { cy.task('setupStubs', [ ...sharedStubs(), - transactionsStubs.getLedgerTransactionsSuccess({ - gatewayAccountId, - transactions: unfilteredTransactions, - transactionLength: 1000 - }) + transactionsStubs.getLedgerTransactionsSuccess({ gatewayAccountId, transactions: unfilteredTransactions }) ]) - cy.visit(transactionsUrl) + cy.visit(transactionsUrl, { failOnStatusCode: false }) - // Ensure the transactions list has the right number of items - cy.get('#transactions-list tbody').find('tr').should('have.length', unfilteredTransactions.length) + // 1. Filtering FROM + // Ensure both the date/time pickers aren't showing + cy.get('.datepicker').should('not.exist') + cy.get('.ui-timepicker-wrapper').should('not.exist') - // Ensure the values are displayed correctly - cy.get('#transactions-list tbody').first().find('td').eq(1).should('have.text', convertPenceToPoundsFormatted(unfilteredTransactions[0].amount)) - cy.get('#transactions-list tbody').find('tr').eq(1).find('td').eq(1).should('have.text', convertPenceToPoundsFormatted(unfilteredTransactions[1].amount)) + // Fill in a from date + cy.get('#fromDate').type('03/5/2018') - // Ensure the card fee is displayed correctly - cy.get('#transactions-list tbody').find('tr').eq(2).find('td').eq(1).should('contain', convertPenceToPoundsFormatted(unfilteredTransactions[2].total_amount)).and('contain', '(with card fee)') + // Fill in a from time + cy.get('#fromTime').type('01:00:00') + + // 2. Filtering TO + + // Fill in a to date + cy.get('#toDate').type('03/5/2023') + + // Fill in a to time + cy.get('#toTime').type('01:00:00') cy.task('clearStubs') cy.task('setupStubs', [ ...sharedStubs(), - transactionsStubs.getLedgerTransactionsDownloadFailure( - { account_id: gatewayAccountId }, + transactionsStubs.getLedgerTransactionsFailure( + { + account_id: gatewayAccountId, + limit_total: 'true', + limit_total_size: '5001', + from_date: '2018-05-03T00:00:00.000Z', + to_date: '2023-05-03T00:00:01.000Z', + page: '1', + display_size: '100' + }, 504) ]) - // TODO Results in Cypress Timeout error - // cy.get('#download-transactions-link').click() + // Click the filter button + cy.get('#filter').click() - // TODO Assertions // Ensure that transaction list is not displayed - // cy.get('#transactions-list tbody').should('not.exist') + cy.get('#transactions-list tbody').should('not.exist') // Ensure a gateway timeout error message is displayed - // cy.get('h1').contains('An error occurred') - // cy.get('#errorMsg').contains('Your request has timed out. Please apply more filters and try again') + cy.get('h1').contains('An error occurred') + cy.get('#errorMsg').contains('Your request has timed out. Please apply more filters and try again') }) }) }) diff --git a/test/cypress/stubs/transaction-stubs.js b/test/cypress/stubs/transaction-stubs.js index 11f1103dc0..e9378653b7 100644 --- a/test/cypress/stubs/transaction-stubs.js +++ b/test/cypress/stubs/transaction-stubs.js @@ -105,13 +105,6 @@ function getLedgerTransactionsFailure (opts, responseCode) { } }) } -function getLedgerTransactionsDownloadFailure (opts, responseCode) { - const path = `/v1/transaction` - return stubBuilder('GET', path, responseCode, { - query: { - account_id: opts.account_id - } }) -} module.exports = { getLedgerEventsSuccess, getLedgerTransactionSuccess, @@ -120,6 +113,5 @@ module.exports = { postRefundSuccess, postRefundAmountNotAvailable, getTransactionsSummarySuccess, - getLedgerTransactionsFailure, - getLedgerTransactionsDownloadFailure + getLedgerTransactionsFailure } From 6a01f3f254155eeff6c6acd9b6dfaac34a758a9e Mon Sep 17 00:00:00 2001 From: "ola.tomoloju" Date: Fri, 27 Oct 2023 18:30:56 +0100 Subject: [PATCH 15/18] PP-10451 Handle gateway timeout error during transactions download. This change is for the single service transaction search and not the all services functionality. Converting test scenarios as Cypress Tests for 400, 500 and 504 responses and removing nock mocking logic. Download of transactions list by clicking the download link was de-scoped, so no tests were written for the download functionality. --- .../transaction-download.controller.js | 9 +---- app/services/transaction.service.js | 5 ++- .../transactions/transaction-search.cy.js | 35 ++++++------------- 3 files changed, 14 insertions(+), 35 deletions(-) diff --git a/app/controllers/transactions/transaction-download.controller.js b/app/controllers/transactions/transaction-download.controller.js index fffa4e1ba0..fe43274846 100644 --- a/app/controllers/transactions/transaction-download.controller.js +++ b/app/controllers/transactions/transaction-download.controller.js @@ -22,14 +22,7 @@ const fetchTransactionCsvWithHeader = function fetchTransactionCsvWithHeader (re transactionService.logCsvFileStreamComplete(timestampStreamStart, filters, [accountId], req.user, false, req.account.type === 'live') res.end() } - const error = () => { - const code = (res || {}).statusCode - if (code === 504) { - renderErrorView(req, res, 'Your request has timed out. Please apply more filters and try again.') - } else { - renderErrorView(req, res, 'Unable to download list of transactions.') - } - } + const error = () => renderErrorView(req, res, 'Unable to download list of transactions.') const client = new Stream(data, complete, error) res.setHeader('Content-disposition', `attachment; filename="${name}"`) diff --git a/app/services/transaction.service.js b/app/services/transaction.service.js index cd46da5012..80dfb77016 100644 --- a/app/services/transaction.service.js +++ b/app/services/transaction.service.js @@ -148,9 +148,8 @@ function getStatusCodeForError (err, response) { return status } -function handleErrorForFailedSearch (err, response) { - const code = (response || {}).statusCode || (err || {}).errorCode - if (code === 504) { +function handleErrorForFailedSearch (err) { + if (err.errorCode === 504) { return new GatewayTimeoutError('Your request has timed out. Please apply more filters and try again.') } else { return new GenericServerError('Unable to retrieve list of transactions or card types.') diff --git a/test/cypress/integration/transactions/transaction-search.cy.js b/test/cypress/integration/transactions/transaction-search.cy.js index 9b6ba2ffb3..7304f6abaa 100644 --- a/test/cypress/integration/transactions/transaction-search.cy.js +++ b/test/cypress/integration/transactions/transaction-search.cy.js @@ -471,25 +471,6 @@ describe('Transactions List', () => { ]) cy.visit(transactionsUrl, { failOnStatusCode: false }) - // 1. Filtering FROM - // Ensure both the date/time pickers aren't showing - cy.get('.datepicker').should('not.exist') - cy.get('.ui-timepicker-wrapper').should('not.exist') - - // Fill in a from date - cy.get('#fromDate').type('33/18/2018') - - // Fill in a from time - cy.get('#fromTime').type('01:00:00') - - // 2. Filtering TO - - // Fill in a to date - cy.get('#toDate').type('33/23/2023') - - // Fill in a to time - cy.get('#toTime').type('01:00:00') - cy.task('clearStubs') cy.task('setupStubs', [ @@ -499,8 +480,8 @@ describe('Transactions List', () => { account_id: gatewayAccountId, limit_total: 'true', limit_total_size: '5001', - from_date: '2018-18-03T00:00:00.000Z', - to_date: '2023-23-03T00:00:01.000Z', + from_date: '', + to_date: '', page: '1', display_size: '100' }, @@ -513,8 +494,10 @@ describe('Transactions List', () => { // Ensure that transaction list is not displayed cy.get('#transactions-list tbody').should('not.exist') - // Ensure a generic error message is displayed + // Ensure an error message header is displayed cy.get('h1').contains('An error occurred') + + // Ensure a generic error message is displayed cy.get('#errorMsg').contains('Unable to retrieve list of transactions or card types') }) it('should display the generic error page, if an internal server error occurs while retrieving the list of transactions', () => { @@ -566,8 +549,10 @@ describe('Transactions List', () => { // Ensure that transaction list is not displayed cy.get('#transactions-list tbody').should('not.exist') - // Ensure a generic error message is displayed + // Ensure an error message header is displayed cy.get('h1').contains('An error occurred') + + // Ensure a generic error message is displayed cy.get('#errorMsg').contains('Unable to retrieve list of transactions or card types') }) @@ -620,8 +605,10 @@ describe('Transactions List', () => { // Ensure that transaction list is not displayed cy.get('#transactions-list tbody').should('not.exist') - // Ensure a gateway timeout error message is displayed + // Ensure an error message header is displayed cy.get('h1').contains('An error occurred') + + // Ensure a gateway timeout error message is displayed cy.get('#errorMsg').contains('Your request has timed out. Please apply more filters and try again') }) }) From ebedcdb4acb1bab820da49c2ec7ed4d35fe1ec20 Mon Sep 17 00:00:00 2001 From: "ola.tomoloju" Date: Tue, 31 Oct 2023 17:43:06 +0000 Subject: [PATCH 16/18] PP-10451 Handle gateway timeout error during transactions download. This change is for the single service transaction search and not the all services functionality. Converting test scenarios as Cypress Tests for 400, 500 and 504 responses and removing nock mocking logic. Download of transactions list by clicking the download link was de-scoped, so no tests were written for the download functionality. --- app/errors.js | 11 +---- app/middleware/error-handler.js | 19 ++++----- app/services/transaction.service.js | 16 +++----- .../transactions/transaction-search.cy.js | 40 +++---------------- 4 files changed, 19 insertions(+), 67 deletions(-) diff --git a/app/errors.js b/app/errors.js index ec17b01992..360448470e 100644 --- a/app/errors.js +++ b/app/errors.js @@ -27,14 +27,6 @@ class GatewayTimeoutError extends Error { } } -class GenericServerError extends Error { - constructor (message) { - super(message) - this.name = this.constructor.name - Error.captureStackTrace(this, this.constructor) - } -} - /** * Thrown when there is no authentication session for the user. */ @@ -113,6 +105,5 @@ module.exports = { InvalidRegistationStateError, InvalidConfigurationError, ExpiredInviteError, - GatewayTimeoutError, - GenericServerError + GatewayTimeoutError } diff --git a/app/middleware/error-handler.js b/app/middleware/error-handler.js index 50ddb140d1..72caff8d74 100644 --- a/app/middleware/error-handler.js +++ b/app/middleware/error-handler.js @@ -15,7 +15,8 @@ const { InvalidRegistationStateError, InvalidConfigurationError, ExpiredInviteError, - RESTClientError, GatewayTimeoutError, GenericServerError + RESTClientError, + GatewayTimeoutError } = require('../errors') const paths = require('../paths') const { renderErrorView, response } = require('../utils/response') @@ -80,6 +81,11 @@ module.exports = function errorHandler (err, req, res, next) { return renderErrorView(req, res, 'There is a problem with the payments platform. Please contact the support team', 400) } + if (err instanceof GatewayTimeoutError) { + logger.info('Gateway Time out Error occurred on Transactions Search Page. Rendering error page') + return renderErrorView(req, res, err.message, 504) + } + if (err instanceof RESTClientError) { logger.info(`Unhandled REST client error caught: ${err.message}`, { service: err.service, @@ -95,17 +101,6 @@ module.exports = function errorHandler (err, req, res, next) { stack: err.stack }) } - - if (err instanceof GatewayTimeoutError) { - logger.info('Gateway Time out Error occurred on Transactions Search Page. Rendering error page') - return renderErrorView(req, res, err.message, 504) - } - - if (err instanceof GenericServerError) { - logger.info('General Error occurred on Transactions Search Page. Rendering error page') - return renderErrorView(req, res, err.message, 500) - } - Sentry.captureException(err) renderErrorView(req, res, 'There is a problem with the payments platform. Please contact the support team.', 500) } diff --git a/app/services/transaction.service.js b/app/services/transaction.service.js index 80dfb77016..df68b761e3 100644 --- a/app/services/transaction.service.js +++ b/app/services/transaction.service.js @@ -11,7 +11,7 @@ const getQueryStringForParams = require('../utils/get-query-string-for-params') const userService = require('../services/user.service') const transactionView = require('../utils/transaction-view') const errorIdentifier = require('../models/error-identifier') -const { GatewayTimeoutError, GenericServerError } = require('../errors') +const { GatewayTimeoutError } = require('../errors') const connector = new ConnectorClient(process.env.CONNECTOR_URL) @@ -25,7 +25,11 @@ const searchLedger = async function searchLedger (gatewayAccountIds = [], filter try { return await Ledger.transactions(gatewayAccountIds, filters) } catch (error) { - throw handleErrorForFailedSearch(error) + if (error.errorCode === 504) { + throw new GatewayTimeoutError('Your request has timed out. Please apply more filters and try again.') + } else { + throw new Error('Unable to retrieve list of transactions or card types.') + } } } @@ -148,14 +152,6 @@ function getStatusCodeForError (err, response) { return status } -function handleErrorForFailedSearch (err) { - if (err.errorCode === 504) { - return new GatewayTimeoutError('Your request has timed out. Please apply more filters and try again.') - } else { - return new GenericServerError('Unable to retrieve list of transactions or card types.') - } -} - module.exports = { search: searchLedger, csvSearchUrl, diff --git a/test/cypress/integration/transactions/transaction-search.cy.js b/test/cypress/integration/transactions/transaction-search.cy.js index 7304f6abaa..23a8c8eeb3 100644 --- a/test/cypress/integration/transactions/transaction-search.cy.js +++ b/test/cypress/integration/transactions/transaction-search.cy.js @@ -507,25 +507,6 @@ describe('Transactions List', () => { ]) cy.visit(transactionsUrl, { failOnStatusCode: false }) - // 1. Filtering FROM - // Ensure both the date/time pickers aren't showing - cy.get('.datepicker').should('not.exist') - cy.get('.ui-timepicker-wrapper').should('not.exist') - - // Fill in a from date - cy.get('#fromDate').type('03/5/2018') - - // Fill in a from time - cy.get('#fromTime').type('01:00:00') - - // 2. Filtering TO - - // Fill in a to date - cy.get('#toDate').type('03/5/2023') - - // Fill in a to time - cy.get('#toTime').type('01:00:00') - cy.task('clearStubs') cy.task('setupStubs', [ @@ -535,8 +516,8 @@ describe('Transactions List', () => { account_id: gatewayAccountId, limit_total: 'true', limit_total_size: '5001', - from_date: '2018-05-03T00:00:00.000Z', - to_date: '2023-05-03T00:00:01.000Z', + from_date: '', + to_date: '', page: '1', display_size: '100' }, @@ -561,27 +542,16 @@ describe('Transactions List', () => { ...sharedStubs(), transactionsStubs.getLedgerTransactionsSuccess({ gatewayAccountId, transactions: unfilteredTransactions }) ]) - cy.visit(transactionsUrl, { failOnStatusCode: false }) - // 1. Filtering FROM - // Ensure both the date/time pickers aren't showing - cy.get('.datepicker').should('not.exist') - cy.get('.ui-timepicker-wrapper').should('not.exist') + cy.visit(transactionsUrl, { failOnStatusCode: false }) - // Fill in a from date + // Fill from and to date cy.get('#fromDate').type('03/5/2018') - - // Fill in a from time cy.get('#fromTime').type('01:00:00') - - // 2. Filtering TO - - // Fill in a to date cy.get('#toDate').type('03/5/2023') - - // Fill in a to time cy.get('#toTime').type('01:00:00') + // 1. Filtering cy.task('clearStubs') cy.task('setupStubs', [ From fa9d21f25bd391361a5078c9d07d4e33a0d787da Mon Sep 17 00:00:00 2001 From: "ola.tomoloju" Date: Tue, 31 Oct 2023 18:58:05 +0000 Subject: [PATCH 17/18] PP-10451 Handle gateway timeout error during transactions download. This change is for the single service transaction search and not the all services functionality. Converting test scenarios as Cypress Tests for 400, 500 and 504 responses and removing nock mocking logic. Download of transactions list by clicking the download link was de-scoped, so no tests were written for the download functionality. --- .../integration/transactions/transaction-search.cy.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/cypress/integration/transactions/transaction-search.cy.js b/test/cypress/integration/transactions/transaction-search.cy.js index 23a8c8eeb3..9363e6a4d3 100644 --- a/test/cypress/integration/transactions/transaction-search.cy.js +++ b/test/cypress/integration/transactions/transaction-search.cy.js @@ -498,8 +498,9 @@ describe('Transactions List', () => { cy.get('h1').contains('An error occurred') // Ensure a generic error message is displayed - cy.get('#errorMsg').contains('Unable to retrieve list of transactions or card types') + cy.get('#errorMsg').contains('There is a problem with the payments platform. Please contact the support team.') }) + it('should display the generic error page, if an internal server error occurs while retrieving the list of transactions', () => { cy.task('setupStubs', [ ...sharedStubs(), @@ -534,7 +535,7 @@ describe('Transactions List', () => { cy.get('h1').contains('An error occurred') // Ensure a generic error message is displayed - cy.get('#errorMsg').contains('Unable to retrieve list of transactions or card types') + cy.get('#errorMsg').contains('There is a problem with the payments platform. Please contact the support team.') }) it('should display the gateway timeout error page, if a gateway timeout error occurs while retrieving the list of transactions', () => { From 6482761bf409e75c592af013270428e952de7dab Mon Sep 17 00:00:00 2001 From: "ola.tomoloju" Date: Wed, 1 Nov 2023 14:02:05 +0000 Subject: [PATCH 18/18] PP-10451 Handle gateway timeout error during transactions download. This change is for the single service transaction search and not the all services functionality. Converting test scenarios as Cypress Tests for 400, 500 and 504 responses and removing nock mocking logic. Download of transactions list by clicking the download link was de-scoped, so no tests were written for the download functionality. --- app/controllers/invite-user.controller.js | 4 +++- .../invite-user.controller.test.js | 23 +++++++++++++++++++ app/views/team-members/team-member-invite.njk | 4 ++-- .../demo-payment/mock-cards-stripe.cy.js | 6 ++--- .../invite-users.controller.ft.test.js | 22 ------------------ test/ui/invite-user.ui.test.js | 4 ++-- 6 files changed, 33 insertions(+), 30 deletions(-) diff --git a/app/controllers/invite-user.controller.js b/app/controllers/invite-user.controller.js index 0c978b3ca4..44c03ff02a 100644 --- a/app/controllers/invite-user.controller.js +++ b/app/controllers/invite-user.controller.js @@ -60,7 +60,9 @@ async function invite (req, res, next) { lodash.set(req, 'session.pageData', { invitee }) res.redirect(303, formatServicePathsFor(paths.service.teamMembers.invite, externalServiceId)) } else if (!role) { - next(new Error(`Cannot identify role from user input ${roleId}`)) + req.flash('genericError', 'Select the team member’s permission level') + lodash.set(req, 'session.pageData', { invitee }) + res.redirect(303, formatServicePathsFor(paths.service.teamMembers.invite, externalServiceId)) } else { try { await userService.createInviteToJoinService(invitee, senderId, externalServiceId, role.name) diff --git a/app/controllers/invite-user.controller.test.js b/app/controllers/invite-user.controller.test.js index 0c292be909..a6995fa695 100644 --- a/app/controllers/invite-user.controller.test.js +++ b/app/controllers/invite-user.controller.test.js @@ -23,4 +23,27 @@ describe('invite user controller', () => { sinon.assert.calledWith(req.flash, 'genericError', 'Enter a valid email address') sinon.assert.calledWith(res.redirect, 303, `/service/${externalServiceId}/team-members/invite`) }) + + it('should error if a role is not recognised', async () => { + const externalServiceId = 'some-external-service-id' + const unknownRoleId = '999' + const req = { + user: { externalId: 'some-ext-id', serviceIds: ['1'] }, + body: { + 'invitee-email': 'valid@example.com', + 'role-input': unknownRoleId + }, + service: { + externalId: externalServiceId + }, + flash: sinon.stub() + } + const res = { + redirect: sinon.stub() + } + + await inviteUserController.invite(req, res) + sinon.assert.calledWith(req.flash, 'genericError', 'Select the team member’s permission level') + sinon.assert.calledWith(res.redirect, 303, `/service/${externalServiceId}/team-members/invite`) + }) }) diff --git a/app/views/team-members/team-member-invite.njk b/app/views/team-members/team-member-invite.njk index cd63a29429..67e18f47c6 100644 --- a/app/views/team-members/team-member-invite.njk +++ b/app/views/team-members/team-member-invite.njk @@ -78,7 +78,7 @@ Invite a new team member - GOV.UK Pay label: { classes: "govuk-label--s" }, - checked: true, + checked: false, hint: { html: "View transactions
Cannot refund payments
@@ -155,7 +155,7 @@ Invite a new team member - GOV.UK Pay label: { classes: "govuk-label--s" }, - checked: true, + checked: false, hint: { html: "View transactions
Cannot refund payments
diff --git a/test/cypress/integration/demo-payment/mock-cards-stripe.cy.js b/test/cypress/integration/demo-payment/mock-cards-stripe.cy.js index 72206035e6..e6984ce97c 100644 --- a/test/cypress/integration/demo-payment/mock-cards-stripe.cy.js +++ b/test/cypress/integration/demo-payment/mock-cards-stripe.cy.js @@ -14,17 +14,17 @@ function setupYourPspStubs (opts = {}) { gatewayAccountId, gatewayAccountExternalId, type: 'test', - paymentProvider: 'stripe' + paymentProvider: 'stripe', }) const stripeAccountSetup = stripeAccountSetupStubs.getGatewayAccountStripeSetupSuccess({ - gatewayAccountId + gatewayAccountId, }) const stubs = [ user, gatewayAccountByExternalId, - stripeAccountSetup + stripeAccountSetup, ] cy.task('setupStubs', stubs) diff --git a/test/integration/invite-users.controller.ft.test.js b/test/integration/invite-users.controller.ft.test.js index 9f1b0b1b32..2d546e26dc 100644 --- a/test/integration/invite-users.controller.ft.test.js +++ b/test/integration/invite-users.controller.ft.test.js @@ -85,27 +85,5 @@ describe('invite user controller', function () { }) .end(done) }) - - it('should error on unknown role externalId', function (done) { - const unknownRoleId = '999' - - const app = session.getAppWithLoggedInUser(getApp(), userInSession) - - supertest(app) - .post(formatServicePathsFor(paths.service.teamMembers.invite, EXTERNAL_SERVICE_ID)) - .set('Accept', 'application/json') - .set('Content-Type', 'application/x-www-form-urlencoded') - .set('x-request-id', 'bob') - .send({ - 'invitee-email': 'invitee@example.com', - 'role-input': unknownRoleId, - csrfToken: csrf().create('123') - }) - .expect(500) - .expect((res) => { - expect(res.body.message).to.equal('There is a problem with the payments platform. Please contact the support team.') - }) - .end(done) - }) }) }) diff --git a/test/ui/invite-user.ui.test.js b/test/ui/invite-user.ui.test.js index 3ca589c8ff..44a3d9110b 100644 --- a/test/ui/invite-user.ui.test.js +++ b/test/ui/invite-user.ui.test.js @@ -36,7 +36,7 @@ describe('Invite a team member view', function () { body.should.containSelector('#role-input-3') .withAttribute('type', 'radio') .withAttribute('value', '4') - .withAttribute('checked') + .withNoAttribute('checked') body.should.not.containSelector('#role-input-4') body.should.not.containSelector('#role-input-5') }) @@ -72,7 +72,7 @@ describe('Invite a team member view', function () { body.should.containSelector('#role-input-3') .withAttribute('type', 'radio') .withAttribute('value', '4') - .withAttribute('checked') + .withNoAttribute('checked') body.should.containSelector('#role-input-4') .withAttribute('type', 'radio') .withAttribute('value', '5')