/** * Property-based tests for user API * **Feature: miniapp-frontend, Property 19: Search Parameters** * **Validates: Requirements 11.2** */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import * as fc from 'fast-check' // Mock uni object vi.mock('../../__tests__/mocks/uni.js', () => ({})) // Create a mock for the request module to capture API calls let capturedRequest = null vi.mock('../../api/request', () => ({ post: vi.fn((url, data) => { capturedRequest = { url, data } return Promise.resolve({ success: true, data: { items: [], total: 0 } }) }), get: vi.fn((url, data) => { capturedRequest = { url, data } return Promise.resolve({ success: true, data: [] }) }) })) // Import after mocking import { search } from '../../api/user.js' import { post } from '../../api/request' describe('User API Property Tests', () => { beforeEach(() => { capturedRequest = null vi.clearAllMocks() }) /** * Property 19: Search Parameters * *For any* search submission, the API call SHALL include all filter parameters * matching the user's selections. * **Validates: Requirements 11.2** */ describe('Property 19: Search Parameters', () => { // Arbitrary for search parameters const searchParamsArb = fc.record({ ageMin: fc.option(fc.integer({ min: 18, max: 50 }), { nil: undefined }), ageMax: fc.option(fc.integer({ min: 18, max: 50 }), { nil: undefined }), heightMin: fc.option(fc.integer({ min: 140, max: 200 }), { nil: undefined }), heightMax: fc.option(fc.integer({ min: 140, max: 200 }), { nil: undefined }), education: fc.option(fc.array(fc.integer({ min: 1, max: 7 }), { minLength: 0, maxLength: 5 }), { nil: undefined }), province: fc.option(fc.stringOf(fc.constantFrom('北京', '上海', '广东', '浙江', '江苏'), { minLength: 0, maxLength: 1 }), { nil: undefined }), city: fc.option(fc.stringOf(fc.constantFrom('北京', '上海', '广州', '深圳', '杭州'), { minLength: 0, maxLength: 1 }), { nil: undefined }), monthlyIncomeMin: fc.option(fc.integer({ min: 0, max: 100000 }), { nil: undefined }), monthlyIncomeMax: fc.option(fc.integer({ min: 0, max: 100000 }), { nil: undefined }), houseStatus: fc.option(fc.array(fc.integer({ min: 1, max: 4 }), { minLength: 0, maxLength: 4 }), { nil: undefined }), carStatus: fc.option(fc.array(fc.integer({ min: 1, max: 3 }), { minLength: 0, maxLength: 3 }), { nil: undefined }), marriageStatus: fc.option(fc.array(fc.integer({ min: 1, max: 4 }), { minLength: 0, maxLength: 4 }), { nil: undefined }), pageIndex: fc.option(fc.integer({ min: 1, max: 100 }), { nil: undefined }), pageSize: fc.option(fc.integer({ min: 10, max: 50 }), { nil: undefined }) }) it('should pass all provided search parameters to the API', async () => { await fc.assert( fc.asyncProperty( searchParamsArb, async (params) => { // Reset captured request capturedRequest = null // Call search with the generated params await search(params) // Verify the API was called expect(post).toHaveBeenCalled() // Get the captured request data const callArgs = post.mock.calls[post.mock.calls.length - 1] const [url, sentData] = callArgs // Verify URL expect(url).toBe('/users/search') // Verify all defined parameters are passed through for (const [key, value] of Object.entries(params)) { if (value !== undefined) { expect(sentData[key]).toEqual(value) } } return true } ), { numRuns: 100 } ) }) it('should handle empty search parameters', async () => { await fc.assert( fc.asyncProperty( fc.constant({}), async (params) => { await search(params) expect(post).toHaveBeenCalledWith('/users/search', params) return true } ), { numRuns: 10 } ) }) it('should preserve parameter types correctly', async () => { await fc.assert( fc.asyncProperty( fc.record({ ageMin: fc.integer({ min: 18, max: 50 }), ageMax: fc.integer({ min: 18, max: 50 }), education: fc.array(fc.integer({ min: 1, max: 7 }), { minLength: 1, maxLength: 3 }), pageIndex: fc.integer({ min: 1, max: 10 }), pageSize: fc.integer({ min: 10, max: 50 }) }), async (params) => { await search(params) const callArgs = post.mock.calls[post.mock.calls.length - 1] const [, sentData] = callArgs // Verify numeric types are preserved expect(typeof sentData.ageMin).toBe('number') expect(typeof sentData.ageMax).toBe('number') expect(typeof sentData.pageIndex).toBe('number') expect(typeof sentData.pageSize).toBe('number') // Verify array type is preserved expect(Array.isArray(sentData.education)).toBe(true) return true } ), { numRuns: 100 } ) }) it('should ensure ageMin <= ageMax when both are provided', async () => { await fc.assert( fc.asyncProperty( fc.integer({ min: 18, max: 50 }).chain(min => fc.record({ ageMin: fc.constant(min), ageMax: fc.integer({ min: min, max: 50 }) }) ), async (params) => { await search(params) const callArgs = post.mock.calls[post.mock.calls.length - 1] const [, sentData] = callArgs // The API should receive the params as-is // Validation of ageMin <= ageMax is a business rule that should be enforced expect(sentData.ageMin).toBeLessThanOrEqual(sentData.ageMax) return true } ), { numRuns: 100 } ) }) }) })