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