Files
juzi/h5/test/accounts.js
T

116 lines
8.3 KiB
JavaScript

'use strict';
const assert = require('node:assert/strict');
const http = require('node:http');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const create = require('../lib/accounts');
const SkinScore = require('../js/skin-score');
const ReportUI = require('../js/report-ui');
(async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'juzi-accounts-test-'));
let accounts = create(root, {});
const server = http.createServer(async (req, res) => {
if (!await accounts.route(req, res, req.url.split('?')[0])) { res.writeHead(404); res.end(); }
});
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
const base = 'http://127.0.0.1:' + server.address().port;
const request = async (url, data, cookie, extra = {}) => {
const r = await fetch(base + url, { redirect: 'manual', method: data ? 'POST' : 'GET',
headers: { ...(cookie ? { cookie } : {}), ...extra }, body: data ? JSON.stringify(data) : undefined });
const text = await r.text();
return { status: r.status, data: text ? JSON.parse(text) : null, cookie: r.headers.getSetCookie().map(c => c.split(';')[0]).join('; '), location: r.headers.get('location'), headers: r.headers };
};
try {
assert.equal((await request('/api/reports')).status, 401);
assert.equal((await request('/api/auth/browser', {})).status, 400);
assert.equal((await request('/api/auth/browser', { consent: true }, '', { Origin: 'https://unrelated.example' })).status, 403);
const a = await request('/api/auth/browser', { consent: true });
const b = await request('/api/auth/browser', { consent: true });
assert.notEqual(a.data.user.id, b.data.user.id);
assert(a.headers.get('set-cookie').includes('HttpOnly'));
const vm = SkinScore.buildViewModel(SkinScore.MOCK_RESULT, SkinScore.MOCK_SIDE, { realAge: 30, skinType: '干性肌肤' });
const presentation = ReportUI.build(vm, SkinScore.MOCK_RESULT);
const snapshot = { id: 'Rtest0', created_at: new Date().toISOString(), vm, presentation,
photos: ['data:image/jpeg;base64,front', 'data:image/jpeg;base64,left', 'data:image/jpeg;base64,right'],
payload: { result: { ...SkinScore.MOCK_RESULT, face_maps: { brown_area: 'data:image/png;base64,saved-layer' } }, side: SkinScore.MOCK_SIDE } };
accounts.storeReport(a.data.user.id, snapshot);
assert.deepEqual((await request('/api/reports/Rtest0', null, a.cookie)).data.report, JSON.parse(JSON.stringify(snapshot)));
assert.deepEqual(accounts.reportPresentation(a.data.user.id, 'Rtest0'), JSON.parse(JSON.stringify(presentation)));
assert.equal(accounts.reportPresentation(b.data.user.id, 'Rtest0'), null);
assert.equal(accounts.reportPresentation(a.data.user.id, '../identities'), null);
assert.equal(accounts.reportPresentation('../reports', 'Rtest0'), null);
assert.equal((await request('/api/reports/Rtest0', null, b.cookie)).status, 404);
assert.equal((await request('/api/reports', null, b.cookie)).data.total, 0);
for (let i = 1; i <= 5; i++) accounts.storeReport(a.data.user.id, { ...snapshot, id: 'Rtest' + i });
assert.equal((await request('/api/reports', null, a.cookie)).data.total, 5);
assert.equal((await request('/api/reports/Rtest0', null, a.cookie)).status, 404);
accounts = create(root, {}); // Process restart reloads sessions and reports.
assert.equal((await request('/api/reports', null, a.cookie)).data.total, 5);
const restored = await request('/api/auth/browser', { consent: true, token: a.data.token });
assert.equal(restored.data.user.id, a.data.user.id);
assert.equal((await request('/api/auth/browser', { consent: true, token: 'bad' })).status, 401);
await request('/api/auth/logout', {}, restored.cookie);
assert.equal((await request('/api/reports', null, restored.cookie)).status, 401);
assert.equal((await request('/api/auth/config')).data.wechat, false);
accounts = create(root, { WECHAT_APP_ID: 'test-app', WECHAT_APP_SECRET: 'test-secret', PUBLIC_BASE_URL: 'https://skin.example.com/state' });
const start = await request('/api/auth/wechat', { consent: true });
const url = new URL(start.data.url);
assert.equal(url.searchParams.get('redirect_uri'), 'https://skin.example.com/state/api/auth/wechat/callback');
assert(start.headers.get('set-cookie').includes('Secure'));
assert(start.headers.get('set-cookie').includes('Path=/state/'));
const state = url.searchParams.get('state');
assert((await request('/api/auth/wechat/callback?code=test&state=' + state)).location.includes('login_error=state'));
// Mock only the upstream token exchange, not our HTTP routes, state or persistence.
const realFetch = global.fetch;
global.fetch = (url, options) => String(url).startsWith('https://api.weixin.qq.com/')
? Promise.resolve({ json: async () => ({ openid: 'same-wechat-user' }) }) : realFetch(url, options);
try {
async function wechat() {
const start = await request('/api/auth/wechat', { consent: true });
const state = new URL(start.data.url).searchParams.get('state');
const callback = await request('/api/auth/wechat/callback?code=test&state=' + state, null, start.cookie);
assert.equal(callback.location, '/state/?login=success');
assert((await request('/api/auth/wechat/callback?code=test&state=' + state, null, start.cookie)).location.includes('login_error=state'));
return (await request('/api/auth/session', null, callback.cookie)).data.user;
}
assert.deepEqual(await wechat(), await wechat());
for (const scenario of [
{ data: { errcode: 40029 }, error: 'wechat' },
{ data: null, error: 'wechat' },
{ data: { openid: {} }, error: 'wechat' },
{ data: { openid: 'must-not-login' }, status: 500, error: 'wechat' },
{ network: true, error: 'network' }
]) {
global.fetch = (url, options) => String(url).startsWith('https://api.weixin.qq.com/')
? scenario.network ? Promise.reject(new Error('fixture network failure'))
: Promise.resolve({ status: scenario.status || 200, json: async () => scenario.data })
: realFetch(url, options);
const started = await request('/api/auth/wechat', { consent: true });
const state = new URL(started.data.url).searchParams.get('state');
const failed = await request('/api/auth/wechat/callback?code=bad&state=' + state, null, started.cookie);
assert.equal(failed.location, '/state/?login_error=' + scenario.error);
assert(!failed.cookie.includes('juzi_session='));
assert.equal((await request('/api/auth/session', null, failed.cookie)).data.user, null);
assert((await request('/api/auth/wechat/callback?code=bad&state=' + state, null, started.cookie)).location.includes('login_error=state'));
}
} finally { global.fetch = realFetch; }
accounts = create(root, { TRUST_LOOPBACK_PROXY: '1' });
// Malformed credentials exercise the limiter without creating 60 users.
for (let i = 0; i < 60; i++) assert.equal((await request('/api/auth/browser', { consent: true, token: 'bad' }, '', { 'X-Real-IP': '203.0.113.1' })).status, 401);
assert.equal((await request('/api/auth/browser', { consent: true, token: 'bad' }, '', { 'X-Real-IP': '203.0.113.1' })).status, 429);
assert.equal((await request('/api/auth/browser', { consent: true, token: 'bad' }, '', { 'X-Real-IP': '203.0.113.2' })).status, 401);
accounts = create(root, {});
for (let i = 0; i < 60; i++) await request('/api/auth/browser', { consent: true, token: 'bad' }, '', { 'X-Real-IP': '203.0.113.1' });
assert.equal((await request('/api/auth/browser', { consent: true, token: 'bad' }, '', { 'X-Real-IP': '203.0.113.2' })).status, 429, 'untrusted forwarding headers cannot bypass rate limit');
assert.equal(presentation.views.length, 7);
assert(presentation.views.every(v => !/\{[a-z_]+\}/.test(v.text)));
assert(!presentation.views.find(v => v.key === 'wrinkles').text.includes('未获取 条'));
console.log('PASS: consent, CSRF, isolated owners, exact snapshots, latest 5, restart, restore, logout, OAuth state/replay/subpath/same identity, seven-view copy');
} finally {
await new Promise(resolve => server.close(resolve));
fs.rmSync(root, { recursive: true, force: true }); // Only this test-created temp directory.
}
})().catch(e => { console.error(e); process.exitCode = 1; });