Files
juzi/taobao/test/runtime.test.cjs
T
xsl 774376fa16 feat: 完善淘宝小程序拍照、报告与登录流程
接入本地实时识别和自动三连拍,调整报告裁切、商品跳转、分享标题及居中页眉;完善游客会话、后台渠道隔离、部署脚本与回归测试。
2026-11-04 15:07:51 +08:00

159 lines
11 KiB
JavaScript

'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');
});