Files
whale-town-end/src/business/admin/api_response_format.property.spec.ts
moyin 5f662ef091 feat: 完善管理员系统和用户管理模块
- 更新管理员控制器和数据库管理功能
- 完善管理员操作日志系统
- 添加全面的属性测试覆盖
- 优化用户管理和用户档案服务
- 更新代码检查规范文档

功能改进:
- 增强管理员权限验证
- 完善操作日志记录
- 优化数据库管理接口
- 提升系统安全性和可维护性
2026-01-09 17:05:08 +08:00

361 lines
13 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.
/**
* 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 './admin_database.controller';
import { DatabaseManagementService } from './database_management.service';
import { AdminOperationLogService } from './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 '../user_mgmt/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;
let mockDatabaseService: any;
beforeAll(async () => {
mockDatabaseService = {
getUserList: jest.fn().mockImplementation((limit, offset) => {
return Promise.resolve({
success: true,
data: {
items: [],
total: 0,
limit: limit || 20,
offset: offset || 0,
has_more: false
},
message: '获取用户列表成功',
timestamp: new Date().toISOString(),
request_id: 'test_' + Date.now()
});
}),
getUserById: jest.fn().mockImplementation((id) => {
const user = PropertyTestGenerators.generateUser();
return Promise.resolve({
success: true,
data: { ...user, id: id.toString() },
message: '获取用户详情成功',
timestamp: new Date().toISOString(),
request_id: 'test_' + Date.now()
});
}),
createUser: jest.fn().mockImplementation((userData) => {
return Promise.resolve({
success: true,
data: { ...userData, id: '1' },
message: '创建用户成功',
timestamp: new Date().toISOString(),
request_id: 'test_' + Date.now()
});
}),
updateUser: jest.fn().mockImplementation((id, updateData) => {
const user = PropertyTestGenerators.generateUser();
return Promise.resolve({
success: true,
data: { ...user, ...updateData, id: id.toString() },
message: '更新用户成功',
timestamp: new Date().toISOString(),
request_id: 'test_' + Date.now()
});
}),
deleteUser: jest.fn().mockImplementation((id) => {
return Promise.resolve({
success: true,
data: { deleted: true, id: id.toString() },
message: '删除用户成功',
timestamp: new Date().toISOString(),
request_id: 'test_' + Date.now()
});
}),
searchUsers: jest.fn().mockImplementation((searchTerm, limit) => {
return Promise.resolve({
success: true,
data: {
items: [],
total: 0,
limit: limit || 20,
offset: 0,
has_more: false
},
message: '搜索用户成功',
timestamp: new Date().toISOString(),
request_id: 'test_' + Date.now()
});
}),
getUserProfileList: jest.fn().mockImplementation((limit, offset) => {
return Promise.resolve({
success: true,
data: {
items: [],
total: 0,
limit: limit || 20,
offset: offset || 0,
has_more: false
},
message: '获取用户档案列表成功',
timestamp: new Date().toISOString(),
request_id: 'test_' + Date.now()
});
}),
getUserProfileById: jest.fn().mockImplementation((id) => {
const profile = PropertyTestGenerators.generateUserProfile();
return Promise.resolve({
success: true,
data: { ...profile, id: id.toString() },
message: '获取用户档案详情成功',
timestamp: new Date().toISOString(),
request_id: 'test_' + Date.now()
});
}),
getUserProfilesByMap: jest.fn().mockImplementation((map, limit, offset) => {
return Promise.resolve({
success: true,
data: {
items: [],
total: 0,
limit: limit || 20,
offset: offset || 0,
has_more: false
},
message: '按地图获取用户档案成功',
timestamp: new Date().toISOString(),
request_id: 'test_' + Date.now()
});
}),
getZulipAccountList: jest.fn().mockImplementation((limit, offset) => {
return Promise.resolve({
success: true,
data: {
items: [],
total: 0,
limit: limit || 20,
offset: offset || 0,
has_more: false
},
message: '获取Zulip账号列表成功',
timestamp: new Date().toISOString(),
request_id: 'test_' + Date.now()
});
}),
getZulipAccountById: jest.fn().mockImplementation((id) => {
const account = PropertyTestGenerators.generateZulipAccount();
return Promise.resolve({
success: true,
data: { ...account, id: id.toString() },
message: '获取Zulip账号详情成功',
timestamp: new Date().toISOString(),
request_id: 'test_' + Date.now()
});
}),
getZulipAccountStatistics: jest.fn().mockImplementation(() => {
return Promise.resolve({
success: true,
data: {
active: 0,
inactive: 0,
suspended: 0,
error: 0,
total: 0
},
message: '获取Zulip账号统计成功',
timestamp: new Date().toISOString(),
request_id: 'test_' + Date.now()
});
})
};
module = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: ['.env.test', '.env']
})
],
controllers: [AdminDatabaseController],
providers: [
{
provide: DatabaseManagementService,
useValue: mockDatabaseService
},
{
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())
}
}
]
})
.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 }
);
});
});
});