soajs.controller 4.3.4 → 4.3.6
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/Gruntfile.js +1 -1
- package/docs/configuration-custom-registry.md +159 -9
- package/docs/middleware/cache.md +275 -0
- package/docs/middleware/idempotency.md +209 -0
- package/docs/request-flow.md +35 -4
- package/lib/request.js +5 -0
- package/mw/cache/index.js +175 -0
- package/mw/cache/model/memory.js +121 -0
- package/mw/cache/model/mongo.js +98 -0
- package/mw/gotoService/redirectToService.js +77 -16
- package/mw/idempotency/index.js +171 -0
- package/mw/idempotency/model/memory.js +132 -0
- package/mw/idempotency/model/mongo.js +116 -0
- package/mw/traffic/model/memory.js +4 -0
- package/package.json +1 -1
- package/server/controller.js +14 -0
- package/test/unit/index.js +4 -0
- package/test/unit/mw/cache/index.js +578 -0
- package/test/unit/mw/cache/memory.js +143 -0
- package/test/unit/mw/idempotency/index.js +354 -0
- package/test/unit/mw/idempotency/memory.js +151 -0
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const helper = require("../../../helper.js");
|
|
4
|
+
const mw = helper.requireModule('./mw/cache/index');
|
|
5
|
+
const assert = require('assert');
|
|
6
|
+
|
|
7
|
+
describe("Unit test for: mw - cache", () => {
|
|
8
|
+
|
|
9
|
+
let mockLog = {
|
|
10
|
+
debug: () => {},
|
|
11
|
+
info: () => {},
|
|
12
|
+
warn: () => {},
|
|
13
|
+
error: () => {}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
let mockConfig = {
|
|
17
|
+
log: mockLog,
|
|
18
|
+
gatewayDB: {}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
describe("middleware initialization", () => {
|
|
22
|
+
it("should return a function", (done) => {
|
|
23
|
+
let functionMw = mw(mockConfig);
|
|
24
|
+
assert.strictEqual(typeof functionMw, 'function');
|
|
25
|
+
done();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("non-GET requests", () => {
|
|
30
|
+
it("should call next() for POST requests", (done) => {
|
|
31
|
+
let req = {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
soajs: {
|
|
34
|
+
tenant: { id: '123' },
|
|
35
|
+
controller: { serviceParams: { name: 'test', serviceInfo: ['', 'test', 'test'] } },
|
|
36
|
+
registry: { custom: {} }
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
let res = {};
|
|
40
|
+
let functionMw = mw(mockConfig);
|
|
41
|
+
functionMw(req, res, (error) => {
|
|
42
|
+
assert.ifError(error);
|
|
43
|
+
done();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("should call next() for PUT requests", (done) => {
|
|
48
|
+
let req = {
|
|
49
|
+
method: 'PUT',
|
|
50
|
+
soajs: {
|
|
51
|
+
tenant: { id: '123' },
|
|
52
|
+
controller: { serviceParams: { name: 'test', serviceInfo: ['', 'test', 'test'] } },
|
|
53
|
+
registry: { custom: {} }
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
let res = {};
|
|
57
|
+
let functionMw = mw(mockConfig);
|
|
58
|
+
functionMw(req, res, (error) => {
|
|
59
|
+
assert.ifError(error);
|
|
60
|
+
done();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("should call next() for DELETE requests", (done) => {
|
|
65
|
+
let req = {
|
|
66
|
+
method: 'DELETE',
|
|
67
|
+
soajs: {
|
|
68
|
+
tenant: { id: '123' },
|
|
69
|
+
controller: { serviceParams: { name: 'test', serviceInfo: ['', 'test', 'test'] } },
|
|
70
|
+
registry: { custom: {} }
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
let res = {};
|
|
74
|
+
let functionMw = mw(mockConfig);
|
|
75
|
+
functionMw(req, res, (error) => {
|
|
76
|
+
assert.ifError(error);
|
|
77
|
+
done();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("GET requests without configuration", () => {
|
|
83
|
+
it("should call next() when no cache configuration exists", (done) => {
|
|
84
|
+
let req = {
|
|
85
|
+
method: 'GET',
|
|
86
|
+
soajs: {
|
|
87
|
+
tenant: { id: '123' },
|
|
88
|
+
controller: { serviceParams: { name: 'test', serviceInfo: ['', 'test', 'test'] } },
|
|
89
|
+
registry: { custom: {} }
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
let res = {};
|
|
93
|
+
let functionMw = mw(mockConfig);
|
|
94
|
+
functionMw(req, res, (error) => {
|
|
95
|
+
assert.ifError(error);
|
|
96
|
+
done();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("should call next() when service not configured", (done) => {
|
|
101
|
+
let req = {
|
|
102
|
+
method: 'GET',
|
|
103
|
+
soajs: {
|
|
104
|
+
tenant: { id: '123' },
|
|
105
|
+
controller: { serviceParams: { name: 'unconfigured', serviceInfo: ['', 'unconfigured', 'test'] } },
|
|
106
|
+
registry: {
|
|
107
|
+
custom: {
|
|
108
|
+
gateway: {
|
|
109
|
+
value: {
|
|
110
|
+
cache: {
|
|
111
|
+
services: {
|
|
112
|
+
other: { enabled: true, apis: { 'GET /test': { enabled: true } } }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
let res = {};
|
|
122
|
+
let functionMw = mw(mockConfig);
|
|
123
|
+
functionMw(req, res, (error) => {
|
|
124
|
+
assert.ifError(error);
|
|
125
|
+
done();
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("should call next() when service not enabled", (done) => {
|
|
130
|
+
let req = {
|
|
131
|
+
method: 'GET',
|
|
132
|
+
soajs: {
|
|
133
|
+
tenant: { id: '123' },
|
|
134
|
+
controller: { serviceParams: { name: 'test', serviceInfo: ['', 'test', 'test'] } },
|
|
135
|
+
registry: {
|
|
136
|
+
custom: {
|
|
137
|
+
gateway: {
|
|
138
|
+
value: {
|
|
139
|
+
cache: {
|
|
140
|
+
services: {
|
|
141
|
+
test: { enabled: false, apis: { 'GET /test': { enabled: true } } }
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
let res = {};
|
|
151
|
+
let functionMw = mw(mockConfig);
|
|
152
|
+
functionMw(req, res, (error) => {
|
|
153
|
+
assert.ifError(error);
|
|
154
|
+
done();
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("should call next() when API not configured", (done) => {
|
|
159
|
+
let req = {
|
|
160
|
+
method: 'GET',
|
|
161
|
+
soajs: {
|
|
162
|
+
tenant: { id: '123' },
|
|
163
|
+
controller: { serviceParams: { name: 'test', serviceInfo: ['', 'test', 'other'] } },
|
|
164
|
+
registry: {
|
|
165
|
+
custom: {
|
|
166
|
+
gateway: {
|
|
167
|
+
value: {
|
|
168
|
+
cache: {
|
|
169
|
+
services: {
|
|
170
|
+
test: { enabled: true, apis: { 'GET /test': { enabled: true } } }
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
let res = {};
|
|
180
|
+
let functionMw = mw(mockConfig);
|
|
181
|
+
functionMw(req, res, (error) => {
|
|
182
|
+
assert.ifError(error);
|
|
183
|
+
done();
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("should call next() when API not enabled", (done) => {
|
|
188
|
+
let req = {
|
|
189
|
+
method: 'GET',
|
|
190
|
+
soajs: {
|
|
191
|
+
tenant: { id: '123' },
|
|
192
|
+
controller: { serviceParams: { name: 'test', serviceInfo: ['', 'test', 'test'] } },
|
|
193
|
+
registry: {
|
|
194
|
+
custom: {
|
|
195
|
+
gateway: {
|
|
196
|
+
value: {
|
|
197
|
+
cache: {
|
|
198
|
+
services: {
|
|
199
|
+
test: { enabled: true, apis: { 'GET /test': { enabled: false } } }
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
let res = {};
|
|
209
|
+
let functionMw = mw(mockConfig);
|
|
210
|
+
functionMw(req, res, (error) => {
|
|
211
|
+
assert.ifError(error);
|
|
212
|
+
done();
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe("cache processing", () => {
|
|
218
|
+
it("should process cache miss and call next()", (done) => {
|
|
219
|
+
let req = {
|
|
220
|
+
method: 'GET',
|
|
221
|
+
query: {},
|
|
222
|
+
soajs: {
|
|
223
|
+
tenant: { id: 'tenant-cache-miss' },
|
|
224
|
+
controller: { serviceParams: { name: 'test', serviceInfo: ['', 'test', 'test'], isAPIPublic: true } },
|
|
225
|
+
registry: {
|
|
226
|
+
custom: {
|
|
227
|
+
gateway: {
|
|
228
|
+
value: {
|
|
229
|
+
cache: {
|
|
230
|
+
model: 'memory',
|
|
231
|
+
defaultTTL: 60000,
|
|
232
|
+
services: {
|
|
233
|
+
test: { enabled: true, apis: { 'GET /test': { enabled: true, ttl: 30000 } } }
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
let headerSet = false;
|
|
243
|
+
let res = {
|
|
244
|
+
setHeader: function(name, value) {
|
|
245
|
+
if (name === 'X-Cache' && value === 'MISS') {
|
|
246
|
+
headerSet = true;
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
writeHead: function() { return this; },
|
|
250
|
+
write: function() { return true; },
|
|
251
|
+
end: function() {}
|
|
252
|
+
};
|
|
253
|
+
let functionMw = mw(mockConfig);
|
|
254
|
+
functionMw(req, res, (error) => {
|
|
255
|
+
assert.ifError(error);
|
|
256
|
+
assert.strictEqual(headerSet, true);
|
|
257
|
+
done();
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("should match API with path parameters", (done) => {
|
|
262
|
+
let req = {
|
|
263
|
+
method: 'GET',
|
|
264
|
+
query: {},
|
|
265
|
+
soajs: {
|
|
266
|
+
tenant: { id: 'tenant-param-cache' },
|
|
267
|
+
controller: { serviceParams: { name: 'test', serviceInfo: ['', 'test', 'users', '123'], isAPIPublic: true } },
|
|
268
|
+
registry: {
|
|
269
|
+
custom: {
|
|
270
|
+
gateway: {
|
|
271
|
+
value: {
|
|
272
|
+
cache: {
|
|
273
|
+
model: 'memory',
|
|
274
|
+
services: {
|
|
275
|
+
test: { enabled: true, apis: { 'GET /users/:id': { enabled: true, ttl: 30000 } } }
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
let headerSet = false;
|
|
285
|
+
let res = {
|
|
286
|
+
setHeader: function(name, value) {
|
|
287
|
+
if (name === 'X-Cache') {
|
|
288
|
+
headerSet = true;
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
writeHead: function() { return this; },
|
|
292
|
+
write: function() { return true; },
|
|
293
|
+
end: function() {}
|
|
294
|
+
};
|
|
295
|
+
let functionMw = mw(mockConfig);
|
|
296
|
+
functionMw(req, res, (error) => {
|
|
297
|
+
assert.ifError(error);
|
|
298
|
+
assert.strictEqual(headerSet, true);
|
|
299
|
+
done();
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("should use defaultTTL when API TTL not specified", (done) => {
|
|
304
|
+
let req = {
|
|
305
|
+
method: 'GET',
|
|
306
|
+
query: {},
|
|
307
|
+
soajs: {
|
|
308
|
+
tenant: { id: 'tenant-default-ttl' },
|
|
309
|
+
controller: { serviceParams: { name: 'test', serviceInfo: ['', 'test', 'test'], isAPIPublic: true } },
|
|
310
|
+
registry: {
|
|
311
|
+
custom: {
|
|
312
|
+
gateway: {
|
|
313
|
+
value: {
|
|
314
|
+
cache: {
|
|
315
|
+
model: 'memory',
|
|
316
|
+
defaultTTL: 300000,
|
|
317
|
+
services: {
|
|
318
|
+
test: { enabled: true, apis: { 'GET /test': { enabled: true } } }
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
let res = {
|
|
328
|
+
setHeader: function() {},
|
|
329
|
+
writeHead: function() { return this; },
|
|
330
|
+
write: function() { return true; },
|
|
331
|
+
end: function() {}
|
|
332
|
+
};
|
|
333
|
+
let functionMw = mw(mockConfig);
|
|
334
|
+
functionMw(req, res, (error) => {
|
|
335
|
+
assert.ifError(error);
|
|
336
|
+
done();
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
describe("query parameter handling", () => {
|
|
342
|
+
it("should include query params in cache key", (done) => {
|
|
343
|
+
let req = {
|
|
344
|
+
method: 'GET',
|
|
345
|
+
query: { page: '1', limit: '10' },
|
|
346
|
+
soajs: {
|
|
347
|
+
tenant: { id: 'tenant-query' },
|
|
348
|
+
controller: { serviceParams: { name: 'test', serviceInfo: ['', 'test', 'test'], isAPIPublic: true } },
|
|
349
|
+
registry: {
|
|
350
|
+
custom: {
|
|
351
|
+
gateway: {
|
|
352
|
+
value: {
|
|
353
|
+
cache: {
|
|
354
|
+
model: 'memory',
|
|
355
|
+
services: {
|
|
356
|
+
test: { enabled: true, apis: { 'GET /test': { enabled: true, ttl: 30000 } } }
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
let res = {
|
|
366
|
+
setHeader: function() {},
|
|
367
|
+
writeHead: function() { return this; },
|
|
368
|
+
write: function() { return true; },
|
|
369
|
+
end: function() {}
|
|
370
|
+
};
|
|
371
|
+
let functionMw = mw(mockConfig);
|
|
372
|
+
functionMw(req, res, (error) => {
|
|
373
|
+
assert.ifError(error);
|
|
374
|
+
done();
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("should handle empty query params", (done) => {
|
|
379
|
+
let req = {
|
|
380
|
+
method: 'GET',
|
|
381
|
+
query: {},
|
|
382
|
+
soajs: {
|
|
383
|
+
tenant: { id: 'tenant-empty-query' },
|
|
384
|
+
controller: { serviceParams: { name: 'test', serviceInfo: ['', 'test', 'test'], isAPIPublic: true } },
|
|
385
|
+
registry: {
|
|
386
|
+
custom: {
|
|
387
|
+
gateway: {
|
|
388
|
+
value: {
|
|
389
|
+
cache: {
|
|
390
|
+
model: 'memory',
|
|
391
|
+
services: {
|
|
392
|
+
test: { enabled: true, apis: { 'GET /test': { enabled: true, ttl: 30000 } } }
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
let res = {
|
|
402
|
+
setHeader: function() {},
|
|
403
|
+
writeHead: function() { return this; },
|
|
404
|
+
write: function() { return true; },
|
|
405
|
+
end: function() {}
|
|
406
|
+
};
|
|
407
|
+
let functionMw = mw(mockConfig);
|
|
408
|
+
functionMw(req, res, (error) => {
|
|
409
|
+
assert.ifError(error);
|
|
410
|
+
done();
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
describe("missing prerequisites", () => {
|
|
416
|
+
it("should call next() when soajs object is missing", (done) => {
|
|
417
|
+
let req = {
|
|
418
|
+
method: 'GET'
|
|
419
|
+
};
|
|
420
|
+
let res = {};
|
|
421
|
+
let functionMw = mw(mockConfig);
|
|
422
|
+
functionMw(req, res, (error) => {
|
|
423
|
+
assert.ifError(error);
|
|
424
|
+
done();
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it("should call next() when tenant is missing", (done) => {
|
|
429
|
+
let req = {
|
|
430
|
+
method: 'GET',
|
|
431
|
+
soajs: {
|
|
432
|
+
controller: { serviceParams: { name: 'test', serviceInfo: ['', 'test', 'test'] } },
|
|
433
|
+
registry: { custom: {} }
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
let res = {};
|
|
437
|
+
let functionMw = mw(mockConfig);
|
|
438
|
+
functionMw(req, res, (error) => {
|
|
439
|
+
assert.ifError(error);
|
|
440
|
+
done();
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it("should call next() when controller is missing", (done) => {
|
|
445
|
+
let req = {
|
|
446
|
+
method: 'GET',
|
|
447
|
+
soajs: {
|
|
448
|
+
tenant: { id: '123' },
|
|
449
|
+
registry: { custom: {} }
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
let res = {};
|
|
453
|
+
let functionMw = mw(mockConfig);
|
|
454
|
+
functionMw(req, res, (error) => {
|
|
455
|
+
assert.ifError(error);
|
|
456
|
+
done();
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
describe("scope handling (public vs private)", () => {
|
|
462
|
+
let noopRes = {
|
|
463
|
+
setHeader: function() {},
|
|
464
|
+
writeHead: function() { return this; },
|
|
465
|
+
write: function() { return true; },
|
|
466
|
+
end: function() {}
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
function buildReq(serviceParamsExtra, uracDriver, apiConfig) {
|
|
470
|
+
return {
|
|
471
|
+
method: 'GET',
|
|
472
|
+
query: {},
|
|
473
|
+
soajs: {
|
|
474
|
+
tenant: { id: 'tenant-scope' },
|
|
475
|
+
uracDriver: uracDriver,
|
|
476
|
+
controller: {
|
|
477
|
+
serviceParams: Object.assign({ name: 'test', serviceInfo: ['', 'test', 'test'] }, serviceParamsExtra)
|
|
478
|
+
},
|
|
479
|
+
registry: {
|
|
480
|
+
custom: {
|
|
481
|
+
gateway: {
|
|
482
|
+
value: {
|
|
483
|
+
cache: {
|
|
484
|
+
model: 'memory',
|
|
485
|
+
services: {
|
|
486
|
+
test: { enabled: true, apis: { 'GET /test': Object.assign({ enabled: true, ttl: 30000 }, apiConfig) } }
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
it("public API: should cache scoped to tenant only (no user in key)", (done) => {
|
|
498
|
+
let req = buildReq({ isAPIPublic: true }, { id: 'user-1' }, {});
|
|
499
|
+
let functionMw = mw(mockConfig);
|
|
500
|
+
functionMw(req, noopRes, (error) => {
|
|
501
|
+
assert.ifError(error);
|
|
502
|
+
assert.ok(req.soajs.cacheContext, 'cacheContext should be set on MISS');
|
|
503
|
+
assert.strictEqual(req.soajs.cacheContext.key.l1, 'tenant-scope');
|
|
504
|
+
assert.strictEqual(req.soajs.cacheContext.key.l2.indexOf(':u:'), -1, 'public key must not contain user part');
|
|
505
|
+
done();
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it("private API: should default to tenant_user scope (user in key)", (done) => {
|
|
510
|
+
let req = buildReq({ isAPIPublic: false }, { id: 'user-1' }, {});
|
|
511
|
+
let functionMw = mw(mockConfig);
|
|
512
|
+
functionMw(req, noopRes, (error) => {
|
|
513
|
+
assert.ifError(error);
|
|
514
|
+
assert.ok(req.soajs.cacheContext, 'cacheContext should be set on MISS');
|
|
515
|
+
assert.notStrictEqual(req.soajs.cacheContext.key.l2.indexOf(':u:user-1'), -1, 'private key must contain user part');
|
|
516
|
+
done();
|
|
517
|
+
});
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it("private API: different users get different cache keys", (done) => {
|
|
521
|
+
let functionMw = mw(mockConfig);
|
|
522
|
+
let reqA = buildReq({ isAPIPublic: false }, { id: 'user-A' }, {});
|
|
523
|
+
let reqB = buildReq({ isAPIPublic: false }, { id: 'user-B' }, {});
|
|
524
|
+
functionMw(reqA, noopRes, (errA) => {
|
|
525
|
+
assert.ifError(errA);
|
|
526
|
+
functionMw(reqB, noopRes, (errB) => {
|
|
527
|
+
assert.ifError(errB);
|
|
528
|
+
assert.notStrictEqual(reqA.soajs.cacheContext.key.l2, reqB.soajs.cacheContext.key.l2, 'different users must not share a key');
|
|
529
|
+
done();
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it("private API without resolved user: should skip caching (no cacheContext)", (done) => {
|
|
535
|
+
let req = buildReq({ isAPIPublic: false }, null, {});
|
|
536
|
+
let functionMw = mw(mockConfig);
|
|
537
|
+
functionMw(req, noopRes, (error) => {
|
|
538
|
+
assert.ifError(error);
|
|
539
|
+
assert.strictEqual(req.soajs.cacheContext, undefined, 'must not set cacheContext when user is unresolved');
|
|
540
|
+
done();
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it("scope override: private API forced to tenant scope", (done) => {
|
|
545
|
+
let req = buildReq({ isAPIPublic: false }, { id: 'user-1' }, { scope: 'tenant' });
|
|
546
|
+
let functionMw = mw(mockConfig);
|
|
547
|
+
functionMw(req, noopRes, (error) => {
|
|
548
|
+
assert.ifError(error);
|
|
549
|
+
assert.ok(req.soajs.cacheContext, 'cacheContext should be set on MISS');
|
|
550
|
+
assert.strictEqual(req.soajs.cacheContext.key.l2.indexOf(':u:'), -1, 'overridden tenant scope must not contain user part');
|
|
551
|
+
done();
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it("scope override: public API forced to tenant_user scope", (done) => {
|
|
556
|
+
let req = buildReq({ isAPIPublic: true }, { id: 'user-1' }, { scope: 'tenant_user' });
|
|
557
|
+
let functionMw = mw(mockConfig);
|
|
558
|
+
functionMw(req, noopRes, (error) => {
|
|
559
|
+
assert.ifError(error);
|
|
560
|
+
assert.ok(req.soajs.cacheContext, 'cacheContext should be set on MISS');
|
|
561
|
+
assert.notStrictEqual(req.soajs.cacheContext.key.l2.indexOf(':u:user-1'), -1, 'overridden tenant_user scope must contain user part');
|
|
562
|
+
done();
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it("invalid scope: should fall back to default (private -> tenant_user)", (done) => {
|
|
567
|
+
let req = buildReq({ isAPIPublic: false }, { id: 'user-1' }, { scope: 'bogus' });
|
|
568
|
+
let functionMw = mw(mockConfig);
|
|
569
|
+
functionMw(req, noopRes, (error) => {
|
|
570
|
+
assert.ifError(error);
|
|
571
|
+
assert.ok(req.soajs.cacheContext, 'cacheContext should be set on MISS');
|
|
572
|
+
assert.notStrictEqual(req.soajs.cacheContext.key.l2.indexOf(':u:user-1'), -1, 'invalid scope must fall back to tenant_user for private API');
|
|
573
|
+
done();
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
});
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const helper = require("../../../helper.js");
|
|
4
|
+
const model = helper.requireModule('./mw/cache/model/memory');
|
|
5
|
+
const assert = require('assert');
|
|
6
|
+
|
|
7
|
+
describe("Unit test for: mw - cache/model/memory", () => {
|
|
8
|
+
|
|
9
|
+
before((done) => {
|
|
10
|
+
model.init({
|
|
11
|
+
debug: () => {},
|
|
12
|
+
info: () => {},
|
|
13
|
+
error: () => {}
|
|
14
|
+
});
|
|
15
|
+
done();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach((done) => {
|
|
19
|
+
// Clean up between tests
|
|
20
|
+
model.invalidate({ l1: 'tenant1', l2: 'key1' });
|
|
21
|
+
model.invalidate({ l1: 'tenant1', l2: 'key2' });
|
|
22
|
+
model.invalidate({ l1: 'tenant2', l2: 'key1' });
|
|
23
|
+
done();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("get", () => {
|
|
27
|
+
it("should return null for non-existent key", (done) => {
|
|
28
|
+
let result = model.get({ l1: 'tenant1', l2: 'nonexistent' });
|
|
29
|
+
assert.strictEqual(result, null);
|
|
30
|
+
done();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should return entry after set", (done) => {
|
|
34
|
+
let key = { l1: 'tenant1', l2: 'key1' };
|
|
35
|
+
let response = { statusCode: 200, body: 'test' };
|
|
36
|
+
model.set(key, response, 60000);
|
|
37
|
+
let result = model.get(key);
|
|
38
|
+
assert.notStrictEqual(result, null);
|
|
39
|
+
assert.deepStrictEqual(result.response, response);
|
|
40
|
+
done();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should return null for expired entry", (done) => {
|
|
44
|
+
let key = { l1: 'tenant1', l2: 'key1' };
|
|
45
|
+
model.set(key, { statusCode: 200 }, 1); // 1ms TTL
|
|
46
|
+
setTimeout(() => {
|
|
47
|
+
let result = model.get(key);
|
|
48
|
+
assert.strictEqual(result, null);
|
|
49
|
+
done();
|
|
50
|
+
}, 10);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("set", () => {
|
|
55
|
+
it("should store response with TTL", (done) => {
|
|
56
|
+
let key = { l1: 'tenant1', l2: 'key1' };
|
|
57
|
+
let response = {
|
|
58
|
+
statusCode: 200,
|
|
59
|
+
headers: { 'Content-Type': 'application/json' },
|
|
60
|
+
body: '{"data": []}'
|
|
61
|
+
};
|
|
62
|
+
model.set(key, response, 60000);
|
|
63
|
+
let result = model.get(key);
|
|
64
|
+
assert.notStrictEqual(result, null);
|
|
65
|
+
assert.deepStrictEqual(result.response, response);
|
|
66
|
+
assert.ok(result.cachedAt);
|
|
67
|
+
assert.ok(result.expiresAt);
|
|
68
|
+
assert.ok(result.expiresAt > result.cachedAt);
|
|
69
|
+
done();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should overwrite existing entry", (done) => {
|
|
73
|
+
let key = { l1: 'tenant1', l2: 'key1' };
|
|
74
|
+
model.set(key, { statusCode: 200, body: 'first' }, 60000);
|
|
75
|
+
model.set(key, { statusCode: 200, body: 'second' }, 60000);
|
|
76
|
+
let result = model.get(key);
|
|
77
|
+
assert.strictEqual(result.response.body, 'second');
|
|
78
|
+
done();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("should isolate keys by tenant", (done) => {
|
|
82
|
+
let key1 = { l1: 'tenant1', l2: 'key1' };
|
|
83
|
+
let key2 = { l1: 'tenant2', l2: 'key1' };
|
|
84
|
+
model.set(key1, { statusCode: 200, body: 'tenant1' }, 60000);
|
|
85
|
+
model.set(key2, { statusCode: 200, body: 'tenant2' }, 60000);
|
|
86
|
+
let result1 = model.get(key1);
|
|
87
|
+
let result2 = model.get(key2);
|
|
88
|
+
assert.strictEqual(result1.response.body, 'tenant1');
|
|
89
|
+
assert.strictEqual(result2.response.body, 'tenant2');
|
|
90
|
+
done();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("invalidate", () => {
|
|
95
|
+
it("should remove cached entry", (done) => {
|
|
96
|
+
let key = { l1: 'tenant1', l2: 'key1' };
|
|
97
|
+
model.set(key, { statusCode: 200 }, 60000);
|
|
98
|
+
model.invalidate(key);
|
|
99
|
+
let result = model.get(key);
|
|
100
|
+
assert.strictEqual(result, null);
|
|
101
|
+
done();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("should handle invalidate of non-existent key", (done) => {
|
|
105
|
+
let key = { l1: 'tenant1', l2: 'nonexistent' };
|
|
106
|
+
// Should not throw
|
|
107
|
+
model.invalidate(key);
|
|
108
|
+
done();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("invalidateTenant", () => {
|
|
113
|
+
it("should remove all entries for tenant", (done) => {
|
|
114
|
+
let key1 = { l1: 'tenant1', l2: 'key1' };
|
|
115
|
+
let key2 = { l1: 'tenant1', l2: 'key2' };
|
|
116
|
+
let key3 = { l1: 'tenant2', l2: 'key1' };
|
|
117
|
+
model.set(key1, { statusCode: 200 }, 60000);
|
|
118
|
+
model.set(key2, { statusCode: 200 }, 60000);
|
|
119
|
+
model.set(key3, { statusCode: 200 }, 60000);
|
|
120
|
+
model.invalidateTenant('tenant1');
|
|
121
|
+
assert.strictEqual(model.get(key1), null);
|
|
122
|
+
assert.strictEqual(model.get(key2), null);
|
|
123
|
+
assert.notStrictEqual(model.get(key3), null);
|
|
124
|
+
done();
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("cleanupExpiredEntries", () => {
|
|
129
|
+
it("should remove expired entries", (done) => {
|
|
130
|
+
let key1 = { l1: 'tenant1', l2: 'key1' };
|
|
131
|
+
let key2 = { l1: 'tenant1', l2: 'key2' };
|
|
132
|
+
model.set(key1, { statusCode: 200 }, 1); // expires immediately
|
|
133
|
+
model.set(key2, { statusCode: 200 }, 60000); // long TTL
|
|
134
|
+
setTimeout(() => {
|
|
135
|
+
model.cleanupExpiredEntries();
|
|
136
|
+
assert.strictEqual(model.get(key1), null);
|
|
137
|
+
assert.notStrictEqual(model.get(key2), null);
|
|
138
|
+
done();
|
|
139
|
+
}, 10);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
});
|