feat: 完善淘宝小程序拍照、报告与登录流程
接入本地实时识别和自动三连拍,调整报告裁切、商品跳转、分享标题及居中页眉;完善游客会话、后台渠道隔离、部署脚本与回归测试。
This commit is contained in:
@@ -13,6 +13,21 @@ test('gateway signature covers raw JSON, identity, extra query fields and algori
|
||||
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' };
|
||||
@@ -39,3 +54,105 @@ test('photo chunks are ordered, bounded, deduplicated, owner-bound, expiring and
|
||||
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');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user