import test from 'node:test'; import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; import { stripTypeScriptTypes } from 'node:module'; // Exercise the real login recovery function without contacting the payment provider. const source = await readFile(new URL('../functions/login.ts', import.meta.url), 'utf8'); const recoverySource = source.slice(source.indexOf('async function getIosOrder('), source.indexOf('async function sead(')); const buildRecovery = new Function('db', 'getOrder', 'sead', 'getClientIp', 'JungleConfig', `return (${stripTypeScriptTypes(recoverySource)});`); function fixture(itemid, extra = {}, providerState = 'SUCCESS') { const user = { _id: 'user-1', openid: 'openid-1', rebornGiftTime: 0, rebornGiftCount: 3, pay_user: false }; const order = { outTradeNo: 'order-1', openid: user.openid, state: 0, type: 'ios', itemid, ...extra }; const providerCalls = []; const db = { collection(name) { const documents = name === 'users' ? [user] : [order]; return { where(query) { const matched = documents.filter(row => Object.entries(query).every(([key, value]) => row[key] === value)); return { async get() { return { data: structuredClone(matched) }; }, async getOne() { return { data: structuredClone(matched[0]) }; }, async update(patch) { matched.forEach(row => Object.assign(row, patch)); }, async remove() { assert.fail('recoverable activity orders must not be deleted'); }, }; } }; } }; const recover = buildRecovery(db, async orderId => { providerCalls.push(orderId); return { trade_state: providerState }; }, () => {}, () => '', { getJunglePaidTierByProductId: () => null }); return { user, order, providerCalls, recover }; } for (const itemid of ['reborn_Gift', 'reborn_Gift_2', 'reborn_Gift_3', 'reborn_Gift_4', 'reborn_Gift_5']) { test(`login preserves main iOS recovery for ${itemid} until client confirms delivery`, async () => { const { user, order, providerCalls, recover } = fixture(itemid); const before = Date.now(); const pending = await recover(user.openid, {}); assert.equal(user.rebornGiftCount, 0); assert(user.rebornGiftTime >= before); assert.equal(user.pay_user, true); assert.equal(order.state, 1, 'confirmed payment stays pending for client reward delivery'); assert.deepEqual(pending.map(row => row.outTradeNo), ['order-1']); assert.deepEqual(providerCalls, ['order-1']); assert.deepEqual(await recover(user.openid, {}), [], 'state=1 goes through ordinary login recovery next time'); assert.deepEqual(providerCalls, ['order-1'], 'no second provider query for confirmed payment'); }); } test('disabled gold miner orders never enter legacy iOS recovery', async () => { const { user, order, providerCalls, recover } = fixture('gold_miner', { goldMiner: { confirmedAt: 0 }, time: 1 }); assert.deepEqual(await recover(user.openid, {}), []); assert.deepEqual(providerCalls, []); assert.equal(order.state, 0); assert.equal(user.rebornGiftCount, 3); }); for (const itemid of ['starter_pack', 'starter_pack_2', 'starter_pack_3', 'starter_pack_4', 'starter_pack_5']) { test(`login recovers ${itemid} without order offer or cycle metadata`, async () => { const { user, order, recover } = fixture(itemid); const pending = await recover(user.openid, {}); assert.equal(user.starter_packState, 1); assert.equal(user.pay_user, true); assert.equal(order.state, 1); assert.equal(pending[0].itemid, itemid); assert.equal(pending[0].starterPackOffer, undefined); assert.equal(pending[0].starterPackBuyRound, undefined); assert.equal(pending[0].starterPackExpiresAt, undefined); assert.deepEqual(await recover(user.openid, {}), []); }); } test('login payment closes the current offer without matching an order round', async () => { const { user, order, recover } = fixture('starter_pack_3', { starterPackBuyRound: 2, starterPackExpiresAt: 1000 }); Object.assign(user, { starterPackRound: 3, starter_pack: 2000, starterPackOffer: { version: 3, round: 3 } }); assert.equal((await recover(user.openid, {})).length, 1); assert.equal(user.starter_packState, 1); assert.equal(user.pay_user, true); assert.equal(order.state, 1); }); test('login closes a matching migrated legacy cycle for an older order without an offer snapshot', async () => { const { user, order, recover } = fixture('starter_pack', { time: Date.now() - 10000 }); Object.assign(user, { starter_pack: Date.now() + 10000, starterPackRound: 2, starterPackOffer: { legacy: true } }); assert.equal((await recover(user.openid, {})).length, 1); assert.equal(order.state, 1); assert.equal(user.starter_packState, 1); assert.equal(order.starterPackOffer, undefined); }); test('login keeps unpaid lucky orders available past the old three-hour cleanup', async () => { for (const itemid of ['starter_pack', 'starter_pack_5']) { const { user, order, recover } = fixture(itemid, { time: Date.now() - 4 * 3600000 }, 'NOTPAY'); assert.deepEqual(await recover(user.openid, {}), []); assert.equal(order.state, 0); } });