feat: 支持单元测试

This commit is contained in:
richarjiang
2026-04-08 16:02:19 +08:00
parent 2d0fee8a9a
commit df05b7280c
18 changed files with 2462 additions and 17 deletions

View File

@@ -0,0 +1,65 @@
import { ApiResponseDto } from './api-response.dto';
describe('ApiResponseDto', () => {
describe('success', () => {
it('should create a success response with data', () => {
const data = { id: '1', name: 'test' };
const response = ApiResponseDto.success(data);
expect(response.success).toBe(true);
expect(response.data).toEqual(data);
expect(response.message).toBeNull();
expect(response.timestamp).toBeInstanceOf(Date);
});
it('should create a success response with null data', () => {
const response = ApiResponseDto.success(null);
expect(response.success).toBe(true);
expect(response.data).toBeNull();
expect(response.message).toBeNull();
});
it('should create a success response with array data', () => {
const data = [1, 2, 3];
const response = ApiResponseDto.success(data);
expect(response.success).toBe(true);
expect(response.data).toEqual([1, 2, 3]);
});
});
describe('error', () => {
it('should create an error response with message', () => {
const response = ApiResponseDto.error('Something went wrong');
expect(response.success).toBe(false);
expect(response.data).toBeNull();
expect(response.message).toBe('Something went wrong');
expect(response.timestamp).toBeInstanceOf(Date);
});
it('should create an error response with Chinese message', () => {
const response = ApiResponseDto.error('积分不足');
expect(response.success).toBe(false);
expect(response.message).toBe('积分不足');
});
});
describe('constructor', () => {
it('should set default message to null', () => {
const dto = new ApiResponseDto(true, 'data');
expect(dto.message).toBeNull();
});
it('should set all properties correctly', () => {
const dto = new ApiResponseDto(false, null, 'error message');
expect(dto.success).toBe(false);
expect(dto.data).toBeNull();
expect(dto.message).toBe('error message');
});
});
});

View File

@@ -0,0 +1,97 @@
import { HttpExceptionFilter } from './http-exception.filter';
import {
HttpException,
HttpStatus,
ArgumentsHost,
BadRequestException,
NotFoundException,
} from '@nestjs/common';
describe('HttpExceptionFilter', () => {
let filter: HttpExceptionFilter;
const mockJson = jest.fn();
const mockStatus = jest.fn().mockReturnValue({ json: mockJson });
const mockGetResponse = jest.fn().mockReturnValue({ status: mockStatus });
const mockGetRequest = jest
.fn()
.mockReturnValue({ url: '/api/v1/test' });
const mockHost: ArgumentsHost = {
switchToHttp: () => ({
getResponse: mockGetResponse,
getRequest: mockGetRequest,
}),
} as unknown as ArgumentsHost;
beforeEach(() => {
filter = new HttpExceptionFilter();
jest.clearAllMocks();
mockStatus.mockReturnValue({ json: mockJson });
});
it('should handle HttpException with string response', () => {
const exception = new HttpException('Not Found', HttpStatus.NOT_FOUND);
filter.catch(exception, mockHost);
expect(mockStatus).toHaveBeenCalledWith(HttpStatus.NOT_FOUND);
expect(mockJson).toHaveBeenCalledWith(
expect.objectContaining({
success: false,
data: null,
path: '/api/v1/test',
}),
);
});
it('should handle BadRequestException with object response', () => {
const exception = new BadRequestException('参数错误');
filter.catch(exception, mockHost);
expect(mockStatus).toHaveBeenCalledWith(HttpStatus.BAD_REQUEST);
expect(mockJson).toHaveBeenCalledWith(
expect.objectContaining({
success: false,
path: '/api/v1/test',
}),
);
});
it('should handle NotFoundException', () => {
const exception = new NotFoundException('资源不存在');
filter.catch(exception, mockHost);
expect(mockStatus).toHaveBeenCalledWith(HttpStatus.NOT_FOUND);
});
it('should handle generic Error with 500 status', () => {
const exception = new Error('Unexpected error');
filter.catch(exception, mockHost);
expect(mockStatus).toHaveBeenCalledWith(HttpStatus.INTERNAL_SERVER_ERROR);
expect(mockJson).toHaveBeenCalledWith(
expect.objectContaining({
success: false,
message: 'Unexpected error',
path: '/api/v1/test',
}),
);
});
it('should handle unknown exception with 500 status', () => {
filter.catch('unexpected string error', mockHost);
expect(mockStatus).toHaveBeenCalledWith(HttpStatus.INTERNAL_SERVER_ERROR);
expect(mockJson).toHaveBeenCalledWith(
expect.objectContaining({
success: false,
message: 'Internal server error',
path: '/api/v1/test',
}),
);
});
});

View File

@@ -0,0 +1,79 @@
import { JwtAuthGuard, JwtPayload } from './jwt-auth.guard';
import { JwtService } from '@nestjs/jwt';
import { ExecutionContext, UnauthorizedException } from '@nestjs/common';
describe('JwtAuthGuard', () => {
let guard: JwtAuthGuard;
const mockJwtService = {
verifyAsync: jest.fn(),
};
beforeEach(() => {
guard = new JwtAuthGuard(mockJwtService as unknown as JwtService);
});
afterEach(() => {
jest.clearAllMocks();
});
function createMockContext(authHeader?: string): ExecutionContext {
const mockRequest: Record<string, unknown> = {
headers: authHeader ? { authorization: authHeader } : {},
};
return {
switchToHttp: () => ({
getRequest: () => mockRequest,
}),
} as unknown as ExecutionContext;
}
it('should allow access with valid Bearer token', async () => {
const payload: JwtPayload = { sub: 'user-1', openid: 'wx-openid' };
mockJwtService.verifyAsync.mockResolvedValue(payload);
const context = createMockContext('Bearer valid-token');
const result = await guard.canActivate(context);
expect(result).toBe(true);
expect(mockJwtService.verifyAsync).toHaveBeenCalledWith('valid-token');
});
it('should attach user payload to request', async () => {
const payload: JwtPayload = { sub: 'user-1', openid: 'wx-openid' };
mockJwtService.verifyAsync.mockResolvedValue(payload);
const context = createMockContext('Bearer valid-token');
const request = context.switchToHttp().getRequest() as Record<string, unknown>;
await guard.canActivate(context);
expect(request.user).toEqual(payload);
});
it('should throw UnauthorizedException when no Authorization header', async () => {
const context = createMockContext();
await expect(guard.canActivate(context)).rejects.toThrow(
UnauthorizedException,
);
});
it('should throw UnauthorizedException when token type is not Bearer', async () => {
const context = createMockContext('Basic some-token');
await expect(guard.canActivate(context)).rejects.toThrow(
UnauthorizedException,
);
});
it('should throw UnauthorizedException when token is invalid', async () => {
mockJwtService.verifyAsync.mockRejectedValue(new Error('invalid token'));
const context = createMockContext('Bearer invalid-token');
await expect(guard.canActivate(context)).rejects.toThrow(
UnauthorizedException,
);
});
});