'use strict'; const { test } = require('node:test'), assert = require('node:assert/strict'), crypto = require('node:crypto'); const auth = require('../../h5/lib/taobao-auth'); test('gateway signature covers raw JSON, identity, extra query fields and algorithm; duplicates fail closed', () => { const env = { TAOBAO_APP_KEY: 'app', TAOBAO_APP_SECRET: 'secret' }, body = '{"consent":true,"challenge":"nonce"}'; const params = { app_key: 'app', open_id: 'open-account', sign_method: 'md5', env: 'online' }; const expected = crypto.createHash('md5').update('secretapp_keyappenvonlineopen_idopen-accountsign_methodmd5' + body + 'secret').digest('hex').toUpperCase(); assert.equal(auth.signature(params, body, env.TAOBAO_APP_SECRET), expected); const url = '/api/taobao/session?' + new URLSearchParams({ ...params, sign: expected }); assert(auth.verify(url, body, env)); assert.equal(auth.verify(url, body + ' ', env), null); assert.equal(auth.verify(url.replace('open-account', 'forged-account'), body, env), null); assert.equal(auth.verify(url + '&open_id=open-account', body, env), null); assert.equal(auth.verify(url, body, {}), null); }); test('guest gateway verification permits missing open_id but still requires app identity and an intact signature', () => { const env = { TAOBAO_APP_KEY: 'fixture-app', TAOBAO_APP_SECRET: 'fixture-secret' }; const body = '{"consent":true,"challenge":"fixture-challenge"}'; const params = { app_key: env.TAOBAO_APP_KEY, env: 'test', request_id: 'gateway-request' }; const signed = values => '/api/taobao/guest-session?' + new URLSearchParams({ ...values, sign: auth.signature(values, body, env.TAOBAO_APP_SECRET) }); const url = signed(params); assert.equal(auth.verifyGateway(url, body, env).appKey, env.TAOBAO_APP_KEY); assert.equal(auth.verify(url, body, env), null, 'normal login still requires a signed open_id'); assert.equal(auth.verifyGateway('/api/taobao/guest-session', body, env), null); assert.equal(auth.verifyGateway(url, body + ' ', env), null); assert.equal(auth.verifyGateway(signed({ ...params, app_key: 'other-app' }), body, env), null); assert.equal(auth.verifyGateway(url + '&env=test', body, env), null); assert.equal(auth.verifyGateway(signed({ ...params, sign_method: 'none' }), body, env), null); }); test('photo chunks are ordered, bounded, deduplicated, owner-bound, expiring and consumed once', async () => { let now = 0; const req = { headers: {}, owner: 'alice' }, other = { headers: {}, owner: 'bob' }; const runtime = require('../../h5/lib/taobao-session')({ now: () => now, session: request => ({ id: request.owner, kind: 'taobao' }) }); const piece = { view: 'front', upload_id: 'test-upload-id', index: 0, total: 2, data: 'AAAA' }; assert.equal(runtime.upload(req, { ...piece, index: 1 }).status, 409); assert.equal(runtime.upload(req, piece).complete, false); assert.equal(runtime.upload(req, piece).complete, false, 'repeated identical chunk is idempotent'); assert.equal(runtime.upload(req, { ...piece, data: 'BBBB' }).status, 409); assert.equal(runtime.upload(req, { ...piece, data: 'A'.repeat(65537) }).status, 400); assert.equal(runtime.upload(req, { ...piece, index: 1 }).complete, true); assert.equal(runtime.takePhotos(other, { front: piece.upload_id }), null); for (const view of ['left', 'right']) assert.equal(runtime.upload(req, { ...piece, view, total: 1 }).status, 200); const refs = { front: piece.upload_id, left: piece.upload_id, right: piece.upload_id }; assert(runtime.takePhotos(req, refs)); assert.equal(runtime.takePhotos(req, refs), null); assert.equal(runtime.upload(req, piece).status, 409); let complete; const job = runtime.start(req, () => new Promise(resolve => { complete = resolve; }), true); await new Promise(resolve => setImmediate(resolve)); assert.equal(runtime.result(other, job), null); assert.equal(runtime.result(req, job).status, 'running'); complete({ ok: true }); await new Promise(resolve => setImmediate(resolve)); assert.equal(runtime.result(req, job).status, 'complete'); assert.equal(runtime.upload(req, piece).status, 200); now = 5 * 60000 + 1; assert.equal(runtime.takePhotos(req, refs), null); const challenge = runtime.challenge('local'); assert(runtime.consume(challenge)); assert(!runtime.consume(challenge)); }); test('analysis attempts are session scoped, deduplicated, content bound and never restarted after result expiry', async () => { let now = 0, charged = 0; const runtime = require('../../h5/lib/taobao-session')({ now: () => now, session: () => ({ id: 'same-account', kind: 'taobao' }) }); const alice = { headers: { authorization: 'Bearer fixture-session-a' } }, bob = { headers: { authorization: 'Bearer fixture-session-b' } }; const body = { photo_refs: { front: 'front-upload', left: 'left-upload', right: 'right-upload' }, profile: { real_age: 30, gender: '女' } }; const requestId = 'analysis-attempt-1'; for (const req of [alice, bob]) { for (const view of ['front', 'left', 'right']) assert.equal(runtime.upload(req, { view, upload_id: body.photo_refs[view], index: 0, total: 1, data: 'AAAA' }).status, 200); } assert(runtime.takePhotos(alice, body.photo_refs)); assert(runtime.takePhotos(bob, body.photo_refs), 'same account on two devices cannot consume each other\'s uploads'); const attempt = runtime.submission(alice, requestId, body); assert.equal(attempt.status, 200); const job = runtime.start(alice, async () => { charged++; return { ok: true }; }, true, attempt); assert.equal(runtime.result(bob, job), null, 'same-account Bearers cannot read each other\'s transient jobs'); assert.deepEqual(runtime.submission(alice, requestId, body), { status: 202, job }); const reordered = { profile: { gender: '女', real_age: 30 }, photo_refs: { right: 'right-upload', front: 'front-upload', left: 'left-upload' } }; assert.equal(runtime.submission(alice, requestId, reordered).job, job, 'JSON property order does not change the request identity'); assert.equal(runtime.submission(alice, requestId, { ...body, profile: { real_age: 31 } }).status, 409); assert.equal(runtime.submission(alice, '', body).status, 400); assert.equal(runtime.submission(alice, undefined, body).status, 200, 'older callers remain supported'); const second = runtime.submission(bob, requestId, body); assert.equal(second.status, 200, 'an id in another Bearer session never exposes a prior job'); const otherJob = runtime.start(bob, async () => { charged++; return { ok: true }; }, true, second); assert.notEqual(otherJob, job); await new Promise(resolve => setImmediate(resolve)); assert.equal(charged, 2); assert.equal(runtime.submission(alice, requestId, body).job, job, 'completed attempts still return the first job'); now = 5 * 60000 + 1; assert.equal(runtime.result(alice, job), null); assert.equal(runtime.submission(alice, requestId, body).status, 410, 'expired results retain a non-recharge tombstone'); assert.equal(charged, 2); runtime.revoke(bob); assert.equal(runtime.result(bob, otherJob), null); }); test('a full analysis queue preserves uploaded photos and the same attempt can retry exactly once after capacity returns', async () => { let charged = 0; const runtime = require('../../h5/lib/taobao-session')({ session: () => ({ id: 'account', kind: 'taobao' }) }); const client = { headers: { authorization: 'Bearer upload-session' } }; const busy = { headers: { authorization: 'Bearer busy-session' } }; for (let i = 0; i < 1000; i++) assert(runtime.start(busy, () => new Promise(() => {}))); const refs = {}; for (const view of ['front', 'left', 'right']) { refs[view] = view + '-capacity-upload'; assert.equal(runtime.upload(client, { view, upload_id: refs[view], index: 0, total: 1, data: 'AAAA' }).status, 200); } const body = { request_id: 'capacity-retry-attempt', photo_refs: refs, profile: { real_age: 30 } }; const run = async photos => { charged++; assert.equal(photos.front_image, 'AAAA'); return { ok: true }; }; assert.equal(runtime.beginAnalysis(client, body, run).status, 429); assert.equal(charged, 0); runtime.revoke(busy); const accepted = runtime.beginAnalysis(client, body, run); assert.equal(accepted.status, 202, 'retry succeeds with the original uploaded refs'); assert.deepEqual(runtime.beginAnalysis(client, body, run), accepted, 'duplicate delivery returns the reserved job'); await new Promise(resolve => setImmediate(resolve)); assert.equal(charged, 1); assert.equal(runtime.result(client, accepted.job).payload.ok, true); assert.deepEqual(runtime.beginAnalysis(client, body, run), accepted, 'completion also retains idempotency'); assert.equal(charged, 1); }); test('login challenges expire, are consumed once and are rate limited', () => { let now = 0; const runtime = require('../../h5/lib/taobao-session')({ now: () => now, session: () => null }); const expired = runtime.challenge('address-a'); now = 60001; assert.equal(runtime.consume(expired), false); const once = runtime.challenge('address-a'); assert.equal(runtime.consume(once), true); assert.equal(runtime.consume(once), false); for (let i = 0; i < 29; i++) assert(runtime.challenge('address-a')); assert.equal(runtime.challenge('address-a'), null); now += 60001; assert(runtime.challenge('address-a')); }); test('miniapp guest Bearers expire, revoke and disappear on reload without entering the account database', t => { const fs = require('node:fs'), path = require('node:path'); const tempRoot = path.resolve(__dirname, '../../temp'); const dir = fs.mkdtempSync(path.join(tempRoot, 'guest-account-test-')); t.after(() => { assert(dir.startsWith(tempRoot + path.sep)); fs.rmSync(dir, { recursive: true, force: true }); }); let now = Date.now(); t.mock.method(Date, 'now', () => now); const createAccounts = require('../../h5/lib/accounts'), accounts = createAccounts(dir, {}); const guest = accounts.loginTaobaoGuest(); const req = { headers: { authorization: 'Bearer ' + guest.token } }; assert.equal(accounts.taobaoSession(req).kind, 'taobao_guest'); assert.equal(fs.existsSync(path.join(dir, 'identities.json')), false); assert.throws(() => accounts.storeReport(guest.user.id, { id: 'Rguest', vm: {} }), /Invalid report owner/); assert.equal(createAccounts(dir, {}).taobaoSession(req), null, 'new account store never reloads guest credentials'); now += 8 * 3600000 + 1; assert.equal(accounts.taobaoSession(req), null); const saved = accounts.loginTaobao('fixture-provider-key'); const dbBeforeGuest = fs.readFileSync(path.join(dir, 'identities.json'), 'utf8'); const visit = accounts.loginTaobaoGuest(), visitReq = { headers: { authorization: 'Bearer ' + visit.token } }; accounts.taobaoLogout(visitReq); assert.equal(accounts.taobaoSession(visitReq), null); assert.equal(accounts.taobaoSession({ headers: { authorization: 'Bearer ' + saved.token } }).kind, 'taobao'); assert.equal(fs.readFileSync(path.join(dir, 'identities.json'), 'utf8'), dbBeforeGuest, 'guest creation and logout never rewrite permanent identities'); });