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); });