@natlibfi/melinda-rest-api-commons 6.0.1 → 6.1.0-alpha.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.
@@ -90,7 +90,10 @@ async function callback({
90
90
  }
91
91
  //const {correlationId} = params;
92
92
 
93
- const opResult = mongoOperator.getStream(params);
93
+ // getStream is now async (it pre-checks file existence and
94
+ // throws 404 for a missing file instead of returning a stream that
95
+ // emits an unhandled 'error' event)
96
+ const opResult = await mongoOperator.getStream(params);
94
97
  const opResultString = await streamToString(opResult);
95
98
 
96
99
  if (contentStream) {
@@ -69,6 +69,16 @@ async function callback({
69
69
  async function doPreFillDb(preFillDb) {
70
70
  if (preFillDb) {
71
71
  await mongoFixtures.populate(getFixture('dbContents.json'));
72
+ // optional gridFSFiles.json uploads GridFS files (filename -> content)
73
+ let gridFSFiles = undefined;
74
+ try {
75
+ gridFSFiles = getFixture('gridFSFiles.json');
76
+ } catch {
77
+ // no GridFS files for this fixture
78
+ }
79
+ if (gridFSFiles) {
80
+ await mongoFixtures.populateFiles(gridFSFiles);
81
+ }
72
82
  return;
73
83
  }
74
84
  return;
@@ -0,0 +1,181 @@
1
+ import assert from 'node:assert';
2
+ import {test, before, after} from 'node:test';
3
+ import mongoFixturesFactory from '@natlibfi/fixura-mongo';
4
+ import {Error as ApiError} from '@natlibfi/melinda-commons';
5
+ import createMongoLogOperator from '../src/mongoLog.js';
6
+
7
+ let mongoFixtures; // eslint-disable-line functional/no-let
8
+ let mongoLogOperator; // eslint-disable-line functional/no-let
9
+
10
+ async function initMongofixtures() {
11
+ mongoFixtures = await mongoFixturesFactory({
12
+ rootPath: [import.meta.dirname, '..', 'test-fixtures', 'mongoLog'],
13
+ gridFS: {bucketName: 'blobs'},
14
+ useObjectId: true,
15
+ format: {
16
+ logs: {
17
+ creationTime: v => new Date(v),
18
+ modificationTime: v => new Date(v)
19
+ }
20
+ }
21
+ });
22
+ const mongoUri = await mongoFixtures.getUri();
23
+ mongoLogOperator = await createMongoLogOperator(mongoUri, '');
24
+ }
25
+
26
+ async function cleanup() {
27
+ await mongoFixtures.clear();
28
+ await mongoFixtures.close();
29
+ }
30
+
31
+ before(async () => {
32
+ await initMongofixtures();
33
+ });
34
+
35
+ after(async () => {
36
+ await cleanup();
37
+ });
38
+
39
+ test('protect is idempotent: protecting an already protected log keeps it protected', async () => {
40
+ const logItem = {
41
+ logItemType: 'MERGE_LOG',
42
+ cataloger: 'IMP_TEST1',
43
+ correlationId: '22222222-2222-4222-8222-222222222222',
44
+ blobSequence: 1
45
+ };
46
+ await mongoLogOperator.addLogItem(logItem);
47
+
48
+ await mongoLogOperator.protect(logItem.correlationId, logItem.blobSequence);
49
+
50
+ // a second call must not toggle the log back to unprotected
51
+ await mongoLogOperator.protect(logItem.correlationId, logItem.blobSequence);
52
+
53
+ const {logs} = await mongoFixtures.dump();
54
+ const own = logs.filter(doc => doc.correlationId === logItem.correlationId);
55
+ assert.equal(own.length, 1);
56
+ assert.equal(own[0].protected, true);
57
+ });
58
+
59
+ test('unprotect sets protected to false after protect', async () => {
60
+ const logItem = {
61
+ logItemType: 'MERGE_LOG',
62
+ cataloger: 'IMP_TEST1',
63
+ correlationId: '33333333-3333-4333-8333-333333333333',
64
+ blobSequence: 2
65
+ };
66
+ await mongoLogOperator.addLogItem(logItem);
67
+
68
+ await mongoLogOperator.protect(logItem.correlationId, logItem.blobSequence);
69
+ const result = await mongoLogOperator.unprotect(logItem.correlationId, logItem.blobSequence);
70
+ assert.equal(result.modifiedCount, 1);
71
+
72
+ const {logs} = await mongoFixtures.dump();
73
+ const own = logs.filter(doc => doc.correlationId === logItem.correlationId);
74
+ assert.equal(own.length, 1);
75
+ assert.equal(own[0].protected, false);
76
+ });
77
+
78
+ test('unprotect without blobSequence unprotects all logs of the correlationId', async () => {
79
+ const correlationId = '44444444-4444-4444-8444-444444444444';
80
+ await mongoLogOperator.addLogItem({logItemType: 'MERGE_LOG', cataloger: 'IMP_TEST1', correlationId, blobSequence: 1});
81
+ await mongoLogOperator.addLogItem({logItemType: 'MATCH_LOG', cataloger: 'IMP_TEST1', correlationId, blobSequence: 2});
82
+
83
+ await mongoLogOperator.protect(correlationId);
84
+ const result = await mongoLogOperator.unprotect(correlationId);
85
+ assert.equal(result.modifiedCount, 2);
86
+
87
+ const {logs} = await mongoFixtures.dump();
88
+ const own = logs.filter(doc => doc.correlationId === correlationId);
89
+ assert.equal(own.length, 2);
90
+ assert.ok(own.every(doc => doc.protected === false));
91
+ });
92
+
93
+ test('getExpandedListOfLogs throws ApiError when logItemTypes is provided but is not an array', async () => {
94
+ await assert.rejects(
95
+ () => mongoLogOperator.getExpandedListOfLogs({logItemTypes: 'MERGE_LOG'}),
96
+ (error) => {
97
+ assert.ok(error instanceof ApiError);
98
+ assert.strictEqual(error.status, 400);
99
+ assert.ok(error.payload.includes('logItemTypes must be an array'));
100
+ return true;
101
+ }
102
+ );
103
+ });
104
+
105
+ test('getExpandedListOfLogs throws ApiError when logItemTypes contains an unknown element', async () => {
106
+ await assert.rejects(
107
+ () => mongoLogOperator.getExpandedListOfLogs({logItemTypes: ['MERGE_LOG', 'BOGUS_LOG']}),
108
+ (error) => {
109
+ assert.ok(error instanceof ApiError);
110
+ assert.strictEqual(error.status, 400);
111
+ assert.ok(error.payload.includes('Unknown logItemType'));
112
+ return true;
113
+ }
114
+ );
115
+ });
116
+
117
+ test('getExpandedListOfLogs uses Date instances for dateBefore and dateAfter as-is', async () => {
118
+ const logItem = {
119
+ logItemType: 'MERGE_LOG',
120
+ cataloger: 'IMP_TEST1',
121
+ correlationId: '66666666-6666-4666-8666-666666666666',
122
+ blobSequence: 1
123
+ };
124
+ await mongoLogOperator.addLogItem(logItem);
125
+ // both bounds as Date instances: the match must use them as-is (no UTC
126
+ // midnight anchoring), so the freshly created log is inside the window
127
+ const result = await mongoLogOperator.getExpandedListOfLogs({
128
+ logItemTypes: ['MERGE_LOG'],
129
+ catalogers: ['IMP_TEST1'],
130
+ dateBefore: new Date(Date.now() + 60 * 60 * 1000),
131
+ dateAfter: new Date(Date.now() - 60 * 60 * 1000)
132
+ });
133
+ const own = result.filter(entry => entry.correlationId === logItem.correlationId);
134
+ assert.equal(own.length, 1);
135
+ assert.equal(own[0].logCount, 1);
136
+ });
137
+
138
+ test('getListOfLogs throws ApiError 500 for an unknown logItemType', async () => {
139
+ await assert.rejects(
140
+ () => mongoLogOperator.getListOfLogs('BOGUS_LOG'),
141
+ (error) => {
142
+ assert.ok(error instanceof ApiError);
143
+ assert.strictEqual(error.status, 500);
144
+ assert.ok(error.payload.includes('Unknown logItemType'));
145
+ return true;
146
+ }
147
+ );
148
+ });
149
+
150
+ test('addLogItem with an unknown logItemType does not throw (warn path)', async () => {
151
+ const logItem = {
152
+ logItemType: 'BOGUS_LOG',
153
+ cataloger: 'IMP_TEST1',
154
+ correlationId: '77777777-7777-4777-8777-777777777777',
155
+ blobSequence: 1
156
+ };
157
+ // checkLogItemType is called with errorUnknown=false for addLogItem, so an
158
+ // unknown type must only warn, not throw, and the log must be stored
159
+ await mongoLogOperator.addLogItem(logItem);
160
+
161
+ const {logs} = await mongoFixtures.dump();
162
+ const own = logs.filter(doc => doc.correlationId === logItem.correlationId);
163
+ assert.equal(own.length, 1);
164
+ assert.equal(own[0].logItemType, 'BOGUS_LOG');
165
+ });
166
+
167
+ test('getExpandedListOfLogs with a valid logItemTypes array still works', async () => {
168
+ const logItem = {
169
+ logItemType: 'MERGE_LOG',
170
+ cataloger: 'IMP_TEST1',
171
+ correlationId: '55555555-5555-4555-8555-555555555555',
172
+ blobSequence: 1
173
+ };
174
+ await mongoLogOperator.addLogItem(logItem);
175
+ const result = await mongoLogOperator.getExpandedListOfLogs({logItemTypes: ['MERGE_LOG'], catalogers: ['IMP_TEST1'], dateBefore: new Date(Date.now() + 60 * 60 * 1000)});
176
+ const own = result.filter(entry => entry.correlationId === logItem.correlationId);
177
+ assert.equal(own.length, 1);
178
+ assert.equal(own[0].logItemType, 'MERGE_LOG');
179
+ assert.equal(own[0].cataloger, 'IMP_TEST1');
180
+ assert.equal(own[0].logCount, 1);
181
+ });
@@ -1,10 +1,11 @@
1
1
  import assert from 'node:assert';
2
2
  import {READERS} from '@natlibfi/fixura';
3
+ import {Error as ApiError} from '@natlibfi/melinda-commons';
3
4
  import generateTests from '@natlibfi/fixugen';
4
5
  import mongoFixturesFactory from '@natlibfi/fixura-mongo';
5
6
  import createMongoLogOperator from '../src/mongoLog.js';
6
7
 
7
- let mongoFixtures; // eslint-disable-line functional/no-let
8
+ let mongoFixtures;
8
9
 
9
10
  generateTests({
10
11
  callback,
@@ -32,9 +33,16 @@ async function initMongofixtures() {
32
33
  rootPath: [import.meta.dirname, '..', 'test-fixtures', 'mongoLog'],
33
34
  gridFS: {bucketName: 'blobs'},
34
35
  useObjectId: true,
36
+ // fixura-mongo format is keyed by COLLECTION name, so the old
37
+ // {creationTime, modificationTime} form never applied and the fixtures
38
+ // stored creationTime as an ISO string (production addLogItem stores a
39
+ // Date). Format under the 'logs' collection so tests see the real
40
+ // production shape.
35
41
  format: {
36
- creationTime: v => new Date(v),
37
- modificationTime: v => new Date(v)
42
+ logs: {
43
+ creationTime: v => new Date(v),
44
+ modificationTime: v => new Date(v)
45
+ }
38
46
  }
39
47
  });
40
48
  }
@@ -43,11 +51,40 @@ async function callback({
43
51
  getFixture,
44
52
  functionName,
45
53
  params,
46
- preFillDb = false
54
+ preFillDb = false,
55
+ expectError = false,
56
+ expectedErrorStatus = 500
47
57
  }) {
48
58
  const mongoUri = await mongoFixtures.getUri();
49
59
  const mongoLogOperator = await createMongoLogOperator(mongoUri, '');
50
- const expectedResult = await getFixture('expectedResult.json');
60
+ const expectedResult = expectError ? undefined : await getFixture('expectedResult.json');
61
+
62
+ if (expectError) {
63
+ // assert the throw (like the removeBySequences branch below)
64
+ if (functionName === 'protect') {
65
+ const [correlationId, blobSequence] = params;
66
+ return assert.rejects(
67
+ () => mongoLogOperator.protect(correlationId, blobSequence),
68
+ error => error instanceof ApiError && error.status === expectedErrorStatus
69
+ );
70
+ }
71
+ if (functionName === 'remove') {
72
+ const [correlationId, force] = params;
73
+ return assert.rejects(
74
+ () => mongoLogOperator.remove(correlationId, force),
75
+ error => error instanceof ApiError && error.status === expectedErrorStatus
76
+ );
77
+ }
78
+ if (functionName === 'removeBySequences') {
79
+ const [correlationId, sequences, force] = params;
80
+ return assert.rejects(
81
+ () => mongoLogOperator.removeBySequences(correlationId, sequences, force),
82
+ error => error instanceof ApiError && error.status === expectedErrorStatus
83
+ );
84
+ }
85
+ throw new Error(`Unknown functionName with expectError: ${functionName}`);
86
+ }
87
+
51
88
  // console.log(typeof mongoUri); // eslint-disable-line
52
89
 
53
90
  if (preFillDb) {
@@ -71,7 +108,7 @@ async function callback({
71
108
  const result = await mongoLogOperator.getExpandedListOfLogs(params);
72
109
  // console.log(result); // eslint-disable-line
73
110
 
74
- return assert.deepEqual(result, expectedResult);
111
+ return assert.deepEqual(normalizeDates(result), expectedResult);
75
112
  }
76
113
 
77
114
  if (functionName === 'getListOfLogs') {
@@ -79,6 +116,76 @@ async function callback({
79
116
  // console.log(result); // eslint-disable-line
80
117
  return assert.deepEqual(result, expectedResult);
81
118
  }
119
+
120
+ if (functionName === 'query') {
121
+ const result = normalizeDates(await mongoLogOperator.query(params));
122
+ // console.log(result); // eslint-disable-line
123
+ return assert.deepEqual(result, expectedResult);
124
+ }
125
+
126
+ if (functionName === 'queryById') {
127
+ const [correlationId, logItemType, skip, limit] = params;
128
+ const result = normalizeDates(await mongoLogOperator.queryById(correlationId, logItemType, skip, limit));
129
+ // console.log(result); // eslint-disable-line
130
+ return assert.deepEqual(result, expectedResult);
131
+ }
132
+
133
+ if (functionName === 'getListOfCorrelationIds') {
134
+ const result = await mongoLogOperator.getListOfCorrelationIds();
135
+ // console.log(result); // eslint-disable-line
136
+ return assert.deepEqual(result, expectedResult);
137
+ }
138
+
139
+ if (functionName === 'protect') {
140
+ const [correlationId, blobSequence] = params;
141
+ const result = await mongoLogOperator.protect(correlationId, blobSequence);
142
+ assert.deepEqual(result, expectedResult);
143
+ const remaining = (await dumpRemaining()).map(doc => {
144
+ // protect() sets modificationTime to the current time, so it can't be
145
+ // compared against a static fixture
146
+ const fixed = {...doc};
147
+ delete fixed.modificationTime;
148
+ return fixed;
149
+ });
150
+ return assert.deepEqual(remaining, getFixture('expectedRemaining.json'));
151
+ }
152
+
153
+ if (functionName === 'remove') {
154
+ const [correlationId, force] = params;
155
+ const result = await mongoLogOperator.remove(correlationId, force);
156
+ assert.deepEqual(result, expectedResult);
157
+ return assert.deepEqual(await dumpRemaining(), getFixture('expectedRemaining.json'));
158
+ }
159
+
160
+ if (functionName === 'removeBySequences') {
161
+ const [correlationId, sequences, force] = params;
162
+ const result = await mongoLogOperator.removeBySequences(correlationId, sequences, force);
163
+ assert.deepEqual(result, expectedResult);
164
+ return assert.deepEqual(await dumpRemaining(), getFixture('expectedRemaining.json'));
165
+ }
166
+
82
167
  throw new Error(`Unknown functionName: ${functionName}`);
83
168
  }
84
169
 
170
+ // Dump the surviving logs, normalized for comparison against a fixture
171
+ async function dumpRemaining() {
172
+ return normalizeDates((await mongoFixtures.dump()).logs);
173
+ }
174
+
175
+ // Convert BSON Dates in the result to ISO strings and drop _id, so the result
176
+ // can be compared with the plain strings in expectedResult.json /
177
+ // expectedRemaining.json (fixura-mongo dump also strips _id)
178
+ function normalizeDates(docs) {
179
+ return docs.map(doc => {
180
+ const fixed = {...doc};
181
+ delete fixed._id;
182
+ if (fixed.creationTime instanceof Date) {
183
+ fixed.creationTime = fixed.creationTime.toISOString();
184
+ }
185
+ if (fixed.modificationTime instanceof Date) {
186
+ fixed.modificationTime = fixed.modificationTime.toISOString();
187
+ }
188
+ return fixed;
189
+ });
190
+ }
191
+