Files
whale-town-end/src/business/admin/admin_operation_log.interceptor.spec.ts
moyin 5f662ef091 feat: 完善管理员系统和用户管理模块
- 更新管理员控制器和数据库管理功能
- 完善管理员操作日志系统
- 添加全面的属性测试覆盖
- 优化用户管理和用户档案服务
- 更新代码检查规范文档

功能改进:
- 增强管理员权限验证
- 完善操作日志记录
- 优化数据库管理接口
- 提升系统安全性和可维护性
2026-01-09 17:05:08 +08:00

415 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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<AdminOperationLogService>;
let reflector: jest.Mocked<Reflector>;
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>(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();
},
});
});
});
});