@defra-fish/pocl-job 1.61.0-rc.8 → 1.61.0
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/package.json +5 -5
- package/src/__tests__/config.spec.js +0 -5
- package/src/io/__tests__/db.spec.js +36 -10
- package/src/io/__tests__/s3.spec.js +148 -115
- package/src/io/db.js +6 -8
- package/src/io/s3.js +3 -2
- package/src/staging/__tests__/pocl-data-staging.spec.js +5 -1
- package/src/transport/__tests__/s3-to-local.spec.js +18 -9
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@defra-fish/pocl-job",
|
|
3
|
-
"version": "1.61.0
|
|
3
|
+
"version": "1.61.0",
|
|
4
4
|
"description": "Post Office Counter Licence sales processor",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
|
-
"node": ">=
|
|
7
|
+
"node": ">=20"
|
|
8
8
|
},
|
|
9
9
|
"keywords": [
|
|
10
10
|
"rod",
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"test": "echo \"Error: run tests from root\" && exit 1"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@defra-fish/business-rules-lib": "1.61.0
|
|
39
|
-
"@defra-fish/connectors-lib": "1.61.0
|
|
38
|
+
"@defra-fish/business-rules-lib": "1.61.0",
|
|
39
|
+
"@defra-fish/connectors-lib": "1.61.0",
|
|
40
40
|
"commander": "^7.2.0",
|
|
41
41
|
"debug": "^4.3.3",
|
|
42
42
|
"filesize": "^6.4.0",
|
|
@@ -45,5 +45,5 @@
|
|
|
45
45
|
"moment-timezone": "^0.5.34",
|
|
46
46
|
"sax-stream": "^1.3.0"
|
|
47
47
|
},
|
|
48
|
-
"gitHead": "
|
|
48
|
+
"gitHead": "8d1841265a40178f285288e5323a36e887483434"
|
|
49
49
|
}
|
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
import AwsMock from 'aws-sdk'
|
|
2
1
|
import config from '../config.js'
|
|
3
2
|
|
|
4
3
|
describe('config', () => {
|
|
5
4
|
beforeAll(async () => {
|
|
6
|
-
AwsMock.SecretsManager.__setResponse('getSecretValue', {
|
|
7
|
-
SecretString: 'test-ssh-key'
|
|
8
|
-
})
|
|
9
|
-
|
|
10
5
|
process.env.POCL_FILE_STAGING_TABLE = 'test-file-staging-table'
|
|
11
6
|
process.env.POCL_RECORD_STAGING_TABLE = 'test-record-staging-table'
|
|
12
7
|
process.env.POCL_STAGING_TTL = 1234
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as db from '../db.js'
|
|
2
|
-
import
|
|
2
|
+
import { AWS } from '@defra-fish/connectors-lib'
|
|
3
|
+
const { docClient } = AWS.mock.results[0].value
|
|
3
4
|
|
|
4
5
|
jest.mock('../../config.js', () => ({
|
|
5
6
|
db: {
|
|
@@ -9,16 +10,28 @@ jest.mock('../../config.js', () => ({
|
|
|
9
10
|
}
|
|
10
11
|
}))
|
|
11
12
|
|
|
13
|
+
jest.mock('@defra-fish/connectors-lib', () => ({
|
|
14
|
+
AWS: jest.fn(() => ({
|
|
15
|
+
docClient: {
|
|
16
|
+
batchWriteAllPromise: jest.fn(),
|
|
17
|
+
get: jest.fn(() => ({ Item: undefined })),
|
|
18
|
+
scanAllPromise: jest.fn(),
|
|
19
|
+
update: jest.fn(),
|
|
20
|
+
createUpdateExpression: jest.fn(() => ({})),
|
|
21
|
+
queryAllPromise: jest.fn()
|
|
22
|
+
}
|
|
23
|
+
}))
|
|
24
|
+
}))
|
|
25
|
+
|
|
12
26
|
describe('database operations', () => {
|
|
13
27
|
const TEST_FILENAME = 'testfile.xml'
|
|
14
28
|
beforeEach(() => {
|
|
15
29
|
jest.clearAllMocks()
|
|
16
|
-
AwsMock.__resetAll()
|
|
17
30
|
})
|
|
18
31
|
describe('getFileRecord', () => {
|
|
19
32
|
it('calls a get operation on dynamodb', async () => {
|
|
20
33
|
await db.getFileRecord(TEST_FILENAME)
|
|
21
|
-
expect(
|
|
34
|
+
expect(docClient.get).toHaveBeenCalledWith({
|
|
22
35
|
TableName: 'TestFileTable',
|
|
23
36
|
Key: { filename: TEST_FILENAME },
|
|
24
37
|
ConsistentRead: true
|
|
@@ -29,7 +42,7 @@ describe('database operations', () => {
|
|
|
29
42
|
describe('getFileRecords', () => {
|
|
30
43
|
it('retrieves all records for the given file if no stages are provided', async () => {
|
|
31
44
|
await db.getFileRecords()
|
|
32
|
-
expect(
|
|
45
|
+
expect(docClient.scanAllPromise).toHaveBeenCalledWith(
|
|
33
46
|
expect.objectContaining({
|
|
34
47
|
TableName: 'TestFileTable',
|
|
35
48
|
ConsistentRead: true
|
|
@@ -39,7 +52,7 @@ describe('database operations', () => {
|
|
|
39
52
|
|
|
40
53
|
it('retrieves all records a given set of stages', async () => {
|
|
41
54
|
await db.getFileRecords('STAGE 1', 'STAGE 2')
|
|
42
|
-
expect(
|
|
55
|
+
expect(docClient.scanAllPromise).toHaveBeenCalledWith(
|
|
43
56
|
expect.objectContaining({
|
|
44
57
|
TableName: 'TestFileTable',
|
|
45
58
|
FilterExpression: 'stage IN (:stage0,:stage1)',
|
|
@@ -52,8 +65,21 @@ describe('database operations', () => {
|
|
|
52
65
|
|
|
53
66
|
describe('updateFileStagingTable', () => {
|
|
54
67
|
it('calls update on dynamodb including all necessary parameters', async () => {
|
|
68
|
+
docClient.createUpdateExpression.mockReturnValueOnce({
|
|
69
|
+
UpdateExpression: 'SET #expires = :expires,#param1 = :param1,#param2 = :param2',
|
|
70
|
+
ExpressionAttributeNames: {
|
|
71
|
+
'#expires': 'expires',
|
|
72
|
+
'#param1': 'param1',
|
|
73
|
+
'#param2': 'param2'
|
|
74
|
+
},
|
|
75
|
+
ExpressionAttributeValues: {
|
|
76
|
+
':expires': expect.any(Number),
|
|
77
|
+
':param1': 'test1',
|
|
78
|
+
':param2': 'test2'
|
|
79
|
+
}
|
|
80
|
+
})
|
|
55
81
|
await db.updateFileStagingTable({ filename: TEST_FILENAME, param1: 'test1', param2: 'test2' })
|
|
56
|
-
expect(
|
|
82
|
+
expect(docClient.update).toHaveBeenCalledWith(
|
|
57
83
|
expect.objectContaining({
|
|
58
84
|
TableName: 'TestFileTable',
|
|
59
85
|
Key: { filename: TEST_FILENAME },
|
|
@@ -77,7 +103,7 @@ describe('database operations', () => {
|
|
|
77
103
|
it('calls batchWrite on dynamodb including all necessary parameters', async () => {
|
|
78
104
|
const records = [{ id: 'test1' }, { id: 'test2' }]
|
|
79
105
|
await db.updateRecordStagingTable(TEST_FILENAME, records)
|
|
80
|
-
expect(
|
|
106
|
+
expect(docClient.batchWriteAllPromise).toHaveBeenCalledWith(
|
|
81
107
|
expect.objectContaining({
|
|
82
108
|
RequestItems: {
|
|
83
109
|
TestRecordTable: [
|
|
@@ -99,14 +125,14 @@ describe('database operations', () => {
|
|
|
99
125
|
|
|
100
126
|
it('is a no-op if records is empty', async () => {
|
|
101
127
|
await db.updateRecordStagingTable(TEST_FILENAME, [])
|
|
102
|
-
expect(
|
|
128
|
+
expect(docClient.batchWriteAllPromise).not.toHaveBeenCalled()
|
|
103
129
|
})
|
|
104
130
|
})
|
|
105
131
|
|
|
106
132
|
describe('getProcessedRecords', () => {
|
|
107
133
|
it('retrieves all records for the given file if no stages are provided', async () => {
|
|
108
134
|
await db.getProcessedRecords(TEST_FILENAME)
|
|
109
|
-
expect(
|
|
135
|
+
expect(docClient.queryAllPromise).toHaveBeenCalledWith(
|
|
110
136
|
expect.objectContaining({
|
|
111
137
|
TableName: 'TestRecordTable',
|
|
112
138
|
KeyConditionExpression: 'filename = :filename',
|
|
@@ -118,7 +144,7 @@ describe('database operations', () => {
|
|
|
118
144
|
|
|
119
145
|
it('retrieves all records a given set of stages', async () => {
|
|
120
146
|
await db.getProcessedRecords(TEST_FILENAME, 'STAGE 1', 'STAGE 2')
|
|
121
|
-
expect(
|
|
147
|
+
expect(docClient.queryAllPromise).toHaveBeenCalledWith(
|
|
122
148
|
expect.objectContaining({
|
|
123
149
|
TableName: 'TestRecordTable',
|
|
124
150
|
KeyConditionExpression: 'filename = :filename',
|
|
@@ -2,19 +2,31 @@ import { refreshS3Metadata } from '../s3'
|
|
|
2
2
|
import moment from 'moment'
|
|
3
3
|
import { updateFileStagingTable } from '../../io/db.js'
|
|
4
4
|
import { DYNAMICS_IMPORT_STAGE, FILE_STAGE, POST_OFFICE_DATASOURCE } from '../../staging/constants.js'
|
|
5
|
-
import { salesApi } from '@defra-fish/connectors-lib'
|
|
5
|
+
import { salesApi, AWS } from '@defra-fish/connectors-lib'
|
|
6
6
|
import fs from 'fs'
|
|
7
|
-
|
|
7
|
+
const { s3, ListObjectsV2Command } = AWS.mock.results[0].value
|
|
8
8
|
|
|
9
|
-
jest.mock('fs')
|
|
10
9
|
jest.mock('md5-file')
|
|
11
10
|
jest.mock('../../io/db.js')
|
|
12
11
|
jest.mock('../../io/file.js')
|
|
13
12
|
|
|
14
13
|
jest.mock('@defra-fish/connectors-lib', () => {
|
|
15
14
|
const actual = jest.requireActual('@defra-fish/connectors-lib')
|
|
15
|
+
const AWS = jest.fn(() => ({
|
|
16
|
+
docClient: {
|
|
17
|
+
update: jest.fn(),
|
|
18
|
+
createUpdateExpression: jest.fn(() => ({}))
|
|
19
|
+
},
|
|
20
|
+
s3: {
|
|
21
|
+
getObject: jest.fn(() => ({
|
|
22
|
+
createReadStream: jest.fn()
|
|
23
|
+
})),
|
|
24
|
+
send: jest.fn()
|
|
25
|
+
},
|
|
26
|
+
ListObjectsV2Command: jest.fn()
|
|
27
|
+
}))
|
|
16
28
|
return {
|
|
17
|
-
AWS
|
|
29
|
+
AWS,
|
|
18
30
|
salesApi: {
|
|
19
31
|
...Object.keys(actual.salesApi).reduce((acc, k) => ({ ...acc, [k]: jest.fn(async () => {}) }), {})
|
|
20
32
|
}
|
|
@@ -29,19 +41,19 @@ jest.mock('../../config.js', () => ({
|
|
|
29
41
|
bucket: 'testbucket'
|
|
30
42
|
}
|
|
31
43
|
}))
|
|
44
|
+
|
|
32
45
|
describe('s3 operations', () => {
|
|
33
46
|
beforeEach(() => {
|
|
34
47
|
jest.clearAllMocks()
|
|
35
|
-
AwsMock.__resetAll()
|
|
36
48
|
})
|
|
37
49
|
|
|
38
50
|
describe('refreshS3Metadata', () => {
|
|
39
|
-
|
|
51
|
+
describe('gets a list of files from S3', () => {
|
|
40
52
|
const s3Key1 = `${moment().format('YYYY-MM-DD')}/test1.xml`
|
|
41
53
|
const s3Key2 = `${moment().format('YYYY-MM-DD')}/test2.xml`
|
|
42
54
|
|
|
43
|
-
|
|
44
|
-
|
|
55
|
+
beforeEach(async () => {
|
|
56
|
+
s3.send.mockReturnValueOnce({
|
|
45
57
|
IsTruncated: false,
|
|
46
58
|
Contents: [
|
|
47
59
|
{
|
|
@@ -58,52 +70,66 @@ describe('s3 operations', () => {
|
|
|
58
70
|
}
|
|
59
71
|
]
|
|
60
72
|
})
|
|
61
|
-
})
|
|
62
73
|
|
|
63
|
-
|
|
74
|
+
await refreshS3Metadata()
|
|
75
|
+
})
|
|
64
76
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
77
|
+
it('calls ListObjectsV2Command, with bucket name and no continuation token', () => {
|
|
78
|
+
expect(ListObjectsV2Command).toHaveBeenNthCalledWith(1, {
|
|
79
|
+
Bucket: 'testbucket',
|
|
80
|
+
ContinuationToken: undefined
|
|
81
|
+
})
|
|
68
82
|
})
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
83
|
+
|
|
84
|
+
it('calls updateFileStagingTable first with initial test file', () => {
|
|
85
|
+
expect(updateFileStagingTable).toHaveBeenNthCalledWith(1, {
|
|
86
|
+
filename: 'test1.xml',
|
|
87
|
+
md5: 'example-md5',
|
|
88
|
+
fileSize: '1 KB',
|
|
89
|
+
stage: FILE_STAGE.Pending,
|
|
90
|
+
s3Key: s3Key1
|
|
91
|
+
})
|
|
75
92
|
})
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
93
|
+
|
|
94
|
+
it('calls updateFileStagingTable a second time with second test file', () => {
|
|
95
|
+
expect(updateFileStagingTable).toHaveBeenNthCalledWith(2, {
|
|
96
|
+
filename: 'test2.xml',
|
|
97
|
+
md5: 'example-md5',
|
|
98
|
+
fileSize: '2 KB',
|
|
99
|
+
stage: FILE_STAGE.Pending,
|
|
100
|
+
s3Key: s3Key2
|
|
101
|
+
})
|
|
82
102
|
})
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
103
|
+
|
|
104
|
+
it('calls upsertTransactionFile for first test file', () => {
|
|
105
|
+
expect(salesApi.upsertTransactionFile).toHaveBeenNthCalledWith(1, 'test1.xml', {
|
|
106
|
+
status: DYNAMICS_IMPORT_STAGE.Pending,
|
|
107
|
+
dataSource: POST_OFFICE_DATASOURCE,
|
|
108
|
+
fileSize: '1 KB',
|
|
109
|
+
receiptTimestamp: expect.any(String),
|
|
110
|
+
salesDate: expect.any(String),
|
|
111
|
+
notes: 'Retrieved from the remote server and awaiting processing'
|
|
112
|
+
})
|
|
90
113
|
})
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
114
|
+
|
|
115
|
+
it('calls upsertTransactionFile for second test file', () => {
|
|
116
|
+
expect(salesApi.upsertTransactionFile).toHaveBeenNthCalledWith(2, 'test2.xml', {
|
|
117
|
+
status: DYNAMICS_IMPORT_STAGE.Pending,
|
|
118
|
+
dataSource: POST_OFFICE_DATASOURCE,
|
|
119
|
+
fileSize: '2 KB',
|
|
120
|
+
receiptTimestamp: expect.any(String),
|
|
121
|
+
salesDate: expect.any(String),
|
|
122
|
+
notes: 'Retrieved from the remote server and awaiting processing'
|
|
123
|
+
})
|
|
98
124
|
})
|
|
99
125
|
})
|
|
100
126
|
|
|
101
|
-
|
|
127
|
+
describe('gets a truncated list of files from S3', () => {
|
|
102
128
|
const s3Key1 = `${moment().format('YYYY-MM-DD')}/test1.xml`
|
|
103
129
|
|
|
104
|
-
|
|
105
|
-
.
|
|
106
|
-
|
|
130
|
+
beforeEach(async () => {
|
|
131
|
+
s3.send
|
|
132
|
+
.mockReturnValue({
|
|
107
133
|
IsTruncated: false,
|
|
108
134
|
Contents: [
|
|
109
135
|
{
|
|
@@ -114,9 +140,7 @@ describe('s3 operations', () => {
|
|
|
114
140
|
}
|
|
115
141
|
]
|
|
116
142
|
})
|
|
117
|
-
|
|
118
|
-
.mockReturnValueOnce({
|
|
119
|
-
promise: () => ({
|
|
143
|
+
.mockReturnValueOnce({
|
|
120
144
|
IsTruncated: true,
|
|
121
145
|
NextContinuationToken: 'token',
|
|
122
146
|
Contents: [
|
|
@@ -128,53 +152,72 @@ describe('s3 operations', () => {
|
|
|
128
152
|
}
|
|
129
153
|
]
|
|
130
154
|
})
|
|
131
|
-
})
|
|
132
155
|
|
|
133
|
-
|
|
156
|
+
await refreshS3Metadata()
|
|
157
|
+
})
|
|
134
158
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
159
|
+
it('calls ListObjectsV2Command a first time with bucket name and no continuation token', () => {
|
|
160
|
+
expect(ListObjectsV2Command).toHaveBeenNthCalledWith(1, {
|
|
161
|
+
Bucket: 'testbucket',
|
|
162
|
+
ContinuationToken: undefined
|
|
163
|
+
})
|
|
138
164
|
})
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
165
|
+
|
|
166
|
+
it('calls ListObjectsV2Command a second time with bucket name and continuation token', () => {
|
|
167
|
+
expect(ListObjectsV2Command).toHaveBeenNthCalledWith(2, {
|
|
168
|
+
Bucket: 'testbucket',
|
|
169
|
+
ContinuationToken: 'token'
|
|
170
|
+
})
|
|
142
171
|
})
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
172
|
+
|
|
173
|
+
it('updates file staging table with first test file', () => {
|
|
174
|
+
expect(updateFileStagingTable).toHaveBeenNthCalledWith(1, {
|
|
175
|
+
filename: 'test1.xml',
|
|
176
|
+
md5: 'example-md5',
|
|
177
|
+
fileSize: '1 KB',
|
|
178
|
+
stage: FILE_STAGE.Pending,
|
|
179
|
+
s3Key: s3Key1
|
|
180
|
+
})
|
|
149
181
|
})
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
182
|
+
|
|
183
|
+
it('updates file staging table with second test file', () => {
|
|
184
|
+
expect(updateFileStagingTable).toHaveBeenNthCalledWith(2, {
|
|
185
|
+
filename: 'test1.xml',
|
|
186
|
+
md5: 'example-md5',
|
|
187
|
+
fileSize: '1 KB',
|
|
188
|
+
stage: FILE_STAGE.Pending,
|
|
189
|
+
s3Key: s3Key1
|
|
190
|
+
})
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('upserts sales api with transaction file details', () => {
|
|
194
|
+
expect(salesApi.upsertTransactionFile).toHaveBeenNthCalledWith(1, 'test1.xml', {
|
|
195
|
+
status: DYNAMICS_IMPORT_STAGE.Pending,
|
|
196
|
+
dataSource: POST_OFFICE_DATASOURCE,
|
|
197
|
+
fileSize: '1 KB',
|
|
198
|
+
receiptTimestamp: expect.any(String),
|
|
199
|
+
salesDate: expect.any(String),
|
|
200
|
+
notes: 'Retrieved from the remote server and awaiting processing'
|
|
201
|
+
})
|
|
157
202
|
})
|
|
158
203
|
})
|
|
159
204
|
|
|
160
205
|
it('skips file processing if a file has already been marked as processed in Dynamics', async () => {
|
|
161
|
-
fs
|
|
162
|
-
fs
|
|
206
|
+
jest.spyOn(fs, 'createReadStream').mockReturnValueOnce('teststream')
|
|
207
|
+
jest.spyOn(fs, 'statSync').mockReturnValueOnce({ size: 1024 })
|
|
163
208
|
salesApi.getTransactionFile.mockResolvedValueOnce({ status: { description: 'Processed' } })
|
|
164
209
|
const s3Key = `${moment().format('YYYY-MM-DD')}/test-already-processed.xml`
|
|
165
210
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
]
|
|
177
|
-
})
|
|
211
|
+
s3.send.mockReturnValueOnce({
|
|
212
|
+
IsTruncated: false,
|
|
213
|
+
Contents: [
|
|
214
|
+
{
|
|
215
|
+
Key: s3Key,
|
|
216
|
+
LastModified: moment().toISOString(),
|
|
217
|
+
ETag: 'example-md5',
|
|
218
|
+
Size: 1024
|
|
219
|
+
}
|
|
220
|
+
]
|
|
178
221
|
})
|
|
179
222
|
|
|
180
223
|
await refreshS3Metadata()
|
|
@@ -186,33 +229,29 @@ describe('s3 operations', () => {
|
|
|
186
229
|
it('skips file processing if a file is older than one week', async () => {
|
|
187
230
|
const s3Key1 = `${moment().format('YYYY-MM-DD')}/test1.xml`
|
|
188
231
|
|
|
189
|
-
|
|
232
|
+
s3.send
|
|
190
233
|
.mockReturnValue({
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
]
|
|
201
|
-
})
|
|
234
|
+
IsTruncated: false,
|
|
235
|
+
Contents: [
|
|
236
|
+
{
|
|
237
|
+
Key: s3Key1,
|
|
238
|
+
LastModified: moment().subtract(1, 'days').toISOString(),
|
|
239
|
+
ETag: 'example-md5',
|
|
240
|
+
Size: 1024
|
|
241
|
+
}
|
|
242
|
+
]
|
|
202
243
|
})
|
|
203
244
|
.mockReturnValueOnce({
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
]
|
|
215
|
-
})
|
|
245
|
+
IsTruncated: true,
|
|
246
|
+
NextContinuationToken: 'token',
|
|
247
|
+
Contents: [
|
|
248
|
+
{
|
|
249
|
+
Key: s3Key1,
|
|
250
|
+
LastModified: moment().subtract(1, 'days').toISOString(),
|
|
251
|
+
ETag: 'example-md5',
|
|
252
|
+
Size: 1024
|
|
253
|
+
}
|
|
254
|
+
]
|
|
216
255
|
})
|
|
217
256
|
|
|
218
257
|
await refreshS3Metadata()
|
|
@@ -221,15 +260,11 @@ describe('s3 operations', () => {
|
|
|
221
260
|
expect(salesApi.upsertTransactionFile).not.toHaveBeenCalled()
|
|
222
261
|
})
|
|
223
262
|
|
|
224
|
-
it('logs any errors raised by calling
|
|
263
|
+
it('logs any errors raised by calling ListObjectsV2Command', async () => {
|
|
225
264
|
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
|
|
226
265
|
|
|
227
266
|
const testError = new Error('Test error')
|
|
228
|
-
|
|
229
|
-
promise: () => {
|
|
230
|
-
throw testError
|
|
231
|
-
}
|
|
232
|
-
})
|
|
267
|
+
s3.send.mockRejectedValueOnce(testError)
|
|
233
268
|
|
|
234
269
|
await expect(refreshS3Metadata()).rejects.toThrow(testError)
|
|
235
270
|
expect(consoleErrorSpy).toHaveBeenCalledWith(testError)
|
|
@@ -238,10 +273,8 @@ describe('s3 operations', () => {
|
|
|
238
273
|
it('raises a warning if the bucket is empty', async () => {
|
|
239
274
|
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
|
|
240
275
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
IsTruncated: false
|
|
244
|
-
})
|
|
276
|
+
s3.send.mockReturnValueOnce({
|
|
277
|
+
IsTruncated: false
|
|
245
278
|
})
|
|
246
279
|
|
|
247
280
|
await refreshS3Metadata()
|
package/src/io/db.js
CHANGED
|
@@ -10,13 +10,11 @@ const { docClient } = AWS()
|
|
|
10
10
|
* @returns {Promise<void>}
|
|
11
11
|
*/
|
|
12
12
|
export const updateFileStagingTable = async ({ filename, ...entries }) => {
|
|
13
|
-
await docClient
|
|
14
|
-
.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
})
|
|
19
|
-
.promise()
|
|
13
|
+
await docClient.update({
|
|
14
|
+
TableName: config.db.fileStagingTable,
|
|
15
|
+
Key: { filename },
|
|
16
|
+
...docClient.createUpdateExpression({ expires: Math.floor(Date.now() / 1000) + config.db.stagingTtlDelta, ...entries })
|
|
17
|
+
})
|
|
20
18
|
}
|
|
21
19
|
|
|
22
20
|
/**
|
|
@@ -42,7 +40,7 @@ export const getFileRecords = async (...stages) => {
|
|
|
42
40
|
* @returns {DocumentClient.AttributeMap}
|
|
43
41
|
*/
|
|
44
42
|
export const getFileRecord = async filename => {
|
|
45
|
-
const result = await docClient.get({ TableName: config.db.fileStagingTable, Key: { filename }, ConsistentRead: true })
|
|
43
|
+
const result = await docClient.get({ TableName: config.db.fileStagingTable, Key: { filename }, ConsistentRead: true })
|
|
46
44
|
return result.Item
|
|
47
45
|
}
|
|
48
46
|
|
package/src/io/s3.js
CHANGED
|
@@ -4,11 +4,12 @@ import config from '../config.js'
|
|
|
4
4
|
import { DYNAMICS_IMPORT_STAGE } from '../staging/constants.js'
|
|
5
5
|
import { storeS3Metadata } from '../transport/storeS3MetaData.js'
|
|
6
6
|
import { AWS, salesApi } from '@defra-fish/connectors-lib'
|
|
7
|
-
const { s3 } = AWS()
|
|
7
|
+
const { s3, ListObjectsV2Command } = AWS()
|
|
8
8
|
|
|
9
9
|
const listObjectsV2 = async function (params) {
|
|
10
10
|
try {
|
|
11
|
-
|
|
11
|
+
const command = new ListObjectsV2Command(params)
|
|
12
|
+
return await s3.send(command)
|
|
12
13
|
} catch (e) {
|
|
13
14
|
console.error(e)
|
|
14
15
|
throw e
|
|
@@ -5,12 +5,12 @@ import { stage } from '../pocl-data-staging.js'
|
|
|
5
5
|
import { createTransactions } from '../create-transactions.js'
|
|
6
6
|
import { finaliseTransactions } from '../finalise-transactions.js'
|
|
7
7
|
import { getFileRecord, updateFileStagingTable } from '../../io/db.js'
|
|
8
|
+
|
|
8
9
|
import fs from 'fs'
|
|
9
10
|
|
|
10
11
|
jest.mock('../create-transactions.js')
|
|
11
12
|
jest.mock('../finalise-transactions.js')
|
|
12
13
|
jest.mock('../../io/db.js')
|
|
13
|
-
jest.mock('fs')
|
|
14
14
|
jest.mock('md5-file', () => () => 'test-md5')
|
|
15
15
|
|
|
16
16
|
jest.mock('@defra-fish/connectors-lib', () => {
|
|
@@ -37,6 +37,7 @@ describe('pocl data staging', () => {
|
|
|
37
37
|
['the file has not previously been processed', undefined],
|
|
38
38
|
['the file has pending state', { stage: FILE_STAGE.Pending }]
|
|
39
39
|
])('%s', async (desc, val) => {
|
|
40
|
+
jest.spyOn(fs, 'statSync')
|
|
40
41
|
getFileRecord.mockResolvedValueOnce(val)
|
|
41
42
|
getFileRecord.mockResolvedValueOnce({
|
|
42
43
|
stagingSucceeded: 5,
|
|
@@ -87,6 +88,7 @@ describe('pocl data staging', () => {
|
|
|
87
88
|
})
|
|
88
89
|
|
|
89
90
|
it('only runs finalisation if the creation phase has previously been completed', async () => {
|
|
91
|
+
jest.spyOn(fs, 'statSync')
|
|
90
92
|
getFileRecord.mockResolvedValue({ stage: FILE_STAGE.Finalising })
|
|
91
93
|
fs.statSync.mockReturnValueOnce({ size: 1024 })
|
|
92
94
|
salesApi.getTransactionFile.mockResolvedValueOnce({ status: { description: DYNAMICS_IMPORT_STAGE.InProgress } })
|
|
@@ -104,6 +106,7 @@ describe('pocl data staging', () => {
|
|
|
104
106
|
})
|
|
105
107
|
|
|
106
108
|
it('updates the status in Dynamics if the Dynamics returns InProgress but now marked completed in DynamoDB', async () => {
|
|
109
|
+
jest.spyOn(fs, 'statSync')
|
|
107
110
|
salesApi.getTransactionFile.mockResolvedValueOnce({ status: { description: DYNAMICS_IMPORT_STAGE.InProgress } })
|
|
108
111
|
getFileRecord.mockResolvedValue({ stage: FILE_STAGE.Completed })
|
|
109
112
|
fs.statSync.mockReturnValueOnce({ size: 1024 })
|
|
@@ -116,6 +119,7 @@ describe('pocl data staging', () => {
|
|
|
116
119
|
})
|
|
117
120
|
|
|
118
121
|
it('is a no-op if the file is marked as processed in Dynamics', async () => {
|
|
122
|
+
jest.spyOn(fs, 'statSync')
|
|
119
123
|
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
|
|
120
124
|
salesApi.getTransactionFile.mockResolvedValueOnce({ status: { description: DYNAMICS_IMPORT_STAGE.ProcessedWithWarnings } })
|
|
121
125
|
getFileRecord.mockResolvedValue({ stage: FILE_STAGE.Completed })
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { s3ToLocal } from '../s3-to-local.js'
|
|
2
2
|
import stream from 'stream'
|
|
3
|
-
import
|
|
3
|
+
import { AWS } from '@defra-fish/connectors-lib'
|
|
4
|
+
import fs from 'fs'
|
|
5
|
+
const { s3 } = AWS.mock.results[0].value
|
|
4
6
|
|
|
5
7
|
const MOCK_TMP = '/tmp/local/mock'
|
|
6
|
-
jest.mock('fs')
|
|
7
8
|
jest.mock('stream')
|
|
8
9
|
jest.mock('../../io/file.js', () => ({
|
|
9
10
|
getTempDir: jest.fn((...subfolders) => `${MOCK_TMP}/${subfolders.join('/')}`)
|
|
@@ -15,17 +16,23 @@ jest.mock('../../config.js', () => ({
|
|
|
15
16
|
}
|
|
16
17
|
}))
|
|
17
18
|
|
|
19
|
+
jest.mock('@defra-fish/connectors-lib', () => ({
|
|
20
|
+
AWS: jest.fn(() => ({
|
|
21
|
+
s3: {
|
|
22
|
+
getObject: jest.fn(() => ({
|
|
23
|
+
createReadStream: jest.fn(() => ({}))
|
|
24
|
+
}))
|
|
25
|
+
}
|
|
26
|
+
}))
|
|
27
|
+
}))
|
|
28
|
+
|
|
18
29
|
describe('s3-to-local', () => {
|
|
19
30
|
beforeEach(() => {
|
|
20
31
|
jest.clearAllMocks()
|
|
21
|
-
AwsMock.__resetAll()
|
|
22
32
|
})
|
|
23
33
|
|
|
24
34
|
it('retrieves a file from s3 for a given key', async () => {
|
|
25
|
-
|
|
26
|
-
AwsMock.S3.mockedMethods.getObject.mockImplementationOnce(() => {
|
|
27
|
-
return { createReadStream: mockCreateReadStream }
|
|
28
|
-
})
|
|
35
|
+
jest.spyOn(fs, 'createWriteStream').mockReturnValueOnce({})
|
|
29
36
|
stream.pipeline.mockImplementation(
|
|
30
37
|
jest.fn((streams, callback) => {
|
|
31
38
|
callback()
|
|
@@ -33,10 +40,12 @@ describe('s3-to-local', () => {
|
|
|
33
40
|
)
|
|
34
41
|
|
|
35
42
|
const result = await s3ToLocal('/example/testS3Key.xml')
|
|
43
|
+
const { createReadStream } = s3.getObject.mock.results[0].value
|
|
44
|
+
|
|
36
45
|
expect(result).toBe(`${MOCK_TMP}/example/testS3Key.xml`)
|
|
37
|
-
expect(
|
|
46
|
+
expect(createReadStream).toHaveBeenCalled()
|
|
38
47
|
expect(stream.pipeline).toHaveBeenCalled()
|
|
39
|
-
expect(
|
|
48
|
+
expect(s3.getObject).toHaveBeenCalledWith({
|
|
40
49
|
Bucket: 'testbucket',
|
|
41
50
|
Key: '/example/testS3Key.xml'
|
|
42
51
|
})
|