216 lines
14 KiB
JavaScript
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);
|
|
}
|
|
}
|
|
});
|