123 lines
9.0 KiB
JavaScript
123 lines
9.0 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 guest = await request('/api/auth/browser', { consent: true });
|
|
assert.equal(guest.data.token, undefined, 'guest credentials are never persisted by clients');
|
|
assert(!guest.headers.get('set-cookie').includes('Max-Age='), 'guest cookie is session-only');
|
|
assert.equal(fs.existsSync(path.join(root, 'identities.json')), false, 'guest login does not write an identity');
|
|
assert.throws(() => accounts.storeReport(guest.data.user.id, { id: 'Rguest' }));
|
|
assert.equal((await request('/api/reports', null, guest.cookie)).data.total, 0);
|
|
accounts = create(root, {});
|
|
assert.equal((await request('/api/auth/session', null, guest.cookie)).data.user, null, 'process restart forgets guests');
|
|
const [a, b] = require('./wechat-fixture.cjs')(root, 2).map(entry => ({ ...entry, data: { user: entry.user }, headers: guest.headers }));
|
|
accounts = create(root, {});
|
|
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);
|
|
assert.equal((await request('/api/auth/session', null, a.cookie)).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', {}, a.cookie);
|
|
assert.equal((await request('/api/reports', null, a.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; });
|