// Real HTTPS cookies + /state/ reverse-proxy layout; only WeChat is simulated. import assert from 'node:assert/strict'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import https from 'node:https'; import { execFileSync, execSync } from 'node:child_process'; import { createRequire } from 'node:module'; import { pathToFileURL, fileURLToPath } from 'node:url'; const require = createRequire(import.meta.url); const createAccounts = require('../lib/accounts'); const { chromium } = await import(pathToFileURL(execSync('npm root -g', { encoding: 'utf8' }).trim() + '/playwright/index.mjs').href); const root = fs.mkdtempSync(path.join(os.tmpdir(), 'juzi-wechat-test-')); const h5 = fileURLToPath(new URL('../', import.meta.url)); let browser, server; const realFetch = global.fetch; try { execFileSync('openssl', ['req', '-x509', '-newkey', 'rsa:2048', '-nodes', '-keyout', path.join(root, 'key.pem'), '-out', path.join(root, 'cert.pem'), '-days', '1', '-subj', '/CN=skin.example.test'], { stdio: 'ignore' }); let accounts; server = https.createServer({ key: fs.readFileSync(path.join(root, 'key.pem')), cert: fs.readFileSync(path.join(root, 'cert.pem')) }, async (req, res) => { const url = new URL(req.url, 'https://skin.example.test'); if (!url.pathname.startsWith('/state/')) { res.writeHead(404); return res.end(); } req.url = req.url.slice('/state'.length); // nginx proxy_pass trailing slash const route = url.pathname.slice('/state'.length); if (await accounts.route(req, res, route)) return; const file = route === '/' ? '/index.html' : route; if (!/^\/(index\.html|(?:js|css|assets|vendor)\/[^.][^]*)$/.test(file) || file.includes('..')) { res.writeHead(404); return res.end(); } try { const types = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.png': 'image/png', '.jpg': 'image/jpeg', '.woff2': 'font/woff2' }; res.setHeader('Content-Type', types[path.extname(file)] || 'application/octet-stream'); res.end(fs.readFileSync(path.join(h5, file))); } catch (_) { res.writeHead(404); res.end(); } }); await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); const base = 'https://skin.example.test:' + server.address().port + '/state/'; accounts = createAccounts(path.join(root, 'accounts'), { WECHAT_APP_ID: 'test-app', WECHAT_APP_SECRET: 'test-secret', PUBLIC_BASE_URL: base }); global.fetch = async url => { assert.equal(new URL(url).origin, 'https://api.weixin.qq.com'); return { ok: true, json: async () => ({ openid: 'test-same-wechat-user' }) }; }; browser = await chromium.launch({ headless: true, args: ['--no-proxy-server', '--host-resolver-rules=MAP skin.example.test 127.0.0.1'] }); let firstId; for (let device = 0; device < 2; device++) { const context = await browser.newContext({ ignoreHTTPSErrors: true, userAgent: 'Mozilla/5.0 MicroMessenger/8.0', viewport: { width: 375, height: 726 } }); const page = await context.newPage(); const errors = []; page.on('pageerror', e => errors.push(e.message)); await page.route('https://open.weixin.qq.com/**', async route => { const auth = new URL(route.request().url()); assert.equal(auth.searchParams.get('scope'), 'snsapi_base'); const callback = new URL(auth.searchParams.get('redirect_uri')); assert.equal(callback.href, base + 'api/auth/wechat/callback'); callback.search = new URLSearchParams({ code: 'test-code', state: auth.searchParams.get('state') }); await route.fulfill({ status: 302, headers: { location: callback.href }, body: '' }); }); await page.goto(base); await page.waitForFunction(() => !document.querySelector('#login-message').textContent.includes('检查')); await page.check('#login-consent'); await page.click('[data-action="login-wechat"]'); // Redirects may preserve WeChat's fragment; it is not sent to the backend. await page.waitForURL(url => url.origin + url.pathname === base && url.searchParams.get('login') === 'success'); await page.waitForSelector('#s-splash.active'); const user = await page.evaluate(() => AccountClient.user()); assert.equal(user.kind, 'wechat'); if (firstId) assert.equal(user.id, firstId); else firstId = user.id; const cookie = (await context.cookies()).find(c => c.name === 'juzi_session'); assert(cookie.secure && cookie.httpOnly && cookie.path === '/state/' && cookie.sameSite === 'Lax'); await page.click('[data-action="history"]'); await page.waitForFunction(() => document.querySelector('#identity-note').textContent.includes('同一微信账号')); assert.deepEqual(errors, []); await context.close(); } console.log('PASS: HTTPS /state/ login → simulated WeChat → callback with bound Secure cookie → homepage/history; two browser contexts share WeChat identity'); } finally { global.fetch = realFetch; if (browser) await browser.close(); if (server) await new Promise(resolve => server.close(resolve)); fs.rmSync(root, { recursive: true, force: true }); }