Files
memind/server/portal-webhook-routes.test.mjs

413 lines
8.5 KiB
JavaScript

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: '<xml>accepted</xml>',
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 =
'<xml><FromUserName><![CDATA[openid-123456]]></FromUserName>' +
'<MsgType><![CDATA[text]]></MsgType>' +
'<Content><![CDATA[hello world]]></Content></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,
'<xml>accepted</xml>',
);
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');
});