@dismissible/nestjs-dismissible 0.0.2-canary.8976e84.0 → 0.0.2-canary.d2f56d7.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 +51 -67
- package/package.json +4 -4
- 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 +3 -0
- package/src/api/use-cases/dismiss/dismiss.controller.spec.ts +1 -2
- package/src/api/use-cases/dismiss/dismiss.controller.ts +8 -8
- 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 +10 -56
- 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 +8 -8
- package/src/api/validation/index.ts +2 -0
- package/src/api/validation/param-validation.pipe.spec.ts +317 -0
- package/src/api/validation/param-validation.pipe.ts +42 -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 -24
- package/src/core/dismissible.service.spec.ts +111 -25
- package/src/core/dismissible.service.ts +115 -49
- package/src/core/hook-runner.service.spec.ts +486 -53
- package/src/core/hook-runner.service.ts +144 -18
- package/src/core/index.ts +0 -1
- package/src/core/lifecycle-hook.interface.ts +56 -10
- package/src/core/service-responses.interface.ts +9 -9
- package/src/dismissible.module.integration.spec.ts +685 -0
- package/src/dismissible.module.ts +6 -10
- package/src/events/dismissible.events.ts +16 -39
- package/src/index.ts +1 -0
- package/src/request/request-context.decorator.ts +1 -0
- package/src/request/request-context.interface.ts +6 -0
- package/src/response/http-exception-filter.spec.ts +213 -0
- package/src/response/http-exception-filter.ts +3 -3
- package/src/testing/factories.ts +5 -8
- 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
|
@@ -2,12 +2,11 @@ import { mock } from 'ts-jest-mocker';
|
|
|
2
2
|
import { ForbiddenException } from '@nestjs/common';
|
|
3
3
|
import { HookRunner } from './hook-runner.service';
|
|
4
4
|
import { IDismissibleLifecycleHook } from './lifecycle-hook.interface';
|
|
5
|
-
import { BaseMetadata } from '@dismissible/nestjs-dismissible-item';
|
|
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,99 @@ 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: { authorizationHeader: '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({ ...context, authorizationHeader: 'Bearer mutated-token' });
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should ignore context mutation when no context provided', async () => {
|
|
182
|
+
const mutatingHook: IDismissibleLifecycleHook = {
|
|
183
|
+
onBeforeRequest: jest.fn(async () => ({
|
|
184
|
+
proceed: true,
|
|
185
|
+
mutations: {
|
|
186
|
+
context: { authorizationHeader: 'Bearer token' },
|
|
187
|
+
},
|
|
188
|
+
})),
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
hookRunner = new HookRunner([mutatingHook], mockLogger);
|
|
192
|
+
const result = await hookRunner.runPreRequest('test-id', testUserId, undefined);
|
|
193
|
+
|
|
194
|
+
expect(result.context).toBeUndefined();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should handle hooks with default priority (undefined)', async () => {
|
|
198
|
+
const executionOrder: string[] = [];
|
|
199
|
+
|
|
200
|
+
const hook1: IDismissibleLifecycleHook = {
|
|
201
|
+
priority: 5,
|
|
202
|
+
onBeforeRequest: jest.fn(async () => {
|
|
203
|
+
executionOrder.push('priority-5');
|
|
204
|
+
return { proceed: true };
|
|
205
|
+
}),
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const hook2: IDismissibleLifecycleHook = {
|
|
209
|
+
// No priority - defaults to 0
|
|
210
|
+
onBeforeRequest: jest.fn(async () => {
|
|
211
|
+
executionOrder.push('priority-default');
|
|
212
|
+
return { proceed: true };
|
|
213
|
+
}),
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
hookRunner = new HookRunner([hook1, hook2], mockLogger);
|
|
217
|
+
await hookRunner.runPreRequest('test-id', testUserId, createTestContext());
|
|
218
|
+
|
|
219
|
+
expect(executionOrder).toEqual(['priority-default', 'priority-5']);
|
|
220
|
+
});
|
|
176
221
|
});
|
|
177
222
|
|
|
178
223
|
describe('error handling', () => {
|
|
179
224
|
it('should throw error from pre-hook', async () => {
|
|
180
|
-
const errorHook: IDismissibleLifecycleHook
|
|
181
|
-
|
|
225
|
+
const errorHook: IDismissibleLifecycleHook = {
|
|
226
|
+
onBeforeRequest: jest.fn(async () => {
|
|
182
227
|
throw new Error('Hook error');
|
|
183
228
|
}),
|
|
184
229
|
};
|
|
@@ -186,15 +231,31 @@ describe('HookRunner', () => {
|
|
|
186
231
|
hookRunner = new HookRunner([errorHook], mockLogger);
|
|
187
232
|
|
|
188
233
|
await expect(
|
|
189
|
-
hookRunner.
|
|
234
|
+
hookRunner.runPreRequest('test-id', testUserId, createTestContext()),
|
|
190
235
|
).rejects.toThrow('Hook error');
|
|
191
236
|
|
|
192
237
|
expect(mockLogger.error).toHaveBeenCalled();
|
|
193
238
|
});
|
|
194
239
|
|
|
240
|
+
it('should throw non-Error from pre-hook', async () => {
|
|
241
|
+
const errorHook: IDismissibleLifecycleHook = {
|
|
242
|
+
onBeforeRequest: jest.fn(async () => {
|
|
243
|
+
throw 'string error';
|
|
244
|
+
}),
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
hookRunner = new HookRunner([errorHook], mockLogger);
|
|
248
|
+
|
|
249
|
+
await expect(
|
|
250
|
+
hookRunner.runPreRequest('test-id', testUserId, createTestContext()),
|
|
251
|
+
).rejects.toBe('string error');
|
|
252
|
+
|
|
253
|
+
expect(mockLogger.error).toHaveBeenCalled();
|
|
254
|
+
});
|
|
255
|
+
|
|
195
256
|
it('should log but not throw errors from post-hooks', async () => {
|
|
196
|
-
const errorHook: IDismissibleLifecycleHook
|
|
197
|
-
|
|
257
|
+
const errorHook: IDismissibleLifecycleHook = {
|
|
258
|
+
onAfterRequest: jest.fn(async () => {
|
|
198
259
|
throw new Error('Post-hook error');
|
|
199
260
|
}),
|
|
200
261
|
};
|
|
@@ -202,7 +263,23 @@ describe('HookRunner', () => {
|
|
|
202
263
|
hookRunner = new HookRunner([errorHook], mockLogger);
|
|
203
264
|
|
|
204
265
|
await expect(
|
|
205
|
-
hookRunner.
|
|
266
|
+
hookRunner.runPostRequest('test-id', createTestItem(), testUserId, createTestContext()),
|
|
267
|
+
).resolves.not.toThrow();
|
|
268
|
+
|
|
269
|
+
expect(mockLogger.error).toHaveBeenCalled();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('should log but not throw non-Error from post-hooks', async () => {
|
|
273
|
+
const errorHook: IDismissibleLifecycleHook = {
|
|
274
|
+
onAfterRequest: jest.fn(async () => {
|
|
275
|
+
throw 'string post-hook error';
|
|
276
|
+
}),
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
hookRunner = new HookRunner([errorHook], mockLogger);
|
|
280
|
+
|
|
281
|
+
await expect(
|
|
282
|
+
hookRunner.runPostRequest('test-id', createTestItem(), testUserId, createTestContext()),
|
|
206
283
|
).resolves.not.toThrow();
|
|
207
284
|
|
|
208
285
|
expect(mockLogger.error).toHaveBeenCalled();
|
|
@@ -222,6 +299,17 @@ describe('HookRunner', () => {
|
|
|
222
299
|
).toThrow(ForbiddenException);
|
|
223
300
|
});
|
|
224
301
|
|
|
302
|
+
it('should throw ForbiddenException with default reason when blocked without reason', () => {
|
|
303
|
+
expect(() =>
|
|
304
|
+
HookRunner.throwIfBlocked({
|
|
305
|
+
proceed: false,
|
|
306
|
+
id: 'test',
|
|
307
|
+
userId: testUserId,
|
|
308
|
+
context: createTestContext(),
|
|
309
|
+
}),
|
|
310
|
+
).toThrow('Operation blocked by lifecycle hook');
|
|
311
|
+
});
|
|
312
|
+
|
|
225
313
|
it('should not throw when not blocked', () => {
|
|
226
314
|
expect(() =>
|
|
227
315
|
HookRunner.throwIfBlocked({
|
|
@@ -234,14 +322,332 @@ describe('HookRunner', () => {
|
|
|
234
322
|
});
|
|
235
323
|
});
|
|
236
324
|
|
|
325
|
+
describe('runPreGet and runPostGet', () => {
|
|
326
|
+
it('should pass item to onBeforeGet hook', async () => {
|
|
327
|
+
const item = createTestItem();
|
|
328
|
+
const context = createTestContext();
|
|
329
|
+
|
|
330
|
+
const hook: IDismissibleLifecycleHook = {
|
|
331
|
+
onBeforeGet: jest.fn().mockResolvedValue({ proceed: true }),
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
hookRunner = new HookRunner([hook], mockLogger);
|
|
335
|
+
await hookRunner.runPreGet('test-id', item, testUserId, context);
|
|
336
|
+
|
|
337
|
+
expect(hook.onBeforeGet).toHaveBeenCalledWith('test-id', item, testUserId, context);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('should block operation when onBeforeGet returns proceed: false', async () => {
|
|
341
|
+
const item = createTestItem();
|
|
342
|
+
const context = createTestContext();
|
|
343
|
+
|
|
344
|
+
const blockingHook: IDismissibleLifecycleHook = {
|
|
345
|
+
onBeforeGet: jest.fn().mockResolvedValue({
|
|
346
|
+
proceed: false,
|
|
347
|
+
reason: 'Item is in invalid state',
|
|
348
|
+
}),
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
hookRunner = new HookRunner([blockingHook], mockLogger);
|
|
352
|
+
const result = await hookRunner.runPreGet('test-id', item, testUserId, context);
|
|
353
|
+
|
|
354
|
+
expect(result.proceed).toBe(false);
|
|
355
|
+
expect(result.reason).toBe('Item is in invalid state');
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('should run onAfterGet hook', async () => {
|
|
359
|
+
const item = createTestItem();
|
|
360
|
+
const context = createTestContext();
|
|
361
|
+
|
|
362
|
+
const hook: IDismissibleLifecycleHook = {
|
|
363
|
+
onAfterGet: jest.fn(),
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
hookRunner = new HookRunner([hook], mockLogger);
|
|
367
|
+
await hookRunner.runPostGet('test-id', item, testUserId, context);
|
|
368
|
+
|
|
369
|
+
expect(hook.onAfterGet).toHaveBeenCalledWith('test-id', item, testUserId, context);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('should apply mutations from onBeforeGet hook', async () => {
|
|
373
|
+
const item = createTestItem();
|
|
374
|
+
const context = createTestContext();
|
|
375
|
+
|
|
376
|
+
const mutatingHook: IDismissibleLifecycleHook = {
|
|
377
|
+
onBeforeGet: jest.fn().mockResolvedValue({
|
|
378
|
+
proceed: true,
|
|
379
|
+
mutations: {
|
|
380
|
+
id: 'mutated-id',
|
|
381
|
+
},
|
|
382
|
+
}),
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
hookRunner = new HookRunner([mutatingHook], mockLogger);
|
|
386
|
+
const result = await hookRunner.runPreGet('original-id', item, testUserId, context);
|
|
387
|
+
|
|
388
|
+
expect(result.id).toBe('mutated-id');
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it('should apply userId mutation from onBeforeGet hook', async () => {
|
|
392
|
+
const item = createTestItem();
|
|
393
|
+
const context = createTestContext();
|
|
394
|
+
|
|
395
|
+
const mutatingHook: IDismissibleLifecycleHook = {
|
|
396
|
+
onBeforeGet: jest.fn().mockResolvedValue({
|
|
397
|
+
proceed: true,
|
|
398
|
+
mutations: {
|
|
399
|
+
userId: 'mutated-user-id',
|
|
400
|
+
},
|
|
401
|
+
}),
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
hookRunner = new HookRunner([mutatingHook], mockLogger);
|
|
405
|
+
const result = await hookRunner.runPreGet('test-id', item, testUserId, context);
|
|
406
|
+
|
|
407
|
+
expect(result.userId).toBe('mutated-user-id');
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it('should apply context mutation from onBeforeGet hook', async () => {
|
|
411
|
+
const item = createTestItem();
|
|
412
|
+
const context = createTestContext();
|
|
413
|
+
|
|
414
|
+
const mutatingHook: IDismissibleLifecycleHook = {
|
|
415
|
+
onBeforeGet: jest.fn().mockResolvedValue({
|
|
416
|
+
proceed: true,
|
|
417
|
+
mutations: {
|
|
418
|
+
context: { authorizationHeader: 'Bearer custom-token' },
|
|
419
|
+
},
|
|
420
|
+
}),
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
hookRunner = new HookRunner([mutatingHook], mockLogger);
|
|
424
|
+
const result = await hookRunner.runPreGet('test-id', item, testUserId, context);
|
|
425
|
+
|
|
426
|
+
expect(result.context).toEqual({ ...context, authorizationHeader: 'Bearer custom-token' });
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it('should ignore context mutation when no context provided to onBeforeGet', async () => {
|
|
430
|
+
const item = createTestItem();
|
|
431
|
+
|
|
432
|
+
const mutatingHook: IDismissibleLifecycleHook = {
|
|
433
|
+
onBeforeGet: jest.fn().mockResolvedValue({
|
|
434
|
+
proceed: true,
|
|
435
|
+
mutations: {
|
|
436
|
+
context: { authorizationHeader: 'Bearer token' },
|
|
437
|
+
},
|
|
438
|
+
}),
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
hookRunner = new HookRunner([mutatingHook], mockLogger);
|
|
442
|
+
const result = await hookRunner.runPreGet('test-id', item, testUserId, undefined);
|
|
443
|
+
|
|
444
|
+
expect(result.context).toBeUndefined();
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it('should throw error from onBeforeGet hook', async () => {
|
|
448
|
+
const item = createTestItem();
|
|
449
|
+
const context = createTestContext();
|
|
450
|
+
|
|
451
|
+
const errorHook: IDismissibleLifecycleHook = {
|
|
452
|
+
onBeforeGet: jest.fn().mockRejectedValue(new Error('Get hook error')),
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
hookRunner = new HookRunner([errorHook], mockLogger);
|
|
456
|
+
|
|
457
|
+
await expect(hookRunner.runPreGet('test-id', item, testUserId, context)).rejects.toThrow(
|
|
458
|
+
'Get hook error',
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
expect(mockLogger.error).toHaveBeenCalled();
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('should throw non-Error from onBeforeGet hook', async () => {
|
|
465
|
+
const item = createTestItem();
|
|
466
|
+
const context = createTestContext();
|
|
467
|
+
|
|
468
|
+
const errorHook: IDismissibleLifecycleHook = {
|
|
469
|
+
onBeforeGet: jest.fn().mockRejectedValue('string error'),
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
hookRunner = new HookRunner([errorHook], mockLogger);
|
|
473
|
+
|
|
474
|
+
await expect(hookRunner.runPreGet('test-id', item, testUserId, context)).rejects.toBe(
|
|
475
|
+
'string error',
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
expect(mockLogger.error).toHaveBeenCalled();
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
describe('runPreCreate, runPreDismiss, runPreRestore mutations', () => {
|
|
483
|
+
it('should apply mutations from onBeforeCreate hook', async () => {
|
|
484
|
+
const context = createTestContext();
|
|
485
|
+
|
|
486
|
+
const mutatingHook: IDismissibleLifecycleHook = {
|
|
487
|
+
onBeforeCreate: jest.fn().mockResolvedValue({
|
|
488
|
+
proceed: true,
|
|
489
|
+
mutations: {
|
|
490
|
+
id: 'mutated-create-id',
|
|
491
|
+
userId: 'mutated-create-user',
|
|
492
|
+
},
|
|
493
|
+
}),
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
hookRunner = new HookRunner([mutatingHook], mockLogger);
|
|
497
|
+
const result = await hookRunner.runPreCreate('original-id', testUserId, context);
|
|
498
|
+
|
|
499
|
+
expect(result.id).toBe('mutated-create-id');
|
|
500
|
+
expect(result.userId).toBe('mutated-create-user');
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
it('should apply context mutation from onBeforeCreate when context is provided', async () => {
|
|
504
|
+
const context = createTestContext();
|
|
505
|
+
|
|
506
|
+
const mutatingHook: IDismissibleLifecycleHook = {
|
|
507
|
+
onBeforeCreate: jest.fn().mockResolvedValue({
|
|
508
|
+
proceed: true,
|
|
509
|
+
mutations: {
|
|
510
|
+
context: { authorizationHeader: 'Bearer create-token' },
|
|
511
|
+
},
|
|
512
|
+
}),
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
hookRunner = new HookRunner([mutatingHook], mockLogger);
|
|
516
|
+
const result = await hookRunner.runPreCreate('test-id', testUserId, context);
|
|
517
|
+
|
|
518
|
+
expect(result.context).toEqual({ ...context, authorizationHeader: 'Bearer create-token' });
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it('should block operation when onBeforeDismiss returns proceed: false', async () => {
|
|
522
|
+
const blockingHook: IDismissibleLifecycleHook = {
|
|
523
|
+
onBeforeDismiss: jest.fn().mockResolvedValue({
|
|
524
|
+
proceed: false,
|
|
525
|
+
reason: 'Cannot dismiss this item',
|
|
526
|
+
}),
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
hookRunner = new HookRunner([blockingHook], mockLogger);
|
|
530
|
+
const result = await hookRunner.runPreDismiss('test-id', testUserId, createTestContext());
|
|
531
|
+
|
|
532
|
+
expect(result.proceed).toBe(false);
|
|
533
|
+
expect(result.reason).toBe('Cannot dismiss this item');
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it('should apply mutations from onBeforeRestore hook', async () => {
|
|
537
|
+
const context = createTestContext();
|
|
538
|
+
|
|
539
|
+
const mutatingHook: IDismissibleLifecycleHook = {
|
|
540
|
+
onBeforeRestore: jest.fn().mockResolvedValue({
|
|
541
|
+
proceed: true,
|
|
542
|
+
mutations: {
|
|
543
|
+
id: 'mutated-restore-id',
|
|
544
|
+
},
|
|
545
|
+
}),
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
hookRunner = new HookRunner([mutatingHook], mockLogger);
|
|
549
|
+
const result = await hookRunner.runPreRestore('original-id', testUserId, context);
|
|
550
|
+
|
|
551
|
+
expect(result.id).toBe('mutated-restore-id');
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
it('should throw error from onBeforeCreate hook', async () => {
|
|
555
|
+
const errorHook: IDismissibleLifecycleHook = {
|
|
556
|
+
onBeforeCreate: jest.fn().mockRejectedValue(new Error('Create hook error')),
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
hookRunner = new HookRunner([errorHook], mockLogger);
|
|
560
|
+
|
|
561
|
+
await expect(
|
|
562
|
+
hookRunner.runPreCreate('test-id', testUserId, createTestContext()),
|
|
563
|
+
).rejects.toThrow('Create hook error');
|
|
564
|
+
|
|
565
|
+
expect(mockLogger.error).toHaveBeenCalled();
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
it('should throw non-Error from onBeforeDismiss hook', async () => {
|
|
569
|
+
const errorHook: IDismissibleLifecycleHook = {
|
|
570
|
+
onBeforeDismiss: jest.fn().mockRejectedValue('dismiss string error'),
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
hookRunner = new HookRunner([errorHook], mockLogger);
|
|
574
|
+
|
|
575
|
+
await expect(
|
|
576
|
+
hookRunner.runPreDismiss('test-id', testUserId, createTestContext()),
|
|
577
|
+
).rejects.toBe('dismiss string error');
|
|
578
|
+
|
|
579
|
+
expect(mockLogger.error).toHaveBeenCalled();
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
describe('post-hook error handling', () => {
|
|
584
|
+
it('should log but not throw Error from onAfterCreate', async () => {
|
|
585
|
+
const errorHook: IDismissibleLifecycleHook = {
|
|
586
|
+
onAfterCreate: jest.fn().mockRejectedValue(new Error('After create error')),
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
hookRunner = new HookRunner([errorHook], mockLogger);
|
|
590
|
+
|
|
591
|
+
await expect(
|
|
592
|
+
hookRunner.runPostCreate('test-id', createTestItem(), testUserId, createTestContext()),
|
|
593
|
+
).resolves.not.toThrow();
|
|
594
|
+
|
|
595
|
+
expect(mockLogger.error).toHaveBeenCalled();
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
it('should log but not throw non-Error from onAfterDismiss', async () => {
|
|
599
|
+
const errorHook: IDismissibleLifecycleHook = {
|
|
600
|
+
onAfterDismiss: jest.fn().mockRejectedValue('dismiss post error'),
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
hookRunner = new HookRunner([errorHook], mockLogger);
|
|
604
|
+
|
|
605
|
+
await expect(
|
|
606
|
+
hookRunner.runPostDismiss('test-id', createTestItem(), testUserId, createTestContext()),
|
|
607
|
+
).resolves.not.toThrow();
|
|
608
|
+
|
|
609
|
+
expect(mockLogger.error).toHaveBeenCalled();
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
it('should log but not throw Error from onAfterRestore', async () => {
|
|
613
|
+
const errorHook: IDismissibleLifecycleHook = {
|
|
614
|
+
onAfterRestore: jest.fn().mockRejectedValue(new Error('After restore error')),
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
hookRunner = new HookRunner([errorHook], mockLogger);
|
|
618
|
+
|
|
619
|
+
await expect(
|
|
620
|
+
hookRunner.runPostRestore('test-id', createTestItem(), testUserId, createTestContext()),
|
|
621
|
+
).resolves.not.toThrow();
|
|
622
|
+
|
|
623
|
+
expect(mockLogger.error).toHaveBeenCalled();
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
it('should log but not throw non-Error from onAfterGet', async () => {
|
|
627
|
+
const errorHook: IDismissibleLifecycleHook = {
|
|
628
|
+
onAfterGet: jest.fn().mockRejectedValue('get post error'),
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
hookRunner = new HookRunner([errorHook], mockLogger);
|
|
632
|
+
|
|
633
|
+
await expect(
|
|
634
|
+
hookRunner.runPostGet('test-id', createTestItem(), testUserId, createTestContext()),
|
|
635
|
+
).resolves.not.toThrow();
|
|
636
|
+
|
|
637
|
+
expect(mockLogger.error).toHaveBeenCalled();
|
|
638
|
+
});
|
|
639
|
+
});
|
|
640
|
+
|
|
237
641
|
describe('all hook methods', () => {
|
|
238
|
-
let allMethodsHook: IDismissibleLifecycleHook
|
|
642
|
+
let allMethodsHook: IDismissibleLifecycleHook;
|
|
239
643
|
|
|
240
644
|
beforeEach(() => {
|
|
241
645
|
allMethodsHook = {
|
|
242
646
|
priority: 0,
|
|
243
|
-
|
|
244
|
-
|
|
647
|
+
onBeforeRequest: jest.fn().mockResolvedValue({ proceed: true }),
|
|
648
|
+
onAfterRequest: jest.fn(),
|
|
649
|
+
onBeforeGet: jest.fn().mockResolvedValue({ proceed: true }),
|
|
650
|
+
onAfterGet: jest.fn(),
|
|
245
651
|
onBeforeCreate: jest.fn().mockResolvedValue({ proceed: true }),
|
|
246
652
|
onAfterCreate: jest.fn(),
|
|
247
653
|
onBeforeDismiss: jest.fn().mockResolvedValue({ proceed: true }),
|
|
@@ -253,6 +659,33 @@ describe('HookRunner', () => {
|
|
|
253
659
|
hookRunner = new HookRunner([allMethodsHook], mockLogger);
|
|
254
660
|
});
|
|
255
661
|
|
|
662
|
+
it('should run pre and post request hooks', async () => {
|
|
663
|
+
const context = createTestContext();
|
|
664
|
+
const item = createTestItem();
|
|
665
|
+
|
|
666
|
+
await hookRunner.runPreRequest('test-id', testUserId, context);
|
|
667
|
+
await hookRunner.runPostRequest('test-id', item, testUserId, context);
|
|
668
|
+
|
|
669
|
+
expect(allMethodsHook.onBeforeRequest).toHaveBeenCalledWith('test-id', testUserId, context);
|
|
670
|
+
expect(allMethodsHook.onAfterRequest).toHaveBeenCalledWith(
|
|
671
|
+
'test-id',
|
|
672
|
+
item,
|
|
673
|
+
testUserId,
|
|
674
|
+
context,
|
|
675
|
+
);
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
it('should run pre and post get hooks', async () => {
|
|
679
|
+
const context = createTestContext();
|
|
680
|
+
const item = createTestItem();
|
|
681
|
+
|
|
682
|
+
await hookRunner.runPreGet('test-id', item, testUserId, context);
|
|
683
|
+
await hookRunner.runPostGet('test-id', item, testUserId, context);
|
|
684
|
+
|
|
685
|
+
expect(allMethodsHook.onBeforeGet).toHaveBeenCalledWith('test-id', item, testUserId, context);
|
|
686
|
+
expect(allMethodsHook.onAfterGet).toHaveBeenCalledWith('test-id', item, testUserId, context);
|
|
687
|
+
});
|
|
688
|
+
|
|
256
689
|
it('should run pre and post create hooks', async () => {
|
|
257
690
|
const context = createTestContext();
|
|
258
691
|
const item = createTestItem();
|