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