- 更新管理员控制器和数据库管理功能 - 完善管理员操作日志系统 - 添加全面的属性测试覆盖 - 优化用户管理和用户档案服务 - 更新代码检查规范文档 功能改进: - 增强管理员权限验证 - 完善操作日志记录 - 优化数据库管理接口 - 提升系统安全性和可维护性
361 lines
13 KiB
TypeScript
361 lines
13 KiB
TypeScript
/**
|
||
* 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 }
|
||
);
|
||
});
|
||
});
|
||
}); |