Files
whale-town-end/src/business/admin/operation_logging.property.spec.ts
moyin 6924416bbd feat:实现管理员系统核心功能
- 添加管理员数据库管理控制器和服务
- 实现管理员操作日志记录系统
- 添加数据库异常处理过滤器
- 完善管理员权限验证和响应格式
- 添加全面的属性测试覆盖
2026-01-08 23:05:34 +08:00

509 lines
19 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.
/**
* 操作日志属性测试
*
* Property 11: 操作日志完整性
*
* Validates: Requirements 7.1, 7.2, 7.3, 7.4, 7.5
*
* 测试目标:
* - 验证操作日志记录的完整性和准确性
* - 确保敏感操作被正确记录
* - 验证日志查询和统计功能
*
* 最近修改:
* - 2026-01-08: 注释规范优化 - 修正@author字段更新版本号和修改记录 (修改者: moyin)
* - 2026-01-08: 功能新增 - 创建操作日志属性测试 (修改者: assistant)
*
* @author moyin
* @version 1.0.1
* @since 2026-01-08
* @lastModified 2026-01-08
*/
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AdminDatabaseController } from '../../controllers/admin_database.controller';
import { AdminOperationLogController } from '../../controllers/admin_operation_log.controller';
import { DatabaseManagementService } from '../../services/database_management.service';
import { AdminOperationLogService } from '../../services/admin_operation_log.service';
import { AdminOperationLogInterceptor } from '../../admin_operation_log.interceptor';
import { AdminDatabaseExceptionFilter } from '../../admin_database_exception.filter';
import { AdminGuard } from '../../admin.guard';
import { UserStatus } from '../../../../core/db/users/user_status.enum';
import {
PropertyTestRunner,
PropertyTestGenerators,
PropertyTestAssertions,
DEFAULT_PROPERTY_CONFIG
} from './admin_property_test.base';
describe('Property Test: 操作日志功能', () => {
let app: INestApplication;
let module: TestingModule;
let databaseController: AdminDatabaseController;
let logController: AdminOperationLogController;
let mockLogService: any;
let logEntries: any[] = [];
beforeAll(async () => {
logEntries = [];
mockLogService = {
createLog: jest.fn().mockImplementation((logData) => {
const logEntry = {
id: `log_${logEntries.length + 1}`,
...logData,
created_at: new Date().toISOString()
};
logEntries.push(logEntry);
return Promise.resolve(logEntry);
}),
queryLogs: jest.fn().mockImplementation((filters, limit, offset) => {
let filteredLogs = [...logEntries];
if (filters.operation_type) {
filteredLogs = filteredLogs.filter(log => log.operation_type === filters.operation_type);
}
if (filters.admin_id) {
filteredLogs = filteredLogs.filter(log => log.admin_id === filters.admin_id);
}
if (filters.entity_type) {
filteredLogs = filteredLogs.filter(log => log.entity_type === filters.entity_type);
}
const total = filteredLogs.length;
const paginatedLogs = filteredLogs.slice(offset, offset + limit);
return Promise.resolve({ logs: paginatedLogs, total });
}),
getLogById: jest.fn().mockImplementation((id) => {
const log = logEntries.find(entry => entry.id === id);
return Promise.resolve(log || null);
}),
getStatistics: jest.fn().mockImplementation(() => {
const stats = {
totalOperations: logEntries.length,
operationsByType: {},
operationsByAdmin: {},
recentActivity: logEntries.slice(-10)
};
logEntries.forEach(log => {
stats.operationsByType[log.operation_type] =
(stats.operationsByType[log.operation_type] || 0) + 1;
stats.operationsByAdmin[log.admin_id] =
(stats.operationsByAdmin[log.admin_id] || 0) + 1;
});
return Promise.resolve(stats);
}),
cleanupExpiredLogs: jest.fn().mockResolvedValue(0),
getAdminOperationHistory: jest.fn().mockImplementation((adminId) => {
const adminLogs = logEntries.filter(log => log.admin_id === adminId);
return Promise.resolve(adminLogs);
}),
getSensitiveOperations: jest.fn().mockImplementation((limit, offset) => {
const sensitiveOps = ['DELETE', 'BATCH_UPDATE', 'ADMIN_CREATE'];
const sensitiveLogs = logEntries.filter(log =>
sensitiveOps.includes(log.operation_type)
);
const total = sensitiveLogs.length;
const paginatedLogs = sensitiveLogs.slice(offset, offset + limit);
return Promise.resolve({ logs: paginatedLogs, total });
})
};
module = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: ['.env.test', '.env']
})
],
controllers: [AdminDatabaseController, AdminOperationLogController],
providers: [
DatabaseManagementService,
{
provide: AdminOperationLogService,
useValue: mockLogService
},
{
provide: AdminOperationLogInterceptor,
useValue: {
intercept: jest.fn().mockImplementation((context, next) => next.handle())
}
},
{
provide: 'UsersService',
useValue: {
findAll: jest.fn().mockResolvedValue([]),
findOne: jest.fn().mockImplementation(() => {
const user = PropertyTestGenerators.generateUser();
return Promise.resolve({ ...user, id: BigInt(1) });
}),
create: jest.fn().mockImplementation((userData) => {
return Promise.resolve({ ...userData, id: BigInt(1) });
}),
update: jest.fn().mockImplementation((id, updateData) => {
const user = PropertyTestGenerators.generateUser();
return Promise.resolve({ ...user, ...updateData, id });
}),
remove: jest.fn().mockResolvedValue(undefined),
search: jest.fn().mockResolvedValue([]),
count: jest.fn().mockResolvedValue(0)
}
},
{
provide: 'IUserProfilesService',
useValue: {
findAll: jest.fn().mockResolvedValue([]),
findOne: jest.fn().mockResolvedValue({ id: BigInt(1) }),
create: jest.fn().mockResolvedValue({ id: BigInt(1) }),
update: jest.fn().mockResolvedValue({ id: BigInt(1) }),
remove: jest.fn().mockResolvedValue(undefined),
findByMap: jest.fn().mockResolvedValue([]),
count: jest.fn().mockResolvedValue(0)
}
},
{
provide: 'ZulipAccountsService',
useValue: {
findMany: jest.fn().mockResolvedValue({ accounts: [] }),
findById: jest.fn().mockResolvedValue({ id: '1' }),
create: jest.fn().mockResolvedValue({ id: '1' }),
update: jest.fn().mockResolvedValue({ id: '1' }),
delete: jest.fn().mockResolvedValue(undefined),
getStatusStatistics: jest.fn().mockResolvedValue({
active: 0, inactive: 0, suspended: 0, error: 0, total: 0
})
}
}
]
})
.overrideGuard(AdminGuard)
.useValue({ canActivate: () => true })
.compile();
app = module.createNestApplication();
app.useGlobalFilters(new AdminDatabaseExceptionFilter());
await app.init();
databaseController = module.get<AdminDatabaseController>(AdminDatabaseController);
logController = module.get<AdminOperationLogController>(AdminOperationLogController);
});
afterAll(async () => {
await app.close();
});
beforeEach(() => {
logEntries.length = 0; // 清空日志记录
});
describe('Property 11: 操作日志完整性', () => {
it('所有CRUD操作都应该生成日志记录', async () => {
await PropertyTestRunner.runPropertyTest(
'CRUD操作日志记录完整性',
() => PropertyTestGenerators.generateUser(),
async (userData) => {
const userWithStatus = { ...userData, status: UserStatus.ACTIVE };
// 执行创建操作
await databaseController.createUser(userWithStatus);
// 执行读取操作
await databaseController.getUserById('1');
// 执行更新操作
await databaseController.updateUser('1', { nickname: 'Updated Name' });
// 执行删除操作
await databaseController.deleteUser('1');
// 验证日志记录
expect(mockLogService.createLog).toHaveBeenCalledTimes(4);
// 验证日志内容包含必要信息
const createLogCall = mockLogService.createLog.mock.calls.find(call =>
call[0].operation_type === 'CREATE'
);
const updateLogCall = mockLogService.createLog.mock.calls.find(call =>
call[0].operation_type === 'UPDATE'
);
const deleteLogCall = mockLogService.createLog.mock.calls.find(call =>
call[0].operation_type === 'DELETE'
);
expect(createLogCall).toBeDefined();
expect(updateLogCall).toBeDefined();
expect(deleteLogCall).toBeDefined();
// 验证日志包含实体信息
expect(createLogCall[0].entity_type).toBe('User');
expect(updateLogCall[0].entity_type).toBe('User');
expect(deleteLogCall[0].entity_type).toBe('User');
},
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 20 }
);
});
it('日志记录应该包含完整的操作上下文', async () => {
await PropertyTestRunner.runPropertyTest(
'日志上下文完整性',
() => ({
user: PropertyTestGenerators.generateUser(),
adminId: `admin_${Math.floor(Math.random() * 1000)}`,
ipAddress: `192.168.1.${Math.floor(Math.random() * 255)}`,
userAgent: 'Test-Agent/1.0'
}),
async ({ user, adminId, ipAddress, userAgent }) => {
const userWithStatus = { ...user, status: UserStatus.ACTIVE };
// 模拟带上下文的操作
await databaseController.createUser(userWithStatus);
// 验证日志记录包含上下文信息
expect(mockLogService.createLog).toHaveBeenCalled();
const logCall = mockLogService.createLog.mock.calls[0][0];
expect(logCall).toHaveProperty('operation_type');
expect(logCall).toHaveProperty('entity_type');
expect(logCall).toHaveProperty('entity_id');
expect(logCall).toHaveProperty('admin_id');
expect(logCall).toHaveProperty('operation_details');
expect(logCall).toHaveProperty('timestamp');
// 验证时间戳格式
expect(new Date(logCall.timestamp)).toBeInstanceOf(Date);
},
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 25 }
);
});
it('敏感操作应该记录详细的前后状态', async () => {
await PropertyTestRunner.runPropertyTest(
'敏感操作详细日志',
() => ({
accounts: Array.from({ length: Math.floor(Math.random() * 5) + 2 },
() => PropertyTestGenerators.generateZulipAccount()),
targetStatus: ['active', 'inactive', 'suspended'][Math.floor(Math.random() * 3)]
}),
async ({ accounts, targetStatus }) => {
const accountIds = accounts.map((_, i) => `account_${i + 1}`);
// 执行批量更新操作(敏感操作)
await databaseController.batchUpdateZulipAccountStatus({
ids: accountIds,
status: targetStatus as any,
reason: '测试批量更新'
});
// 验证敏感操作日志
expect(mockLogService.createLog).toHaveBeenCalled();
const logCall = mockLogService.createLog.mock.calls[0][0];
expect(logCall.operation_type).toBe('BATCH_UPDATE');
expect(logCall.entity_type).toBe('ZulipAccount');
expect(logCall.operation_details).toContain('reason');
expect(logCall.operation_details).toContain(targetStatus);
},
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 15 }
);
});
it('日志查询应该支持多种过滤条件', async () => {
await PropertyTestRunner.runPropertyTest(
'日志查询过滤功能',
() => {
// 预先创建一些日志记录
const operations = ['CREATE', 'UPDATE', 'DELETE', 'BATCH_UPDATE'];
const entities = ['User', 'UserProfile', 'ZulipAccount'];
const adminIds = ['admin1', 'admin2', 'admin3'];
return {
operation_type: operations[Math.floor(Math.random() * operations.length)],
entity_type: entities[Math.floor(Math.random() * entities.length)],
admin_id: adminIds[Math.floor(Math.random() * adminIds.length)]
};
},
async (filters) => {
// 预先添加一些测试日志
await mockLogService.createLog({
operation_type: filters.operation_type,
entity_type: filters.entity_type,
admin_id: filters.admin_id,
entity_id: '1',
operation_details: JSON.stringify({ test: true }),
timestamp: new Date().toISOString()
});
// 查询日志
const response = await logController.queryLogs(
filters.operation_type,
filters.entity_type,
filters.admin_id,
undefined,
undefined,
'20', // 修复:传递字符串而不是数字
0
);
expect(response.success).toBe(true);
PropertyTestAssertions.assertListResponseFormat(response);
// 验证过滤结果
response.data.items.forEach((log: any) => {
expect(log.operation_type).toBe(filters.operation_type);
expect(log.entity_type).toBe(filters.entity_type);
expect(log.admin_id).toBe(filters.admin_id);
});
},
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 20 }
);
});
it('日志统计应该准确反映操作情况', async () => {
await PropertyTestRunner.runPropertyTest(
'日志统计准确性',
() => {
const operations = Array.from({ length: Math.floor(Math.random() * 10) + 5 }, () => ({
operation_type: ['CREATE', 'UPDATE', 'DELETE'][Math.floor(Math.random() * 3)],
entity_type: ['User', 'UserProfile'][Math.floor(Math.random() * 2)],
admin_id: `admin_${Math.floor(Math.random() * 3) + 1}`
}));
return { operations };
},
async ({ operations }) => {
// 创建测试日志
for (const op of operations) {
await mockLogService.createLog({
...op,
entity_id: '1',
operation_details: JSON.stringify({}),
timestamp: new Date().toISOString()
});
}
// 获取统计信息
const response = await logController.getStatistics();
expect(response.success).toBe(true);
expect(response.data.totalOperations).toBe(operations.length);
expect(response.data.operationsByType).toBeDefined();
expect(response.data.operationsByAdmin).toBeDefined();
// 验证统计数据准确性
const expectedByType = {};
const expectedByAdmin = {};
operations.forEach(op => {
expectedByType[op.operation_type] = (expectedByType[op.operation_type] || 0) + 1;
expectedByAdmin[op.admin_id] = (expectedByAdmin[op.admin_id] || 0) + 1;
});
expect(response.data.operationsByType).toEqual(expectedByType);
expect(response.data.operationsByAdmin).toEqual(expectedByAdmin);
},
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 15 }
);
});
it('敏感操作查询应该正确识别和过滤', async () => {
await PropertyTestRunner.runPropertyTest(
'敏感操作识别准确性',
() => {
const allOperations = ['CREATE', 'READ', 'UPDATE', 'DELETE', 'BATCH_UPDATE', 'ADMIN_CREATE'];
const operations = Array.from({ length: Math.floor(Math.random() * 8) + 3 }, () =>
allOperations[Math.floor(Math.random() * allOperations.length)]
);
return { operations };
},
async ({ operations }) => {
// 创建测试日志
for (const op of operations) {
await mockLogService.createLog({
operation_type: op,
entity_type: 'User',
admin_id: 'admin1',
entity_id: '1',
operation_details: JSON.stringify({}),
timestamp: new Date().toISOString()
});
}
// 查询敏感操作
const response = await logController.getSensitiveOperations(20, 0);
expect(response.success).toBe(true);
PropertyTestAssertions.assertListResponseFormat(response);
// 验证只返回敏感操作
const sensitiveOps = ['DELETE', 'BATCH_UPDATE', 'ADMIN_CREATE'];
const expectedSensitiveCount = operations.filter(op =>
sensitiveOps.includes(op)
).length;
expect(response.data.total).toBe(expectedSensitiveCount);
response.data.items.forEach((log: any) => {
expect(sensitiveOps).toContain(log.operation_type);
});
},
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 20 }
);
});
it('管理员操作历史应该完整记录', async () => {
await PropertyTestRunner.runPropertyTest(
'管理员操作历史完整性',
() => {
const adminId = `admin_${Math.floor(Math.random() * 100)}`;
const operations = Array.from({ length: Math.floor(Math.random() * 5) + 2 }, () => ({
operation_type: ['CREATE', 'UPDATE', 'DELETE'][Math.floor(Math.random() * 3)],
entity_type: 'User',
admin_id: adminId
}));
return { adminId, operations };
},
async ({ adminId, operations }) => {
// 创建该管理员的操作日志
for (const op of operations) {
await mockLogService.createLog({
...op,
entity_id: '1',
operation_details: JSON.stringify({}),
timestamp: new Date().toISOString()
});
}
// 创建其他管理员的操作日志(干扰数据)
await mockLogService.createLog({
operation_type: 'CREATE',
entity_type: 'User',
admin_id: 'other_admin',
entity_id: '2',
operation_details: JSON.stringify({}),
timestamp: new Date().toISOString()
});
// 查询特定管理员的操作历史
const response = await logController.getAdminOperationHistory(adminId);
expect(response.success).toBe(true);
expect(response.data).toHaveLength(operations.length);
// 验证所有返回的日志都属于指定管理员
response.data.forEach((log: any) => {
expect(log.admin_id).toBe(adminId);
});
},
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 15 }
);
});
});
});