test: harden overseas pda token session
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
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)
|
||||
})
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import {
|
||||
PDA_AUTHORITY, PDA_ROLE, replaceTokens, restoreSession, session,
|
||||
decodeAccessToken, signInWithTokens, signOut, validatePdaTokenPair
|
||||
} from '../src/stores/session.js'
|
||||
|
||||
const future = () => Math.floor(Date.now() / 1000) + 3600
|
||||
const jwt = (claims) => [
|
||||
Buffer.from(JSON.stringify({ alg: 'none', typ: 'JWT' })).toString('base64url'),
|
||||
Buffer.from(JSON.stringify(claims)).toString('base64url'), 'signature'
|
||||
].join('.')
|
||||
const rawJwt = (json) => [
|
||||
Buffer.from(JSON.stringify({ alg: 'none', typ: 'JWT' })).toString('base64url'),
|
||||
Buffer.from(json).toString('base64url'), 'signature'
|
||||
].join('.')
|
||||
|
||||
function tokenPair(overrides = {}) {
|
||||
const base = {
|
||||
user_id: 42, membership_id: 84, tenant_id: 0, app_code: 'PLATFORM',
|
||||
overseas_warehouse_id: 900, exp: future()
|
||||
}
|
||||
const domestic = { ...base, username: 'picker-42', ...(overrides.domestic || {}) }
|
||||
const overseas = {
|
||||
...base, roles: [PDA_ROLE], authorities: [PDA_AUTHORITY],
|
||||
...(overrides.overseas || {})
|
||||
}
|
||||
return {
|
||||
accessToken: jwt(domestic), overseasAccessToken: jwt(overseas),
|
||||
refreshToken: overrides.refreshToken ?? 'refresh-42'
|
||||
}
|
||||
}
|
||||
|
||||
function storage() {
|
||||
const values = new Map()
|
||||
globalThis.uni = {
|
||||
setStorageSync: (key, value) => values.set(key, JSON.parse(JSON.stringify(value))),
|
||||
getStorageSync: (key) => values.get(key),
|
||||
removeStorageSync: (key) => values.delete(key)
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
test('valid platform warehouse picker tokens freeze the same operator and warehouse', () => {
|
||||
storage(); signOut()
|
||||
const tokens = tokenPair()
|
||||
const pair = validatePdaTokenPair(tokens.accessToken, tokens.overseasAccessToken)
|
||||
assert.equal(pair.warehouseId, '900')
|
||||
const user = signInWithTokens(tokens)
|
||||
assert.deepEqual(user.roles, [PDA_ROLE])
|
||||
assert.equal(user.warehouseId, '900')
|
||||
assert.equal(user.warehouse, '海外仓 #900')
|
||||
assert.equal(session.tenantId, '0')
|
||||
assert.equal(session.token, tokens.overseasAccessToken)
|
||||
})
|
||||
|
||||
test('JWT decoding preserves unquoted snowflake subjects and warehouse IDs exactly', () => {
|
||||
const decoded = decodeAccessToken(rawJwt('{"user_id":2091919753535229955,"membership_id":2091919753535229956,"tenant_id":0,"overseas_warehouse_id":2091919753155878914}'))
|
||||
assert.equal(decoded.user_id, '2091919753535229955')
|
||||
assert.equal(decoded.membership_id, '2091919753535229956')
|
||||
assert.equal(decoded.overseas_warehouse_id, '2091919753155878914')
|
||||
})
|
||||
|
||||
test('login fails closed for wrong role, authority, warehouse, application, or token subject', () => {
|
||||
storage(); signOut()
|
||||
const invalidPairs = [
|
||||
tokenPair({ overseas: { roles: ['PLATFORM_ADMIN'] } }),
|
||||
tokenPair({ overseas: { authorities: [] } }),
|
||||
tokenPair({ overseas: { overseas_warehouse_id: null } }),
|
||||
tokenPair({ domestic: { app_code: 'MERCHANT', tenant_id: 100 } }),
|
||||
tokenPair({ overseas: { user_id: 99 } }),
|
||||
tokenPair({ overseas: { membership_id: 99 } }),
|
||||
tokenPair({ overseas: { overseas_warehouse_id: 901 } })
|
||||
]
|
||||
for (const tokens of invalidPairs) {
|
||||
assert.throws(() => signInWithTokens(tokens))
|
||||
assert.equal(session.currentUser, null)
|
||||
}
|
||||
})
|
||||
|
||||
test('refresh replacement validates both tokens before atomically changing session', () => {
|
||||
storage(); signOut()
|
||||
const original = tokenPair()
|
||||
signInWithTokens(original)
|
||||
const before = { domestic: session.domesticToken, overseas: session.token, refresh: session.refreshToken }
|
||||
const invalid = tokenPair({ overseas: { authorities: [] }, refreshToken: 'invalid-refresh' })
|
||||
assert.throws(() => replaceTokens(invalid), /lastleg:pda:operate/)
|
||||
assert.deepEqual({ domestic: session.domesticToken, overseas: session.token, refresh: session.refreshToken }, before)
|
||||
|
||||
const replacement = tokenPair({ refreshToken: 'refresh-43' })
|
||||
replaceTokens(replacement)
|
||||
assert.equal(session.domesticToken, replacement.accessToken)
|
||||
assert.equal(session.token, replacement.overseasAccessToken)
|
||||
assert.equal(session.refreshToken, 'refresh-43')
|
||||
})
|
||||
|
||||
test('restore rebuilds identity from claims and removes expired or tampered storage', () => {
|
||||
const values = storage(); signOut()
|
||||
signInWithTokens(tokenPair())
|
||||
const saved = structuredClone([...values.values()][0])
|
||||
signOut(); values.set('overseas-pda-auth-session', saved)
|
||||
assert.equal(restoreSession(), true)
|
||||
assert.equal(session.currentUser.warehouseId, '900')
|
||||
|
||||
const tampered = { ...saved, token: tokenPair({ overseas: { overseas_warehouse_id: 901 } }).overseasAccessToken }
|
||||
signOut(); values.set('overseas-pda-auth-session', tampered)
|
||||
assert.equal(restoreSession(), false)
|
||||
assert.equal(session.currentUser, null)
|
||||
assert.equal(values.has('overseas-pda-auth-session'), false)
|
||||
|
||||
const expired = tokenPair({ domestic: { exp: 1 }, overseas: { exp: 1 } })
|
||||
values.set('overseas-pda-auth-session', { ...saved, domesticToken: expired.accessToken, token: expired.overseasAccessToken })
|
||||
assert.equal(restoreSession(), false)
|
||||
assert.equal(values.has('overseas-pda-auth-session'), false)
|
||||
})
|
||||
Reference in New Issue
Block a user