@defra-fish/connectors-lib 1.62.0-rc.8 → 1.62.0-rst.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,7 +20,8 @@ Provides connectivity to the resources/infrastructure used in the rod licensing
20
20
  | GOV_PAY_APIKEY | GOV pay access identifier | yes | | | |
21
21
  | GOV_PAY_RECURRING_APIKEY | GOV pay access identifier for recurring payments | yes | | | |
22
22
  | GOV_PAY_REQUEST_TIMEOUT_MS | Timeout in milliseconds for API requests | no | 10000 | | |
23
- | GOV_PAY_RCP_API_URL | The GOV.UK Pay API url for agreements | yes | |
23
+ | GOV_PAY_RCP_API_URL | The GOV.UK Pay API url for agreements | yes | | | |
24
+ | GOV_PAY_HEALTH_CHECK_URL | The Gov.UK Pay health check url | yes | | | |
24
25
 
25
26
  # Prerequisites
26
27
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defra-fish/connectors-lib",
3
- "version": "1.62.0-rc.8",
3
+ "version": "1.62.0-rst.1",
4
4
  "description": "Shared connectors",
5
5
  "type": "module",
6
6
  "engines": {
@@ -45,6 +45,5 @@
45
45
  "ioredis": "^4.28.5",
46
46
  "node-fetch": "^2.7.0",
47
47
  "redlock": "^4.2.0"
48
- },
49
- "gitHead": "5d9e947d9ce87f6f43aa6ca7ac8d78f797ecebdf"
48
+ }
50
49
  }
@@ -1,6 +1,6 @@
1
1
  import { createDocumentClient } from '../documentclient-decorator'
2
2
  import { DynamoDB } from '@aws-sdk/client-dynamodb'
3
- import { DynamoDBDocument } from '@aws-sdk/lib-dynamodb'
3
+ import { DynamoDBDocument, QueryCommand, ScanCommand } from '@aws-sdk/lib-dynamodb'
4
4
 
5
5
  jest.mock('@aws-sdk/client-dynamodb')
6
6
  jest.mock('@aws-sdk/lib-dynamodb')
