feat:实现管理员系统核心功能
- 添加管理员数据库管理控制器和服务 - 实现管理员操作日志记录系统 - 添加数据库异常处理过滤器 - 完善管理员权限验证和响应格式 - 添加全面的属性测试覆盖
This commit is contained in:
358
src/business/admin/user_management.property.spec.ts
Normal file
358
src/business/admin/user_management.property.spec.ts
Normal file
@@ -0,0 +1,358 @@
|
||||
/**
|
||||
* 用户管理属性测试
|
||||
*
|
||||
* Property 1: 用户管理CRUD操作一致性
|
||||
* Property 2: 用户搜索结果准确性
|
||||
* Property 12: 数据验证完整性
|
||||
*
|
||||
* Validates: Requirements 1.1-1.6, 6.1-6.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 { 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;
|
||||
|
||||
beforeAll(async () => {
|
||||
mockUsersService = {
|
||||
findAll: jest.fn().mockResolvedValue([]),
|
||||
findOne: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
remove: jest.fn().mockResolvedValue(undefined),
|
||||
search: 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: mockUsersService
|
||||
},
|
||||
{
|
||||
provide: 'IUserProfilesService',
|
||||
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),
|
||||
findByMap: jest.fn().mockResolvedValue([]),
|
||||
count: jest.fn().mockResolvedValue(0)
|
||||
}
|
||||
},
|
||||
{
|
||||
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 1: 用户管理CRUD操作一致性', () => {
|
||||
it('创建用户后应该能够读取相同的数据', async () => {
|
||||
await PropertyTestRunner.runPropertyTest(
|
||||
'用户创建-读取一致性',
|
||||
() => PropertyTestGenerators.generateUser(),
|
||||
async (userData) => {
|
||||
const userWithStatus = { ...userData, status: UserStatus.ACTIVE };
|
||||
|
||||
// Mock创建和读取操作
|
||||
const createdUser = { ...userWithStatus, id: BigInt(1) };
|
||||
mockUsersService.create.mockResolvedValueOnce(createdUser);
|
||||
mockUsersService.findOne.mockResolvedValueOnce(createdUser);
|
||||
|
||||
// 执行创建操作
|
||||
const createResponse = await controller.createUser(userWithStatus);
|
||||
|
||||
// 执行读取操作
|
||||
const readResponse = await controller.getUserById('1');
|
||||
|
||||
// 验证一致性
|
||||
PropertyTestAssertions.assertCrudConsistency(
|
||||
createResponse,
|
||||
readResponse,
|
||||
createResponse // 使用创建响应作为更新响应的占位符
|
||||
);
|
||||
|
||||
expect(createResponse.data.username).toBe(userWithStatus.username);
|
||||
expect(readResponse.data.username).toBe(userWithStatus.username);
|
||||
},
|
||||
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 30 }
|
||||
);
|
||||
});
|
||||
|
||||
it('更新用户后数据应该反映变更', async () => {
|
||||
await PropertyTestRunner.runPropertyTest(
|
||||
'用户更新一致性',
|
||||
() => ({
|
||||
original: PropertyTestGenerators.generateUser(),
|
||||
updates: PropertyTestGenerators.generateUser()
|
||||
}),
|
||||
async ({ original, updates }) => {
|
||||
const originalWithId = { ...original, id: BigInt(1), status: UserStatus.ACTIVE };
|
||||
const updatedUser = { ...originalWithId, ...updates, status: UserStatus.ACTIVE };
|
||||
|
||||
// Mock操作
|
||||
mockUsersService.findOne.mockResolvedValueOnce(originalWithId);
|
||||
mockUsersService.update.mockResolvedValueOnce(updatedUser);
|
||||
|
||||
// 执行更新操作
|
||||
const updateResponse = await controller.updateUser('1', {
|
||||
...updates,
|
||||
status: UserStatus.ACTIVE
|
||||
});
|
||||
|
||||
expect(updateResponse.success).toBe(true);
|
||||
expect(updateResponse.data.id).toBe('1');
|
||||
|
||||
// 验证更新的字段
|
||||
if (updates.username) {
|
||||
expect(updateResponse.data.username).toBe(updates.username);
|
||||
}
|
||||
if (updates.email) {
|
||||
expect(updateResponse.data.email).toBe(updates.email);
|
||||
}
|
||||
},
|
||||
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 30 }
|
||||
);
|
||||
});
|
||||
|
||||
it('删除用户后应该无法读取', async () => {
|
||||
await PropertyTestRunner.runPropertyTest(
|
||||
'用户删除一致性',
|
||||
() => PropertyTestGenerators.generateUser(),
|
||||
async (userData) => {
|
||||
const userWithId = { ...userData, id: BigInt(1), status: UserStatus.ACTIVE };
|
||||
|
||||
// Mock删除操作
|
||||
mockUsersService.remove.mockResolvedValueOnce(undefined);
|
||||
|
||||
// 执行删除操作
|
||||
const deleteResponse = await controller.deleteUser('1');
|
||||
|
||||
expect(deleteResponse.success).toBe(true);
|
||||
expect(deleteResponse.data.deleted).toBe(true);
|
||||
expect(deleteResponse.data.id).toBe('1');
|
||||
},
|
||||
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 20 }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Property 2: 用户搜索结果准确性', () => {
|
||||
it('搜索结果应该包含匹配的用户', async () => {
|
||||
await PropertyTestRunner.runPropertyTest(
|
||||
'用户搜索准确性',
|
||||
() => {
|
||||
const user = PropertyTestGenerators.generateUser();
|
||||
return {
|
||||
user,
|
||||
searchTerm: user.username.substring(0, 3) // 使用用户名前3个字符作为搜索词
|
||||
};
|
||||
},
|
||||
async ({ user, searchTerm }) => {
|
||||
const userWithId = { ...user, id: BigInt(1), status: UserStatus.ACTIVE };
|
||||
|
||||
// Mock搜索操作 - 如果搜索词匹配,返回用户
|
||||
const shouldMatch = user.username.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
user.email?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
user.nickname?.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
|
||||
mockUsersService.search.mockResolvedValueOnce(shouldMatch ? [userWithId] : []);
|
||||
|
||||
// 执行搜索操作
|
||||
const searchResponse = await controller.searchUsers(searchTerm, 20);
|
||||
|
||||
expect(searchResponse.success).toBe(true);
|
||||
PropertyTestAssertions.assertListResponseFormat(searchResponse);
|
||||
|
||||
if (shouldMatch) {
|
||||
expect(searchResponse.data.items.length).toBeGreaterThan(0);
|
||||
const foundUser = searchResponse.data.items[0];
|
||||
expect(foundUser.username).toBe(user.username);
|
||||
}
|
||||
},
|
||||
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 25 }
|
||||
);
|
||||
});
|
||||
|
||||
it('空搜索词应该返回空结果或错误', async () => {
|
||||
await PropertyTestRunner.runPropertyTest(
|
||||
'空搜索词处理',
|
||||
() => ({ searchTerm: '' }),
|
||||
async ({ searchTerm }) => {
|
||||
mockUsersService.search.mockResolvedValueOnce([]);
|
||||
|
||||
const searchResponse = await controller.searchUsers(searchTerm, 20);
|
||||
|
||||
// 空搜索应该返回空结果
|
||||
expect(searchResponse.success).toBe(true);
|
||||
expect(searchResponse.data.items).toEqual([]);
|
||||
},
|
||||
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 10 }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Property 12: 数据验证完整性', () => {
|
||||
it('有效的用户数据应该通过验证', async () => {
|
||||
await PropertyTestRunner.runPropertyTest(
|
||||
'有效用户数据验证',
|
||||
() => PropertyTestGenerators.generateUser(),
|
||||
async (userData) => {
|
||||
const validUser = {
|
||||
...userData,
|
||||
status: UserStatus.ACTIVE,
|
||||
email: userData.email || 'test@example.com', // 确保有有效邮箱
|
||||
role: Math.max(0, Math.min(userData.role || 1, 9)) // 确保角色在有效范围内
|
||||
};
|
||||
|
||||
const createdUser = { ...validUser, id: BigInt(1) };
|
||||
mockUsersService.create.mockResolvedValueOnce(createdUser);
|
||||
|
||||
const createResponse = await controller.createUser(validUser);
|
||||
|
||||
expect(createResponse.success).toBe(true);
|
||||
expect(createResponse.data).toBeDefined();
|
||||
},
|
||||
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 40 }
|
||||
);
|
||||
});
|
||||
|
||||
it('边界值应该被正确处理', async () => {
|
||||
const boundaryValues = PropertyTestGenerators.generateBoundaryValues();
|
||||
|
||||
await PropertyTestRunner.runPropertyTest(
|
||||
'边界值验证',
|
||||
() => {
|
||||
const user = PropertyTestGenerators.generateUser();
|
||||
return {
|
||||
...user,
|
||||
role: boundaryValues.numbers[Math.floor(Math.random() * boundaryValues.numbers.length)],
|
||||
username: boundaryValues.strings[Math.floor(Math.random() * boundaryValues.strings.length)] || 'defaultuser',
|
||||
status: UserStatus.ACTIVE
|
||||
};
|
||||
},
|
||||
async (userData) => {
|
||||
// 只测试有效的边界值
|
||||
if (userData.role >= 0 && userData.role <= 9 && userData.username.length > 0) {
|
||||
const createdUser = { ...userData, id: BigInt(1) };
|
||||
mockUsersService.create.mockResolvedValueOnce(createdUser);
|
||||
|
||||
const createResponse = await controller.createUser(userData);
|
||||
expect(createResponse.success).toBe(true);
|
||||
} else {
|
||||
// 无效值应该被拒绝,但我们的mock不会抛出错误
|
||||
// 在实际实现中,这些会被DTO验证拦截
|
||||
expect(true).toBe(true); // 占位符断言
|
||||
}
|
||||
},
|
||||
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 20 }
|
||||
);
|
||||
});
|
||||
|
||||
it('分页参数应该被正确验证和限制', async () => {
|
||||
await PropertyTestRunner.runPropertyTest(
|
||||
'分页参数验证',
|
||||
() => PropertyTestGenerators.generatePaginationParams(),
|
||||
async (params) => {
|
||||
const { limit, offset } = params;
|
||||
|
||||
mockUsersService.findAll.mockResolvedValueOnce([]);
|
||||
mockUsersService.count.mockResolvedValueOnce(0);
|
||||
|
||||
const response = await controller.getUserList(limit, offset);
|
||||
|
||||
expect(response.success).toBe(true);
|
||||
|
||||
// 验证分页参数被正确限制
|
||||
expect(response.data.limit).toBeLessThanOrEqual(100); // 最大限制
|
||||
expect(response.data.offset).toBeGreaterThanOrEqual(0); // 最小偏移
|
||||
},
|
||||
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 30 }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user