import assert from 'node:assert/strict'; import test from 'node:test'; import { attachPortalWechatAuthRoutes, } from './portal-wechat-auth-routes.mjs'; function createResponse() { return { statusCode: 200, headers: {}, body: undefined, redirectUrl: null, status(code) { this.statusCode = code; return this; }, set(name, value) { this.headers[name] = value; return this; }, json(body) { this.body = body; return this; }, redirect(code, url) { this.statusCode = code; this.redirectUrl = url; return this; }, send(body) { this.body = body; return this; }, }; } function createSetup(overrides = {}) { const routes = []; const calls = []; let userAuth = { async getMe(token) { calls.push(['get-me', token]); return { id: 'user-1' }; }, async getWechatBindingStatus(userId, appId) { calls.push([ 'binding-status', userId, appId, ]); return { bound: true, nickname: 'John', }; }, async getWechatPendingBind(token) { calls.push(['pending', token]); return { nickname: '微信用户', avatar_url: 'avatar.png', return_to: '/chat', }; }, async completeWechatRegister(options) { calls.push(['register', options]); return { ok: true, token: 'register-token', user: { id: 'user-2' }, isNewUser: true, returnTo: '/welcome', utmSource: 'wechat-menu', utmMedium: 'oauth', utmCampaign: 'summer', }; }, async completeWechatBindAccount(options) { calls.push(['bind', options]); return { ok: true, token: 'bind-token', user: { id: 'user-1' }, returnTo: '/bound', }; }, }; let wechatOAuthService = { enabled: true, publicConfig(req) { calls.push(['public-config', req]); return { enabled: true, scanEnabled: true, }; }, async startScanLogin(req) { calls.push(['scan-start', req]); return { state: 'scan-state', url: 'https://wechat.example/scan', }; }, async pollScanLogin(state) { calls.push(['scan-poll', state]); return { status: 'complete', token: 'scan-token', }; }, async buildAuthorizeRedirect(req, options) { calls.push([ 'authorize', req, options, ]); return 'https://wechat.example/authorize'; }, async handleCallback(options) { calls.push(['callback', options]); return { token: 'callback-token', user: { id: 'user-3' }, isNewUser: true, returnTo: '/after-wechat', utmMedium: 'oauth', }; }, }; let wechatMpService = { enabled: true, async createJsSdkSignature(url) { calls.push(['signature', url]); return { appId: 'mp-app', nonceStr: 'nonce', }; }, async getRouteStatusForUser(userId) { calls.push(['route-status', userId]); return { enabled: true, routeId: 'route-1', }; }, async recreateRouteForUser(userId) { calls.push(['route-reset', userId]); return { ok: true, routeId: 'route-2', }; }, }; const jsonBody = (_req, _res, next) => next(); const app = { get(path, ...handlers) { routes.push({ method: 'get', path, handlers, }); }, post(path, ...handlers) { routes.push({ method: 'post', path, handlers, }); }, }; const plazaSeo = { recordAttribution(payload, ip) { calls.push([ 'attribution', payload, ip, ]); return Promise.resolve(); }, }; const options = { app, jsonBody, userAuthReady: Promise.resolve(), getUserAuth: () => userAuth, getWechatOAuthService: () => wechatOAuthService, getWechatMpService: () => wechatMpService, wechatMpConfig: { enabled: true, publicBaseUrl: 'https://portal.example', }, userToken: () => 'request-token', setUserLoginCookies(_res, _req, token) { calls.push(['set-cookies', token]); }, isSecureRequest() { calls.push(['secure']); return true; }, getPlazaSeo: () => plazaSeo, plazaClientIp: () => '203.0.113.8', logger: { warn(...args) { calls.push(['warn', ...args]); }, error(...args) { calls.push(['error', ...args]); }, }, isWechatUserAgentFn(userAgent) { calls.push(['wechat-agent', userAgent]); return userAgent.includes('MicroMessenger'); }, loadWechatOAuthConfigFn: () => ({ appId: 'oauth-app', }), validateWechatShareSignatureUrlFn( url, validationOptions, ) { calls.push([ 'validate-signature-url', url, validationOptions, ]); return `validated:${url}`; }, ...overrides, }; attachPortalWechatAuthRoutes(options); return { routes, calls, jsonBody, setUserAuth(value) { userAuth = value; }, setWechatOAuthService(value) { wechatOAuthService = value; }, setWechatMpService(value) { wechatMpService = value; }, route(method, path) { return routes.find( (route) => route.method === method && route.path === path, ); }, }; } async function invoke( setup, method, path, req = {}, ) { const route = setup.route(method, path); assert.ok( route, `${method.toUpperCase()} ${path}`, ); const res = createResponse(); const headers = { host: 'portal.example', ...req.headers, }; await route.handlers.at(-1)( { body: {}, query: {}, params: {}, ip: '127.0.0.1', get(name) { return headers[String(name).toLowerCase()] ?? ''; }, ...req, }, res, ); return res; } test('registers the WeChat auth route inventory, order, and conditional Agent routes', () => { const setup = createSetup(); assert.deepEqual( setup.routes.map(({ method, path }) => [ method, path, ]), [ ['get', '/auth/wechat/config'], ['get', '/auth/wechat/js-sdk-signature'], [ 'get', '/auth/wechat/public-js-sdk-signature', ], ['get', '/auth/wechat/status'], ['get', '/auth/wechat/agent-route'], [ 'post', '/auth/wechat/agent-route/reset', ], ['get', '/auth/wechat/pending/:token'], ['post', '/auth/wechat/register'], ['post', '/auth/wechat/bind'], ['post', '/auth/wechat/scan/start'], ['get', '/auth/wechat/scan/poll'], ['get', '/auth/wechat/authorize'], ['get', '/auth/wechat/callback'], ], ); assert.equal( setup.route( 'post', '/auth/wechat/register', ).handlers[0], setup.jsonBody, ); assert.equal( setup.route( 'post', '/auth/wechat/bind', ).handlers[0], setup.jsonBody, ); const disabled = createSetup({ wechatMpConfig: { enabled: false, publicBaseUrl: '', }, }); assert.equal( disabled.route( 'get', '/auth/wechat/agent-route', ), undefined, ); assert.equal(disabled.routes.length, 11); }); test('preserves public WeChat configuration modes', async () => { const disabled = createSetup(); disabled.setWechatOAuthService(null); const disabledRes = await invoke( disabled, 'get', '/auth/wechat/config', { headers: { 'user-agent': 'MicroMessenger/8.0', }, }, ); assert.deepEqual(disabledRes.body, { enabled: false, inWechat: true, scanEnabled: false, }); const enabled = createSetup(); const enabledRes = await invoke( enabled, 'get', '/auth/wechat/config', ); assert.deepEqual(enabledRes.body, { enabled: true, scanEnabled: true, }); }); test('preserves authenticated and public JS-SDK signature flows', async () => { const setup = createSetup(); const privateRes = await invoke( setup, 'get', '/auth/wechat/js-sdk-signature', { query: { url: 'https://portal.example/page#section', }, headers: { 'x-forwarded-host': 'public.example', }, }, ); assert.deepEqual(privateRes.body, { appId: 'mp-app', nonceStr: 'nonce', }); assert.deepEqual( setup.calls.find( ([name]) => name === 'validate-signature-url', ), [ 'validate-signature-url', 'https://portal.example/page', { publicBaseUrl: 'https://portal.example', requestHost: 'public.example', }, ], ); const publicRes = await invoke( setup, 'get', '/auth/wechat/public-js-sdk-signature', { query: { url: 'https://portal.example/public', }, }, ); assert.equal(publicRes.statusCode, 200); setup.setUserAuth(null); const unavailable = await invoke( setup, 'get', '/auth/wechat/js-sdk-signature', { query: { url: 'https://portal.example' }, }, ); assert.equal(unavailable.statusCode, 503); }); test('preserves JS-SDK validation and signing errors', async () => { const missing = createSetup(); const missingRes = await invoke( missing, 'get', '/auth/wechat/public-js-sdk-signature', ); assert.equal(missingRes.statusCode, 400); const failing = createSetup({ validateWechatShareSignatureUrlFn() { throw new Error('invalid signature url'); }, }); const failingRes = await invoke( failing, 'get', '/auth/wechat/public-js-sdk-signature', { query: { url: 'https://invalid.example', }, }, ); assert.equal(failingRes.statusCode, 502); assert.deepEqual(failingRes.body, { message: 'invalid signature url', }); }); test('preserves binding status and公众号 Agent route operations', async () => { const setup = createSetup(); const status = await invoke( setup, 'get', '/auth/wechat/status', ); assert.deepEqual(status.body, { enabled: true, bound: true, nickname: 'John', }); const routeStatus = await invoke( setup, 'get', '/auth/wechat/agent-route', ); assert.deepEqual(routeStatus.body, { enabled: true, routeId: 'route-1', }); const reset = await invoke( setup, 'post', '/auth/wechat/agent-route/reset', ); assert.deepEqual(reset.body, { ok: true, routeId: 'route-2', }); assert.ok( setup.calls.some( (call) => call[0] === 'binding-status' && call[1] === 'user-1' && call[2] === 'oauth-app', ), ); }); test('preserves pending bind lookup and WeChat registration attribution', async () => { const setup = createSetup(); const pending = await invoke( setup, 'get', '/auth/wechat/pending/:token', { params: { token: 'pending-1' }, }, ); assert.deepEqual(pending.body, { nickname: '微信用户', avatarUrl: 'avatar.png', returnTo: '/chat', }); const registered = await invoke( setup, 'post', '/auth/wechat/register', { body: { pendingToken: 'pending-1' }, }, ); assert.deepEqual(registered.body, { authenticated: true, user: { id: 'user-2' }, returnTo: '/welcome', }); assert.ok( setup.calls.some( (call) => call[0] === 'set-cookies' && call[1] === 'register-token', ), ); assert.ok( setup.calls.some( (call) => call[0] === 'attribution' && call[1].utm_source === 'wechat-menu' && call[2] === '203.0.113.8', ), ); }); test('preserves WeChat account binding validation, retry limits, and success', async () => { const missing = createSetup(); const missingRes = await invoke( missing, 'post', '/auth/wechat/bind', { body: { pendingToken: 'pending-1', username: '', password: '', }, }, ); assert.equal(missingRes.statusCode, 400); const limited = createSetup(); limited.setUserAuth({ async completeWechatBindAccount() { return { ok: false, message: 'too many attempts', retryAfterMs: 2500, }; }, }); const limitedRes = await invoke( limited, 'post', '/auth/wechat/bind', { body: { pendingToken: 'pending-1', username: 'john', password: 'secret', }, }, ); assert.equal(limitedRes.statusCode, 429); assert.equal( limitedRes.headers['Retry-After'], '3', ); const success = createSetup(); const successRes = await invoke( success, 'post', '/auth/wechat/bind', { body: { pendingToken: 'pending-1', username: 'john', password: 'secret', }, }, ); assert.deepEqual(successRes.body, { authenticated: true, user: { id: 'user-1' }, bound: true, returnTo: '/bound', }); }); test('preserves scan start, polling, and completed-login cookies', async () => { const setup = createSetup(); const started = await invoke( setup, 'post', '/auth/wechat/scan/start', ); assert.deepEqual(started.body, { state: 'scan-state', url: 'https://wechat.example/scan', }); const polled = await invoke( setup, 'get', '/auth/wechat/scan/poll', { query: { state: 'scan-state' }, }, ); assert.deepEqual(polled.body, { status: 'complete', token: 'scan-token', }); assert.ok( setup.calls.some( (call) => call[0] === 'set-cookies' && call[1] === 'scan-token', ), ); setup.setWechatOAuthService(null); const unavailable = await invoke( setup, 'post', '/auth/wechat/scan/start', ); assert.equal(unavailable.statusCode, 503); }); test('preserves authorize login and authenticated binding intents', async () => { const setup = createSetup(); const login = await invoke( setup, 'get', '/auth/wechat/authorize', ); assert.equal(login.statusCode, 302); assert.equal( login.redirectUrl, 'https://wechat.example/authorize', ); const binding = await invoke( setup, 'get', '/auth/wechat/authorize', { query: { intent: ' BIND ' }, }, ); assert.equal(binding.statusCode, 302); assert.ok( setup.calls.some( (call) => call[0] === 'authorize' && call[2].bindUserId === 'user-1', ), ); setup.setUserAuth({ async getMe() { return null; }, }); const unauthorized = await invoke( setup, 'get', '/auth/wechat/authorize', { query: { intent: 'bind' }, }, ); assert.equal(unauthorized.statusCode, 401); }); test('preserves callback gates, attribution, cookies, scan HTML, and errors', async () => { const setup = createSetup(); const completed = await invoke( setup, 'get', '/auth/wechat/callback', { query: { code: 'oauth-code', state: 'oauth-state', }, ip: '198.51.100.4', }, ); assert.equal(completed.statusCode, 302); assert.equal( completed.redirectUrl, '/after-wechat', ); assert.ok( setup.calls.some( (call) => call[0] === 'set-cookies' && call[1] === 'callback-token', ), ); const gate = createSetup(); gate.setWechatOAuthService({ enabled: true, async handleCallback() { return { action: 'binding_gate', pendingToken: 'pending token', returnTo: '/chat?a=1', }; }, }); const gateRes = await invoke( gate, 'get', '/auth/wechat/callback', ); assert.equal( gateRes.redirectUrl, '/?wechat_pending=pending+token&return_to=%2Fchat%3Fa%3D1', ); const scan = createSetup(); scan.setWechatOAuthService({ enabled: true, async handleCallback() { return { authMode: 'scan', token: 'unused', }; }, }); const scanRes = await invoke( scan, 'get', '/auth/wechat/callback', ); assert.match(scanRes.body, /扫码登录成功/); assert.equal( scan.calls.some( ([name]) => name === 'set-cookies', ), false, ); const failing = createSetup(); failing.setWechatOAuthService({ enabled: true, async handleCallback() { throw new Error('callback failed'); }, }); const failingRes = await invoke( failing, 'get', '/auth/wechat/callback', ); assert.equal( failingRes.redirectUrl, '/?wechat_error=callback%20failed', ); failing.setWechatOAuthService(null); const unavailable = await invoke( failing, 'get', '/auth/wechat/callback', ); assert.equal( unavailable.redirectUrl, '/?wechat_error=unavailable', ); });