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

500 lines
18 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 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 }
);
});
});
});