vending-machine/mobile/__tests__/request.property.test.js
2026-04-08 20:45:41 +08:00

191 lines
6.0 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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时resolvesuccess为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 }
)
})
})