feat(http): 适配后端大写 envelope 与测试环境 BASE_URL
This commit is contained in:
parent
fb66a9efe7
commit
9fbe9266eb
|
|
@ -2,7 +2,10 @@ import Taro from '@tarojs/taro'
|
||||||
import { store } from '@/store'
|
import { store } from '@/store'
|
||||||
import { ApiError, BusinessError } from './errors'
|
import { ApiError, BusinessError } from './errors'
|
||||||
|
|
||||||
const BASE_URL = '' // 后端就绪后填入
|
// 测试环境(内网,开发者工具需勾选「不校验合法域名」);正式域名为占位符,上线前必须替换(spec D2)
|
||||||
|
const BASE_URL = process.env.NODE_ENV === 'development'
|
||||||
|
? 'http://192.168.10.11:39101/api/biz'
|
||||||
|
: 'https://PROD_PLACEHOLDER/api/biz'
|
||||||
|
|
||||||
export interface RequestConfig {
|
export interface RequestConfig {
|
||||||
method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
|
method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
|
||||||
|
|
@ -11,10 +14,14 @@ export interface RequestConfig {
|
||||||
needAuth?: boolean // 默认 true;登录接口自身传 false
|
needAuth?: boolean // 默认 true;登录接口自身传 false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 后端统一响应格式(大写 key、字符串状态码)
|
||||||
export interface ApiEnvelope<T> {
|
export interface ApiEnvelope<T> {
|
||||||
code: number
|
Code: string
|
||||||
data: T
|
Message?: string
|
||||||
message?: string
|
Content: T
|
||||||
|
RecordCount?: number
|
||||||
|
PageIndex?: number
|
||||||
|
PageSize?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function request<T>(
|
export async function request<T>(
|
||||||
|
|
@ -25,7 +32,7 @@ export async function request<T>(
|
||||||
const method = config.method ?? 'GET'
|
const method = config.method ?? 'GET'
|
||||||
const timeout = config.timeout ?? 10000
|
const timeout = config.timeout ?? 10000
|
||||||
|
|
||||||
// token 注入(spec §3B):从 authSlice 读取
|
// token 注入:从 authSlice 读取
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...config.headers
|
...config.headers
|
||||||
|
|
@ -55,10 +62,10 @@ export async function request<T>(
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = res.data as ApiEnvelope<T>
|
const body = res.data as ApiEnvelope<T>
|
||||||
if (body.code !== 0) {
|
if (String(body.Code) !== '200') {
|
||||||
throw new BusinessError(body.message ?? '业务错误', body.code)
|
throw new BusinessError(body.Message ?? '业务错误', String(body.Code ?? ''))
|
||||||
}
|
}
|
||||||
return body.data
|
return body.Content
|
||||||
}
|
}
|
||||||
|
|
||||||
request.get = <T>(url: string, params?: Record<string, unknown>, config?: RequestConfig) =>
|
request.get = <T>(url: string, params?: Record<string, unknown>, config?: RequestConfig) =>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||||
|
|
||||||
|
// Taro.request 由各用例设置返回值(vi.hoisted 保证 mock 提升后仍可引用)
|
||||||
|
const mockRequest = vi.hoisted(() => vi.fn())
|
||||||
|
vi.mock('@tarojs/taro', () => ({ default: { request: mockRequest } }))
|
||||||
|
|
||||||
|
// client 依赖 store 取 token;此处只读,不触发 401 流程
|
||||||
|
vi.mock('@/store', () => ({
|
||||||
|
store: { getState: () => ({ auth: { token: 'unit-token' } }), dispatch: vi.fn() }
|
||||||
|
}))
|
||||||
|
|
||||||
|
import { request } from '@/http/client'
|
||||||
|
import { ApiError, BusinessError } from '@/http/errors'
|
||||||
|
|
||||||
|
const okBody = <T>(content: T) => ({
|
||||||
|
statusCode: 200,
|
||||||
|
data: { Code: '200', Message: '', Content: content, RecordCount: 0, PageIndex: 0, PageSize: 0 }
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockRequest.mockReset()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('client envelope 解析', () => {
|
||||||
|
it('Code=200 时解包返回 Content', async () => {
|
||||||
|
mockRequest.mockResolvedValue(okBody({ name: '张三' }))
|
||||||
|
const res = await request.get<{ name: string }>('/demo')
|
||||||
|
expect(res).toEqual({ name: '张三' })
|
||||||
|
// GET 参数经 data 透传(Taro 运行时序列化为 query string)
|
||||||
|
expect(mockRequest.mock.calls[0][0].data).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('GET 参数放入 data 字段', async () => {
|
||||||
|
mockRequest.mockResolvedValue(okBody(null))
|
||||||
|
await request.get('/demo', { page: 1 })
|
||||||
|
expect(mockRequest.mock.calls[0][0].data).toEqual({ page: 1 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Code='500' 抛 BusinessError(message + 字符串 code)", async () => {
|
||||||
|
mockRequest.mockResolvedValue({
|
||||||
|
statusCode: 200,
|
||||||
|
data: { Code: '500', Message: '手机号已绑定', Content: null }
|
||||||
|
})
|
||||||
|
await expect(request.get('/demo')).rejects.toMatchObject({
|
||||||
|
name: 'BusinessError',
|
||||||
|
message: '手机号已绑定',
|
||||||
|
code: '500'
|
||||||
|
})
|
||||||
|
await expect(request.get('/demo')).rejects.toBeInstanceOf(BusinessError)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('HTTP 404 抛 ApiError 并带 statusCode', async () => {
|
||||||
|
mockRequest.mockResolvedValue({ statusCode: 404, data: 'Not Found' })
|
||||||
|
await expect(request.get('/demo')).rejects.toMatchObject({
|
||||||
|
name: 'ApiError',
|
||||||
|
message: 'HTTP 404',
|
||||||
|
statusCode: 404
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Taro.request 异常(断网等)抛 ApiError', async () => {
|
||||||
|
mockRequest.mockRejectedValue(new Error('request:fail timeout'))
|
||||||
|
await expect(request.get('/demo')).rejects.toMatchObject({
|
||||||
|
name: 'ApiError',
|
||||||
|
message: 'request:fail timeout'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('需鉴权请求注入 Bearer token 头', async () => {
|
||||||
|
mockRequest.mockResolvedValue(okBody(null))
|
||||||
|
await request.get('/demo')
|
||||||
|
expect(mockRequest.mock.calls[0][0].header.Authorization).toBe('Bearer unit-token')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('needAuth: false 时不注入 Authorization', async () => {
|
||||||
|
mockRequest.mockResolvedValue(okBody(null))
|
||||||
|
await request.post('/demo', {}, { needAuth: false })
|
||||||
|
expect(mockRequest.mock.calls[0][0].header.Authorization).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('BASE_URL 拼接:请求 url = BASE_URL + path', async () => {
|
||||||
|
mockRequest.mockResolvedValue(okBody(null))
|
||||||
|
await request.get('/thirdPlatform/wxDevLogin/miniapp/login')
|
||||||
|
expect(mockRequest.mock.calls[0][0].url).toMatch(/\/api\/biz\/thirdPlatform\/wxDevLogin\/miniapp\/login$/)
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue