优化
This commit is contained in:
@@ -1,23 +1,21 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { http } from './http'
|
||||
import { overseasHttp } from './overseasHttp'
|
||||
import { getLoadingAssociationSummary, getOverseasInboundCodeRelationship, getPlatformInboundCodeAssociation } from './inboundCodeRelations'
|
||||
|
||||
vi.mock('./http', async (original) => ({ ...(await original<typeof import('./http')>()), http: { get: vi.fn() } }))
|
||||
vi.mock('./overseasHttp', () => ({ overseasHttp: { get: vi.fn() } }))
|
||||
|
||||
describe('inbound-code relationship APIs', () => {
|
||||
beforeEach(() => vi.clearAllMocks())
|
||||
|
||||
it('uses the frozen domestic and overseas read models and encodes the business code', async () => {
|
||||
vi.mocked(http.get).mockResolvedValue({ code: '200', data: { inboundCode: 'IN/01', orderNo: 'ORD-1' } })
|
||||
vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', data: { inboundCode: 'IN/01', found: false, taillegOrders: [], inboundPlans: [], palletCodes: [], inventory: [] } })
|
||||
vi.mocked(http.get).mockResolvedValue({ code: '200', data: { inboundCode: 'IN/01', found: false, taillegOrders: [], inboundPlans: [], palletCodes: [], inventory: [] } })
|
||||
await getPlatformInboundCodeAssociation('IN/01')
|
||||
await getLoadingAssociationSummary('IN/01')
|
||||
await getOverseasInboundCodeRelationship('IN/01')
|
||||
expect(http.get).toHaveBeenCalledWith('/platform/v1/orders/first-leg/inbound-code-associations/IN%2F01')
|
||||
expect(http.get).toHaveBeenCalledWith('/platform/v1/first-leg/loading-associations/summary', { params: { inboundCode: 'IN/01' } })
|
||||
expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/inventory/inbound-code-relationships/IN%2F01')
|
||||
expect(http.get).toHaveBeenCalledWith('/platform/v1/last-leg/inventory/inbound-code-relationships/IN%2F01')
|
||||
})
|
||||
|
||||
it('does not accept customer or internal ids as lookup parameters', () => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { http, unwrapApiResponse, type ApiEnvelope } from './http'
|
||||
import { overseasHttp } from './overseasHttp'
|
||||
|
||||
export interface PlatformInboundCodeAssociation {
|
||||
inboundCode: string
|
||||
@@ -68,7 +67,7 @@ export async function getLoadingAssociationSummaryByInboundId(inboundId: string)
|
||||
}
|
||||
|
||||
export async function getOverseasInboundCodeRelationship(inboundCode: string) {
|
||||
const response = await overseasHttp.get<ApiEnvelope<OverseasInboundCodeRelationship>, ApiEnvelope<OverseasInboundCodeRelationship>>(
|
||||
const response = await http.get<ApiEnvelope<OverseasInboundCodeRelationship>, ApiEnvelope<OverseasInboundCodeRelationship>>(
|
||||
`/platform/v1/last-leg/inventory/inbound-code-relationships/${encodeURIComponent(inboundCode)}`,
|
||||
)
|
||||
return unwrapApiResponse(response)
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
import axios, { type InternalAxiosRequestConfig } from 'axios'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { registerAuthRefreshHandler } from './http'
|
||||
import { clearOverseasAccessToken, overseasHttp, replaceOverseasAccessToken } from './overseasHttp'
|
||||
|
||||
const originalOverseasAdapter = overseasHttp.defaults.adapter
|
||||
|
||||
function success(config: InternalAxiosRequestConfig, data: unknown) {
|
||||
return Promise.resolve({ status: 200, statusText: 'OK', headers: {}, config, data })
|
||||
}
|
||||
|
||||
describe('overseas HTTP client', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.setItem('cc-admin-token', 'domestic-token')
|
||||
clearOverseasAccessToken()
|
||||
registerAuthRefreshHandler(async () => 'domestic-token')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
overseasHttp.defaults.adapter = originalOverseasAdapter
|
||||
localStorage.clear()
|
||||
clearOverseasAccessToken()
|
||||
})
|
||||
|
||||
it('uses the overseas token supplied by the login session for concurrent requests', async () => {
|
||||
replaceOverseasAccessToken('overseas-token', 900)
|
||||
const authorizations: unknown[] = []
|
||||
overseasHttp.defaults.adapter = async (config) => {
|
||||
authorizations.push(config.headers.Authorization)
|
||||
return success(config, { code: '200', message: 'success', data: [] })
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
overseasHttp.get('/platform/v1/last-leg/orders'),
|
||||
overseasHttp.get('/platform/v1/warehouse-admin'),
|
||||
])
|
||||
|
||||
expect(authorizations).toEqual(['Bearer overseas-token', 'Bearer overseas-token'])
|
||||
})
|
||||
|
||||
it('refreshes the unified session once and replays once after an overseas 401', async () => {
|
||||
replaceOverseasAccessToken('overseas-1', 900)
|
||||
const refresh = vi.fn(async () => {
|
||||
replaceOverseasAccessToken('overseas-2', 900)
|
||||
return 'domestic-2'
|
||||
})
|
||||
registerAuthRefreshHandler(refresh)
|
||||
let requests = 0
|
||||
const authorizations: unknown[] = []
|
||||
overseasHttp.defaults.adapter = async (config) => {
|
||||
authorizations.push(config.headers.Authorization)
|
||||
if (requests++ === 0) {
|
||||
throw new axios.AxiosError('Unauthorized', 'ERR_BAD_REQUEST', config, undefined, {
|
||||
status: 401,
|
||||
statusText: 'Unauthorized',
|
||||
headers: {},
|
||||
config,
|
||||
data: { code: 'A0401', message: '海外令牌失效' },
|
||||
})
|
||||
}
|
||||
return success(config, { code: '200', message: 'success', data: [] })
|
||||
}
|
||||
|
||||
await overseasHttp.get('/platform/v1/last-leg/orders')
|
||||
|
||||
expect(refresh).toHaveBeenCalledTimes(1)
|
||||
expect(requests).toBe(2)
|
||||
expect(authorizations).toEqual(['Bearer overseas-1', 'Bearer overseas-2'])
|
||||
})
|
||||
|
||||
it('refreshes the unified session when no usable overseas token is present', async () => {
|
||||
const refresh = vi.fn(async () => {
|
||||
replaceOverseasAccessToken('overseas-refreshed', 900)
|
||||
return 'domestic-refreshed'
|
||||
})
|
||||
registerAuthRefreshHandler(refresh)
|
||||
let authorization: unknown
|
||||
overseasHttp.defaults.adapter = async (config) => {
|
||||
authorization = config.headers.Authorization
|
||||
return success(config, { code: '200', message: 'success', data: [] })
|
||||
}
|
||||
|
||||
await overseasHttp.get('/platform/v1/last-leg/orders')
|
||||
|
||||
expect(refresh).toHaveBeenCalledTimes(1)
|
||||
expect(authorization).toBe('Bearer overseas-refreshed')
|
||||
})
|
||||
|
||||
it('fails before sending a business request when refresh is domestic-only', async () => {
|
||||
const refresh = vi.fn(async () => 'domestic-only')
|
||||
registerAuthRefreshHandler(refresh)
|
||||
const adapter = vi.fn(async (config: InternalAxiosRequestConfig) => success(config, { code: '200', message: 'success', data: [] }))
|
||||
overseasHttp.defaults.adapter = adapter
|
||||
|
||||
await expect(overseasHttp.get('/platform/v1/last-leg/orders')).rejects.toThrow('当前环境未签发海外访问令牌')
|
||||
expect(refresh).toHaveBeenCalledTimes(1)
|
||||
expect(adapter).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,77 +0,0 @@
|
||||
import axios, { type InternalAxiosRequestConfig } from 'axios'
|
||||
import { parseApiJsonResponse } from '@chongchong/shared'
|
||||
import { refreshAuthenticatedSession } from './http'
|
||||
|
||||
interface CachedOverseasToken {
|
||||
accessToken: string
|
||||
refreshAfter: number
|
||||
}
|
||||
|
||||
type RetryableOverseasRequestConfig = InternalAxiosRequestConfig & { _overseasAuthRetry?: boolean }
|
||||
|
||||
const TOKEN_REFRESH_SAFETY_WINDOW_MS = 30_000
|
||||
|
||||
let cachedToken: CachedOverseasToken | null = null
|
||||
|
||||
export const overseasHttp = axios.create({
|
||||
baseURL: import.meta.env.VITE_OVERSEAS_API_BASE_URL || '/overseas-api',
|
||||
timeout: 15_000,
|
||||
transformResponse: [(data: unknown) => parseApiJsonResponse(data)],
|
||||
})
|
||||
|
||||
function currentOverseasAccessToken() {
|
||||
return cachedToken && cachedToken.refreshAfter > Date.now() ? cachedToken.accessToken : ''
|
||||
}
|
||||
|
||||
async function requireOverseasAccessToken() {
|
||||
const current = currentOverseasAccessToken()
|
||||
if (current) return current
|
||||
await refreshAuthenticatedSession()
|
||||
const refreshed = currentOverseasAccessToken()
|
||||
if (!refreshed) throw new Error('当前环境未签发海外访问令牌')
|
||||
return refreshed
|
||||
}
|
||||
|
||||
export function replaceOverseasAccessToken(accessToken: string | null, expiresIn: number) {
|
||||
if (!accessToken && expiresIn === 0) {
|
||||
cachedToken = null
|
||||
return
|
||||
}
|
||||
if (!accessToken || !Number.isFinite(expiresIn) || expiresIn <= 0) {
|
||||
throw new Error('海外访问令牌响应无效')
|
||||
}
|
||||
cachedToken = {
|
||||
accessToken,
|
||||
refreshAfter: Date.now() + Math.max(0, expiresIn * 1000 - TOKEN_REFRESH_SAFETY_WINDOW_MS),
|
||||
}
|
||||
}
|
||||
|
||||
export function clearOverseasAccessToken() {
|
||||
cachedToken = null
|
||||
}
|
||||
|
||||
overseasHttp.interceptors.request.use(async (config) => {
|
||||
const accessToken = await requireOverseasAccessToken()
|
||||
config.headers.Authorization = `Bearer ${accessToken}`
|
||||
return config
|
||||
})
|
||||
|
||||
overseasHttp.interceptors.response.use(
|
||||
(response) => response.data,
|
||||
async (error) => {
|
||||
const config = error.config as RetryableOverseasRequestConfig | undefined
|
||||
if (error.response?.status !== 401 || !config || config._overseasAuthRetry) {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
config._overseasAuthRetry = true
|
||||
clearOverseasAccessToken()
|
||||
try {
|
||||
await refreshAuthenticatedSession()
|
||||
const accessToken = await requireOverseasAccessToken()
|
||||
config.headers.Authorization = `Bearer ${accessToken}`
|
||||
return overseasHttp.request(config)
|
||||
} catch {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,49 @@
|
||||
import axios from 'axios'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { http, registerAuthRefreshHandler } from './http'
|
||||
|
||||
const originalAdapter = http.defaults.adapter
|
||||
const paths = ['/platform/v1/orders', '/platform/v1/last-leg/orders', '/platform/v1/overseas-warehouse-resources/pallets']
|
||||
|
||||
describe('unified business session', () => {
|
||||
beforeEach(() => localStorage.setItem('cc-admin-token', 'session-1'))
|
||||
afterEach(() => {
|
||||
http.defaults.adapter = originalAdapter
|
||||
localStorage.clear()
|
||||
registerAuthRefreshHandler(async () => { throw new Error('no active test session') })
|
||||
})
|
||||
|
||||
it('uses one base URL and the same token across order, lastleg and warehouse resources', async () => {
|
||||
const requests: unknown[] = []
|
||||
const refresh = vi.fn()
|
||||
registerAuthRefreshHandler(refresh)
|
||||
http.defaults.adapter = async (config) => {
|
||||
requests.push([config.baseURL, config.headers.Authorization])
|
||||
return { status: 200, statusText: 'OK', headers: {}, config, data: { code: '200', data: [] } }
|
||||
}
|
||||
await Promise.all(paths.map(path => http.get(path)))
|
||||
expect(requests).toEqual(paths.map(() => [http.defaults.baseURL, 'Bearer session-1']))
|
||||
expect(refresh).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('shares a single refresh across simultaneous 401s from both business domains', async () => {
|
||||
const refresh = vi.fn(async () => {
|
||||
await Promise.resolve()
|
||||
localStorage.setItem('cc-admin-token', 'session-2')
|
||||
return 'session-2'
|
||||
})
|
||||
registerAuthRefreshHandler(refresh)
|
||||
const successes: string[] = []
|
||||
http.defaults.adapter = async (config) => {
|
||||
if (config.headers.Authorization === 'Bearer session-1') {
|
||||
throw new axios.AxiosError('Unauthorized', 'ERR_BAD_REQUEST', config, undefined,
|
||||
{ status: 401, statusText: 'Unauthorized', headers: {}, config, data: {} })
|
||||
}
|
||||
successes.push(String(config.headers.Authorization))
|
||||
return { status: 200, statusText: 'OK', headers: {}, config, data: { code: '200', data: [] } }
|
||||
}
|
||||
await Promise.all(paths.map(path => http.get(path)))
|
||||
expect(refresh).toHaveBeenCalledTimes(1)
|
||||
expect(successes).toEqual(paths.map(() => 'Bearer session-2'))
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user