@@ -9,6 +9,7 @@ describe('document client decorations', () => {
9
9
  beforeAll(() => {
10
10
  jest.spyOn(global, 'setTimeout').mockImplementation(cb => cb())
11
11
  DynamoDBDocument.from.mockReturnValue({
12
+ send: jest.fn().mockResolvedValue({ Items: [], lastEvaluatedKey: false }),
12
13
  query: jest.fn().mockResolvedValue({ Items: [], lastEvaluatedKey: false }),
13
14
  scan: jest.fn().mockResolvedValue({ Items: [], lastEvaluatedKey: false }),
14
15
  batchWrite: jest.fn().mockResolvedValue({ UnprocessedItems: {} })
@@ -46,23 +47,30 @@ describe('document client decorations', () => {
46
47
  })
47
48
 
48
49
  describe.each`
49
- aggregateMethod | baseMethod
50
- ${'queryAllPromise'} | ${'query'}
51
- ${'scanAllPromise'} | ${'scan'}
52
- `('$aggregateMethod', ({ aggregateMethod, baseMethod }) => {
50
+ aggregateMethod | commandType
51
+ ${'queryAllPromise'} | ${QueryCommand}
52
+ ${'scanAllPromise'} | ${ScanCommand}
53
+ `('$aggregateMethod', ({ aggregateMethod, commandType }) => {
53
54
  it('is added to document client', () => {
54
55
  const docClient = createDocumentClient()
55
56
  expect(docClient[aggregateMethod]).toBeDefined()
56
57
  })
57
58
 
58
- it(`passes arguments provided for ${aggregateMethod} to ${baseMethod}`, async () => {
59
+ it(`passes arguments provided for ${aggregateMethod} to ${commandType.name}`, async () => {
59
60
  const params = { TableName: 'TEST', KeyConditionExpression: 'id = :id', ExpressionAttributeValues: { ':id': 1 } }
60
61
  const docClient = createDocumentClient()
61
62
  await docClient[aggregateMethod](params)
62
- expect(docClient[baseMethod]).toHaveBeenCalledWith(params)
63
+ expect(commandType).toHaveBeenCalledWith(params)
63
64
  })
64
65
 
65
- it(`calls ${baseMethod} repeatedly until LastEvaluatedKey evaluates to false, concatenating all returned items`, async () => {
66
+ it(`passes created command ${commandType.name} to docClient.send`, async () => {
67
+ const docClient = createDocumentClient()
68
+ await docClient[aggregateMethod]()
69
+ const [command] = commandType.mock.instances
70
+ expect(docClient.send).toHaveBeenCalledWith(command)
71
+ })
72
+
73
+ it('calls send repeatedly until LastEvaluatedKey evaluates to false, concatenating all returned items', async () => {
66
74
  const expectedItems = [
67
75
  { id: 1, data: Symbol('data1') },
68
76
  { id: 2, data: Symbol('data2') },
@@ -71,7 +79,7 @@ describe('document client decorations', () => {
71
79
  { id: 5, data: Symbol('data5') }
72
80
  ]
73
81
  const docClient = createDocumentClient()
74
- docClient[baseMethod]
82
+ docClient.send
75
83
  .mockResolvedValueOnce({ Items: expectedItems.slice(0, 2), LastEvaluatedKey: true })
76
84
  .mockResolvedValueOnce({ Items: expectedItems.slice(2, 4), LastEvaluatedKey: true })
77
85
  .mockResolvedValueOnce({ Items: expectedItems.slice(4), LastEvaluatedKey: false })
@@ -79,13 +87,12 @@ describe('document client decorations', () => {
79
87
  expect(actualItems).toEqual(expectedItems)
80
88
  })
81
89
 
82
- it(`whilst concatenating ${baseMethod} results, passes ExclusiveStartKey param`, async () => {
90
+ it(`whilst concatenating ${commandType.name} results, passes ExclusiveStartKey param`, async () => {
83
91
  const expectedKey = Symbol('🔑')
84
92
  const docClient = createDocumentClient()
85
- docClient[baseMethod].mockResolvedValueOnce({ Items: [], LastEvaluatedKey: expectedKey }).mockResolvedValueOnce({ Items: [] })
93
+ docClient.send.mockResolvedValueOnce({ Items: [], LastEvaluatedKey: expectedKey }).mockResolvedValueOnce({ Items: [] })
86
94
  await docClient[aggregateMethod]()
87
- expect(docClient[baseMethod]).toHaveBeenNthCalledWith(
88
- 2,
95
+ expect(commandType).toHaveBeenLastCalledWith(
89
96
  expect.objectContaining({
90
97
  ExclusiveStartKey: expectedKey
91
98
  })
@@ -95,7 +102,7 @@ describe('document client decorations', () => {
95
102
  it("omits ExclusiveStartKey if previous LastEvaluatedKey isn't available", async () => {
96
103
  const docClient = createDocumentClient()
97
104
  await docClient[aggregateMethod]()
98
- expect(docClient[baseMethod]).toHaveBeenNthCalledWith(
105
+ expect(docClient.send).toHaveBeenNthCalledWith(
99
106
  1,
100
107
  expect.not.objectContaining({
101
108
  ExclusiveStartKey: expect.anything()
@@ -24,7 +24,7 @@ describe('govuk-pay-api-connector', () => {
24
24
 
25
25
  describe('createPayment', () => {
26
26
  it('creates new payments', async () => {
27
- fetch.mockReturnValue({ ok: true, status: 200 })
27
+ fetch.mockReturnValueOnce({ ok: true, status: 200 })
28
28
  await expect(govUkPayApi.createPayment({ cost: 0 })).resolves.toEqual({ ok: true, status: 200 })
29
29
  expect(fetch).toHaveBeenCalledWith('http://0.0.0.0/payment', {
30
30
  body: JSON.stringify({ cost: 0 }),
@@ -36,7 +36,7 @@ describe('govuk-pay-api-connector', () => {
36
36
 
37
37
  it('logs and throws errors', async () => {
38
38
  const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(jest.fn())
39
- fetch.mockImplementation(() => {
39
+ fetch.mockImplementationOnce(() => {
40
40
  throw new Error('')
41
41
  })
42
42
  expect(govUkPayApi.createPayment({ cost: 0 })).rejects.toEqual(Error(''))
@@ -50,7 +50,7 @@ describe('govuk-pay-api-connector', () => {
50
50
  })
51
51
 
52
52
  it('uses the correct API key if recurring arg is set to true', async () => {
53
- fetch.mockReturnValue({ ok: true, status: 200 })
53
+ fetch.mockReturnValueOnce({ ok: true, status: 200 })
54
54
  await expect(govUkPayApi.createPayment({ cost: 0 }, true)).resolves.toEqual({ ok: true, status: 200 })
55
55
  expect(fetch).toHaveBeenCalledWith('http://0.0.0.0/payment', {
56
56
  body: JSON.stringify({ cost: 0 }),
@@ -63,7 +63,7 @@ describe('govuk-pay-api-connector', () => {
63
63
 
64
64
  describe('fetchPaymentStatus', () => {
65
65
  it('retrieves payment status', async () => {
66
- fetch.mockReturnValue({ ok: true, status: 200, json: () => {} })
66
+ fetch.mockReturnValueOnce({ ok: true, status: 200, json: () => {} })
67
67
  await expect(govUkPayApi.fetchPaymentStatus(123)).resolves.toEqual(expect.objectContaining({ ok: true, status: 200 }))
68
68
  expect(fetch).toHaveBeenCalledWith('http://0.0.0.0/payment/123', {
69
69
  headers,
@@ -74,7 +74,7 @@ describe('govuk-pay-api-connector', () => {
74
74
 
75
75
  it('logs and throws errors', async () => {
76
76
  const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(jest.fn())
77
- fetch.mockImplementation(() => {
77
+ fetch.mockImplementationOnce(() => {
78
78
  throw new Error('')
79
79
  })
80
80
  await expect(govUkPayApi.fetchPaymentStatus(123)).rejects.toEqual(Error(''))
@@ -83,7 +83,7 @@ describe('govuk-pay-api-connector', () => {
83
83
  })
84
84
 
85
85
  it('uses the correct API key if recurring arg is set to true', async () => {
86
- fetch.mockReturnValue({ ok: true, status: 200, json: () => {} })
86
+ fetch.mockReturnValueOnce({ ok: true, status: 200, json: () => {} })
87
87
  await expect(govUkPayApi.fetchPaymentStatus(123, true)).resolves.toEqual(expect.objectContaining({ ok: true, status: 200 }))
88
88
  expect(fetch).toHaveBeenCalledWith('http://0.0.0.0/payment/123', {
89
89
  headers: recurringHeaders,
@@ -95,14 +95,14 @@ describe('govuk-pay-api-connector', () => {
95
95
 
96
96
  describe('fetchPaymentEvents', () => {
97
97
  it('retrieves payment events', async () => {
98
- fetch.mockReturnValue({ ok: true, status: 200, json: () => {} })
98
+ fetch.mockReturnValueOnce({ ok: true, status: 200, json: () => {} })
99
99
  await expect(govUkPayApi.fetchPaymentEvents(123)).resolves.toEqual(expect.objectContaining({ ok: true, status: 200 }))
100
100
  expect(fetch).toHaveBeenCalledWith('http://0.0.0.0/payment/123/events', { headers, method: 'get', timeout: 10000 })
101
101
  })
102
102
 
103
103
  it('logs and throws errors', async () => {
104
104
  const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(jest.fn())
105
- fetch.mockImplementation(() => {
105
+ fetch.mockImplementationOnce(() => {
106
106
  throw new Error('test event error')
107
107
  })
108
108
  await expect(govUkPayApi.fetchPaymentEvents(123)).rejects.toEqual(Error('test event error'))
@@ -110,7 +110,7 @@ describe('govuk-pay-api-connector', () => {
110
110
  })
111
111
 
112
112
  it('uses the correct API key if recurring arg is set to true', async () => {
113
- fetch.mockReturnValue({ ok: true, status: 200, json: () => {} })
113
+ fetch.mockReturnValueOnce({ ok: true, status: 200, json: () => {} })
114
114
  await expect(govUkPayApi.fetchPaymentEvents(123, true)).resolves.toEqual(expect.objectContaining({ ok: true, status: 200 }))
115
115
  expect(fetch).toHaveBeenCalledWith('http://0.0.0.0/payment/123/events', {
116
116
  headers: recurringHeaders,
@@ -122,7 +122,7 @@ describe('govuk-pay-api-connector', () => {
122
122
 
123
123
  describe('createRecurringPaymentAgreement', () => {
124
124
  it('creates new payments', async () => {
125
- fetch.mockReturnValue({ ok: true, status: 200 })
125
+ fetch.mockReturnValueOnce({ ok: true, status: 200 })
126
126
  await expect(govUkPayApi.createRecurringPaymentAgreement({ cost: 0 })).resolves.toEqual({ ok: true, status: 200 })
127
127
  expect(fetch).toHaveBeenCalledWith('http://0.0.0.0/agreement', {
128
128
  body: JSON.stringify({ cost: 0 }),
@@ -134,7 +134,7 @@ describe('govuk-pay-api-connector', () => {
134
134
 
135
135
  it('logs and throws errors', async () => {
136
136
  const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(jest.fn())
137
- fetch.mockImplementation(() => {
137
+ fetch.mockImplementationOnce(() => {
138
138
  throw new Error('')
139
139
  })
140
140
  expect(govUkPayApi.createRecurringPaymentAgreement({ reference: '123' })).rejects.toEqual(Error(''))
@@ -148,6 +148,43 @@ describe('govuk-pay-api-connector', () => {
148
148
  })
149
149
  })
150
150
 
151
+ describe('isGovPayUp', () => {
152
+ it.each(['http://gov.uk.pay/health/check/url', 'https://gov-uk-pay?health-check-url'])(
153
+ 'calls healthy endpoint %s',
154
+ async healthCheckURL => {
155
+ process.env.GOV_PAY_HEALTH_CHECK_URL = healthCheckURL
156
+ await govUkPayApi.isGovPayUp()
157
+ expect(fetch).toHaveBeenCalledWith(healthCheckURL)
158
+ }
159
+ )
160
+
161
+ it('returns the fetch response', async () => {
162
+ const response = Symbol('response')
163
+ fetch.mockReturnValueOnce(response)
164
+ expect(await govUkPayApi.isGovPayUp()).toBe(response)
165
+ })
166
+
167
+ it('throws errors that are thrown by fetch', async () => {
168
+ const error = new Error('Fail')
169
+ fetch.mockImplementationOnce(() => {
170
+ throw error
171
+ })
172
+ await expect(govUkPayApi.isGovPayUp()).rejects.toBe(error)
173
+ })
174
+
175
+ it('logs errors', async () => {
176
+ const error = new Error('Fail')
177
+ const consoleErrorSpy = jest.spyOn(console, 'error')
178
+ fetch.mockImplementationOnce(() => {
179
+ throw error
180
+ })
181
+ try {
182
+ await govUkPayApi.isGovPayUp()
183
+ } catch {}
184
+ expect(consoleErrorSpy).toHaveBeenCalledWith('Error retrieving GovPay health status', error)
185
+ })
186
+ })
187
+
151
188
  describe('getRecurringPaymentAgreementInformation', () => {
152
189
  it('retrieves recurring payment agreement information', async () => {
153
190
  fetch.mockReturnValue({ ok: true, status: 200, json: () => {} })
@@ -746,4 +746,44 @@ describe('sales-api-connector', () => {
746
746
  await expect(salesApi.cancelRecurringPayment('id')).rejects.toThrow('Internal Server Error')
747
747
  })
748
748
  })
749
+
750
+ describe('retrieveStagedTransaction', () => {
751
+ describe.each([['id'], ['abc-123']])("Retrieving staged transaction id '%s'", id => {
752
+ beforeEach(() => {
753
+ fetch.mockReturnValue({
754
+ ok: true,
755
+ status: 200,
756
+ statusText: 'OK',
757
+ text: async () => JSON.stringify({ id })
758
+ })
759
+ })
760
+
761
+ it('calls the endpoint with the correct parameters', async () => {
762
+ await salesApi.retrieveStagedTransaction(id)
763
+
764
+ expect(fetch).toHaveBeenCalledWith(`http://0.0.0.0:4000/retrieveStagedTransaction/${id}`, {
765
+ method: 'get',
766
+ headers: expect.any(Object),
767
+ timeout: 20000
768
+ })
769
+ })
770
+
771
+ it('returns the expected response data', async () => {
772
+ const processedResult = await salesApi.retrieveStagedTransaction(id)
773
+
774
+ expect(processedResult).toEqual({ id })
775
+ })
776
+ })
777
+
778
+ it('throws an error on non-2xx response', async () => {
779
+ fetch.mockReturnValue({
780
+ ok: false,
781
+ status: 500,
782
+ statusText: 'Internal Server Error',
783
+ text: async () => 'Server Error'
784
+ })
785
+
786
+ await expect(salesApi.retrieveStagedTransaction('id')).rejects.toThrow('Internal Server Error')
787
+ })
788
+ })
749
789
  })
