@defra-fish/pocl-job 1.61.0-rc.11 → 1.61.0-rc.12
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 +4 -4
- 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 -114
- package/src/io/db.js +6 -8
- package/src/io/s3.js +1 -1
- 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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@defra-fish/pocl-job",
|
|
3
|
-
"version": "1.61.0-rc.
|
|
3
|
+
"version": "1.61.0-rc.12",
|
|
4
4
|
"description": "Post Office Counter Licence sales processor",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -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-rc.
|
|
39
|
-
"@defra-fish/connectors-lib": "1.61.0-rc.
|
|
38
|
+
"@defra-fish/business-rules-lib": "1.61.0-rc.12",
|
|
39
|
+
"@defra-fish/connectors-lib": "1.61.0-rc.12",
|
|
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": "80e68e472c3990e87675e58d0d4b55b1ed6d6c6f"
|
|
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,32 @@ 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 } = 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
|
+
listObjectsV2: jest.fn(() => ({
|
|
22
|
+
Contents: []
|
|
23
|
+
})),
|
|
24
|
+
getObject: jest.fn(() => ({
|
|
25
|
+
createReadStream: jest.fn()
|
|
26
|
+
}))
|
|
27
|
+
}
|
|
28
|
+
}))
|
|
16
29
|
return {
|
|
17
|
-
AWS
|
|
30
|
+
AWS,
|
|
18
31
|
salesApi: {
|
|
19
32
|
...Object.keys(actual.salesApi).reduce((acc, k) => ({ ...acc, [k]: jest.fn(async () => {}) }), {})
|
|
20
33
|
}
|
|
@@ -29,19 +42,19 @@ jest.mock('../../config.js', () => ({
|
|
|
29
42
|
bucket: 'testbucket'
|
|
30
43
|
}
|
|
31
44
|
}))
|
|
45
|
+
|
|
32
46
|
describe('s3 operations', () => {
|
|
33
47
|
beforeEach(() => {
|
|
34
48
|
jest.clearAllMocks()
|
|
35
|
-
AwsMock.__resetAll()
|
|
36
49
|
})
|
|
37
50
|
|
|
38
51
|
describe('refreshS3Metadata', () => {
|
|
39
|
-
|
|
52
|
+
describe('gets a list of files from S3', () => {
|
|
40
53
|
const s3Key1 = `${moment().format('YYYY-MM-DD')}/test1.xml`
|
|
41
54
|
const s3Key2 = `${moment().format('YYYY-MM-DD')}/test2.xml`
|
|
42
55
|
|
|
43
|
-
|
|
44
|
-
|
|
56
|
+
beforeEach(async () => {
|
|
57
|
+
s3.listObjectsV2.mockReturnValueOnce({
|
|
45
58
|
IsTruncated: false,
|
|
46
59
|
Contents: [
|
|
47
60
|
{
|
|
@@ -58,52 +71,66 @@ describe('s3 operations', () => {
|
|
|
58
71
|
}
|
|
59
72
|
]
|
|
60
73
|
})
|
|
61
|
-
})
|
|
62
74
|
|
|
63
|
-
|
|
75
|
+
await refreshS3Metadata()
|
|
76
|
+
})
|
|
64
77
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
78
|
+
it('calls listObjectsV2, with bucket name and no continuation token', () => {
|
|
79
|
+
expect(s3.listObjectsV2).toHaveBeenNthCalledWith(1, {
|
|
80
|
+
Bucket: 'testbucket',
|
|
81
|
+
ContinuationToken: undefined
|
|
82
|
+
})
|
|
68
83
|
})
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
84
|
+
|
|
85
|
+
it('calls updateFileStagingTable first with initial test file', () => {
|
|
86
|
+
expect(updateFileStagingTable).toHaveBeenNthCalledWith(1, {
|
|
87
|
+
filename: 'test1.xml',
|
|
88
|
+
md5: 'example-md5',
|
|
89
|
+
fileSize: '1 KB',
|
|
90
|
+
stage: FILE_STAGE.Pending,
|
|
91
|
+
s3Key: s3Key1
|
|
92
|
+
})
|
|
75
93
|
})
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
94
|
+
|
|
95
|
+
it('calls updateFileStagingTable a second time with second test file', () => {
|
|
96
|
+
expect(updateFileStagingTable).toHaveBeenNthCalledWith(2, {
|
|
97
|
+
filename: 'test2.xml',
|
|
98
|
+
md5: 'example-md5',
|
|
99
|
+
fileSize: '2 KB',
|
|
100
|
+
stage: FILE_STAGE.Pending,
|
|
101
|
+
s3Key: s3Key2
|
|
102
|
+
})
|
|
82
103
|
})
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
104
|
+
|
|
105
|
+
it('calls upsertTransactionFile for first test file', () => {
|
|
106
|
+
expect(salesApi.upsertTransactionFile).toHaveBeenNthCalledWith(1, 'test1.xml', {
|
|
107
|
+
status: DYNAMICS_IMPORT_STAGE.Pending,
|
|
108
|
+
dataSource: POST_OFFICE_DATASOURCE,
|
|
109
|
+
fileSize: '1 KB',
|
|
110
|
+
receiptTimestamp: expect.any(String),
|
|
111
|
+
salesDate: expect.any(String),
|
|
112
|
+
notes: 'Retrieved from the remote server and awaiting processing'
|
|
113
|
+
})
|
|
90
114
|
})
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
115
|
+
|
|
116
|
+
it('calls upsertTransactionFile for second test file', () => {
|
|
117
|
+
expect(salesApi.upsertTransactionFile).toHaveBeenNthCalledWith(2, 'test2.xml', {
|
|
118
|
+
status: DYNAMICS_IMPORT_STAGE.Pending,
|
|
119
|
+
dataSource: POST_OFFICE_DATASOURCE,
|
|
120
|
+
fileSize: '2 KB',
|
|
121
|
+
receiptTimestamp: expect.any(String),
|
|
122
|
+
salesDate: expect.any(String),
|
|
123
|
+
notes: 'Retrieved from the remote server and awaiting processing'
|
|
124
|
+
})
|
|
98
125
|
})
|
|
99
126
|
})
|
|
100
127
|
|
|
101
|
-
|
|
128
|
+
describe('gets a truncated list of files from S3', () => {
|
|
102
129
|
const s3Key1 = `${moment().format('YYYY-MM-DD')}/test1.xml`
|
|
103
130
|
|
|
104
|
-
|
|
105
|
-
.
|
|
106
|
-
|
|
131
|
+
beforeEach(async () => {
|
|
132
|
+
s3.listObjectsV2
|
|
133
|
+
.mockReturnValue({
|
|
107
134
|
IsTruncated: false,
|
|
108
135
|
Contents: [
|
|
109
136
|
{
|
|
@@ -114,9 +141,7 @@ describe('s3 operations', () => {
|
|
|
114
141
|
}
|
|
115
142
|
]
|
|
116
143
|
})
|
|
117
|
-
|
|
118
|
-
.mockReturnValueOnce({
|
|
119
|
-
promise: () => ({
|
|
144
|
+
.mockReturnValueOnce({
|
|
120
145
|
IsTruncated: true,
|
|
121
146
|
NextContinuationToken: 'token',
|
|
122
147
|
Contents: [
|
|
@@ -128,53 +153,72 @@ describe('s3 operations', () => {
|
|
|
128
153
|
}
|
|
129
154
|
]
|
|
130
155
|
})
|
|
131
|
-
})
|
|
132
156
|
|
|
133
|
-
|
|
157
|
+
await refreshS3Metadata()
|
|
158
|
+
})
|
|
134
159
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
160
|
+
it('calls listObjectsV2 a first time with bucket name and no continuation token', () => {
|
|
161
|
+
expect(s3.listObjectsV2).toHaveBeenNthCalledWith(1, {
|
|
162
|
+
Bucket: 'testbucket',
|
|
163
|
+
ContinuationToken: undefined
|
|
164
|
+
})
|
|
138
165
|
})
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
166
|
+
|
|
167
|
+
it('calls listObjectsV2 a second time with bucket name and continuation token', () => {
|
|
168
|
+
expect(s3.listObjectsV2).toHaveBeenNthCalledWith(2, {
|
|
169
|
+
Bucket: 'testbucket',
|
|
170
|
+
ContinuationToken: 'token'
|
|
171
|
+
})
|
|
142
172
|
})
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
173
|
+
|
|
174
|
+
it('updates file staging table with first test file', () => {
|
|
175
|
+
expect(updateFileStagingTable).toHaveBeenNthCalledWith(1, {
|
|
176
|
+
filename: 'test1.xml',
|
|
177
|
+
md5: 'example-md5',
|
|
178
|
+
fileSize: '1 KB',
|
|
179
|
+
stage: FILE_STAGE.Pending,
|
|
180
|
+
s3Key: s3Key1
|
|
181
|
+
})
|
|
149
182
|
})
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
183
|
+
|
|
184
|
+
it('updates file staging table with second test file', () => {
|
|
185
|
+
expect(updateFileStagingTable).toHaveBeenNthCalledWith(2, {
|
|
186
|
+
filename: 'test1.xml',
|
|
187
|
+
md5: 'example-md5',
|
|
188
|
+
fileSize: '1 KB',
|
|
189
|
+
stage: FILE_STAGE.Pending,
|
|
190
|
+
s3Key: s3Key1
|
|
191
|
+
})
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('upserts sales api with transaction file details', () => {
|
|
195
|
+
expect(salesApi.upsertTransactionFile).toHaveBeenNthCalledWith(1, 'test1.xml', {
|
|
196
|
+
status: DYNAMICS_IMPORT_STAGE.Pending,
|
|
197
|
+
dataSource: POST_OFFICE_DATASOURCE,
|
|
198
|
+
fileSize: '1 KB',
|
|
199
|
+
receiptTimestamp: expect.any(String),
|
|
200
|
+
salesDate: expect.any(String),
|
|
201
|
+
notes: 'Retrieved from the remote server and awaiting processing'
|
|
202
|
+
})
|
|
157
203
|
})
|
|
158
204
|
})
|
|
159
205
|
|
|
160
206
|
it('skips file processing if a file has already been marked as processed in Dynamics', async () => {
|
|
161
|
-
fs
|
|
162
|
-
fs
|
|
207
|
+
jest.spyOn(fs, 'createReadStream').mockReturnValueOnce('teststream')
|
|
208
|
+
jest.spyOn(fs, 'statSync').mockReturnValueOnce({ size: 1024 })
|
|
163
209
|
salesApi.getTransactionFile.mockResolvedValueOnce({ status: { description: 'Processed' } })
|
|
164
210
|
const s3Key = `${moment().format('YYYY-MM-DD')}/test-already-processed.xml`
|
|
165
211
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
]
|
|
177
|
-
})
|
|
212
|
+
s3.listObjectsV2.mockReturnValueOnce({
|
|
213
|
+
IsTruncated: false,
|
|
214
|
+
Contents: [
|
|
215
|
+
{
|
|
216
|
+
Key: s3Key,
|
|
217
|
+
LastModified: moment().toISOString(),
|
|
218
|
+
ETag: 'example-md5',
|
|
219
|
+
Size: 1024
|
|
220
|
+
}
|
|
221
|
+
]
|
|
178
222
|
})
|
|
179
223
|
|
|
180
224
|
await refreshS3Metadata()
|
|
@@ -186,33 +230,29 @@ describe('s3 operations', () => {
|
|
|
186
230
|
it('skips file processing if a file is older than one week', async () => {
|
|
187
231
|
const s3Key1 = `${moment().format('YYYY-MM-DD')}/test1.xml`
|
|
188
232
|
|
|
189
|
-
|
|
233
|
+
s3.listObjectsV2
|
|
190
234
|
.mockReturnValue({
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
]
|
|
201
|
-
})
|
|
235
|
+
IsTruncated: false,
|
|
236
|
+
Contents: [
|
|
237
|
+
{
|
|
238
|
+
Key: s3Key1,
|
|
239
|
+
LastModified: moment().subtract(1, 'days').toISOString(),
|
|
240
|
+
ETag: 'example-md5',
|
|
241
|
+
Size: 1024
|
|
242
|
+
}
|
|
243
|
+
]
|
|
202
244
|
})
|
|
203
245
|
.mockReturnValueOnce({
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
]
|
|
215
|
-
})
|
|
246
|
+
IsTruncated: true,
|
|
247
|
+
NextContinuationToken: 'token',
|
|
248
|
+
Contents: [
|
|
249
|
+
{
|
|
250
|
+
Key: s3Key1,
|
|
251
|
+
LastModified: moment().subtract(1, 'days').toISOString(),
|
|
252
|
+
ETag: 'example-md5',
|
|
253
|
+
Size: 1024
|
|
254
|
+
}
|
|
255
|
+
]
|
|
216
256
|
})
|
|
217
257
|
|
|
218
258
|
await refreshS3Metadata()
|
|
@@ -225,11 +265,7 @@ describe('s3 operations', () => {
|
|
|
225
265
|
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
|
|
226
266
|
|
|
227
267
|
const testError = new Error('Test error')
|
|
228
|
-
|
|
229
|
-
promise: () => {
|
|
230
|
-
throw testError
|
|
231
|
-
}
|
|
232
|
-
})
|
|
268
|
+
s3.listObjectsV2.mockRejectedValueOnce(testError)
|
|
233
269
|
|
|
234
270
|
await expect(refreshS3Metadata()).rejects.toThrow(testError)
|
|
235
271
|
expect(consoleErrorSpy).toHaveBeenCalledWith(testError)
|
|
@@ -238,10 +274,8 @@ describe('s3 operations', () => {
|
|
|
238
274
|
it('raises a warning if the bucket is empty', async () => {
|
|
239
275
|
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
|
|
240
276
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
IsTruncated: false
|
|
244
|
-
})
|
|
277
|
+
s3.listObjectsV2.mockReturnValueOnce({
|
|
278
|
+
IsTruncated: false
|
|
245
279
|
})
|
|
246
280
|
|
|
247
281
|
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
|
@@ -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
|
})
|