2026-07-30 17:59:43 +08:00
|
|
|
import axios, { AxiosHeaders } from 'axios'
|
2026-07-31 13:43:16 +08:00
|
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
|
import {
|
|
|
|
|
ApiResponseError,
|
|
|
|
|
getApiErrorDetails,
|
|
|
|
|
http,
|
|
|
|
|
registerAuthRefreshHandler,
|
|
|
|
|
registerAuthSessionExpiredHandler,
|
|
|
|
|
unwrapApiResponse,
|
|
|
|
|
} from './http'
|
|
|
|
|
|
|
|
|
|
const originalAdapter = http.defaults.adapter
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
http.defaults.adapter = originalAdapter
|
|
|
|
|
registerAuthRefreshHandler(async () => { throw new Error('refresh handler is not configured') })
|
|
|
|
|
registerAuthSessionExpiredHandler(() => undefined)
|
|
|
|
|
})
|
2026-07-30 17:59:43 +08:00
|
|
|
|
|
|
|
|
describe('API error diagnostics', () => {
|
2026-08-01 12:17:33 +08:00
|
|
|
it('treats only body code 200 as success', () => {
|
|
|
|
|
expect(unwrapApiResponse({ code: '200', message: 'success', data: { id: '1' } }))
|
|
|
|
|
.toEqual({ id: '1' })
|
|
|
|
|
expect(() => unwrapApiResponse({ code: '0', message: 'legacy success code', data: null }))
|
|
|
|
|
.toThrow(ApiResponseError)
|
|
|
|
|
})
|
|
|
|
|
|
2026-07-30 17:59:43 +08:00
|
|
|
it('keeps business request and trace identifiers when unwrapping fails', () => {
|
|
|
|
|
expect(() => unwrapApiResponse({
|
|
|
|
|
code: 'A0403',
|
|
|
|
|
message: '无权限',
|
|
|
|
|
data: null,
|
|
|
|
|
requestId: 'request-1',
|
|
|
|
|
traceId: 'trace-1',
|
|
|
|
|
})).toThrow(ApiResponseError)
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
unwrapApiResponse({ code: 'A0403', message: '无权限', data: null, requestId: 'request-1', traceId: 'trace-1' })
|
|
|
|
|
} catch (error) {
|
|
|
|
|
expect(getApiErrorDetails(error)).toEqual({
|
|
|
|
|
message: '无权限',
|
|
|
|
|
code: 'A0403',
|
|
|
|
|
requestId: 'request-1',
|
|
|
|
|
traceId: 'trace-1',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('extracts identifiers from an HTTP error envelope', () => {
|
|
|
|
|
const error = new axios.AxiosError('Request failed', 'ERR_BAD_RESPONSE', undefined, undefined, {
|
|
|
|
|
status: 503,
|
|
|
|
|
statusText: 'Service Unavailable',
|
|
|
|
|
headers: {},
|
|
|
|
|
config: { headers: new AxiosHeaders() },
|
|
|
|
|
data: { code: 'S0503', message: '服务不可用', requestId: 'request-2', traceId: 'trace-2' },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(getApiErrorDetails(error)).toEqual({
|
|
|
|
|
message: '服务不可用',
|
|
|
|
|
code: 'S0503',
|
|
|
|
|
requestId: 'request-2',
|
|
|
|
|
traceId: 'trace-2',
|
|
|
|
|
})
|
|
|
|
|
})
|
2026-07-31 13:43:16 +08:00
|
|
|
|
2026-08-04 14:15:32 +08:00
|
|
|
it('explains a proxy 413 response without a JSON error envelope', () => {
|
|
|
|
|
const error = new axios.AxiosError('Request failed', 'ERR_BAD_REQUEST', undefined, undefined, {
|
|
|
|
|
status: 413,
|
|
|
|
|
statusText: 'Payload Too Large',
|
|
|
|
|
headers: {},
|
|
|
|
|
config: { headers: new AxiosHeaders() },
|
|
|
|
|
data: '<html>Request Entity Too Large</html>',
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(getApiErrorDetails(error)).toEqual({
|
|
|
|
|
message: '上传文件的请求体过大,服务器已拒绝;请压缩后重试,如文件未超过 5MB,请联系管理员检查网关上传限制',
|
|
|
|
|
code: 'HTTP_413',
|
|
|
|
|
requestId: undefined,
|
|
|
|
|
traceId: undefined,
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
2026-07-31 13:43:16 +08:00
|
|
|
it('notifies session expiry once when concurrent 401 requests share a failed refresh', async () => {
|
|
|
|
|
const refresh = vi.fn(async () => { throw new Error('refresh token expired') })
|
|
|
|
|
const expired = vi.fn()
|
|
|
|
|
registerAuthRefreshHandler(refresh)
|
|
|
|
|
registerAuthSessionExpiredHandler(expired)
|
|
|
|
|
http.defaults.adapter = async (config) => {
|
|
|
|
|
throw new axios.AxiosError('Unauthorized', 'ERR_BAD_REQUEST', config, undefined, {
|
|
|
|
|
status: 401,
|
|
|
|
|
statusText: 'Unauthorized',
|
|
|
|
|
headers: {},
|
|
|
|
|
config,
|
|
|
|
|
data: { code: 'A0401', message: '登录状态已过期' },
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const results = await Promise.allSettled([
|
|
|
|
|
http.get('/platform/v1/orders'),
|
|
|
|
|
http.get('/platform/v1/warehouses'),
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
expect(results.every((result) => result.status === 'rejected')).toBe(true)
|
|
|
|
|
expect(refresh).toHaveBeenCalledTimes(1)
|
|
|
|
|
expect(expired).toHaveBeenCalledTimes(1)
|
|
|
|
|
})
|
2026-07-30 17:59:43 +08:00
|
|
|
})
|