/** * ThrottleGuard 单元测试 * * @author moyin * @version 1.0.0 * @since 2026-01-07 */ import { Test, TestingModule } from '@nestjs/testing'; import { ExecutionContext, HttpException, HttpStatus } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { ThrottleGuard } from './throttle.guard'; import { ThrottleConfig } from './throttle.decorator'; describe('ThrottleGuard', () => { let guard: ThrottleGuard; let reflector: jest.Mocked; beforeEach(async () => { const mockReflector = { get: jest.fn(), }; const module: TestingModule = await Test.createTestingModule({ providers: [ ThrottleGuard, { provide: Reflector, useValue: mockReflector }, ], }).compile(); guard = module.get(ThrottleGuard); reflector = module.get(Reflector); }); afterEach(() => { jest.clearAllMocks(); guard.clearAllRecords(); // 确保清理定时器 guard.onModuleDestroy(); }); describe('canActivate', () => { it('should allow request when no throttle config is found', async () => { // Arrange reflector.get.mockReturnValue(null); const mockContext = createMockContext(); // Act const result = await guard.canActivate(mockContext); // Assert expect(result).toBe(true); }); it('should allow first request within limit', async () => { // Arrange const config: ThrottleConfig = { limit: 5, ttl: 60 }; reflector.get.mockReturnValueOnce(config).mockReturnValueOnce(null); const mockContext = createMockContext(); // Act const result = await guard.canActivate(mockContext); // Assert expect(result).toBe(true); }); it('should throw HttpException when limit exceeded', async () => { // Arrange const config: ThrottleConfig = { limit: 1, ttl: 60 }; reflector.get.mockReturnValue(config); const mockContext = createMockContext(); // Act - first request should pass await guard.canActivate(mockContext); // Assert - second request should throw await expect(guard.canActivate(mockContext)).rejects.toThrow(HttpException); }); }); describe('getStats', () => { it('should return empty stats initially', () => { const stats = guard.getStats(); expect(stats.totalRecords).toBe(0); }); }); describe('clearAllRecords', () => { it('should clear all records', () => { guard.clearAllRecords(); const stats = guard.getStats(); expect(stats.totalRecords).toBe(0); }); }); describe('onModuleDestroy', () => { it('should cleanup resources', () => { expect(() => guard.onModuleDestroy()).not.toThrow(); }); }); function createMockContext(): ExecutionContext { const mockRequest = { ip: '127.0.0.1', method: 'POST', url: '/api/test', route: { path: '/api/test' }, get: jest.fn(), }; return { switchToHttp: jest.fn().mockReturnValue({ getRequest: jest.fn().mockReturnValue(mockRequest), }), getHandler: jest.fn(), getClass: jest.fn(), } as any; } });