413 lines
8.5 KiB
JavaScript
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');
|
|
});
|