xiangyixiangqin/miniapp/__tests__/properties/user.property.test.js
2026-01-02 18:00:49 +08:00

171 lines
6.1 KiB
JavaScript

/**
* 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 }
)
})
})
})