const assert = require('node:assert/strict') const fs = require('node:fs') const path = require('node:path') const { test } = require('node:test') const ts = require('typescript') const operation = { operation_id: '12345678-1234-1234-1234-123456789abc', device_id: 'test-device', action: 'bind', status: 'pending', traffic_status: 'pending' } const iccid = '898604E91123D1334901' function load() { const storage = new Map() let user = 7 let version = 'trial' const calls = [] const jumps = [] const timers = new Map() let request = async (url, options) => { calls.push({ url, options }); return { ...operation, status: 'completed' } } const Taro = { getStorageSync: k => storage.get(k), setStorageSync: (k, v) => storage.set(k, v), navigateTo: () => new Promise(() => {}), getAccountInfoSync: () => ({ miniProgram: { envVersion: version } }), navigateToMiniProgram: async options => { jumps.push(options) } } const source = fs.readFileSync(path.join(__dirname, '../src/services/traffic-binding.ts'), 'utf8') const js = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2019, esModuleInterop: true } }).outputText const module = { exports: {} } new Function('require', 'module', 'exports', 'setTimeout', 'clearTimeout', js)(id => { if (id === '@tarojs/taro') return Taro if (id === './session') return { getCurrentUserId: () => user } if (id === './api') return { request: (...args) => request(...args) } throw Error(id) }, module, module.exports, (fn, ms) => { timers.set(fn, ms); return fn }, fn => timers.delete(fn)) return { api: module.exports, storage, calls, jumps, timers, user: value => { user = value }, version: value => { version = value }, request: fn => { request = fn } } } function returned(data = {}, appId = 'wx56e1cfc52aab630f') { return { referrerInfo: { appId, extraData: { operationId: operation.operation_id, action: 'bind', requestId: `joint-${operation.operation_id}`, code: 'OK', success: true, iccid, ...data } } } } test('only matching source, operation, action and business request can confirm', () => { const { api } = load() api.rememberOperation(operation) assert.equal(api.captureTrafficReturn(returned({}, 'wrong')), false) for (const invalid of [{ operationId: 'old' }, { action: 'unbind' }, { requestId: 'wrong' }, { success: 'true' }, { iccid: 'short' }]) { assert.equal(api.captureTrafficReturn(returned(invalid)), false) } assert.equal(api.captureTrafficReturn(returned()), true) }) test('receipt survives network failure and account switch cannot consume it', async () => { const context = load() context.api.rememberOperation(operation) context.api.captureTrafficReturn(returned()) context.request(async () => { throw new Error('network') }) await assert.rejects(context.api.flushTrafficReceipt(), /network/) assert.ok(context.api.getPendingTraffic().receipt) context.user(8) assert.equal(context.api.getPendingTraffic(), null) assert.equal(context.api.captureTrafficReturn(returned()), false) context.user(7) context.request(async () => ({ ...operation, status: 'completed' })) await context.api.flushTrafficReceipt() assert.equal(context.api.getPendingTraffic().receipt, undefined) }) test('only confirmed association opens query and recharge, unbinding closes them', () => { const { api } = load() assert.equal(api.trafficLinked(null), false) assert.equal(api.trafficLinked({ bind: operation }), false) assert.equal(api.trafficLinked({ bind: { ...operation, status: 'completed', traffic_status: 'client_confirmed' }, unbind: null }), false) assert.equal(api.trafficLinked({ bind: { ...operation, status: 'completed', traffic_status: 'server_confirmed' }, unbind: null }), true) assert.equal(api.trafficLinked({ bind: { ...operation, status: 'completed' }, unbind: { status: 'pending' } }), false) }) test('jump uses trial for testing and release in production and never treats navigation as binding', async () => { const context = load() context.request(async () => ({ operation, target_app_id: 'wx56e1cfc52aab630f', handoff_token: 'secret', request_id: `joint-${operation.operation_id}`, source_binding_id: 'banban:test' })) assert.equal(await context.api.jumpTraffic(operation), null) assert.equal(context.jumps[0].envVersion, 'trial') assert.equal(context.api.getPendingTraffic().operation.status, 'pending') assert.ok(!JSON.stringify([...context.storage.values()]).includes('secret')) context.version('release') await context.api.jumpTraffic(operation) assert.equal(context.jumps[1].envVersion, 'release') assert.equal(context.timers.size, 0) }) test('a late response after account change cannot start a jump or overwrite state', async () => { const context = load() context.request(async () => { context.user(8); return { operation } }) await assert.rejects(context.api.jumpTraffic(operation), /账号已变化/) assert.equal(context.jumps.length, 0) assert.equal(context.api.getPendingTraffic(), null) }) test('rebind discards a stale receipt and starts a current operation once', async () => { const context = load() context.api.rememberOperation(operation) context.api.captureTrafficReturn(returned()) const urls = [] context.request(async url => { urls.push(url) if (url.endsWith('/result')) throw Object.assign(new Error('stale'), { code: 'STALE_OPERATION' }) return { ...operation, operation_id: 'new-instance' } }) const resumed = await context.api.resumeTrafficOperation(operation.device_id, 'bind') assert.equal(resumed.operation_id, 'new-instance') assert.equal(urls.length, 2) assert.equal(context.api.getPendingTraffic().receipt, undefined) }) test('stale cached operation recovers once; network failure keeps its receipt for retry', async () => { const context = load() context.api.rememberOperation(operation) context.request(async url => { if (url.endsWith(`${operation.operation_id}/check`)) throw Object.assign(new Error('stale'), { code: 'STALE_OPERATION' }) return { ...operation, operation_id: 'new-instance' } }) assert.equal((await context.api.resumeTrafficOperation(operation.device_id, 'bind')).operation_id, 'new-instance') context.api.rememberOperation(operation) context.api.captureTrafficReturn(returned()) context.request(async () => { throw new Error('network') }) await assert.rejects(context.api.resumeTrafficOperation(operation.device_id, 'bind'), /network/) assert.ok(context.api.getPendingTraffic().receipt) }) test('missing navigation callback cannot keep the management button locked forever', async () => { const context = load() const pending = context.api.openTrafficPage(operation.device_id, 'unbind') const [timer, duration] = [...context.timers][0] assert.equal(duration, 15000) timer() await assert.rejects(pending, /跳转等待已结束/) assert.equal(context.timers.size, 0) }) test('server-confirmed release proceeds locally without a redundant 4G jump', async () => { const context = load() const released = { ...operation, action: 'unbind', traffic_status: 'server_confirmed' } context.request(async () => ({ operation: released })) assert.deepEqual(await context.api.jumpTraffic(released), released) assert.equal(context.jumps.length, 0) }) test('a legacy client receipt alone cannot skip the 4G result check', async () => { const context = load() const pending = { ...operation, action: 'unbind', traffic_status: 'client_confirmed' } context.request(async () => ({ operation: pending, target_app_id: 'wx56e1cfc52aab630f', iccid, request_id: `joint-${operation.operation_id}`, source_binding_id: 'banban:test' })) assert.equal(await context.api.jumpTraffic(pending), null) assert.equal(context.jumps.length, 1) }) test('management refresh checks the original relation before showing traffic entrances', async () => { const context = load() const calls = [] context.request(async url => { calls.push(url) return url.endsWith('/check') ? { ...operation, status: 'pending', traffic_status: 'pending' } : { device_id: operation.device_id, bind: { ...operation, status: 'completed', traffic_status: 'server_confirmed' }, unbind: null } }) const overview = await context.api.getTrafficOverview(operation.device_id) assert.equal(calls.length, 2) assert.equal(context.api.trafficLinked(overview), false) })