soajs.controller 4.3.18 → 4.3.20
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/.github/workflows/ci.yml +4 -1
- package/.github/workflows/release.yml +4 -1
- package/docs/maintenance-routes.md +31 -0
- package/docs/middleware/cache.md +74 -1
- package/docs/middleware/idempotency.md +31 -2
- package/mw/cache/index.js +24 -5
- package/mw/cache/invalidate.js +71 -0
- package/mw/cache/model/mongo.js +95 -1
- package/mw/gotoService/redirectToService.js +21 -9
- package/mw/idempotency/index.js +42 -4
- package/mw/idempotency/model/memory.js +12 -2
- package/mw/idempotency/model/mongo.js +10 -3
- package/package.json +1 -1
- package/server/maintenance.js +41 -0
- package/test/data/provision/customRegistry/gateway.js +47 -0
- package/test/data/provision/environment/dev.js +9 -0
- package/test/data/provision/hosts/idempotent.js +12 -0
- package/test/data/provision/hosts/spaces.js +12 -0
- package/test/data/provision/marketplace/idempotent.js +37 -0
- package/test/data/provision/marketplace/spaces.js +36 -0
- package/test/data/provision/oauth/token/second_owner_token.js +72 -0
- package/test/data/provision/products/dsbrd.js +49 -0
- package/test/data/provision/urac/users/second_owner.js +61 -0
- package/test/integration/UC6-idempotency/index.js +197 -0
- package/test/integration/UC7-cache-invalidate/index.js +209 -0
- package/test/integration/index.js +8 -0
- package/test/unit/index.js +3 -0
- package/test/unit/mw/cache/index.js +122 -0
- package/test/unit/mw/cache/invalidate.js +99 -0
- package/test/unit/mw/cache/mongo.js +104 -0
- package/test/unit/mw/idempotency/index.js +122 -0
- package/test/unit/mw/idempotency/memory.js +36 -4
- package/test/unit/mw/idempotency/mongo.js +62 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const helper = require("../../../helper.js");
|
|
4
|
+
const model = helper.requireModule('./mw/cache/model/mongo');
|
|
5
|
+
const assert = require('assert');
|
|
6
|
+
|
|
7
|
+
describe("Unit test for: mw - cache/model/mongo invalidation", () => {
|
|
8
|
+
|
|
9
|
+
let calls = null;
|
|
10
|
+
let invalidation = null;
|
|
11
|
+
let deleted = 0;
|
|
12
|
+
let originalMongo = null;
|
|
13
|
+
let originalLog = null;
|
|
14
|
+
|
|
15
|
+
before(() => {
|
|
16
|
+
originalMongo = model.mongo;
|
|
17
|
+
originalLog = model.log;
|
|
18
|
+
model.log = {
|
|
19
|
+
debug: () => {},
|
|
20
|
+
info: () => {},
|
|
21
|
+
error: () => {}
|
|
22
|
+
};
|
|
23
|
+
model.mongo = {
|
|
24
|
+
findOne: async (col, condition) => {
|
|
25
|
+
calls.push({ "op": "findOne", col, condition });
|
|
26
|
+
return invalidation;
|
|
27
|
+
},
|
|
28
|
+
updateOne: async (col, condition, s, options) => {
|
|
29
|
+
calls.push({ "op": "updateOne", col, condition, s, options });
|
|
30
|
+
},
|
|
31
|
+
deleteMany: async (col, condition) => {
|
|
32
|
+
calls.push({ "op": "deleteMany", col, condition });
|
|
33
|
+
return { deletedCount: deleted };
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
after(() => {
|
|
38
|
+
model.mongo = originalMongo;
|
|
39
|
+
model.log = originalLog;
|
|
40
|
+
});
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
calls = [];
|
|
43
|
+
invalidation = null;
|
|
44
|
+
deleted = 0;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
let key = { l1: 'tenant1', l2: 'spaces:GET:/spaces::u:user-1' };
|
|
48
|
+
let response = { statusCode: 200, headers: {}, body: '[]' };
|
|
49
|
+
|
|
50
|
+
it("set stores the fields an invalidation matches on", async () => {
|
|
51
|
+
let stored = await model.set(key, response, 60000, { service: 'spaces', api: 'GET /spaces', user: 'user-1', startedAt: Date.now() });
|
|
52
|
+
assert.strictEqual(stored, true);
|
|
53
|
+
let update = calls.find((c) => c.op === "updateOne");
|
|
54
|
+
assert.strictEqual(update.col, 'cache_store');
|
|
55
|
+
assert.strictEqual(update.s.$set.service, 'spaces');
|
|
56
|
+
assert.strictEqual(update.s.$set.api, 'GET /spaces');
|
|
57
|
+
assert.strictEqual(update.s.$set.user, 'user-1');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("set looks for an invalidation after the request started, for that api or all, that user or all", async () => {
|
|
61
|
+
let startedAt = Date.now();
|
|
62
|
+
await model.set(key, response, 60000, { service: 'spaces', api: 'GET /spaces', user: 'user-1', startedAt: startedAt });
|
|
63
|
+
let lookup = calls.find((c) => c.op === "findOne");
|
|
64
|
+
assert.strictEqual(lookup.col, 'cache_invalidation');
|
|
65
|
+
assert.deepStrictEqual(lookup.condition, {
|
|
66
|
+
l1: 'tenant1',
|
|
67
|
+
service: 'spaces',
|
|
68
|
+
api: { $in: ['GET /spaces', null] },
|
|
69
|
+
user: { $in: ['user-1', null] },
|
|
70
|
+
clearedAt: { $gt: new Date(startedAt) }
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("set does not store a response read before an invalidation", async () => {
|
|
75
|
+
invalidation = { clearedAt: new Date() };
|
|
76
|
+
let stored = await model.set(key, response, 60000, { service: 'spaces', api: 'GET /spaces', user: 'user-1', startedAt: Date.now() - 1000 });
|
|
77
|
+
assert.strictEqual(stored, false);
|
|
78
|
+
assert.strictEqual(calls.filter((c) => c.op === "updateOne").length, 0);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("set without meta stores as before", async () => {
|
|
82
|
+
let stored = await model.set(key, response, 60000);
|
|
83
|
+
assert.strictEqual(stored, true);
|
|
84
|
+
assert.strictEqual(calls.filter((c) => c.op === "findOne").length, 0);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("invalidateMatching records the invalidation before removing", async () => {
|
|
88
|
+
deleted = 3;
|
|
89
|
+
let removed = await model.invalidateMatching({ tenant: 'tenant1', service: 'spaces', api: 'GET /spaces', user: 'user-1' });
|
|
90
|
+
assert.strictEqual(removed, 3);
|
|
91
|
+
assert.deepStrictEqual(calls.map((c) => c.op), ['updateOne', 'deleteMany']);
|
|
92
|
+
assert.strictEqual(calls[0].col, 'cache_invalidation');
|
|
93
|
+
assert.deepStrictEqual(calls[0].condition, { l1: 'tenant1', service: 'spaces', api: 'GET /spaces', user: 'user-1' });
|
|
94
|
+
assert.ok(calls[0].s.$set.clearedAt instanceof Date);
|
|
95
|
+
assert.ok(calls[0].s.$set.expiresAt > calls[0].s.$set.clearedAt);
|
|
96
|
+
assert.deepStrictEqual(calls[1].condition, { l1: 'tenant1', service: 'spaces', api: 'GET /spaces', user: 'user-1' });
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("invalidateMatching without api and user covers the whole service", async () => {
|
|
100
|
+
await model.invalidateMatching({ tenant: 'tenant1', service: 'spaces' });
|
|
101
|
+
assert.deepStrictEqual(calls[0].condition, { l1: 'tenant1', service: 'spaces', api: null, user: null });
|
|
102
|
+
assert.deepStrictEqual(calls[1].condition, { l1: 'tenant1', service: 'spaces' });
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -590,4 +590,126 @@ describe("Unit test for: mw - idempotency", () => {
|
|
|
590
590
|
});
|
|
591
591
|
});
|
|
592
592
|
|
|
593
|
+
describe("release of the key when the request ends before the service", () => {
|
|
594
|
+
const EventEmitter = require('events');
|
|
595
|
+
|
|
596
|
+
function buildReq(idempotencyKey, answers) {
|
|
597
|
+
return {
|
|
598
|
+
headers: { 'idempotency-key': idempotencyKey },
|
|
599
|
+
method: 'POST',
|
|
600
|
+
soajs: {
|
|
601
|
+
tenant: { id: 'tenant-release' },
|
|
602
|
+
controller: { serviceParams: { name: 'test', serviceInfo: ['', 'test', 'test'] } },
|
|
603
|
+
registry: {
|
|
604
|
+
custom: {
|
|
605
|
+
gateway: {
|
|
606
|
+
value: {
|
|
607
|
+
idempotency: {
|
|
608
|
+
model: 'memory',
|
|
609
|
+
services: {
|
|
610
|
+
test: { enabled: true, ttl: 60000, apis: ['POST /test'] }
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
},
|
|
617
|
+
controllerResponse: (data) => {
|
|
618
|
+
answers.push(data);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function buildRes() {
|
|
625
|
+
let res = new EventEmitter();
|
|
626
|
+
res.replayed = null;
|
|
627
|
+
res.writeHead = (statusCode) => {
|
|
628
|
+
res.replayed = statusCode;
|
|
629
|
+
return res;
|
|
630
|
+
};
|
|
631
|
+
res.end = () => {
|
|
632
|
+
};
|
|
633
|
+
return res;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Runs the middleware and resolves with "next" or the answer it gave
|
|
637
|
+
function run(functionMw, idempotencyKey) {
|
|
638
|
+
return new Promise((resolve) => {
|
|
639
|
+
let answers = [];
|
|
640
|
+
let req = buildReq(idempotencyKey, answers);
|
|
641
|
+
let res = buildRes();
|
|
642
|
+
functionMw(req, res, () => {
|
|
643
|
+
resolve({ "next": true, req, res, answers });
|
|
644
|
+
});
|
|
645
|
+
setTimeout(() => {
|
|
646
|
+
resolve({ "next": false, req, res, answers });
|
|
647
|
+
}, 20);
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
let functionMw = null;
|
|
652
|
+
before(() => {
|
|
653
|
+
functionMw = mw(mockConfig);
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
it("an answer before the service, a 429 for example, releases the key", async () => {
|
|
657
|
+
let key = '550e8400-e29b-41d4-a716-446655440101';
|
|
658
|
+
let first = await run(functionMw, key);
|
|
659
|
+
assert.strictEqual(first.next, true);
|
|
660
|
+
first.res.emit('finish');
|
|
661
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
662
|
+
let retry = await run(functionMw, key);
|
|
663
|
+
assert.strictEqual(retry.next, true, JSON.stringify(retry.answers));
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
it("a client disconnect before the service releases the key", async () => {
|
|
667
|
+
let key = '550e8400-e29b-41d4-a716-446655440102';
|
|
668
|
+
let first = await run(functionMw, key);
|
|
669
|
+
assert.strictEqual(first.next, true);
|
|
670
|
+
first.res.emit('close');
|
|
671
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
672
|
+
let retry = await run(functionMw, key);
|
|
673
|
+
assert.strictEqual(retry.next, true, JSON.stringify(retry.answers));
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
it("a proxied request is left to gotoService, a duplicate still gets 409/181", async () => {
|
|
677
|
+
let key = '550e8400-e29b-41d4-a716-446655440103';
|
|
678
|
+
let first = await run(functionMw, key);
|
|
679
|
+
assert.strictEqual(first.next, true);
|
|
680
|
+
first.req.soajs.idempotencyContext.proxied = true;
|
|
681
|
+
first.res.emit('finish');
|
|
682
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
683
|
+
let duplicate = await run(functionMw, key);
|
|
684
|
+
assert.strictEqual(duplicate.next, false);
|
|
685
|
+
assert.strictEqual(duplicate.answers[0].status, 409);
|
|
686
|
+
assert.strictEqual(duplicate.answers[0].code, 181);
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
it("a completed request is replayed, finish does not remove it", async () => {
|
|
690
|
+
let key = '550e8400-e29b-41d4-a716-446655440104';
|
|
691
|
+
let first = await run(functionMw, key);
|
|
692
|
+
let context = first.req.soajs.idempotencyContext;
|
|
693
|
+
context.proxied = true;
|
|
694
|
+
context.model.complete(context.key, { statusCode: 201, headers: {}, body: '{}' }, context.ttl);
|
|
695
|
+
first.res.emit('finish');
|
|
696
|
+
first.res.emit('close');
|
|
697
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
698
|
+
let retry = await run(functionMw, key);
|
|
699
|
+
assert.strictEqual(retry.next, false);
|
|
700
|
+
assert.strictEqual(retry.res.replayed, 201);
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
it("the release of an expired lock does not remove the lock of a later retry", async () => {
|
|
704
|
+
let key = '550e8400-e29b-41d4-a716-446655440105';
|
|
705
|
+
let first = await run(functionMw, key);
|
|
706
|
+
let context = first.req.soajs.idempotencyContext;
|
|
707
|
+
// the ttl of the first lock ran out and a retry locked the key again
|
|
708
|
+
context.model.get(context.key).expiresAt = Date.now() - 1;
|
|
709
|
+
assert.strictEqual(context.model.lock(context.key, 60000, 'lock-of-the-retry'), true);
|
|
710
|
+
first.res.emit('finish');
|
|
711
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
712
|
+
assert.strictEqual(context.model.get(context.key).lockId, 'lock-of-the-retry');
|
|
713
|
+
});
|
|
714
|
+
});
|
|
593
715
|
});
|
|
@@ -16,10 +16,12 @@ describe("Unit test for: mw - idempotency/model/memory", () => {
|
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
afterEach((done) => {
|
|
19
|
-
// Clean up between tests
|
|
20
|
-
model.
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
// Clean up between tests, unlock leaves completed entries in place so reset the store
|
|
20
|
+
model.init({
|
|
21
|
+
debug: () => {},
|
|
22
|
+
info: () => {},
|
|
23
|
+
error: () => {}
|
|
24
|
+
});
|
|
23
25
|
done();
|
|
24
26
|
});
|
|
25
27
|
|
|
@@ -133,6 +135,36 @@ describe("Unit test for: mw - idempotency/model/memory", () => {
|
|
|
133
135
|
});
|
|
134
136
|
});
|
|
135
137
|
|
|
138
|
+
describe("unlock only an own lock in flight", () => {
|
|
139
|
+
it("should keep a completed entry", (done) => {
|
|
140
|
+
let key = { l1: 'tenant1', l2: 'key1' };
|
|
141
|
+
model.lock(key, 60000, 'lock-a');
|
|
142
|
+
model.complete(key, { statusCode: 200 }, 60000);
|
|
143
|
+
model.unlock(key, 'lock-a');
|
|
144
|
+
let result = model.get(key);
|
|
145
|
+
assert.strictEqual(result.status, 'completed');
|
|
146
|
+
done();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("should keep the lock of another request", (done) => {
|
|
150
|
+
let key = { l1: 'tenant1', l2: 'key1' };
|
|
151
|
+
model.lock(key, 60000, 'lock-b');
|
|
152
|
+
model.unlock(key, 'lock-a');
|
|
153
|
+
let result = model.get(key);
|
|
154
|
+
assert.strictEqual(result.status, 'in_flight');
|
|
155
|
+
assert.strictEqual(result.lockId, 'lock-b');
|
|
156
|
+
done();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("should remove its own lock", (done) => {
|
|
160
|
+
let key = { l1: 'tenant1', l2: 'key1' };
|
|
161
|
+
model.lock(key, 60000, 'lock-a');
|
|
162
|
+
model.unlock(key, 'lock-a');
|
|
163
|
+
assert.strictEqual(model.get(key), null);
|
|
164
|
+
done();
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
136
168
|
describe("cleanupExpiredEntries", () => {
|
|
137
169
|
it("should remove expired entries", (done) => {
|
|
138
170
|
let key1 = { l1: 'tenant1', l2: 'key1' };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const helper = require("../../../helper.js");
|
|
4
|
+
const model = helper.requireModule('./mw/idempotency/model/mongo');
|
|
5
|
+
const assert = require('assert');
|
|
6
|
+
|
|
7
|
+
describe("Unit test for: mw - idempotency/model/mongo", () => {
|
|
8
|
+
|
|
9
|
+
let calls = null;
|
|
10
|
+
let originalMongo = null;
|
|
11
|
+
|
|
12
|
+
before(() => {
|
|
13
|
+
originalMongo = model.mongo;
|
|
14
|
+
model.log = {
|
|
15
|
+
debug: () => {},
|
|
16
|
+
info: () => {},
|
|
17
|
+
error: () => {}
|
|
18
|
+
};
|
|
19
|
+
model.mongo = {
|
|
20
|
+
findOne: async (col, condition) => {
|
|
21
|
+
calls.push({ "op": "findOne", "condition": condition });
|
|
22
|
+
return null;
|
|
23
|
+
},
|
|
24
|
+
updateOne: async (col, condition, s, options) => {
|
|
25
|
+
calls.push({ "op": "updateOne", "condition": condition, "s": s, "options": options });
|
|
26
|
+
},
|
|
27
|
+
deleteOne: async (col, condition) => {
|
|
28
|
+
calls.push({ "op": "deleteOne", "condition": condition });
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
after(() => {
|
|
33
|
+
model.mongo = originalMongo;
|
|
34
|
+
});
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
calls = [];
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("lock stores the lockId", async () => {
|
|
40
|
+
let locked = await model.lock({ l1: 'tenant1', l2: 'key1' }, 60000, 'lock-a');
|
|
41
|
+
assert.strictEqual(locked, true);
|
|
42
|
+
let update = calls.find((c) => c.op === "updateOne");
|
|
43
|
+
assert.strictEqual(update.s.$set.status, 'in_flight');
|
|
44
|
+
assert.strictEqual(update.s.$set.lockId, 'lock-a');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("unlock removes only an own record in flight", async () => {
|
|
48
|
+
await model.unlock({ l1: 'tenant1', l2: 'key1' }, 'lock-a');
|
|
49
|
+
assert.deepStrictEqual(calls, [{
|
|
50
|
+
"op": "deleteOne",
|
|
51
|
+
"condition": { l1: 'tenant1', l2: 'key1', status: 'in_flight', lockId: 'lock-a' }
|
|
52
|
+
}]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("unlock without a lockId still never removes a completed record", async () => {
|
|
56
|
+
await model.unlock({ l1: 'tenant1', l2: 'key1' });
|
|
57
|
+
assert.deepStrictEqual(calls, [{
|
|
58
|
+
"op": "deleteOne",
|
|
59
|
+
"condition": { l1: 'tenant1', l2: 'key1', status: 'in_flight' }
|
|
60
|
+
}]);
|
|
61
|
+
});
|
|
62
|
+
});
|