@dismissible/nestjs-jwt-auth-hook 0.0.1 → 0.0.2-canary.43c6bbd.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/project.json DELETED
@@ -1,42 +0,0 @@
1
- {
2
- "name": "jwt-auth-hook",
3
- "$schema": "../../node_modules/nx/schemas/project-schema.json",
4
- "sourceRoot": "libs/jwt-auth-hook/src",
5
- "projectType": "library",
6
- "tags": [],
7
- "targets": {
8
- "build": {
9
- "executor": "@nx/js:tsc",
10
- "outputs": ["{options.outputPath}"],
11
- "options": {
12
- "outputPath": "dist/libs/jwt-auth-hook",
13
- "main": "libs/jwt-auth-hook/src/index.ts",
14
- "tsConfig": "libs/jwt-auth-hook/tsconfig.lib.json",
15
- "assets": ["libs/jwt-auth-hook/package.json", "libs/jwt-auth-hook/README.md"],
16
- "generatePackageJson": true
17
- }
18
- },
19
- "lint": {
20
- "executor": "@nx/eslint:lint",
21
- "outputs": ["{options.outputFile}"],
22
- "options": {
23
- "lintFilePatterns": ["libs/jwt-auth-hook/**/*.ts"]
24
- }
25
- },
26
- "test": {
27
- "executor": "@nx/jest:jest",
28
- "outputs": ["{workspaceRoot}/coverage/libs/jwt-auth-hook"],
29
- "options": {
30
- "jestConfig": "libs/jwt-auth-hook/jest.config.ts",
31
- "passWithNoTests": true
32
- }
33
- },
34
- "npm-publish": {
35
- "executor": "nx:run-commands",
36
- "options": {
37
- "command": "npm publish --access public",
38
- "cwd": "dist/libs/jwt-auth-hook"
39
- }
40
- }
41
- }
42
- }
@@ -1,158 +0,0 @@
1
- import 'reflect-metadata';
2
- import { plainToInstance } from 'class-transformer';
3
- import { validate } from 'class-validator';
4
- import { JwtAuthHookConfig } from './jwt-auth-hook.config';
5
-
6
- describe('JwtAuthHookConfig', () => {
7
- describe('enabled property', () => {
8
- it('should transform boolean true to true', async () => {
9
- const config = plainToInstance(JwtAuthHookConfig, {
10
- enabled: true,
11
- wellKnownUrl: 'https://auth.example.com/.well-known/openid-configuration',
12
- });
13
-
14
- expect(config.enabled).toBe(true);
15
- });
16
-
17
- it('should transform boolean false to false', async () => {
18
- const config = plainToInstance(JwtAuthHookConfig, {
19
- enabled: false,
20
- wellKnownUrl: 'https://auth.example.com/.well-known/openid-configuration',
21
- });
22
-
23
- expect(config.enabled).toBe(false);
24
- });
25
-
26
- it('should transform string "true" to boolean true', async () => {
27
- const config = plainToInstance(JwtAuthHookConfig, {
28
- enabled: 'true',
29
- wellKnownUrl: 'https://auth.example.com/.well-known/openid-configuration',
30
- });
31
-
32
- expect(config.enabled).toBe(true);
33
- });
34
-
35
- it('should transform string "false" to boolean false', async () => {
36
- const config = plainToInstance(JwtAuthHookConfig, {
37
- enabled: 'false',
38
- wellKnownUrl: 'https://auth.example.com/.well-known/openid-configuration',
39
- });
40
-
41
- expect(config.enabled).toBe(false);
42
- });
43
-
44
- it('should transform string "True" (case insensitive) to boolean true', async () => {
45
- const config = plainToInstance(JwtAuthHookConfig, {
46
- enabled: 'True',
47
- wellKnownUrl: 'https://auth.example.com/.well-known/openid-configuration',
48
- });
49
-
50
- expect(config.enabled).toBe(true);
51
- });
52
-
53
- it('should convert non-true string values to false', async () => {
54
- const config = plainToInstance(JwtAuthHookConfig, {
55
- enabled: 'other',
56
- wellKnownUrl: 'https://auth.example.com/.well-known/openid-configuration',
57
- });
58
-
59
- // The transform converts string values: 'true' -> true, anything else -> false
60
- expect(config.enabled).toBe(false);
61
- });
62
- });
63
-
64
- describe('wellKnownUrl validation', () => {
65
- it('should require wellKnownUrl when enabled is true', async () => {
66
- const config = plainToInstance(JwtAuthHookConfig, {
67
- enabled: true,
68
- });
69
-
70
- const errors = await validate(config);
71
- expect(errors.length).toBeGreaterThan(0);
72
- expect(errors.some((e) => e.property === 'wellKnownUrl')).toBe(true);
73
- });
74
-
75
- it('should not require wellKnownUrl when enabled is false', async () => {
76
- const config = plainToInstance(JwtAuthHookConfig, {
77
- enabled: false,
78
- });
79
-
80
- const errors = await validate(config);
81
- // wellKnownUrl should not be required when enabled is false
82
- expect(errors.some((e) => e.property === 'wellKnownUrl')).toBe(false);
83
- });
84
-
85
- it('should validate wellKnownUrl is a valid URL', async () => {
86
- const config = plainToInstance(JwtAuthHookConfig, {
87
- enabled: true,
88
- wellKnownUrl: 'not-a-valid-url',
89
- });
90
-
91
- const errors = await validate(config);
92
- expect(errors.length).toBeGreaterThan(0);
93
- expect(errors.some((e) => e.property === 'wellKnownUrl')).toBe(true);
94
- });
95
- });
96
-
97
- describe('optional properties', () => {
98
- it('should accept optional issuer', async () => {
99
- const config = plainToInstance(JwtAuthHookConfig, {
100
- enabled: true,
101
- wellKnownUrl: 'https://auth.example.com/.well-known/openid-configuration',
102
- issuer: 'https://auth.example.com',
103
- });
104
-
105
- expect(config.issuer).toBe('https://auth.example.com');
106
- });
107
-
108
- it('should accept optional audience', async () => {
109
- const config = plainToInstance(JwtAuthHookConfig, {
110
- enabled: true,
111
- wellKnownUrl: 'https://auth.example.com/.well-known/openid-configuration',
112
- audience: 'my-api',
113
- });
114
-
115
- expect(config.audience).toBe('my-api');
116
- });
117
-
118
- it('should accept optional algorithms array', async () => {
119
- const config = plainToInstance(JwtAuthHookConfig, {
120
- enabled: true,
121
- wellKnownUrl: 'https://auth.example.com/.well-known/openid-configuration',
122
- algorithms: ['RS256', 'RS384'],
123
- });
124
-
125
- expect(config.algorithms).toEqual(['RS256', 'RS384']);
126
- });
127
-
128
- it('should transform jwksCacheDuration to number', async () => {
129
- const config = plainToInstance(JwtAuthHookConfig, {
130
- enabled: true,
131
- wellKnownUrl: 'https://auth.example.com/.well-known/openid-configuration',
132
- jwksCacheDuration: '600000',
133
- });
134
-
135
- expect(config.jwksCacheDuration).toBe(600000);
136
- });
137
-
138
- it('should transform requestTimeout to number', async () => {
139
- const config = plainToInstance(JwtAuthHookConfig, {
140
- enabled: true,
141
- wellKnownUrl: 'https://auth.example.com/.well-known/openid-configuration',
142
- requestTimeout: '30000',
143
- });
144
-
145
- expect(config.requestTimeout).toBe(30000);
146
- });
147
-
148
- it('should transform priority to number', async () => {
149
- const config = plainToInstance(JwtAuthHookConfig, {
150
- enabled: true,
151
- wellKnownUrl: 'https://auth.example.com/.well-known/openid-configuration',
152
- priority: '-100',
153
- });
154
-
155
- expect(config.priority).toBe(-100);
156
- });
157
- });
158
- });
@@ -1,94 +0,0 @@
1
- import {
2
- IsString,
3
- IsUrl,
4
- IsOptional,
5
- IsArray,
6
- IsNumber,
7
- IsBoolean,
8
- ValidateIf,
9
- } from 'class-validator';
10
- import { Type } from 'class-transformer';
11
- import { TransformBoolean } from '@dismissible/nestjs-validation';
12
-
13
- /**
14
- * Injection token for JWT auth hook configuration.
15
- */
16
- export const JWT_AUTH_HOOK_CONFIG = Symbol('JWT_AUTH_HOOK_CONFIG');
17
-
18
- /**
19
- * Configuration options for JWT authentication hook.
20
- */
21
- export class JwtAuthHookConfig {
22
- @IsBoolean()
23
- @TransformBoolean()
24
- public readonly enabled!: boolean;
25
-
26
- /**
27
- * The OpenID Connect well-known URL (e.g., https://auth.example.com/.well-known/openid-configuration).
28
- * The JWKS URI will be fetched from this endpoint.
29
- */
30
- @ValidateIf((o) => o.enabled === true)
31
- @IsUrl()
32
- public readonly wellKnownUrl!: string;
33
-
34
- /**
35
- * Optional: Expected issuer claim (iss) to validate.
36
- * If not provided, issuer validation is skipped.
37
- */
38
- @IsOptional()
39
- @IsString()
40
- public readonly issuer?: string;
41
-
42
- /**
43
- * Optional: Expected audience claim (aud) to validate.
44
- * If not provided, audience validation is skipped.
45
- */
46
- @IsOptional()
47
- @IsString()
48
- public readonly audience?: string;
49
-
50
- /**
51
- * Optional: Allowed algorithms for JWT verification.
52
- * Defaults to ['RS256'].
53
- */
54
- @IsOptional()
55
- @IsArray()
56
- @IsString({ each: true })
57
- public readonly algorithms?: string[];
58
-
59
- /**
60
- * Optional: Cache duration in milliseconds for JWKS.
61
- * Defaults to 600000 (10 minutes).
62
- */
63
- @IsOptional()
64
- @IsNumber()
65
- @Type(() => Number)
66
- public readonly jwksCacheDuration?: number;
67
-
68
- /**
69
- * Optional: Request timeout in milliseconds.
70
- * Defaults to 30000 (30 seconds).
71
- */
72
- @IsOptional()
73
- @IsNumber()
74
- @Type(() => Number)
75
- public readonly requestTimeout?: number;
76
-
77
- /**
78
- * Optional: Hook priority (lower numbers run first).
79
- * Defaults to -100 (runs early for authentication).
80
- */
81
- @IsOptional()
82
- @IsNumber()
83
- @Type(() => Number)
84
- public readonly priority?: number;
85
-
86
- /**
87
- * Optional: Verify that the userId parameter matches the JWT subject (sub) claim.
88
- * Defaults to true for security. Set to false for service-to-service scenarios.
89
- */
90
- @IsOptional()
91
- @IsBoolean()
92
- @TransformBoolean(true) // Default to true if not provided
93
- public readonly verifyUserIdMatch?: boolean;
94
- }
@@ -1,79 +0,0 @@
1
- import { Module, DynamicModule, InjectionToken } from '@nestjs/common';
2
- import { HttpModule } from '@nestjs/axios';
3
- import { JwtAuthHook } from './jwt-auth.hook';
4
- import { JwtAuthService } from './jwt-auth.service';
5
- import { JWT_AUTH_HOOK_CONFIG, JwtAuthHookConfig } from './jwt-auth-hook.config';
6
-
7
- /**
8
- * Async module options for JWT auth hook.
9
- */
10
- export interface IJwtAuthHookModuleAsyncOptions {
11
- useFactory: (...args: unknown[]) => JwtAuthHookConfig | Promise<JwtAuthHookConfig>;
12
- inject?: InjectionToken[];
13
- }
14
-
15
- /**
16
- * Module that provides JWT authentication hook for Dismissible.
17
- *
18
- * @example
19
- * ```typescript
20
- * import { DismissibleModule } from '@dismissible/nestjs-dismissible';
21
- * import { JwtAuthHookModule, JwtAuthHook } from '@dismissible/nestjs-jwt-auth-hook';
22
- *
23
- * @Module({
24
- * imports: [
25
- * JwtAuthHookModule.forRoot({
26
- * wellKnownUrl: 'https://auth.example.com/.well-known/openid-configuration',
27
- * issuer: 'https://auth.example.com',
28
- * audience: 'my-api',
29
- * }),
30
- * DismissibleModule.forRoot({
31
- * hooks: [JwtAuthHook],
32
- * // ... other options
33
- * }),
34
- * ],
35
- * })
36
- * export class AppModule {}
37
- * ```
38
- */
39
- @Module({})
40
- export class JwtAuthHookModule {
41
- static forRoot(config: JwtAuthHookConfig): DynamicModule {
42
- return {
43
- module: JwtAuthHookModule,
44
- imports: [HttpModule],
45
- providers: [
46
- {
47
- provide: JWT_AUTH_HOOK_CONFIG,
48
- useValue: config,
49
- },
50
- JwtAuthService,
51
- JwtAuthHook,
52
- ],
53
- exports: [JwtAuthHook, JwtAuthService, JWT_AUTH_HOOK_CONFIG],
54
- global: true,
55
- };
56
- }
57
-
58
- /**
59
- * Create module with async configuration.
60
- * Useful when config values come from environment or other async sources.
61
- */
62
- static forRootAsync(options: IJwtAuthHookModuleAsyncOptions): DynamicModule {
63
- return {
64
- module: JwtAuthHookModule,
65
- imports: [HttpModule],
66
- providers: [
67
- {
68
- provide: JWT_AUTH_HOOK_CONFIG,
69
- useFactory: options.useFactory,
70
- inject: options.inject ?? [],
71
- },
72
- JwtAuthService,
73
- JwtAuthHook,
74
- ],
75
- exports: [JwtAuthHook, JwtAuthService, JWT_AUTH_HOOK_CONFIG],
76
- global: true,
77
- };
78
- }
79
- }
@@ -1,283 +0,0 @@
1
- import { UnauthorizedException, ForbiddenException } from '@nestjs/common';
2
- import { mock } from 'ts-jest-mocker';
3
- import { JwtAuthHook } from './jwt-auth.hook';
4
- import { JwtAuthService, IJwtValidationResult } from './jwt-auth.service';
5
- import { JwtAuthHookConfig } from './jwt-auth-hook.config';
6
- import { IDismissibleLogger } from '@dismissible/nestjs-logger';
7
- import { IRequestContext } from '@dismissible/nestjs-dismissible';
8
-
9
- describe('JwtAuthHook', () => {
10
- let hook: JwtAuthHook;
11
- let mockJwtAuthService: jest.Mocked<JwtAuthService>;
12
- let mockLogger: jest.Mocked<IDismissibleLogger>;
13
- let mockConfig: JwtAuthHookConfig;
14
-
15
- const testItemId = 'test-item-id';
16
- const testUserId = 'test-user-id';
17
-
18
- beforeEach(() => {
19
- mockJwtAuthService = mock(JwtAuthService, { failIfMockNotProvided: false });
20
- mockLogger = mock<IDismissibleLogger>({ failIfMockNotProvided: false });
21
- mockConfig = {
22
- enabled: true,
23
- wellKnownUrl: 'https://auth.example.com/.well-known/openid-configuration',
24
- issuer: 'https://auth.example.com',
25
- };
26
-
27
- hook = new JwtAuthHook(mockJwtAuthService, mockConfig, mockLogger);
28
- });
29
-
30
- describe('priority', () => {
31
- it('should default to -100 priority', () => {
32
- expect(hook.priority).toBe(-100);
33
- });
34
-
35
- it('should use custom priority when provided', () => {
36
- const customConfig = { ...mockConfig, priority: -50 };
37
- const customHook = new JwtAuthHook(mockJwtAuthService, customConfig, mockLogger);
38
- expect(customHook.priority).toBe(-50);
39
- });
40
- });
41
-
42
- describe('onBeforeRequest', () => {
43
- it('should skip validation and proceed when disabled', async () => {
44
- const disabledConfig = { ...mockConfig, enabled: false };
45
- const disabledHook = new JwtAuthHook(mockJwtAuthService, disabledConfig, mockLogger);
46
-
47
- const context: IRequestContext = { requestId: 'req-123' };
48
- const result = await disabledHook.onBeforeRequest(testItemId, testUserId, context);
49
-
50
- expect(result.proceed).toBe(true);
51
- expect(mockJwtAuthService.extractBearerToken).not.toHaveBeenCalled();
52
- expect(mockJwtAuthService.validateToken).not.toHaveBeenCalled();
53
- });
54
-
55
- it('should throw UnauthorizedException when no authorization header is present', async () => {
56
- mockJwtAuthService.extractBearerToken.mockReturnValue(null);
57
-
58
- const context: IRequestContext = { requestId: 'req-123' };
59
-
60
- await expect(hook.onBeforeRequest(testItemId, testUserId, context)).rejects.toThrow(
61
- UnauthorizedException,
62
- );
63
- await expect(hook.onBeforeRequest(testItemId, testUserId, context)).rejects.toThrow(
64
- 'Missing or invalid bearer token',
65
- );
66
- expect(mockJwtAuthService.extractBearerToken).toHaveBeenCalledWith(undefined);
67
- });
68
-
69
- it('should throw UnauthorizedException when authorization header is malformed', async () => {
70
- mockJwtAuthService.extractBearerToken.mockReturnValue(null);
71
-
72
- const context: IRequestContext = {
73
- requestId: 'req-123',
74
- authorizationHeader: 'Basic abc123',
75
- };
76
-
77
- await expect(hook.onBeforeRequest(testItemId, testUserId, context)).rejects.toThrow(
78
- UnauthorizedException,
79
- );
80
- await expect(hook.onBeforeRequest(testItemId, testUserId, context)).rejects.toThrow(
81
- 'Missing or invalid bearer token',
82
- );
83
- });
84
-
85
- it('should throw UnauthorizedException when token validation fails', async () => {
86
- const validationResult: IJwtValidationResult = {
87
- valid: false,
88
- error: 'Token expired',
89
- };
90
- mockJwtAuthService.extractBearerToken.mockReturnValue('valid-token');
91
- mockJwtAuthService.validateToken.mockResolvedValue(validationResult);
92
-
93
- const context: IRequestContext = {
94
- requestId: 'req-123',
95
- authorizationHeader: 'Bearer valid-token',
96
- };
97
-
98
- await expect(hook.onBeforeRequest(testItemId, testUserId, context)).rejects.toThrow(
99
- UnauthorizedException,
100
- );
101
- await expect(hook.onBeforeRequest(testItemId, testUserId, context)).rejects.toThrow(
102
- 'Token expired',
103
- );
104
- expect(mockJwtAuthService.validateToken).toHaveBeenCalledWith('valid-token');
105
- });
106
-
107
- it('should proceed when token is valid', async () => {
108
- const matchingUserId = 'user-123';
109
- const validationResult: IJwtValidationResult = {
110
- valid: true,
111
- payload: {
112
- sub: matchingUserId,
113
- iss: 'https://auth.example.com',
114
- },
115
- };
116
- mockJwtAuthService.extractBearerToken.mockReturnValue('valid-token');
117
- mockJwtAuthService.validateToken.mockResolvedValue(validationResult);
118
-
119
- const context: IRequestContext = {
120
- requestId: 'req-123',
121
- authorizationHeader: 'Bearer valid-token',
122
- };
123
- const result = await hook.onBeforeRequest(testItemId, matchingUserId, context);
124
-
125
- expect(result.proceed).toBe(true);
126
- expect(result.reason).toBeUndefined();
127
- });
128
-
129
- it('should throw UnauthorizedException when context is missing', async () => {
130
- mockJwtAuthService.extractBearerToken.mockReturnValue(null);
131
-
132
- await expect(hook.onBeforeRequest(testItemId, testUserId, undefined)).rejects.toThrow(
133
- UnauthorizedException,
134
- );
135
- expect(mockJwtAuthService.extractBearerToken).toHaveBeenCalledWith(undefined);
136
- });
137
-
138
- it('should log debug message on successful validation', async () => {
139
- const matchingUserId = 'user-123';
140
- const validationResult: IJwtValidationResult = {
141
- valid: true,
142
- payload: { sub: matchingUserId },
143
- };
144
- mockJwtAuthService.extractBearerToken.mockReturnValue('valid-token');
145
- mockJwtAuthService.validateToken.mockResolvedValue(validationResult);
146
-
147
- const context: IRequestContext = {
148
- requestId: 'req-123',
149
- authorizationHeader: 'Bearer valid-token',
150
- };
151
- await hook.onBeforeRequest(testItemId, matchingUserId, context);
152
-
153
- expect(mockLogger.debug).toHaveBeenCalledWith(
154
- 'JWT auth hook: Token validated successfully',
155
- expect.objectContaining({
156
- itemId: testItemId,
157
- userId: matchingUserId,
158
- requestId: 'req-123',
159
- subject: matchingUserId,
160
- }),
161
- );
162
- });
163
-
164
- it('should throw ForbiddenException when userId does not match JWT sub claim', async () => {
165
- const validationResult: IJwtValidationResult = {
166
- valid: true,
167
- payload: {
168
- sub: 'different-user-id',
169
- iss: 'https://auth.example.com',
170
- },
171
- };
172
- mockJwtAuthService.extractBearerToken.mockReturnValue('valid-token');
173
- mockJwtAuthService.validateToken.mockResolvedValue(validationResult);
174
-
175
- const context: IRequestContext = {
176
- requestId: 'req-123',
177
- authorizationHeader: 'Bearer valid-token',
178
- };
179
-
180
- await expect(hook.onBeforeRequest(testItemId, testUserId, context)).rejects.toThrow(
181
- ForbiddenException,
182
- );
183
- await expect(hook.onBeforeRequest(testItemId, testUserId, context)).rejects.toThrow(
184
- 'User ID in request does not match authenticated user',
185
- );
186
-
187
- expect(mockLogger.debug).toHaveBeenCalledWith(
188
- 'JWT auth hook: User ID mismatch',
189
- expect.objectContaining({
190
- itemId: testItemId,
191
- userId: testUserId,
192
- requestId: 'req-123',
193
- tokenSubject: 'different-user-id',
194
- }),
195
- );
196
- });
197
-
198
- it('should proceed when userId matches JWT sub claim', async () => {
199
- const validationResult: IJwtValidationResult = {
200
- valid: true,
201
- payload: {
202
- sub: testUserId,
203
- iss: 'https://auth.example.com',
204
- },
205
- };
206
- mockJwtAuthService.extractBearerToken.mockReturnValue('valid-token');
207
- mockJwtAuthService.validateToken.mockResolvedValue(validationResult);
208
-
209
- const context: IRequestContext = {
210
- requestId: 'req-123',
211
- authorizationHeader: 'Bearer valid-token',
212
- };
213
- const result = await hook.onBeforeRequest(testItemId, testUserId, context);
214
-
215
- expect(result.proceed).toBe(true);
216
- expect(result.reason).toBeUndefined();
217
- });
218
-
219
- it('should proceed when verifyUserIdMatch is disabled even if userId does not match', async () => {
220
- const disabledVerifyConfig = { ...mockConfig, verifyUserIdMatch: false };
221
- const disabledVerifyHook = new JwtAuthHook(
222
- mockJwtAuthService,
223
- disabledVerifyConfig,
224
- mockLogger,
225
- );
226
-
227
- const validationResult: IJwtValidationResult = {
228
- valid: true,
229
- payload: {
230
- sub: 'different-user-id',
231
- iss: 'https://auth.example.com',
232
- },
233
- };
234
- mockJwtAuthService.extractBearerToken.mockReturnValue('valid-token');
235
- mockJwtAuthService.validateToken.mockResolvedValue(validationResult);
236
-
237
- const context: IRequestContext = {
238
- requestId: 'req-123',
239
- authorizationHeader: 'Bearer valid-token',
240
- };
241
- const result = await disabledVerifyHook.onBeforeRequest(testItemId, testUserId, context);
242
-
243
- expect(result.proceed).toBe(true);
244
- });
245
-
246
- it('should proceed when JWT payload does not have sub claim and verifyUserIdMatch is enabled', async () => {
247
- const validationResult: IJwtValidationResult = {
248
- valid: true,
249
- payload: {
250
- iss: 'https://auth.example.com',
251
- // No sub claim
252
- },
253
- };
254
- mockJwtAuthService.extractBearerToken.mockReturnValue('valid-token');
255
- mockJwtAuthService.validateToken.mockResolvedValue(validationResult);
256
-
257
- const context: IRequestContext = {
258
- requestId: 'req-123',
259
- authorizationHeader: 'Bearer valid-token',
260
- };
261
- const result = await hook.onBeforeRequest(testItemId, testUserId, context);
262
-
263
- expect(result.proceed).toBe(true);
264
- });
265
-
266
- it('should proceed when JWT payload is undefined and verifyUserIdMatch is enabled', async () => {
267
- const validationResult: IJwtValidationResult = {
268
- valid: true,
269
- // No payload
270
- };
271
- mockJwtAuthService.extractBearerToken.mockReturnValue('valid-token');
272
- mockJwtAuthService.validateToken.mockResolvedValue(validationResult);
273
-
274
- const context: IRequestContext = {
275
- requestId: 'req-123',
276
- authorizationHeader: 'Bearer valid-token',
277
- };
278
- const result = await hook.onBeforeRequest(testItemId, testUserId, context);
279
-
280
- expect(result.proceed).toBe(true);
281
- });
282
- });
283
- });