import test from 'node:test' import assert from 'node:assert/strict' import { overseasRequest, overseasUpload, resetOverseasRequestStateForTest } from '../src/api/overseas.js' import { PDA_AUTHORITY, PDA_ROLE, session, signInWithTokens, signOut } from '../src/stores/session.js' const future = () => Math.floor(Date.now() / 1000) + 3600 const jwt = (claims) => [ Buffer.from(JSON.stringify({ alg: 'none' })).toString('base64url'), Buffer.from(JSON.stringify(claims)).toString('base64url'), 'signature' ].join('.') function tokens(version = 'old') { const base = { user_id: 42, membership_id: 84, tenant_id: 0, app_code: 'PLATFORM', overseas_warehouse_id: 900, exp: future(), version } return { accessToken: jwt({ ...base, username: 'picker-42' }), overseasAccessToken: jwt({ ...base, roles: [PDA_ROLE], authorities: [PDA_AUTHORITY] }), refreshToken: `refresh-${version}` } } function prepareSession() { globalThis.uni = { setStorageSync() {}, getStorageSync() {}, removeStorageSync() {} } resetOverseasRequestStateForTest(); signOut() const value = tokens('old') signInWithTokens(value) return value } const ok = (data, headers = {}) => ({ statusCode: 200, data: { code: '200', data }, header: headers }) const denied = (requestId = 'request-401', traceId = 'trace-401') => ({ statusCode: 401, data: { code: 'A0401', message: 'expired', requestId, traceId }, header: {} }) test('concurrent old-token 401 responses share one refresh and each replay once with the atomically replaced token', async () => { const old = prepareSession(); const next = tokens('new') let refreshCount = 0; let oldCalls = 0; let newCalls = 0 globalThis.uni.request = (options) => { if (options.url.endsWith('/auth/refresh')) { refreshCount += 1 return setTimeout(() => options.success(ok(next)), 10) } if (options.header.Authorization === `Bearer ${old.overseasAccessToken}`) { oldCalls += 1 return setTimeout(() => options.success(denied()), oldCalls === 1 ? 0 : 30) } assert.equal(options.header.Authorization, `Bearer ${next.overseasAccessToken}`) newCalls += 1 setTimeout(() => options.success(ok({ call: newCalls })), 0) } const results = await Promise.all([ overseasRequest('GET', '/pda/one'), overseasRequest('GET', '/pda/two') ]) assert.equal(results.length, 2) assert.equal(refreshCount, 1) assert.equal(oldCalls, 2) assert.equal(newCalls, 2) assert.equal(session.token, next.overseasAccessToken) assert.equal(session.domesticToken, next.accessToken) }) test('request retries only once and preserves the final request and trace identifiers', async () => { prepareSession(); const next = tokens('retry') let businessCalls = 0; let refreshCount = 0 globalThis.uni.request = (options) => { if (options.url.endsWith('/auth/refresh')) { refreshCount += 1 return options.success(ok(next)) } businessCalls += 1 options.success(denied(`request-${businessCalls}`, `trace-${businessCalls}`)) } await assert.rejects(overseasRequest('POST', '/pda/write', { value: 1 }), (error) => { assert.equal(error.code, 'A0401') assert.equal(error.requestId, 'request-2') assert.equal(error.traceId, 'trace-2') return true }) assert.equal(refreshCount, 1) assert.equal(businessCalls, 2) }) test('refresh rejection clears every local token and returns the auth trace', async () => { prepareSession() globalThis.uni.request = (options) => { if (options.url.endsWith('/auth/refresh')) { return options.success({ statusCode: 200, data: { code: 'A0401', message: 'refresh expired', requestId: 'auth-request', traceId: 'auth-trace' }, header: {} }) } options.success(denied()) } await assert.rejects(overseasRequest('GET', '/pda/current'), (error) => { assert.equal(error.requestId, 'auth-request') assert.equal(error.traceId, 'auth-trace') return true }) assert.equal(session.currentUser, null) assert.equal(session.token, '') assert.equal(session.domesticToken, '') assert.equal(session.refreshToken, '') }) test('photo upload refreshes and replays once while retaining the file payload', async () => { const old = prepareSession(); const next = tokens('upload') let refreshCount = 0; const uploads = [] globalThis.uni.request = (options) => { refreshCount += 1 assert.ok(options.url.endsWith('/auth/refresh')) options.success(ok(next)) } globalThis.uni.uploadFile = (options) => { uploads.push({ filePath: options.filePath, name: options.name, authorization: options.header.Authorization }) if (options.header.Authorization === `Bearer ${old.overseasAccessToken}`) { return options.success({ statusCode: 401, data: JSON.stringify({ code: 'A0401', requestId: 'upload-old', traceId: 'upload-old-trace' }), header: {} }) } options.success({ statusCode: 200, data: JSON.stringify({ code: '200', data: { fileId: '99' } }), header: {} }) } assert.deepEqual(await overseasUpload('/pda/photo', '/tmp/photo.jpg'), { fileId: '99' }) assert.equal(refreshCount, 1) assert.equal(uploads.length, 2) assert.deepEqual(uploads.map(({ filePath, name }) => ({ filePath, name })), [ { filePath: '/tmp/photo.jpg', name: 'file' }, { filePath: '/tmp/photo.jpg', name: 'file' } ]) }) test('business and network errors retain diagnostics without refreshing or clearing a valid session', async () => { prepareSession(); let refreshCount = 0 globalThis.uni.request = (options) => { if (options.url.endsWith('/auth/refresh')) refreshCount += 1 options.success({ statusCode: 200, data: { code: 'A0409', message: 'locked', requestId: 'business-request', traceId: 'business-trace' }, header: {} }) } await assert.rejects(overseasRequest('POST', '/pda/locked'), (error) => { assert.equal(error.code, 'A0409') assert.equal(error.requestId, 'business-request') assert.equal(error.traceId, 'business-trace') return true }) assert.equal(refreshCount, 0) assert.ok(session.currentUser) globalThis.uni.request = (options) => options.fail({ errMsg: 'socket disconnected' }) await assert.rejects(overseasRequest('GET', '/pda/network'), (error) => error.network === true && /socket/.test(error.message)) assert.ok(session.currentUser) })