/** * AdminOperationLogInterceptor 单元测试 * * 功能描述: * - 测试管理员操作日志拦截器的所有功能 * - 验证操作拦截和日志记录的正确性 * - 测试成功和失败场景的处理 * * 职责分离: * - 拦截器逻辑测试,不涉及具体业务 * - Mock日志服务,专注拦截器功能 * - 验证日志记录的完整性和准确性 * * 最近修改: * - 2026-01-09: 功能新增 - 创建AdminOperationLogInterceptor单元测试 (修改者: moyin) * * @author moyin * @version 1.0.0 * @since 2026-01-09 * @lastModified 2026-01-09 */ import { Test, TestingModule } from '@nestjs/testing'; import { ExecutionContext, CallHandler } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { of, throwError } from 'rxjs'; import { AdminOperationLogInterceptor } from './admin_operation_log.interceptor'; import { AdminOperationLogService } from './admin_operation_log.service'; import { LOG_ADMIN_OPERATION_KEY, LogAdminOperationOptions } from './log_admin_operation.decorator'; import { OPERATION_RESULTS } from './admin_constants'; describe('AdminOperationLogInterceptor', () => { let interceptor: AdminOperationLogInterceptor; let logService: jest.Mocked; let reflector: jest.Mocked; const mockLogService = { createLog: jest.fn(), }; const mockReflector = { get: jest.fn(), }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ AdminOperationLogInterceptor, { provide: AdminOperationLogService, useValue: mockLogService, }, { provide: Reflector, useValue: mockReflector, }, ], }).compile(); interceptor = module.get(AdminOperationLogInterceptor); logService = module.get(AdminOperationLogService); reflector = module.get(Reflector); }); afterEach(() => { jest.clearAllMocks(); }); const createMockExecutionContext = (requestData: any = {}) => { const mockRequest = { method: 'POST', url: '/admin/users', route: { path: '/admin/users' }, params: { id: '1' }, query: { limit: '10' }, body: { username: 'testuser' }, headers: { 'user-agent': 'test-agent' }, user: { id: 'admin1', username: 'admin' }, ip: '127.0.0.1', ...requestData, }; const mockResponse = {}; const mockContext = { switchToHttp: () => ({ getRequest: () => mockRequest, getResponse: () => mockResponse, }), getHandler: () => ({}), } as ExecutionContext; return { mockContext, mockRequest, mockResponse }; }; const createMockCallHandler = (responseData: any = { success: true }) => { return { handle: () => of(responseData), } as CallHandler; }; describe('intercept', () => { it('should pass through when no log options configured', (done) => { const { mockContext } = createMockExecutionContext(); const mockHandler = createMockCallHandler(); reflector.get.mockReturnValue(undefined); interceptor.intercept(mockContext, mockHandler).subscribe({ next: (result) => { expect(result).toEqual({ success: true }); expect(logService.createLog).not.toHaveBeenCalled(); done(); }, }); }); it('should log successful operation', (done) => { const { mockContext } = createMockExecutionContext(); const mockHandler = createMockCallHandler({ success: true, data: { id: '1' } }); const logOptions: LogAdminOperationOptions = { operationType: 'CREATE', targetType: 'users', description: 'Create user', }; reflector.get.mockReturnValue(logOptions); logService.createLog.mockResolvedValue({} as any); interceptor.intercept(mockContext, mockHandler).subscribe({ next: (result) => { expect(result).toEqual({ success: true, data: { id: '1' } }); // 验证日志记录调用 expect(logService.createLog).toHaveBeenCalledWith( expect.objectContaining({ adminUserId: 'admin1', adminUsername: 'admin', operationType: 'CREATE', targetType: 'users', operationDescription: 'Create user', httpMethodPath: 'POST /admin/users', operationResult: OPERATION_RESULTS.SUCCESS, targetId: '1', requestParams: expect.objectContaining({ params: { id: '1' }, query: { limit: '10' }, body: { username: 'testuser' }, }), afterData: { success: true, data: { id: '1' } }, clientIp: '127.0.0.1', userAgent: 'test-agent', }) ); done(); }, }); }); it('should log failed operation', (done) => { const { mockContext } = createMockExecutionContext(); const error = new Error('Operation failed'); const mockHandler = { handle: () => throwError(() => error), } as CallHandler; const logOptions: LogAdminOperationOptions = { operationType: 'UPDATE', targetType: 'users', description: 'Update user', }; reflector.get.mockReturnValue(logOptions); logService.createLog.mockResolvedValue({} as any); interceptor.intercept(mockContext, mockHandler).subscribe({ error: (err) => { expect(err).toBe(error); // 验证错误日志记录调用 expect(logService.createLog).toHaveBeenCalledWith( expect.objectContaining({ adminUserId: 'admin1', adminUsername: 'admin', operationType: 'UPDATE', targetType: 'users', operationDescription: 'Update user', operationResult: OPERATION_RESULTS.FAILED, errorMessage: 'Operation failed', errorCode: 'UNKNOWN_ERROR', }) ); done(); }, }); }); it('should handle missing admin user', (done) => { const { mockContext } = createMockExecutionContext({ user: undefined }); const mockHandler = createMockCallHandler(); const logOptions: LogAdminOperationOptions = { operationType: 'QUERY', targetType: 'users', description: 'Query users', }; reflector.get.mockReturnValue(logOptions); logService.createLog.mockResolvedValue({} as any); interceptor.intercept(mockContext, mockHandler).subscribe({ next: () => { expect(logService.createLog).toHaveBeenCalledWith( expect.objectContaining({ adminUserId: 'unknown', adminUsername: 'unknown', }) ); done(); }, }); }); it('should handle sensitive operations', (done) => { const { mockContext } = createMockExecutionContext(); const mockHandler = createMockCallHandler(); const logOptions: LogAdminOperationOptions = { operationType: 'DELETE', targetType: 'users', description: 'Delete user', isSensitive: true, }; reflector.get.mockReturnValue(logOptions); logService.createLog.mockResolvedValue({} as any); interceptor.intercept(mockContext, mockHandler).subscribe({ next: () => { expect(logService.createLog).toHaveBeenCalledWith( expect.objectContaining({ isSensitive: true, }) ); done(); }, }); }); it('should disable request params capture when configured', (done) => { const { mockContext } = createMockExecutionContext(); const mockHandler = createMockCallHandler(); const logOptions: LogAdminOperationOptions = { operationType: 'QUERY', targetType: 'users', description: 'Query users', captureRequestParams: false, }; reflector.get.mockReturnValue(logOptions); logService.createLog.mockResolvedValue({} as any); interceptor.intercept(mockContext, mockHandler).subscribe({ next: () => { expect(logService.createLog).toHaveBeenCalledWith( expect.objectContaining({ requestParams: undefined, }) ); done(); }, }); }); it('should disable after data capture when configured', (done) => { const { mockContext } = createMockExecutionContext(); const mockHandler = createMockCallHandler({ data: 'sensitive' }); const logOptions: LogAdminOperationOptions = { operationType: 'QUERY', targetType: 'users', description: 'Query users', captureAfterData: false, }; reflector.get.mockReturnValue(logOptions); logService.createLog.mockResolvedValue({} as any); interceptor.intercept(mockContext, mockHandler).subscribe({ next: () => { expect(logService.createLog).toHaveBeenCalledWith( expect.objectContaining({ afterData: undefined, }) ); done(); }, }); }); it('should extract affected records from response', (done) => { const { mockContext } = createMockExecutionContext(); const responseData = { success: true, data: { items: [{ id: '1' }, { id: '2' }, { id: '3' }], total: 3, }, }; const mockHandler = createMockCallHandler(responseData); const logOptions: LogAdminOperationOptions = { operationType: 'QUERY', targetType: 'users', description: 'Query users', }; reflector.get.mockReturnValue(logOptions); logService.createLog.mockResolvedValue({} as any); interceptor.intercept(mockContext, mockHandler).subscribe({ next: () => { expect(logService.createLog).toHaveBeenCalledWith( expect.objectContaining({ affectedRecords: 3, // Should extract from items array length }) ); done(); }, }); }); it('should handle log service errors gracefully', (done) => { const { mockContext } = createMockExecutionContext(); const mockHandler = createMockCallHandler(); const logOptions: LogAdminOperationOptions = { operationType: 'CREATE', targetType: 'users', description: 'Create user', }; reflector.get.mockReturnValue(logOptions); logService.createLog.mockRejectedValue(new Error('Log service error')); // 即使日志记录失败,原始操作也应该成功 interceptor.intercept(mockContext, mockHandler).subscribe({ next: (result) => { expect(result).toEqual({ success: true }); expect(logService.createLog).toHaveBeenCalled(); done(); }, }); }); it('should extract target ID from different sources', (done) => { const { mockContext } = createMockExecutionContext({ params: {}, body: { id: 'body-id' }, query: { id: 'query-id' }, }); const mockHandler = createMockCallHandler(); const logOptions: LogAdminOperationOptions = { operationType: 'UPDATE', targetType: 'users', description: 'Update user', }; reflector.get.mockReturnValue(logOptions); logService.createLog.mockResolvedValue({} as any); interceptor.intercept(mockContext, mockHandler).subscribe({ next: () => { expect(logService.createLog).toHaveBeenCalledWith( expect.objectContaining({ targetId: 'body-id', // Should prefer body over query }) ); done(); }, }); }); it('should handle missing route information', (done) => { const { mockContext } = createMockExecutionContext({ route: undefined, url: '/admin/custom-endpoint', }); const mockHandler = createMockCallHandler(); const logOptions: LogAdminOperationOptions = { operationType: 'QUERY', targetType: 'custom', description: 'Custom operation', }; reflector.get.mockReturnValue(logOptions); logService.createLog.mockResolvedValue({} as any); interceptor.intercept(mockContext, mockHandler).subscribe({ next: () => { expect(logService.createLog).toHaveBeenCalledWith( expect.objectContaining({ httpMethodPath: 'POST /admin/custom-endpoint', }) ); done(); }, }); }); }); });