soajs.controller 4.3.17 → 4.3.19
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/ARCHITECTURE.md +1 -0
- package/docs/configuration-custom-registry.md +206 -6
- package/docs/middleware/idempotency.md +31 -2
- package/docs/middleware/mt.md +18 -0
- package/docs/multi-tenant-security.md +1 -0
- package/docs/tenant-access-resolver.md +81 -0
- package/mw/gotoService/redirectToService.js +20 -8
- package/mw/idempotency/index.js +42 -4
- package/mw/idempotency/model/memory.js +12 -2
- package/mw/idempotency/model/mongo.js +10 -3
- package/mw/mt/tenantAccess.js +173 -0
- package/mw/mt/urac.js +61 -12
- package/package.json +1 -1
- package/test/data/provision/customRegistry/gateway.js +32 -0
- package/test/data/provision/customRegistry/urac.js +22 -0
- package/test/data/provision/environment/dev.js +9 -0
- package/test/data/provision/hosts/departments.js +12 -0
- package/test/data/provision/hosts/idempotent.js +12 -0
- package/test/data/provision/marketplace/departments.js +38 -0
- package/test/data/provision/marketplace/idempotent.js +37 -0
- package/test/data/provision/oauth/token/department_member_token.js +72 -0
- package/test/data/provision/oauth/token/department_outsider_token.js +72 -0
- package/test/data/provision/products/dsbrd.js +25 -0
- package/test/data/provision/tenants/dbtn.js +6 -0
- package/test/integration/UC5-tenant-access/index.js +153 -0
- package/test/integration/UC6-idempotency/index.js +197 -0
- package/test/integration/index.js +8 -0
- package/test/unit/index.js +2 -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
- package/test/unit/mw/mt/tenantAccess.js +338 -0
- package/test/unit/mw/mt/urac.js +229 -1
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const assert = require('assert');
|
|
4
|
+
const express = require('express');
|
|
5
|
+
const helper = require("../../helper.js");
|
|
6
|
+
const requester = require('../requester');
|
|
7
|
+
let Controller = helper.requireModule('./server/controller');
|
|
8
|
+
let localConfig = helper.requireModule('./config.js');
|
|
9
|
+
|
|
10
|
+
let mock = require('../UC2-http-methods/service-mock/index.js');
|
|
11
|
+
|
|
12
|
+
//NOTE: the users of these tokens live in tenant HOME, the key belongs to tenant DBTN which names
|
|
13
|
+
// the department resolver in its profile
|
|
14
|
+
const memberToken = "d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d901";
|
|
15
|
+
const outsiderToken = "d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d902";
|
|
16
|
+
const ownerToken = "cfb209a91b23896820f510aadbf1f4284b512123";
|
|
17
|
+
|
|
18
|
+
function startResolver(calls, callback) {
|
|
19
|
+
const sApp = express();
|
|
20
|
+
const mApp = express();
|
|
21
|
+
sApp.use(express.json());
|
|
22
|
+
sApp.post('/tenant/access', (req, res) => {
|
|
23
|
+
calls.push({ "body": req.body, "injectObj": JSON.parse(req.headers.soajsinjectobj) });
|
|
24
|
+
if (req.body.user.username === "dept_member" && req.body.subject === "dept_1") {
|
|
25
|
+
return res.json({
|
|
26
|
+
"result": true,
|
|
27
|
+
"data": {
|
|
28
|
+
"access": true,
|
|
29
|
+
"groups": ["member"],
|
|
30
|
+
"groupsConfig": { "allowedPackages": { "DSBRD": ["DSBRD_OWNER"] } }
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return res.json({ "result": true, "data": { "access": false } });
|
|
35
|
+
});
|
|
36
|
+
mApp.get('/heartbeat', (req, res) => {
|
|
37
|
+
res.json({ "result": true, "ts": Date.now(), "service": { "service": "departments", "type": "rest", "route": "/heartbeat" } });
|
|
38
|
+
});
|
|
39
|
+
let sAppServer = sApp.listen(4013, () => {
|
|
40
|
+
let mAppServer = mApp.listen(5014, () => {
|
|
41
|
+
return callback({ "sAppServer": sAppServer, "mAppServer": mAppServer, "name": "departments" });
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe("Integration for Usecase 5 - tenant access resolver", function () {
|
|
47
|
+
let extKey = "3d90163cf9d6b3076ad26aa5ed58556348069258e5c6c941ee0f18448b570ad1c5c790e2d2a1989680c55f4904e2005ff5f8e71606e4aa641e67882f4210ebbc5460ff305dcb36e6ec2a2299cf0448ef60b9e38f41950ec251c1cf41f05f3ce9";
|
|
48
|
+
|
|
49
|
+
let c = new Controller({
|
|
50
|
+
"serviceName": localConfig.serviceName,
|
|
51
|
+
"serviceVersion": localConfig.serviceVersion,
|
|
52
|
+
"serviceGroup": localConfig.serviceGroup,
|
|
53
|
+
"servicePort": localConfig.servicePort,
|
|
54
|
+
"maintenance": localConfig.maintenance
|
|
55
|
+
});
|
|
56
|
+
let registry = null;
|
|
57
|
+
let log = null;
|
|
58
|
+
let service = null;
|
|
59
|
+
let server = null;
|
|
60
|
+
let serverMaintenance = null;
|
|
61
|
+
let httpmethods = null;
|
|
62
|
+
let resolver = null;
|
|
63
|
+
let calls = [];
|
|
64
|
+
|
|
65
|
+
function call(token, cb) {
|
|
66
|
+
let options = {
|
|
67
|
+
uri: 'http://127.0.0.1:' + localConfig.servicePort + '/httpmethods/myroute',
|
|
68
|
+
headers: {
|
|
69
|
+
'Content-Type': 'application/json',
|
|
70
|
+
key: extKey
|
|
71
|
+
},
|
|
72
|
+
"qs": {
|
|
73
|
+
access_token: token
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
requester('get', options, cb);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
before((done) => {
|
|
80
|
+
startResolver(calls, (servers) => {
|
|
81
|
+
resolver = servers;
|
|
82
|
+
mock.startServer(null, function (servers) {
|
|
83
|
+
httpmethods = servers;
|
|
84
|
+
});
|
|
85
|
+
c.init((_registry, _log, _service, _server, _serverMaintenance) => {
|
|
86
|
+
registry = _registry;
|
|
87
|
+
log = _log;
|
|
88
|
+
service = _service;
|
|
89
|
+
server = _server;
|
|
90
|
+
serverMaintenance = _serverMaintenance;
|
|
91
|
+
c.start(registry, log, service, server, serverMaintenance, () => {
|
|
92
|
+
setTimeout(function () {
|
|
93
|
+
done();
|
|
94
|
+
}, 5000);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
beforeEach(() => {
|
|
100
|
+
calls.length = 0;
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("department member switching to the tenant gets the resolver ACL", function (done) {
|
|
104
|
+
call(memberToken, (error, body) => {
|
|
105
|
+
assert.ifError(error);
|
|
106
|
+
assert.ok(body);
|
|
107
|
+
assert.strictEqual(body.result, true, JSON.stringify(body.errors));
|
|
108
|
+
assert.deepStrictEqual(body.data.method, 'GET');
|
|
109
|
+
assert.strictEqual(calls.length, 1);
|
|
110
|
+
assert.deepStrictEqual(calls[0].body, {
|
|
111
|
+
"user": {
|
|
112
|
+
"id": "5c8d0c505653de3985aa1001",
|
|
113
|
+
"username": "dept_member",
|
|
114
|
+
"tenant": { "id": "5c0e74ba9acc3c5a84a51260", "code": "HOME" }
|
|
115
|
+
},
|
|
116
|
+
"tenant": { "id": "5c0e74ba9acc3c5a84a51259", "code": "DBTN" },
|
|
117
|
+
"subject": "dept_1"
|
|
118
|
+
});
|
|
119
|
+
assert.strictEqual(calls[0].injectObj.tenant.id, "5c0e74ba9acc3c5a84a51259");
|
|
120
|
+
assert.strictEqual(calls[0].injectObj.application.product, "DSBRD");
|
|
121
|
+
done();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("user outside the department is refused after the urac fallback", function (done) {
|
|
126
|
+
call(outsiderToken, (error, body) => {
|
|
127
|
+
assert.ok(body);
|
|
128
|
+
assert.strictEqual(body.result, false);
|
|
129
|
+
assert.deepStrictEqual(body.errors.codes, [146]);
|
|
130
|
+
assert.strictEqual(calls.length, 1);
|
|
131
|
+
done();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("user of the tenant itself does not call the resolver", function (done) {
|
|
136
|
+
call(ownerToken, (error, body) => {
|
|
137
|
+
assert.ifError(error);
|
|
138
|
+
assert.ok(body);
|
|
139
|
+
assert.strictEqual(body.result, true, JSON.stringify(body.errors));
|
|
140
|
+
assert.strictEqual(calls.length, 0);
|
|
141
|
+
done();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
after((done) => {
|
|
146
|
+
mock.killServer(httpmethods);
|
|
147
|
+
resolver.sAppServer.close();
|
|
148
|
+
resolver.mAppServer.close();
|
|
149
|
+
c.stop(registry, log, service, server, serverMaintenance, () => {
|
|
150
|
+
done();
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
});
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const assert = require('assert');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const express = require('express');
|
|
6
|
+
const request = require('request');
|
|
7
|
+
const helper = require("../../helper.js");
|
|
8
|
+
let Controller = helper.requireModule('./server/controller');
|
|
9
|
+
let localConfig = helper.requireModule('./config.js');
|
|
10
|
+
|
|
11
|
+
//NOTE: the gateway custom registry turns idempotency on with the mongo model for every route of the
|
|
12
|
+
// idempotent service, and throttles /throttled to one call per minute
|
|
13
|
+
const ownerToken = "cfb209a91b23896820f510aadbf1f4284b512123";
|
|
14
|
+
|
|
15
|
+
function startService(calls, callback) {
|
|
16
|
+
const sApp = express();
|
|
17
|
+
const mApp = express();
|
|
18
|
+
let failed = false;
|
|
19
|
+
let count = (route) => {
|
|
20
|
+
calls[route] = (calls[route] || 0) + 1;
|
|
21
|
+
};
|
|
22
|
+
sApp.post('/ok', (req, res) => {
|
|
23
|
+
count("ok");
|
|
24
|
+
res.json({ "result": true, "data": { "call": calls.ok } });
|
|
25
|
+
});
|
|
26
|
+
sApp.post('/fail', (req, res) => {
|
|
27
|
+
count("fail");
|
|
28
|
+
if (!failed) {
|
|
29
|
+
failed = true;
|
|
30
|
+
return res.status(503).json({ "result": false, "errors": { "codes": [503], "details": [{ "code": 503, "message": "unavailable" }] } });
|
|
31
|
+
}
|
|
32
|
+
res.json({ "result": true, "data": { "call": calls.fail } });
|
|
33
|
+
});
|
|
34
|
+
sApp.post('/bad', (req, res) => {
|
|
35
|
+
count("bad");
|
|
36
|
+
res.status(400).json({ "result": false, "errors": { "codes": [400], "details": [{ "code": 400, "message": "bad input" }] } });
|
|
37
|
+
});
|
|
38
|
+
sApp.post('/slow', (req, res) => {
|
|
39
|
+
count("slow");
|
|
40
|
+
setTimeout(() => {
|
|
41
|
+
res.json({ "result": true, "data": { "call": calls.slow } });
|
|
42
|
+
}, 1500);
|
|
43
|
+
});
|
|
44
|
+
sApp.post('/throttled', (req, res) => {
|
|
45
|
+
count("throttled");
|
|
46
|
+
res.json({ "result": true, "data": { "call": calls.throttled } });
|
|
47
|
+
});
|
|
48
|
+
mApp.get('/heartbeat', (req, res) => {
|
|
49
|
+
res.json({ "result": true, "ts": Date.now(), "service": { "service": "idempotent", "type": "rest", "route": "/heartbeat" } });
|
|
50
|
+
});
|
|
51
|
+
let sAppServer = sApp.listen(4014, () => {
|
|
52
|
+
let mAppServer = mApp.listen(5015, () => {
|
|
53
|
+
return callback({ "sAppServer": sAppServer, "mAppServer": mAppServer });
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe("Integration for Usecase 6 - idempotency", function () {
|
|
59
|
+
let extKey = "3d90163cf9d6b3076ad26aa5ed58556348069258e5c6c941ee0f18448b570ad1c5c790e2d2a1989680c55f4904e2005ff5f8e71606e4aa641e67882f4210ebbc5460ff305dcb36e6ec2a2299cf0448ef60b9e38f41950ec251c1cf41f05f3ce9";
|
|
60
|
+
|
|
61
|
+
let c = new Controller({
|
|
62
|
+
"serviceName": localConfig.serviceName,
|
|
63
|
+
"serviceVersion": localConfig.serviceVersion,
|
|
64
|
+
"serviceGroup": localConfig.serviceGroup,
|
|
65
|
+
"servicePort": localConfig.servicePort,
|
|
66
|
+
"maintenance": localConfig.maintenance
|
|
67
|
+
});
|
|
68
|
+
let registry = null;
|
|
69
|
+
let log = null;
|
|
70
|
+
let service = null;
|
|
71
|
+
let server = null;
|
|
72
|
+
let serverMaintenance = null;
|
|
73
|
+
let mockServers = null;
|
|
74
|
+
let calls = {};
|
|
75
|
+
|
|
76
|
+
function call(route, key, cb) {
|
|
77
|
+
request.post({
|
|
78
|
+
"uri": 'http://127.0.0.1:' + localConfig.servicePort + '/idempotent' + route,
|
|
79
|
+
"json": true,
|
|
80
|
+
"headers": {
|
|
81
|
+
"key": extKey,
|
|
82
|
+
"idempotency-key": key,
|
|
83
|
+
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.1 Safari/537.36"
|
|
84
|
+
},
|
|
85
|
+
"qs": { "access_token": ownerToken }
|
|
86
|
+
}, (error, response, body) => {
|
|
87
|
+
assert.ifError(error);
|
|
88
|
+
return cb(response.statusCode, body);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
before((done) => {
|
|
93
|
+
//NOTE: the unit tests run in the same process and connect the idempotency mongo model to a dummy
|
|
94
|
+
// database, the model connects only once, drop it so the gateway connects it to the real one
|
|
95
|
+
helper.requireModule('./mw/idempotency/model/mongo').mongo = null;
|
|
96
|
+
startService(calls, (servers) => {
|
|
97
|
+
mockServers = servers;
|
|
98
|
+
c.init((_registry, _log, _service, _server, _serverMaintenance) => {
|
|
99
|
+
registry = _registry;
|
|
100
|
+
log = _log;
|
|
101
|
+
service = _service;
|
|
102
|
+
server = _server;
|
|
103
|
+
serverMaintenance = _serverMaintenance;
|
|
104
|
+
c.start(registry, log, service, server, serverMaintenance, () => {
|
|
105
|
+
setTimeout(function () {
|
|
106
|
+
done();
|
|
107
|
+
}, 5000);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("a service answer is completed and replayed", function (done) {
|
|
114
|
+
let key = crypto.randomUUID();
|
|
115
|
+
call("/ok", key, (status, body) => {
|
|
116
|
+
assert.strictEqual(status, 200, JSON.stringify(body));
|
|
117
|
+
assert.strictEqual(calls.ok, 1);
|
|
118
|
+
call("/ok", key, (status, body) => {
|
|
119
|
+
assert.strictEqual(status, 200);
|
|
120
|
+
assert.strictEqual(body.data.call, 1);
|
|
121
|
+
assert.strictEqual(calls.ok, 1);
|
|
122
|
+
done();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("a 4xx of the service is completed and replayed", function (done) {
|
|
128
|
+
let key = crypto.randomUUID();
|
|
129
|
+
call("/bad", key, (status) => {
|
|
130
|
+
assert.strictEqual(status, 400);
|
|
131
|
+
call("/bad", key, (status) => {
|
|
132
|
+
assert.strictEqual(status, 400);
|
|
133
|
+
assert.strictEqual(calls.bad, 1);
|
|
134
|
+
done();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("a 5xx of the service releases the key, the retry reaches the service", function (done) {
|
|
140
|
+
let key = crypto.randomUUID();
|
|
141
|
+
call("/fail", key, (status) => {
|
|
142
|
+
assert.strictEqual(status, 503);
|
|
143
|
+
call("/fail", key, (status, body) => {
|
|
144
|
+
assert.strictEqual(status, 200, JSON.stringify(body));
|
|
145
|
+
assert.strictEqual(calls.fail, 2);
|
|
146
|
+
call("/fail", key, (status, body) => {
|
|
147
|
+
assert.strictEqual(status, 200);
|
|
148
|
+
assert.strictEqual(body.data.call, 2);
|
|
149
|
+
assert.strictEqual(calls.fail, 2);
|
|
150
|
+
done();
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("a duplicate while the first is running gets 409/181", function (done) {
|
|
157
|
+
let key = crypto.randomUUID();
|
|
158
|
+
let first = null;
|
|
159
|
+
call("/slow", key, (status) => {
|
|
160
|
+
first = status;
|
|
161
|
+
});
|
|
162
|
+
setTimeout(() => {
|
|
163
|
+
call("/slow", key, (status, body) => {
|
|
164
|
+
assert.strictEqual(status, 409);
|
|
165
|
+
assert.deepStrictEqual(body.errors.codes, [181]);
|
|
166
|
+
setTimeout(() => {
|
|
167
|
+
assert.strictEqual(first, 200);
|
|
168
|
+
assert.strictEqual(calls.slow, 1);
|
|
169
|
+
done();
|
|
170
|
+
}, 2000);
|
|
171
|
+
});
|
|
172
|
+
}, 300);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("a throttled request releases the key, the retry is not refused with 409", function (done) {
|
|
176
|
+
call("/throttled", crypto.randomUUID(), (status, body) => {
|
|
177
|
+
assert.strictEqual(status, 200, JSON.stringify(body));
|
|
178
|
+
let key = crypto.randomUUID();
|
|
179
|
+
call("/throttled", key, (status) => {
|
|
180
|
+
assert.strictEqual(status, 429);
|
|
181
|
+
call("/throttled", key, (status, body) => {
|
|
182
|
+
assert.strictEqual(status, 429, JSON.stringify(body));
|
|
183
|
+
assert.strictEqual(calls.throttled, 1);
|
|
184
|
+
done();
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
after((done) => {
|
|
191
|
+
mockServers.sAppServer.close();
|
|
192
|
+
mockServers.mAppServer.close();
|
|
193
|
+
c.stop(registry, log, service, server, serverMaintenance, () => {
|
|
194
|
+
done();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
});
|
|
@@ -36,6 +36,14 @@ describe("starting integration tests", () => {
|
|
|
36
36
|
// to test roaming
|
|
37
37
|
require("./UC3-roaming/index.js");
|
|
38
38
|
|
|
39
|
+
|
|
40
|
+
// to test the tenant access resolver
|
|
41
|
+
require("./UC5-tenant-access/index.js");
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
// to test idempotency with the mongo model
|
|
45
|
+
require("./UC6-idempotency/index.js");
|
|
46
|
+
|
|
39
47
|
done();
|
|
40
48
|
});
|
|
41
49
|
});
|
package/test/unit/index.js
CHANGED
|
@@ -29,6 +29,7 @@ describe("Starting Gateway Unit test", () => {
|
|
|
29
29
|
|
|
30
30
|
require("./mw/mt/index.js");
|
|
31
31
|
require("./mw/mt/urac.js");
|
|
32
|
+
require("./mw/mt/tenantAccess.js");
|
|
32
33
|
require("./mw/mt/utils.js");
|
|
33
34
|
require("./mw/mt/lib.js");
|
|
34
35
|
|
|
@@ -38,6 +39,7 @@ describe("Starting Gateway Unit test", () => {
|
|
|
38
39
|
require("./mw/traffic/index.js");
|
|
39
40
|
require("./mw/idempotency/index.js");
|
|
40
41
|
require("./mw/idempotency/memory.js");
|
|
42
|
+
require("./mw/idempotency/mongo.js");
|
|
41
43
|
require("./mw/cache/index.js");
|
|
42
44
|
require("./mw/cache/memory.js");
|
|
43
45
|
require("./mw/lastSeen/index.js");
|
|
@@ -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
|
+
});
|