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,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
|
+
});
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const assert = require('assert');
|
|
4
|
+
const express = require('express');
|
|
5
|
+
const request = require('request');
|
|
6
|
+
const helper = require("../../helper.js");
|
|
7
|
+
let Controller = helper.requireModule('./server/controller');
|
|
8
|
+
let localConfig = helper.requireModule('./config.js');
|
|
9
|
+
|
|
10
|
+
//NOTE: the gateway custom registry caches the spaces service with the mongo model, per user except
|
|
11
|
+
// /catalog which is per tenant. Both tokens are users of the console tenant.
|
|
12
|
+
const tenantId = "5c0e74ba9acc3c5a84a51259";
|
|
13
|
+
const ownerToken = "cfb209a91b23896820f510aadbf1f4284b512123";
|
|
14
|
+
const ownerId = "5c8d0c505653de3985aa0ffd";
|
|
15
|
+
const secondToken = "c4c4e5e5f6f6a7a7b8b8c9c9d0d0e1e1f2f2a301";
|
|
16
|
+
const secondId = "5c8d0c505653de3985aa2001";
|
|
17
|
+
const maintenanceUri = 'http://127.0.0.1:5001/cache/invalidate';
|
|
18
|
+
|
|
19
|
+
function startService(calls, callback) {
|
|
20
|
+
const sApp = express();
|
|
21
|
+
const mApp = express();
|
|
22
|
+
let answer = (name) => (req, res) => {
|
|
23
|
+
calls[name] = (calls[name] || 0) + 1;
|
|
24
|
+
res.json({ "result": true, "data": { "call": calls[name], "url": req.url } });
|
|
25
|
+
};
|
|
26
|
+
sApp.get('/spaces', answer("spaces"));
|
|
27
|
+
sApp.get('/space/:id', answer("space"));
|
|
28
|
+
sApp.get('/catalog', answer("catalog"));
|
|
29
|
+
sApp.get('/slow', (req, res) => {
|
|
30
|
+
calls.slow = (calls.slow || 0) + 1;
|
|
31
|
+
let call = calls.slow;
|
|
32
|
+
setTimeout(() => {
|
|
33
|
+
res.json({ "result": true, "data": { "call": call } });
|
|
34
|
+
}, 1000);
|
|
35
|
+
});
|
|
36
|
+
mApp.get('/heartbeat', (req, res) => {
|
|
37
|
+
res.json({ "result": true, "ts": Date.now(), "service": { "service": "spaces", "type": "rest", "route": "/heartbeat" } });
|
|
38
|
+
});
|
|
39
|
+
let sAppServer = sApp.listen(4015, () => {
|
|
40
|
+
let mAppServer = mApp.listen(5016, () => {
|
|
41
|
+
return callback({ "sAppServer": sAppServer, "mAppServer": mAppServer });
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe("Integration for Usecase 7 - cache invalidation", 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 mockServers = null;
|
|
62
|
+
let calls = {};
|
|
63
|
+
|
|
64
|
+
// GET through the gateway, resolves with the X-Cache header and the body
|
|
65
|
+
function get(path, token) {
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
request.get({
|
|
68
|
+
"uri": 'http://127.0.0.1:' + localConfig.servicePort + '/spaces' + path,
|
|
69
|
+
"json": true,
|
|
70
|
+
"headers": {
|
|
71
|
+
"key": extKey,
|
|
72
|
+
"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"
|
|
73
|
+
},
|
|
74
|
+
"qs": { "access_token": token }
|
|
75
|
+
}, (error, response, body) => {
|
|
76
|
+
assert.ifError(error);
|
|
77
|
+
resolve({ "cache": response.headers['x-cache'], "status": response.statusCode, body });
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function invalidate(body, method) {
|
|
83
|
+
return new Promise((resolve) => {
|
|
84
|
+
request({
|
|
85
|
+
"method": method || 'POST',
|
|
86
|
+
"uri": maintenanceUri,
|
|
87
|
+
"json": body === undefined ? true : body
|
|
88
|
+
}, (error, response, responseBody) => {
|
|
89
|
+
assert.ifError(error);
|
|
90
|
+
resolve({ "status": response.statusCode, "body": responseBody });
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// the response is stored once the proxy finishes, after the client got it
|
|
96
|
+
const settle = () => new Promise((r) => setTimeout(r, 150));
|
|
97
|
+
|
|
98
|
+
before((done) => {
|
|
99
|
+
//NOTE: the unit tests run in the same process and connect the cache mongo model to a dummy
|
|
100
|
+
// database, the model connects only once, drop it so the gateway connects it to the real one
|
|
101
|
+
helper.requireModule('./mw/cache/model/mongo').mongo = null;
|
|
102
|
+
startService(calls, (servers) => {
|
|
103
|
+
mockServers = servers;
|
|
104
|
+
c.init((_registry, _log, _service, _server, _serverMaintenance) => {
|
|
105
|
+
registry = _registry;
|
|
106
|
+
log = _log;
|
|
107
|
+
service = _service;
|
|
108
|
+
server = _server;
|
|
109
|
+
serverMaintenance = _serverMaintenance;
|
|
110
|
+
c.start(registry, log, service, server, serverMaintenance, () => {
|
|
111
|
+
setTimeout(function () {
|
|
112
|
+
done();
|
|
113
|
+
}, 5000);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("a cached response is served from the cache", async () => {
|
|
120
|
+
let first = await get('/spaces', ownerToken);
|
|
121
|
+
assert.strictEqual(first.status, 200, JSON.stringify(first.body));
|
|
122
|
+
assert.strictEqual(first.cache, 'MISS');
|
|
123
|
+
await settle();
|
|
124
|
+
let second = await get('/spaces', ownerToken);
|
|
125
|
+
assert.strictEqual(second.cache, 'HIT');
|
|
126
|
+
assert.strictEqual(second.body.data.call, first.body.data.call);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("one user: every query variant of the api is removed, the other user keeps theirs", async () => {
|
|
130
|
+
await get('/spaces?page=1', ownerToken);
|
|
131
|
+
await get('/spaces?page=2', ownerToken);
|
|
132
|
+
await get('/spaces', secondToken);
|
|
133
|
+
await settle();
|
|
134
|
+
assert.strictEqual((await get('/spaces?page=1', ownerToken)).cache, 'HIT');
|
|
135
|
+
assert.strictEqual((await get('/spaces', secondToken)).cache, 'HIT');
|
|
136
|
+
|
|
137
|
+
let outcome = await invalidate({ "tenant": tenantId, "service": "spaces", "api": "GET /spaces", "user": ownerId });
|
|
138
|
+
assert.strictEqual(outcome.status, 200, JSON.stringify(outcome.body));
|
|
139
|
+
assert.strictEqual(outcome.body.result, true);
|
|
140
|
+
assert.strictEqual(outcome.body.data.removed, 3);
|
|
141
|
+
|
|
142
|
+
assert.strictEqual((await get('/spaces', ownerToken)).cache, 'MISS');
|
|
143
|
+
assert.strictEqual((await get('/spaces?page=1', ownerToken)).cache, 'MISS');
|
|
144
|
+
assert.strictEqual((await get('/spaces?page=2', ownerToken)).cache, 'MISS');
|
|
145
|
+
assert.strictEqual((await get('/spaces', secondToken)).cache, 'HIT');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("without a user every user's entries are removed", async () => {
|
|
149
|
+
await settle();
|
|
150
|
+
assert.strictEqual((await get('/spaces', ownerToken)).cache, 'HIT');
|
|
151
|
+
assert.strictEqual((await get('/spaces', secondToken)).cache, 'HIT');
|
|
152
|
+
let outcome = await invalidate({ "tenant": tenantId, "service": "spaces", "api": "GET /spaces" });
|
|
153
|
+
assert.strictEqual(outcome.status, 200);
|
|
154
|
+
assert.ok(outcome.body.data.removed >= 2);
|
|
155
|
+
assert.strictEqual((await get('/spaces', ownerToken)).cache, 'MISS');
|
|
156
|
+
assert.strictEqual((await get('/spaces', secondToken)).cache, 'MISS');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("an api with a path param is removed for every value", async () => {
|
|
160
|
+
await get('/space/1', ownerToken);
|
|
161
|
+
await get('/space/2', ownerToken);
|
|
162
|
+
await settle();
|
|
163
|
+
assert.strictEqual((await get('/space/1', ownerToken)).cache, 'HIT');
|
|
164
|
+
let outcome = await invalidate({ "tenant": tenantId, "service": "spaces", "api": "GET /space/:id", "user": ownerId });
|
|
165
|
+
assert.strictEqual(outcome.body.data.removed, 2);
|
|
166
|
+
assert.strictEqual((await get('/space/1', ownerToken)).cache, 'MISS');
|
|
167
|
+
assert.strictEqual((await get('/space/2', ownerToken)).cache, 'MISS');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("a tenant scoped entry is shared by users, and removed without a user", async () => {
|
|
171
|
+
await get('/catalog', ownerToken);
|
|
172
|
+
await settle();
|
|
173
|
+
assert.strictEqual((await get('/catalog', secondToken)).cache, 'HIT');
|
|
174
|
+
let outcome = await invalidate({ "tenant": tenantId, "service": "spaces", "api": "GET /catalog" });
|
|
175
|
+
assert.strictEqual(outcome.body.data.removed, 1);
|
|
176
|
+
assert.strictEqual((await get('/catalog', secondToken)).cache, 'MISS');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("a request that started before the invalidation is not stored", async () => {
|
|
180
|
+
let inFlight = get('/slow', ownerToken);
|
|
181
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
182
|
+
let outcome = await invalidate({ "tenant": tenantId, "service": "spaces", "api": "GET /slow", "user": ownerId });
|
|
183
|
+
assert.strictEqual(outcome.status, 200);
|
|
184
|
+
let first = await inFlight;
|
|
185
|
+
assert.strictEqual(first.status, 200);
|
|
186
|
+
await settle();
|
|
187
|
+
let next = await get('/slow', ownerToken);
|
|
188
|
+
assert.strictEqual(next.cache, 'MISS');
|
|
189
|
+
assert.strictEqual(next.body.data.call, first.body.data.call + 1);
|
|
190
|
+
await settle();
|
|
191
|
+
assert.strictEqual((await get('/slow', ownerToken)).cache, 'HIT');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("refuses an invalid request", async () => {
|
|
195
|
+
assert.strictEqual((await invalidate({ "service": "spaces" })).status, 400);
|
|
196
|
+
assert.strictEqual((await invalidate({ "tenant": tenantId, "service": "spaces", "api": "/spaces" })).status, 400);
|
|
197
|
+
let wrongMethod = await invalidate(undefined, 'GET');
|
|
198
|
+
assert.strictEqual(wrongMethod.status, 405);
|
|
199
|
+
assert.strictEqual(wrongMethod.body.result, false);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
after((done) => {
|
|
203
|
+
mockServers.sAppServer.close();
|
|
204
|
+
mockServers.mAppServer.close();
|
|
205
|
+
c.stop(registry, log, service, server, serverMaintenance, () => {
|
|
206
|
+
done();
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
});
|
|
@@ -40,6 +40,14 @@ describe("starting integration tests", () => {
|
|
|
40
40
|
// to test the tenant access resolver
|
|
41
41
|
require("./UC5-tenant-access/index.js");
|
|
42
42
|
|
|
43
|
+
|
|
44
|
+
// to test idempotency with the mongo model
|
|
45
|
+
require("./UC6-idempotency/index.js");
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
// to test cache invalidation with the mongo model
|
|
49
|
+
require("./UC7-cache-invalidate/index.js");
|
|
50
|
+
|
|
43
51
|
done();
|
|
44
52
|
});
|
|
45
53
|
});
|
package/test/unit/index.js
CHANGED
|
@@ -39,8 +39,11 @@ describe("Starting Gateway Unit test", () => {
|
|
|
39
39
|
require("./mw/traffic/index.js");
|
|
40
40
|
require("./mw/idempotency/index.js");
|
|
41
41
|
require("./mw/idempotency/memory.js");
|
|
42
|
+
require("./mw/idempotency/mongo.js");
|
|
42
43
|
require("./mw/cache/index.js");
|
|
43
44
|
require("./mw/cache/memory.js");
|
|
45
|
+
require("./mw/cache/mongo.js");
|
|
46
|
+
require("./mw/cache/invalidate.js");
|
|
44
47
|
require("./mw/lastSeen/index.js");
|
|
45
48
|
|
|
46
49
|
require("./utilities/utils.js");
|
|
@@ -575,4 +575,126 @@ describe("Unit test for: mw - cache", () => {
|
|
|
575
575
|
});
|
|
576
576
|
});
|
|
577
577
|
|
|
578
|
+
describe("what an invalidation matches on", () => {
|
|
579
|
+
let noopRes = {
|
|
580
|
+
setHeader: function() {},
|
|
581
|
+
writeHead: function() { return this; },
|
|
582
|
+
write: function() { return true; },
|
|
583
|
+
end: function() {}
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
function buildReq(path, isAPIPublic, apis) {
|
|
587
|
+
return {
|
|
588
|
+
method: 'GET',
|
|
589
|
+
query: { page: '2' },
|
|
590
|
+
soajs: {
|
|
591
|
+
tenant: { id: 'tenant-meta' },
|
|
592
|
+
uracDriver: { id: 'user-7' },
|
|
593
|
+
controller: {
|
|
594
|
+
serviceParams: { name: 'spaces', serviceInfo: ['', 'spaces'].concat(path.split('/').slice(1)), isAPIPublic: isAPIPublic }
|
|
595
|
+
},
|
|
596
|
+
registry: {
|
|
597
|
+
custom: {
|
|
598
|
+
gateway: {
|
|
599
|
+
value: {
|
|
600
|
+
cache: {
|
|
601
|
+
model: 'memory',
|
|
602
|
+
services: {
|
|
603
|
+
spaces: { enabled: true, apis: apis }
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
it("records the service, the api as configured and the user", (done) => {
|
|
615
|
+
let before = Date.now();
|
|
616
|
+
let req = buildReq('/space/42', false, { 'GET /space/:id': { enabled: true } });
|
|
617
|
+
mw(mockConfig)(req, noopRes, (error) => {
|
|
618
|
+
assert.ifError(error);
|
|
619
|
+
let meta = req.soajs.cacheContext.meta;
|
|
620
|
+
assert.strictEqual(meta.service, 'spaces');
|
|
621
|
+
assert.strictEqual(meta.api, 'GET /space/:id');
|
|
622
|
+
assert.strictEqual(meta.user, 'user-7');
|
|
623
|
+
assert.ok(meta.startedAt >= before && meta.startedAt <= Date.now());
|
|
624
|
+
done();
|
|
625
|
+
});
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
it("records no user for a tenant scoped api", (done) => {
|
|
629
|
+
let req = buildReq('/catalog', false, { 'GET /catalog': { enabled: true, scope: 'tenant' } });
|
|
630
|
+
mw(mockConfig)(req, noopRes, (error) => {
|
|
631
|
+
assert.ifError(error);
|
|
632
|
+
assert.strictEqual(req.soajs.cacheContext.meta.api, 'GET /catalog');
|
|
633
|
+
assert.strictEqual(req.soajs.cacheContext.meta.user, null);
|
|
634
|
+
done();
|
|
635
|
+
});
|
|
636
|
+
});
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
describe("the access token is not part of the key", () => {
|
|
640
|
+
let noopRes = {
|
|
641
|
+
setHeader: function() {},
|
|
642
|
+
writeHead: function() { return this; },
|
|
643
|
+
write: function() { return true; },
|
|
644
|
+
end: function() {}
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
function buildReq(query, userId) {
|
|
648
|
+
return {
|
|
649
|
+
method: 'GET',
|
|
650
|
+
query: query,
|
|
651
|
+
soajs: {
|
|
652
|
+
tenant: { id: 'tenant-token' },
|
|
653
|
+
uracDriver: { id: userId },
|
|
654
|
+
controller: { serviceParams: { name: 'test', serviceInfo: ['', 'test', 'catalog'], isAPIPublic: true } },
|
|
655
|
+
registry: {
|
|
656
|
+
custom: {
|
|
657
|
+
gateway: {
|
|
658
|
+
value: {
|
|
659
|
+
cache: {
|
|
660
|
+
model: 'memory',
|
|
661
|
+
services: {
|
|
662
|
+
test: { enabled: true, apis: { 'GET /catalog': { enabled: true, scope: 'tenant' } } }
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function keyOf(query, userId) {
|
|
674
|
+
return new Promise((resolve) => {
|
|
675
|
+
let req = buildReq(query, userId);
|
|
676
|
+
mw(mockConfig)(req, noopRes, () => {
|
|
677
|
+
resolve(req.soajs.cacheContext.key.l2);
|
|
678
|
+
});
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
it("two users with their own tokens share a tenant scoped entry", async () => {
|
|
683
|
+
let ann = await keyOf({ access_token: 'AAA', page: '1' }, 'ann');
|
|
684
|
+
let bob = await keyOf({ access_token: 'BBB', page: '1' }, 'bob');
|
|
685
|
+
assert.strictEqual(ann, bob);
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
it("a token alone gives the same key as no query", async () => {
|
|
689
|
+
let withToken = await keyOf({ access_token: 'AAA' }, 'ann');
|
|
690
|
+
let without = await keyOf({}, 'ann');
|
|
691
|
+
assert.strictEqual(withToken, without);
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
it("the other query params still make the key", async () => {
|
|
695
|
+
let one = await keyOf({ access_token: 'AAA', page: '1' }, 'ann');
|
|
696
|
+
let two = await keyOf({ access_token: 'AAA', page: '2' }, 'ann');
|
|
697
|
+
assert.notStrictEqual(one, two);
|
|
698
|
+
});
|
|
699
|
+
});
|
|
578
700
|
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const helper = require("../../../helper.js");
|
|
4
|
+
const invalidate = helper.requireModule('./mw/cache/invalidate');
|
|
5
|
+
const model = helper.requireModule('./mw/cache/model/mongo');
|
|
6
|
+
const assert = require('assert');
|
|
7
|
+
|
|
8
|
+
describe("Unit test for: mw - cache invalidate", () => {
|
|
9
|
+
|
|
10
|
+
let received = null;
|
|
11
|
+
let failure = null;
|
|
12
|
+
let originalInvalidateMatching = null;
|
|
13
|
+
let originalMongo = null;
|
|
14
|
+
|
|
15
|
+
function registry(model) {
|
|
16
|
+
return { custom: { gateway: { value: { cache: { model: model, services: {} } } } } };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
before(() => {
|
|
20
|
+
originalInvalidateMatching = model.invalidateMatching;
|
|
21
|
+
originalMongo = model.mongo;
|
|
22
|
+
model.mongo = {};
|
|
23
|
+
model.invalidateMatching = async (criteria) => {
|
|
24
|
+
received = criteria;
|
|
25
|
+
if (failure) {
|
|
26
|
+
throw failure;
|
|
27
|
+
}
|
|
28
|
+
return 2;
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
after(() => {
|
|
32
|
+
model.invalidateMatching = originalInvalidateMatching;
|
|
33
|
+
model.mongo = originalMongo;
|
|
34
|
+
});
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
received = null;
|
|
37
|
+
failure = null;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("removes the matching entries", async () => {
|
|
41
|
+
let outcome = await invalidate(registry('mongo'), { tenant: 't1', service: 'spaces', api: 'GET /spaces', user: 'u1' });
|
|
42
|
+
assert.deepStrictEqual(outcome, { status: 200, removed: 2 });
|
|
43
|
+
assert.deepStrictEqual(received, { tenant: 't1', service: 'spaces', api: 'GET /spaces', user: 'u1' });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("api and user are optional", async () => {
|
|
47
|
+
let outcome = await invalidate(registry('mongo'), { tenant: 't1', service: 'spaces' });
|
|
48
|
+
assert.strictEqual(outcome.status, 200);
|
|
49
|
+
assert.deepStrictEqual(received, { tenant: 't1', service: 'spaces', api: undefined, user: undefined });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("refuses the memory model", async () => {
|
|
53
|
+
let outcome = await invalidate(registry('memory'), { tenant: 't1', service: 'spaces' });
|
|
54
|
+
assert.strictEqual(outcome.status, 400);
|
|
55
|
+
assert.ok(outcome.error.indexOf('mongo') !== -1);
|
|
56
|
+
assert.strictEqual(received, null);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("refuses when no model is set, memory is the default", async () => {
|
|
60
|
+
let outcome = await invalidate(registry(undefined), { tenant: 't1', service: 'spaces' });
|
|
61
|
+
assert.strictEqual(outcome.status, 400);
|
|
62
|
+
assert.strictEqual(received, null);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("refuses when caching is not configured", async () => {
|
|
66
|
+
let outcome = await invalidate({ custom: {} }, { tenant: 't1', service: 'spaces' });
|
|
67
|
+
assert.strictEqual(outcome.status, 400);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("requires tenant and service", async () => {
|
|
71
|
+
assert.strictEqual((await invalidate(registry('mongo'), null)).status, 400);
|
|
72
|
+
assert.strictEqual((await invalidate(registry('mongo'), { service: 'spaces' })).status, 400);
|
|
73
|
+
assert.strictEqual((await invalidate(registry('mongo'), { tenant: 't1' })).status, 400);
|
|
74
|
+
assert.strictEqual(received, null);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("requires the api as written in the configuration", async () => {
|
|
78
|
+
assert.strictEqual((await invalidate(registry('mongo'), { tenant: 't1', service: 'spaces', api: '/spaces' })).status, 400);
|
|
79
|
+
assert.strictEqual((await invalidate(registry('mongo'), { tenant: 't1', service: 'spaces', api: 'POST /spaces' })).status, 400);
|
|
80
|
+
assert.strictEqual((await invalidate(registry('mongo'), { tenant: 't1', service: 'spaces', user: 42 })).status, 400);
|
|
81
|
+
assert.strictEqual(received, null);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("answers 500 when the store fails", async () => {
|
|
85
|
+
failure = new Error("down");
|
|
86
|
+
let outcome = await invalidate(registry('mongo'), { tenant: 't1', service: 'spaces' });
|
|
87
|
+
assert.strictEqual(outcome.status, 500);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("answers 500 when the store is not ready", async () => {
|
|
91
|
+
model.mongo = null;
|
|
92
|
+
try {
|
|
93
|
+
let outcome = await invalidate(registry('mongo'), { tenant: 't1', service: 'spaces' });
|
|
94
|
+
assert.strictEqual(outcome.status, 500);
|
|
95
|
+
} finally {
|
|
96
|
+
model.mongo = {};
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
});
|