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