forked from datawhale/whale-town-end
feat:实现管理员系统核心功能
- 添加管理员数据库管理控制器和服务 - 实现管理员操作日志记录系统 - 添加数据库异常处理过滤器 - 完善管理员权限验证和响应格式 - 添加全面的属性测试覆盖
This commit is contained in:
392
src/business/admin/user_profile_management.property.spec.ts
Normal file
392
src/business/admin/user_profile_management.property.spec.ts
Normal file
@@ -0,0 +1,392 @@
|
||||
/**
|
||||
* 用户档案管理属性测试
|
||||
*
|
||||
* Property 3: 用户档案管理操作完整性
|
||||
* Property 4: 地图用户查询正确性
|
||||
*
|
||||
* Validates: Requirements 2.1-2.6
|
||||
*
|
||||
* 测试目标:
|
||||
* - 验证用户档案CRUD操作的完整性
|
||||
* - 确保地图查询功能的正确性
|
||||
* - 验证位置数据的处理逻辑
|
||||
*
|
||||
* 最近修改:
|
||||
* - 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 {
|
||||
PropertyTestRunner,
|
||||
PropertyTestGenerators,
|
||||
PropertyTestAssertions,
|
||||
DEFAULT_PROPERTY_CONFIG
|
||||
} from './admin_property_test.base';
|
||||
|
||||
describe('Property Test: 用户档案管理功能', () => {
|
||||
let app: INestApplication;
|
||||
let module: TestingModule;
|
||||
let controller: AdminDatabaseController;
|
||||
let mockUserProfilesService: any;
|
||||
|
||||
beforeAll(async () => {
|
||||
mockUserProfilesService = {
|
||||
findAll: jest.fn().mockResolvedValue([]),
|
||||
findOne: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
remove: jest.fn().mockResolvedValue(undefined),
|
||||
findByMap: jest.fn(),
|
||||
count: jest.fn().mockResolvedValue(0)
|
||||
};
|
||||
|
||||
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().mockResolvedValue({ id: BigInt(1) }),
|
||||
create: jest.fn().mockResolvedValue({ id: BigInt(1) }),
|
||||
update: jest.fn().mockResolvedValue({ id: BigInt(1) }),
|
||||
remove: jest.fn().mockResolvedValue(undefined),
|
||||
search: jest.fn().mockResolvedValue([]),
|
||||
count: jest.fn().mockResolvedValue(0)
|
||||
}
|
||||
},
|
||||
{
|
||||
provide: 'IUserProfilesService',
|
||||
useValue: mockUserProfilesService
|
||||
},
|
||||
{
|
||||
provide: 'ZulipAccountsService',
|
||||
useValue: {
|
||||
findMany: jest.fn().mockResolvedValue({ accounts: [] }),
|
||||
findById: jest.fn().mockResolvedValue({ id: '1' }),
|
||||
create: jest.fn().mockResolvedValue({ id: '1' }),
|
||||
update: jest.fn().mockResolvedValue({ id: '1' }),
|
||||
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 3: 用户档案管理操作完整性', () => {
|
||||
it('创建用户档案后应该能够读取相同的数据', async () => {
|
||||
await PropertyTestRunner.runPropertyTest(
|
||||
'用户档案创建-读取一致性',
|
||||
() => PropertyTestGenerators.generateUserProfile(),
|
||||
async (profileData) => {
|
||||
const profileWithId = { ...profileData, id: BigInt(1) };
|
||||
|
||||
// Mock创建和读取操作
|
||||
mockUserProfilesService.create.mockResolvedValueOnce(profileWithId);
|
||||
mockUserProfilesService.findOne.mockResolvedValueOnce(profileWithId);
|
||||
|
||||
// 执行创建操作
|
||||
const createResponse = await controller.createUserProfile(profileData);
|
||||
|
||||
// 执行读取操作
|
||||
const readResponse = await controller.getUserProfileById('1');
|
||||
|
||||
// 验证一致性
|
||||
PropertyTestAssertions.assertCrudConsistency(
|
||||
createResponse,
|
||||
readResponse,
|
||||
createResponse
|
||||
);
|
||||
|
||||
expect(createResponse.data.user_id).toBe(profileData.user_id);
|
||||
expect(readResponse.data.user_id).toBe(profileData.user_id);
|
||||
},
|
||||
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 30 }
|
||||
);
|
||||
});
|
||||
|
||||
it('更新用户档案后数据应该反映变更', async () => {
|
||||
await PropertyTestRunner.runPropertyTest(
|
||||
'用户档案更新一致性',
|
||||
() => ({
|
||||
original: PropertyTestGenerators.generateUserProfile(),
|
||||
updates: PropertyTestGenerators.generateUserProfile()
|
||||
}),
|
||||
async ({ original, updates }) => {
|
||||
const originalWithId = { ...original, id: BigInt(1) };
|
||||
const updatedProfile = { ...originalWithId, ...updates };
|
||||
|
||||
// Mock操作
|
||||
mockUserProfilesService.findOne.mockResolvedValueOnce(originalWithId);
|
||||
mockUserProfilesService.update.mockResolvedValueOnce(updatedProfile);
|
||||
|
||||
// 执行更新操作
|
||||
const updateResponse = await controller.updateUserProfile('1', updates);
|
||||
|
||||
expect(updateResponse.success).toBe(true);
|
||||
expect(updateResponse.data.id).toBe('1');
|
||||
|
||||
// 验证更新的字段
|
||||
if (updates.bio) {
|
||||
expect(updateResponse.data.bio).toBe(updates.bio);
|
||||
}
|
||||
if (updates.current_map) {
|
||||
expect(updateResponse.data.current_map).toBe(updates.current_map);
|
||||
}
|
||||
},
|
||||
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 30 }
|
||||
);
|
||||
});
|
||||
|
||||
it('位置数据应该被正确处理', async () => {
|
||||
await PropertyTestRunner.runPropertyTest(
|
||||
'位置数据处理正确性',
|
||||
() => {
|
||||
const profile = PropertyTestGenerators.generateUserProfile();
|
||||
return {
|
||||
...profile,
|
||||
pos_x: Math.random() * 2000 - 1000, // -1000 到 1000
|
||||
pos_y: Math.random() * 2000 - 1000, // -1000 到 1000
|
||||
};
|
||||
},
|
||||
async (profileData) => {
|
||||
const profileWithId = { ...profileData, id: BigInt(1) };
|
||||
|
||||
mockUserProfilesService.create.mockResolvedValueOnce(profileWithId);
|
||||
|
||||
const createResponse = await controller.createUserProfile(profileData);
|
||||
|
||||
expect(createResponse.success).toBe(true);
|
||||
expect(typeof createResponse.data.pos_x).toBe('number');
|
||||
expect(typeof createResponse.data.pos_y).toBe('number');
|
||||
|
||||
// 验证位置数据的合理性
|
||||
expect(createResponse.data.pos_x).toBe(profileData.pos_x);
|
||||
expect(createResponse.data.pos_y).toBe(profileData.pos_y);
|
||||
},
|
||||
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 25 }
|
||||
);
|
||||
});
|
||||
|
||||
it('JSON字段应该被正确序列化和反序列化', async () => {
|
||||
await PropertyTestRunner.runPropertyTest(
|
||||
'JSON字段处理正确性',
|
||||
() => {
|
||||
const profile = PropertyTestGenerators.generateUserProfile();
|
||||
return {
|
||||
...profile,
|
||||
tags: JSON.stringify(['tag1', 'tag2', 'tag3']),
|
||||
social_links: JSON.stringify({
|
||||
github: 'https://github.com/user',
|
||||
linkedin: 'https://linkedin.com/in/user',
|
||||
twitter: 'https://twitter.com/user'
|
||||
})
|
||||
};
|
||||
},
|
||||
async (profileData) => {
|
||||
const profileWithId = { ...profileData, id: BigInt(1) };
|
||||
|
||||
mockUserProfilesService.create.mockResolvedValueOnce(profileWithId);
|
||||
|
||||
const createResponse = await controller.createUserProfile(profileData);
|
||||
|
||||
expect(createResponse.success).toBe(true);
|
||||
expect(createResponse.data.tags).toBe(profileData.tags);
|
||||
expect(createResponse.data.social_links).toBe(profileData.social_links);
|
||||
|
||||
// 验证JSON格式有效性
|
||||
expect(() => JSON.parse(profileData.tags)).not.toThrow();
|
||||
expect(() => JSON.parse(profileData.social_links)).not.toThrow();
|
||||
},
|
||||
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 20 }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Property 4: 地图用户查询正确性', () => {
|
||||
it('按地图查询应该返回正确的用户档案', async () => {
|
||||
await PropertyTestRunner.runPropertyTest(
|
||||
'地图查询正确性',
|
||||
() => {
|
||||
const maps = ['plaza', 'forest', 'beach', 'mountain', 'city'];
|
||||
const selectedMap = maps[Math.floor(Math.random() * maps.length)];
|
||||
const profiles = Array.from({ length: 5 }, () => {
|
||||
const profile = PropertyTestGenerators.generateUserProfile();
|
||||
return {
|
||||
...profile,
|
||||
id: BigInt(Math.floor(Math.random() * 1000) + 1),
|
||||
current_map: Math.random() > 0.5 ? selectedMap : maps[Math.floor(Math.random() * maps.length)]
|
||||
};
|
||||
});
|
||||
|
||||
return { selectedMap, profiles };
|
||||
},
|
||||
async ({ selectedMap, profiles }) => {
|
||||
// 过滤出应该匹配的档案
|
||||
const expectedProfiles = profiles.filter(p => p.current_map === selectedMap);
|
||||
|
||||
mockUserProfilesService.findByMap.mockResolvedValueOnce(expectedProfiles);
|
||||
mockUserProfilesService.count.mockResolvedValueOnce(expectedProfiles.length);
|
||||
|
||||
const response = await controller.getUserProfilesByMap(selectedMap, 20, 0);
|
||||
|
||||
expect(response.success).toBe(true);
|
||||
PropertyTestAssertions.assertListResponseFormat(response);
|
||||
|
||||
// 验证返回的档案都属于指定地图
|
||||
response.data.items.forEach((profile: any) => {
|
||||
expect(profile.current_map).toBe(selectedMap);
|
||||
});
|
||||
|
||||
expect(response.data.items.length).toBe(expectedProfiles.length);
|
||||
},
|
||||
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 25 }
|
||||
);
|
||||
});
|
||||
|
||||
it('不存在的地图应该返回空结果', async () => {
|
||||
await PropertyTestRunner.runPropertyTest(
|
||||
'不存在地图查询处理',
|
||||
() => ({
|
||||
nonExistentMap: `nonexistent_${Math.random().toString(36).substring(7)}`
|
||||
}),
|
||||
async ({ nonExistentMap }) => {
|
||||
mockUserProfilesService.findByMap.mockResolvedValueOnce([]);
|
||||
mockUserProfilesService.count.mockResolvedValueOnce(0);
|
||||
|
||||
const response = await controller.getUserProfilesByMap(nonExistentMap, 20, 0);
|
||||
|
||||
expect(response.success).toBe(true);
|
||||
expect(response.data.items).toEqual([]);
|
||||
expect(response.data.total).toBe(0);
|
||||
},
|
||||
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 15 }
|
||||
);
|
||||
});
|
||||
|
||||
it('地图查询应该支持分页', async () => {
|
||||
await PropertyTestRunner.runPropertyTest(
|
||||
'地图查询分页支持',
|
||||
() => {
|
||||
const map = 'plaza';
|
||||
const pagination = PropertyTestGenerators.generatePaginationParams();
|
||||
const totalProfiles = Math.floor(Math.random() * 100) + 50; // 50-149个档案
|
||||
|
||||
return { map, pagination, totalProfiles };
|
||||
},
|
||||
async ({ map, pagination, totalProfiles }) => {
|
||||
const { limit, offset } = pagination;
|
||||
const safeLimit = Math.min(Math.max(limit, 1), 100);
|
||||
const safeOffset = Math.max(offset, 0);
|
||||
|
||||
// 模拟分页结果
|
||||
const itemsToReturn = Math.min(safeLimit, Math.max(0, totalProfiles - safeOffset));
|
||||
const mockProfiles = Array.from({ length: itemsToReturn }, (_, i) => ({
|
||||
...PropertyTestGenerators.generateUserProfile(),
|
||||
id: BigInt(safeOffset + i + 1),
|
||||
current_map: map
|
||||
}));
|
||||
|
||||
mockUserProfilesService.findByMap.mockResolvedValueOnce(mockProfiles);
|
||||
mockUserProfilesService.count.mockResolvedValueOnce(totalProfiles);
|
||||
|
||||
const response = await controller.getUserProfilesByMap(map, safeLimit, safeOffset);
|
||||
|
||||
expect(response.success).toBe(true);
|
||||
PropertyTestAssertions.assertPaginationLogic(response, safeLimit, safeOffset);
|
||||
|
||||
// 验证返回的档案数量
|
||||
expect(response.data.items.length).toBe(itemsToReturn);
|
||||
expect(response.data.total).toBe(totalProfiles);
|
||||
},
|
||||
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 20 }
|
||||
);
|
||||
});
|
||||
|
||||
it('地图名称应该被正确处理', async () => {
|
||||
await PropertyTestRunner.runPropertyTest(
|
||||
'地图名称处理',
|
||||
() => {
|
||||
const mapNames = [
|
||||
'plaza', 'forest', 'beach', 'mountain', 'city',
|
||||
'special-map', 'map_with_underscore', 'map123',
|
||||
'中文地图', 'café-map'
|
||||
];
|
||||
return {
|
||||
mapName: mapNames[Math.floor(Math.random() * mapNames.length)]
|
||||
};
|
||||
},
|
||||
async ({ mapName }) => {
|
||||
mockUserProfilesService.findByMap.mockResolvedValueOnce([]);
|
||||
mockUserProfilesService.count.mockResolvedValueOnce(0);
|
||||
|
||||
const response = await controller.getUserProfilesByMap(mapName, 20, 0);
|
||||
|
||||
expect(response.success).toBe(true);
|
||||
expect(response.data).toBeDefined();
|
||||
|
||||
// 验证地图名称被正确传递
|
||||
expect(mockUserProfilesService.findByMap).toHaveBeenCalledWith(
|
||||
mapName, undefined, 20, 0
|
||||
);
|
||||
},
|
||||
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 15 }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user