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

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

407 lines
13 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.
/**
* AdminOperationLogService 单元测试
*
* 功能描述:
* - 测试管理员操作日志服务的所有方法
* - 验证日志记录和查询的正确性
* - 测试统计功能和清理功能
*
* 职责分离:
* - 业务逻辑测试不涉及HTTP层
* - Mock数据库操作专注服务逻辑
* - 验证日志处理的正确性
*
* 最近修改:
* - 2026-01-09: 功能新增 - 创建AdminOperationLogService单元测试 (修改者: moyin)
*
* @author moyin
* @version 1.0.0
* @since 2026-01-09
* @lastModified 2026-01-09
*/
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { AdminOperationLogService, CreateLogParams, LogQueryParams } from './admin_operation_log.service';
import { AdminOperationLog } from './admin_operation_log.entity';
describe('AdminOperationLogService', () => {
let service: AdminOperationLogService;
let repository: jest.Mocked<Repository<AdminOperationLog>>;
const mockRepository = {
create: jest.fn(),
save: jest.fn(),
createQueryBuilder: jest.fn(),
findOne: jest.fn(),
find: jest.fn(),
findAndCount: jest.fn(),
};
const mockQueryBuilder = {
andWhere: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
limit: jest.fn().mockReturnThis(),
offset: jest.fn().mockReturnThis(),
getManyAndCount: jest.fn(),
getCount: jest.fn(),
clone: jest.fn().mockReturnThis(),
select: jest.fn().mockReturnThis(),
addSelect: jest.fn().mockReturnThis(),
groupBy: jest.fn().mockReturnThis(),
getRawMany: jest.fn(),
getRawOne: jest.fn(),
delete: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
execute: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
AdminOperationLogService,
{
provide: getRepositoryToken(AdminOperationLog),
useValue: mockRepository,
},
],
}).compile();
service = module.get<AdminOperationLogService>(AdminOperationLogService);
repository = module.get(getRepositoryToken(AdminOperationLog));
// Setup default mock behavior
mockRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('createLog', () => {
it('should create log successfully', async () => {
const logParams: CreateLogParams = {
adminUserId: 'admin1',
adminUsername: 'admin',
operationType: 'CREATE',
targetType: 'users',
targetId: '1',
operationDescription: 'Create user',
httpMethodPath: 'POST /admin/users',
operationResult: 'SUCCESS',
durationMs: 100,
requestId: 'req_123',
};
const mockLog = {
id: 'log1',
admin_user_id: logParams.adminUserId,
admin_username: logParams.adminUsername,
operation_type: logParams.operationType,
target_type: logParams.targetType,
target_id: logParams.targetId,
operation_description: logParams.operationDescription,
http_method_path: logParams.httpMethodPath,
operation_result: logParams.operationResult,
duration_ms: logParams.durationMs,
request_id: logParams.requestId,
is_sensitive: false,
affected_records: 0,
created_at: new Date(),
updated_at: new Date()
} as AdminOperationLog;
mockRepository.create.mockReturnValue(mockLog);
mockRepository.save.mockResolvedValue(mockLog);
const result = await service.createLog(logParams);
expect(mockRepository.create).toHaveBeenCalledWith({
admin_user_id: logParams.adminUserId,
admin_username: logParams.adminUsername,
operation_type: logParams.operationType,
target_type: logParams.targetType,
target_id: logParams.targetId,
operation_description: logParams.operationDescription,
http_method_path: logParams.httpMethodPath,
request_params: logParams.requestParams,
before_data: logParams.beforeData,
after_data: logParams.afterData,
operation_result: logParams.operationResult,
error_message: logParams.errorMessage,
error_code: logParams.errorCode,
duration_ms: logParams.durationMs,
client_ip: logParams.clientIp,
user_agent: logParams.userAgent,
request_id: logParams.requestId,
context: logParams.context,
is_sensitive: false,
affected_records: 0,
batch_id: logParams.batchId,
});
expect(mockRepository.save).toHaveBeenCalledWith(mockLog);
expect(result).toEqual(mockLog);
});
it('should handle creation error', async () => {
const logParams: CreateLogParams = {
adminUserId: 'admin1',
adminUsername: 'admin',
operationType: 'CREATE',
targetType: 'users',
operationDescription: 'Create user',
httpMethodPath: 'POST /admin/users',
operationResult: 'SUCCESS',
durationMs: 100,
requestId: 'req_123',
};
mockRepository.create.mockReturnValue({} as AdminOperationLog);
mockRepository.save.mockRejectedValue(new Error('Database error'));
await expect(service.createLog(logParams)).rejects.toThrow('Database error');
});
});
describe('queryLogs', () => {
it('should query logs successfully', async () => {
const queryParams: LogQueryParams = {
adminUserId: 'admin1',
operationType: 'CREATE',
limit: 10,
offset: 0,
};
const mockLogs = [
{ id: 'log1', admin_user_id: 'admin1' },
{ id: 'log2', admin_user_id: 'admin1' },
] as AdminOperationLog[];
mockQueryBuilder.getManyAndCount.mockResolvedValue([mockLogs, 2]);
const result = await service.queryLogs(queryParams);
expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('log.admin_user_id = :adminUserId', { adminUserId: 'admin1' });
expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('log.operation_type = :operationType', { operationType: 'CREATE' });
expect(mockQueryBuilder.orderBy).toHaveBeenCalledWith('log.created_at', 'DESC');
expect(mockQueryBuilder.limit).toHaveBeenCalledWith(10);
expect(mockQueryBuilder.offset).toHaveBeenCalledWith(0);
expect(result.logs).toEqual(mockLogs);
expect(result.total).toBe(2);
});
it('should query logs with date range', async () => {
const startDate = new Date('2026-01-01');
const endDate = new Date('2026-01-31');
const queryParams: LogQueryParams = {
startDate,
endDate,
isSensitive: true,
};
const mockLogs = [] as AdminOperationLog[];
mockQueryBuilder.getManyAndCount.mockResolvedValue([mockLogs, 0]);
const result = await service.queryLogs(queryParams);
expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('log.created_at BETWEEN :startDate AND :endDate', {
startDate,
endDate
});
expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('log.is_sensitive = :isSensitive', { isSensitive: true });
});
it('should handle query error', async () => {
const queryParams: LogQueryParams = {};
mockQueryBuilder.getManyAndCount.mockRejectedValue(new Error('Query error'));
await expect(service.queryLogs(queryParams)).rejects.toThrow('Query error');
});
});
describe('getLogById', () => {
it('should get log by id successfully', async () => {
const mockLog = { id: 'log1', admin_user_id: 'admin1' } as AdminOperationLog;
mockRepository.findOne.mockResolvedValue(mockLog);
const result = await service.getLogById('log1');
expect(mockRepository.findOne).toHaveBeenCalledWith({ where: { id: 'log1' } });
expect(result).toEqual(mockLog);
});
it('should return null when log not found', async () => {
mockRepository.findOne.mockResolvedValue(null);
const result = await service.getLogById('nonexistent');
expect(result).toBeNull();
});
it('should handle get error', async () => {
mockRepository.findOne.mockRejectedValue(new Error('Database error'));
await expect(service.getLogById('log1')).rejects.toThrow('Database error');
});
});
describe('getStatistics', () => {
it('should get statistics successfully', async () => {
// Mock basic statistics
mockQueryBuilder.getCount
.mockResolvedValueOnce(100) // total
.mockResolvedValueOnce(80) // successful
.mockResolvedValueOnce(10); // sensitive
// Mock operation type statistics
mockQueryBuilder.getRawMany.mockResolvedValueOnce([
{ type: 'CREATE', count: '50' },
{ type: 'UPDATE', count: '30' },
{ type: 'DELETE', count: '20' },
]);
// Mock target type statistics
mockQueryBuilder.getRawMany.mockResolvedValueOnce([
{ type: 'users', count: '60' },
{ type: 'profiles', count: '40' },
]);
// Mock performance statistics
mockQueryBuilder.getRawOne
.mockResolvedValueOnce({ avgDuration: '150.5' }) // average duration
.mockResolvedValueOnce({ uniqueAdmins: '5' }); // unique admins
const result = await service.getStatistics();
expect(result.totalOperations).toBe(100);
expect(result.successfulOperations).toBe(80);
expect(result.failedOperations).toBe(20);
expect(result.sensitiveOperations).toBe(10);
expect(result.operationsByType).toEqual({
CREATE: 50,
UPDATE: 30,
DELETE: 20,
});
expect(result.operationsByTarget).toEqual({
users: 60,
profiles: 40,
});
expect(result.averageDuration).toBe(150.5);
expect(result.uniqueAdmins).toBe(5);
});
it('should get statistics with date range', async () => {
const startDate = new Date('2026-01-01');
const endDate = new Date('2026-01-31');
mockQueryBuilder.getCount.mockResolvedValue(50);
mockQueryBuilder.getRawMany.mockResolvedValue([]);
mockQueryBuilder.getRawOne.mockResolvedValue({ avgDuration: '100', uniqueAdmins: '3' });
const result = await service.getStatistics(startDate, endDate);
expect(mockQueryBuilder.where).toHaveBeenCalledWith('log.created_at BETWEEN :startDate AND :endDate', {
startDate,
endDate
});
expect(result.totalOperations).toBe(50);
});
});
describe('cleanupExpiredLogs', () => {
it('should cleanup expired logs successfully', async () => {
mockQueryBuilder.execute.mockResolvedValue({ affected: 25 });
const result = await service.cleanupExpiredLogs(30);
expect(mockQueryBuilder.delete).toHaveBeenCalled();
expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('is_sensitive = :sensitive', { sensitive: false });
expect(result).toBe(25);
});
it('should use default retention days', async () => {
mockQueryBuilder.execute.mockResolvedValue({ affected: 10 });
const result = await service.cleanupExpiredLogs();
expect(result).toBe(10);
});
it('should handle cleanup error', async () => {
mockQueryBuilder.execute.mockRejectedValue(new Error('Cleanup error'));
await expect(service.cleanupExpiredLogs(30)).rejects.toThrow('Cleanup error');
});
});
describe('getAdminOperationHistory', () => {
it('should get admin operation history successfully', async () => {
const mockLogs = [
{ id: 'log1', admin_user_id: 'admin1' },
{ id: 'log2', admin_user_id: 'admin1' },
] as AdminOperationLog[];
mockRepository.find.mockResolvedValue(mockLogs);
const result = await service.getAdminOperationHistory('admin1', 10);
expect(mockRepository.find).toHaveBeenCalledWith({
where: { admin_user_id: 'admin1' },
order: { created_at: 'DESC' },
take: 10
});
expect(result).toEqual(mockLogs);
});
it('should use default limit', async () => {
mockRepository.find.mockResolvedValue([]);
const result = await service.getAdminOperationHistory('admin1');
expect(mockRepository.find).toHaveBeenCalledWith({
where: { admin_user_id: 'admin1' },
order: { created_at: 'DESC' },
take: 20 // DEFAULT_LIMIT
});
});
});
describe('getSensitiveOperations', () => {
it('should get sensitive operations successfully', async () => {
const mockLogs = [
{ id: 'log1', is_sensitive: true },
] as AdminOperationLog[];
mockRepository.findAndCount.mockResolvedValue([mockLogs, 1]);
const result = await service.getSensitiveOperations(10, 0);
expect(mockRepository.findAndCount).toHaveBeenCalledWith({
where: { is_sensitive: true },
order: { created_at: 'DESC' },
take: 10,
skip: 0
});
expect(result.logs).toEqual(mockLogs);
expect(result.total).toBe(1);
});
it('should use default pagination', async () => {
mockRepository.findAndCount.mockResolvedValue([[], 0]);
const result = await service.getSensitiveOperations();
expect(mockRepository.findAndCount).toHaveBeenCalledWith({
where: { is_sensitive: true },
order: { created_at: 'DESC' },
take: 50, // DEFAULT_LIMIT
skip: 0
});
});
});
});