@dismissible/nestjs-dismissible 0.0.2-canary.738340d.0 → 0.0.2-canary.b0d8bfe.0
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/README.md +58 -74
- package/jest.config.ts +1 -1
- package/package.json +12 -12
- package/project.json +1 -1
- package/src/api/dismissible-item-response.dto.ts +0 -8
- package/src/api/dismissible-item.mapper.spec.ts +0 -12
- package/src/api/dismissible-item.mapper.ts +2 -8
- package/src/api/index.ts +2 -3
- package/src/api/use-cases/dismiss/dismiss.controller.spec.ts +1 -2
- package/src/api/use-cases/dismiss/dismiss.controller.ts +9 -10
- package/src/api/use-cases/get-or-create/get-or-create.controller.spec.ts +2 -42
- package/src/api/use-cases/get-or-create/get-or-create.controller.ts +11 -58
- package/src/api/use-cases/get-or-create/index.ts +0 -1
- package/src/api/use-cases/restore/restore.controller.spec.ts +1 -2
- package/src/api/use-cases/restore/restore.controller.ts +9 -10
- package/src/api/validation/index.ts +2 -0
- package/src/api/validation/param-validation.pipe.spec.ts +313 -0
- package/src/api/validation/param-validation.pipe.ts +38 -0
- package/src/api/validation/param.decorators.ts +32 -0
- package/src/core/dismissible-core.service.spec.ts +75 -29
- package/src/core/dismissible-core.service.ts +40 -28
- package/src/core/dismissible.service.spec.ts +106 -24
- package/src/core/dismissible.service.ts +93 -54
- package/src/core/hook-runner.service.spec.ts +495 -54
- package/src/core/hook-runner.service.ts +125 -24
- package/src/core/index.ts +0 -1
- package/src/core/lifecycle-hook.interface.ts +7 -122
- package/src/core/service-responses.interface.ts +9 -9
- package/src/dismissible.module.integration.spec.ts +704 -0
- package/src/dismissible.module.ts +10 -11
- package/src/events/dismissible.events.ts +17 -40
- package/src/index.ts +1 -1
- package/src/response/http-exception-filter.spec.ts +179 -0
- package/src/response/http-exception-filter.ts +3 -3
- package/src/response/response.service.spec.ts +0 -14
- package/src/testing/factories.ts +24 -9
- package/src/utils/dismissible.helper.ts +2 -2
- package/src/validation/dismissible-input.dto.ts +47 -0
- package/src/validation/index.ts +1 -0
- package/tsconfig.json +3 -0
- package/tsconfig.spec.json +12 -0
- package/src/api/use-cases/get-or-create/get-or-create.request.dto.ts +0 -17
- package/src/core/create-options.ts +0 -9
- package/src/request/index.ts +0 -2
- package/src/request/request-context.decorator.ts +0 -14
- package/src/request/request-context.interface.ts +0 -6
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { mock } from 'ts-jest-mocker';
|
|
2
2
|
import { ForbiddenException } from '@nestjs/common';
|
|
3
3
|
import { HookRunner } from './hook-runner.service';
|
|
4
|
-
import { IDismissibleLifecycleHook } from '
|
|
5
|
-
import { BaseMetadata } from '@dismissible/nestjs-dismissible-item';
|
|
4
|
+
import { IDismissibleLifecycleHook } from '@dismissible/nestjs-dismissible-hooks';
|
|
6
5
|
import { createTestItem, createTestContext } from '../testing/factories';
|
|
7
6
|
import { IDismissibleLogger } from '@dismissible/nestjs-logger';
|
|
8
7
|
|
|
9
8
|
describe('HookRunner', () => {
|
|
10
|
-
let hookRunner: HookRunner
|
|
9
|
+
let hookRunner: HookRunner;
|
|
11
10
|
let mockLogger: jest.Mocked<IDismissibleLogger>;
|
|
12
11
|
|
|
13
12
|
const testUserId = 'test-user-id';
|
|
@@ -21,9 +20,9 @@ describe('HookRunner', () => {
|
|
|
21
20
|
hookRunner = new HookRunner([], mockLogger);
|
|
22
21
|
});
|
|
23
22
|
|
|
24
|
-
it('should return proceed: true for pre-
|
|
23
|
+
it('should return proceed: true for pre-request', async () => {
|
|
25
24
|
const context = createTestContext();
|
|
26
|
-
const result = await hookRunner.
|
|
25
|
+
const result = await hookRunner.runPreRequest('test-id', testUserId, context);
|
|
27
26
|
|
|
28
27
|
expect(result.proceed).toBe(true);
|
|
29
28
|
expect(result.id).toBe('test-id');
|
|
@@ -31,12 +30,12 @@ describe('HookRunner', () => {
|
|
|
31
30
|
expect(result.context).toEqual(context);
|
|
32
31
|
});
|
|
33
32
|
|
|
34
|
-
it('should complete post-
|
|
33
|
+
it('should complete post-request without error', async () => {
|
|
35
34
|
const item = createTestItem();
|
|
36
35
|
const context = createTestContext();
|
|
37
36
|
|
|
38
37
|
await expect(
|
|
39
|
-
hookRunner.
|
|
38
|
+
hookRunner.runPostRequest('test-id', item, testUserId, context),
|
|
40
39
|
).resolves.not.toThrow();
|
|
41
40
|
});
|
|
42
41
|
});
|
|
@@ -45,32 +44,32 @@ describe('HookRunner', () => {
|
|
|
45
44
|
it('should execute hooks in priority order (low to high) for pre-hooks', async () => {
|
|
46
45
|
const executionOrder: number[] = [];
|
|
47
46
|
|
|
48
|
-
const hook1: IDismissibleLifecycleHook
|
|
47
|
+
const hook1: IDismissibleLifecycleHook = {
|
|
49
48
|
priority: 10,
|
|
50
|
-
|
|
49
|
+
onBeforeRequest: jest.fn(async () => {
|
|
51
50
|
executionOrder.push(10);
|
|
52
51
|
return { proceed: true };
|
|
53
52
|
}),
|
|
54
53
|
};
|
|
55
54
|
|
|
56
|
-
const hook2: IDismissibleLifecycleHook
|
|
55
|
+
const hook2: IDismissibleLifecycleHook = {
|
|
57
56
|
priority: 5,
|
|
58
|
-
|
|
57
|
+
onBeforeRequest: jest.fn(async () => {
|
|
59
58
|
executionOrder.push(5);
|
|
60
59
|
return { proceed: true };
|
|
61
60
|
}),
|
|
62
61
|
};
|
|
63
62
|
|
|
64
|
-
const hook3: IDismissibleLifecycleHook
|
|
63
|
+
const hook3: IDismissibleLifecycleHook = {
|
|
65
64
|
priority: 15,
|
|
66
|
-
|
|
65
|
+
onBeforeRequest: jest.fn(async () => {
|
|
67
66
|
executionOrder.push(15);
|
|
68
67
|
return { proceed: true };
|
|
69
68
|
}),
|
|
70
69
|
};
|
|
71
70
|
|
|
72
71
|
hookRunner = new HookRunner([hook1, hook2, hook3], mockLogger);
|
|
73
|
-
await hookRunner.
|
|
72
|
+
await hookRunner.runPreRequest('test-id', testUserId, createTestContext());
|
|
74
73
|
|
|
75
74
|
expect(executionOrder).toEqual([5, 10, 15]);
|
|
76
75
|
});
|
|
@@ -78,56 +77,51 @@ describe('HookRunner', () => {
|
|
|
78
77
|
it('should execute hooks in reverse priority order for post-hooks', async () => {
|
|
79
78
|
const executionOrder: number[] = [];
|
|
80
79
|
|
|
81
|
-
const hook1: IDismissibleLifecycleHook
|
|
80
|
+
const hook1: IDismissibleLifecycleHook = {
|
|
82
81
|
priority: 10,
|
|
83
|
-
|
|
82
|
+
onAfterRequest: jest.fn(async () => {
|
|
84
83
|
executionOrder.push(10);
|
|
85
84
|
}),
|
|
86
85
|
};
|
|
87
86
|
|
|
88
|
-
const hook2: IDismissibleLifecycleHook
|
|
87
|
+
const hook2: IDismissibleLifecycleHook = {
|
|
89
88
|
priority: 5,
|
|
90
|
-
|
|
89
|
+
onAfterRequest: jest.fn(async () => {
|
|
91
90
|
executionOrder.push(5);
|
|
92
91
|
}),
|
|
93
92
|
};
|
|
94
93
|
|
|
95
|
-
const hook3: IDismissibleLifecycleHook
|
|
94
|
+
const hook3: IDismissibleLifecycleHook = {
|
|
96
95
|
priority: 15,
|
|
97
|
-
|
|
96
|
+
onAfterRequest: jest.fn(async () => {
|
|
98
97
|
executionOrder.push(15);
|
|
99
98
|
}),
|
|
100
99
|
};
|
|
101
100
|
|
|
102
101
|
hookRunner = new HookRunner([hook1, hook2, hook3], mockLogger);
|
|
103
|
-
await hookRunner.
|
|
104
|
-
'test-id',
|
|
105
|
-
createTestItem(),
|
|
106
|
-
testUserId,
|
|
107
|
-
createTestContext(),
|
|
108
|
-
);
|
|
102
|
+
await hookRunner.runPostRequest('test-id', createTestItem(), testUserId, createTestContext());
|
|
109
103
|
|
|
110
104
|
expect(executionOrder).toEqual([15, 10, 5]);
|
|
111
105
|
});
|
|
112
106
|
|
|
113
107
|
it('should block operation when pre-hook returns proceed: false', async () => {
|
|
114
|
-
const blockingHook: IDismissibleLifecycleHook
|
|
115
|
-
|
|
108
|
+
const blockingHook: IDismissibleLifecycleHook = {
|
|
109
|
+
onBeforeRequest: jest.fn(async () => ({
|
|
116
110
|
proceed: false,
|
|
117
111
|
reason: 'Rate limit exceeded',
|
|
118
112
|
})),
|
|
119
113
|
};
|
|
120
114
|
|
|
121
115
|
hookRunner = new HookRunner([blockingHook], mockLogger);
|
|
122
|
-
const result = await hookRunner.
|
|
116
|
+
const result = await hookRunner.runPreRequest('test-id', testUserId, createTestContext());
|
|
123
117
|
|
|
124
118
|
expect(result.proceed).toBe(false);
|
|
125
119
|
expect(result.reason).toBe('Rate limit exceeded');
|
|
126
120
|
});
|
|
127
121
|
|
|
128
122
|
it('should apply mutations from pre-hooks', async () => {
|
|
129
|
-
const mutatingHook: IDismissibleLifecycleHook
|
|
130
|
-
|
|
123
|
+
const mutatingHook: IDismissibleLifecycleHook = {
|
|
124
|
+
onBeforeRequest: jest.fn(async () => ({
|
|
131
125
|
proceed: true,
|
|
132
126
|
mutations: {
|
|
133
127
|
id: 'mutated-id',
|
|
@@ -137,48 +131,101 @@ describe('HookRunner', () => {
|
|
|
137
131
|
};
|
|
138
132
|
|
|
139
133
|
hookRunner = new HookRunner([mutatingHook], mockLogger);
|
|
140
|
-
const result = await hookRunner.
|
|
141
|
-
'original-id',
|
|
142
|
-
testUserId,
|
|
143
|
-
createTestContext(),
|
|
144
|
-
);
|
|
134
|
+
const result = await hookRunner.runPreRequest('original-id', testUserId, createTestContext());
|
|
145
135
|
|
|
146
136
|
expect(result.id).toBe('mutated-id');
|
|
147
137
|
expect(result.userId).toBe('mutated-user');
|
|
148
138
|
});
|
|
149
139
|
|
|
150
140
|
it('should pass mutations through multiple hooks', async () => {
|
|
151
|
-
const hook1: IDismissibleLifecycleHook
|
|
141
|
+
const hook1: IDismissibleLifecycleHook = {
|
|
152
142
|
priority: 1,
|
|
153
|
-
|
|
143
|
+
onBeforeRequest: jest.fn(async (itemId) => ({
|
|
154
144
|
proceed: true,
|
|
155
145
|
mutations: { id: `${itemId}-hook1` },
|
|
156
146
|
})),
|
|
157
147
|
};
|
|
158
148
|
|
|
159
|
-
const hook2: IDismissibleLifecycleHook
|
|
149
|
+
const hook2: IDismissibleLifecycleHook = {
|
|
160
150
|
priority: 2,
|
|
161
|
-
|
|
151
|
+
onBeforeRequest: jest.fn(async (itemId) => ({
|
|
162
152
|
proceed: true,
|
|
163
153
|
mutations: { id: `${itemId}-hook2` },
|
|
164
154
|
})),
|
|
165
155
|
};
|
|
166
156
|
|
|
167
157
|
hookRunner = new HookRunner([hook1, hook2], mockLogger);
|
|
168
|
-
const result = await hookRunner.
|
|
169
|
-
'original',
|
|
170
|
-
testUserId,
|
|
171
|
-
createTestContext(),
|
|
172
|
-
);
|
|
158
|
+
const result = await hookRunner.runPreRequest('original', testUserId, createTestContext());
|
|
173
159
|
|
|
174
160
|
expect(result.id).toBe('original-hook1-hook2');
|
|
175
161
|
});
|
|
162
|
+
|
|
163
|
+
it('should apply context mutations from pre-hooks', async () => {
|
|
164
|
+
const context = createTestContext();
|
|
165
|
+
|
|
166
|
+
const mutatingHook: IDismissibleLifecycleHook = {
|
|
167
|
+
onBeforeRequest: jest.fn(async () => ({
|
|
168
|
+
proceed: true,
|
|
169
|
+
mutations: {
|
|
170
|
+
context: { headers: { authorization: 'Bearer mutated-token' } },
|
|
171
|
+
},
|
|
172
|
+
})),
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
hookRunner = new HookRunner([mutatingHook], mockLogger);
|
|
176
|
+
const result = await hookRunner.runPreRequest('test-id', testUserId, context);
|
|
177
|
+
|
|
178
|
+
expect(result.context).toEqual({
|
|
179
|
+
...context,
|
|
180
|
+
headers: { authorization: 'Bearer mutated-token' },
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should ignore context mutation when no context provided', async () => {
|
|
185
|
+
const mutatingHook: IDismissibleLifecycleHook = {
|
|
186
|
+
onBeforeRequest: jest.fn(async () => ({
|
|
187
|
+
proceed: true,
|
|
188
|
+
mutations: {
|
|
189
|
+
context: { headers: { authorization: 'Bearer token' } },
|
|
190
|
+
},
|
|
191
|
+
})),
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
hookRunner = new HookRunner([mutatingHook], mockLogger);
|
|
195
|
+
const result = await hookRunner.runPreRequest('test-id', testUserId, undefined);
|
|
196
|
+
|
|
197
|
+
expect(result.context).toBeUndefined();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should handle hooks with default priority (undefined)', async () => {
|
|
201
|
+
const executionOrder: string[] = [];
|
|
202
|
+
|
|
203
|
+
const hook1: IDismissibleLifecycleHook = {
|
|
204
|
+
priority: 5,
|
|
205
|
+
onBeforeRequest: jest.fn(async () => {
|
|
206
|
+
executionOrder.push('priority-5');
|
|
207
|
+
return { proceed: true };
|
|
208
|
+
}),
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const hook2: IDismissibleLifecycleHook = {
|
|
212
|
+
onBeforeRequest: jest.fn(async () => {
|
|
213
|
+
executionOrder.push('priority-default');
|
|
214
|
+
return { proceed: true };
|
|
215
|
+
}),
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
hookRunner = new HookRunner([hook1, hook2], mockLogger);
|
|
219
|
+
await hookRunner.runPreRequest('test-id', testUserId, createTestContext());
|
|
220
|
+
|
|
221
|
+
expect(executionOrder).toEqual(['priority-default', 'priority-5']);
|
|
222
|
+
});
|
|
176
223
|
});
|
|
177
224
|
|
|
178
225
|
describe('error handling', () => {
|
|
179
226
|
it('should throw error from pre-hook', async () => {
|
|
180
|
-
const errorHook: IDismissibleLifecycleHook
|
|
181
|
-
|
|
227
|
+
const errorHook: IDismissibleLifecycleHook = {
|
|
228
|
+
onBeforeRequest: jest.fn(async () => {
|
|
182
229
|
throw new Error('Hook error');
|
|
183
230
|
}),
|
|
184
231
|
};
|
|
@@ -186,15 +233,31 @@ describe('HookRunner', () => {
|
|
|
186
233
|
hookRunner = new HookRunner([errorHook], mockLogger);
|
|
187
234
|
|
|
188
235
|
await expect(
|
|
189
|
-
hookRunner.
|
|
236
|
+
hookRunner.runPreRequest('test-id', testUserId, createTestContext()),
|
|
190
237
|
).rejects.toThrow('Hook error');
|
|
191
238
|
|
|
192
239
|
expect(mockLogger.error).toHaveBeenCalled();
|
|
193
240
|
});
|
|
194
241
|
|
|
242
|
+
it('should throw non-Error from pre-hook', async () => {
|
|
243
|
+
const errorHook: IDismissibleLifecycleHook = {
|
|
244
|
+
onBeforeRequest: jest.fn(async () => {
|
|
245
|
+
throw 'string error';
|
|
246
|
+
}),
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
hookRunner = new HookRunner([errorHook], mockLogger);
|
|
250
|
+
|
|
251
|
+
await expect(
|
|
252
|
+
hookRunner.runPreRequest('test-id', testUserId, createTestContext()),
|
|
253
|
+
).rejects.toBe('string error');
|
|
254
|
+
|
|
255
|
+
expect(mockLogger.error).toHaveBeenCalled();
|
|
256
|
+
});
|
|
257
|
+
|
|
195
258
|
it('should log but not throw errors from post-hooks', async () => {
|
|
196
|
-
const errorHook: IDismissibleLifecycleHook
|
|
197
|
-
|
|
259
|
+
const errorHook: IDismissibleLifecycleHook = {
|
|
260
|
+
onAfterRequest: jest.fn(async () => {
|
|
198
261
|
throw new Error('Post-hook error');
|
|
199
262
|
}),
|
|
200
263
|
};
|
|
@@ -202,7 +265,23 @@ describe('HookRunner', () => {
|
|
|
202
265
|
hookRunner = new HookRunner([errorHook], mockLogger);
|
|
203
266
|
|
|
204
267
|
await expect(
|
|
205
|
-
hookRunner.
|
|
268
|
+
hookRunner.runPostRequest('test-id', createTestItem(), testUserId, createTestContext()),
|
|
269
|
+
).resolves.not.toThrow();
|
|
270
|
+
|
|
271
|
+
expect(mockLogger.error).toHaveBeenCalled();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('should log but not throw non-Error from post-hooks', async () => {
|
|
275
|
+
const errorHook: IDismissibleLifecycleHook = {
|
|
276
|
+
onAfterRequest: jest.fn(async () => {
|
|
277
|
+
throw 'string post-hook error';
|
|
278
|
+
}),
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
hookRunner = new HookRunner([errorHook], mockLogger);
|
|
282
|
+
|
|
283
|
+
await expect(
|
|
284
|
+
hookRunner.runPostRequest('test-id', createTestItem(), testUserId, createTestContext()),
|
|
206
285
|
).resolves.not.toThrow();
|
|
207
286
|
|
|
208
287
|
expect(mockLogger.error).toHaveBeenCalled();
|
|
@@ -222,6 +301,17 @@ describe('HookRunner', () => {
|
|
|
222
301
|
).toThrow(ForbiddenException);
|
|
223
302
|
});
|
|
224
303
|
|
|
304
|
+
it('should throw ForbiddenException with default reason when blocked without reason', () => {
|
|
305
|
+
expect(() =>
|
|
306
|
+
HookRunner.throwIfBlocked({
|
|
307
|
+
proceed: false,
|
|
308
|
+
id: 'test',
|
|
309
|
+
userId: testUserId,
|
|
310
|
+
context: createTestContext(),
|
|
311
|
+
}),
|
|
312
|
+
).toThrow('Operation blocked by lifecycle hook');
|
|
313
|
+
});
|
|
314
|
+
|
|
225
315
|
it('should not throw when not blocked', () => {
|
|
226
316
|
expect(() =>
|
|
227
317
|
HookRunner.throwIfBlocked({
|
|
@@ -234,14 +324,338 @@ describe('HookRunner', () => {
|
|
|
234
324
|
});
|
|
235
325
|
});
|
|
236
326
|
|
|
327
|
+
describe('runPreGet and runPostGet', () => {
|
|
328
|
+
it('should pass item to onBeforeGet hook', async () => {
|
|
329
|
+
const item = createTestItem();
|
|
330
|
+
const context = createTestContext();
|
|
331
|
+
|
|
332
|
+
const hook: IDismissibleLifecycleHook = {
|
|
333
|
+
onBeforeGet: jest.fn().mockResolvedValue({ proceed: true }),
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
hookRunner = new HookRunner([hook], mockLogger);
|
|
337
|
+
await hookRunner.runPreGet('test-id', item, testUserId, context);
|
|
338
|
+
|
|
339
|
+
expect(hook.onBeforeGet).toHaveBeenCalledWith('test-id', item, testUserId, context);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('should block operation when onBeforeGet returns proceed: false', async () => {
|
|
343
|
+
const item = createTestItem();
|
|
344
|
+
const context = createTestContext();
|
|
345
|
+
|
|
346
|
+
const blockingHook: IDismissibleLifecycleHook = {
|
|
347
|
+
onBeforeGet: jest.fn().mockResolvedValue({
|
|
348
|
+
proceed: false,
|
|
349
|
+
reason: 'Item is in invalid state',
|
|
350
|
+
}),
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
hookRunner = new HookRunner([blockingHook], mockLogger);
|
|
354
|
+
const result = await hookRunner.runPreGet('test-id', item, testUserId, context);
|
|
355
|
+
|
|
356
|
+
expect(result.proceed).toBe(false);
|
|
357
|
+
expect(result.reason).toBe('Item is in invalid state');
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('should run onAfterGet hook', async () => {
|
|
361
|
+
const item = createTestItem();
|
|
362
|
+
const context = createTestContext();
|
|
363
|
+
|
|
364
|
+
const hook: IDismissibleLifecycleHook = {
|
|
365
|
+
onAfterGet: jest.fn(),
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
hookRunner = new HookRunner([hook], mockLogger);
|
|
369
|
+
await hookRunner.runPostGet('test-id', item, testUserId, context);
|
|
370
|
+
|
|
371
|
+
expect(hook.onAfterGet).toHaveBeenCalledWith('test-id', item, testUserId, context);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('should apply mutations from onBeforeGet hook', async () => {
|
|
375
|
+
const item = createTestItem();
|
|
376
|
+
const context = createTestContext();
|
|
377
|
+
|
|
378
|
+
const mutatingHook: IDismissibleLifecycleHook = {
|
|
379
|
+
onBeforeGet: jest.fn().mockResolvedValue({
|
|
380
|
+
proceed: true,
|
|
381
|
+
mutations: {
|
|
382
|
+
id: 'mutated-id',
|
|
383
|
+
},
|
|
384
|
+
}),
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
hookRunner = new HookRunner([mutatingHook], mockLogger);
|
|
388
|
+
const result = await hookRunner.runPreGet('original-id', item, testUserId, context);
|
|
389
|
+
|
|
390
|
+
expect(result.id).toBe('mutated-id');
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('should apply userId mutation from onBeforeGet hook', async () => {
|
|
394
|
+
const item = createTestItem();
|
|
395
|
+
const context = createTestContext();
|
|
396
|
+
|
|
397
|
+
const mutatingHook: IDismissibleLifecycleHook = {
|
|
398
|
+
onBeforeGet: jest.fn().mockResolvedValue({
|
|
399
|
+
proceed: true,
|
|
400
|
+
mutations: {
|
|
401
|
+
userId: 'mutated-user-id',
|
|
402
|
+
},
|
|
403
|
+
}),
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
hookRunner = new HookRunner([mutatingHook], mockLogger);
|
|
407
|
+
const result = await hookRunner.runPreGet('test-id', item, testUserId, context);
|
|
408
|
+
|
|
409
|
+
expect(result.userId).toBe('mutated-user-id');
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it('should apply context mutation from onBeforeGet hook', async () => {
|
|
413
|
+
const item = createTestItem();
|
|
414
|
+
const context = createTestContext();
|
|
415
|
+
|
|
416
|
+
const mutatingHook: IDismissibleLifecycleHook = {
|
|
417
|
+
onBeforeGet: jest.fn().mockResolvedValue({
|
|
418
|
+
proceed: true,
|
|
419
|
+
mutations: {
|
|
420
|
+
context: { headers: { authorization: 'Bearer custom-token' } },
|
|
421
|
+
},
|
|
422
|
+
}),
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
hookRunner = new HookRunner([mutatingHook], mockLogger);
|
|
426
|
+
const result = await hookRunner.runPreGet('test-id', item, testUserId, context);
|
|
427
|
+
|
|
428
|
+
expect(result.context).toEqual({
|
|
429
|
+
...context,
|
|
430
|
+
headers: { authorization: 'Bearer custom-token' },
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it('should ignore context mutation when no context provided to onBeforeGet', async () => {
|
|
435
|
+
const item = createTestItem();
|
|
436
|
+
|
|
437
|
+
const mutatingHook: IDismissibleLifecycleHook = {
|
|
438
|
+
onBeforeGet: jest.fn().mockResolvedValue({
|
|
439
|
+
proceed: true,
|
|
440
|
+
mutations: {
|
|
441
|
+
context: { headers: { authorization: 'Bearer token' } },
|
|
442
|
+
},
|
|
443
|
+
}),
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
hookRunner = new HookRunner([mutatingHook], mockLogger);
|
|
447
|
+
const result = await hookRunner.runPreGet('test-id', item, testUserId, undefined);
|
|
448
|
+
|
|
449
|
+
expect(result.context).toBeUndefined();
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('should throw error from onBeforeGet hook', async () => {
|
|
453
|
+
const item = createTestItem();
|
|
454
|
+
const context = createTestContext();
|
|
455
|
+
|
|
456
|
+
const errorHook: IDismissibleLifecycleHook = {
|
|
457
|
+
onBeforeGet: jest.fn().mockRejectedValue(new Error('Get hook error')),
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
hookRunner = new HookRunner([errorHook], mockLogger);
|
|
461
|
+
|
|
462
|
+
await expect(hookRunner.runPreGet('test-id', item, testUserId, context)).rejects.toThrow(
|
|
463
|
+
'Get hook error',
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
expect(mockLogger.error).toHaveBeenCalled();
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it('should throw non-Error from onBeforeGet hook', async () => {
|
|
470
|
+
const item = createTestItem();
|
|
471
|
+
const context = createTestContext();
|
|
472
|
+
|
|
473
|
+
const errorHook: IDismissibleLifecycleHook = {
|
|
474
|
+
onBeforeGet: jest.fn().mockRejectedValue('string error'),
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
hookRunner = new HookRunner([errorHook], mockLogger);
|
|
478
|
+
|
|
479
|
+
await expect(hookRunner.runPreGet('test-id', item, testUserId, context)).rejects.toBe(
|
|
480
|
+
'string error',
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
expect(mockLogger.error).toHaveBeenCalled();
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
describe('runPreCreate, runPreDismiss, runPreRestore mutations', () => {
|
|
488
|
+
it('should apply mutations from onBeforeCreate hook', async () => {
|
|
489
|
+
const context = createTestContext();
|
|
490
|
+
|
|
491
|
+
const mutatingHook: IDismissibleLifecycleHook = {
|
|
492
|
+
onBeforeCreate: jest.fn().mockResolvedValue({
|
|
493
|
+
proceed: true,
|
|
494
|
+
mutations: {
|
|
495
|
+
id: 'mutated-create-id',
|
|
496
|
+
userId: 'mutated-create-user',
|
|
497
|
+
},
|
|
498
|
+
}),
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
hookRunner = new HookRunner([mutatingHook], mockLogger);
|
|
502
|
+
const result = await hookRunner.runPreCreate('original-id', testUserId, context);
|
|
503
|
+
|
|
504
|
+
expect(result.id).toBe('mutated-create-id');
|
|
505
|
+
expect(result.userId).toBe('mutated-create-user');
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it('should apply context mutation from onBeforeCreate when context is provided', async () => {
|
|
509
|
+
const context = createTestContext();
|
|
510
|
+
|
|
511
|
+
const mutatingHook: IDismissibleLifecycleHook = {
|
|
512
|
+
onBeforeCreate: jest.fn().mockResolvedValue({
|
|
513
|
+
proceed: true,
|
|
514
|
+
mutations: {
|
|
515
|
+
context: { headers: { authorization: 'Bearer create-token' } },
|
|
516
|
+
},
|
|
517
|
+
}),
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
hookRunner = new HookRunner([mutatingHook], mockLogger);
|
|
521
|
+
const result = await hookRunner.runPreCreate('test-id', testUserId, context);
|
|
522
|
+
|
|
523
|
+
expect(result.context).toEqual({
|
|
524
|
+
...context,
|
|
525
|
+
headers: { authorization: 'Bearer create-token' },
|
|
526
|
+
});
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it('should block operation when onBeforeDismiss returns proceed: false', async () => {
|
|
530
|
+
const blockingHook: IDismissibleLifecycleHook = {
|
|
531
|
+
onBeforeDismiss: jest.fn().mockResolvedValue({
|
|
532
|
+
proceed: false,
|
|
533
|
+
reason: 'Cannot dismiss this item',
|
|
534
|
+
}),
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
hookRunner = new HookRunner([blockingHook], mockLogger);
|
|
538
|
+
const result = await hookRunner.runPreDismiss('test-id', testUserId, createTestContext());
|
|
539
|
+
|
|
540
|
+
expect(result.proceed).toBe(false);
|
|
541
|
+
expect(result.reason).toBe('Cannot dismiss this item');
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it('should apply mutations from onBeforeRestore hook', async () => {
|
|
545
|
+
const context = createTestContext();
|
|
546
|
+
|
|
547
|
+
const mutatingHook: IDismissibleLifecycleHook = {
|
|
548
|
+
onBeforeRestore: jest.fn().mockResolvedValue({
|
|
549
|
+
proceed: true,
|
|
550
|
+
mutations: {
|
|
551
|
+
id: 'mutated-restore-id',
|
|
552
|
+
},
|
|
553
|
+
}),
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
hookRunner = new HookRunner([mutatingHook], mockLogger);
|
|
557
|
+
const result = await hookRunner.runPreRestore('original-id', testUserId, context);
|
|
558
|
+
|
|
559
|
+
expect(result.id).toBe('mutated-restore-id');
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it('should throw error from onBeforeCreate hook', async () => {
|
|
563
|
+
const errorHook: IDismissibleLifecycleHook = {
|
|
564
|
+
onBeforeCreate: jest.fn().mockRejectedValue(new Error('Create hook error')),
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
hookRunner = new HookRunner([errorHook], mockLogger);
|
|
568
|
+
|
|
569
|
+
await expect(
|
|
570
|
+
hookRunner.runPreCreate('test-id', testUserId, createTestContext()),
|
|
571
|
+
).rejects.toThrow('Create hook error');
|
|
572
|
+
|
|
573
|
+
expect(mockLogger.error).toHaveBeenCalled();
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it('should throw non-Error from onBeforeDismiss hook', async () => {
|
|
577
|
+
const errorHook: IDismissibleLifecycleHook = {
|
|
578
|
+
onBeforeDismiss: jest.fn().mockRejectedValue('dismiss string error'),
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
hookRunner = new HookRunner([errorHook], mockLogger);
|
|
582
|
+
|
|
583
|
+
await expect(
|
|
584
|
+
hookRunner.runPreDismiss('test-id', testUserId, createTestContext()),
|
|
585
|
+
).rejects.toBe('dismiss string error');
|
|
586
|
+
|
|
587
|
+
expect(mockLogger.error).toHaveBeenCalled();
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
describe('post-hook error handling', () => {
|
|
592
|
+
it('should log but not throw Error from onAfterCreate', async () => {
|
|
593
|
+
const errorHook: IDismissibleLifecycleHook = {
|
|
594
|
+
onAfterCreate: jest.fn().mockRejectedValue(new Error('After create error')),
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
hookRunner = new HookRunner([errorHook], mockLogger);
|
|
598
|
+
|
|
599
|
+
await expect(
|
|
600
|
+
hookRunner.runPostCreate('test-id', createTestItem(), testUserId, createTestContext()),
|
|
601
|
+
).resolves.not.toThrow();
|
|
602
|
+
|
|
603
|
+
expect(mockLogger.error).toHaveBeenCalled();
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
it('should log but not throw non-Error from onAfterDismiss', async () => {
|
|
607
|
+
const errorHook: IDismissibleLifecycleHook = {
|
|
608
|
+
onAfterDismiss: jest.fn().mockRejectedValue('dismiss post error'),
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
hookRunner = new HookRunner([errorHook], mockLogger);
|
|
612
|
+
|
|
613
|
+
await expect(
|
|
614
|
+
hookRunner.runPostDismiss('test-id', createTestItem(), testUserId, createTestContext()),
|
|
615
|
+
).resolves.not.toThrow();
|
|
616
|
+
|
|
617
|
+
expect(mockLogger.error).toHaveBeenCalled();
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
it('should log but not throw Error from onAfterRestore', async () => {
|
|
621
|
+
const errorHook: IDismissibleLifecycleHook = {
|
|
622
|
+
onAfterRestore: jest.fn().mockRejectedValue(new Error('After restore error')),
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
hookRunner = new HookRunner([errorHook], mockLogger);
|
|
626
|
+
|
|
627
|
+
await expect(
|
|
628
|
+
hookRunner.runPostRestore('test-id', createTestItem(), testUserId, createTestContext()),
|
|
629
|
+
).resolves.not.toThrow();
|
|
630
|
+
|
|
631
|
+
expect(mockLogger.error).toHaveBeenCalled();
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
it('should log but not throw non-Error from onAfterGet', async () => {
|
|
635
|
+
const errorHook: IDismissibleLifecycleHook = {
|
|
636
|
+
onAfterGet: jest.fn().mockRejectedValue('get post error'),
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
hookRunner = new HookRunner([errorHook], mockLogger);
|
|
640
|
+
|
|
641
|
+
await expect(
|
|
642
|
+
hookRunner.runPostGet('test-id', createTestItem(), testUserId, createTestContext()),
|
|
643
|
+
).resolves.not.toThrow();
|
|
644
|
+
|
|
645
|
+
expect(mockLogger.error).toHaveBeenCalled();
|
|
646
|
+
});
|
|
647
|
+
});
|
|
648
|
+
|
|
237
649
|
describe('all hook methods', () => {
|
|
238
|
-
let allMethodsHook: IDismissibleLifecycleHook
|
|
650
|
+
let allMethodsHook: IDismissibleLifecycleHook;
|
|
239
651
|
|
|
240
652
|
beforeEach(() => {
|
|
241
653
|
allMethodsHook = {
|
|
242
654
|
priority: 0,
|
|
243
|
-
|
|
244
|
-
|
|
655
|
+
onBeforeRequest: jest.fn().mockResolvedValue({ proceed: true }),
|
|
656
|
+
onAfterRequest: jest.fn(),
|
|
657
|
+
onBeforeGet: jest.fn().mockResolvedValue({ proceed: true }),
|
|
658
|
+
onAfterGet: jest.fn(),
|
|
245
659
|
onBeforeCreate: jest.fn().mockResolvedValue({ proceed: true }),
|
|
246
660
|
onAfterCreate: jest.fn(),
|
|
247
661
|
onBeforeDismiss: jest.fn().mockResolvedValue({ proceed: true }),
|
|
@@ -253,6 +667,33 @@ describe('HookRunner', () => {
|
|
|
253
667
|
hookRunner = new HookRunner([allMethodsHook], mockLogger);
|
|
254
668
|
});
|
|
255
669
|
|
|
670
|
+
it('should run pre and post request hooks', async () => {
|
|
671
|
+
const context = createTestContext();
|
|
672
|
+
const item = createTestItem();
|
|
673
|
+
|
|
674
|
+
await hookRunner.runPreRequest('test-id', testUserId, context);
|
|
675
|
+
await hookRunner.runPostRequest('test-id', item, testUserId, context);
|
|
676
|
+
|
|
677
|
+
expect(allMethodsHook.onBeforeRequest).toHaveBeenCalledWith('test-id', testUserId, context);
|
|
678
|
+
expect(allMethodsHook.onAfterRequest).toHaveBeenCalledWith(
|
|
679
|
+
'test-id',
|
|
680
|
+
item,
|
|
681
|
+
testUserId,
|
|
682
|
+
context,
|
|
683
|
+
);
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
it('should run pre and post get hooks', async () => {
|
|
687
|
+
const context = createTestContext();
|
|
688
|
+
const item = createTestItem();
|
|
689
|
+
|
|
690
|
+
await hookRunner.runPreGet('test-id', item, testUserId, context);
|
|
691
|
+
await hookRunner.runPostGet('test-id', item, testUserId, context);
|
|
692
|
+
|
|
693
|
+
expect(allMethodsHook.onBeforeGet).toHaveBeenCalledWith('test-id', item, testUserId, context);
|
|
694
|
+
expect(allMethodsHook.onAfterGet).toHaveBeenCalledWith('test-id', item, testUserId, context);
|
|
695
|
+
});
|
|
696
|
+
|
|
256
697
|
it('should run pre and post create hooks', async () => {
|
|
257
698
|
const context = createTestContext();
|
|
258
699
|
const item = createTestItem();
|