Files
whale-town-end/src/business/admin/pagination_query.property.spec.ts
moyin 6924416bbd feat:实现管理员系统核心功能
- 添加管理员数据库管理控制器和服务
- 实现管理员操作日志记录系统
- 添加数据库异常处理过滤器
- 完善管理员权限验证和响应格式
- 添加全面的属性测试覆盖
2026-01-08 23:05:34 +08:00

431 lines
16 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.
/**
* 分页查询属性测试
*
* 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 }
);
});
});
});