@dismissible/nestjs-api 0.0.2-canary.c91edbc.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.

Potentially problematic release.


This version of @dismissible/nestjs-api might be problematic. Click here for more details.

@@ -0,0 +1,353 @@
1
+ import { INestApplication } from '@nestjs/common';
2
+ import request from 'supertest';
3
+ import { join } from 'path';
4
+ import { createTestApp, cleanupTestData } from '../src/app-test.factory';
5
+ import { JwtAuthService, IJwtValidationResult } from '@dismissible/nestjs-jwt-auth-hook';
6
+
7
+ describe('JWT Authentication E2E', () => {
8
+ let app: INestApplication;
9
+ let mockValidateToken: jest.Mock;
10
+
11
+ // Helper to create a mock validation result
12
+ const createValidResult = (sub = 'test-user'): IJwtValidationResult => ({
13
+ valid: true,
14
+ payload: {
15
+ sub,
16
+ iss: 'https://auth.example.com',
17
+ aud: 'dismissible-api',
18
+ exp: Math.floor(Date.now() / 1000) + 3600,
19
+ iat: Math.floor(Date.now() / 1000),
20
+ },
21
+ });
22
+
23
+ const createInvalidResult = (error: string): IJwtValidationResult => ({
24
+ valid: false,
25
+ error,
26
+ });
27
+
28
+ beforeAll(async () => {
29
+ // Create the mock function before app initialization
30
+ mockValidateToken = jest.fn();
31
+
32
+ app = await createTestApp({
33
+ moduleOptions: {
34
+ // Use test-specific config directory with JWT auth enabled
35
+ configPath: join(__dirname, 'config-jwt-auth'),
36
+ },
37
+ customize: (builder) => {
38
+ // Override JwtAuthService to prevent real JWKS calls during onModuleInit
39
+ return builder.overrideProvider(JwtAuthService).useValue({
40
+ // Mock initializeJwksClient to do nothing
41
+ initializeJwksClient: jest.fn().mockResolvedValue(undefined),
42
+ // Use the real extractBearerToken logic
43
+ extractBearerToken: (authHeader: string | undefined) => {
44
+ if (!authHeader) return null;
45
+ const parts = authHeader.split(' ');
46
+ if (parts.length !== 2 || parts[0].toLowerCase() !== 'bearer') return null;
47
+ return parts[1];
48
+ },
49
+ // Use our mock for validateToken
50
+ validateToken: mockValidateToken,
51
+ });
52
+ },
53
+ });
54
+
55
+ await cleanupTestData(app);
56
+ });
57
+
58
+ afterAll(async () => {
59
+ if (app) {
60
+ await cleanupTestData(app);
61
+ await app.close();
62
+ }
63
+ });
64
+
65
+ beforeEach(() => {
66
+ // Reset mock before each test
67
+ mockValidateToken.mockReset();
68
+ });
69
+
70
+ describe('Successful authentication scenarios', () => {
71
+ it('should allow GET request with valid bearer token', async () => {
72
+ mockValidateToken.mockResolvedValue(createValidResult('auth-user-1'));
73
+
74
+ const response = await request(app.getHttpServer())
75
+ .get('/v1/users/auth-user-1/items/jwt-test-get')
76
+ .set('Authorization', 'Bearer valid-token-123')
77
+ .expect(200);
78
+
79
+ expect(response.body.data).toBeDefined();
80
+ expect(response.body.data.itemId).toBe('jwt-test-get');
81
+ expect(response.body.data.userId).toBe('auth-user-1');
82
+ expect(mockValidateToken).toHaveBeenCalledWith('valid-token-123');
83
+ });
84
+
85
+ it('should allow DELETE (dismiss) request with valid bearer token', async () => {
86
+ mockValidateToken.mockResolvedValue(createValidResult('auth-user-2'));
87
+
88
+ // First create the item
89
+ await request(app.getHttpServer())
90
+ .get('/v1/users/auth-user-2/items/jwt-test-dismiss')
91
+ .set('Authorization', 'Bearer valid-token-123')
92
+ .expect(200);
93
+
94
+ // Then dismiss it
95
+ const response = await request(app.getHttpServer())
96
+ .delete('/v1/users/auth-user-2/items/jwt-test-dismiss')
97
+ .set('Authorization', 'Bearer valid-token-123')
98
+ .expect(200);
99
+
100
+ expect(response.body.data).toBeDefined();
101
+ expect(response.body.data.dismissedAt).toBeDefined();
102
+ });
103
+
104
+ it('should allow POST (restore) request with valid bearer token', async () => {
105
+ mockValidateToken.mockResolvedValue(createValidResult('auth-user-3'));
106
+
107
+ // Create and dismiss the item
108
+ await request(app.getHttpServer())
109
+ .get('/v1/users/auth-user-3/items/jwt-test-restore')
110
+ .set('Authorization', 'Bearer valid-token-123')
111
+ .expect(200);
112
+
113
+ await request(app.getHttpServer())
114
+ .delete('/v1/users/auth-user-3/items/jwt-test-restore')
115
+ .set('Authorization', 'Bearer valid-token-123')
116
+ .expect(200);
117
+
118
+ // Restore it
119
+ const response = await request(app.getHttpServer())
120
+ .post('/v1/users/auth-user-3/items/jwt-test-restore')
121
+ .set('Authorization', 'Bearer valid-token-123')
122
+ .expect(201);
123
+
124
+ expect(response.body.data).toBeDefined();
125
+ expect(response.body.data.dismissedAt).toBeUndefined();
126
+ });
127
+
128
+ it('should handle lowercase bearer prefix', async () => {
129
+ mockValidateToken.mockResolvedValue(createValidResult('auth-user-4'));
130
+
131
+ const response = await request(app.getHttpServer())
132
+ .get('/v1/users/auth-user-4/items/jwt-test-lowercase')
133
+ .set('Authorization', 'bearer lowercase-token')
134
+ .expect(200);
135
+
136
+ expect(response.body.data).toBeDefined();
137
+ expect(mockValidateToken).toHaveBeenCalledWith('lowercase-token');
138
+ });
139
+ });
140
+
141
+ describe('Failed authentication scenarios', () => {
142
+ it('should reject request without Authorization header', async () => {
143
+ const response = await request(app.getHttpServer())
144
+ .get('/v1/users/noauth-user/items/jwt-test-noheader')
145
+ .expect(401);
146
+
147
+ expect(response.body.error).toBeDefined();
148
+ expect(response.body.error.message).toContain('Missing or invalid bearer token');
149
+ expect(mockValidateToken).not.toHaveBeenCalled();
150
+ });
151
+
152
+ it('should reject request with empty Authorization header', async () => {
153
+ const response = await request(app.getHttpServer())
154
+ .get('/v1/users/noauth-user/items/jwt-test-empty')
155
+ .set('Authorization', '')
156
+ .expect(401);
157
+
158
+ expect(response.body.error).toBeDefined();
159
+ expect(response.body.error.message).toContain('Missing or invalid bearer token');
160
+ });
161
+
162
+ it('should reject request with non-Bearer auth scheme', async () => {
163
+ const response = await request(app.getHttpServer())
164
+ .get('/v1/users/noauth-user/items/jwt-test-basic')
165
+ .set('Authorization', 'Basic dXNlcjpwYXNz')
166
+ .expect(401);
167
+
168
+ expect(response.body.error).toBeDefined();
169
+ expect(response.body.error.message).toContain('Missing or invalid bearer token');
170
+ });
171
+
172
+ it('should reject request with Bearer but no token', async () => {
173
+ const response = await request(app.getHttpServer())
174
+ .get('/v1/users/noauth-user/items/jwt-test-notoken')
175
+ .set('Authorization', 'Bearer')
176
+ .expect(401);
177
+
178
+ expect(response.body.error).toBeDefined();
179
+ expect(response.body.error.message).toContain('Missing or invalid bearer token');
180
+ });
181
+
182
+ it('should reject request with expired token', async () => {
183
+ mockValidateToken.mockResolvedValue(createInvalidResult('jwt expired'));
184
+
185
+ const response = await request(app.getHttpServer())
186
+ .get('/v1/users/expired-user/items/jwt-test-expired')
187
+ .set('Authorization', 'Bearer expired-token')
188
+ .expect(401);
189
+
190
+ expect(response.body.error).toBeDefined();
191
+ expect(response.body.error.message).toContain('jwt expired');
192
+ expect(mockValidateToken).toHaveBeenCalledWith('expired-token');
193
+ });
194
+
195
+ it('should reject request with invalid token signature', async () => {
196
+ mockValidateToken.mockResolvedValue(createInvalidResult('invalid signature'));
197
+
198
+ const response = await request(app.getHttpServer())
199
+ .get('/v1/users/invalid-user/items/jwt-test-invalid-sig')
200
+ .set('Authorization', 'Bearer tampered-token')
201
+ .expect(401);
202
+
203
+ expect(response.body.error).toBeDefined();
204
+ expect(response.body.error.message).toContain('invalid signature');
205
+ });
206
+
207
+ it('should reject request with malformed token', async () => {
208
+ mockValidateToken.mockResolvedValue(createInvalidResult('Invalid token format'));
209
+
210
+ const response = await request(app.getHttpServer())
211
+ .get('/v1/users/malformed-user/items/jwt-test-malformed')
212
+ .set('Authorization', 'Bearer not.a.valid.jwt.token')
213
+ .expect(401);
214
+
215
+ expect(response.body.error).toBeDefined();
216
+ expect(response.body.error.message).toContain('Invalid token format');
217
+ });
218
+
219
+ it('should reject request when token is missing key ID (kid)', async () => {
220
+ mockValidateToken.mockResolvedValue(createInvalidResult('Token missing key ID (kid)'));
221
+
222
+ const response = await request(app.getHttpServer())
223
+ .get('/v1/users/nokid-user/items/jwt-test-nokid')
224
+ .set('Authorization', 'Bearer token-without-kid')
225
+ .expect(401);
226
+
227
+ expect(response.body.error).toBeDefined();
228
+ expect(response.body.error.message).toContain('Token missing key ID');
229
+ });
230
+
231
+ it('should reject request when signing key cannot be found', async () => {
232
+ mockValidateToken.mockResolvedValue(createInvalidResult('Unable to find signing key'));
233
+
234
+ const response = await request(app.getHttpServer())
235
+ .get('/v1/users/nokey-user/items/jwt-test-nokey')
236
+ .set('Authorization', 'Bearer token-with-unknown-kid')
237
+ .expect(401);
238
+
239
+ expect(response.body.error).toBeDefined();
240
+ expect(response.body.error.message).toContain('Unable to find signing key');
241
+ });
242
+
243
+ it('should reject dismiss request without valid token', async () => {
244
+ // First create the item with a valid token
245
+ mockValidateToken.mockResolvedValue(createValidResult('dismiss-noauth-user'));
246
+
247
+ await request(app.getHttpServer())
248
+ .get('/v1/users/dismiss-noauth-user/items/jwt-test-dismiss-fail')
249
+ .set('Authorization', 'Bearer valid-token')
250
+ .expect(200);
251
+
252
+ // Reset mock and try to dismiss without token
253
+ mockValidateToken.mockReset();
254
+
255
+ const response = await request(app.getHttpServer())
256
+ .delete('/v1/users/dismiss-noauth-user/items/jwt-test-dismiss-fail')
257
+ .expect(401);
258
+
259
+ expect(response.body.error).toBeDefined();
260
+ expect(response.body.error.message).toContain('Missing or invalid bearer token');
261
+ });
262
+
263
+ it('should reject restore request without valid token', async () => {
264
+ // First create and dismiss the item with a valid token
265
+ mockValidateToken.mockResolvedValue(createValidResult('restore-noauth-user'));
266
+
267
+ await request(app.getHttpServer())
268
+ .get('/v1/users/restore-noauth-user/items/jwt-test-restore-fail')
269
+ .set('Authorization', 'Bearer valid-token')
270
+ .expect(200);
271
+
272
+ await request(app.getHttpServer())
273
+ .delete('/v1/users/restore-noauth-user/items/jwt-test-restore-fail')
274
+ .set('Authorization', 'Bearer valid-token')
275
+ .expect(200);
276
+
277
+ // Reset mock and try to restore without token
278
+ mockValidateToken.mockReset();
279
+
280
+ const response = await request(app.getHttpServer())
281
+ .post('/v1/users/restore-noauth-user/items/jwt-test-restore-fail')
282
+ .expect(401);
283
+
284
+ expect(response.body.error).toBeDefined();
285
+ expect(response.body.error.message).toContain('Missing or invalid bearer token');
286
+ });
287
+ });
288
+
289
+ describe('Token validation edge cases', () => {
290
+ it('should validate token for each request independently', async () => {
291
+ // First request with valid token
292
+ mockValidateToken.mockResolvedValueOnce(createValidResult('multi-user'));
293
+
294
+ await request(app.getHttpServer())
295
+ .get('/v1/users/multi-user/items/jwt-test-multi-1')
296
+ .set('Authorization', 'Bearer token-1')
297
+ .expect(200);
298
+
299
+ // Second request with different valid token
300
+ mockValidateToken.mockResolvedValueOnce(createValidResult('multi-user'));
301
+
302
+ await request(app.getHttpServer())
303
+ .get('/v1/users/multi-user/items/jwt-test-multi-2')
304
+ .set('Authorization', 'Bearer token-2')
305
+ .expect(200);
306
+
307
+ expect(mockValidateToken).toHaveBeenCalledTimes(2);
308
+ expect(mockValidateToken).toHaveBeenNthCalledWith(1, 'token-1');
309
+ expect(mockValidateToken).toHaveBeenNthCalledWith(2, 'token-2');
310
+ });
311
+
312
+ it('should handle validation service errors gracefully', async () => {
313
+ mockValidateToken.mockRejectedValue(new Error('JWKS client error'));
314
+
315
+ const response = await request(app.getHttpServer())
316
+ .get('/v1/users/error-user/items/jwt-test-error')
317
+ .set('Authorization', 'Bearer some-token')
318
+ .expect(500);
319
+
320
+ // Generic errors are handled by NestJS default exception filter
321
+ // which returns { statusCode, message, error } format
322
+ expect(response.body.statusCode).toBe(500);
323
+ });
324
+
325
+ it('should reject token with wrong issuer', async () => {
326
+ mockValidateToken.mockResolvedValue(
327
+ createInvalidResult('jwt issuer invalid. expected: https://auth.example.com'),
328
+ );
329
+
330
+ const response = await request(app.getHttpServer())
331
+ .get('/v1/users/issuer-user/items/jwt-test-issuer')
332
+ .set('Authorization', 'Bearer wrong-issuer-token')
333
+ .expect(401);
334
+
335
+ expect(response.body.error).toBeDefined();
336
+ expect(response.body.error.message).toContain('issuer invalid');
337
+ });
338
+
339
+ it('should reject token with wrong audience', async () => {
340
+ mockValidateToken.mockResolvedValue(
341
+ createInvalidResult('jwt audience invalid. expected: dismissible-api'),
342
+ );
343
+
344
+ const response = await request(app.getHttpServer())
345
+ .get('/v1/users/audience-user/items/jwt-test-audience')
346
+ .set('Authorization', 'Bearer wrong-audience-token')
347
+ .expect(401);
348
+
349
+ expect(response.body.error).toBeDefined();
350
+ expect(response.body.error.message).toContain('audience invalid');
351
+ });
352
+ });
353
+ });
@@ -0,0 +1,63 @@
1
+ import { INestApplication } from '@nestjs/common';
2
+ import request from 'supertest';
3
+ import { join } from 'path';
4
+ import { createTestApp, cleanupTestData } from '../src/app-test.factory';
5
+
6
+ describe('POST /v1/users/:userId/items/:id (restore)', () => {
7
+ let app: INestApplication;
8
+
9
+ beforeAll(async () => {
10
+ app = await createTestApp({
11
+ moduleOptions: {
12
+ configPath: join(__dirname, 'config'),
13
+ },
14
+ });
15
+ await cleanupTestData(app);
16
+ });
17
+
18
+ afterAll(async () => {
19
+ await cleanupTestData(app);
20
+ await app.close();
21
+ });
22
+
23
+ it('should restore a dismissed item', async () => {
24
+ const userId = 'user-restore-1';
25
+ // Create and dismiss the item
26
+ await request(app.getHttpServer()).get(`/v1/users/${userId}/items/restore-test-1`).expect(200);
27
+
28
+ await request(app.getHttpServer())
29
+ .delete(`/v1/users/${userId}/items/restore-test-1`)
30
+ .expect(200);
31
+
32
+ // Restore it
33
+ const response = await request(app.getHttpServer())
34
+ .post(`/v1/users/${userId}/items/restore-test-1`)
35
+ .expect(201);
36
+
37
+ expect(response.body.data).toBeDefined();
38
+ expect(response.body.data.dismissedAt).toBeUndefined();
39
+ });
40
+
41
+ it('should return 400 when restoring non-existent item', async () => {
42
+ const response = await request(app.getHttpServer())
43
+ .post('/v1/users/user-123/items/non-existent-restore')
44
+ .expect(400);
45
+
46
+ expect(response.body.error).toBeDefined();
47
+ expect(response.body.error.message).toContain('not found');
48
+ });
49
+
50
+ it('should return 400 when restoring non-dismissed item', async () => {
51
+ const userId = 'user-restore-2';
52
+ // Create item but don't dismiss it
53
+ await request(app.getHttpServer()).get(`/v1/users/${userId}/items/restore-test-2`).expect(200);
54
+
55
+ // Try to restore
56
+ const response = await request(app.getHttpServer())
57
+ .post(`/v1/users/${userId}/items/restore-test-2`)
58
+ .expect(400);
59
+
60
+ expect(response.body.error).toBeDefined();
61
+ expect(response.body.error.message).toContain('not dismissed');
62
+ });
63
+ });
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "../dist/out-tsc",
5
+ "module": "commonjs",
6
+ "types": ["node", "jest"],
7
+ "emitDecoratorMetadata": true,
8
+ "experimentalDecorators": true,
9
+ "target": "ES2021"
10
+ },
11
+ "include": ["test/**/*.e2e-spec.ts", "src/**/*.ts"]
12
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "../dist/out-tsc",
5
+ "module": "commonjs",
6
+ "types": ["node", "jest"],
7
+ "emitDecoratorMetadata": true,
8
+ "experimentalDecorators": true,
9
+ "target": "ES2021"
10
+ },
11
+ "include": ["src/**/*.spec.ts", "src/**/*.test.ts"]
12
+ }
@@ -1,221 +0,0 @@
1
- import { INestApplication } from '@nestjs/common';
2
- import request from 'supertest';
3
- import { join } from 'path';
4
- import { createTestApp, cleanupTestData } from './app-test.factory';
5
-
6
- describe('Dismissible API (e2e)', () => {
7
- let app: INestApplication;
8
-
9
- beforeAll(async () => {
10
- app = await createTestApp({
11
- moduleOptions: {
12
- configPath: join(__dirname, '../config'),
13
- },
14
- });
15
- await cleanupTestData(app);
16
- });
17
-
18
- afterAll(async () => {
19
- await cleanupTestData(app);
20
- await app.close();
21
- });
22
-
23
- describe('GET /v1/user/:userId/dismissible-item/:id (get-or-create)', () => {
24
- it('should create a new item on first request', async () => {
25
- const response = await request(app.getHttpServer())
26
- .get('/v1/user/user-123/dismissible-item/test-banner-1')
27
- .expect(200);
28
-
29
- expect(response.body.data).toBeDefined();
30
- expect(response.body.data.itemId).toBe('test-banner-1');
31
- expect(response.body.data.userId).toBe('user-123');
32
- expect(response.body.data.createdAt).toBeDefined();
33
- expect(response.body.data.dismissedAt).toBeUndefined();
34
- });
35
-
36
- it('should return existing item on subsequent requests', async () => {
37
- // First request - creates the item
38
- const firstResponse = await request(app.getHttpServer())
39
- .get('/v1/user/user-456/dismissible-item/test-banner-2')
40
- .expect(200);
41
-
42
- const createdAt = firstResponse.body.data.createdAt;
43
-
44
- // Second request - returns existing item
45
- const secondResponse = await request(app.getHttpServer())
46
- .get('/v1/user/user-456/dismissible-item/test-banner-2')
47
- .expect(200);
48
-
49
- expect(secondResponse.body.data.itemId).toBe('test-banner-2');
50
- expect(secondResponse.body.data.createdAt).toBe(createdAt);
51
- });
52
-
53
- it('should store and retrieve metadata', async () => {
54
- const response = await request(app.getHttpServer())
55
- .get('/v1/user/user-789/dismissible-item/test-banner-metadata')
56
- .query({ metadata: ['version:2', 'category:promo'] })
57
- .expect(200);
58
-
59
- expect(response.body.data.metadata).toEqual({
60
- version: 2,
61
- category: 'promo',
62
- });
63
- });
64
- });
65
-
66
- describe('DELETE /v1/user/:userId/dismissible-item/:id (dismiss)', () => {
67
- it('should dismiss an existing item', async () => {
68
- const userId = 'user-dismiss-1';
69
- // First create the item
70
- await request(app.getHttpServer())
71
- .get(`/v1/user/${userId}/dismissible-item/dismiss-test-1`)
72
- .expect(200);
73
-
74
- // Then dismiss it
75
- const response = await request(app.getHttpServer())
76
- .delete(`/v1/user/${userId}/dismissible-item/dismiss-test-1`)
77
- .expect(200);
78
-
79
- expect(response.body.data).toBeDefined();
80
- expect(response.body.data.dismissedAt).toBeDefined();
81
- });
82
-
83
- it('should return 400 when dismissing non-existent item', async () => {
84
- const response = await request(app.getHttpServer())
85
- .delete('/v1/user/user-123/dismissible-item/non-existent-item')
86
- .expect(400);
87
-
88
- expect(response.body.error).toBeDefined();
89
- expect(response.body.error.message).toContain('not found');
90
- });
91
-
92
- it('should return 400 when dismissing already dismissed item', async () => {
93
- const userId = 'user-dismiss-2';
94
- // Create the item
95
- await request(app.getHttpServer())
96
- .get(`/v1/user/${userId}/dismissible-item/dismiss-test-2`)
97
- .expect(200);
98
-
99
- // Dismiss it first time
100
- await request(app.getHttpServer())
101
- .delete(`/v1/user/${userId}/dismissible-item/dismiss-test-2`)
102
- .expect(200);
103
-
104
- // Try to dismiss again
105
- const response = await request(app.getHttpServer())
106
- .delete(`/v1/user/${userId}/dismissible-item/dismiss-test-2`)
107
- .expect(400);
108
-
109
- expect(response.body.error).toBeDefined();
110
- expect(response.body.error.message).toContain('already dismissed');
111
- });
112
- });
113
-
114
- describe('POST /v1/user/:userId/dismissible-item/:id (restore)', () => {
115
- it('should restore a dismissed item', async () => {
116
- const userId = 'user-restore-1';
117
- // Create and dismiss the item
118
- await request(app.getHttpServer())
119
- .get(`/v1/user/${userId}/dismissible-item/restore-test-1`)
120
- .expect(200);
121
-
122
- await request(app.getHttpServer())
123
- .delete(`/v1/user/${userId}/dismissible-item/restore-test-1`)
124
- .expect(200);
125
-
126
- // Restore it
127
- const response = await request(app.getHttpServer())
128
- .post(`/v1/user/${userId}/dismissible-item/restore-test-1`)
129
- .expect(201);
130
-
131
- expect(response.body.data).toBeDefined();
132
- expect(response.body.data.dismissedAt).toBeUndefined();
133
- });
134
-
135
- it('should return 400 when restoring non-existent item', async () => {
136
- const response = await request(app.getHttpServer())
137
- .post('/v1/user/user-123/dismissible-item/non-existent-restore')
138
- .expect(400);
139
-
140
- expect(response.body.error).toBeDefined();
141
- expect(response.body.error.message).toContain('not found');
142
- });
143
-
144
- it('should return 400 when restoring non-dismissed item', async () => {
145
- const userId = 'user-restore-2';
146
- // Create item but don't dismiss it
147
- await request(app.getHttpServer())
148
- .get(`/v1/user/${userId}/dismissible-item/restore-test-2`)
149
- .expect(200);
150
-
151
- // Try to restore
152
- const response = await request(app.getHttpServer())
153
- .post(`/v1/user/${userId}/dismissible-item/restore-test-2`)
154
- .expect(400);
155
-
156
- expect(response.body.error).toBeDefined();
157
- expect(response.body.error.message).toContain('not dismissed');
158
- });
159
- });
160
-
161
- describe('Full lifecycle flow', () => {
162
- it('should handle create -> dismiss -> restore -> dismiss cycle', async () => {
163
- const userId = 'lifecycle-user';
164
- const itemId = 'lifecycle-test';
165
-
166
- // Create
167
- const createResponse = await request(app.getHttpServer())
168
- .get(`/v1/user/${userId}/dismissible-item/${itemId}`)
169
- .expect(200);
170
-
171
- expect(createResponse.body.data.dismissedAt).toBeUndefined();
172
-
173
- // Dismiss
174
- const dismissResponse = await request(app.getHttpServer())
175
- .delete(`/v1/user/${userId}/dismissible-item/${itemId}`)
176
- .expect(200);
177
-
178
- expect(dismissResponse.body.data.dismissedAt).toBeDefined();
179
-
180
- // Restore
181
- const restoreResponse = await request(app.getHttpServer())
182
- .post(`/v1/user/${userId}/dismissible-item/${itemId}`)
183
- .expect(201);
184
-
185
- expect(restoreResponse.body.data.dismissedAt).toBeUndefined();
186
-
187
- // Dismiss again
188
- const dismissAgainResponse = await request(app.getHttpServer())
189
- .delete(`/v1/user/${userId}/dismissible-item/${itemId}`)
190
- .expect(200);
191
-
192
- expect(dismissAgainResponse.body.data.dismissedAt).toBeDefined();
193
- });
194
-
195
- it('should preserve metadata through dismiss and restore', async () => {
196
- const userId = 'user-metadata';
197
- const itemId = 'metadata-preserve-test';
198
-
199
- // Create with metadata
200
- await request(app.getHttpServer())
201
- .get(`/v1/user/${userId}/dismissible-item/${itemId}`)
202
- .query({ metadata: ['key:value', 'count:42'] })
203
- .expect(200);
204
-
205
- // Dismiss
206
- await request(app.getHttpServer())
207
- .delete(`/v1/user/${userId}/dismissible-item/${itemId}`)
208
- .expect(200);
209
-
210
- // Restore and verify metadata
211
- const restoreResponse = await request(app.getHttpServer())
212
- .post(`/v1/user/${userId}/dismissible-item/${itemId}`)
213
- .expect(201);
214
-
215
- expect(restoreResponse.body.data.metadata).toEqual({
216
- key: 'value',
217
- count: 42,
218
- });
219
- });
220
- });
221
- });
@@ -1,2 +0,0 @@
1
- export * from './transform-boolean.decorator';
2
- export * from './transform-comma-separated.decorator';