191 lines
6.0 KiB
JavaScript
191 lines
6.0 KiB
JavaScript
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 }
|
||
)
|
||
})
|
||
})
|