MatchMaster/tools/test-reborn-gift.cjs
2026-09-21 17:13:35 +08:00

216 lines
14 KiB
JavaScript

const test = require('node:test');
const assert = require('node:assert/strict');
const crypto = require('node:crypto');
const { loadMembers, loadModule } = require('./new-player-ab-test-support.cjs');
const products = ['reborn_Gift', 'reborn_Gift_2', 'reborn_Gift_3', 'reborn_Gift_4', 'reborn_Gift_5'];
const prices = [600, 800, 1000, 1200, 1500];
const MapController = loadMembers('assets/Script/Map.ts', [
'rebornGiftCount', 'rebornGiftOrders', 'recordRebornGiftPurchase', 'getRebornGiftProduct',
]);
function fixture() {
const storage = new Map(), calls = { rewards: [], revived: [], acknowledgements: [], products: [], props: [] };
const info = { uid: 'u', coin: 100, review: 3, iosCanPay: true };
const map = new MapController();
const cc = {
fx: { GameConfig: { GM_INFO: info }, StorageMessage: {
getStorage: key => storage.get(key), setStorage: (key, value) => storage.set(key, value),
} },
sys: { platform: 1, WECHAT_GAME: 1, BYTEDANCE_GAME: 2 },
isValid: value => !!value && !value.destroyed, find: () => null,
};
MapController._instance = map;
map.SceneManager = { openRewardWindow: rewards => calls.rewards.push(rewards) };
const MiniGameSdk = { API: { showToast() {}, yinli_Pay() {}, shushu_SetSuperProperties() {} } };
const Tool = loadMembers('assets/Script/module/Tool/GameTool.ts', ['shopBuy'], { cc, MapConroler: MapController, MiniGameSdk });
cc.fx.GameTool = Object.assign(new Tool(), {
changeCoin: amount => { info.coin += amount; },
getShopProp: props => calls.props.push(props), shushu_Track() {},
});
const Utils = {
outTradeNo: 'unrelated-order',
buyProp: (id, count, price, platform, product, callback) => { calls.products.push({ id, count, price }); calls.pay = callback; },
getPayInfo: (callback, order) => { calls.poll = callback; calls.polledOrder = order; },
getIosPayInfo: (order, callback) => { calls.iosPoll = callback; calls.polledOrder = order; },
setPayInfo: (callback, order) => { calls.acknowledgements.push(order); calls.ack = callback; },
GoKEFu: (data, callback) => { calls.iosRequest = data; info.iosReviveOrder = 'ios-order'; callback('success'); },
};
const Revive = loadMembers('assets/Script/Revive.ts', [
'iosMap', 'iosUid', 'checkingIosOrder', 'iosPrice', 'iosProductId', 'iosCount',
'buyProduct', 'onShow', 'iosOldPay', 'completePurchase',
], { cc, MapConroler: MapController, MiniGameSdk, Utils, wx: { getSystemInfoSync: () => ({ platform: 'android' }) }, console: { log() {}, error() {} } });
const revive = Object.assign(new Revive(), {
btn_Touch: true, openLoad() {}, closeLoad() {}, buyGift() {}, updateGameCoin() {}, updatePrice() {}, openConfirmBox() {},
revivew: source => calls.revived.push(source),
});
return { map, revive, info, calls, cc, Utils, newMap: () => {
const next = new MapController(); next.SceneManager = map.SceneManager; MapController._instance = next; return next;
} };
}
test('one run advances through five prices, caps at 15 yuan and leaves coin revival untouched', () => {
const f = fixture();
for (let i = 0; i < 8; i++) {
const tier = Math.min(i, 4), offer = f.map.getRebornGiftProduct();
assert.equal(offer.id, products[tier]); assert.equal(offer.price, prices[tier]);
f.revive.completePurchase(offer.id, `order-${i}`, f.map, 'u');
assert.equal(f.map.rebornGiftCount, i + 1);
assert.equal(f.calls.props[i].price, prices[tier]);
assert.equal(f.calls.props[i].freeze, 1); assert.equal(f.calls.props[i].hammer, 1); assert.equal(f.calls.props[i].magic_wand, 1);
}
assert.equal(f.info.coin, 8100); assert.equal(f.info.review, 3);
assert.equal(f.calls.revived.length, 8);
assert.equal(f.newMap().getRebornGiftProduct().price, 600);
});
test('duplicate callbacks and login compensation grant each order only once', () => {
const f = fixture();
f.revive.completePurchase('reborn_Gift', 'same', f.map, 'u');
f.revive.completePurchase('reborn_Gift', 'same', f.map, 'u');
f.cc.fx.GameTool.shopBuy('reborn_Gift', true, { outTradeNo: 'same' });
assert.equal(f.info.coin, 1100); assert.equal(f.map.rebornGiftCount, 1); assert.equal(f.calls.revived.length, 1);
for (const product of products) f.cc.fx.GameTool.shopBuy(product, true, { outTradeNo: product });
assert.equal(f.info.coin, 6100); assert.equal(f.map.rebornGiftCount, 1);
});
test('old-run payment grants rewards without reviving or advancing a new run; changed account is ignored', () => {
const f = fixture(); const next = f.newMap();
f.revive.completePurchase('reborn_Gift_3', 'late', f.map, 'u');
assert.equal(f.info.coin, 1100); assert.equal(next.rebornGiftCount, 0); assert.equal(f.calls.revived.length, 0);
f.info.uid = 'other'; f.revive.completePurchase('reborn_Gift_4', 'other-account', f.map, 'u');
assert.equal(f.info.coin, 1100);
});
test('ordinary payment captures its order and run, blocks repeated clicks and counts verified success once', () => {
const f = fixture();
f.revive.buyProduct(null); f.revive.buyProduct(null);
assert.equal(f.calls.products.length, 1); assert.equal(f.map.rebornGiftCount, 0);
f.calls.pay({ outTradeNo: 'paid-order' }); assert.equal(f.calls.polledOrder, 'paid-order');
f.calls.poll({ code: 1, data: { pay_state: 2 } });
assert.equal(f.map.rebornGiftCount, 1); assert.equal(f.info.coin, 1100);
assert.deepEqual(f.calls.acknowledgements, ['paid-order']);
f.calls.ack({ code: 1 }); assert.equal(f.map.rebornGiftCount, 1);
});
test('cancelled, failed and unconfirmed payments keep the six-yuan tier', () => {
for (const response of [null, { err: true, errCode: -2 }, { err: true, errCode: -1 }]) {
const f = fixture(); f.revive.buyProduct(null); f.calls.pay(response);
assert.equal(f.map.rebornGiftCount, 0); assert.equal(f.revive.btn_Touch, true);
}
const f = fixture(); f.revive.buyProduct(null); f.calls.pay({ outTradeNo: 'unknown' });
f.calls.poll({ code: 0, data: { pay_state: -1 } });
assert.equal(f.map.rebornGiftCount, 0); assert.equal(f.info.coin, 100);
const missing = fixture(); missing.revive.buyProduct(null); missing.calls.pay({});
assert.equal(missing.calls.polledOrder, undefined); assert.equal(missing.revive.btn_Touch, true);
});
test('iOS background return keeps the run; overlapping polls and late callbacks do not double grant', () => {
const f = fixture(); f.map.recordRebornGiftPurchase('earlier');
f.revive.iosOldPay(800, 'reborn_Gift_2', f.map, 'u');
assert.equal(f.calls.iosRequest.price, 800);
f.revive.onShow(); const poll = f.calls.iosPoll; f.revive.onShow(); assert.equal(f.calls.iosPoll, poll);
poll({ code: 1, data: { payment_name: 'reborn_Gift_2', goodsPrice: 800 } });
assert.equal(f.map.rebornGiftCount, 2); assert.equal(f.info.coin, 1100);
assert.deepEqual(f.calls.acknowledgements, ['ios-order']);
f.info.iosReviveOrder = 'late-ios'; const next = f.newMap(); f.revive.onShow();
f.calls.iosPoll({ code: 1, data: { payment_name: 'reborn_Gift_3', goodsPrice: 1000 } });
assert.equal(next.rebornGiftCount, 0); assert.equal(f.info.coin, 2100); assert.equal(f.calls.revived.length, 1);
});
test('price node updates the amount on every opening, including 10/12/15 yuan', () => {
const data = JSON.parse(require('node:fs').readFileSync('assets/lose/prefab/Lose.prefab', 'utf8'));
const node = data.find(n => n.__type__ === 'cc.Node' && n._name === 'price');
assert.ok(node._components.some(ref => data[ref.__id__].__type__ === 'cc.Label'));
assert.ok(data[data.findIndex(n => n === node)]._parent);
assert.ok(data[node._parent.__id__]._children.some(ref => data[ref.__id__]._name === 'yuan'));
const label = {}, price = { x: node._trs.array[0], width: node._contentSize.width, anchorX: node._anchorPoint.x, getComponent: () => label };
const right = price.x + price.width * (1 - price.anchorX);
const map = new MapController(); MapController._instance = map;
const cc = { Label: {} };
const Panel = loadMembers('assets/Script/Revive.ts', ['updatePrice'], { cc, MapConroler: MapController });
const panel = new Panel(); panel.node = { getChildByName: name => name === 'price' ? price : null };
for (let i = 0; i < 7; i++) {
map.rebornGiftCount = i; panel.updatePrice();
assert.equal(label.string, String(prices[Math.min(i, 4)] / 100));
assert.equal(price.x, right); assert.equal(price.anchorX, 1);
price.width = label.string.length * 44;
}
});
function serverFixture() {
const state = { orders: [], user: { _id: 'u', openid: 'o', session_key: 'session' }, requests: [] };
const matches = (row, query) => Object.entries(query).every(([key, value]) => row[key] === value);
const db = { collection: name => ({
add: async row => { state.orders.push({ ...row, collection: name }); return {}; },
where: query => ({
getOne: async () => ({ data: (name === 'users' ? [state.user] : state.orders).find(row => matches(row, query)) }),
update: async values => { (name === 'users' ? [state.user] : state.orders).filter(row => matches(row, query)).forEach(row => Object.assign(row, values)); return {}; },
}),
}) };
const cloud = { database: () => db, shared: { get: key => key === 'wxaccess_token' ? 'token' : {}, set() {} }, storage: { bucket: () => ({ readFile: async () => ({ Body: { transformToString: async () => 'key' } }) }) } };
const deps = {
'@lafjs/cloud': { default: cloud }, '@/limitedTimeEvent': { starterPackOrderSnapshot: () => ({}), reportRookieGift: async () => {} },
'@/passCheckUpgrade': { passOrderFields: () => ({}), PASS_PRICES: {}, isUpgradeOrder: () => false },
'@/jungleConfig': { getJunglePaidTierByProductId: () => null },
'thinkingdata-node': {},
crypto: { ...crypto, default: { ...crypto, createSign: () => ({ update() {}, sign: () => 'signature' }) } },
};
const globals = { process: { env: { PAYMENT_APP_ENV: 'production', WX_MIDAS_PAY_SIGN_KEY: 'test-key' } }, console: { log() {}, error() {} },
fetch: async (url, options) => { state.requests.push({ url, body: options?.body && JSON.parse(options.body) }); return { ok: true, json: async () => ({ prepay_id: 'prepay', errcode: 0, trade_state: 'SUCCESS' }) }; },
};
return { state, load: file => loadModule(file, deps, globals).default };
}
test('Android and iOS signing enforce all five server prices and quantity one', async () => {
for (const file of ['orderPaySig', 'iosorderPaySig']) {
const f = serverFixture(), create = f.load(`server/laf-cloud/functions/wx/${file}.ts`);
for (let i = 0; i < products.length; i++) {
const result = await create({ body: { uid: 'u', itemid: products[i], itemPrice: 1, itemCount: 99 } });
assert.equal(result.code, 1);
const signed = JSON.parse(result.data.signData);
assert.equal(signed.goodsPrice, prices[i]); assert.equal(signed.buyQuantity, 1);
assert.equal(f.state.orders[i].goodsPrice, prices[i]);
}
}
});
test('legacy iOS customer service enforces the same prices and names', async () => {
const f = serverFixture(), create = f.load('server/laf-cloud/functions/wx/KeFuInfo.ts');
for (let i = 0; i < products.length; i++) {
await create({ body: { FromUserName: 'o', SessionFrom: JSON.stringify({ tpye: 'ios', propName: products[i], price: 1, count: 99, outTradeNo: `ios-${i}` }) } });
assert.equal(f.state.orders[i].goodsPrice, prices[i]); assert.equal(f.state.orders[i].itemCount, 1);
const payment = f.state.requests.filter(r => r.url.includes('/v3/pay/transactions/jsapi'))[i];
assert.equal(payment.body.amount.total, prices[i]);
}
});
test('reborn acknowledgement rejects unpaid or foreign orders and tolerates retries', async () => {
const f = serverFixture(), acknowledge = f.load('server/laf-cloud/functions/wx/getOrderReward.ts');
const order = { outTradeNo: 'paid', itemid: 'reborn_Gift_5', openid: 'o', state: 0 }; f.state.orders.push(order);
const request = () => acknowledge({ body: { outTradeNo: 'paid', uid: 'u' } });
assert.equal((await request()).code, 0); assert.equal(order.state, 0);
order.state = 1; order.openid = 'other'; assert.equal((await request()).code, 0);
order.openid = 'o'; assert.equal((await request()).code, 1); assert.equal(order.state, 2);
assert.equal((await request()).code, 1);
});
test('legacy iOS confirms all tiers but leaves new-client rewards pending until acknowledgement', async () => {
for (const version of [undefined, 1]) for (let i = 0; i < products.length; i++) {
const f = serverFixture(), poll = f.load('server/laf-cloud/functions/wx/iosgetPayInfo.ts');
const order = { outTradeNo: 'ios', itemid: products[i], goodsPrice: prices[i], openid: 'o', state: 0 };
f.state.orders.push(order);
const result = await poll({ body: { uid: 'u', outTradeNo: 'ios', rebornGiftVersion: version }, headers: {}, socket: {} });
assert.equal(result.code, 1); assert.equal(result.data.goodsPrice, prices[i]);
assert.equal(order.state, version === 1 ? 1 : 2);
assert.equal(f.state.user.rebornGiftCount, 0, 'legacy global limit stays disabled');
if (version === 1) {
const again = await poll({ body: { uid: 'u', outTradeNo: 'ios', rebornGiftVersion: 1 }, headers: {}, socket: {} });
assert.equal(again.code, 1); assert.equal(order.state, 1);
const acknowledge = f.load('server/laf-cloud/functions/wx/getOrderReward.ts');
assert.equal((await acknowledge({ body: { uid: 'u', outTradeNo: 'ios' } })).code, 1);
assert.equal(order.state, 2);
}
}
});