forked from datawhale/whale-town-end
feat:实现管理员系统核心功能
- 添加管理员数据库管理控制器和服务 - 实现管理员操作日志记录系统 - 添加数据库异常处理过滤器 - 完善管理员权限验证和响应格式 - 添加全面的属性测试覆盖
This commit is contained in:
431
src/business/admin/pagination_query.property.spec.ts
Normal file
431
src/business/admin/pagination_query.property.spec.ts
Normal file
@@ -0,0 +1,431 @@
|
||||
/**
|
||||
* 分页查询属性测试
|
||||
*
|
||||
* Property 8: 分页查询正确性
|
||||
* Property 14: 分页限制保护
|
||||
*
|
||||
* Validates: Requirements 4.4, 4.5, 8.3
|
||||
*
|
||||
* 测试目标:
|
||||
* - 验证分页查询的正确性和一致性
|
||||
* - 确保分页限制保护机制有效
|
||||
* - 验证分页参数的边界处理
|
||||
*
|
||||
* 最近修改:
|
||||
* - 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 mockUsersService: any;
|
||||
let mockUserProfilesService: any;
|
||||
let mockZulipAccountsService: any;
|
||||
|
||||
beforeAll(async () => {
|
||||
mockUsersService = {
|
||||
findAll: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
search: jest.fn(),
|
||||
count: jest.fn()
|
||||
};
|
||||
|
||||
mockUserProfilesService = {
|
||||
findAll: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
findByMap: jest.fn(),
|
||||
count: jest.fn()
|
||||
};
|
||||
|
||||
mockZulipAccountsService = {
|
||||
findMany: jest.fn(),
|
||||
findById: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
getStatusStatistics: jest.fn()
|
||||
};
|
||||
|
||||
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: mockUserProfilesService
|
||||
},
|
||||
{
|
||||
provide: 'ZulipAccountsService',
|
||||
useValue: mockZulipAccountsService
|
||||
}
|
||||
]
|
||||
})
|
||||
.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 8: 分页查询正确性', () => {
|
||||
it('分页参数应该被正确传递和处理', async () => {
|
||||
await PropertyTestRunner.runPropertyTest(
|
||||
'分页参数传递正确性',
|
||||
() => PropertyTestGenerators.generatePaginationParams(),
|
||||
async (params) => {
|
||||
const { limit, offset } = params;
|
||||
const safeLimit = Math.min(Math.max(limit, 1), 100);
|
||||
const safeOffset = Math.max(offset, 0);
|
||||
|
||||
const totalItems = Math.floor(Math.random() * 200) + 50;
|
||||
const itemsToReturn = Math.min(safeLimit, Math.max(0, totalItems - safeOffset));
|
||||
|
||||
// Mock用户列表查询
|
||||
const mockUsers = Array.from({ length: itemsToReturn }, (_, i) => ({
|
||||
...PropertyTestGenerators.generateUser(),
|
||||
id: BigInt(safeOffset + i + 1)
|
||||
}));
|
||||
|
||||
mockUsersService.findAll.mockResolvedValueOnce(mockUsers);
|
||||
mockUsersService.count.mockResolvedValueOnce(totalItems);
|
||||
|
||||
const response = await controller.getUserList(safeLimit, safeOffset);
|
||||
|
||||
expect(response.success).toBe(true);
|
||||
PropertyTestAssertions.assertPaginationLogic(response, safeLimit, safeOffset);
|
||||
|
||||
// 验证分页计算正确性
|
||||
expect(response.data.limit).toBe(safeLimit);
|
||||
expect(response.data.offset).toBe(safeOffset);
|
||||
expect(response.data.total).toBe(totalItems);
|
||||
expect(response.data.items.length).toBe(itemsToReturn);
|
||||
|
||||
const expectedHasMore = safeOffset + itemsToReturn < totalItems;
|
||||
expect(response.data.has_more).toBe(expectedHasMore);
|
||||
},
|
||||
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 50 }
|
||||
);
|
||||
});
|
||||
|
||||
it('不同实体类型的分页查询应该保持一致性', async () => {
|
||||
await PropertyTestRunner.runPropertyTest(
|
||||
'多实体分页一致性',
|
||||
() => PropertyTestGenerators.generatePaginationParams(),
|
||||
async (params) => {
|
||||
const { limit, offset } = params;
|
||||
const safeLimit = Math.min(Math.max(limit, 1), 100);
|
||||
const safeOffset = Math.max(offset, 0);
|
||||
|
||||
const totalCount = Math.floor(Math.random() * 100) + 20;
|
||||
const itemCount = Math.min(safeLimit, Math.max(0, totalCount - safeOffset));
|
||||
|
||||
// Mock所有实体类型的查询
|
||||
mockUsersService.findAll.mockResolvedValueOnce(
|
||||
Array.from({ length: itemCount }, () => PropertyTestGenerators.generateUser())
|
||||
);
|
||||
mockUsersService.count.mockResolvedValueOnce(totalCount);
|
||||
|
||||
mockUserProfilesService.findAll.mockResolvedValueOnce(
|
||||
Array.from({ length: itemCount }, () => PropertyTestGenerators.generateUserProfile())
|
||||
);
|
||||
mockUserProfilesService.count.mockResolvedValueOnce(totalCount);
|
||||
|
||||
mockZulipAccountsService.findMany.mockResolvedValueOnce({
|
||||
accounts: Array.from({ length: itemCount }, () => PropertyTestGenerators.generateZulipAccount()),
|
||||
total: totalCount
|
||||
});
|
||||
|
||||
// 测试所有列表端点
|
||||
const userResponse = await controller.getUserList(safeLimit, safeOffset);
|
||||
const profileResponse = await controller.getUserProfileList(safeLimit, safeOffset);
|
||||
const zulipResponse = await controller.getZulipAccountList(safeLimit, safeOffset);
|
||||
|
||||
// 验证所有响应的分页格式一致
|
||||
[userResponse, profileResponse, zulipResponse].forEach(response => {
|
||||
PropertyTestAssertions.assertPaginationLogic(response, safeLimit, safeOffset);
|
||||
expect(response.data.limit).toBe(safeLimit);
|
||||
expect(response.data.offset).toBe(safeOffset);
|
||||
});
|
||||
},
|
||||
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 30 }
|
||||
);
|
||||
});
|
||||
|
||||
it('边界条件下的分页查询应该正确处理', async () => {
|
||||
const boundaryValues = PropertyTestGenerators.generateBoundaryValues();
|
||||
|
||||
await PropertyTestRunner.runPropertyTest(
|
||||
'分页边界条件处理',
|
||||
() => {
|
||||
const limits = boundaryValues.limits;
|
||||
const offsets = boundaryValues.offsets;
|
||||
|
||||
return {
|
||||
limit: limits[Math.floor(Math.random() * limits.length)],
|
||||
offset: offsets[Math.floor(Math.random() * offsets.length)]
|
||||
};
|
||||
},
|
||||
async ({ limit, offset }) => {
|
||||
const safeLimit = Math.min(Math.max(limit, 1), 100);
|
||||
const safeOffset = Math.max(offset, 0);
|
||||
|
||||
const totalItems = 150;
|
||||
const itemsToReturn = Math.min(safeLimit, Math.max(0, totalItems - safeOffset));
|
||||
|
||||
mockUsersService.findAll.mockResolvedValueOnce(
|
||||
Array.from({ length: itemsToReturn }, () => PropertyTestGenerators.generateUser())
|
||||
);
|
||||
mockUsersService.count.mockResolvedValueOnce(totalItems);
|
||||
|
||||
const response = await controller.getUserList(limit, offset);
|
||||
|
||||
expect(response.success).toBe(true);
|
||||
|
||||
// 验证边界值被正确处理
|
||||
expect(response.data.limit).toBeGreaterThan(0);
|
||||
expect(response.data.limit).toBeLessThanOrEqual(100);
|
||||
expect(response.data.offset).toBeGreaterThanOrEqual(0);
|
||||
expect(response.data.items.length).toBeLessThanOrEqual(response.data.limit);
|
||||
},
|
||||
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 40 }
|
||||
);
|
||||
});
|
||||
|
||||
it('空结果集的分页查询应该正确处理', async () => {
|
||||
await PropertyTestRunner.runPropertyTest(
|
||||
'空结果集分页处理',
|
||||
() => PropertyTestGenerators.generatePaginationParams(),
|
||||
async (params) => {
|
||||
const { limit, offset } = params;
|
||||
const safeLimit = Math.min(Math.max(limit, 1), 100);
|
||||
const safeOffset = Math.max(offset, 0);
|
||||
|
||||
// Mock空结果
|
||||
mockUsersService.findAll.mockResolvedValueOnce([]);
|
||||
mockUsersService.count.mockResolvedValueOnce(0);
|
||||
|
||||
const response = await controller.getUserList(safeLimit, safeOffset);
|
||||
|
||||
expect(response.success).toBe(true);
|
||||
expect(response.data.items).toEqual([]);
|
||||
expect(response.data.total).toBe(0);
|
||||
expect(response.data.has_more).toBe(false);
|
||||
expect(response.data.limit).toBe(safeLimit);
|
||||
expect(response.data.offset).toBe(safeOffset);
|
||||
},
|
||||
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 20 }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Property 14: 分页限制保护', () => {
|
||||
it('超大limit值应该被限制到最大值', async () => {
|
||||
await PropertyTestRunner.runPropertyTest(
|
||||
'超大limit限制保护',
|
||||
() => ({
|
||||
limit: Math.floor(Math.random() * 9900) + 101, // 101-10000
|
||||
offset: Math.floor(Math.random() * 100)
|
||||
}),
|
||||
async ({ limit, offset }) => {
|
||||
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.limit).toBeGreaterThan(0);
|
||||
},
|
||||
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 30 }
|
||||
);
|
||||
});
|
||||
|
||||
it('负数limit值应该被修正为正数', async () => {
|
||||
await PropertyTestRunner.runPropertyTest(
|
||||
'负数limit修正保护',
|
||||
() => ({
|
||||
limit: -Math.floor(Math.random() * 100) - 1, // 负数
|
||||
offset: Math.floor(Math.random() * 100)
|
||||
}),
|
||||
async ({ limit, offset }) => {
|
||||
mockUsersService.findAll.mockResolvedValueOnce([]);
|
||||
mockUsersService.count.mockResolvedValueOnce(0);
|
||||
|
||||
const response = await controller.getUserList(limit, offset);
|
||||
|
||||
expect(response.success).toBe(true);
|
||||
expect(response.data.limit).toBeGreaterThan(0); // 应该被修正为正数
|
||||
},
|
||||
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 20 }
|
||||
);
|
||||
});
|
||||
|
||||
it('负数offset值应该被修正为0', async () => {
|
||||
await PropertyTestRunner.runPropertyTest(
|
||||
'负数offset修正保护',
|
||||
() => ({
|
||||
limit: Math.floor(Math.random() * 50) + 1,
|
||||
offset: -Math.floor(Math.random() * 100) - 1 // 负数
|
||||
}),
|
||||
async ({ limit, offset }) => {
|
||||
mockUsersService.findAll.mockResolvedValueOnce([]);
|
||||
mockUsersService.count.mockResolvedValueOnce(0);
|
||||
|
||||
const response = await controller.getUserList(limit, offset);
|
||||
|
||||
expect(response.success).toBe(true);
|
||||
expect(response.data.offset).toBeGreaterThanOrEqual(0); // 应该被修正为非负数
|
||||
},
|
||||
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 20 }
|
||||
);
|
||||
});
|
||||
|
||||
it('零值limit应该被修正为默认值', async () => {
|
||||
await PropertyTestRunner.runPropertyTest(
|
||||
'零值limit修正保护',
|
||||
() => ({
|
||||
limit: 0,
|
||||
offset: Math.floor(Math.random() * 100)
|
||||
}),
|
||||
async ({ limit, offset }) => {
|
||||
mockUsersService.findAll.mockResolvedValueOnce([]);
|
||||
mockUsersService.count.mockResolvedValueOnce(0);
|
||||
|
||||
const response = await controller.getUserList(limit, offset);
|
||||
|
||||
expect(response.success).toBe(true);
|
||||
expect(response.data.limit).toBeGreaterThan(0); // 应该被修正为正数
|
||||
},
|
||||
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 15 }
|
||||
);
|
||||
});
|
||||
|
||||
it('极大offset值应该返回空结果但不报错', async () => {
|
||||
await PropertyTestRunner.runPropertyTest(
|
||||
'极大offset处理保护',
|
||||
() => ({
|
||||
limit: Math.floor(Math.random() * 50) + 1,
|
||||
offset: Math.floor(Math.random() * 90000) + 10000 // 极大偏移
|
||||
}),
|
||||
async ({ limit, offset }) => {
|
||||
const totalItems = Math.floor(Math.random() * 1000) + 100;
|
||||
|
||||
mockUsersService.findAll.mockResolvedValueOnce([]);
|
||||
mockUsersService.count.mockResolvedValueOnce(totalItems);
|
||||
|
||||
const response = await controller.getUserList(limit, offset);
|
||||
|
||||
expect(response.success).toBe(true);
|
||||
|
||||
// 当offset超过总数时,应该返回空结果
|
||||
if (offset >= totalItems) {
|
||||
expect(response.data.items).toEqual([]);
|
||||
expect(response.data.has_more).toBe(false);
|
||||
}
|
||||
|
||||
expect(response.data.offset).toBe(offset); // offset应该保持原值
|
||||
},
|
||||
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 25 }
|
||||
);
|
||||
});
|
||||
|
||||
it('分页保护机制应该在所有端点中一致', async () => {
|
||||
await PropertyTestRunner.runPropertyTest(
|
||||
'分页保护一致性',
|
||||
() => ({
|
||||
limit: Math.floor(Math.random() * 200) + 101, // 超过限制的值
|
||||
offset: -Math.floor(Math.random() * 50) - 1 // 负数偏移
|
||||
}),
|
||||
async ({ limit, offset }) => {
|
||||
// Mock所有服务
|
||||
mockUsersService.findAll.mockResolvedValueOnce([]);
|
||||
mockUsersService.count.mockResolvedValueOnce(0);
|
||||
|
||||
mockUserProfilesService.findAll.mockResolvedValueOnce([]);
|
||||
mockUserProfilesService.count.mockResolvedValueOnce(0);
|
||||
|
||||
mockZulipAccountsService.findMany.mockResolvedValueOnce({ accounts: [], total: 0 });
|
||||
|
||||
// 测试所有列表端点
|
||||
const userResponse = await controller.getUserList(limit, offset);
|
||||
const profileResponse = await controller.getUserProfileList(limit, offset);
|
||||
const zulipResponse = await controller.getZulipAccountList(limit, offset);
|
||||
|
||||
// 验证所有端点的保护机制一致
|
||||
[userResponse, profileResponse, zulipResponse].forEach(response => {
|
||||
expect(response.success).toBe(true);
|
||||
expect(response.data.limit).toBeLessThanOrEqual(100); // 最大限制
|
||||
expect(response.data.limit).toBeGreaterThan(0); // 最小限制
|
||||
expect(response.data.offset).toBeGreaterThanOrEqual(0); // 非负偏移
|
||||
});
|
||||
},
|
||||
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 25 }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user