@nathapp/nestjs-csrf 3.3.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/.eslintrc.js +8 -0
- package/.nax/features/nestjs-csrf/.nax-acceptance.test.ts +785 -0
- package/.turbo/turbo-build.log +4 -0
- package/CHANGELOG.md +15 -0
- package/README.md +29 -0
- package/dist/constant/csrf.constants.d.ts +2 -0
- package/dist/constant/csrf.constants.js +6 -0
- package/dist/constant/csrf.constants.js.map +1 -0
- package/dist/constant/index.d.ts +1 -0
- package/dist/constant/index.js +7 -0
- package/dist/constant/index.js.map +1 -0
- package/dist/decorator/index.d.ts +1 -0
- package/dist/decorator/index.js +6 -0
- package/dist/decorator/index.js.map +1 -0
- package/dist/decorator/skip-csrf.decorator.d.ts +1 -0
- package/dist/decorator/skip-csrf.decorator.js +9 -0
- package/dist/decorator/skip-csrf.decorator.js.map +1 -0
- package/dist/guard/csrf.guard.d.ts +13 -0
- package/dist/guard/csrf.guard.js +85 -0
- package/dist/guard/csrf.guard.js.map +1 -0
- package/dist/guard/index.d.ts +1 -0
- package/dist/guard/index.js +6 -0
- package/dist/guard/index.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/interface/csrf-options.interface.d.ts +27 -0
- package/dist/interface/csrf-options.interface.js +3 -0
- package/dist/interface/csrf-options.interface.js.map +1 -0
- package/dist/interface/index.d.ts +1 -0
- package/dist/interface/index.js +3 -0
- package/dist/interface/index.js.map +1 -0
- package/dist/module/csrf.module.d.ts +15 -0
- package/dist/module/csrf.module.js +64 -0
- package/dist/module/csrf.module.js.map +1 -0
- package/dist/module/index.d.ts +1 -0
- package/dist/module/index.js +6 -0
- package/dist/module/index.js.map +1 -0
- package/dist/service/csrf-token.service.d.ts +4 -0
- package/dist/service/csrf-token.service.js +47 -0
- package/dist/service/csrf-token.service.js.map +1 -0
- package/dist/service/index.d.ts +2 -0
- package/dist/service/index.js +8 -0
- package/dist/service/index.js.map +1 -0
- package/dist/service/resolve-options.d.ts +2 -0
- package/dist/service/resolve-options.js +37 -0
- package/dist/service/resolve-options.js.map +1 -0
- package/lib/constant/csrf.constants.ts +3 -0
- package/lib/constant/index.ts +1 -0
- package/lib/decorator/index.ts +1 -0
- package/lib/decorator/skip-csrf.decorator.ts +6 -0
- package/lib/guard/csrf.guard.ts +98 -0
- package/lib/guard/index.ts +1 -0
- package/lib/index.ts +12 -0
- package/lib/interface/csrf-options.interface.ts +31 -0
- package/lib/interface/index.ts +7 -0
- package/lib/module/csrf.module.ts +74 -0
- package/lib/module/index.ts +1 -0
- package/lib/service/csrf-token.service.ts +34 -0
- package/lib/service/index.ts +2 -0
- package/lib/service/resolve-options.ts +47 -0
- package/package.json +55 -0
- package/test/csrf-token.service.behavior.spec.ts +110 -0
- package/test/csrf-token.service.spec.ts +18 -0
- package/test/csrf.constants.spec.ts +11 -0
- package/test/csrf.module.spec.ts +267 -0
- package/test/guard/csrf.guard.spec.ts +347 -0
- package/test/jest-nax.json +27 -0
- package/test/jest-test.json +28 -0
- package/test/resolve-csrf-options.spec.ts +128 -0
- package/test/skip-csrf.decorator.spec.ts +77 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,785 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll, jest } from '@jest/globals';
|
|
2
|
+
import { Test } from '@nestjs/testing';
|
|
3
|
+
import {
|
|
4
|
+
Controller,
|
|
5
|
+
Get,
|
|
6
|
+
Post,
|
|
7
|
+
INestApplication,
|
|
8
|
+
HttpException,
|
|
9
|
+
} from '@nestjs/common';
|
|
10
|
+
import { Reflector } from '@nestjs/core';
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
CsrfTokenService,
|
|
14
|
+
CsrfModule,
|
|
15
|
+
DefaultCsrfGuard,
|
|
16
|
+
CSRF_MODULE_OPTIONS,
|
|
17
|
+
SKIP_CSRF_METADATA_KEY,
|
|
18
|
+
SkipCsrf,
|
|
19
|
+
resolveCsrfOptions,
|
|
20
|
+
} from '../../../lib';
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Shared helpers
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
function makeContext(
|
|
27
|
+
req: Record<string, unknown>,
|
|
28
|
+
res: Record<string, unknown>,
|
|
29
|
+
handler: object = {},
|
|
30
|
+
cls: object = {},
|
|
31
|
+
) {
|
|
32
|
+
return {
|
|
33
|
+
switchToHttp: () => ({
|
|
34
|
+
getRequest: () => req,
|
|
35
|
+
getResponse: () => res,
|
|
36
|
+
}),
|
|
37
|
+
getHandler: () => handler,
|
|
38
|
+
getClass: () => cls,
|
|
39
|
+
} as any;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function buildGuard(
|
|
43
|
+
optionOverrides: Record<string, unknown> = {},
|
|
44
|
+
tokensOverride: Record<string, unknown> | null = null,
|
|
45
|
+
reflectorResultOverride: Record<string, unknown> | null = null,
|
|
46
|
+
) {
|
|
47
|
+
const rawOptions = { secret: 'test-secret', ...optionOverrides };
|
|
48
|
+
const mockTokens = tokensOverride ?? {
|
|
49
|
+
mint: jest.fn().mockReturnValue('csrf-minted-value.fakesig1234'),
|
|
50
|
+
verify: jest.fn().mockReturnValue(false),
|
|
51
|
+
};
|
|
52
|
+
const mockReflector = reflectorResultOverride ?? {
|
|
53
|
+
getAllAndOverride: jest.fn().mockReturnValue(false),
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
guard: new (DefaultCsrfGuard as any)(rawOptions, mockTokens, mockReflector),
|
|
57
|
+
tokens: mockTokens as any,
|
|
58
|
+
reflector: mockReflector as any,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Integration test controller
|
|
63
|
+
@Controller()
|
|
64
|
+
class AppController {
|
|
65
|
+
@Get('page')
|
|
66
|
+
getPage() { return { ok: true }; }
|
|
67
|
+
|
|
68
|
+
@Post('action')
|
|
69
|
+
postAction() { return { ok: true }; }
|
|
70
|
+
|
|
71
|
+
@Post('skip')
|
|
72
|
+
@SkipCsrf()
|
|
73
|
+
skipAction() { return { ok: true }; }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function extractCsrfTokenFromSetCookie(setCookieHeader: string | string[] | undefined): string {
|
|
77
|
+
const raw = Array.isArray(setCookieHeader) ? setCookieHeader[0] : setCookieHeader;
|
|
78
|
+
if (!raw) return '';
|
|
79
|
+
const part = raw.split(';')[0]; // e.g. "csrf-token=abc.xyz"
|
|
80
|
+
const eqIdx = part.indexOf('=');
|
|
81
|
+
return eqIdx >= 0 ? part.slice(eqIdx + 1) : '';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function thrown403(err: unknown): boolean {
|
|
85
|
+
if (!err) return false;
|
|
86
|
+
if (typeof (err as HttpException).getStatus === 'function') {
|
|
87
|
+
return (err as HttpException).getStatus() === 403;
|
|
88
|
+
}
|
|
89
|
+
return (err as any)?.status === 403 || (err as any)?.statusCode === 403;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// AC-1: CsrfTokenService injectable from Test.createTestingModule
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
describe('AC-1: CsrfTokenService injectable from Test.createTestingModule', () => {
|
|
96
|
+
it('compiles module and resolves CsrfTokenService without throwing', async () => {
|
|
97
|
+
const moduleRef = await Test.createTestingModule({
|
|
98
|
+
providers: [CsrfTokenService],
|
|
99
|
+
}).compile();
|
|
100
|
+
const svc = moduleRef.get(CsrfTokenService);
|
|
101
|
+
expect(svc).toBeDefined();
|
|
102
|
+
expect(svc).toBeInstanceOf(CsrfTokenService);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// AC-2: mint then verify(secret, token, token) returns true
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
describe('AC-2: CsrfTokenService.mint then verify(secret, token, token) returns true', () => {
|
|
110
|
+
it('verify returns true when the same token is used as both cookie and header', () => {
|
|
111
|
+
const svc = new CsrfTokenService();
|
|
112
|
+
const token = svc.mint('test-secret');
|
|
113
|
+
expect(typeof token).toBe('string');
|
|
114
|
+
expect(token.length).toBeGreaterThan(0);
|
|
115
|
+
const result = svc.verify('test-secret', token, token);
|
|
116
|
+
expect(result).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// AC-3: verify rejects double-submit mismatch
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
describe('AC-3: verify(secret, token1, differentToken) returns false', () => {
|
|
124
|
+
it('returns false when header does not match cookie', () => {
|
|
125
|
+
const svc = new CsrfTokenService();
|
|
126
|
+
const token1 = svc.mint('test-secret');
|
|
127
|
+
const result = svc.verify('test-secret', token1, 'different-token');
|
|
128
|
+
expect(result).toBe(false);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// AC-4: verify rejects tampered signature
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
describe('AC-4: verify(secret, tampered, tampered) returns false for altered signature', () => {
|
|
136
|
+
it('returns false when value is intact but signature is replaced', () => {
|
|
137
|
+
const svc = new CsrfTokenService();
|
|
138
|
+
const token = svc.mint('test-secret');
|
|
139
|
+
const [value] = token.split('.');
|
|
140
|
+
const tampered = `${value}.0000000000000000000000000000000000000000000000000000000000`;
|
|
141
|
+
const result = svc.verify('test-secret', tampered, tampered);
|
|
142
|
+
expect(result).toBe(false);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// AC-5: verify rejects token minted with wrong secret
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
describe('AC-5: verify(secret-two, token-from-secret-one, same-token) returns false', () => {
|
|
150
|
+
it('returns false when the verifying secret differs from the minting secret', () => {
|
|
151
|
+
const svc = new CsrfTokenService();
|
|
152
|
+
const token = svc.mint('secret-one');
|
|
153
|
+
const result = svc.verify('secret-two', token, token);
|
|
154
|
+
expect(result).toBe(false);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// AC-6: verify does not throw on empty strings
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
describe('AC-6: verify(secret, "", "") returns false without throwing', () => {
|
|
162
|
+
it('returns false for empty cookie and header values', () => {
|
|
163
|
+
const svc = new CsrfTokenService();
|
|
164
|
+
let result: boolean | undefined;
|
|
165
|
+
let threw = false;
|
|
166
|
+
try {
|
|
167
|
+
result = svc.verify('test-secret', '', '');
|
|
168
|
+
} catch {
|
|
169
|
+
threw = true;
|
|
170
|
+
}
|
|
171
|
+
expect(threw).toBe(false);
|
|
172
|
+
expect(result).toBe(false);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// AC-7: verify does not throw on missing dot separator
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
describe('AC-7: verify(secret, "no-separator", "no-separator") returns false without throwing', () => {
|
|
180
|
+
it('returns false for a malformed token with no dot separator', () => {
|
|
181
|
+
const svc = new CsrfTokenService();
|
|
182
|
+
let result: boolean | undefined;
|
|
183
|
+
let threw = false;
|
|
184
|
+
try {
|
|
185
|
+
result = svc.verify('test-secret', 'no-separator', 'no-separator');
|
|
186
|
+
} catch {
|
|
187
|
+
threw = true;
|
|
188
|
+
}
|
|
189
|
+
expect(threw).toBe(false);
|
|
190
|
+
expect(result).toBe(false);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
// AC-8: resolveCsrfOptions default cookie.name
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
describe('AC-8: resolveCsrfOptions({ secret: "s" }).cookie.name === "csrf-token"', () => {
|
|
198
|
+
it('default cookie name is csrf-token', () => {
|
|
199
|
+
const result = resolveCsrfOptions({ secret: 's' });
|
|
200
|
+
expect(result.cookie.name).toBe('csrf-token');
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
// AC-9: resolveCsrfOptions default headerName
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
describe('AC-9: resolveCsrfOptions({ secret: "s" }).headerName === "x-csrf-token"', () => {
|
|
208
|
+
it('default header name is x-csrf-token', () => {
|
|
209
|
+
const result = resolveCsrfOptions({ secret: 's' });
|
|
210
|
+
expect(result.headerName).toBe('x-csrf-token');
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
// AC-10: resolveCsrfOptions default ignoreMethods
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
describe('AC-10: resolveCsrfOptions({ secret: "s" }).ignoreMethods deep-equals ["GET","HEAD","OPTIONS"]', () => {
|
|
218
|
+
it('default ignoreMethods is GET, HEAD, OPTIONS in that order', () => {
|
|
219
|
+
const result = resolveCsrfOptions({ secret: 's' });
|
|
220
|
+
expect(JSON.stringify(result.ignoreMethods)).toBe(JSON.stringify(['GET', 'HEAD', 'OPTIONS']));
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
// AC-11: resolveCsrfOptions default cookie.sameSite
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
describe('AC-11: resolveCsrfOptions({ secret: "s" }).cookie.sameSite === "lax"', () => {
|
|
228
|
+
it('default sameSite is lax', () => {
|
|
229
|
+
const result = resolveCsrfOptions({ secret: 's' });
|
|
230
|
+
expect(result.cookie.sameSite).toBe('lax');
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
// AC-12: resolveCsrfOptions default skipWhenBearer
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
describe('AC-12: resolveCsrfOptions({ secret: "s" }).skipWhenBearer === true', () => {
|
|
238
|
+
it('default skipWhenBearer is true', () => {
|
|
239
|
+
const result = resolveCsrfOptions({ secret: 's' });
|
|
240
|
+
expect(result.skipWhenBearer).toBe(true);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
// AC-13: resolveCsrfOptions default enabled
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
describe('AC-13: resolveCsrfOptions({ secret: "s" }).enabled === true', () => {
|
|
248
|
+
it('default enabled is true', () => {
|
|
249
|
+
const result = resolveCsrfOptions({ secret: 's' });
|
|
250
|
+
expect(result.enabled).toBe(true);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
// AC-14: CSRF_MODULE_OPTIONS and SKIP_CSRF_METADATA_KEY are distinct
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
describe('AC-14: CSRF_MODULE_OPTIONS !== SKIP_CSRF_METADATA_KEY', () => {
|
|
258
|
+
it('both tokens are truthy and not equal to each other', () => {
|
|
259
|
+
expect(CSRF_MODULE_OPTIONS).toBeTruthy();
|
|
260
|
+
expect(SKIP_CSRF_METADATA_KEY).toBeTruthy();
|
|
261
|
+
expect(CSRF_MODULE_OPTIONS).not.toBe(SKIP_CSRF_METADATA_KEY);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
// AC-15: @SkipCsrf() sets SKIP_CSRF_METADATA_KEY to true on the decorated method
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
describe('AC-15: @SkipCsrf() exposes SKIP_CSRF_METADATA_KEY === true via Reflector.get', () => {
|
|
269
|
+
it('Reflector.get returns true for the decorated handler', () => {
|
|
270
|
+
class TestService {
|
|
271
|
+
@SkipCsrf()
|
|
272
|
+
skipMethod() {}
|
|
273
|
+
}
|
|
274
|
+
const reflector = new Reflector();
|
|
275
|
+
const result = reflector.get(SKIP_CSRF_METADATA_KEY, TestService.prototype.skipMethod);
|
|
276
|
+
expect(result).toBe(true);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
281
|
+
// AC-16: DefaultCsrfGuard injectable with canActivate of arity 1
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
describe('AC-16: DefaultCsrfGuard resolvable from Test module with canActivate(context)', () => {
|
|
284
|
+
it('module compiles without throwing and guard.canActivate has length 1', async () => {
|
|
285
|
+
const moduleRef = await Test.createTestingModule({
|
|
286
|
+
providers: [
|
|
287
|
+
DefaultCsrfGuard,
|
|
288
|
+
CsrfTokenService,
|
|
289
|
+
{ provide: CSRF_MODULE_OPTIONS, useValue: { secret: 'test-secret' } },
|
|
290
|
+
{ provide: Reflector, useValue: new Reflector() },
|
|
291
|
+
],
|
|
292
|
+
}).compile();
|
|
293
|
+
const guard = moduleRef.get(DefaultCsrfGuard);
|
|
294
|
+
expect(guard).toBeDefined();
|
|
295
|
+
expect(typeof guard.canActivate).toBe('function');
|
|
296
|
+
expect(guard.canActivate.length).toBe(1);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// ---------------------------------------------------------------------------
|
|
301
|
+
// AC-17: GET with Express adapter (no req.raw) calls res.cookie with name and token
|
|
302
|
+
// ---------------------------------------------------------------------------
|
|
303
|
+
describe('AC-17: canActivate on safe GET (Express) calls res.cookie with cookie name and minted token', () => {
|
|
304
|
+
it('returns true and res.cookie called with csrf-token name and non-empty string value', () => {
|
|
305
|
+
const resCookie = jest.fn();
|
|
306
|
+
const req = { method: 'GET', raw: undefined, cookies: {}, headers: {} };
|
|
307
|
+
const res = { cookie: resCookie, setCookie: undefined };
|
|
308
|
+
const mintedValue = 'csrf-minted-express-token.validhmacsig';
|
|
309
|
+
const { guard } = buildGuard({}, {
|
|
310
|
+
mint: jest.fn().mockReturnValue(mintedValue),
|
|
311
|
+
verify: jest.fn().mockReturnValue(true),
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const ctx = makeContext(req, res);
|
|
315
|
+
const result = guard.canActivate(ctx);
|
|
316
|
+
|
|
317
|
+
const resolvedOpts = resolveCsrfOptions({ secret: 'test-secret' });
|
|
318
|
+
expect(result).toBe(true);
|
|
319
|
+
expect((resCookie as any).mock.calls.length).toBeGreaterThanOrEqual(1);
|
|
320
|
+
expect((resCookie as any).mock.calls[0][0]).toBe(resolvedOpts.cookie.name);
|
|
321
|
+
expect(typeof (resCookie as any).mock.calls[0][1]).toBe('string');
|
|
322
|
+
expect(((resCookie as any).mock.calls[0][1] as string).length).toBeGreaterThan(0);
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// ---------------------------------------------------------------------------
|
|
327
|
+
// AC-18: GET with Fastify adapter (req.raw defined) calls res.setCookie with name and token
|
|
328
|
+
// ---------------------------------------------------------------------------
|
|
329
|
+
describe('AC-18: canActivate on safe GET (Fastify) calls res.setCookie with cookie name and minted token', () => {
|
|
330
|
+
it('returns true and res.setCookie called with csrf-token name and non-empty string value', () => {
|
|
331
|
+
const resSetCookie = jest.fn();
|
|
332
|
+
const req = { method: 'GET', raw: {}, cookies: {}, headers: {} };
|
|
333
|
+
const res = { setCookie: resSetCookie, cookie: undefined };
|
|
334
|
+
const mintedValue = 'csrf-minted-fastify-token.validhmacsig';
|
|
335
|
+
const { guard } = buildGuard({}, {
|
|
336
|
+
mint: jest.fn().mockReturnValue(mintedValue),
|
|
337
|
+
verify: jest.fn().mockReturnValue(true),
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
const ctx = makeContext(req, res);
|
|
341
|
+
const result = guard.canActivate(ctx);
|
|
342
|
+
|
|
343
|
+
const resolvedOpts = resolveCsrfOptions({ secret: 'test-secret' });
|
|
344
|
+
expect(result).toBe(true);
|
|
345
|
+
expect((resSetCookie as any).mock.calls.length).toBeGreaterThanOrEqual(1);
|
|
346
|
+
expect((resSetCookie as any).mock.calls[0][0]).toBe(resolvedOpts.cookie.name);
|
|
347
|
+
expect(typeof (resSetCookie as any).mock.calls[0][1]).toBe('string');
|
|
348
|
+
expect(((resSetCookie as any).mock.calls[0][1] as string).length).toBeGreaterThan(0);
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// ---------------------------------------------------------------------------
|
|
353
|
+
// AC-19: POST with SKIP_CSRF_METADATA_KEY=true returns true without calling verify or issuing cookie
|
|
354
|
+
// ---------------------------------------------------------------------------
|
|
355
|
+
describe('AC-19: canActivate returns true when reflector returns true for SKIP_CSRF_METADATA_KEY on handler', () => {
|
|
356
|
+
it('returns true without calling tokens.verify or res.cookie', () => {
|
|
357
|
+
const verifyMock = jest.fn().mockReturnValue(false);
|
|
358
|
+
const mintMock = jest.fn().mockReturnValue('any-token.sig');
|
|
359
|
+
const resCookie = jest.fn();
|
|
360
|
+
const { guard } = buildGuard(
|
|
361
|
+
{},
|
|
362
|
+
{ mint: mintMock, verify: verifyMock },
|
|
363
|
+
{ getAllAndOverride: jest.fn().mockReturnValue(true) },
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
const req = { method: 'POST', cookies: {}, headers: {}, raw: undefined };
|
|
367
|
+
const res = { cookie: resCookie };
|
|
368
|
+
const ctx = makeContext(req, res);
|
|
369
|
+
const result = guard.canActivate(ctx);
|
|
370
|
+
|
|
371
|
+
expect(result).toBe(true);
|
|
372
|
+
expect((verifyMock as any).mock.calls.length).toBe(0);
|
|
373
|
+
expect((resCookie as any).mock.calls.length).toBe(0);
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// ---------------------------------------------------------------------------
|
|
378
|
+
// AC-20: POST with Bearer auth and skipWhenBearer=true returns true without calling verify
|
|
379
|
+
// ---------------------------------------------------------------------------
|
|
380
|
+
describe('AC-20: canActivate returns true for Bearer authorization when skipWhenBearer is true', () => {
|
|
381
|
+
it('returns true and tokens.verify is not called', () => {
|
|
382
|
+
const verifyMock = jest.fn().mockReturnValue(false);
|
|
383
|
+
const { guard } = buildGuard(
|
|
384
|
+
{ skipWhenBearer: true },
|
|
385
|
+
{
|
|
386
|
+
mint: jest.fn().mockReturnValue('any.token'),
|
|
387
|
+
verify: verifyMock,
|
|
388
|
+
},
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
const req = {
|
|
392
|
+
method: 'POST',
|
|
393
|
+
headers: { authorization: 'Bearer abc' },
|
|
394
|
+
cookies: {},
|
|
395
|
+
raw: undefined,
|
|
396
|
+
};
|
|
397
|
+
const res = { cookie: jest.fn() };
|
|
398
|
+
const ctx = makeContext(req, res);
|
|
399
|
+
const result = guard.canActivate(ctx);
|
|
400
|
+
|
|
401
|
+
expect(result).toBe(true);
|
|
402
|
+
expect((verifyMock as any).mock.calls.length).toBe(0);
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// ---------------------------------------------------------------------------
|
|
407
|
+
// AC-21: POST with matching cookie+header and verify returning true → canActivate returns true
|
|
408
|
+
// ---------------------------------------------------------------------------
|
|
409
|
+
describe('AC-21: canActivate returns true when tokens.verify returns true for POST with matching tokens', () => {
|
|
410
|
+
it('returns true when cookie and header match and verify returns true', () => {
|
|
411
|
+
const verifyMock = jest.fn().mockReturnValue(true);
|
|
412
|
+
const { guard } = buildGuard({}, {
|
|
413
|
+
mint: jest.fn().mockReturnValue('any.token'),
|
|
414
|
+
verify: verifyMock,
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
const req = {
|
|
418
|
+
method: 'POST',
|
|
419
|
+
cookies: { 'csrf-token': 'validToken123' },
|
|
420
|
+
headers: { 'x-csrf-token': 'validToken123' },
|
|
421
|
+
raw: undefined,
|
|
422
|
+
};
|
|
423
|
+
const res = { cookie: jest.fn() };
|
|
424
|
+
const ctx = makeContext(req, res);
|
|
425
|
+
const result = guard.canActivate(ctx);
|
|
426
|
+
|
|
427
|
+
expect(result).toBe(true);
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// ---------------------------------------------------------------------------
|
|
432
|
+
// AC-22: verify is invoked with the cookie value and header value from the request
|
|
433
|
+
// ---------------------------------------------------------------------------
|
|
434
|
+
describe('AC-22: tokens.verify mock is called with the cookie and header values on POST', () => {
|
|
435
|
+
it('verify.mock.calls contains both cookie value and header value', () => {
|
|
436
|
+
const verifyMock = jest.fn().mockReturnValue(true);
|
|
437
|
+
const { guard } = buildGuard({}, {
|
|
438
|
+
mint: jest.fn().mockReturnValue('any.token'),
|
|
439
|
+
verify: verifyMock,
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
const cookieName = resolveCsrfOptions({ secret: 'test-secret' }).cookie.name;
|
|
443
|
+
const req = {
|
|
444
|
+
method: 'POST',
|
|
445
|
+
cookies: { [cookieName]: 'validToken123' },
|
|
446
|
+
headers: { 'x-csrf-token': 'validToken123' },
|
|
447
|
+
raw: undefined,
|
|
448
|
+
};
|
|
449
|
+
const res = { cookie: jest.fn() };
|
|
450
|
+
const ctx = makeContext(req, res);
|
|
451
|
+
guard.canActivate(ctx);
|
|
452
|
+
|
|
453
|
+
expect((verifyMock as any).mock.calls.length).toBeGreaterThanOrEqual(1);
|
|
454
|
+
const callArgs = (verifyMock as any).mock.calls[0] as string[];
|
|
455
|
+
// Both the cookie value and the header value must appear in the call arguments
|
|
456
|
+
const tokenOccurrences = callArgs.filter((a) => a === 'validToken123');
|
|
457
|
+
expect(tokenOccurrences.length).toBeGreaterThanOrEqual(2);
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// ---------------------------------------------------------------------------
|
|
462
|
+
// AC-23: POST with cookie present but no x-csrf-token header → throws 403
|
|
463
|
+
// ---------------------------------------------------------------------------
|
|
464
|
+
describe('AC-23: canActivate throws 403 when cookie is present but x-csrf-token header is absent', () => {
|
|
465
|
+
it('throws an exception with statusCode 403', () => {
|
|
466
|
+
const { guard } = buildGuard({}, {
|
|
467
|
+
mint: jest.fn().mockReturnValue('any.token'),
|
|
468
|
+
verify: jest.fn().mockReturnValue(false),
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
const req = {
|
|
472
|
+
method: 'POST',
|
|
473
|
+
cookies: { 'csrf-token': 'someToken' },
|
|
474
|
+
headers: {},
|
|
475
|
+
raw: undefined,
|
|
476
|
+
};
|
|
477
|
+
const res = { cookie: jest.fn() };
|
|
478
|
+
const ctx = makeContext(req, res);
|
|
479
|
+
|
|
480
|
+
let thrown: unknown;
|
|
481
|
+
try {
|
|
482
|
+
guard.canActivate(ctx);
|
|
483
|
+
} catch (err) {
|
|
484
|
+
thrown = err;
|
|
485
|
+
}
|
|
486
|
+
expect(thrown).toBeDefined();
|
|
487
|
+
expect(thrown403(thrown)).toBe(true);
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
// ---------------------------------------------------------------------------
|
|
492
|
+
// AC-24: POST with mismatched cookie and x-csrf-token → throws 403
|
|
493
|
+
// ---------------------------------------------------------------------------
|
|
494
|
+
describe('AC-24: canActivate throws 403 when cookie value and x-csrf-token header value differ', () => {
|
|
495
|
+
it('throws an exception with statusCode 403', () => {
|
|
496
|
+
const { guard } = buildGuard({}, {
|
|
497
|
+
mint: jest.fn().mockReturnValue('any.token'),
|
|
498
|
+
verify: jest.fn().mockReturnValue(false),
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
const req = {
|
|
502
|
+
method: 'POST',
|
|
503
|
+
cookies: { 'csrf-token': 'tokenA' },
|
|
504
|
+
headers: { 'x-csrf-token': 'tokenB' },
|
|
505
|
+
raw: undefined,
|
|
506
|
+
};
|
|
507
|
+
const res = { cookie: jest.fn() };
|
|
508
|
+
const ctx = makeContext(req, res);
|
|
509
|
+
|
|
510
|
+
let thrown: unknown;
|
|
511
|
+
try {
|
|
512
|
+
guard.canActivate(ctx);
|
|
513
|
+
} catch (err) {
|
|
514
|
+
thrown = err;
|
|
515
|
+
}
|
|
516
|
+
expect(thrown).toBeDefined();
|
|
517
|
+
expect(thrown403(thrown)).toBe(true);
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
// ---------------------------------------------------------------------------
|
|
522
|
+
// AC-25: POST with no cookie but x-csrf-token header present → throws 403
|
|
523
|
+
// ---------------------------------------------------------------------------
|
|
524
|
+
describe('AC-25: canActivate throws 403 when cookie is absent but x-csrf-token header is present', () => {
|
|
525
|
+
it('throws an exception with statusCode 403', () => {
|
|
526
|
+
const { guard } = buildGuard({}, {
|
|
527
|
+
mint: jest.fn().mockReturnValue('any.token'),
|
|
528
|
+
verify: jest.fn().mockReturnValue(false),
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
const req = {
|
|
532
|
+
method: 'POST',
|
|
533
|
+
cookies: {},
|
|
534
|
+
headers: { 'x-csrf-token': 'someToken' },
|
|
535
|
+
raw: undefined,
|
|
536
|
+
};
|
|
537
|
+
const res = { cookie: jest.fn() };
|
|
538
|
+
const ctx = makeContext(req, res);
|
|
539
|
+
|
|
540
|
+
let thrown: unknown;
|
|
541
|
+
try {
|
|
542
|
+
guard.canActivate(ctx);
|
|
543
|
+
} catch (err) {
|
|
544
|
+
thrown = err;
|
|
545
|
+
}
|
|
546
|
+
expect(thrown).toBeDefined();
|
|
547
|
+
expect(thrown403(thrown)).toBe(true);
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
// ---------------------------------------------------------------------------
|
|
552
|
+
// AC-26: POST with neither cookie nor x-csrf-token header → throws 403
|
|
553
|
+
// ---------------------------------------------------------------------------
|
|
554
|
+
describe('AC-26: canActivate throws 403 when both cookie and x-csrf-token header are absent', () => {
|
|
555
|
+
it('throws an exception with statusCode 403', () => {
|
|
556
|
+
const { guard } = buildGuard({}, {
|
|
557
|
+
mint: jest.fn().mockReturnValue('any.token'),
|
|
558
|
+
verify: jest.fn().mockReturnValue(false),
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
const req = {
|
|
562
|
+
method: 'POST',
|
|
563
|
+
cookies: undefined,
|
|
564
|
+
headers: {},
|
|
565
|
+
raw: undefined,
|
|
566
|
+
};
|
|
567
|
+
const res = { cookie: jest.fn() };
|
|
568
|
+
const ctx = makeContext(req, res);
|
|
569
|
+
|
|
570
|
+
let thrown: unknown;
|
|
571
|
+
try {
|
|
572
|
+
guard.canActivate(ctx);
|
|
573
|
+
} catch (err) {
|
|
574
|
+
thrown = err;
|
|
575
|
+
}
|
|
576
|
+
expect(thrown).toBeDefined();
|
|
577
|
+
expect(thrown403(thrown)).toBe(true);
|
|
578
|
+
});
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
// ---------------------------------------------------------------------------
|
|
582
|
+
// AC-27: CsrfModule.register returns global DynamicModule
|
|
583
|
+
// ---------------------------------------------------------------------------
|
|
584
|
+
describe('AC-27: CsrfModule.register({ secret: "s" }) returns object with global === true', () => {
|
|
585
|
+
it('returned dynamic module has global: true', () => {
|
|
586
|
+
const result = CsrfModule.register({ secret: 's' });
|
|
587
|
+
expect((result as any).global).toBe(true);
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
// ---------------------------------------------------------------------------
|
|
592
|
+
// AC-28: Integration — POST without CSRF tokens returns 403
|
|
593
|
+
// ---------------------------------------------------------------------------
|
|
594
|
+
describe('AC-28: CsrfModule.register app returns 403 for POST without CSRF cookie or header', () => {
|
|
595
|
+
let app: INestApplication;
|
|
596
|
+
|
|
597
|
+
beforeAll(async () => {
|
|
598
|
+
const moduleRef = await Test.createTestingModule({
|
|
599
|
+
imports: [CsrfModule.register({ secret: 's' })],
|
|
600
|
+
controllers: [AppController],
|
|
601
|
+
}).compile();
|
|
602
|
+
app = moduleRef.createNestApplication();
|
|
603
|
+
await app.init();
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
afterAll(async () => {
|
|
607
|
+
await app?.close();
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
it('POST /action with no CSRF tokens returns 403', async () => {
|
|
611
|
+
const supertest = require('supertest');
|
|
612
|
+
const res = await supertest(app.getHttpServer()).post('/action');
|
|
613
|
+
expect(res.statusCode).toBe(403);
|
|
614
|
+
});
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
// ---------------------------------------------------------------------------
|
|
618
|
+
// AC-29: Integration — GET returns 2xx and Set-Cookie with CSRF token
|
|
619
|
+
// ---------------------------------------------------------------------------
|
|
620
|
+
describe('AC-29: CsrfModule.register app returns 2xx and Set-Cookie on GET', () => {
|
|
621
|
+
let app: INestApplication;
|
|
622
|
+
|
|
623
|
+
beforeAll(async () => {
|
|
624
|
+
const moduleRef = await Test.createTestingModule({
|
|
625
|
+
imports: [CsrfModule.register({ secret: 's' })],
|
|
626
|
+
controllers: [AppController],
|
|
627
|
+
}).compile();
|
|
628
|
+
app = moduleRef.createNestApplication();
|
|
629
|
+
await app.init();
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
afterAll(async () => {
|
|
633
|
+
await app?.close();
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
it('GET /page returns 2xx and Set-Cookie header containing a CSRF token value', async () => {
|
|
637
|
+
const supertest = require('supertest');
|
|
638
|
+
const res = await supertest(app.getHttpServer()).get('/page');
|
|
639
|
+
expect(res.statusCode).toBeGreaterThanOrEqual(200);
|
|
640
|
+
expect(res.statusCode).toBeLessThan(300);
|
|
641
|
+
const setCookie: string | string[] | undefined = res.headers['set-cookie'];
|
|
642
|
+
const cookieStr = Array.isArray(setCookie) ? setCookie.join(', ') : (setCookie ?? '');
|
|
643
|
+
expect(cookieStr).toContain('csrf-token');
|
|
644
|
+
const tokenValue = extractCsrfTokenFromSetCookie(setCookie);
|
|
645
|
+
expect(tokenValue.length).toBeGreaterThan(0);
|
|
646
|
+
});
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
// ---------------------------------------------------------------------------
|
|
650
|
+
// AC-30: Integration — full double-submit round-trip returns 2xx
|
|
651
|
+
// ---------------------------------------------------------------------------
|
|
652
|
+
describe('AC-30: CsrfModule.register app full round-trip: GET cookie, POST with cookie+header returns 2xx', () => {
|
|
653
|
+
let app: INestApplication;
|
|
654
|
+
|
|
655
|
+
beforeAll(async () => {
|
|
656
|
+
const moduleRef = await Test.createTestingModule({
|
|
657
|
+
imports: [CsrfModule.register({ secret: 's' })],
|
|
658
|
+
controllers: [AppController],
|
|
659
|
+
}).compile();
|
|
660
|
+
app = moduleRef.createNestApplication();
|
|
661
|
+
// Enable cookie parsing so the guard can read req.cookies on POST
|
|
662
|
+
const cookieParser = require('cookie-parser');
|
|
663
|
+
app.use(cookieParser());
|
|
664
|
+
await app.init();
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
afterAll(async () => {
|
|
668
|
+
await app?.close();
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
it('POST with valid CSRF cookie and matching x-csrf-token header returns 2xx', async () => {
|
|
672
|
+
const supertest = require('supertest');
|
|
673
|
+
|
|
674
|
+
// 1. GET to receive the CSRF cookie
|
|
675
|
+
const getRes = await supertest(app.getHttpServer()).get('/page');
|
|
676
|
+
expect(getRes.statusCode).toBeGreaterThanOrEqual(200);
|
|
677
|
+
const tokenValue = extractCsrfTokenFromSetCookie(getRes.headers['set-cookie']);
|
|
678
|
+
expect(tokenValue.length).toBeGreaterThan(0);
|
|
679
|
+
|
|
680
|
+
// 2. POST echoing back the same token in cookie + header
|
|
681
|
+
const postRes = await supertest(app.getHttpServer())
|
|
682
|
+
.post('/action')
|
|
683
|
+
.set('Cookie', `csrf-token=${tokenValue}`)
|
|
684
|
+
.set('x-csrf-token', tokenValue);
|
|
685
|
+
expect(postRes.statusCode).toBeGreaterThanOrEqual(200);
|
|
686
|
+
expect(postRes.statusCode).toBeLessThan(300);
|
|
687
|
+
});
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
// ---------------------------------------------------------------------------
|
|
691
|
+
// AC-31: Integration — enabled:false allows POST without CSRF tokens
|
|
692
|
+
// ---------------------------------------------------------------------------
|
|
693
|
+
describe('AC-31: CsrfModule.register({ enabled: false }) allows POST without CSRF tokens', () => {
|
|
694
|
+
let app: INestApplication;
|
|
695
|
+
|
|
696
|
+
beforeAll(async () => {
|
|
697
|
+
const moduleRef = await Test.createTestingModule({
|
|
698
|
+
imports: [CsrfModule.register({ secret: 's', enabled: false })],
|
|
699
|
+
controllers: [AppController],
|
|
700
|
+
}).compile();
|
|
701
|
+
app = moduleRef.createNestApplication();
|
|
702
|
+
await app.init();
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
afterAll(async () => {
|
|
706
|
+
await app?.close();
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
it('POST /action with no CSRF tokens returns 2xx when enabled is false', async () => {
|
|
710
|
+
const supertest = require('supertest');
|
|
711
|
+
const res = await supertest(app.getHttpServer()).post('/action');
|
|
712
|
+
expect(res.statusCode).toBeGreaterThanOrEqual(200);
|
|
713
|
+
expect(res.statusCode).toBeLessThan(300);
|
|
714
|
+
});
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
// ---------------------------------------------------------------------------
|
|
718
|
+
// AC-32: Integration — @SkipCsrf() decorated handler allows POST without CSRF
|
|
719
|
+
// ---------------------------------------------------------------------------
|
|
720
|
+
describe('AC-32: POST to @SkipCsrf() decorated handler returns 2xx without CSRF token', () => {
|
|
721
|
+
let app: INestApplication;
|
|
722
|
+
|
|
723
|
+
beforeAll(async () => {
|
|
724
|
+
const moduleRef = await Test.createTestingModule({
|
|
725
|
+
imports: [CsrfModule.register({ secret: 's' })],
|
|
726
|
+
controllers: [AppController],
|
|
727
|
+
}).compile();
|
|
728
|
+
app = moduleRef.createNestApplication();
|
|
729
|
+
await app.init();
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
afterAll(async () => {
|
|
733
|
+
await app?.close();
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
it('POST /skip with no CSRF tokens returns 2xx because handler is decorated with @SkipCsrf()', async () => {
|
|
737
|
+
const supertest = require('supertest');
|
|
738
|
+
const res = await supertest(app.getHttpServer()).post('/skip');
|
|
739
|
+
expect(res.statusCode).toBeGreaterThanOrEqual(200);
|
|
740
|
+
expect(res.statusCode).toBeLessThan(300);
|
|
741
|
+
});
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
// ---------------------------------------------------------------------------
|
|
745
|
+
// AC-33: Integration — registerAsync applies guard and returns 403 for tokenless POST
|
|
746
|
+
// ---------------------------------------------------------------------------
|
|
747
|
+
describe('AC-33: CsrfModule.registerAsync({ useFactory }) returns 403 for POST without CSRF tokens', () => {
|
|
748
|
+
let app: INestApplication;
|
|
749
|
+
|
|
750
|
+
beforeAll(async () => {
|
|
751
|
+
const moduleRef = await Test.createTestingModule({
|
|
752
|
+
imports: [CsrfModule.registerAsync({ useFactory: () => ({ secret: 's' }) })],
|
|
753
|
+
controllers: [AppController],
|
|
754
|
+
}).compile();
|
|
755
|
+
app = moduleRef.createNestApplication();
|
|
756
|
+
await app.init();
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
afterAll(async () => {
|
|
760
|
+
await app?.close();
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
it('POST /action without CSRF tokens returns 403', async () => {
|
|
764
|
+
const supertest = require('supertest');
|
|
765
|
+
const res = await supertest(app.getHttpServer()).post('/action');
|
|
766
|
+
expect(res.statusCode).toBe(403);
|
|
767
|
+
});
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
// ---------------------------------------------------------------------------
|
|
771
|
+
// AC-34: CsrfModule.register without secret throws Error containing "secret"
|
|
772
|
+
// ---------------------------------------------------------------------------
|
|
773
|
+
describe('AC-34: CsrfModule.register({} as any) throws an Error with message containing "secret"', () => {
|
|
774
|
+
it('throws at registration time with a message containing "secret"', () => {
|
|
775
|
+
let thrownError: Error | undefined;
|
|
776
|
+
try {
|
|
777
|
+
CsrfModule.register({} as any);
|
|
778
|
+
} catch (err: any) {
|
|
779
|
+
thrownError = err;
|
|
780
|
+
}
|
|
781
|
+
expect(thrownError).toBeDefined();
|
|
782
|
+
expect(thrownError).toBeInstanceOf(Error);
|
|
783
|
+
expect(thrownError!.message).toMatch(/secret/i);
|
|
784
|
+
});
|
|
785
|
+
});
|