import assert from 'node:assert/strict'; import test from 'node:test'; import { WECHAT_NOTIFY_SUCCESS_V2 } from '../wechat-pay.mjs'; import { attachPortalWebhookRoutes, } from './portal-webhook-routes.mjs'; function createResponse() { return { statusCode: 200, contentType: null, body: undefined, status(code) { this.statusCode = code; return this; }, type(value) { this.contentType = value; return this; }, json(body) { this.body = body; return this; }, send(body) { this.body = body; return this; }, }; } function createSetup({ mpEnabled = true, apiVersion = 'v3', payEnabled = true, } = {}) { const routes = []; const calls = []; const rawBody = () => {}; const textBody = () => {}; let rechargeService = { async handleWechatNotify(payload) { calls.push(['pay-notify', payload]); }, }; let wechatPayClient = { enabled: payEnabled, apiVersion, }; let wechatMpService = { enabled: true, verifyUrlChallenge(query) { calls.push(['mp-verify', query]); return { ok: true, body: query.echostr, }; }, async handleInboundMessage(body, query) { calls.push(['mp-message', body, query]); return { status: 202, contentType: 'text/xml', body: 'accepted', task: Promise.resolve(), }; }, }; const app = { get(path, ...handlers) { routes.push({ method: 'get', path, handlers, }); }, post(path, ...handlers) { routes.push({ method: 'post', path, handlers, }); }, }; const logger = { log(...args) { calls.push(['log', ...args]); }, warn(...args) { calls.push(['warn', ...args]); }, error(...args) { calls.push(['error', ...args]); }, }; attachPortalWebhookRoutes({ app, userAuthReady: Promise.resolve(), wechatNotifyBody: rawBody, wechatMpBody: textBody, wechatMpConfig: { enabled: mpEnabled, }, getRechargeService: () => rechargeService, getWechatPayClient: () => wechatPayClient, getWechatMpService: () => wechatMpService, logger, }); return { routes, calls, rawBody, textBody, setRechargeService(value) { rechargeService = value; }, setWechatPayClient(value) { wechatPayClient = value; }, setWechatMpService(value) { wechatMpService = value; }, route(method, path) { return routes.find( (route) => route.method === method && route.path === path, ); }, }; } async function invoke( setup, method, path, request = {}, ) { const route = setup.route(method, path); assert.ok( route, `${method.toUpperCase()} ${path}`, ); const response = createResponse(); await route.handlers.at(-1)( { body: '', headers: {}, query: {}, ...request, }, response, ); return response; } test('registers payment and enabled MP webhook inventory with body middleware', () => { const setup = createSetup(); assert.deepEqual( setup.routes.map(({ method, path }) => [ method, path, ]), [ ['post', '/webhooks/wechat-pay/notify'], ['get', '/webhooks/wechat-mp/messages'], ['post', '/webhooks/wechat-mp/messages'], ], ); assert.equal( setup.routes[0].handlers[0], setup.rawBody, ); assert.equal( setup.routes[2].handlers[0], setup.textBody, ); const disabled = createSetup({ mpEnabled: false, }); assert.deepEqual( disabled.routes.map(({ method, path }) => [ method, path, ]), [ ['post', '/webhooks/wechat-pay/notify'], ], ); }); test('preserves WeChat Pay v3 success, disabled, and failure responses', async () => { const setup = createSetup(); const success = await invoke( setup, 'post', '/webhooks/wechat-pay/notify', { body: Buffer.from('signed-body'), headers: { signature: 'sig' }, }, ); assert.deepEqual(success.body, { code: 'SUCCESS', message: '成功', }); assert.deepEqual(setup.calls[0], [ 'pay-notify', { headers: { signature: 'sig' }, body: 'signed-body', }, ]); setup.setWechatPayClient({ enabled: false, apiVersion: 'v3', }); const disabled = await invoke( setup, 'post', '/webhooks/wechat-pay/notify', ); assert.equal(disabled.statusCode, 503); assert.deepEqual(disabled.body, { code: 'FAIL', message: '支付未启用', }); setup.setWechatPayClient({ enabled: true, apiVersion: 'v3', }); setup.setRechargeService({ async handleWechatNotify() { throw new Error('bad signature'); }, }); const failed = await invoke( setup, 'post', '/webhooks/wechat-pay/notify', ); assert.equal(failed.statusCode, 500); assert.deepEqual(failed.body, { code: 'FAIL', message: 'bad signature', }); }); test('preserves WeChat Pay v2 XML responses', async () => { const setup = createSetup({ apiVersion: 'v2', }); const success = await invoke( setup, 'post', '/webhooks/wechat-pay/notify', ); assert.equal(success.contentType, 'text/xml'); assert.equal( success.body, WECHAT_NOTIFY_SUCCESS_V2, ); setup.setWechatPayClient({ enabled: false, apiVersion: 'v2', }); const disabled = await invoke( setup, 'post', '/webhooks/wechat-pay/notify', ); assert.equal(disabled.statusCode, 503); assert.match(disabled.body, /支付未启用/); setup.setWechatPayClient({ enabled: true, apiVersion: 'v2', }); setup.setRechargeService({ async handleWechatNotify() { throw 'unknown'; }, }); const failed = await invoke( setup, 'post', '/webhooks/wechat-pay/notify', ); assert.equal(failed.statusCode, 500); assert.match(failed.body, /处理失败/); }); test('preserves MP verification success, rejection, and disabled responses', async () => { const setup = createSetup(); const success = await invoke( setup, 'get', '/webhooks/wechat-mp/messages', { query: { echostr: 'challenge', timestamp: '1', nonce: '2', }, }, ); assert.equal(success.contentType, 'text/plain'); assert.equal(success.body, 'challenge'); setup.setWechatMpService({ enabled: true, verifyUrlChallenge() { return { ok: false, status: 401, body: 'rejected', }; }, }); const rejected = await invoke( setup, 'get', '/webhooks/wechat-mp/messages', ); assert.equal(rejected.statusCode, 401); assert.equal(rejected.body, 'rejected'); setup.setWechatMpService(null); const disabled = await invoke( setup, 'get', '/webhooks/wechat-mp/messages', ); assert.equal(disabled.statusCode, 503); assert.equal( disabled.body, 'wechat mp disabled', ); }); test('preserves MP message handling metadata and response', async () => { const setup = createSetup(); const xml = '' + '' + ''; const response = await invoke( setup, 'post', '/webhooks/wechat-mp/messages', { body: xml, query: { signature: 'sig' }, }, ); assert.equal(response.statusCode, 202); assert.equal(response.contentType, 'text/xml'); assert.equal( response.body, 'accepted', ); assert.ok( setup.calls.some( (call) => call[0] === 'mp-message' && call[1] === xml, ), ); assert.ok( setup.calls.some( (call) => call[0] === 'log' && call[1] === 'WeChat MP message received:' && call[2].fromUser === 'openid-1...', ), ); }); test('preserves MP message disabled and failure responses', async () => { const setup = createSetup(); setup.setWechatMpService(null); const disabled = await invoke( setup, 'post', '/webhooks/wechat-mp/messages', ); assert.equal(disabled.statusCode, 503); assert.equal( disabled.body, 'wechat mp disabled', ); setup.setWechatMpService({ enabled: true, async handleInboundMessage() { throw new Error('broken handler'); }, }); const failed = await invoke( setup, 'post', '/webhooks/wechat-mp/messages', ); assert.equal(failed.statusCode, 500); assert.equal(failed.body, 'internal error'); });