136 lines
8.6 KiB
JavaScript
136 lines
8.6 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { registerHooks } from 'node:module';
|
|
import { createHmac } from 'node:crypto';
|
|
|
|
const state = { orders: [], user: null, reads: 0, writes: [], requests: [], reply: null };
|
|
const matches = (item, query) => item && Object.entries(query).every(([k, v]) => (v === null ? item[k] == null : item[k] === v));
|
|
globalThis.__routingCloud = {
|
|
database: () => ({
|
|
collection(name) {
|
|
return {
|
|
async add(data) { state.writes.push({ name, data }); if (name === 'order') state.orders.push(data); return { ok: true }; },
|
|
where(query) {
|
|
return {
|
|
async getOne() { state.reads++; return { data: name === 'order' ? state.orders.find(o => matches(o, query)) : state.user }; },
|
|
async update(data) {
|
|
state.writes.push({ name, data });
|
|
const target = name === 'order' ? state.orders.find(o => matches(o, query)) : state.user;
|
|
if (target) Object.assign(target, data);
|
|
return { ok: true };
|
|
},
|
|
};
|
|
},
|
|
};
|
|
}
|
|
}),
|
|
async fetch(request) { state.requests.push(request); if (state.reply instanceof Error) throw state.reply; return state.reply; },
|
|
};
|
|
globalThis.require = () => ({ initWithBatchMode: () => ({ track() { }, trackFirst() { }, close() { } }) });
|
|
registerHooks({
|
|
resolve(specifier, context, next) {
|
|
if (specifier === '@lafjs/cloud') return { url: 'data:text/javascript,export default globalThis.__routingCloud', shortCircuit: true };
|
|
if (specifier === '@/limitedTimeEvent') return { url: new URL('../functions/limitedTimeEvent.ts', import.meta.url).href, shortCircuit: true };
|
|
if (specifier === '@/passCheckUpgrade') return { url: new URL('../functions/passCheckUpgrade.ts', import.meta.url).href, shortCircuit: true };
|
|
if (specifier === '@/Utils') return { url: new URL('../functions/Utils.ts', import.meta.url).href, shortCircuit: true };
|
|
if (specifier === '@/jungleConfig') return { url: new URL('../functions/jungleConfig.ts', import.meta.url).href, shortCircuit: true };
|
|
return next(specifier, context);
|
|
}
|
|
});
|
|
const { default: callback } = await import('../functions/wx/payCallBack.ts');
|
|
const { default: androidOrder } = await import('../functions/wx/orderPaySig.ts');
|
|
const { default: iosOrder } = await import('../functions/wx/iosorderPaySig.ts');
|
|
const key = 'synthetic-payment-test-key';
|
|
function reset(env = 'production') {
|
|
process.env.PAYMENT_APP_ENV = env; process.env.WX_MIDAS_PAY_SIGN_KEY = key;
|
|
Object.assign(state, { orders: [], user: { _id: 'u', openid: 'o', session_key: 'session' }, reads: 0, writes: [], requests: [], reply: { status: 200, data: { ErrCode: 0 } } });
|
|
}
|
|
function order(id = 'wct_123', extra = {}) {
|
|
const result = { outTradeNo: id, openid: 'o', itemid: 'starter_pack', itemCount: 1, goodsPrice: 300, state: 0, paymentAppEnv: 'test', ...extra };
|
|
state.orders.push(result); return result;
|
|
}
|
|
function notification(id = 'wct_123', extra = {}) {
|
|
const Payload = JSON.stringify({ OutTradeNo: id, OpenId: 'o', Env: 0, GoodsInfo: { ProductId: 'starter_pack', Quantity: 1, OrigPrice: 300 }, ...extra }, null, 2);
|
|
const Event = 'minigame_game_pay_goods_deliver_notify';
|
|
return { body: { Event, MiniGame: { Payload, PayEventSig: createHmac('sha256', key).update(Event + '&' + Payload).digest('hex'), IsMock: false } }, headers: {}, socket: { remoteAddress: '127.0.0.1' } };
|
|
}
|
|
test('both platforms use server environment, record it and sign the same routed order id', async () => {
|
|
for (const create of [androidOrder, iosOrder]) for (const env of ['production', 'test']) {
|
|
reset(env);
|
|
const result = await create({ body: { uid: 'u', itemid: 'starter_pack', itemCount: 1, itemPrice: 300, isDebug: env !== 'test', paymentAppEnv: env === 'test' ? 'production' : 'test' } });
|
|
assert.equal(result.code, 1);
|
|
const data = JSON.parse(result.data.signData);
|
|
assert.ok(data.outTradeNo.startsWith(env === 'test' ? 'wct_' : 'wcx_'));
|
|
assert.ok(data.outTradeNo.length <= 32); assert.equal(data.env, 0);
|
|
assert.equal(state.orders[0].paymentAppEnv, env);
|
|
assert.equal(data.outTradeNo, state.orders[0].outTradeNo);
|
|
assert.equal(result.data.paySig, createHmac('sha256', key).update('requestMidasPaymentGameItem&' + result.data.signData).digest('hex'));
|
|
}
|
|
});
|
|
test('missing test key or invalid deployment environment rejects creation before database access', async () => {
|
|
for (const create of [androidOrder, iosOrder]) {
|
|
reset('test'); delete process.env.WX_MIDAS_PAY_SIGN_KEY;
|
|
assert.equal((await create({ body: {} })).code, 0); assert.equal(state.reads, 0);
|
|
reset('typo'); assert.equal((await create({ body: {} })).code, 0); assert.equal(state.writes.length, 0);
|
|
}
|
|
});
|
|
test('production forwards signed test notification unchanged without reading or writing production DB', async () => {
|
|
reset(); const ctx = notification(); order();
|
|
assert.equal((await callback(ctx)).ErrCode, 0);
|
|
assert.equal(state.reads, 0); assert.equal(state.writes.length, 0);
|
|
const request = state.requests[0];
|
|
assert.equal(request.url, 'https://sor779u2w8.sealoshzh.site/wx/payCallBack');
|
|
assert.equal(request.data, ctx.body); assert.equal(request.timeout, 3000); assert.equal(request.maxRedirects, 0);
|
|
});
|
|
test('tampered/missing signatures, wrong event, sandbox and mock notifications never forward or update', async () => {
|
|
for (const env of ['production', 'test']) for (const change of [
|
|
ctx => { ctx.body.MiniGame.PayEventSig = '0'.repeat(64); },
|
|
ctx => { delete ctx.body.MiniGame.PayEventSig; },
|
|
ctx => { ctx.body.MiniGame.Payload = ctx.body.MiniGame.Payload.replace('300', '301'); },
|
|
ctx => { ctx.body.Event = 'other_event'; },
|
|
ctx => { ctx.body.MiniGame.IsMock = true; },
|
|
ctx => { Object.assign(ctx, notification('wct_123', { Env: 1 })); },
|
|
]) {
|
|
reset(env); order(); const ctx = notification(); change(ctx);
|
|
assert.notEqual((await callback(ctx)).ErrCode, 0);
|
|
assert.equal(state.requests.length, 0); assert.equal(state.writes.length, 0); assert.equal(state.reads, 0);
|
|
}
|
|
});
|
|
test('missing verifier key fails closed in both deployments', async () => {
|
|
for (const env of ['production', 'test']) {
|
|
reset(env); delete process.env.WX_MIDAS_PAY_SIGN_KEY;
|
|
assert.notEqual((await callback(notification())).ErrCode, 0); assert.equal(state.requests.length, 0);
|
|
}
|
|
});
|
|
test('forward error, timeout, malformed response and redirect do not acknowledge payment', async () => {
|
|
for (const reply of [new Error('timeout'), { status: 500, data: { ErrCode: 0 } }, { status: 302, data: { ErrCode: 0 } }, { status: 200, data: { ErrCode: -1 } }, { status: 200, data: 'html' }, null]) {
|
|
reset(); state.reply = reply;
|
|
assert.notEqual((await callback(notification())).ErrCode, 0); assert.equal(state.writes.length, 0);
|
|
}
|
|
reset(); const ctx = notification(); ctx.headers['x-payment-forwarded'] = '1';
|
|
assert.notEqual((await callback(ctx)).ErrCode, 0); assert.equal(state.requests.length, 0);
|
|
});
|
|
test('test deployment confirms only matching local test order and repeated callbacks preserve completed state', async () => {
|
|
reset('test'); const row = order(); const ctx = notification(); ctx.headers['x-payment-forwarded'] = '1';
|
|
assert.equal((await callback(ctx)).ErrCode, 0); assert.equal(row.state, 1);
|
|
assert.equal(state.user.starter_packState, 1); assert.equal(state.requests.length, 0);
|
|
row.state = 2; const analytics = state.writes.filter(w => w.name === 'susu').length;
|
|
assert.equal((await callback(ctx)).ErrCode, 0); assert.equal(row.state, 2);
|
|
assert.equal(state.writes.filter(w => w.name === 'susu').length, analytics);
|
|
});
|
|
test('test environment rejects missing or mismatched orders and production order ids', async () => {
|
|
for (const mismatch of [null, { openid: 'wrong' }, { itemid: 'other' }, { itemCount: 2 }, { goodsPrice: 301 }, { paymentAppEnv: 'production' }]) {
|
|
reset('test'); if (mismatch) order('wct_123', mismatch);
|
|
assert.notEqual((await callback(notification())).ErrCode, 0); assert.equal(state.writes.length, 0);
|
|
}
|
|
reset('test'); order('wcx_old');
|
|
assert.notEqual((await callback(notification('wcx_old'))).ErrCode, 0); assert.equal(state.writes.length, 0);
|
|
});
|
|
test('unconfigured production keeps legacy orders local, including missing-order failures', async () => {
|
|
reset(); delete process.env.PAYMENT_APP_ENV; delete process.env.WX_MIDAS_PAY_SIGN_KEY;
|
|
const row = order('wcx_old', { paymentAppEnv: undefined });
|
|
const ctx = notification('wcx_old'); delete ctx.body.MiniGame.PayEventSig;
|
|
assert.equal((await callback(ctx)).ErrCode, 0); assert.equal(row.state, 1); assert.equal(state.requests.length, 0);
|
|
assert.notEqual((await callback(notification('wcx_missing'))).ErrCode, 0); assert.equal(state.requests.length, 0);
|
|
});
|