soajs.controller 4.3.4 → 4.3.5

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.
@@ -0,0 +1,354 @@
1
+ "use strict";
2
+
3
+ const helper = require("../../../helper.js");
4
+ const mw = helper.requireModule('./mw/idempotency/index');
5
+ const assert = require('assert');
6
+
7
+ describe("Unit test for: mw - idempotency", () => {
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("no idempotency key header", () => {
30
+ it("should call next() when no Idempotency-Key header", (done) => {
31
+ let req = {
32
+ headers: {},
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
+
48
+ describe("invalid idempotency key format", () => {
49
+ it("should return 400 for invalid UUID format", (done) => {
50
+ let responseData = null;
51
+ let req = {
52
+ headers: { 'idempotency-key': 'not-a-valid-uuid' },
53
+ soajs: {
54
+ tenant: { id: '123' },
55
+ controller: { serviceParams: { name: 'test', serviceInfo: ['', 'test', 'test'] } },
56
+ registry: { custom: {} },
57
+ controllerResponse: (data) => {
58
+ responseData = data;
59
+ }
60
+ }
61
+ };
62
+ let res = {};
63
+ let functionMw = mw(mockConfig);
64
+ functionMw(req, res, () => {
65
+ assert.fail('Should not call next');
66
+ });
67
+ setTimeout(() => {
68
+ assert.notStrictEqual(responseData, null);
69
+ assert.strictEqual(responseData.status, 400);
70
+ assert.strictEqual(responseData.code, 180);
71
+ done();
72
+ }, 10);
73
+ });
74
+
75
+ it("should return 400 for UUID v1 format", (done) => {
76
+ let responseData = null;
77
+ let req = {
78
+ headers: { 'idempotency-key': 'a1b2c3d4-e5f6-1789-abcd-ef0123456789' }, // v1 UUID
79
+ soajs: {
80
+ tenant: { id: '123' },
81
+ controller: { serviceParams: { name: 'test', serviceInfo: ['', 'test', 'test'] } },
82
+ registry: { custom: {} },
83
+ controllerResponse: (data) => {
84
+ responseData = data;
85
+ }
86
+ }
87
+ };
88
+ let res = {};
89
+ let functionMw = mw(mockConfig);
90
+ functionMw(req, res, () => {
91
+ assert.fail('Should not call next');
92
+ });
93
+ setTimeout(() => {
94
+ assert.notStrictEqual(responseData, null);
95
+ assert.strictEqual(responseData.status, 400);
96
+ assert.strictEqual(responseData.code, 180);
97
+ done();
98
+ }, 10);
99
+ });
100
+ });
101
+
102
+ describe("valid idempotency key", () => {
103
+ it("should call next() when no configuration exists", (done) => {
104
+ let req = {
105
+ headers: { 'idempotency-key': '550e8400-e29b-41d4-a716-446655440000' },
106
+ method: 'POST',
107
+ soajs: {
108
+ tenant: { id: '123' },
109
+ controller: { serviceParams: { name: 'test', serviceInfo: ['', 'test', 'test'] } },
110
+ registry: { custom: {} }
111
+ }
112
+ };
113
+ let res = {};
114
+ let functionMw = mw(mockConfig);
115
+ functionMw(req, res, (error) => {
116
+ assert.ifError(error);
117
+ done();
118
+ });
119
+ });
120
+
121
+ it("should call next() for GET requests", (done) => {
122
+ let req = {
123
+ headers: { 'idempotency-key': '550e8400-e29b-41d4-a716-446655440000' },
124
+ method: 'GET',
125
+ soajs: {
126
+ tenant: { id: '123' },
127
+ controller: { serviceParams: { name: 'test', serviceInfo: ['', 'test', 'test'] } },
128
+ registry: {
129
+ custom: {
130
+ gateway: {
131
+ value: {
132
+ idempotency: {
133
+ services: {
134
+ test: { enabled: true, ttl: 60000, apis: ['POST /test'] }
135
+ }
136
+ }
137
+ }
138
+ }
139
+ }
140
+ }
141
+ }
142
+ };
143
+ let res = {};
144
+ let functionMw = mw(mockConfig);
145
+ functionMw(req, res, (error) => {
146
+ assert.ifError(error);
147
+ done();
148
+ });
149
+ });
150
+
151
+ it("should call next() when service not configured", (done) => {
152
+ let req = {
153
+ headers: { 'idempotency-key': '550e8400-e29b-41d4-a716-446655440000' },
154
+ method: 'POST',
155
+ soajs: {
156
+ tenant: { id: '123' },
157
+ controller: { serviceParams: { name: 'unconfigured', serviceInfo: ['', 'unconfigured', 'test'] } },
158
+ registry: {
159
+ custom: {
160
+ gateway: {
161
+ value: {
162
+ idempotency: {
163
+ services: {
164
+ other: { enabled: true, ttl: 60000, apis: ['POST /test'] }
165
+ }
166
+ }
167
+ }
168
+ }
169
+ }
170
+ }
171
+ }
172
+ };
173
+ let res = {};
174
+ let functionMw = mw(mockConfig);
175
+ functionMw(req, res, (error) => {
176
+ assert.ifError(error);
177
+ done();
178
+ });
179
+ });
180
+
181
+ it("should call next() when service not enabled", (done) => {
182
+ let req = {
183
+ headers: { 'idempotency-key': '550e8400-e29b-41d4-a716-446655440000' },
184
+ method: 'POST',
185
+ soajs: {
186
+ tenant: { id: '123' },
187
+ controller: { serviceParams: { name: 'test', serviceInfo: ['', 'test', 'test'] } },
188
+ registry: {
189
+ custom: {
190
+ gateway: {
191
+ value: {
192
+ idempotency: {
193
+ services: {
194
+ test: { enabled: false, ttl: 60000, apis: ['POST /test'] }
195
+ }
196
+ }
197
+ }
198
+ }
199
+ }
200
+ }
201
+ }
202
+ };
203
+ let res = {};
204
+ let functionMw = mw(mockConfig);
205
+ functionMw(req, res, (error) => {
206
+ assert.ifError(error);
207
+ done();
208
+ });
209
+ });
210
+
211
+ it("should call next() when API not in configured list", (done) => {
212
+ let req = {
213
+ headers: { 'idempotency-key': '550e8400-e29b-41d4-a716-446655440000' },
214
+ method: 'POST',
215
+ soajs: {
216
+ tenant: { id: '123' },
217
+ controller: { serviceParams: { name: 'test', serviceInfo: ['', 'test', 'other'] } },
218
+ registry: {
219
+ custom: {
220
+ gateway: {
221
+ value: {
222
+ idempotency: {
223
+ services: {
224
+ test: { enabled: true, ttl: 60000, apis: ['POST /test'] }
225
+ }
226
+ }
227
+ }
228
+ }
229
+ }
230
+ }
231
+ }
232
+ };
233
+ let res = {};
234
+ let functionMw = mw(mockConfig);
235
+ functionMw(req, res, (error) => {
236
+ assert.ifError(error);
237
+ done();
238
+ });
239
+ });
240
+ });
241
+
242
+ describe("idempotency processing", () => {
243
+ it("should process new request and call next()", (done) => {
244
+ let req = {
245
+ headers: { 'idempotency-key': '550e8400-e29b-41d4-a716-446655440001' },
246
+ method: 'POST',
247
+ soajs: {
248
+ tenant: { id: 'tenant-new' },
249
+ controller: { serviceParams: { name: 'test', serviceInfo: ['', 'test', 'test'] } },
250
+ registry: {
251
+ custom: {
252
+ gateway: {
253
+ value: {
254
+ idempotency: {
255
+ model: 'memory',
256
+ services: {
257
+ test: { enabled: true, ttl: 60000, apis: ['POST /test'] }
258
+ }
259
+ }
260
+ }
261
+ }
262
+ }
263
+ }
264
+ }
265
+ };
266
+ let res = {
267
+ writeHead: function() { return this; },
268
+ write: function() { return true; },
269
+ end: function() {},
270
+ on: function() {}
271
+ };
272
+ let functionMw = mw(mockConfig);
273
+ functionMw(req, res, (error) => {
274
+ assert.ifError(error);
275
+ done();
276
+ });
277
+ });
278
+
279
+ it("should match API with path parameters", (done) => {
280
+ let req = {
281
+ headers: { 'idempotency-key': '550e8400-e29b-41d4-a716-446655440002' },
282
+ method: 'PUT',
283
+ soajs: {
284
+ tenant: { id: 'tenant-param' },
285
+ controller: { serviceParams: { name: 'test', serviceInfo: ['', 'test', 'users', '123', 'update'] } },
286
+ registry: {
287
+ custom: {
288
+ gateway: {
289
+ value: {
290
+ idempotency: {
291
+ model: 'memory',
292
+ services: {
293
+ test: { enabled: true, ttl: 60000, apis: ['PUT /users/:id/update'] }
294
+ }
295
+ }
296
+ }
297
+ }
298
+ }
299
+ }
300
+ }
301
+ };
302
+ let res = {
303
+ writeHead: function() { return this; },
304
+ write: function() { return true; },
305
+ end: function() {},
306
+ on: function() {}
307
+ };
308
+ let functionMw = mw(mockConfig);
309
+ functionMw(req, res, (error) => {
310
+ assert.ifError(error);
311
+ done();
312
+ });
313
+ });
314
+ });
315
+
316
+ describe("UUID validation", () => {
317
+ it("should accept valid UUID v4", (done) => {
318
+ let req = {
319
+ headers: { 'idempotency-key': 'f47ac10b-58cc-4372-a567-0e02b2c3d479' },
320
+ method: 'POST',
321
+ soajs: {
322
+ tenant: { id: '123' },
323
+ controller: { serviceParams: { name: 'test', serviceInfo: ['', 'test', 'test'] } },
324
+ registry: { custom: {} }
325
+ }
326
+ };
327
+ let res = {};
328
+ let functionMw = mw(mockConfig);
329
+ functionMw(req, res, (error) => {
330
+ assert.ifError(error);
331
+ done();
332
+ });
333
+ });
334
+
335
+ it("should accept UUID v4 with uppercase", (done) => {
336
+ let req = {
337
+ headers: { 'idempotency-key': 'F47AC10B-58CC-4372-A567-0E02B2C3D479' },
338
+ method: 'POST',
339
+ soajs: {
340
+ tenant: { id: '123' },
341
+ controller: { serviceParams: { name: 'test', serviceInfo: ['', 'test', 'test'] } },
342
+ registry: { custom: {} }
343
+ }
344
+ };
345
+ let res = {};
346
+ let functionMw = mw(mockConfig);
347
+ functionMw(req, res, (error) => {
348
+ assert.ifError(error);
349
+ done();
350
+ });
351
+ });
352
+ });
353
+
354
+ });
@@ -0,0 +1,151 @@
1
+ "use strict";
2
+
3
+ const helper = require("../../../helper.js");
4
+ const model = helper.requireModule('./mw/idempotency/model/memory');
5
+ const assert = require('assert');
6
+
7
+ describe("Unit test for: mw - idempotency/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.unlock({ l1: 'tenant1', l2: 'key1' });
21
+ model.unlock({ l1: 'tenant1', l2: 'key2' });
22
+ model.unlock({ 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 lock", (done) => {
34
+ let key = { l1: 'tenant1', l2: 'key1' };
35
+ model.lock(key, 60000);
36
+ let result = model.get(key);
37
+ assert.notStrictEqual(result, null);
38
+ assert.strictEqual(result.status, 'in_flight');
39
+ done();
40
+ });
41
+
42
+ it("should return null for expired entry", (done) => {
43
+ let key = { l1: 'tenant1', l2: 'key1' };
44
+ model.lock(key, 1); // 1ms TTL
45
+ setTimeout(() => {
46
+ let result = model.get(key);
47
+ assert.strictEqual(result, null);
48
+ done();
49
+ }, 10);
50
+ });
51
+ });
52
+
53
+ describe("lock", () => {
54
+ it("should successfully lock a new key", (done) => {
55
+ let key = { l1: 'tenant1', l2: 'key1' };
56
+ let result = model.lock(key, 60000);
57
+ assert.strictEqual(result, true);
58
+ done();
59
+ });
60
+
61
+ it("should fail to lock an already locked key", (done) => {
62
+ let key = { l1: 'tenant1', l2: 'key1' };
63
+ model.lock(key, 60000);
64
+ let result = model.lock(key, 60000);
65
+ assert.strictEqual(result, false);
66
+ done();
67
+ });
68
+
69
+ it("should allow locking after key expires", (done) => {
70
+ let key = { l1: 'tenant1', l2: 'key1' };
71
+ model.lock(key, 1); // 1ms TTL
72
+ setTimeout(() => {
73
+ let result = model.lock(key, 60000);
74
+ assert.strictEqual(result, true);
75
+ done();
76
+ }, 10);
77
+ });
78
+
79
+ it("should isolate keys by tenant", (done) => {
80
+ let key1 = { l1: 'tenant1', l2: 'key1' };
81
+ let key2 = { l1: 'tenant2', l2: 'key1' };
82
+ let result1 = model.lock(key1, 60000);
83
+ let result2 = model.lock(key2, 60000);
84
+ assert.strictEqual(result1, true);
85
+ assert.strictEqual(result2, true);
86
+ done();
87
+ });
88
+ });
89
+
90
+ describe("complete", () => {
91
+ it("should store response data", (done) => {
92
+ let key = { l1: 'tenant1', l2: 'key1' };
93
+ let response = {
94
+ statusCode: 200,
95
+ headers: { 'Content-Type': 'application/json' },
96
+ body: '{"result": true}'
97
+ };
98
+ model.lock(key, 60000);
99
+ model.complete(key, response, 60000);
100
+ let result = model.get(key);
101
+ assert.notStrictEqual(result, null);
102
+ assert.strictEqual(result.status, 'completed');
103
+ assert.deepStrictEqual(result.response, response);
104
+ done();
105
+ });
106
+
107
+ it("should update expiresAt on complete", (done) => {
108
+ let key = { l1: 'tenant1', l2: 'key1' };
109
+ model.lock(key, 100);
110
+ let afterLock = model.get(key);
111
+ model.complete(key, { statusCode: 200 }, 60000);
112
+ let afterComplete = model.get(key);
113
+ assert.ok(afterComplete.expiresAt > afterLock.expiresAt);
114
+ done();
115
+ });
116
+ });
117
+
118
+ describe("unlock", () => {
119
+ it("should remove locked key", (done) => {
120
+ let key = { l1: 'tenant1', l2: 'key1' };
121
+ model.lock(key, 60000);
122
+ model.unlock(key);
123
+ let result = model.get(key);
124
+ assert.strictEqual(result, null);
125
+ done();
126
+ });
127
+
128
+ it("should handle unlock of non-existent key", (done) => {
129
+ let key = { l1: 'tenant1', l2: 'nonexistent' };
130
+ // Should not throw
131
+ model.unlock(key);
132
+ done();
133
+ });
134
+ });
135
+
136
+ describe("cleanupExpiredEntries", () => {
137
+ it("should remove expired entries", (done) => {
138
+ let key1 = { l1: 'tenant1', l2: 'key1' };
139
+ let key2 = { l1: 'tenant1', l2: 'key2' };
140
+ model.lock(key1, 1); // expires immediately
141
+ model.lock(key2, 60000); // long TTL
142
+ setTimeout(() => {
143
+ model.cleanupExpiredEntries();
144
+ assert.strictEqual(model.get(key1), null);
145
+ assert.notStrictEqual(model.get(key2), null);
146
+ done();
147
+ }, 10);
148
+ });
149
+ });
150
+
151
+ });