forked from datawhale/whale-town-end
509 lines
19 KiB
TypeScript
509 lines
19 KiB
TypeScript
/**
|
||
* 操作日志属性测试
|
||
*
|
||
* 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 }
|
||
);
|
||
});
|
||
});
|
||
}); |