import { describe, it, expect, beforeEach, vi } from 'vitest' import * as fc from 'fast-check' import { TOKEN_KEY, LOCALE_KEY } from '../utils/storage.js' // 模拟uni全局对象 let mockStorage = {} let lastRequestOptions = null let requestSuccessCallback = null let requestFailCallback = null beforeEach(() => { mockStorage = {} lastRequestOptions = null requestSuccessCallback = null requestFailCallback = null globalThis.uni = { setStorageSync: (key, value) => { mockStorage[key] = value }, getStorageSync: (key) => mockStorage[key] || '', removeStorageSync: (key) => { delete mockStorage[key] }, showToast: vi.fn(), reLaunch: vi.fn(), request: (options) => { lastRequestOptions = options requestSuccessCallback = options.success requestFailCallback = options.fail } } }) /** * 动态导入request模块(每次测试重新加载以获取最新的mock状态) */ async function loadRequestModule() { // 清除模块缓存以确保重新读取mock vi.resetModules() return await import('../api/request.js') } describe('Property 3: 请求拦截器请求头注入', () => { /** * **Feature: mobile-app, Property 3: 请求拦截器请求头注入** * **Validates: Requirements 1.3, 2.6, 11.1** * * 对于任意已存储的JWT Token和任意当前语言设置, * 通过请求模块发出的每个HTTP请求,其请求头中Authorization字段应为"Bearer {token}", * Accept-Language字段应为当前语言标识。 */ it('请求头应包含正确的Authorization和Accept-Language', async () => { const tokenArb = fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0) const localeArb = fc.constantFrom('zh-CN', 'zh-TW', 'en') await fc.assert( fc.asyncProperty(tokenArb, localeArb, async (token, locale) => { mockStorage[TOKEN_KEY] = token mockStorage[LOCALE_KEY] = locale const { get } = await loadRequestModule() get('/api/test') expect(lastRequestOptions).not.toBeNull() expect(lastRequestOptions.header['Authorization']).toBe(`Bearer ${token}`) expect(lastRequestOptions.header['Accept-Language']).toBe(locale) }), { numRuns: 100 } ) }) }) describe('Property 6: 401响应自动清除认证', () => { /** * **Feature: mobile-app, Property 6: 401响应自动清除认证** * **Validates: Requirements 2.8, 11.3** * * 对于任意API请求,当响应状态码为401时, * 本地存储中的Token应被清除,用户状态应重置为未登录。 */ it('收到401响应时应清除本地Token', async () => { const tokenArb = fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0) await fc.assert( fc.asyncProperty(tokenArb, async (token) => { mockStorage[TOKEN_KEY] = token const { get } = await loadRequestModule() const promise = get('/api/test') // 模拟401响应 requestSuccessCallback({ statusCode: 401, data: {} }) try { await promise } catch (e) { // 预期会reject } // 验证Token已被清除 expect(mockStorage[TOKEN_KEY]).toBeUndefined() expect(uni.reLaunch).toHaveBeenCalled() }), { numRuns: 100 } ) }) }) describe('Property 14: API响应统一处理', () => { /** * **Feature: mobile-app, Property 14: API响应统一处理** * **Validates: Requirements 11.2** * * 对于任意API响应体,当success为true时应返回data数据, * 当success为false时应抛出或返回包含message的错误信息。 */ it('success为true时resolve,success为false时reject并包含message', async () => { const messageArb = fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0) const dataArb = fc.anything() // 测试success=true的情况 await fc.assert( fc.asyncProperty(dataArb, async (data) => { const { get } = await loadRequestModule() const promise = get('/api/test') requestSuccessCallback({ statusCode: 200, data: { success: true, data, message: 'ok' } }) const result = await promise expect(result.success).toBe(true) }), { numRuns: 50 } ) // 测试success=false的情况 await fc.assert( fc.asyncProperty(messageArb, async (message) => { const { get } = await loadRequestModule() const promise = get('/api/test') requestSuccessCallback({ statusCode: 200, data: { success: false, data: null, message } }) await expect(promise).rejects.toThrow(message) expect(uni.showToast).toHaveBeenCalledWith({ title: message, icon: 'none' }) }), { numRuns: 50 } ) }) }) describe('Property 15: 网络错误友好提示', () => { /** * **Feature: mobile-app, Property 15: 网络错误友好提示** * **Validates: Requirements 11.5** * * 对于任意网络错误(超时、断网、服务器错误), * 请求模块应捕获错误并调用提示函数展示错误信息,而非抛出未处理异常。 */ it('网络错误时应调用showToast展示友好提示', async () => { const errorTypeArb = fc.constantFrom('timeout', 'network', 'server') await fc.assert( fc.asyncProperty(errorTypeArb, async (errorType) => { const { get } = await loadRequestModule() const promise = get('/api/test') if (errorType === 'timeout') { requestFailCallback({ errMsg: 'request:fail timeout' }) } else if (errorType === 'network') { requestFailCallback({ errMsg: 'request:fail' }) } else { // 服务器500错误 requestSuccessCallback({ statusCode: 500, data: {} }) } await expect(promise).rejects.toThrow() expect(uni.showToast).toHaveBeenCalled() const toastCall = uni.showToast.mock.calls[uni.showToast.mock.calls.length - 1][0] expect(toastCall.title).toBeTruthy() expect(toastCall.icon).toBe('none') }), { numRuns: 100 } ) }) })