@@ -1,6 +1,6 @@
1
1
  import db from 'debug'
2
2
  import { DynamoDB } from '@aws-sdk/client-dynamodb'
3
- import { DynamoDBDocument } from '@aws-sdk/lib-dynamodb'
3
+ import { DynamoDBDocument, QueryCommand, ScanCommand } from '@aws-sdk/lib-dynamodb'
4
4
  const debug = db('connectors:aws')
5
5
 
6
6
  export const createDocumentClient = options => {
@@ -13,23 +13,24 @@ export const createDocumentClient = options => {
13
13
  })
14
14
 
15
15
  // Support for large query/scan operations which return results in pages
16
- const wrapPagedDocumentClientOperation = operationName => {
16
+ const wrapPagedDocumentClientOperation = CommandType => {
17
17
  return async params => {
18
18
  const items = []
19
19
  let lastEvaluatedKey = null
20
20
  do {
21
- const response = await docClient[operationName]({
21
+ const command = new CommandType({
22
22
  ...params,
23
23
  ...(lastEvaluatedKey && { ExclusiveStartKey: lastEvaluatedKey })
24
24
  })
25
+ const response = await docClient.send(command)
25
26
  lastEvaluatedKey = response.LastEvaluatedKey
26
27
  response.Items && items.push(...response.Items)
27
28
  } while (lastEvaluatedKey)
28
29
  return items
29
30
  }
30
31
  }
31
- docClient.queryAllPromise = wrapPagedDocumentClientOperation('query')
32
- docClient.scanAllPromise = wrapPagedDocumentClientOperation('scan')
32
+ docClient.queryAllPromise = wrapPagedDocumentClientOperation(QueryCommand)
33
+ docClient.scanAllPromise = wrapPagedDocumentClientOperation(ScanCommand)
33
34
 
34
35
  /**
35
36
  * Handles batch writes which may return UnprocessedItems. If UnprocessedItems are returned then they will be retried with exponential backoff
@@ -87,6 +87,15 @@ export const fetchPaymentEvents = async (paymentId, recurring = false) => {
87
87
  }
88
88
  }
89
89
 
90
+ export const isGovPayUp = async () => {
91
+ try {
92
+ return await fetch(process.env.GOV_PAY_HEALTH_CHECK_URL)
93
+ } catch (err) {
94
+ console.error('Error retrieving GovPay health status', err)
95
+ throw err
96
+ }
97
+ }
98
+
90
99
  /**
91
100
  * Gets payment information linked too a payment
92
101
  * @param agreementId - agreementId set up when creating recurring payment
@@ -319,3 +319,14 @@ export const processRPResult = async (transactionId, paymentId, createdDate) =>
319
319
  export const cancelRecurringPayment = async id => {
320
320
  return exec2xxOrThrow(call(new URL(`/cancelRecurringPayment/${id}`, urlBase), 'get'))
321
321
  }
322
+
323
+ /**
324
+ * Retrieve a staged transaction
325
+ *
326
+ * @param id
327
+ * @returns {Promise<*>}
328
+ * @throws on a non-2xx response
329
+ */
330
+ export const retrieveStagedTransaction = async id => {
331
+ return exec2xxOrThrow(call(new URL(`/retrieveStagedTransaction/${id}`, urlBase), 'get'))
332
+ }