93 lines
5.7 KiB
JavaScript
93 lines
5.7 KiB
JavaScript
// Isolated HTTP + browser integration. Upstream Face++ is fixture-backed; no paid call.
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import vm from 'node:vm';
|
|
import http from 'node:http';
|
|
import { spawn, execSync } from 'node:child_process';
|
|
import { once } from 'node:events';
|
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
const { chromium } = await import(pathToFileURL(execSync('npm root -g', { encoding: 'utf8' }).trim() + '/playwright/index.mjs').href);
|
|
const h5 = fileURLToPath(new URL('../', import.meta.url));
|
|
const dataRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'juzi-report-test-'));
|
|
const fx = {};
|
|
vm.runInNewContext(fs.readFileSync(path.join(h5, 'assets/mock/report-fixture.js'), 'utf8'), { window: fx });
|
|
const fixture = fx.MOCK_FIXTURE;
|
|
const child = spawn(process.execPath, ['-e', `
|
|
const fs=require('fs'), vm=require('vm'), w={};
|
|
vm.runInNewContext(fs.readFileSync('assets/mock/report-fixture.js','utf8'),{window:w});
|
|
const f=w.MOCK_FIXTURE;
|
|
global.fetch=async()=>({ok:true,status:200,json:async()=>({result:f.result,face_rectangle:f.face_rectangle,left_side_result:f.side.left,right_side_result:f.side.right})});
|
|
require('./server.js');
|
|
`], { cwd: h5, env: { ...process.env, PORT: '18089', HOST: '127.0.0.1', JUZI_DATA_ROOT: dataRoot,
|
|
SKIN_API_KEY: 'fixture-key', SKIN_API_SECRET: 'fixture-secret', PUBLIC_BASE_URL: '', WECHAT_APP_ID: '', WECHAT_APP_SECRET: '' }, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
let browser;
|
|
try {
|
|
await new Promise((resolve, reject) => {
|
|
const timeout = setTimeout(() => reject(new Error('test server startup timeout')), 10000);
|
|
child.stdout.on('data', b => { if (b.toString().includes('已启动')) { clearTimeout(timeout); resolve(); } });
|
|
child.once('exit', code => { clearTimeout(timeout); reject(new Error('server exited: ' + code)); });
|
|
});
|
|
const base = 'http://127.0.0.1:18089/';
|
|
browser = await chromium.launch({ headless: true, args: ['--no-sandbox'] });
|
|
const page = await browser.newPage({ viewport: { width: 375, height: 726 } });
|
|
const errors = [];
|
|
page.on('pageerror', e => errors.push(e.message));
|
|
await page.goto(base);
|
|
await page.waitForFunction(() => !document.querySelector('#login-message').textContent.includes('检查'));
|
|
await page.check('#login-consent');
|
|
await page.click('[data-action="login-browser"]');
|
|
await page.waitForSelector('#s-splash.active');
|
|
const api = page.context().request;
|
|
const result = await (await api.post(base + 'api/skin-analyze', { data: { front_image: fixture.photos[0], left_image: fixture.photos[0], right_image: fixture.photos[0], profile: { real_age: 28, daily_skin_type: '干性肌肤' } } })).json();
|
|
assert.equal(result.ok, true);
|
|
assert.equal(result.report_saved, true);
|
|
assert(result.vm && result.presentation);
|
|
const snapshot = await (await api.get(base + 'api/reports/' + result.record_id)).json();
|
|
assert.equal(snapshot.report.photos.length, 3);
|
|
assert.deepEqual(snapshot.report.presentation, result.presentation);
|
|
assert.deepEqual(snapshot.report.payload.result.face_maps, result.result.face_maps);
|
|
assert.equal(snapshot.report.photos[0], fixture.photos[0]);
|
|
// A separate browser cannot access this user's report, even knowing its ID.
|
|
const other = await browser.newContext();
|
|
assert.equal((await other.request.get(base + 'api/reports/' + result.record_id)).status(), 401);
|
|
await other.request.post(base + 'api/auth/browser', { data: { consent: true } });
|
|
assert.equal((await other.request.get(base + 'api/reports/' + result.record_id)).status(), 404);
|
|
for (const route of ['data/accounts/identities.json', 'lib/accounts.js', 'server.js', 'test/accounts.js', '%2edata/identities.json']) {
|
|
assert([403,404].includes((await api.get(base + route)).status()), route);
|
|
}
|
|
// Raw HTTP avoids URL clients normalizing away the malicious path under test.
|
|
for (const route of ['//data/accounts/identities.json', '/%2fdata/accounts/identities.json',
|
|
'/%5cdata/accounts/identities.json', '//lib/accounts.js', '/%2fserver.js', '/assets/../data/accounts/identities.json']) {
|
|
const status = await new Promise((resolve, reject) => {
|
|
http.get({ hostname: '127.0.0.1', port: 18089, path: route }, r => { r.resume(); resolve(r.statusCode); }).on('error', reject);
|
|
});
|
|
assert([403,404].includes(status), route);
|
|
}
|
|
await page.click('[data-action="history"]');
|
|
await page.click('[data-report-id]');
|
|
await page.waitForSelector('#s-report.active');
|
|
await page.waitForFunction(() => [...document.querySelectorAll('.ditem-img')].every(i => !i.hidden && i.naturalWidth));
|
|
const imagesBefore = await page.locator('.ditem-img').evaluateAll(images => images.map(i => i.src));
|
|
const copyBefore = await page.locator('.ditem-para').allTextContents();
|
|
await page.reload();
|
|
await page.waitForSelector('#s-splash.active');
|
|
await page.click('[data-action="history"]');
|
|
await page.click('[data-report-id]');
|
|
await page.waitForFunction(() => [...document.querySelectorAll('.ditem-img')].every(i => !i.hidden && i.naturalWidth));
|
|
assert.deepEqual(await page.locator('.ditem-img').evaluateAll(images => images.map(i => i.src)), imagesBefore);
|
|
assert.deepEqual(await page.locator('.ditem-para').allTextContents(), copyBefore);
|
|
await page.goBack();
|
|
await page.waitForSelector('#s-history.active');
|
|
await page.goBack();
|
|
await page.waitForSelector('#s-splash.active');
|
|
assert.deepEqual(errors, []);
|
|
console.log('PASS: authenticated analyze → full snapshot → backend history → reload → seven identical images/copy; owner isolation, private files, browser back');
|
|
} finally {
|
|
if (browser) await browser.close();
|
|
child.kill('SIGTERM');
|
|
if (child.exitCode === null) await once(child, 'exit');
|
|
fs.rmSync(dataRoot, { recursive: true, force: true });
|
|
}
|