@oalacea/daemon 0.7.1 → 0.7.3
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/bin/Dockerfile +4 -4
- package/dist/prompts/DEPS_EFFICIENCY.md +558 -0
- package/dist/prompts/E2E.md +491 -0
- package/dist/prompts/EXECUTE.md +1060 -0
- package/dist/prompts/INTEGRATION_API.md +484 -0
- package/dist/prompts/INTEGRATION_DB.md +425 -0
- package/dist/prompts/PERF_API.md +433 -0
- package/dist/prompts/PERF_DB.md +430 -0
- package/dist/prompts/PERF_FRONT.md +357 -0
- package/dist/prompts/REMEDIATION.md +482 -0
- package/dist/prompts/UNIT.md +260 -0
- package/dist/templates/README.md +221 -0
- package/dist/templates/k6/load-test.js +54 -0
- package/dist/templates/nestjs/controller.spec.ts +203 -0
- package/dist/templates/nestjs/e2e/api.e2e-spec.ts +451 -0
- package/dist/templates/nestjs/e2e/auth.e2e-spec.ts +533 -0
- package/dist/templates/nestjs/fixtures/test-module.ts +311 -0
- package/dist/templates/nestjs/guard.spec.ts +314 -0
- package/dist/templates/nestjs/interceptor.spec.ts +458 -0
- package/dist/templates/nestjs/module.spec.ts +173 -0
- package/dist/templates/nestjs/pipe.spec.ts +474 -0
- package/dist/templates/nestjs/service.spec.ts +296 -0
- package/dist/templates/playwright/e2e.spec.ts +61 -0
- package/dist/templates/rust/Cargo.toml +72 -0
- package/dist/templates/rust/actix-controller.test.rs +114 -0
- package/dist/templates/rust/axum-handler.test.rs +117 -0
- package/dist/templates/rust/integration.test.rs +63 -0
- package/dist/templates/rust/rocket-route.test.rs +106 -0
- package/dist/templates/rust/unit.test.rs +38 -0
- package/dist/templates/vitest/angular-component.test.ts +38 -0
- package/dist/templates/vitest/api.test.ts +51 -0
- package/dist/templates/vitest/component.test.ts +27 -0
- package/dist/templates/vitest/hook.test.ts +36 -0
- package/dist/templates/vitest/solid-component.test.ts +34 -0
- package/dist/templates/vitest/svelte-component.test.ts +33 -0
- package/dist/templates/vitest/vue-component.test.ts +39 -0
- package/package.json +2 -2
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NestJS Test Module Fixture
|
|
3
|
+
*
|
|
4
|
+
* Reusable test module configuration for unit testing.
|
|
5
|
+
* Provides common providers, mocks, and utilities.
|
|
6
|
+
*
|
|
7
|
+
* @package test
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Test, TestingModule } from '@nestjs/testing';
|
|
11
|
+
import { INestApplication, ValidationPipe } from '@nestjs/common';
|
|
12
|
+
import { JwtService } from '@nestjs/jwt';
|
|
13
|
+
import { getRepositoryToken } from '@nestjs/typeorm';
|
|
14
|
+
import { Repository } from 'typeorm';
|
|
15
|
+
import { ConfigModule } from '@nestjs/config';
|
|
16
|
+
import { PassportModule } from '@nestjs/passport';
|
|
17
|
+
import { JwtModule } from '@nestjs/jwt';
|
|
18
|
+
|
|
19
|
+
// Mock repositories
|
|
20
|
+
export const mockRepository = () => ({
|
|
21
|
+
find: jest.fn(),
|
|
22
|
+
findOne: jest.fn(),
|
|
23
|
+
findOneBy: jest.fn(),
|
|
24
|
+
create: jest.fn(),
|
|
25
|
+
save: jest.fn(),
|
|
26
|
+
update: jest.fn(),
|
|
27
|
+
delete: jest.fn(),
|
|
28
|
+
count: jest.fn(),
|
|
29
|
+
query: jest.fn(),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Mock JwtService
|
|
33
|
+
export const mockJwtService = () => ({
|
|
34
|
+
sign: jest.fn(),
|
|
35
|
+
verify: jest.fn(),
|
|
36
|
+
verifyAsync: jest.fn(),
|
|
37
|
+
decode: jest.fn(),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Mock ConfigService
|
|
41
|
+
export const mockConfigService = () => ({
|
|
42
|
+
get: jest.fn((key: string) => {
|
|
43
|
+
const config: Record<string, any> = {
|
|
44
|
+
JWT_SECRET: 'test-secret',
|
|
45
|
+
JWT_EXPIRES_IN: '1h',
|
|
46
|
+
DATABASE_URL: 'sqlite::memory:',
|
|
47
|
+
};
|
|
48
|
+
return config[key];
|
|
49
|
+
}),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Create a test module with common configuration
|
|
54
|
+
*/
|
|
55
|
+
export async function createTestModule(options: {
|
|
56
|
+
imports?: any[];
|
|
57
|
+
providers?: any[];
|
|
58
|
+
controllers?: any[];
|
|
59
|
+
}) {
|
|
60
|
+
const moduleFixture: TestingModule = await Test.createTestingModule({
|
|
61
|
+
imports: [
|
|
62
|
+
ConfigModule.forRoot({ isGlobal: true }),
|
|
63
|
+
PassportModule.register({ defaultStrategy: 'jwt' }),
|
|
64
|
+
JwtModule.register({
|
|
65
|
+
secret: 'test-secret',
|
|
66
|
+
signOptions: { expiresIn: '1h' },
|
|
67
|
+
}),
|
|
68
|
+
...(options.imports ?? []),
|
|
69
|
+
],
|
|
70
|
+
providers: options.providers ?? [],
|
|
71
|
+
controllers: options.controllers ?? [],
|
|
72
|
+
})
|
|
73
|
+
.overrideProvider(JwtService)
|
|
74
|
+
.useValue(mockJwtService())
|
|
75
|
+
.compile();
|
|
76
|
+
|
|
77
|
+
return moduleFixture;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Create a test application with global configuration
|
|
82
|
+
*/
|
|
83
|
+
export async function createTestApplication(module: TestingModule): Promise<INestApplication> {
|
|
84
|
+
const app = module.createNestApplication();
|
|
85
|
+
|
|
86
|
+
// Apply global validation pipe
|
|
87
|
+
app.useGlobalPipes(
|
|
88
|
+
new ValidationPipe({
|
|
89
|
+
whitelist: true,
|
|
90
|
+
transform: true,
|
|
91
|
+
forbidNonWhitelisted: true,
|
|
92
|
+
transformOptions: {
|
|
93
|
+
enableImplicitConversion: true,
|
|
94
|
+
},
|
|
95
|
+
})
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// Enable shutdown hooks
|
|
99
|
+
app.enableShutdownHooks();
|
|
100
|
+
|
|
101
|
+
await app.init();
|
|
102
|
+
|
|
103
|
+
return app;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Mock user for authentication tests
|
|
108
|
+
*/
|
|
109
|
+
export const mockUser = {
|
|
110
|
+
id: '1',
|
|
111
|
+
email: 'test@example.com',
|
|
112
|
+
name: 'Test User',
|
|
113
|
+
roles: ['user'],
|
|
114
|
+
createdAt: new Date(),
|
|
115
|
+
updatedAt: new Date(),
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Mock admin user for role tests
|
|
120
|
+
*/
|
|
121
|
+
export const mockAdmin = {
|
|
122
|
+
id: '2',
|
|
123
|
+
email: 'admin@example.com',
|
|
124
|
+
name: 'Admin User',
|
|
125
|
+
roles: ['admin'],
|
|
126
|
+
createdAt: new Date(),
|
|
127
|
+
updatedAt: new Date(),
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Create mock ExecutionContext
|
|
132
|
+
*/
|
|
133
|
+
export function createMockExecutionContext(request: any = {}) {
|
|
134
|
+
return {
|
|
135
|
+
switchToHttp: () => ({
|
|
136
|
+
getRequest: () => ({
|
|
137
|
+
method: 'GET',
|
|
138
|
+
url: '/',
|
|
139
|
+
headers: {},
|
|
140
|
+
user: null,
|
|
141
|
+
...request,
|
|
142
|
+
}),
|
|
143
|
+
getResponse: () => ({
|
|
144
|
+
statusCode: 200,
|
|
145
|
+
}),
|
|
146
|
+
}),
|
|
147
|
+
getHandler: () => ({}),
|
|
148
|
+
getClass: () => ({}),
|
|
149
|
+
getArgByIndex: () => ({}),
|
|
150
|
+
getArgs: () => [],
|
|
151
|
+
getType: () => 'http',
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Create mock CallHandler
|
|
157
|
+
*/
|
|
158
|
+
export function createMockCallHandler(value: any = { data: 'test' }) {
|
|
159
|
+
return {
|
|
160
|
+
handle: () => of(value),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Helper to create test entity
|
|
166
|
+
*/
|
|
167
|
+
export function createTestEntity(overrides: Partial<any> = {}) {
|
|
168
|
+
return {
|
|
169
|
+
id: '1',
|
|
170
|
+
createdAt: new Date(),
|
|
171
|
+
updatedAt: new Date(),
|
|
172
|
+
...overrides,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Wait for async operations
|
|
178
|
+
*/
|
|
179
|
+
export function delay(ms: number): Promise<void> {
|
|
180
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Create test database connection options
|
|
185
|
+
*/
|
|
186
|
+
export function createTestDatabaseOptions() {
|
|
187
|
+
return {
|
|
188
|
+
type: 'sqlite',
|
|
189
|
+
database: ':memory:',
|
|
190
|
+
dropSchema: true,
|
|
191
|
+
synchronize: true,
|
|
192
|
+
logging: false,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Setup test database
|
|
198
|
+
*/
|
|
199
|
+
export async function setupTestDatabase(entities: any[]) {
|
|
200
|
+
const { DataSource } = require('typeorm');
|
|
201
|
+
|
|
202
|
+
const dataSource = new DataSource({
|
|
203
|
+
type: 'sqlite',
|
|
204
|
+
database: ':memory:',
|
|
205
|
+
entities,
|
|
206
|
+
synchronize: true,
|
|
207
|
+
logging: false,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
await dataSource.initialize();
|
|
211
|
+
|
|
212
|
+
return dataSource;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Clean test database
|
|
217
|
+
*/
|
|
218
|
+
export async function cleanTestDatabase(dataSource: any) {
|
|
219
|
+
const entities = dataSource.entityMetadatas;
|
|
220
|
+
const repositoryNames = entities.map((e: any) => e.name);
|
|
221
|
+
|
|
222
|
+
// Clear all tables
|
|
223
|
+
for (const name of repositoryNames) {
|
|
224
|
+
const repository = dataSource.getRepository(name);
|
|
225
|
+
await repository.clear();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Mock file upload for testing
|
|
231
|
+
*/
|
|
232
|
+
export function createMockFile(overrides: Partial<Express.Multer.File> = {}) {
|
|
233
|
+
return {
|
|
234
|
+
fieldname: 'file',
|
|
235
|
+
originalname: 'test.jpg',
|
|
236
|
+
encoding: '7bit',
|
|
237
|
+
mimetype: 'image/jpeg',
|
|
238
|
+
size: 1024,
|
|
239
|
+
buffer: Buffer.from('test'),
|
|
240
|
+
destination: '/tmp',
|
|
241
|
+
filename: 'test.jpg',
|
|
242
|
+
path: '/tmp/test.jpg',
|
|
243
|
+
stream: null,
|
|
244
|
+
...overrides,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Create test DTO with validation
|
|
250
|
+
*/
|
|
251
|
+
export function createTestDto<T>(dto: Class<T>, data: Partial<T>): T {
|
|
252
|
+
return Object.assign(Object.create(dto.prototype), data);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Type for class reference
|
|
257
|
+
*/
|
|
258
|
+
type Class<T> = new (...args: any[]) => T;
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Mock pagination options
|
|
262
|
+
*/
|
|
263
|
+
export function createMockPaginationOptions(overrides: any = {}) {
|
|
264
|
+
return {
|
|
265
|
+
page: 1,
|
|
266
|
+
limit: 10,
|
|
267
|
+
sortBy: 'createdAt',
|
|
268
|
+
sortOrder: 'DESC' as const,
|
|
269
|
+
...overrides,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Mock filtered query options
|
|
275
|
+
*/
|
|
276
|
+
export function createMockFilterOptions(overrides: any = {}) {
|
|
277
|
+
return {
|
|
278
|
+
search: '',
|
|
279
|
+
status: null,
|
|
280
|
+
dateFrom: null,
|
|
281
|
+
dateTo: null,
|
|
282
|
+
...overrides,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Export all test utilities
|
|
288
|
+
*/
|
|
289
|
+
export const testUtils = {
|
|
290
|
+
createTestModule,
|
|
291
|
+
createTestApplication,
|
|
292
|
+
createMockExecutionContext,
|
|
293
|
+
createMockCallHandler,
|
|
294
|
+
createTestEntity,
|
|
295
|
+
delay,
|
|
296
|
+
createTestDatabaseOptions,
|
|
297
|
+
setupTestDatabase,
|
|
298
|
+
cleanTestDatabase,
|
|
299
|
+
createMockFile,
|
|
300
|
+
createTestDto,
|
|
301
|
+
createMockPaginationOptions,
|
|
302
|
+
createMockFilterOptions,
|
|
303
|
+
mockRepository,
|
|
304
|
+
mockJwtService,
|
|
305
|
+
mockConfigService,
|
|
306
|
+
mockUser,
|
|
307
|
+
mockAdmin,
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
// Import RxJS 'of' for observable creation
|
|
311
|
+
import { of } from 'rxjs';
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NestJS Guard Test Template
|
|
3
|
+
*
|
|
4
|
+
* Tests for NestJS guards following best practices:
|
|
5
|
+
* - Authentication logic
|
|
6
|
+
* - Authorization checks
|
|
7
|
+
* - Role/permission validation
|
|
8
|
+
* - Context handling
|
|
9
|
+
*
|
|
10
|
+
* @package test
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { ExecutionContext, ForbiddenException } from '@nestjs/common';
|
|
14
|
+
import { Reflector } from '@nestjs/core';
|
|
15
|
+
import { AuthGuard } from './auth.guard';
|
|
16
|
+
import { RolesGuard } from './roles.guard';
|
|
17
|
+
import { JwtAuthGuard } from './jwt-auth.guard';
|
|
18
|
+
|
|
19
|
+
describe('AuthGuard', () => {
|
|
20
|
+
let guard: AuthGuard;
|
|
21
|
+
let reflector: Reflector;
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
reflector = new Reflector();
|
|
25
|
+
guard = new AuthGuard(reflector);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('canActivate', () => {
|
|
29
|
+
it('should allow access with valid token', () => {
|
|
30
|
+
const context = createMockExecutionContext({
|
|
31
|
+
headers: {
|
|
32
|
+
authorization: 'Bearer valid-token',
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
jest.spyOn(guard, 'validateToken').mockResolvedValue(true);
|
|
37
|
+
|
|
38
|
+
const result = guard.canActivate(context);
|
|
39
|
+
|
|
40
|
+
expect(result).resolves.toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should deny access without token', () => {
|
|
44
|
+
const context = createMockExecutionContext({
|
|
45
|
+
headers: {},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const result = guard.canActivate(context);
|
|
49
|
+
|
|
50
|
+
expect(result).resolves.toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should deny access with invalid token', () => {
|
|
54
|
+
const context = createMockExecutionContext({
|
|
55
|
+
headers: {
|
|
56
|
+
authorization: 'Bearer invalid-token',
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
jest.spyOn(guard, 'validateToken').mockResolvedValue(false);
|
|
61
|
+
|
|
62
|
+
const result = guard.canActivate(context);
|
|
63
|
+
|
|
64
|
+
expect(result).resolves.toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should handle malformed authorization header', () => {
|
|
68
|
+
const context = createMockExecutionContext({
|
|
69
|
+
headers: {
|
|
70
|
+
authorization: 'InvalidFormat token',
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const result = guard.canActivate(context);
|
|
75
|
+
|
|
76
|
+
expect(result).resolves.toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should extract user from request and attach to context', async () => {
|
|
80
|
+
const context = createMockExecutionContext({
|
|
81
|
+
headers: {
|
|
82
|
+
authorization: 'Bearer valid-token',
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const mockUser = { id: '1', username: 'test' };
|
|
87
|
+
jest.spyOn(guard, 'getUserFromToken').mockResolvedValue(mockUser);
|
|
88
|
+
|
|
89
|
+
await guard.canActivate(context);
|
|
90
|
+
|
|
91
|
+
const request = context.switchToHttp().getRequest();
|
|
92
|
+
expect(request.user).toEqual(mockUser);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('public routes', () => {
|
|
97
|
+
it('should allow access to public routes', () => {
|
|
98
|
+
const context = createMockExecutionContext({
|
|
99
|
+
headers: {},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
jest.spyOn(reflector, 'get').mockReturnValue(true);
|
|
103
|
+
|
|
104
|
+
const result = guard.canActivate(context);
|
|
105
|
+
|
|
106
|
+
expect(result).resolves.toBe(true);
|
|
107
|
+
expect(reflector.get).toHaveBeenCalledWith('isPublic', expect.any());
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should check for isPublic decorator on handler', () => {
|
|
111
|
+
const context = createMockExecutionContext();
|
|
112
|
+
|
|
113
|
+
jest.spyOn(reflector, 'get').mockReturnValue(true);
|
|
114
|
+
|
|
115
|
+
guard.canActivate(context);
|
|
116
|
+
|
|
117
|
+
expect(reflector.get).toHaveBeenCalledWith(
|
|
118
|
+
'isPublic',
|
|
119
|
+
context.getHandler()
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('RolesGuard', () => {
|
|
126
|
+
let guard: RolesGuard;
|
|
127
|
+
let reflector: Reflector;
|
|
128
|
+
|
|
129
|
+
beforeEach(() => {
|
|
130
|
+
reflector = new Reflector();
|
|
131
|
+
guard = new RolesGuard(reflector);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('canActivate', () => {
|
|
135
|
+
it('should allow access when user has required role', () => {
|
|
136
|
+
const context = createMockExecutionContext({
|
|
137
|
+
user: { roles: ['admin'] },
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
jest.spyOn(reflector, 'get').mockReturnValue(['admin']);
|
|
141
|
+
|
|
142
|
+
const result = guard.canActivate(context);
|
|
143
|
+
|
|
144
|
+
expect(result).resolves.toBe(true);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should deny access when user lacks required role', () => {
|
|
148
|
+
const context = createMockExecutionContext({
|
|
149
|
+
user: { roles: ['user'] },
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
jest.spyOn(reflector, 'get').mockReturnValue(['admin']);
|
|
153
|
+
|
|
154
|
+
const result = guard.canActivate(context);
|
|
155
|
+
|
|
156
|
+
expect(result).resolves.toBe(false);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should allow access when no roles are required', () => {
|
|
160
|
+
const context = createMockExecutionContext({
|
|
161
|
+
user: { roles: ['user'] },
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
jest.spyOn(reflector, 'get').mockReturnValue(null);
|
|
165
|
+
|
|
166
|
+
const result = guard.canActivate(context);
|
|
167
|
+
|
|
168
|
+
expect(result).resolves.toBe(true);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should allow access when user has multiple roles including required', () => {
|
|
172
|
+
const context = createMockExecutionContext({
|
|
173
|
+
user: { roles: ['user', 'moderator', 'admin'] },
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
jest.spyOn(reflector, 'get').mockReturnValue(['admin', 'moderator']);
|
|
177
|
+
|
|
178
|
+
const result = guard.canActivate(context);
|
|
179
|
+
|
|
180
|
+
expect(result).resolves.toBe(true);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should handle user without roles property', () => {
|
|
184
|
+
const context = createMockExecutionContext({
|
|
185
|
+
user: {},
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
jest.spyOn(reflector, 'get').mockReturnValue(['user']);
|
|
189
|
+
|
|
190
|
+
const result = guard.canActivate(context);
|
|
191
|
+
|
|
192
|
+
expect(result).resolves.toBe(false);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should handle missing user in request', () => {
|
|
196
|
+
const context = createMockExecutionContext({
|
|
197
|
+
user: null,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
jest.spyOn(reflector, 'get').mockReturnValue(['admin']);
|
|
201
|
+
|
|
202
|
+
const result = guard.canActivate(context);
|
|
203
|
+
|
|
204
|
+
expect(result).resolves.toBe(false);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe('role hierarchy', () => {
|
|
209
|
+
it('should respect role hierarchy', () => {
|
|
210
|
+
const context = createMockExecutionContext({
|
|
211
|
+
user: { roles: ['super-admin'] },
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
jest.spyOn(reflector, 'get').mockReturnValue(['admin']);
|
|
215
|
+
jest.spyOn(guard, 'hasHigherRole').mockReturnValue(true);
|
|
216
|
+
|
|
217
|
+
const result = guard.canActivate(context);
|
|
218
|
+
|
|
219
|
+
expect(result).resolves.toBe(true);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe('JwtAuthGuard', () => {
|
|
225
|
+
let guard: JwtAuthGuard;
|
|
226
|
+
let jwtService: any;
|
|
227
|
+
|
|
228
|
+
beforeEach(() => {
|
|
229
|
+
jwtService = {
|
|
230
|
+
verifyAsync: jest.fn(),
|
|
231
|
+
};
|
|
232
|
+
guard = new JwtAuthGuard(jwtService);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe('canActivate', () => {
|
|
236
|
+
it('should validate JWT token', async () => {
|
|
237
|
+
const context = createMockExecutionContext({
|
|
238
|
+
headers: {
|
|
239
|
+
authorization: 'Bearer valid.jwt.token',
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const payload = { sub: '1', username: 'test' };
|
|
244
|
+
jest.spyOn(jwtService, 'verifyAsync').mockResolvedValue(payload);
|
|
245
|
+
|
|
246
|
+
const result = await guard.canActivate(context);
|
|
247
|
+
|
|
248
|
+
expect(result).toBe(true);
|
|
249
|
+
expect(jwtService.verifyAsync).toHaveBeenCalledWith('valid.jwt.token');
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('should attach user payload to request', async () => {
|
|
253
|
+
const context = createMockExecutionContext({
|
|
254
|
+
headers: {
|
|
255
|
+
authorization: 'Bearer valid.jwt.token',
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const payload = { sub: '1', username: 'test' };
|
|
260
|
+
jest.spyOn(jwtService, 'verifyAsync').mockResolvedValue(payload);
|
|
261
|
+
|
|
262
|
+
await guard.canActivate(context);
|
|
263
|
+
|
|
264
|
+
const request = context.switchToHttp().getRequest();
|
|
265
|
+
expect(request.user).toEqual(payload);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('should throw UnauthorizedException for invalid token', async () => {
|
|
269
|
+
const context = createMockExecutionContext({
|
|
270
|
+
headers: {
|
|
271
|
+
authorization: 'Bearer invalid.token',
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
jest.spyOn(jwtService, 'verifyAsync').mockRejectedValue(
|
|
276
|
+
new Error('Invalid token')
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
await expect(guard.canActivate(context)).rejects.toThrow();
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('should handle expired tokens', async () => {
|
|
283
|
+
const context = createMockExecutionContext({
|
|
284
|
+
headers: {
|
|
285
|
+
authorization: 'Bearer expired.token',
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
jest.spyOn(jwtService, 'verifyAsync').mockRejectedValue(
|
|
290
|
+
new Error('Token expired')
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
await expect(guard.canActivate(context)).rejects.toThrow();
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// Helper function to create mock ExecutionContext
|
|
299
|
+
function createMockExecutionContext(requestData: any = {}): ExecutionContext {
|
|
300
|
+
return {
|
|
301
|
+
switchToHttp: () => ({
|
|
302
|
+
getRequest: () => ({
|
|
303
|
+
headers: requestData.headers || {},
|
|
304
|
+
user: requestData.user,
|
|
305
|
+
...requestData,
|
|
306
|
+
}),
|
|
307
|
+
}),
|
|
308
|
+
getHandler: () => ({}),
|
|
309
|
+
getClass: () => ({}),
|
|
310
|
+
getArgByIndex: () => ({}),
|
|
311
|
+
getArgs: () => [],
|
|
312
|
+
getType: () => 'http',
|
|
313
|
+
} as unknown as ExecutionContext;
|
|
314
|
+
}
|