import assert from 'node:assert/strict' import test from 'node:test' import { verifyPdaWarehouseRevocation, warehouseClaim } from './unified-pda-session-revocation.mjs' import { pickerSessionHandoff } from './phase4-overseas-pda.mjs' const token = warehouse => 'header.' + Buffer.from(JSON.stringify({ overseas_warehouse_id: warehouse })).toString('base64url') + '.signature' test('signed binding identity retains 64-bit decimal precision for numeric and string claims', () => { const make = raw => 'header.' + Buffer.from(raw).toString('base64url') + '.signature' assert.equal(warehouseClaim(make('{"overseas_warehouse_id":193000000000200004}')), '193000000000200004') assert.equal(warehouseClaim(token('193000000000200004')), '193000000000200004') assert.equal(warehouseClaim(make('{"overseas_warehouse_id":null}')), '') assert.equal(warehouseClaim(make('{"overseas_warehouse_id":22,"overseas_warehouse_id":23}')), '') }) function fixture(wrongWarehouse = false) { const checks = [], calls = [] let cleared = false let isolatedRole const picker = { membershipId: '7', username: 'offline-user', password: 'offline-password', token: 'old-access', refreshToken: 'old-refresh', userUpdate: { displayName: 'offline', phone: '13900000001', email: 'offline@example.invalid', tenantId: '0', roleIds: ['9'], overseasWarehouseId: '11', domesticWarehouseId: null } } const response = (data, code = '200', http = 200) => ({ http, body: { code, data } }) const context = { assert: (_m, name, _r, passed, evidence) => checks.push({ name, passed, evidence }), apiSuccess: (_m, _n, r) => r.body.code === '200' ? r.body.data : null, request: async (method, path, body, bearer) => { calls.push({ method, path, body, bearer }) if (path.includes('receiving-options')) return response({ records: [{ id: '11' }, { id: '22' }] }) if (path === '/api/platform/v1/iam/roles' && method === 'POST') { isolatedRole = { ...body, id: '99', status: 'ACTIVE', permissionIds: [] } return response(isolatedRole) } if (path === '/api/platform/v1/iam/roles') return response([isolatedRole]) if (path === '/api/platform/v1/iam/roles/99/status') return response({ status: 'DISABLED' }) if (method === 'PUT') { assert.equal(body.phone, picker.userUpdate.phone) assert.equal(body.email, picker.userUpdate.email) assert.ok(body.roleIds.length > 0, 'IAM user edits require a role') cleared = body.roleIds.includes('99') return response({ ...body }) } if (path === '/api/auth/login') return response({ accessToken: token(cleared ? null : wrongWarehouse ? '11' : '22'), refreshToken: 'new-refresh' }) if (path === '/api/auth/refresh') { assert.equal(bearer, '') return response(null, 'A0401') } if (bearer === 'old-access' || (cleared && bearer === token('22'))) return response(null, 'A0401', 401) return cleared ? response(null, 'A0403', 403) : response(null, 'A0404') } } return { context, picker: pickerSessionHandoff(picker), checks, calls } } test('binding changes revoke both tokens and role/binding clearing prevents fresh PDA access', async () => { const { context, picker, checks, calls } = fixture() await verifyPdaWarehouseRevocation(context, picker) for (const name of ['PDA仓绑定变更旧Access失效', 'PDA仓绑定变更旧Refresh不能恢复会话', '新PDA会话只携带服务器签发的新仓绑定', 'PDA角色与仓绑定清空旧Access失效', 'PDA角色与仓绑定清空旧Refresh不能恢复会话', '无作业仓绑定的新会话不得进入PDA']) { assert.equal(checks.find(row => row.name === name)?.passed, true, name) } assert.ok(checks.every(row => row.passed)) assert.ok(checks.every(row => !/offline-password|old-refresh|new-refresh/.test(row.evidence || ''))) assert.equal(calls.filter(row => row.method === 'PUT' && row.path.includes('/iam/users/')).length, 2) assert.ok(calls.some(row => row.path === '/api/platform/v1/iam/roles/99/status')) assert.ok(calls.some(row => row.path === '/api/auth/refresh' && row.body.refreshToken === 'old-refresh')) }) test('fresh login retaining the original warehouse blocks instead of accepting token presence', async () => { const { context, picker, checks } = fixture(true) await assert.rejects(verifyPdaWarehouseRevocation(context, picker), /绑定漂移/) assert.ok(checks.some(row => !row.passed)) })