fix gocardless error handling (#439)

* fix gocardless error handling

* release note

* remove redundant tests

* tcrasset feedback
This commit is contained in:
Matt Fiddaman
2024-08-25 17:53:16 +01:00
committed by GitHub
parent 6a41d28404
commit 2b37d5a642
4 changed files with 134 additions and 181 deletions

View File

@@ -5,9 +5,10 @@ import { inspect } from 'util';
import { goCardlessService } from './services/gocardless-service.js';
import {
RequisitionNotLinked,
AccountNotLinedToRequisition,
GenericGoCardlessError,
RateLimitError,
RequisitionNotLinked,
} from './errors.js';
import { handleError } from './util/handle-error.js';
import { sha256String } from '../util/hash.js';
@@ -191,6 +192,14 @@ app.post(
reason: 'Account not linked with this requisition',
});
break;
case error instanceof RateLimitError:
sendErrorResponse({
error_type: 'RATE_LIMIT_EXCEEDED',
error_code: 'NORDIGEN_ERROR',
status: 'rejected',
reason: 'Rate limit exceeded',
});
break;
case error instanceof GenericGoCardlessError:
console.log('Something went wrong', inspect(error, { depth: null }));
sendErrorResponse({

View File

@@ -1,15 +1,16 @@
import BankFactory, { BANKS_WITH_LIMITED_HISTORY } from '../bank-factory.js';
import {
RequisitionNotLinked,
AccessDeniedError,
AccountNotLinedToRequisition,
GenericGoCardlessError,
InvalidInputDataError,
InvalidGoCardlessTokenError,
AccessDeniedError,
NotFoundError,
ResourceSuspended,
RateLimitError,
UnknownError,
ResourceSuspended,
RequisitionNotLinked,
ServiceError,
UnknownError,
} from '../errors.js';
import * as nordigenNode from 'nordigen-node';
import * as uuid from 'uuid';
@@ -35,26 +36,28 @@ const getGocardlessClient = () => {
return clients.get(hash);
};
export const handleGoCardlessError = (response) => {
switch (response.status_code) {
export const handleGoCardlessError = (error) => {
const status = error?.response?.status;
switch (status) {
case 400:
throw new InvalidInputDataError(response);
throw new InvalidInputDataError(error);
case 401:
throw new InvalidGoCardlessTokenError(response);
throw new InvalidGoCardlessTokenError(error);
case 403:
throw new AccessDeniedError(response);
throw new AccessDeniedError(error);
case 404:
throw new NotFoundError(response);
throw new NotFoundError(error);
case 409:
throw new ResourceSuspended(response);
throw new ResourceSuspended(error);
case 429:
throw new RateLimitError(response);
throw new RateLimitError(error);
case 500:
throw new UnknownError(response);
throw new UnknownError(error);
case 503:
throw new ServiceError(response);
throw new ServiceError(error);
default:
return;
throw new GenericGoCardlessError(error);
}
};
@@ -87,8 +90,11 @@ export const goCardlessService = {
if (isExpiredJwtToken(getGocardlessClient().token)) {
// Generate new access token. Token is valid for 24 hours
// Note: access_token is automatically injected to other requests after you successfully obtain it
const tokenData = await client.generateToken();
handleGoCardlessError(tokenData);
try {
await client.generateToken();
} catch (error) {
handleGoCardlessError(error);
}
}
},
@@ -261,23 +267,26 @@ export const goCardlessService = {
const institution = await goCardlessService.getInstitution(institutionId);
const bank = BankFactory(institutionId);
const response = await client.initSession({
redirectUrl: host + '/gocardless/link',
institutionId,
referenceId: uuid.v4(),
accessValidForDays: bank.accessValidForDays,
maxHistoricalDays: BANKS_WITH_LIMITED_HISTORY.includes(institutionId)
? Number(institution.transaction_total_days) >= 90
? '89'
: institution.transaction_total_days
: institution.transaction_total_days,
userLanguage: 'en',
ssn: null,
redirectImmediate: false,
accountSelection: false,
});
handleGoCardlessError(response);
let response;
try {
response = await client.initSession({
redirectUrl: host + '/gocardless/link',
institutionId,
referenceId: uuid.v4(),
accessValidForDays: bank.accessValidForDays,
maxHistoricalDays: BANKS_WITH_LIMITED_HISTORY.includes(institutionId)
? Number(institution.transaction_total_days) >= 90
? '89'
: institution.transaction_total_days
: institution.transaction_total_days,
userLanguage: 'en',
ssn: null,
redirectImmediate: false,
accountSelection: false,
});
} catch (error) {
handleGoCardlessError(error);
}
const { link, id: requisitionId } = response;
@@ -302,9 +311,14 @@ export const goCardlessService = {
*/
deleteRequisition: async (requisitionId) => {
await goCardlessService.getRequisition(requisitionId);
const response = client.deleteRequisition(requisitionId);
handleGoCardlessError(response);
let response;
try {
response = client.deleteRequisition(requisitionId);
} catch (error) {
handleGoCardlessError(error);
}
return response;
},
@@ -325,9 +339,12 @@ export const goCardlessService = {
getRequisition: async (requisitionId) => {
await goCardlessService.setToken();
const response = client.getRequisitionById(requisitionId);
handleGoCardlessError(response);
let response;
try {
response = client.getRequisitionById(requisitionId);
} catch (error) {
handleGoCardlessError(error);
}
return response;
},
@@ -338,13 +355,15 @@ export const goCardlessService = {
* @returns {Promise<import('../gocardless.types.js').DetailedAccount>}
*/
getDetailedAccount: async (accountId) => {
const [detailedAccount, metadataAccount] = await Promise.all([
client.getDetails(accountId),
client.getMetadata(accountId),
]);
handleGoCardlessError(detailedAccount);
handleGoCardlessError(metadataAccount);
let detailedAccount, metadataAccount;
try {
[detailedAccount, metadataAccount] = await Promise.all([
client.getDetails(accountId),
client.getMetadata(accountId),
]);
} catch (error) {
handleGoCardlessError(error);
}
return {
...detailedAccount.account,
@@ -361,9 +380,12 @@ export const goCardlessService = {
* @returns {Promise<import('../gocardless-node.types.js').GoCardlessAccountMetadata>}
*/
getAccountMetadata: async (accountId) => {
const response = await client.getMetadata(accountId);
handleGoCardlessError(response);
let response;
try {
response = await client.getMetadata(accountId);
} catch (error) {
handleGoCardlessError(error);
}
return response;
},
@@ -382,9 +404,12 @@ export const goCardlessService = {
* @returns {Promise<Array<import('../gocardless-node.types.js').Institution>>}
*/
getInstitutions: async (country) => {
const response = await client.getInstitutions(country);
handleGoCardlessError(response);
let response;
try {
response = await client.getInstitutions(country);
} catch (error) {
handleGoCardlessError(error);
}
return response;
},
@@ -403,9 +428,12 @@ export const goCardlessService = {
* @returns {Promise<import('../gocardless-node.types.js').Institution>}
*/
getInstitution: async (institutionId) => {
const response = await client.getInstitutionById(institutionId);
handleGoCardlessError(response);
let response;
try {
response = await client.getInstitutionById(institutionId);
} catch (error) {
handleGoCardlessError(error);
}
return response;
},
@@ -444,13 +472,16 @@ export const goCardlessService = {
* @returns {Promise<import('../gocardless.types.js').GetTransactionsResponse>}
*/
getTransactions: async ({ institutionId, accountId, startDate, endDate }) => {
const response = await client.getTransactions({
accountId,
dateFrom: startDate,
dateTo: endDate,
});
handleGoCardlessError(response);
let response;
try {
response = await client.getTransactions({
accountId,
dateFrom: startDate,
dateTo: endDate,
});
} catch (error) {
handleGoCardlessError(error);
}
const bank = BankFactory(institutionId);
response.transactions.booked = response.transactions.booked
@@ -477,9 +508,12 @@ export const goCardlessService = {
* @returns {Promise<import('../gocardless.types.js').GetBalances>}
*/
getBalances: async (accountId) => {
const response = await client.getBalances(accountId);
handleGoCardlessError(response);
let response;
try {
response = await client.getBalances(accountId);
} catch (error) {
handleGoCardlessError(error);
}
return response;
},

View File

@@ -1,20 +1,20 @@
import { jest } from '@jest/globals';
import {
AccessDeniedError,
AccountNotLinedToRequisition,
GenericGoCardlessError,
InvalidInputDataError,
InvalidGoCardlessTokenError,
AccessDeniedError,
NotFoundError,
ResourceSuspended,
RateLimitError,
UnknownError,
ServiceError,
ResourceSuspended,
RequisitionNotLinked,
AccountNotLinedToRequisition,
ServiceError,
UnknownError,
} from '../../errors.js';
import {
mockedBalances,
mockUnknownError,
mockTransactions,
mockDetailedAccount,
mockInstitution,
@@ -252,17 +252,6 @@ describe('goCardlessService', () => {
expect(createRequisitionSpy).toBeCalledTimes(1);
});
it('handle error if status_code present in the response', async () => {
setTokenSpy.mockResolvedValue();
getInstitutionSpy.mockResolvedValue(mockInstitution);
createRequisitionSpy.mockResolvedValue(mockUnknownError);
await expect(() =>
goCardlessService.createRequisition(params),
).rejects.toThrow(UnknownError);
});
});
describe('#deleteRequisition', () => {
@@ -281,17 +270,6 @@ describe('goCardlessService', () => {
expect(getRequisitionsSpy).toBeCalledTimes(1);
expect(deleteRequisitionsSpy).toBeCalledTimes(1);
});
it('handle error if status_code present in the response', async () => {
setTokenSpy.mockResolvedValue();
getRequisitionsSpy.mockResolvedValue(mockRequisition);
deleteRequisitionsSpy.mockReturnValue(mockUnknownError);
await expect(() =>
goCardlessService.deleteRequisition(requisitionId),
).rejects.toThrow(UnknownError);
});
});
describe('#getRequisition', () => {
@@ -308,16 +286,6 @@ describe('goCardlessService', () => {
expect(setTokenSpy).toBeCalledTimes(1);
expect(getRequisitionsSpy).toBeCalledTimes(1);
});
it('handle error if status_code present in the response', async () => {
setTokenSpy.mockResolvedValue();
getRequisitionsSpy.mockReturnValue(mockUnknownError);
await expect(() =>
goCardlessService.getRequisition(requisitionId),
).rejects.toThrow(UnknownError);
});
});
describe('#getDetailedAccount', () => {
@@ -332,30 +300,6 @@ describe('goCardlessService', () => {
expect(getDetailsSpy).toBeCalledTimes(1);
expect(getMetadataSpy).toBeCalledTimes(1);
});
it('handle error if status_code present in the detailedAccount response', async () => {
getDetailsSpy.mockResolvedValue(mockUnknownError);
getMetadataSpy.mockResolvedValue(mockAccountMetaData);
await expect(() =>
goCardlessService.getDetailedAccount(accountId),
).rejects.toThrow(UnknownError);
expect(getDetailsSpy).toBeCalledTimes(1);
expect(getMetadataSpy).toBeCalledTimes(1);
});
it('handle error if status_code present in the metadataAccount response', async () => {
getDetailsSpy.mockResolvedValue(mockAccountDetails);
getMetadataSpy.mockResolvedValue(mockUnknownError);
await expect(() =>
goCardlessService.getDetailedAccount(accountId),
).rejects.toThrow(UnknownError);
expect(getDetailsSpy).toBeCalledTimes(1);
expect(getMetadataSpy).toBeCalledTimes(1);
});
});
describe('#getInstitutions', () => {
@@ -368,14 +312,6 @@ describe('goCardlessService', () => {
]);
expect(getInstitutionsSpy).toBeCalledTimes(1);
});
it('handle error if status_code present in the response', async () => {
getInstitutionsSpy.mockResolvedValue(mockUnknownError);
await expect(() =>
goCardlessService.getInstitutions({ country }),
).rejects.toThrow(UnknownError);
});
});
describe('#getInstitution', () => {
@@ -388,14 +324,6 @@ describe('goCardlessService', () => {
);
expect(getInstitutionSpy).toBeCalledTimes(1);
});
it('handle error if status_code present in the response', async () => {
getInstitutionSpy.mockResolvedValue(mockUnknownError);
await expect(() =>
goCardlessService.getInstitution(institutionId),
).rejects.toThrow(UnknownError);
});
});
describe('#extendAccountsAboutInstitutions', () => {
@@ -532,19 +460,6 @@ describe('goCardlessService', () => {
`);
expect(getTransactionsSpy).toBeCalledTimes(1);
});
it('handle error if status_code present in the response', async () => {
getTransactionsSpy.mockResolvedValue(mockUnknownError);
await expect(() =>
goCardlessService.getTransactions({
institutionId: 'SANDBOXFINANCE_SFIN0000',
accountId,
startDate: '',
endDate: '',
}),
).rejects.toThrow(UnknownError);
});
});
describe('#getBalances', () => {
@@ -556,69 +471,58 @@ describe('goCardlessService', () => {
);
expect(getBalancesSpy).toBeCalledTimes(1);
});
it('handle error if status_code present in the response', async () => {
getBalancesSpy.mockResolvedValue(mockUnknownError);
await expect(() =>
goCardlessService.getBalances(accountId),
).rejects.toThrow(UnknownError);
});
});
});
describe('#handleGoCardlessError', () => {
it('throws InvalidInputDataError for status code 400', () => {
const response = { status_code: 400 };
const response = { response: { status: 400 } };
expect(() => handleGoCardlessError(response)).toThrow(
InvalidInputDataError,
);
});
it('throws InvalidGoCardlessTokenError for status code 401', () => {
const response = { status_code: 401 };
const response = { response: { status: 401 } };
expect(() => handleGoCardlessError(response)).toThrow(
InvalidGoCardlessTokenError,
);
});
it('throws AccessDeniedError for status code 403', () => {
const response = { status_code: 403 };
const response = { response: { status: 403 } };
expect(() => handleGoCardlessError(response)).toThrow(AccessDeniedError);
});
it('throws NotFoundError for status code 404', () => {
const response = { status_code: 404 };
const response = { response: { status: 404 } };
expect(() => handleGoCardlessError(response)).toThrow(NotFoundError);
});
it('throws ResourceSuspended for status code 409', () => {
const response = { status_code: 409 };
const response = { response: { status: 409 } };
expect(() => handleGoCardlessError(response)).toThrow(ResourceSuspended);
});
it('throws RateLimitError for status code 429', () => {
const response = { status_code: 429 };
const response = { response: { status: 429 } };
expect(() => handleGoCardlessError(response)).toThrow(RateLimitError);
});
it('throws UnknownError for status code 500', () => {
const response = { status_code: 500 };
const response = { response: { status: 500 } };
expect(() => handleGoCardlessError(response)).toThrow(UnknownError);
});
it('throws ServiceError for status code 503', () => {
const response = { status_code: 503 };
const response = { response: { status: 503 } };
expect(() => handleGoCardlessError(response)).toThrow(ServiceError);
});
it('does not throw an error for status code 200', () => {
const response = { status_code: 200 };
expect(() => handleGoCardlessError(response)).not.toThrow();
});
it('does not throw an error when status code is not present', () => {
const response = { foo: 'bar' };
expect(() => handleGoCardlessError(response)).not.toThrow();
it('throws a generic error when the status code is not recognised', () => {
const response = { response: { status: 0 } };
expect(() => handleGoCardlessError(response)).toThrow(
GenericGoCardlessError,
);
});
});

View File

@@ -0,0 +1,6 @@
---
category: Bugfix
authors: [matt-fidd]
---
Fix GoCardless error handling