@natlibfi/melinda-rest-api-commons 6.2.0-alpha.1 → 6.2.0-alpha.3
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 +7 -5
- package/dist/amqp.d.ts.map +1 -1
- package/dist/amqp.js +10 -3
- package/dist/amqp.js.map +1 -1
- package/dist/helpers/fix-strip-f884s.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/mongo.d.ts +1 -0
- package/dist/mongo.d.ts.map +1 -1
- package/dist/mongo.js +25 -2
- package/dist/mongo.js.map +1 -1
- package/dist/mongoLog.d.ts +14 -1
- package/dist/mongoLog.d.ts.map +1 -1
- package/dist/mongoLog.js +11 -2
- package/dist/mongoLog.js.map +1 -1
- package/package.json +2 -2
- package/test/amqp-error-branches.test.ts +122 -33
- package/test/mongo-add-blob-size.test.ts +6 -0
- package/test/mongo-check-and-check-state.test.ts +6 -0
- package/test/mongo-check-and-set-import-job-state.test.ts +6 -0
- package/test/mongo-check-timeout.test.ts +6 -0
- package/test/mongo-create-bulk.test.ts +6 -0
- package/test/mongo-create-prio.test.ts +6 -0
- package/test/mongo-error-branches.test.ts +129 -3
- package/test/mongo-get-one.test.ts +6 -0
- package/test/mongo-get-stream.test.ts +6 -0
- package/test/mongo-push-ids.test.ts +6 -0
- package/test/mongo-push-messages.test.ts +6 -0
- package/test/mongo-query-by-id.test.ts +6 -0
- package/test/mongo-query.test.ts +6 -0
- package/test/mongo-read-content.test.ts +6 -0
- package/test/mongo-remove.test.ts +6 -0
- package/test/mongo-set-blob-size.test.ts +6 -0
- package/test/mongo-set-import-job-state.test.ts +6 -0
- package/test/mongo-set-operation.test.ts +6 -0
- package/test/mongo-set-operations.test.ts +6 -0
- package/test/mongo-set-state.test.ts +6 -0
- package/test/mongoLog-protect.test.ts +1 -0
- package/test/mongoLog.test.ts +6 -0
package/package.json
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"url": "https://github.com/NatLibFi/melinda-rest-api-commons"
|
|
14
14
|
},
|
|
15
15
|
"license": "MIT",
|
|
16
|
-
"version": "6.2.0-alpha.
|
|
16
|
+
"version": "6.2.0-alpha.3",
|
|
17
17
|
"main": "./dist/index.js",
|
|
18
18
|
"type": "module",
|
|
19
19
|
"engines": {
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"@natlibfi/marc-record-validate": "^9.0.2",
|
|
43
43
|
"@natlibfi/marc-record-validators-melinda": "^13.0.6",
|
|
44
44
|
"@natlibfi/melinda-commons": "^16.0.0",
|
|
45
|
+
"@types/node": "^25.9.9",
|
|
45
46
|
"amqplib": "^2.2.0",
|
|
46
47
|
"debug": "^4.4.3",
|
|
47
48
|
"http-status": "^2.1.0",
|
|
@@ -53,7 +54,6 @@
|
|
|
53
54
|
"@natlibfi/fixura-mongo": "^4.0.2",
|
|
54
55
|
"@types/debug": "^4.1.13",
|
|
55
56
|
"@types/http-status": "^0.2.30",
|
|
56
|
-
"@types/node": "^25.9.9",
|
|
57
57
|
"cross-env": "^10.1.0",
|
|
58
58
|
"eslint": "^10.12.0",
|
|
59
59
|
"jiti": "^2.7.0",
|
|
@@ -555,13 +555,11 @@ test('healthCheck error while running rethrows into the un-awaited loop promise'
|
|
|
555
555
|
assert.ok(stdout.endsWith('OK'), `expected child output to end with OK, got: ${stdout}`);
|
|
556
556
|
});
|
|
557
557
|
|
|
558
|
-
test('
|
|
559
|
-
//
|
|
560
|
-
//
|
|
561
|
-
//
|
|
562
|
-
//
|
|
563
|
-
// - 'close' must cancel the health check loop so no iterations keep
|
|
564
|
-
// running on a dead link (matters for runHealthCheck: false consumers)
|
|
558
|
+
test('graceful shutdown: expected close events cancel the loop without a fatal signal', async () => {
|
|
559
|
+
// closeChannel/closeConnection set shuttingDown before the intentional
|
|
560
|
+
// close, so the 'close' events amqplib emits for the expected close cancel
|
|
561
|
+
// the health check loop instead of leaving it to fail on a closed channel.
|
|
562
|
+
// The whole shutdown must not leak the fatal rejection.
|
|
565
563
|
const handlers: Record<string, ((...args: unknown[]) => void)[]> = {};
|
|
566
564
|
const makeOn = (target: string) => (event: string, handler: (...args: unknown[]) => void) => {
|
|
567
565
|
const key = `${target}:${event}`;
|
|
@@ -569,41 +567,132 @@ test('broker loss (error + close events) logs the error and close cancels the he
|
|
|
569
567
|
list.push(handler);
|
|
570
568
|
return {off: () => {}};
|
|
571
569
|
};
|
|
570
|
+
let closed = false;
|
|
572
571
|
let healthCheckCalls = 0;
|
|
573
572
|
const channel = makeChannel({
|
|
574
573
|
on: makeOn('channel'),
|
|
575
574
|
assertQueue: async (name: string) => {
|
|
576
575
|
if (name === 'HEALTHCHECK') {
|
|
577
576
|
healthCheckCalls += 1;
|
|
577
|
+
if (closed) {
|
|
578
|
+
throw new Error('channel closed');
|
|
579
|
+
}
|
|
578
580
|
}
|
|
579
581
|
return {messageCount: 0};
|
|
582
|
+
},
|
|
583
|
+
close: async () => {
|
|
584
|
+
closed = true;
|
|
580
585
|
}
|
|
581
586
|
});
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
587
|
+
const unhandled: unknown[] = [];
|
|
588
|
+
const onUnhandledRejection = (error: unknown) => unhandled.push(error);
|
|
589
|
+
process.on('unhandledRejection', onUnhandledRejection);
|
|
590
|
+
try {
|
|
591
|
+
const amqpOperator = await createAmqp(DUMMY_URL, true, {
|
|
592
|
+
connection: {createChannel: async () => channel, close: async () => {}, on: makeOn('connection')},
|
|
593
|
+
channel
|
|
594
|
+
});
|
|
595
|
+
await new Promise((resolve) => setTimeout(resolve, 350));
|
|
596
|
+
assert.ok(healthCheckCalls > 0, 'health check should have run before close');
|
|
597
|
+
await amqpOperator.closeChannel();
|
|
598
|
+
// the expected close events: they must not change the already-cancelled
|
|
599
|
+
// state into a fatal one
|
|
600
|
+
for (const handler of handlers['channel:close'] ?? []) handler();
|
|
601
|
+
for (const handler of handlers['connection:close'] ?? []) handler();
|
|
602
|
+
// wait longer than the 200 ms loop interval
|
|
603
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
604
|
+
} finally {
|
|
605
|
+
process.off('unhandledRejection', onUnhandledRejection);
|
|
606
|
+
}
|
|
607
|
+
assert.deepStrictEqual(unhandled, [], 'no unhandled rejections on graceful shutdown');
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
test('broker loss (error + close events) preserves the healthCheck fast-fail', async () => {
|
|
611
|
+
// Failure-path listeners on the amqplib connection/channel:
|
|
612
|
+
// - 'error' must only log: without a handler an unhandled EventEmitter
|
|
613
|
+
// 'error' would become an uncaughtException
|
|
614
|
+
// - an UNEXPECTED 'close' (link death / broker loss, shuttingDown false)
|
|
615
|
+
// must NOT cancel the health check loop: the loop keeps failing on the
|
|
616
|
+
// dead broker and the error escapes as an unhandled rejection - the
|
|
617
|
+
// documented fast-fail (exit(1)). Only an intentional
|
|
618
|
+
// closeChannel/closeConnection cancels the loop and suppresses the
|
|
619
|
+
// rethrow.
|
|
620
|
+
// The escape is verified in a child node process that installs its own
|
|
621
|
+
// unhandledRejection handler before triggering the loss (the node test
|
|
622
|
+
// runner fails a test on any in-process unhandled rejection).
|
|
623
|
+
const script = `
|
|
624
|
+
import createAmqp from '${AMQP_MODULE_PATH}';
|
|
625
|
+
const handlers = {};
|
|
626
|
+
const makeOn = (target) => (event, handler) => {
|
|
627
|
+
const key = target + ':' + event;
|
|
628
|
+
const list = handlers[key] ?? (handlers[key] = []);
|
|
629
|
+
list.push(handler);
|
|
630
|
+
return {off: () => {}};
|
|
631
|
+
};
|
|
632
|
+
let brokerLost = false;
|
|
633
|
+
let healthCheckCalls = 0;
|
|
634
|
+
const channel = {
|
|
635
|
+
assertQueue: /** @param {*} name */ async (name) => {
|
|
636
|
+
if (name === 'HEALTHCHECK') {
|
|
637
|
+
healthCheckCalls += 1;
|
|
638
|
+
if (brokerLost) {
|
|
639
|
+
throw new Error('broker lost');
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
return {messageCount: 0};
|
|
643
|
+
},
|
|
644
|
+
checkQueue: async () => ({messageCount: 0}),
|
|
645
|
+
get: async () => false,
|
|
646
|
+
ack: () => {},
|
|
647
|
+
nack: () => {},
|
|
648
|
+
purgeQueue: async () => {},
|
|
649
|
+
close: async () => {},
|
|
650
|
+
sendToQueue: async () => {},
|
|
651
|
+
on: makeOn('channel')
|
|
652
|
+
};
|
|
653
|
+
const captured = [];
|
|
654
|
+
process.on('unhandledRejection', (err) => captured.push(err));
|
|
655
|
+
await createAmqp('amqp://stub:5672/', true, {
|
|
656
|
+
connection: {createChannel: async () => channel, close: async () => {}, on: makeOn('connection')},
|
|
657
|
+
channel
|
|
658
|
+
});
|
|
659
|
+
// all four listeners must be attached (connection + channel, error + close)
|
|
660
|
+
const attached = Object.keys(handlers).sort();
|
|
661
|
+
const listenersOk = JSON.stringify(attached) === JSON.stringify(['channel:close', 'channel:error', 'connection:close', 'connection:error']);
|
|
662
|
+
// let the loop run a couple of iterations, then simulate broker loss the
|
|
663
|
+
// way amqplib signals it: an 'error' event followed by an unexpected
|
|
664
|
+
// 'close' (no closeChannel/closeConnection was called)
|
|
665
|
+
await new Promise((resolve) => setTimeout(resolve, 350));
|
|
666
|
+
for (const handler of handlers['channel:error'] ?? []) handler(new Error('socket lost'));
|
|
667
|
+
for (const handler of handlers['connection:error'] ?? []) handler(new Error('socket lost'));
|
|
668
|
+
for (const handler of handlers['channel:close'] ?? []) handler();
|
|
669
|
+
for (const handler of handlers['connection:close'] ?? []) handler();
|
|
670
|
+
// the unexpected close must NOT cancel the loop: it keeps running on the
|
|
671
|
+
// still-healthy stub (wait longer than the 200 ms loop interval)
|
|
672
|
+
const callsAfterClose = healthCheckCalls;
|
|
673
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
674
|
+
const keptRunning = healthCheckCalls > callsAfterClose;
|
|
675
|
+
// now the broker is actually gone: the next failed assert must escape as
|
|
676
|
+
// the unhandled rejection (the documented fast-fail)
|
|
677
|
+
brokerLost = true;
|
|
678
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
679
|
+
const ok =
|
|
680
|
+
listenersOk &&
|
|
681
|
+
healthCheckCalls > 0 &&
|
|
682
|
+
keptRunning &&
|
|
683
|
+
captured.length === 1 &&
|
|
684
|
+
/broker lost/.test(String(captured[0] && captured[0].message));
|
|
685
|
+
process.stdout.write(ok ? 'OK' : 'FAIL:' + JSON.stringify({attached, healthCheckCalls, rejections: captured.map((e) => String(e && e.message))}));
|
|
686
|
+
process.exit(ok ? 0 : 1);
|
|
687
|
+
`;
|
|
688
|
+
const {code, stdout} = await new Promise<{code: number | string, stdout: string}>((resolve) => {
|
|
689
|
+
const child = execFile(process.execPath, ['--input-type=module', '-e', script], (err, out) => {
|
|
690
|
+
resolve({code: err ? (err.code ?? 1) : 0, stdout: out ?? ''});
|
|
691
|
+
});
|
|
692
|
+
void child;
|
|
585
693
|
});
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
'connection:close',
|
|
591
|
-
'connection:error'
|
|
592
|
-
]);
|
|
593
|
-
// let the loop run a couple of iterations, then simulate broker loss the
|
|
594
|
-
// way amqplib signals it: an 'error' event followed by 'close'
|
|
595
|
-
await new Promise((resolve) => setTimeout(resolve, 350));
|
|
596
|
-
for (const handler of handlers['channel:error'] ?? []) handler(new Error('socket lost'));
|
|
597
|
-
for (const handler of handlers['connection:error'] ?? []) handler(new Error('socket lost'));
|
|
598
|
-
// the 'error' events alone must not cancel the loop
|
|
599
|
-
const callsAfterError = healthCheckCalls;
|
|
600
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
601
|
-
assert.ok(healthCheckCalls > 0, 'health check should have run before the broker loss');
|
|
602
|
-
assert.ok(healthCheckCalls > callsAfterError, 'health check keeps running after the error events');
|
|
603
|
-
for (const handler of handlers['channel:close'] ?? []) handler();
|
|
604
|
-
for (const handler of handlers['connection:close'] ?? []) handler();
|
|
605
|
-
const callsAfterClose = healthCheckCalls;
|
|
606
|
-
// wait longer than the 200 ms loop interval - no further health checks may fire
|
|
607
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
608
|
-
assert.equal(healthCheckCalls, callsAfterClose, 'no health checks after the close events');
|
|
694
|
+
assert.equal(code, 0, `child process output: ${stdout}`);
|
|
695
|
+
// the error listeners and handleAmqpErrors log to stdout before the escape,
|
|
696
|
+
// so only the tail matters
|
|
697
|
+
assert.ok(stdout.endsWith('OK'), `expected child output to end with OK, got: ${stdout}`);
|
|
609
698
|
});
|
|
@@ -8,6 +8,7 @@ import {getMongoOperator, handleError, compareToFirstDbEntry} from './testUtils.
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
let mongoFixtures: any;
|
|
11
|
+
const openedOperators: any[] = [];
|
|
11
12
|
const debug = createDebugLogger('@natlibfi/melinda-rest-api-commons/mongo:add-blob-size:test');
|
|
12
13
|
|
|
13
14
|
generateTests({
|
|
@@ -34,6 +35,10 @@ generateTests({
|
|
|
34
35
|
},
|
|
35
36
|
after: async () => {
|
|
36
37
|
//debug(`<< After`);
|
|
38
|
+
// close the operators the tests opened (one per test) before the server
|
|
39
|
+
for (const op of openedOperators) {
|
|
40
|
+
await op.close();
|
|
41
|
+
}
|
|
37
42
|
await mongoFixtures.close();
|
|
38
43
|
}
|
|
39
44
|
}
|
|
@@ -61,6 +66,7 @@ async function callback(callbackParams: Record<string, any>) {
|
|
|
61
66
|
} = callbackParams;
|
|
62
67
|
|
|
63
68
|
const mongoOperator = await getMongoOperator(mongoFixtures);
|
|
69
|
+
openedOperators.push(mongoOperator);
|
|
64
70
|
const expectedResult = await getFixture('expectedResult.json');
|
|
65
71
|
|
|
66
72
|
await doPreFillDb(preFillDb);
|
|
@@ -8,6 +8,7 @@ import {getMongoOperator, handleError, compareToFirstDbEntry} from './testUtils.
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
let mongoFixtures: any;
|
|
11
|
+
const openedOperators: any[] = [];
|
|
11
12
|
const debug = createDebugLogger('@natlibfi/melinda-rest-api-commons/mongo:test');
|
|
12
13
|
|
|
13
14
|
generateTests({
|
|
@@ -34,6 +35,10 @@ generateTests({
|
|
|
34
35
|
},
|
|
35
36
|
after: async () => {
|
|
36
37
|
//debug(`<< After`);
|
|
38
|
+
// close the operators the tests opened (one per test) before the server
|
|
39
|
+
for (const op of openedOperators) {
|
|
40
|
+
await op.close();
|
|
41
|
+
}
|
|
37
42
|
await mongoFixtures.close();
|
|
38
43
|
}
|
|
39
44
|
}
|
|
@@ -62,6 +67,7 @@ async function callback(callbackParams: Record<string, any>) {
|
|
|
62
67
|
} = callbackParams;
|
|
63
68
|
|
|
64
69
|
const mongoOperator = await getMongoOperator(mongoFixtures);
|
|
70
|
+
openedOperators.push(mongoOperator);
|
|
65
71
|
const expectedResult = await getFixture('expectedResult.json');
|
|
66
72
|
|
|
67
73
|
await doPreFillDb(preFillDb);
|
|
@@ -7,6 +7,7 @@ import createDebugLogger from 'debug';
|
|
|
7
7
|
import {getMongoOperator, handleError, compareToFirstDbEntry} from './testUtils.ts';
|
|
8
8
|
|
|
9
9
|
let mongoFixtures: any;
|
|
10
|
+
const openedOperators: any[] = [];
|
|
10
11
|
const debug = createDebugLogger('@natlibfi/melinda-rest-api-commons/mongo:check-and-set-import-job-state-test');
|
|
11
12
|
|
|
12
13
|
generateTests({
|
|
@@ -33,6 +34,10 @@ generateTests({
|
|
|
33
34
|
},
|
|
34
35
|
after: async () => {
|
|
35
36
|
//debug(`<< After`);
|
|
37
|
+
// close the operators the tests opened (one per test) before the server
|
|
38
|
+
for (const op of openedOperators) {
|
|
39
|
+
await op.close();
|
|
40
|
+
}
|
|
36
41
|
await mongoFixtures.close();
|
|
37
42
|
}
|
|
38
43
|
}
|
|
@@ -61,6 +66,7 @@ async function callback(callbackParams: Record<string, any>) {
|
|
|
61
66
|
} = callbackParams;
|
|
62
67
|
|
|
63
68
|
const mongoOperator = await getMongoOperator(mongoFixtures);
|
|
69
|
+
openedOperators.push(mongoOperator);
|
|
64
70
|
const expectedResult = await getFixture('expectedResult.json');
|
|
65
71
|
|
|
66
72
|
await doPreFillDb(preFillDb);
|
|
@@ -8,6 +8,7 @@ import {getMongoOperator, handleError, compareToFirstDbEntry} from './testUtils.
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
let mongoFixtures: any;
|
|
11
|
+
const openedOperators: any[] = [];
|
|
11
12
|
const debug = createDebugLogger('@natlibfi/melinda-rest-api-commons/mongo:check-timeout:test');
|
|
12
13
|
|
|
13
14
|
generateTests({
|
|
@@ -34,6 +35,10 @@ generateTests({
|
|
|
34
35
|
},
|
|
35
36
|
after: async () => {
|
|
36
37
|
//debug(`<< After`);
|
|
38
|
+
// close the operators the tests opened (one per test) before the server
|
|
39
|
+
for (const op of openedOperators) {
|
|
40
|
+
await op.close();
|
|
41
|
+
}
|
|
37
42
|
await mongoFixtures.close();
|
|
38
43
|
}
|
|
39
44
|
}
|
|
@@ -62,6 +67,7 @@ async function callback(callbackParams: Record<string, any>) {
|
|
|
62
67
|
} = callbackParams;
|
|
63
68
|
|
|
64
69
|
const mongoOperator = await getMongoOperator(mongoFixtures);
|
|
70
|
+
openedOperators.push(mongoOperator);
|
|
65
71
|
const expectedResult = await getFixture('expectedResult.json');
|
|
66
72
|
|
|
67
73
|
await doPreFillDb(preFillDb);
|
|
@@ -6,6 +6,7 @@ import createDebugLogger from 'debug';
|
|
|
6
6
|
import {getMongoOperator, handleError, compareToFirstDbEntry} from './testUtils.ts';
|
|
7
7
|
|
|
8
8
|
let mongoFixtures: any;
|
|
9
|
+
const openedOperators: any[] = [];
|
|
9
10
|
const debug = createDebugLogger('@natlibfi/melinda-rest-api-commons/mongo:test:create-bulk');
|
|
10
11
|
|
|
11
12
|
generateTests({
|
|
@@ -32,6 +33,10 @@ generateTests({
|
|
|
32
33
|
},
|
|
33
34
|
after: async () => {
|
|
34
35
|
//debug(`<< After`);
|
|
36
|
+
// close the operators the tests opened (one per test) before the server
|
|
37
|
+
for (const op of openedOperators) {
|
|
38
|
+
await op.close();
|
|
39
|
+
}
|
|
35
40
|
await mongoFixtures.close();
|
|
36
41
|
}
|
|
37
42
|
}
|
|
@@ -59,6 +64,7 @@ async function callback(callbackParams: Record<string, any>) {
|
|
|
59
64
|
} = callbackParams;
|
|
60
65
|
|
|
61
66
|
const mongoOperator = await getMongoOperator(mongoFixtures);
|
|
67
|
+
openedOperators.push(mongoOperator);
|
|
62
68
|
const expectedResult = await getFixture('expectedResult.json');
|
|
63
69
|
|
|
64
70
|
await doPreFillDb(preFillDb);
|
|
@@ -7,6 +7,7 @@ import {getMongoOperator, handleError, compareToFirstDbEntry} from './testUtils.
|
|
|
7
7
|
// import {handleError, compareToFirstDbEntry, compareToDbEntry, formatQueueItem, streamToString} from './testUtils.js';
|
|
8
8
|
|
|
9
9
|
let mongoFixtures: any;
|
|
10
|
+
const openedOperators: any[] = [];
|
|
10
11
|
const debug = createDebugLogger('@natlibfi/melinda-rest-api-commons/mongo:create-prio-test');
|
|
11
12
|
|
|
12
13
|
generateTests({
|
|
@@ -33,6 +34,10 @@ generateTests({
|
|
|
33
34
|
},
|
|
34
35
|
after: async () => {
|
|
35
36
|
//debug(`<< After`);
|
|
37
|
+
// close the operators the tests opened (one per test) before the server
|
|
38
|
+
for (const op of openedOperators) {
|
|
39
|
+
await op.close();
|
|
40
|
+
}
|
|
36
41
|
await mongoFixtures.close();
|
|
37
42
|
}
|
|
38
43
|
}
|
|
@@ -58,6 +63,7 @@ async function callback(callbackParams: Record<string, any>) {
|
|
|
58
63
|
} = callbackParams;
|
|
59
64
|
|
|
60
65
|
const mongoOperator = await getMongoOperator(mongoFixtures);
|
|
66
|
+
openedOperators.push(mongoOperator);
|
|
61
67
|
const expectedResult = await getFixture('expectedResult.json');
|
|
62
68
|
|
|
63
69
|
await doPreFillDb(preFillDb);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from 'node:assert';
|
|
2
|
-
import {test} from 'node:test';
|
|
2
|
+
import {test, after} from 'node:test';
|
|
3
3
|
import {Readable, PassThrough} from 'node:stream';
|
|
4
4
|
import httpStatus from 'http-status';
|
|
5
5
|
import {MongoDriverError} from 'mongodb';
|
|
@@ -9,6 +9,9 @@ import createMongoLog from '../src/mongoLog.ts';
|
|
|
9
9
|
|
|
10
10
|
const DUMMY_URI = 'mongodb://stub:27017/rest-api';
|
|
11
11
|
const COLLECTION = 'foobar';
|
|
12
|
+
// every test creates its own (stubbed) operators; close them all at the
|
|
13
|
+
// end - close is a no-op for stubbed factories, which is the point
|
|
14
|
+
const stubOperators: any[] = [];
|
|
12
15
|
|
|
13
16
|
// the deps seam takes unknown-typed dependencies, so plain stub objects
|
|
14
17
|
// are fine to inject (the driver types are never exercised here)
|
|
@@ -35,7 +38,9 @@ function makeGridFSBucket(overrides: Record<string, any> = {}) {
|
|
|
35
38
|
* @param deps the injected stub operator/gridFSBucket
|
|
36
39
|
*/
|
|
37
40
|
async function makeMongo(deps: Record<string, any>) {
|
|
38
|
-
|
|
41
|
+
const mongo = await createMongo(DUMMY_URI, COLLECTION, 'rest-api', deps);
|
|
42
|
+
stubOperators.push(mongo);
|
|
43
|
+
return mongo;
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
// The 400-guard tests pass intentionally malformed inputs (non-string
|
|
@@ -214,6 +219,119 @@ test('checkTimeOut returns false (no crash) when the queueItem does not exist',
|
|
|
214
219
|
assert.equal(result, false);
|
|
215
220
|
});
|
|
216
221
|
|
|
222
|
+
// checkAndSetState / checkAndSetImportJobState must report a missing item
|
|
223
|
+
// as 'unchanged' (with a null queueItem) — not as a timeout
|
|
224
|
+
|
|
225
|
+
test('checkAndSetState returns {status: "unchanged", queueItem: null} when the queueItem does not exist', async () => {
|
|
226
|
+
const mongo = await makeMongo({
|
|
227
|
+
operator: makeOperator({findOne: async () => null}),
|
|
228
|
+
gridFSBucket: makeGridFSBucket()
|
|
229
|
+
});
|
|
230
|
+
const result = await mongo.checkAndSetState({correlationId: 'NOPE', state: 'VALIDATING'});
|
|
231
|
+
assert.deepStrictEqual(result, {status: 'unchanged', queueItem: null});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('checkAndSetImportJobState returns {status: "unchanged", queueItem: null} when the queueItem does not exist', async () => {
|
|
235
|
+
const mongo = await makeMongo({
|
|
236
|
+
operator: makeOperator({findOne: async () => null}),
|
|
237
|
+
gridFSBucket: makeGridFSBucket()
|
|
238
|
+
});
|
|
239
|
+
const result = await mongo.checkAndSetImportJobState({correlationId: 'NOPE', operation: 'FIX', importJobState: 'DONE'});
|
|
240
|
+
assert.deepStrictEqual(result, {status: 'unchanged', queueItem: null});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// a pre-existing ABORT item must be reported as 'unchanged' (not
|
|
244
|
+
// 'timeout', which is reserved for items aborted by checkTimeOut in this
|
|
245
|
+
// call) and the document must be left untouched
|
|
246
|
+
|
|
247
|
+
test('checkAndSetState reports a pre-existing ABORT as "unchanged" without writing', async () => {
|
|
248
|
+
const doc = {correlationId: 'FOO', queueItemState: 'ABORT', errorStatus: 408, errorMessage: 'Timeout in VALIDATING'};
|
|
249
|
+
const writes: unknown[] = [];
|
|
250
|
+
const mongo = await makeMongo({
|
|
251
|
+
operator: makeOperator({
|
|
252
|
+
findOne: async () => doc,
|
|
253
|
+
findOneAndUpdate: async (filter: unknown, update: unknown) => {
|
|
254
|
+
writes.push({filter, update});
|
|
255
|
+
return doc;
|
|
256
|
+
}
|
|
257
|
+
}),
|
|
258
|
+
gridFSBucket: makeGridFSBucket()
|
|
259
|
+
});
|
|
260
|
+
const result = await mongo.checkAndSetState({correlationId: 'FOO', state: 'VALIDATING'});
|
|
261
|
+
assert.deepStrictEqual(result, {status: 'unchanged', queueItem: doc});
|
|
262
|
+
assert.deepStrictEqual(writes, []);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test('checkAndSetImportJobState reports a pre-existing ABORT as "unchanged" without writing', async () => {
|
|
266
|
+
const doc = {correlationId: 'FOO', queueItemState: 'ABORT', errorStatus: 408, errorMessage: 'Timeout in VALIDATING'};
|
|
267
|
+
const writes: unknown[] = [];
|
|
268
|
+
const mongo = await makeMongo({
|
|
269
|
+
operator: makeOperator({
|
|
270
|
+
findOne: async () => doc,
|
|
271
|
+
findOneAndUpdate: async (filter: unknown, update: unknown) => {
|
|
272
|
+
writes.push({filter, update});
|
|
273
|
+
return doc;
|
|
274
|
+
}
|
|
275
|
+
}),
|
|
276
|
+
gridFSBucket: makeGridFSBucket()
|
|
277
|
+
});
|
|
278
|
+
const result = await mongo.checkAndSetImportJobState({correlationId: 'FOO', operation: 'FIX', importJobState: 'DONE'});
|
|
279
|
+
assert.deepStrictEqual(result, {status: 'unchanged', queueItem: doc});
|
|
280
|
+
assert.deepStrictEqual(writes, []);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// Known pre-existing concurrency outcome, pinned here as a regression test
|
|
284
|
+
// (a future redesign intentionally changes it): the document is VALIDATING,
|
|
285
|
+
// a concurrent ABORT request comes in and aborts the item, then a second
|
|
286
|
+
// ABORT request comes in. If that second request's pre-read still sees
|
|
287
|
+
// VALIDATING (the first request's abort lands between the pre-read and the
|
|
288
|
+
// internal timeout check), checkTimeOut finds the already-ABORTED document,
|
|
289
|
+
// declines to act, and the post-hoc read still sees ABORT — the second
|
|
290
|
+
// ABORT is reported as 'timeout' (not 'unchanged')
|
|
291
|
+
test('checkAndSetState reports a second concurrent ABORT as "timeout" when the first ABORT lands after its pre-read', async () => {
|
|
292
|
+
const validDoc = {correlationId: 'FOO', queueItemState: 'VALIDATING'};
|
|
293
|
+
const abortDoc = {correlationId: 'FOO', queueItemState: 'ABORT', errorStatus: 408, errorMessage: 'Timeout in VALIDATING'};
|
|
294
|
+
// read order for the second ABORT request: the pre-read still sees
|
|
295
|
+
// VALIDATING, then the first request's abort lands, so both the timeout
|
|
296
|
+
// check read and the post-hoc read see ABORT
|
|
297
|
+
const reads = [validDoc, abortDoc, abortDoc];
|
|
298
|
+
const writes: unknown[] = [];
|
|
299
|
+
const mongo = await makeMongo({
|
|
300
|
+
operator: makeOperator({
|
|
301
|
+
findOne: async () => reads.shift() ?? abortDoc,
|
|
302
|
+
findOneAndUpdate: async (filter: unknown, update: unknown) => {
|
|
303
|
+
writes.push({filter, update});
|
|
304
|
+
return abortDoc;
|
|
305
|
+
}
|
|
306
|
+
}),
|
|
307
|
+
gridFSBucket: makeGridFSBucket()
|
|
308
|
+
});
|
|
309
|
+
const result = await mongo.checkAndSetState({correlationId: 'FOO', state: 'ABORT'});
|
|
310
|
+
assert.deepStrictEqual(result, {status: 'timeout', queueItem: abortDoc});
|
|
311
|
+
assert.deepStrictEqual(writes, []);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// a pre-existing DONE item is terminal too (like ABORT/ERROR) and must be
|
|
315
|
+
// reported as 'unchanged' without touching the document
|
|
316
|
+
|
|
317
|
+
test('checkAndSetState reports a pre-existing DONE as "unchanged" without writing', async () => {
|
|
318
|
+
const doc = {correlationId: 'FOO', queueItemState: 'DONE'};
|
|
319
|
+
const writes: unknown[] = [];
|
|
320
|
+
const mongo = await makeMongo({
|
|
321
|
+
operator: makeOperator({
|
|
322
|
+
findOne: async () => doc,
|
|
323
|
+
findOneAndUpdate: async (filter: unknown, update: unknown) => {
|
|
324
|
+
writes.push({filter, update});
|
|
325
|
+
return doc;
|
|
326
|
+
}
|
|
327
|
+
}),
|
|
328
|
+
gridFSBucket: makeGridFSBucket()
|
|
329
|
+
});
|
|
330
|
+
const result = await mongo.checkAndSetState({correlationId: 'FOO', state: 'VALIDATING'});
|
|
331
|
+
assert.deepStrictEqual(result, {status: 'unchanged', queueItem: doc});
|
|
332
|
+
assert.deepStrictEqual(writes, []);
|
|
333
|
+
});
|
|
334
|
+
|
|
217
335
|
test('setOperation rejects with ApiError 404 for a non-matching correlationId', async () => {
|
|
218
336
|
const mongo = await makeMongo({
|
|
219
337
|
operator: makeOperator({findOne: async () => null}),
|
|
@@ -750,9 +868,17 @@ function makeLogCollection(overrides: Record<string, any> = {}) {
|
|
|
750
868
|
* @param deps the injected stub collection
|
|
751
869
|
*/
|
|
752
870
|
async function makeMongoLog(deps: Record<string, any>) {
|
|
753
|
-
|
|
871
|
+
const mongoLog = await createMongoLog(DUMMY_URI, 'rest-api', deps);
|
|
872
|
+
stubOperators.push(mongoLog);
|
|
873
|
+
return mongoLog;
|
|
754
874
|
}
|
|
755
875
|
|
|
876
|
+
after(async () => {
|
|
877
|
+
for (const op of stubOperators) {
|
|
878
|
+
await op.close();
|
|
879
|
+
}
|
|
880
|
+
});
|
|
881
|
+
|
|
756
882
|
function mongoErrored500(text: string) {
|
|
757
883
|
return (error: unknown) => {
|
|
758
884
|
assert.ok(error instanceof ApiError);
|
|
@@ -8,6 +8,7 @@ import {getMongoOperator, handleError} from './testUtils.ts';
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
let mongoFixtures: any;
|
|
11
|
+
const openedOperators: any[] = [];
|
|
11
12
|
const debug = createDebugLogger('@natlibfi/melinda-rest-api-commons/mongo:get-one:test');
|
|
12
13
|
|
|
13
14
|
generateTests({
|
|
@@ -34,6 +35,10 @@ generateTests({
|
|
|
34
35
|
},
|
|
35
36
|
after: async () => {
|
|
36
37
|
//debug(`<< After`);
|
|
38
|
+
// close the operators the tests opened (one per test) before the server
|
|
39
|
+
for (const op of openedOperators) {
|
|
40
|
+
await op.close();
|
|
41
|
+
}
|
|
37
42
|
await mongoFixtures.close();
|
|
38
43
|
}
|
|
39
44
|
}
|
|
@@ -60,6 +65,7 @@ async function callback(callbackParams: Record<string, any>) {
|
|
|
60
65
|
} = callbackParams;
|
|
61
66
|
|
|
62
67
|
const mongoOperator = await getMongoOperator(mongoFixtures);
|
|
68
|
+
openedOperators.push(mongoOperator);
|
|
63
69
|
//const expectedResult = await getFixture('expectedResult.json');
|
|
64
70
|
|
|
65
71
|
await doPreFillDb(preFillDb);
|
|
@@ -7,6 +7,7 @@ import createDebugLogger from 'debug';
|
|
|
7
7
|
import {getMongoOperator, handleError, streamToString} from './testUtils.ts';
|
|
8
8
|
|
|
9
9
|
let mongoFixtures: any;
|
|
10
|
+
const openedOperators: any[] = [];
|
|
10
11
|
const debug = createDebugLogger('@natlibfi/melinda-rest-api-commons/mongo:get-stream:test');
|
|
11
12
|
|
|
12
13
|
generateTests({
|
|
@@ -33,6 +34,10 @@ generateTests({
|
|
|
33
34
|
},
|
|
34
35
|
after: async () => {
|
|
35
36
|
//debug(`<< After`);
|
|
37
|
+
// close the operators the tests opened (one per test) before the server
|
|
38
|
+
for (const op of openedOperators) {
|
|
39
|
+
await op.close();
|
|
40
|
+
}
|
|
36
41
|
await mongoFixtures.close();
|
|
37
42
|
}
|
|
38
43
|
}
|
|
@@ -60,6 +65,7 @@ async function callback(callbackParams: Record<string, any>) {
|
|
|
60
65
|
} = callbackParams;
|
|
61
66
|
|
|
62
67
|
const mongoOperator = await getMongoOperator(mongoFixtures);
|
|
68
|
+
openedOperators.push(mongoOperator);
|
|
63
69
|
//const expectedResult = await getFixture('expectedResult.json');
|
|
64
70
|
|
|
65
71
|
await doPreFillDb(preFillDb);
|
|
@@ -7,6 +7,7 @@ import {getMongoOperator, handleError, compareToFirstDbEntry} from './testUtils.
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
let mongoFixtures: any;
|
|
10
|
+
const openedOperators: any[] = [];
|
|
10
11
|
const debug = createDebugLogger('@natlibfi/melinda-rest-api-commons/mongo:push-ids:test');
|
|
11
12
|
|
|
12
13
|
generateTests({
|
|
@@ -33,6 +34,10 @@ generateTests({
|
|
|
33
34
|
},
|
|
34
35
|
after: async () => {
|
|
35
36
|
//debug(`<< After`);
|
|
37
|
+
// close the operators the tests opened (one per test) before the server
|
|
38
|
+
for (const op of openedOperators) {
|
|
39
|
+
await op.close();
|
|
40
|
+
}
|
|
36
41
|
await mongoFixtures.close();
|
|
37
42
|
}
|
|
38
43
|
}
|
|
@@ -59,6 +64,7 @@ async function callback(callbackParams: Record<string, any>) {
|
|
|
59
64
|
} = callbackParams;
|
|
60
65
|
|
|
61
66
|
const mongoOperator = await getMongoOperator(mongoFixtures);
|
|
67
|
+
openedOperators.push(mongoOperator);
|
|
62
68
|
const expectedResult = await getFixture('expectedResult.json');
|
|
63
69
|
|
|
64
70
|
await doPreFillDb(preFillDb);
|
|
@@ -7,6 +7,7 @@ import createDebugLogger from 'debug';
|
|
|
7
7
|
import {getMongoOperator, handleError, compareToFirstDbEntry} from './testUtils.ts';
|
|
8
8
|
|
|
9
9
|
let mongoFixtures: any;
|
|
10
|
+
const openedOperators: any[] = [];
|
|
10
11
|
const debug = createDebugLogger('@natlibfi/melinda-rest-api-commons/mongo:push-messages:test');
|
|
11
12
|
|
|
12
13
|
generateTests({
|
|
@@ -33,6 +34,10 @@ generateTests({
|
|
|
33
34
|
},
|
|
34
35
|
after: async () => {
|
|
35
36
|
//debug(`<< After`);
|
|
37
|
+
// close the operators the tests opened (one per test) before the server
|
|
38
|
+
for (const op of openedOperators) {
|
|
39
|
+
await op.close();
|
|
40
|
+
}
|
|
36
41
|
await mongoFixtures.close();
|
|
37
42
|
}
|
|
38
43
|
}
|
|
@@ -59,6 +64,7 @@ async function callback(callbackParams: Record<string, any>) {
|
|
|
59
64
|
} = callbackParams;
|
|
60
65
|
|
|
61
66
|
const mongoOperator = await getMongoOperator(mongoFixtures);
|
|
67
|
+
openedOperators.push(mongoOperator);
|
|
62
68
|
const expectedResult = await getFixture('expectedResult.json');
|
|
63
69
|
|
|
64
70
|
await doPreFillDb(preFillDb);
|
|
@@ -8,6 +8,7 @@ import {getMongoOperator, handleError, compareToDbEntry, formatQueueItem} from '
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
let mongoFixtures: any;
|
|
11
|
+
const openedOperators: any[] = [];
|
|
11
12
|
const debug = createDebugLogger('@natlibfi/melinda-rest-api-commons/mongo:query-by-id:test');
|
|
12
13
|
|
|
13
14
|
generateTests({
|
|
@@ -34,6 +35,10 @@ generateTests({
|
|
|
34
35
|
},
|
|
35
36
|
after: async () => {
|
|
36
37
|
//debug(`<< After`);
|
|
38
|
+
// close the operators the tests opened (one per test) before the server
|
|
39
|
+
for (const op of openedOperators) {
|
|
40
|
+
await op.close();
|
|
41
|
+
}
|
|
37
42
|
await mongoFixtures.close();
|
|
38
43
|
}
|
|
39
44
|
}
|
|
@@ -63,6 +68,7 @@ async function callback(callbackParams: Record<string, any>) {
|
|
|
63
68
|
} = callbackParams;
|
|
64
69
|
|
|
65
70
|
const mongoOperator = await getMongoOperator(mongoFixtures);
|
|
71
|
+
openedOperators.push(mongoOperator);
|
|
66
72
|
const expectedResult = await getFixture('expectedResult.json');
|
|
67
73
|
|
|
68
74
|
await doPreFillDb(preFillDb);
|