feat:实现管理员系统核心功能

- 添加管理员数据库管理控制器和服务
- 实现管理员操作日志记录系统
- 添加数据库异常处理过滤器
- 完善管理员权限验证和响应格式
- 添加全面的属性测试覆盖
This commit is contained in:
moyin
2026-01-08 23:05:34 +08:00
parent 0f37130832
commit 6924416bbd
34 changed files with 9481 additions and 199 deletions

View File

@@ -0,0 +1,509 @@
/**
* 操作日志属性测试
*
* 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 }
);
});
});
});