@flusys/nestjs-shared 6.0.2 → 6.1.1
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/cjs/classes/api-controller.class.spec.js +684 -0
- package/cjs/classes/api-service.class.spec.js +809 -0
- package/cjs/classes/handle-custome-db.spec.js +82 -0
- package/cjs/classes/hybrid-cache.class.spec.js +168 -0
- package/cjs/classes/winston.logger.class.spec.js +330 -0
- package/cjs/decorators/api-response.decorator.spec.js +126 -0
- package/cjs/decorators/current-user.decorator.spec.js +74 -0
- package/cjs/decorators/log-action.decorator.spec.js +353 -0
- package/cjs/decorators/public.decorator.spec.js +50 -0
- package/cjs/decorators/require-permission.decorator.spec.js +147 -0
- package/cjs/exceptions/base-app.exception.spec.js +157 -0
- package/cjs/exceptions/permission.exception.spec.js +82 -0
- package/cjs/filters/global-exception.filter.spec.js +440 -0
- package/cjs/guards/jwt-auth.guard.spec.js +75 -0
- package/cjs/guards/optional-jwt-auth.guard.spec.js +55 -0
- package/cjs/guards/permission.guard.spec.js +358 -0
- package/cjs/interceptors/delete-empty-id-from-body.interceptor.spec.js +107 -0
- package/cjs/interceptors/idempotency.interceptor.spec.js +116 -0
- package/cjs/interceptors/response-meta.interceptor.spec.js +89 -0
- package/cjs/interceptors/set-user-field-on-body.interceptor.spec.js +128 -0
- package/cjs/interceptors/slug.interceptor.spec.js +110 -0
- package/cjs/middlewares/logger.middleware.spec.js +462 -0
- package/cjs/modules/cache/cache.module.spec.js +48 -0
- package/cjs/modules/datasource/multi-tenant-datasource.service.spec.js +381 -0
- package/cjs/modules/utils/utils.service.spec.js +169 -0
- package/cjs/utils/date-time.util.spec.js +84 -0
- package/cjs/utils/html-sanitizer.util.spec.js +76 -0
- package/cjs/utils/permission-cache-key.util.spec.js +88 -0
- package/cjs/utils/query-helpers.util.spec.js +209 -0
- package/cjs/utils/request.util.spec.js +249 -0
- package/cjs/utils/string.util.spec.js +63 -0
- package/fesm/classes/api-controller.class.spec.js +680 -0
- package/fesm/classes/api-service.class.spec.js +805 -0
- package/fesm/classes/handle-custome-db.spec.js +78 -0
- package/fesm/classes/hybrid-cache.class.spec.js +164 -0
- package/fesm/classes/winston.logger.class.spec.js +327 -0
- package/fesm/decorators/api-response.decorator.spec.js +122 -0
- package/fesm/decorators/current-user.decorator.spec.js +73 -0
- package/fesm/decorators/log-action.decorator.spec.js +349 -0
- package/fesm/decorators/public.decorator.spec.js +46 -0
- package/fesm/decorators/require-permission.decorator.spec.js +143 -0
- package/fesm/exceptions/base-app.exception.spec.js +153 -0
- package/fesm/exceptions/permission.exception.spec.js +78 -0
- package/fesm/filters/global-exception.filter.spec.js +436 -0
- package/fesm/guards/jwt-auth.guard.spec.js +71 -0
- package/fesm/guards/optional-jwt-auth.guard.spec.js +51 -0
- package/fesm/guards/permission.guard.spec.js +354 -0
- package/fesm/interceptors/delete-empty-id-from-body.interceptor.spec.js +103 -0
- package/fesm/interceptors/idempotency.interceptor.spec.js +112 -0
- package/fesm/interceptors/response-meta.interceptor.spec.js +85 -0
- package/fesm/interceptors/set-user-field-on-body.interceptor.spec.js +124 -0
- package/fesm/interceptors/slug.interceptor.spec.js +106 -0
- package/fesm/middlewares/logger.middleware.spec.js +459 -0
- package/fesm/modules/cache/cache.module.spec.js +44 -0
- package/fesm/modules/datasource/multi-tenant-datasource.service.spec.js +377 -0
- package/fesm/modules/utils/utils.service.spec.js +165 -0
- package/fesm/utils/date-time.util.spec.js +80 -0
- package/fesm/utils/html-sanitizer.util.spec.js +72 -0
- package/fesm/utils/permission-cache-key.util.spec.js +84 -0
- package/fesm/utils/query-helpers.util.spec.js +205 -0
- package/fesm/utils/request.util.spec.js +245 -0
- package/fesm/utils/string.util.spec.js +59 -0
- package/package.json +2 -2
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
const _common = require("@nestjs/common");
|
|
6
|
+
const _core = require("@nestjs/core");
|
|
7
|
+
const _constants = require("../constants");
|
|
8
|
+
const _permissionexception = require("../exceptions/permission.exception");
|
|
9
|
+
const _permissionguard = require("./permission.guard");
|
|
10
|
+
function buildContext(request) {
|
|
11
|
+
return {
|
|
12
|
+
getHandler: ()=>({}),
|
|
13
|
+
getClass: ()=>({}),
|
|
14
|
+
switchToHttp: ()=>({
|
|
15
|
+
getRequest: ()=>request
|
|
16
|
+
})
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
describe('PermissionGuard', ()=>{
|
|
20
|
+
let reflector;
|
|
21
|
+
let cache;
|
|
22
|
+
let getAllAndOverrideMock;
|
|
23
|
+
beforeEach(()=>{
|
|
24
|
+
reflector = new _core.Reflector();
|
|
25
|
+
cache = {
|
|
26
|
+
get: jest.fn(),
|
|
27
|
+
set: jest.fn(),
|
|
28
|
+
del: jest.fn()
|
|
29
|
+
};
|
|
30
|
+
getAllAndOverrideMock = jest.fn();
|
|
31
|
+
jest.spyOn(reflector, 'getAllAndOverride').mockImplementation(getAllAndOverrideMock);
|
|
32
|
+
});
|
|
33
|
+
function mockMetadata(isPublic, permissionConfig) {
|
|
34
|
+
getAllAndOverrideMock.mockImplementation((key)=>{
|
|
35
|
+
if (key === _constants.IS_PUBLIC_KEY) return isPublic;
|
|
36
|
+
if (key === _constants.PERMISSIONS_KEY) return permissionConfig;
|
|
37
|
+
return undefined;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
describe('public routes and missing permission metadata', ()=>{
|
|
41
|
+
it('should allow access when the route is public', async ()=>{
|
|
42
|
+
mockMetadata(true, undefined);
|
|
43
|
+
const guard = new _permissionguard.PermissionGuard(reflector, cache);
|
|
44
|
+
await expect(guard.canActivate(buildContext({}))).resolves.toBe(true);
|
|
45
|
+
});
|
|
46
|
+
it('should allow access when no permission metadata is set', async ()=>{
|
|
47
|
+
mockMetadata(false, undefined);
|
|
48
|
+
const guard = new _permissionguard.PermissionGuard(reflector, cache);
|
|
49
|
+
await expect(guard.canActivate(buildContext({
|
|
50
|
+
user: {
|
|
51
|
+
id: 'u1'
|
|
52
|
+
}
|
|
53
|
+
}))).resolves.toBe(true);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
describe('authentication requirement', ()=>{
|
|
57
|
+
it('should throw UnauthorizedException when there is no user on the request', async ()=>{
|
|
58
|
+
mockMetadata(false, 'user.read');
|
|
59
|
+
const guard = new _permissionguard.PermissionGuard(reflector, cache);
|
|
60
|
+
await expect(guard.canActivate(buildContext({}))).rejects.toMatchObject({
|
|
61
|
+
constructor: _common.UnauthorizedException
|
|
62
|
+
});
|
|
63
|
+
try {
|
|
64
|
+
await guard.canActivate(buildContext({}));
|
|
65
|
+
} catch (error) {
|
|
66
|
+
expect(error.getResponse()).toMatchObject({
|
|
67
|
+
messageKey: _constants.AUTH_MESSAGES.TOKEN_REQUIRED
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
describe('cache availability', ()=>{
|
|
73
|
+
it('should throw PermissionSystemUnavailableException when no cache is injected', async ()=>{
|
|
74
|
+
mockMetadata(false, 'user.read');
|
|
75
|
+
const guard = new _permissionguard.PermissionGuard(reflector, undefined);
|
|
76
|
+
await expect(guard.canActivate(buildContext({
|
|
77
|
+
user: {
|
|
78
|
+
id: 'u1'
|
|
79
|
+
}
|
|
80
|
+
}))).rejects.toBeInstanceOf(_permissionexception.PermissionSystemUnavailableException);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
describe('permission resolution', ()=>{
|
|
84
|
+
it('should throw NoPermissionsFoundException when the user has no cached permissions', async ()=>{
|
|
85
|
+
mockMetadata(false, 'user.read');
|
|
86
|
+
cache.get.mockResolvedValue(undefined);
|
|
87
|
+
const guard = new _permissionguard.PermissionGuard(reflector, cache);
|
|
88
|
+
await expect(guard.canActivate(buildContext({
|
|
89
|
+
user: {
|
|
90
|
+
id: 'u1'
|
|
91
|
+
}
|
|
92
|
+
}))).rejects.toBeInstanceOf(_permissionexception.NoPermissionsFoundException);
|
|
93
|
+
});
|
|
94
|
+
it('should throw NoPermissionsFoundException when the cached permission array is empty', async ()=>{
|
|
95
|
+
mockMetadata(false, 'user.read');
|
|
96
|
+
cache.get.mockResolvedValue([]);
|
|
97
|
+
const guard = new _permissionguard.PermissionGuard(reflector, cache);
|
|
98
|
+
await expect(guard.canActivate(buildContext({
|
|
99
|
+
user: {
|
|
100
|
+
id: 'u1'
|
|
101
|
+
}
|
|
102
|
+
}))).rejects.toBeInstanceOf(_permissionexception.NoPermissionsFoundException);
|
|
103
|
+
});
|
|
104
|
+
it('should allow access for a single string permission that the user has', async ()=>{
|
|
105
|
+
mockMetadata(false, 'user.read');
|
|
106
|
+
cache.get.mockResolvedValue([
|
|
107
|
+
'user.read'
|
|
108
|
+
]);
|
|
109
|
+
const guard = new _permissionguard.PermissionGuard(reflector, cache);
|
|
110
|
+
await expect(guard.canActivate(buildContext({
|
|
111
|
+
user: {
|
|
112
|
+
id: 'u1'
|
|
113
|
+
}
|
|
114
|
+
}))).resolves.toBe(true);
|
|
115
|
+
});
|
|
116
|
+
it('should throw InsufficientPermissionsException for a single string permission the user lacks', async ()=>{
|
|
117
|
+
mockMetadata(false, 'user.write');
|
|
118
|
+
cache.get.mockResolvedValue([
|
|
119
|
+
'user.read'
|
|
120
|
+
]);
|
|
121
|
+
const guard = new _permissionguard.PermissionGuard(reflector, cache);
|
|
122
|
+
await expect(guard.canActivate(buildContext({
|
|
123
|
+
user: {
|
|
124
|
+
id: 'u1'
|
|
125
|
+
}
|
|
126
|
+
}))).rejects.toBeInstanceOf(_permissionexception.InsufficientPermissionsException);
|
|
127
|
+
});
|
|
128
|
+
it('should support a wildcard "*" permission granting everything', async ()=>{
|
|
129
|
+
mockMetadata(false, 'anything.here');
|
|
130
|
+
cache.get.mockResolvedValue([
|
|
131
|
+
'*'
|
|
132
|
+
]);
|
|
133
|
+
const guard = new _permissionguard.PermissionGuard(reflector, cache);
|
|
134
|
+
await expect(guard.canActivate(buildContext({
|
|
135
|
+
user: {
|
|
136
|
+
id: 'u1'
|
|
137
|
+
}
|
|
138
|
+
}))).resolves.toBe(true);
|
|
139
|
+
});
|
|
140
|
+
it('should support a namespaced wildcard like "user.*"', async ()=>{
|
|
141
|
+
mockMetadata(false, 'user.delete');
|
|
142
|
+
cache.get.mockResolvedValue([
|
|
143
|
+
'user.*'
|
|
144
|
+
]);
|
|
145
|
+
const guard = new _permissionguard.PermissionGuard(reflector, cache);
|
|
146
|
+
await expect(guard.canActivate(buildContext({
|
|
147
|
+
user: {
|
|
148
|
+
id: 'u1'
|
|
149
|
+
}
|
|
150
|
+
}))).resolves.toBe(true);
|
|
151
|
+
});
|
|
152
|
+
it('should evaluate an array of permissions as AND logic', async ()=>{
|
|
153
|
+
mockMetadata(false, [
|
|
154
|
+
'user.read',
|
|
155
|
+
'user.write'
|
|
156
|
+
]);
|
|
157
|
+
cache.get.mockResolvedValue([
|
|
158
|
+
'user.read'
|
|
159
|
+
]);
|
|
160
|
+
const guard = new _permissionguard.PermissionGuard(reflector, cache);
|
|
161
|
+
await expect(guard.canActivate(buildContext({
|
|
162
|
+
user: {
|
|
163
|
+
id: 'u1'
|
|
164
|
+
}
|
|
165
|
+
}))).rejects.toBeInstanceOf(_permissionexception.InsufficientPermissionsException);
|
|
166
|
+
});
|
|
167
|
+
it('should pass an array of permissions when the user has all of them', async ()=>{
|
|
168
|
+
mockMetadata(false, [
|
|
169
|
+
'user.read',
|
|
170
|
+
'user.write'
|
|
171
|
+
]);
|
|
172
|
+
cache.get.mockResolvedValue([
|
|
173
|
+
'user.read',
|
|
174
|
+
'user.write'
|
|
175
|
+
]);
|
|
176
|
+
const guard = new _permissionguard.PermissionGuard(reflector, cache);
|
|
177
|
+
await expect(guard.canActivate(buildContext({
|
|
178
|
+
user: {
|
|
179
|
+
id: 'u1'
|
|
180
|
+
}
|
|
181
|
+
}))).resolves.toBe(true);
|
|
182
|
+
});
|
|
183
|
+
it('should return true for an empty permission array (no-op logic node)', async ()=>{
|
|
184
|
+
mockMetadata(false, []);
|
|
185
|
+
cache.get.mockResolvedValue([
|
|
186
|
+
'user.read'
|
|
187
|
+
]);
|
|
188
|
+
const guard = new _permissionguard.PermissionGuard(reflector, cache);
|
|
189
|
+
await expect(guard.canActivate(buildContext({
|
|
190
|
+
user: {
|
|
191
|
+
id: 'u1'
|
|
192
|
+
}
|
|
193
|
+
}))).resolves.toBe(true);
|
|
194
|
+
});
|
|
195
|
+
it('should evaluate SimplePermissionConfig with OR operator', async ()=>{
|
|
196
|
+
mockMetadata(false, {
|
|
197
|
+
permissions: [
|
|
198
|
+
'admin',
|
|
199
|
+
'manager'
|
|
200
|
+
],
|
|
201
|
+
operator: 'OR'
|
|
202
|
+
});
|
|
203
|
+
cache.get.mockResolvedValue([
|
|
204
|
+
'manager'
|
|
205
|
+
]);
|
|
206
|
+
const guard = new _permissionguard.PermissionGuard(reflector, cache);
|
|
207
|
+
await expect(guard.canActivate(buildContext({
|
|
208
|
+
user: {
|
|
209
|
+
id: 'u1'
|
|
210
|
+
}
|
|
211
|
+
}))).resolves.toBe(true);
|
|
212
|
+
});
|
|
213
|
+
it('should reject SimplePermissionConfig with OR operator when none match', async ()=>{
|
|
214
|
+
mockMetadata(false, {
|
|
215
|
+
permissions: [
|
|
216
|
+
'admin',
|
|
217
|
+
'manager'
|
|
218
|
+
],
|
|
219
|
+
operator: 'OR'
|
|
220
|
+
});
|
|
221
|
+
cache.get.mockResolvedValue([
|
|
222
|
+
'guest'
|
|
223
|
+
]);
|
|
224
|
+
const guard = new _permissionguard.PermissionGuard(reflector, cache);
|
|
225
|
+
await expect(guard.canActivate(buildContext({
|
|
226
|
+
user: {
|
|
227
|
+
id: 'u1'
|
|
228
|
+
}
|
|
229
|
+
}))).rejects.toBeInstanceOf(_permissionexception.InsufficientPermissionsException);
|
|
230
|
+
});
|
|
231
|
+
it('should return true for SimplePermissionConfig with an empty permissions array', async ()=>{
|
|
232
|
+
mockMetadata(false, {
|
|
233
|
+
permissions: [],
|
|
234
|
+
operator: 'AND'
|
|
235
|
+
});
|
|
236
|
+
cache.get.mockResolvedValue([
|
|
237
|
+
'user.read'
|
|
238
|
+
]);
|
|
239
|
+
const guard = new _permissionguard.PermissionGuard(reflector, cache);
|
|
240
|
+
await expect(guard.canActivate(buildContext({
|
|
241
|
+
user: {
|
|
242
|
+
id: 'u1'
|
|
243
|
+
}
|
|
244
|
+
}))).resolves.toBe(true);
|
|
245
|
+
});
|
|
246
|
+
it('should evaluate a nested ILogicNode tree (AND of action + OR group)', async ()=>{
|
|
247
|
+
mockMetadata(false, {
|
|
248
|
+
type: 'group',
|
|
249
|
+
operator: 'AND',
|
|
250
|
+
children: [
|
|
251
|
+
{
|
|
252
|
+
type: 'action',
|
|
253
|
+
actionId: 'users.read'
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
type: 'group',
|
|
257
|
+
operator: 'OR',
|
|
258
|
+
children: [
|
|
259
|
+
{
|
|
260
|
+
type: 'action',
|
|
261
|
+
actionId: 'admin'
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
type: 'action',
|
|
265
|
+
actionId: 'manager'
|
|
266
|
+
}
|
|
267
|
+
]
|
|
268
|
+
}
|
|
269
|
+
]
|
|
270
|
+
});
|
|
271
|
+
cache.get.mockResolvedValue([
|
|
272
|
+
'users.read',
|
|
273
|
+
'manager'
|
|
274
|
+
]);
|
|
275
|
+
const guard = new _permissionguard.PermissionGuard(reflector, cache);
|
|
276
|
+
await expect(guard.canActivate(buildContext({
|
|
277
|
+
user: {
|
|
278
|
+
id: 'u1'
|
|
279
|
+
}
|
|
280
|
+
}))).resolves.toBe(true);
|
|
281
|
+
});
|
|
282
|
+
it('should fail-closed on a group node with no children', async ()=>{
|
|
283
|
+
mockMetadata(false, {
|
|
284
|
+
type: 'group',
|
|
285
|
+
operator: 'AND',
|
|
286
|
+
children: []
|
|
287
|
+
});
|
|
288
|
+
cache.get.mockResolvedValue([
|
|
289
|
+
'user.read'
|
|
290
|
+
]);
|
|
291
|
+
const guard = new _permissionguard.PermissionGuard(reflector, cache);
|
|
292
|
+
await expect(guard.canActivate(buildContext({
|
|
293
|
+
user: {
|
|
294
|
+
id: 'u1'
|
|
295
|
+
}
|
|
296
|
+
}))).rejects.toBeInstanceOf(_permissionexception.InsufficientPermissionsException);
|
|
297
|
+
});
|
|
298
|
+
it('should read backendCodes from an object-shaped cache value', async ()=>{
|
|
299
|
+
mockMetadata(false, 'user.read');
|
|
300
|
+
cache.get.mockResolvedValue({
|
|
301
|
+
backendCodes: [
|
|
302
|
+
'user.read'
|
|
303
|
+
]
|
|
304
|
+
});
|
|
305
|
+
const guard = new _permissionguard.PermissionGuard(reflector, cache);
|
|
306
|
+
await expect(guard.canActivate(buildContext({
|
|
307
|
+
user: {
|
|
308
|
+
id: 'u1'
|
|
309
|
+
}
|
|
310
|
+
}))).resolves.toBe(true);
|
|
311
|
+
});
|
|
312
|
+
it('should treat an object-shaped cache value with no backendCodes as no permissions', async ()=>{
|
|
313
|
+
mockMetadata(false, 'user.read');
|
|
314
|
+
cache.get.mockResolvedValue({});
|
|
315
|
+
const guard = new _permissionguard.PermissionGuard(reflector, cache);
|
|
316
|
+
await expect(guard.canActivate(buildContext({
|
|
317
|
+
user: {
|
|
318
|
+
id: 'u1'
|
|
319
|
+
}
|
|
320
|
+
}))).rejects.toBeInstanceOf(_permissionexception.NoPermissionsFoundException);
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
describe('company-scoped cache key', ()=>{
|
|
324
|
+
it('should include companyId/branchId in the cache lookup when company feature is enabled', async ()=>{
|
|
325
|
+
mockMetadata(false, 'user.read');
|
|
326
|
+
cache.get.mockResolvedValue([
|
|
327
|
+
'user.read'
|
|
328
|
+
]);
|
|
329
|
+
const guard = new _permissionguard.PermissionGuard(reflector, cache, {
|
|
330
|
+
enableCompanyFeature: true
|
|
331
|
+
});
|
|
332
|
+
await guard.canActivate(buildContext({
|
|
333
|
+
user: {
|
|
334
|
+
id: 'u1',
|
|
335
|
+
companyId: 'c1',
|
|
336
|
+
branchId: 'b1'
|
|
337
|
+
}
|
|
338
|
+
}));
|
|
339
|
+
expect(cache.get).toHaveBeenCalledWith(expect.stringContaining('company:c1:branch:b1:user:u1'));
|
|
340
|
+
});
|
|
341
|
+
it('should fall back to the user-scoped cache key when company feature is disabled', async ()=>{
|
|
342
|
+
mockMetadata(false, 'user.read');
|
|
343
|
+
cache.get.mockResolvedValue([
|
|
344
|
+
'user.read'
|
|
345
|
+
]);
|
|
346
|
+
const guard = new _permissionguard.PermissionGuard(reflector, cache);
|
|
347
|
+
await guard.canActivate(buildContext({
|
|
348
|
+
user: {
|
|
349
|
+
id: 'u1',
|
|
350
|
+
companyId: 'c1',
|
|
351
|
+
branchId: 'b1'
|
|
352
|
+
}
|
|
353
|
+
}));
|
|
354
|
+
expect(cache.get).toHaveBeenCalledWith(expect.stringContaining(':user:u1'));
|
|
355
|
+
expect(cache.get).toHaveBeenCalledWith(expect.not.stringContaining('company'));
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
const _rxjs = require("rxjs");
|
|
6
|
+
const _deleteemptyidfrombodyinterceptor = require("./delete-empty-id-from-body.interceptor");
|
|
7
|
+
function buildContext(body) {
|
|
8
|
+
const request = {
|
|
9
|
+
body
|
|
10
|
+
};
|
|
11
|
+
const context = {
|
|
12
|
+
switchToHttp: ()=>({
|
|
13
|
+
getRequest: ()=>request
|
|
14
|
+
})
|
|
15
|
+
};
|
|
16
|
+
return {
|
|
17
|
+
context,
|
|
18
|
+
request
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function buildCallHandler() {
|
|
22
|
+
return {
|
|
23
|
+
handle: jest.fn().mockReturnValue((0, _rxjs.of)('next-result'))
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
describe('DeleteEmptyIdFromBodyInterceptor', ()=>{
|
|
27
|
+
let interceptor;
|
|
28
|
+
beforeEach(()=>{
|
|
29
|
+
interceptor = new _deleteemptyidfrombodyinterceptor.DeleteEmptyIdFromBodyInterceptor();
|
|
30
|
+
});
|
|
31
|
+
it('should delete an empty-string id on a single object body', ()=>{
|
|
32
|
+
const { context, request } = buildContext({
|
|
33
|
+
id: '',
|
|
34
|
+
name: 'Product'
|
|
35
|
+
});
|
|
36
|
+
interceptor.intercept(context, buildCallHandler());
|
|
37
|
+
expect(request.body).toEqual({
|
|
38
|
+
name: 'Product'
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
it('should delete a null id on a single object body', ()=>{
|
|
42
|
+
const { context, request } = buildContext({
|
|
43
|
+
id: null,
|
|
44
|
+
name: 'Product'
|
|
45
|
+
});
|
|
46
|
+
interceptor.intercept(context, buildCallHandler());
|
|
47
|
+
expect(request.body).toEqual({
|
|
48
|
+
name: 'Product'
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
it('should keep a truthy id on a single object body', ()=>{
|
|
52
|
+
const { context, request } = buildContext({
|
|
53
|
+
id: 'uuid-1',
|
|
54
|
+
name: 'Product'
|
|
55
|
+
});
|
|
56
|
+
interceptor.intercept(context, buildCallHandler());
|
|
57
|
+
expect(request.body).toEqual({
|
|
58
|
+
id: 'uuid-1',
|
|
59
|
+
name: 'Product'
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
it('should strip empty ids from each item in an array body', ()=>{
|
|
63
|
+
const { context, request } = buildContext([
|
|
64
|
+
{
|
|
65
|
+
id: '',
|
|
66
|
+
name: 'A'
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: 'uuid-2',
|
|
70
|
+
name: 'B'
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: null,
|
|
74
|
+
name: 'C'
|
|
75
|
+
}
|
|
76
|
+
]);
|
|
77
|
+
interceptor.intercept(context, buildCallHandler());
|
|
78
|
+
expect(request.body).toEqual([
|
|
79
|
+
{
|
|
80
|
+
name: 'A'
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: 'uuid-2',
|
|
84
|
+
name: 'B'
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'C'
|
|
88
|
+
}
|
|
89
|
+
]);
|
|
90
|
+
});
|
|
91
|
+
it('should leave a null body untouched', ()=>{
|
|
92
|
+
const { context, request } = buildContext(null);
|
|
93
|
+
interceptor.intercept(context, buildCallHandler());
|
|
94
|
+
expect(request.body).toBeNull();
|
|
95
|
+
});
|
|
96
|
+
it('should call next.handle() and return its observable', (done)=>{
|
|
97
|
+
const { context } = buildContext({
|
|
98
|
+
id: ''
|
|
99
|
+
});
|
|
100
|
+
const callHandler = buildCallHandler();
|
|
101
|
+
interceptor.intercept(context, callHandler).subscribe((value)=>{
|
|
102
|
+
expect(value).toBe('next-result');
|
|
103
|
+
expect(callHandler.handle).toHaveBeenCalled();
|
|
104
|
+
done();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
const _common = require("@nestjs/common");
|
|
6
|
+
const _rxjs = require("rxjs");
|
|
7
|
+
const _constants = require("../constants");
|
|
8
|
+
const _idempotencyinterceptor = require("./idempotency.interceptor");
|
|
9
|
+
function buildContext(headers = {}) {
|
|
10
|
+
const request = {
|
|
11
|
+
headers
|
|
12
|
+
};
|
|
13
|
+
return {
|
|
14
|
+
switchToHttp: ()=>({
|
|
15
|
+
getRequest: ()=>request
|
|
16
|
+
})
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function buildCallHandler(response = {
|
|
20
|
+
success: true
|
|
21
|
+
}) {
|
|
22
|
+
return {
|
|
23
|
+
handle: jest.fn().mockReturnValue((0, _rxjs.of)(response))
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
describe('IdempotencyInterceptor', ()=>{
|
|
27
|
+
let cache;
|
|
28
|
+
beforeEach(()=>{
|
|
29
|
+
cache = {
|
|
30
|
+
get: jest.fn(),
|
|
31
|
+
set: jest.fn(),
|
|
32
|
+
del: jest.fn()
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
it('should skip idempotency handling when no cache is injected', async ()=>{
|
|
36
|
+
const interceptor = new _idempotencyinterceptor.IdempotencyInterceptor(undefined);
|
|
37
|
+
const callHandler = buildCallHandler('raw-result');
|
|
38
|
+
const result$ = await interceptor.intercept(buildContext({
|
|
39
|
+
[_constants.IDEMPOTENCY_KEY_HEADER]: 'key-1'
|
|
40
|
+
}), callHandler);
|
|
41
|
+
await new Promise((resolve)=>{
|
|
42
|
+
result$.subscribe((value)=>{
|
|
43
|
+
expect(value).toBe('raw-result');
|
|
44
|
+
resolve();
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
expect(callHandler.handle).toHaveBeenCalled();
|
|
48
|
+
});
|
|
49
|
+
it('should skip idempotency handling when no idempotency key header is provided', async ()=>{
|
|
50
|
+
const interceptor = new _idempotencyinterceptor.IdempotencyInterceptor(cache);
|
|
51
|
+
const callHandler = buildCallHandler('raw-result');
|
|
52
|
+
const result$ = await interceptor.intercept(buildContext({}), callHandler);
|
|
53
|
+
await new Promise((resolve)=>{
|
|
54
|
+
result$.subscribe((value)=>{
|
|
55
|
+
expect(value).toBe('raw-result');
|
|
56
|
+
resolve();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
expect(cache.get).not.toHaveBeenCalled();
|
|
60
|
+
});
|
|
61
|
+
it('should process and cache the response when the key is new', async ()=>{
|
|
62
|
+
cache.get.mockResolvedValue(undefined);
|
|
63
|
+
const interceptor = new _idempotencyinterceptor.IdempotencyInterceptor(cache);
|
|
64
|
+
const callHandler = buildCallHandler({
|
|
65
|
+
success: true,
|
|
66
|
+
data: 'X'
|
|
67
|
+
});
|
|
68
|
+
const result$ = await interceptor.intercept(buildContext({
|
|
69
|
+
[_constants.IDEMPOTENCY_KEY_HEADER]: 'key-1'
|
|
70
|
+
}), callHandler);
|
|
71
|
+
const cacheKey = `${_constants.IDEMPOTENCY_CACHE_PREFIX}:key-1`;
|
|
72
|
+
expect(cache.set).toHaveBeenCalledWith(cacheKey, '__PROCESSING__', 24 * 60 * 60 * 1000);
|
|
73
|
+
await new Promise((resolve)=>{
|
|
74
|
+
result$.subscribe((value)=>{
|
|
75
|
+
expect(value).toEqual({
|
|
76
|
+
success: true,
|
|
77
|
+
data: 'X'
|
|
78
|
+
});
|
|
79
|
+
resolve();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
// Allow the tap() side-effect microtask to flush
|
|
83
|
+
await Promise.resolve();
|
|
84
|
+
await Promise.resolve();
|
|
85
|
+
expect(cache.set).toHaveBeenCalledWith(cacheKey, {
|
|
86
|
+
success: true,
|
|
87
|
+
data: 'X'
|
|
88
|
+
}, 24 * 60 * 60 * 1000);
|
|
89
|
+
});
|
|
90
|
+
it('should return the cached response when the key already has a completed result', async ()=>{
|
|
91
|
+
const cachedResponse = {
|
|
92
|
+
success: true,
|
|
93
|
+
data: 'cached'
|
|
94
|
+
};
|
|
95
|
+
cache.get.mockResolvedValue(cachedResponse);
|
|
96
|
+
const interceptor = new _idempotencyinterceptor.IdempotencyInterceptor(cache);
|
|
97
|
+
const callHandler = buildCallHandler();
|
|
98
|
+
const result$ = await interceptor.intercept(buildContext({
|
|
99
|
+
[_constants.IDEMPOTENCY_KEY_HEADER]: 'key-1'
|
|
100
|
+
}), callHandler);
|
|
101
|
+
await new Promise((resolve)=>{
|
|
102
|
+
result$.subscribe((value)=>{
|
|
103
|
+
expect(value).toEqual(cachedResponse);
|
|
104
|
+
resolve();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
expect(callHandler.handle).not.toHaveBeenCalled();
|
|
108
|
+
});
|
|
109
|
+
it('should throw ConflictException when the key is still marked as processing', async ()=>{
|
|
110
|
+
cache.get.mockResolvedValue('__PROCESSING__');
|
|
111
|
+
const interceptor = new _idempotencyinterceptor.IdempotencyInterceptor(cache);
|
|
112
|
+
await expect(interceptor.intercept(buildContext({
|
|
113
|
+
[_constants.IDEMPOTENCY_KEY_HEADER]: 'key-1'
|
|
114
|
+
}), buildCallHandler())).rejects.toBeInstanceOf(_common.ConflictException);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
const _rxjs = require("rxjs");
|
|
6
|
+
const _constants = require("../constants");
|
|
7
|
+
const _responsemetainterceptor = require("./response-meta.interceptor");
|
|
8
|
+
function buildContext(headers = {}) {
|
|
9
|
+
const request = {
|
|
10
|
+
headers
|
|
11
|
+
};
|
|
12
|
+
const context = {
|
|
13
|
+
switchToHttp: ()=>({
|
|
14
|
+
getRequest: ()=>request
|
|
15
|
+
})
|
|
16
|
+
};
|
|
17
|
+
return {
|
|
18
|
+
context,
|
|
19
|
+
request
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function buildCallHandler(response) {
|
|
23
|
+
return {
|
|
24
|
+
handle: jest.fn().mockReturnValue((0, _rxjs.of)(response))
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
describe('ResponseMetaInterceptor', ()=>{
|
|
28
|
+
let interceptor;
|
|
29
|
+
beforeEach(()=>{
|
|
30
|
+
interceptor = new _responsemetainterceptor.ResponseMetaInterceptor();
|
|
31
|
+
});
|
|
32
|
+
it('should attach _meta to a response object with a success field', (done)=>{
|
|
33
|
+
const { context } = buildContext();
|
|
34
|
+
interceptor.intercept(context, buildCallHandler({
|
|
35
|
+
success: true,
|
|
36
|
+
data: {}
|
|
37
|
+
})).subscribe((result)=>{
|
|
38
|
+
expect(result.success).toBe(true);
|
|
39
|
+
expect(result._meta).toMatchObject({
|
|
40
|
+
requestId: expect.stringMatching(/^req_/),
|
|
41
|
+
timestamp: expect.any(String),
|
|
42
|
+
responseTime: expect.any(Number)
|
|
43
|
+
});
|
|
44
|
+
done();
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
it('should use the existing x-request-id header instead of generating one', (done)=>{
|
|
48
|
+
const { context } = buildContext({
|
|
49
|
+
[_constants.REQUEST_ID_HEADER]: 'existing-id'
|
|
50
|
+
});
|
|
51
|
+
interceptor.intercept(context, buildCallHandler({
|
|
52
|
+
success: true
|
|
53
|
+
})).subscribe((result)=>{
|
|
54
|
+
expect(result._meta.requestId).toBe('existing-id');
|
|
55
|
+
done();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
it('should set request.requestId as a side effect', ()=>{
|
|
59
|
+
const { context, request } = buildContext();
|
|
60
|
+
interceptor.intercept(context, buildCallHandler({
|
|
61
|
+
success: true
|
|
62
|
+
})).subscribe();
|
|
63
|
+
expect(request.requestId).toEqual(expect.stringMatching(/^req_/));
|
|
64
|
+
});
|
|
65
|
+
it('should not modify a response without a "success" field', (done)=>{
|
|
66
|
+
const { context } = buildContext();
|
|
67
|
+
const response = {
|
|
68
|
+
data: 'raw'
|
|
69
|
+
};
|
|
70
|
+
interceptor.intercept(context, buildCallHandler(response)).subscribe((result)=>{
|
|
71
|
+
expect(result).toBe(response);
|
|
72
|
+
done();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
it('should not modify a primitive response', (done)=>{
|
|
76
|
+
const { context } = buildContext();
|
|
77
|
+
interceptor.intercept(context, buildCallHandler('plain-string')).subscribe((result)=>{
|
|
78
|
+
expect(result).toBe('plain-string');
|
|
79
|
+
done();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
it('should not modify a null response', (done)=>{
|
|
83
|
+
const { context } = buildContext();
|
|
84
|
+
interceptor.intercept(context, buildCallHandler(null)).subscribe((result)=>{
|
|
85
|
+
expect(result).toBeNull();
|
|
86
|
+
done();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
});
|