forked from datawhale/whale-town-end
feat:实现管理员系统核心功能
- 添加管理员数据库管理控制器和服务 - 实现管理员操作日志记录系统 - 添加数据库异常处理过滤器 - 完善管理员权限验证和响应格式 - 添加全面的属性测试覆盖
This commit is contained in:
500
src/business/admin/error_handling.property.spec.ts
Normal file
500
src/business/admin/error_handling.property.spec.ts
Normal file
@@ -0,0 +1,500 @@
|
||||
/**
|
||||
* 错误处理属性测试
|
||||
*
|
||||
* Property 9: 错误处理标准化
|
||||
*
|
||||
* Validates: Requirements 4.6
|
||||
*
|
||||
* 测试目标:
|
||||
* - 验证错误处理的标准化和一致性
|
||||
* - 确保错误响应格式统一
|
||||
* - 验证不同类型错误的正确处理
|
||||
*
|
||||
* 最近修改:
|
||||
* - 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 { 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 controller: AdminDatabaseController;
|
||||
let mockUsersService: any;
|
||||
let mockUserProfilesService: any;
|
||||
let mockZulipAccountsService: any;
|
||||
|
||||
beforeAll(async () => {
|
||||
mockUsersService = {
|
||||
findAll: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
search: jest.fn(),
|
||||
count: jest.fn()
|
||||
};
|
||||
|
||||
mockUserProfilesService = {
|
||||
findAll: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
findByMap: jest.fn(),
|
||||
count: jest.fn()
|
||||
};
|
||||
|
||||
mockZulipAccountsService = {
|
||||
findMany: jest.fn(),
|
||||
findById: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
getStatusStatistics: jest.fn()
|
||||
};
|
||||
|
||||
module = await Test.createTestingModule({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
envFilePath: ['.env.test', '.env']
|
||||
})
|
||||
],
|
||||
controllers: [AdminDatabaseController],
|
||||
providers: [
|
||||
DatabaseManagementService,
|
||||
{
|
||||
provide: AdminOperationLogService,
|
||||
useValue: {
|
||||
createLog: jest.fn().mockResolvedValue({}),
|
||||
queryLogs: jest.fn().mockResolvedValue({ logs: [], total: 0 }),
|
||||
getLogById: jest.fn().mockResolvedValue(null),
|
||||
getStatistics: jest.fn().mockResolvedValue({}),
|
||||
cleanupExpiredLogs: jest.fn().mockResolvedValue(0),
|
||||
getAdminOperationHistory: jest.fn().mockResolvedValue([]),
|
||||
getSensitiveOperations: jest.fn().mockResolvedValue({ logs: [], total: 0 })
|
||||
}
|
||||
},
|
||||
{
|
||||
provide: AdminOperationLogInterceptor,
|
||||
useValue: {
|
||||
intercept: jest.fn().mockImplementation((context, next) => next.handle())
|
||||
}
|
||||
},
|
||||
{
|
||||
provide: 'UsersService',
|
||||
useValue: mockUsersService
|
||||
},
|
||||
{
|
||||
provide: 'IUserProfilesService',
|
||||
useValue: mockUserProfilesService
|
||||
},
|
||||
{
|
||||
provide: 'ZulipAccountsService',
|
||||
useValue: mockZulipAccountsService
|
||||
}
|
||||
]
|
||||
})
|
||||
.overrideGuard(AdminGuard)
|
||||
.useValue({ canActivate: () => true })
|
||||
.compile();
|
||||
|
||||
app = module.createNestApplication();
|
||||
app.useGlobalFilters(new AdminDatabaseExceptionFilter());
|
||||
await app.init();
|
||||
|
||||
controller = module.get<AdminDatabaseController>(AdminDatabaseController);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
describe('Property 9: 错误处理标准化', () => {
|
||||
it('数据库连接错误应该返回标准化错误响应', async () => {
|
||||
await PropertyTestRunner.runPropertyTest(
|
||||
'数据库连接错误标准化',
|
||||
() => PropertyTestGenerators.generateUser(),
|
||||
async (userData) => {
|
||||
// 模拟数据库连接错误
|
||||
mockUsersService.create.mockRejectedValueOnce(
|
||||
new Error('Connection timeout')
|
||||
);
|
||||
|
||||
try {
|
||||
const response = await controller.createUser({
|
||||
...userData,
|
||||
status: UserStatus.ACTIVE
|
||||
});
|
||||
|
||||
// 如果没有抛出异常,验证错误响应格式
|
||||
if (!response.success) {
|
||||
expect(response).toHaveProperty('success', false);
|
||||
expect(response).toHaveProperty('message');
|
||||
expect(response).toHaveProperty('error_code');
|
||||
expect(response).toHaveProperty('timestamp');
|
||||
expect(response).toHaveProperty('request_id');
|
||||
|
||||
expect(typeof response.message).toBe('string');
|
||||
expect(typeof response.error_code).toBe('string');
|
||||
expect(typeof response.timestamp).toBe('string');
|
||||
expect(typeof response.request_id).toBe('string');
|
||||
}
|
||||
} catch (error) {
|
||||
// 如果抛出异常,验证异常被正确处理
|
||||
expect(error).toBeDefined();
|
||||
}
|
||||
},
|
||||
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 20 }
|
||||
);
|
||||
});
|
||||
|
||||
it('资源不存在错误应该返回一致的404响应', async () => {
|
||||
await PropertyTestRunner.runPropertyTest(
|
||||
'资源不存在错误一致性',
|
||||
() => ({
|
||||
entityType: ['User', 'UserProfile', 'ZulipAccount'][Math.floor(Math.random() * 3)],
|
||||
entityId: `nonexistent_${Math.floor(Math.random() * 1000)}`
|
||||
}),
|
||||
async ({ entityType, entityId }) => {
|
||||
// 模拟资源不存在
|
||||
if (entityType === 'User') {
|
||||
mockUsersService.findOne.mockResolvedValueOnce(null);
|
||||
} else if (entityType === 'UserProfile') {
|
||||
mockUserProfilesService.findOne.mockResolvedValueOnce(null);
|
||||
} else {
|
||||
mockZulipAccountsService.findById.mockResolvedValueOnce(null);
|
||||
}
|
||||
|
||||
try {
|
||||
let response;
|
||||
if (entityType === 'User') {
|
||||
response = await controller.getUserById(entityId);
|
||||
} else if (entityType === 'UserProfile') {
|
||||
response = await controller.getUserProfileById(entityId);
|
||||
} else {
|
||||
response = await controller.getZulipAccountById(entityId);
|
||||
}
|
||||
|
||||
// 验证404错误响应格式
|
||||
if (!response.success) {
|
||||
expect(response.success).toBe(false);
|
||||
expect(response.error_code).toContain('NOT_FOUND');
|
||||
expect(response.message).toContain('not found');
|
||||
PropertyTestAssertions.assertApiResponseFormat(response, false);
|
||||
}
|
||||
} catch (error: any) {
|
||||
// 验证异常包含正确信息
|
||||
expect(error.message).toContain('not found');
|
||||
}
|
||||
},
|
||||
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 25 }
|
||||
);
|
||||
});
|
||||
|
||||
it('数据验证错误应该返回详细的错误信息', async () => {
|
||||
await PropertyTestRunner.runPropertyTest(
|
||||
'数据验证错误详细信息',
|
||||
() => {
|
||||
const invalidData = {
|
||||
username: '', // 空用户名
|
||||
email: 'invalid-email', // 无效邮箱格式
|
||||
role: -1, // 无效角色
|
||||
status: 'INVALID_STATUS' as any // 无效状态
|
||||
};
|
||||
|
||||
return invalidData;
|
||||
},
|
||||
async (invalidData) => {
|
||||
// 模拟验证错误
|
||||
mockUsersService.create.mockRejectedValueOnce(
|
||||
new Error('Validation failed: username is required, email format invalid')
|
||||
);
|
||||
|
||||
try {
|
||||
const response = await controller.createUser({
|
||||
...invalidData,
|
||||
nickname: 'Test Nickname' // 添加必需的nickname字段
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
expect(response.success).toBe(false);
|
||||
expect(response.error_code).toContain('VALIDATION');
|
||||
expect(response.message).toContain('validation');
|
||||
PropertyTestAssertions.assertApiResponseFormat(response, false);
|
||||
|
||||
// 验证错误信息包含具体字段
|
||||
expect(response.message.toLowerCase()).toMatch(/(username|email|role|status)/);
|
||||
}
|
||||
} catch (error: any) {
|
||||
expect(error.message).toContain('validation');
|
||||
}
|
||||
},
|
||||
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 20 }
|
||||
);
|
||||
});
|
||||
|
||||
it('权限不足错误应该返回标准化403响应', async () => {
|
||||
await PropertyTestRunner.runPropertyTest(
|
||||
'权限不足错误标准化',
|
||||
() => PropertyTestGenerators.generateUser(),
|
||||
async (userData) => {
|
||||
// 模拟权限不足错误
|
||||
mockUsersService.create.mockRejectedValueOnce(
|
||||
new Error('Insufficient permissions')
|
||||
);
|
||||
|
||||
try {
|
||||
const response = await controller.createUser({
|
||||
...userData,
|
||||
status: UserStatus.ACTIVE
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
expect(response.success).toBe(false);
|
||||
expect(response.error_code).toContain('FORBIDDEN');
|
||||
expect(response.message).toContain('permission');
|
||||
PropertyTestAssertions.assertApiResponseFormat(response, false);
|
||||
}
|
||||
} catch (error: any) {
|
||||
expect(error.message).toContain('permission');
|
||||
}
|
||||
},
|
||||
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 15 }
|
||||
);
|
||||
});
|
||||
|
||||
it('并发冲突错误应该返回适当的错误响应', async () => {
|
||||
await PropertyTestRunner.runPropertyTest(
|
||||
'并发冲突错误处理',
|
||||
() => ({
|
||||
user: PropertyTestGenerators.generateUser(),
|
||||
conflictType: ['duplicate_key', 'version_conflict', 'resource_locked'][
|
||||
Math.floor(Math.random() * 3)
|
||||
]
|
||||
}),
|
||||
async ({ user, conflictType }) => {
|
||||
// 模拟不同类型的并发冲突
|
||||
let errorMessage;
|
||||
switch (conflictType) {
|
||||
case 'duplicate_key':
|
||||
errorMessage = 'Duplicate key violation: username already exists';
|
||||
break;
|
||||
case 'version_conflict':
|
||||
errorMessage = 'Version conflict: resource was modified by another user';
|
||||
break;
|
||||
case 'resource_locked':
|
||||
errorMessage = 'Resource is locked by another operation';
|
||||
break;
|
||||
}
|
||||
|
||||
mockUsersService.create.mockRejectedValueOnce(new Error(errorMessage));
|
||||
|
||||
try {
|
||||
const response = await controller.createUser({
|
||||
...user,
|
||||
status: UserStatus.ACTIVE
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
expect(response.success).toBe(false);
|
||||
expect(response.error_code).toContain('CONFLICT');
|
||||
PropertyTestAssertions.assertApiResponseFormat(response, false);
|
||||
|
||||
// 验证错误信息反映冲突类型
|
||||
if (conflictType === 'duplicate_key') {
|
||||
expect(response.message).toContain('duplicate');
|
||||
} else if (conflictType === 'version_conflict') {
|
||||
expect(response.message).toContain('conflict');
|
||||
} else {
|
||||
expect(response.message).toContain('locked');
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
expect(error.message).toBe(errorMessage);
|
||||
}
|
||||
},
|
||||
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 20 }
|
||||
);
|
||||
});
|
||||
|
||||
it('系统内部错误应该返回通用错误响应', async () => {
|
||||
await PropertyTestRunner.runPropertyTest(
|
||||
'系统内部错误处理',
|
||||
() => PropertyTestGenerators.generateUser(),
|
||||
async (userData) => {
|
||||
// 模拟系统内部错误
|
||||
mockUsersService.create.mockRejectedValueOnce(
|
||||
new Error('Internal system error: unexpected null pointer')
|
||||
);
|
||||
|
||||
try {
|
||||
const response = await controller.createUser({
|
||||
...userData,
|
||||
status: UserStatus.ACTIVE
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
expect(response.success).toBe(false);
|
||||
expect(response.error_code).toContain('INTERNAL_ERROR');
|
||||
expect(response.message).toContain('internal error');
|
||||
PropertyTestAssertions.assertApiResponseFormat(response, false);
|
||||
|
||||
// 内部错误不应该暴露敏感信息
|
||||
expect(response.message).not.toContain('null pointer');
|
||||
expect(response.message).not.toContain('stack trace');
|
||||
}
|
||||
} catch (error: any) {
|
||||
// 如果抛出异常,验证异常被适当处理
|
||||
expect(error).toBeDefined();
|
||||
}
|
||||
},
|
||||
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 15 }
|
||||
);
|
||||
});
|
||||
|
||||
it('网络超时错误应该返回适当的错误响应', async () => {
|
||||
await PropertyTestRunner.runPropertyTest(
|
||||
'网络超时错误处理',
|
||||
() => PropertyTestGenerators.generateUser(),
|
||||
async (userData) => {
|
||||
// 模拟网络超时错误
|
||||
const timeoutError = new Error('Request timeout');
|
||||
timeoutError.name = 'TimeoutError';
|
||||
mockUsersService.create.mockRejectedValueOnce(timeoutError);
|
||||
|
||||
try {
|
||||
const response = await controller.createUser({
|
||||
...userData,
|
||||
status: UserStatus.ACTIVE
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
expect(response.success).toBe(false);
|
||||
expect(response.error_code).toContain('TIMEOUT');
|
||||
expect(response.message).toContain('timeout');
|
||||
PropertyTestAssertions.assertApiResponseFormat(response, false);
|
||||
}
|
||||
} catch (error: any) {
|
||||
expect(error.message).toContain('timeout');
|
||||
}
|
||||
},
|
||||
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 15 }
|
||||
);
|
||||
});
|
||||
|
||||
it('错误响应应该包含有用的调试信息', async () => {
|
||||
await PropertyTestRunner.runPropertyTest(
|
||||
'错误调试信息完整性',
|
||||
() => PropertyTestGenerators.generateUser(),
|
||||
async (userData) => {
|
||||
// 模拟带详细信息的错误
|
||||
mockUsersService.create.mockRejectedValueOnce(
|
||||
new Error('Database constraint violation: unique_username_constraint')
|
||||
);
|
||||
|
||||
try {
|
||||
const response = await controller.createUser({
|
||||
...userData,
|
||||
status: UserStatus.ACTIVE
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
PropertyTestAssertions.assertApiResponseFormat(response, false);
|
||||
|
||||
// 验证调试信息
|
||||
expect(response.timestamp).toBeDefined();
|
||||
expect(response.request_id).toBeDefined();
|
||||
expect(response.error_code).toBeDefined();
|
||||
|
||||
// 验证时间戳格式
|
||||
const timestamp = new Date(response.timestamp);
|
||||
expect(timestamp.toISOString()).toBe(response.timestamp);
|
||||
|
||||
// 验证请求ID格式
|
||||
expect(response.request_id).toMatch(/^[a-zA-Z0-9_-]+$/);
|
||||
|
||||
// 验证错误码格式
|
||||
expect(response.error_code).toMatch(/^[A-Z_]+$/);
|
||||
}
|
||||
} catch (error: any) {
|
||||
expect(error).toBeDefined();
|
||||
}
|
||||
},
|
||||
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 20 }
|
||||
);
|
||||
});
|
||||
|
||||
it('批量操作中的部分错误应该被正确处理', async () => {
|
||||
await PropertyTestRunner.runPropertyTest(
|
||||
'批量操作部分错误处理',
|
||||
() => {
|
||||
const accountIds = Array.from({ length: Math.floor(Math.random() * 5) + 3 },
|
||||
(_, i) => `account_${i + 1}`);
|
||||
const targetStatus = 'active' as const;
|
||||
|
||||
return { accountIds, targetStatus };
|
||||
},
|
||||
async ({ accountIds, targetStatus }) => {
|
||||
// 模拟部分成功,部分失败的批量操作
|
||||
accountIds.forEach((id, index) => {
|
||||
if (index === 0) {
|
||||
// 第一个操作失败
|
||||
mockZulipAccountsService.update.mockRejectedValueOnce(
|
||||
new Error(`Failed to update account ${id}: validation error`)
|
||||
);
|
||||
} else {
|
||||
// 其他操作成功
|
||||
mockZulipAccountsService.update.mockResolvedValueOnce({
|
||||
id,
|
||||
status: targetStatus,
|
||||
...PropertyTestGenerators.generateZulipAccount()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const response = await controller.batchUpdateZulipAccountStatus({
|
||||
ids: accountIds,
|
||||
status: targetStatus,
|
||||
reason: '测试批量更新'
|
||||
});
|
||||
|
||||
expect(response.success).toBe(true); // 批量操作本身成功
|
||||
expect(response.data.failed).toBe(1); // 一个失败
|
||||
expect(response.data.success).toBe(accountIds.length - 1); // 其他成功
|
||||
|
||||
// 验证错误信息格式
|
||||
expect(response.data.errors).toHaveLength(1);
|
||||
expect(response.data.errors[0]).toHaveProperty('id');
|
||||
expect(response.data.errors[0]).toHaveProperty('success', false);
|
||||
expect(response.data.errors[0]).toHaveProperty('error');
|
||||
|
||||
PropertyTestAssertions.assertApiResponseFormat(response, true);
|
||||
},
|
||||
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 15 }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user