feat:实现管理员系统核心功能
- 添加管理员数据库管理控制器和服务 - 实现管理员操作日志记录系统 - 添加数据库异常处理过滤器 - 完善管理员权限验证和响应格式 - 添加全面的属性测试覆盖
This commit is contained in:
271
src/business/admin/api_response_format.property.spec.ts
Normal file
271
src/business/admin/api_response_format.property.spec.ts
Normal file
@@ -0,0 +1,271 @@
|
||||
/**
|
||||
* API响应格式一致性属性测试
|
||||
*
|
||||
* Property 7: API响应格式一致性
|
||||
* Validates: Requirements 4.1, 4.2, 4.3
|
||||
*
|
||||
* 测试目标:
|
||||
* - 验证所有API端点返回统一的响应格式
|
||||
* - 确保成功和失败响应都符合规范
|
||||
* - 验证响应字段类型和必需性
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-08: 注释规范优化 - 修正@author字段,更新版本号和修改记录 (修改者: moyin)
|
||||
* - 2026-01-08: 功能新增 - 创建API响应格式属性测试 (修改者: 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: API响应格式一致性', () => {
|
||||
let app: INestApplication;
|
||||
let module: TestingModule;
|
||||
let controller: AdminDatabaseController;
|
||||
|
||||
beforeAll(async () => {
|
||||
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: {
|
||||
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().mockImplementation(() => {
|
||||
const profile = PropertyTestGenerators.generateUserProfile();
|
||||
return Promise.resolve({ ...profile, id: BigInt(1) });
|
||||
}),
|
||||
create: jest.fn().mockImplementation((profileData) => {
|
||||
return Promise.resolve({ ...profileData, id: BigInt(1) });
|
||||
}),
|
||||
update: jest.fn().mockImplementation((id, updateData) => {
|
||||
const profile = PropertyTestGenerators.generateUserProfile();
|
||||
return Promise.resolve({ ...profile, ...updateData, id });
|
||||
}),
|
||||
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().mockImplementation(() => {
|
||||
const account = PropertyTestGenerators.generateZulipAccount();
|
||||
return Promise.resolve({ ...account, id: '1' });
|
||||
}),
|
||||
create: jest.fn().mockImplementation((accountData) => {
|
||||
return Promise.resolve({ ...accountData, id: '1' });
|
||||
}),
|
||||
update: jest.fn().mockImplementation((id, updateData) => {
|
||||
const account = PropertyTestGenerators.generateZulipAccount();
|
||||
return Promise.resolve({ ...account, ...updateData, id });
|
||||
}),
|
||||
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();
|
||||
|
||||
controller = module.get<AdminDatabaseController>(AdminDatabaseController);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
describe('Property 7: API响应格式一致性', () => {
|
||||
it('所有成功响应应该有统一的格式', async () => {
|
||||
await PropertyTestRunner.runPropertyTest(
|
||||
'API成功响应格式一致性',
|
||||
() => PropertyTestGenerators.generateUser(),
|
||||
async (userData) => {
|
||||
// 测试用户管理端点
|
||||
const userListResponse = await controller.getUserList(20, 0);
|
||||
PropertyTestAssertions.assertListResponseFormat(userListResponse);
|
||||
|
||||
const userDetailResponse = await controller.getUserById('1');
|
||||
PropertyTestAssertions.assertApiResponseFormat(userDetailResponse, true);
|
||||
|
||||
const createUserResponse = await controller.createUser({
|
||||
...userData,
|
||||
status: UserStatus.ACTIVE
|
||||
});
|
||||
PropertyTestAssertions.assertApiResponseFormat(createUserResponse, true);
|
||||
|
||||
// 测试用户档案管理端点
|
||||
const profileListResponse = await controller.getUserProfileList(20, 0);
|
||||
PropertyTestAssertions.assertListResponseFormat(profileListResponse);
|
||||
|
||||
const profileDetailResponse = await controller.getUserProfileById('1');
|
||||
PropertyTestAssertions.assertApiResponseFormat(profileDetailResponse, true);
|
||||
|
||||
// 测试Zulip账号管理端点
|
||||
const zulipListResponse = await controller.getZulipAccountList(20, 0);
|
||||
PropertyTestAssertions.assertListResponseFormat(zulipListResponse);
|
||||
|
||||
const zulipDetailResponse = await controller.getZulipAccountById('1');
|
||||
PropertyTestAssertions.assertApiResponseFormat(zulipDetailResponse, true);
|
||||
|
||||
const zulipStatsResponse = await controller.getZulipAccountStatistics();
|
||||
PropertyTestAssertions.assertApiResponseFormat(zulipStatsResponse, true);
|
||||
|
||||
// 测试系统端点
|
||||
const healthResponse = await controller.healthCheck();
|
||||
PropertyTestAssertions.assertApiResponseFormat(healthResponse, true);
|
||||
},
|
||||
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 50 }
|
||||
);
|
||||
});
|
||||
|
||||
it('所有列表响应应该有正确的分页信息', async () => {
|
||||
await PropertyTestRunner.runPropertyTest(
|
||||
'列表响应分页格式一致性',
|
||||
() => PropertyTestGenerators.generatePaginationParams(),
|
||||
async (paginationParams) => {
|
||||
const { limit, offset } = paginationParams;
|
||||
|
||||
// 限制参数范围以避免无效请求
|
||||
const safeLimit = Math.min(Math.max(limit, 1), 100);
|
||||
const safeOffset = Math.max(offset, 0);
|
||||
|
||||
// 测试所有列表端点
|
||||
const userListResponse = await controller.getUserList(safeLimit, safeOffset);
|
||||
PropertyTestAssertions.assertPaginationLogic(userListResponse, safeLimit, safeOffset);
|
||||
|
||||
const profileListResponse = await controller.getUserProfileList(safeLimit, safeOffset);
|
||||
PropertyTestAssertions.assertPaginationLogic(profileListResponse, safeLimit, safeOffset);
|
||||
|
||||
const zulipListResponse = await controller.getZulipAccountList(safeLimit, safeOffset);
|
||||
PropertyTestAssertions.assertPaginationLogic(zulipListResponse, safeLimit, safeOffset);
|
||||
|
||||
const mapProfilesResponse = await controller.getUserProfilesByMap('plaza', safeLimit, safeOffset);
|
||||
PropertyTestAssertions.assertPaginationLogic(mapProfilesResponse, safeLimit, safeOffset);
|
||||
},
|
||||
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 30 }
|
||||
);
|
||||
});
|
||||
|
||||
it('响应时间戳应该是有效的ISO格式', async () => {
|
||||
await PropertyTestRunner.runPropertyTest(
|
||||
'响应时间戳格式验证',
|
||||
() => ({}),
|
||||
async () => {
|
||||
const response = await controller.healthCheck();
|
||||
|
||||
expect(response.timestamp).toBeDefined();
|
||||
expect(typeof response.timestamp).toBe('string');
|
||||
|
||||
// 验证ISO 8601格式
|
||||
const timestamp = new Date(response.timestamp);
|
||||
expect(timestamp.toISOString()).toBe(response.timestamp);
|
||||
|
||||
// 验证时间戳是最近的(在过去1分钟内)
|
||||
const now = new Date();
|
||||
const timeDiff = now.getTime() - timestamp.getTime();
|
||||
expect(timeDiff).toBeLessThan(60000); // 1分钟
|
||||
},
|
||||
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 20 }
|
||||
);
|
||||
});
|
||||
|
||||
it('请求ID应该是唯一的', async () => {
|
||||
const requestIds = new Set<string>();
|
||||
|
||||
await PropertyTestRunner.runPropertyTest(
|
||||
'请求ID唯一性验证',
|
||||
() => ({}),
|
||||
async () => {
|
||||
const response = await controller.healthCheck();
|
||||
|
||||
expect(response.request_id).toBeDefined();
|
||||
expect(typeof response.request_id).toBe('string');
|
||||
expect(response.request_id.length).toBeGreaterThan(0);
|
||||
|
||||
// 验证请求ID唯一性
|
||||
expect(requestIds.has(response.request_id)).toBe(false);
|
||||
requestIds.add(response.request_id);
|
||||
},
|
||||
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 100 }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user