范围:src/gateway/auth/, src/business/auth/, src/app.module.ts 涉及文件: - 新增:src/gateway/auth/ 目录及所有文件 - 移动:Controller、Guard、Decorator、DTO从business层移至gateway层 - 修改:src/business/auth/index.ts(移除Gateway层组件导出) - 修改:src/app.module.ts(使用AuthGatewayModule替代AuthModule) 主要改进: - 明确Gateway层和Business层的职责边界 - Controller、Guard、Decorator属于Gateway层职责 - Business层专注于业务逻辑和服务 - 符合分层架构设计原则
165 lines
5.2 KiB
TypeScript
165 lines
5.2 KiB
TypeScript
/**
|
|
* JwtAuthGuard 单元测试
|
|
*
|
|
* 功能描述:
|
|
* - 测试JWT认证守卫的令牌验证功能
|
|
* - 验证用户信息提取和注入
|
|
* - 测试认证失败的异常处理
|
|
*
|
|
* 最近修改:
|
|
* - 2026-01-14: 架构重构 - 从business层移动到gateway层 (修改者: moyin)
|
|
* - 2026-01-12: 代码规范优化 - 创建缺失的守卫测试文件 (修改者: moyin)
|
|
*
|
|
* @author moyin
|
|
* @version 1.1.0
|
|
* @since 2026-01-12
|
|
* @lastModified 2026-01-14
|
|
*/
|
|
|
|
import { Test, TestingModule } from '@nestjs/testing';
|
|
import { ExecutionContext, UnauthorizedException } from '@nestjs/common';
|
|
import { JwtAuthGuard } from './jwt_auth.guard';
|
|
import { LoginCoreService } from '../../core/login_core/login_core.service';
|
|
|
|
describe('JwtAuthGuard', () => {
|
|
let guard: JwtAuthGuard;
|
|
let loginCoreService: jest.Mocked<LoginCoreService>;
|
|
let mockExecutionContext: jest.Mocked<ExecutionContext>;
|
|
let mockRequest: any;
|
|
|
|
beforeEach(async () => {
|
|
const mockLoginCoreService = {
|
|
verifyToken: jest.fn(),
|
|
};
|
|
|
|
const module: TestingModule = await Test.createTestingModule({
|
|
providers: [
|
|
JwtAuthGuard,
|
|
{
|
|
provide: LoginCoreService,
|
|
useValue: mockLoginCoreService,
|
|
},
|
|
],
|
|
}).compile();
|
|
|
|
guard = module.get<JwtAuthGuard>(JwtAuthGuard);
|
|
loginCoreService = module.get(LoginCoreService);
|
|
|
|
// Mock request object
|
|
mockRequest = {
|
|
headers: {},
|
|
user: undefined,
|
|
};
|
|
|
|
// Mock execution context
|
|
mockExecutionContext = {
|
|
switchToHttp: jest.fn().mockReturnValue({
|
|
getRequest: jest.fn().mockReturnValue(mockRequest),
|
|
}),
|
|
} as any;
|
|
});
|
|
|
|
it('should be defined', () => {
|
|
expect(guard).toBeDefined();
|
|
});
|
|
|
|
describe('canActivate', () => {
|
|
it('should allow access with valid JWT token', async () => {
|
|
const mockPayload = {
|
|
sub: '1',
|
|
username: 'testuser',
|
|
role: 1,
|
|
type: 'access' as const,
|
|
iat: Math.floor(Date.now() / 1000),
|
|
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
};
|
|
|
|
mockRequest.headers.authorization = 'Bearer valid_jwt_token';
|
|
loginCoreService.verifyToken.mockResolvedValue(mockPayload);
|
|
|
|
const result = await guard.canActivate(mockExecutionContext);
|
|
|
|
expect(result).toBe(true);
|
|
expect(mockRequest.user).toEqual(mockPayload);
|
|
expect(loginCoreService.verifyToken).toHaveBeenCalledWith('valid_jwt_token', 'access');
|
|
});
|
|
|
|
it('should deny access when authorization header is missing', async () => {
|
|
mockRequest.headers.authorization = undefined;
|
|
|
|
await expect(guard.canActivate(mockExecutionContext))
|
|
.rejects.toThrow(UnauthorizedException);
|
|
|
|
expect(loginCoreService.verifyToken).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should deny access when token format is invalid', async () => {
|
|
mockRequest.headers.authorization = 'InvalidFormat token';
|
|
|
|
await expect(guard.canActivate(mockExecutionContext))
|
|
.rejects.toThrow(UnauthorizedException);
|
|
|
|
expect(loginCoreService.verifyToken).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should deny access when token is not Bearer type', async () => {
|
|
mockRequest.headers.authorization = 'Basic dXNlcjpwYXNz';
|
|
|
|
await expect(guard.canActivate(mockExecutionContext))
|
|
.rejects.toThrow(UnauthorizedException);
|
|
|
|
expect(loginCoreService.verifyToken).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should deny access when JWT token verification fails', async () => {
|
|
mockRequest.headers.authorization = 'Bearer invalid_jwt_token';
|
|
loginCoreService.verifyToken.mockRejectedValue(new Error('Token expired'));
|
|
|
|
await expect(guard.canActivate(mockExecutionContext))
|
|
.rejects.toThrow(UnauthorizedException);
|
|
|
|
expect(loginCoreService.verifyToken).toHaveBeenCalledWith('invalid_jwt_token', 'access');
|
|
});
|
|
|
|
it('should extract token correctly from Authorization header', async () => {
|
|
const mockPayload = {
|
|
sub: '1',
|
|
username: 'testuser',
|
|
role: 1,
|
|
type: 'access' as const,
|
|
iat: Math.floor(Date.now() / 1000),
|
|
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
};
|
|
|
|
mockRequest.headers.authorization = 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.test.token';
|
|
loginCoreService.verifyToken.mockResolvedValue(mockPayload);
|
|
|
|
const result = await guard.canActivate(mockExecutionContext);
|
|
|
|
expect(result).toBe(true);
|
|
expect(loginCoreService.verifyToken).toHaveBeenCalledWith(
|
|
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.test.token',
|
|
'access'
|
|
);
|
|
});
|
|
|
|
it('should handle empty token after Bearer', async () => {
|
|
mockRequest.headers.authorization = 'Bearer ';
|
|
|
|
await expect(guard.canActivate(mockExecutionContext))
|
|
.rejects.toThrow(UnauthorizedException);
|
|
|
|
expect(loginCoreService.verifyToken).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle authorization header with only Bearer', async () => {
|
|
mockRequest.headers.authorization = 'Bearer';
|
|
|
|
await expect(guard.canActivate(mockExecutionContext))
|
|
.rejects.toThrow(UnauthorizedException);
|
|
|
|
expect(loginCoreService.verifyToken).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|