import test from "node:test"; import assert from "node:assert/strict"; import { existsSync } from "node:fs"; import { readFile } from "node:fs/promises"; import { registerHooks } from "node:module"; const functionUrl = new URL("../functions/jungleTreasure.ts", import.meta.url); const yamlUrl = new URL("../functions/jungleTreasure.yaml", import.meta.url); const configUrl = new URL("../functions/jungleConfig.ts", import.meta.url); const configYamlUrl = new URL("../functions/jungleConfig.yaml", import.meta.url); const payCallbackUrl = new URL("../functions/wx/payCallBack.ts", import.meta.url); const orderPaySigUrl = new URL("../functions/wx/orderPaySig.ts", import.meta.url); const iosOrderPaySigUrl = new URL("../functions/wx/iosorderPaySig.ts", import.meta.url); const databaseState = { user: null, orders: [], collections: [], updates: [], reads: 0, }; globalThis.__jungleCloudMock = { database() { return { collection(name) { databaseState.collections.push(name); return { async add(payload) { databaseState.updates.push({ collection: name, query: null, payload }); if (name === "order") { databaseState.orders.push(payload); } return { ok: true }; }, where(query) { return { async getOne() { databaseState.reads++; if (name === "order") { return { data: databaseState.orders.find((order) => matches(order, query)) || null }; } return { data: databaseState.user }; }, async update(payload) { databaseState.updates.push({ collection: name, query, payload }); if (name === "order") { const orders = databaseState.orders.filter((order) => matches(order, query)); orders.forEach((order) => Object.assign(order, payload)); return { ok: true, updated: orders.length }; } if (!databaseState.user || !matches(databaseState.user, query)) { return { ok: true, updated: 0 }; } Object.assign(databaseState.user, payload); return { ok: true, updated: 1 }; }, }; }, }; }, }; }, }; globalThis.__jungleUtilsMock = { checkToken(received, expected) { return received === expected; }, }; globalThis.require = () => ({ initWithBatchMode() { return { track() {}, close() {} }; }, }); registerHooks({ resolve(specifier, context, nextResolve) { const aliases = { "@/Utils": "data:text/javascript,export default globalThis.__jungleUtilsMock", "@/jungleConfig": configUrl.href, }; if (specifier === "@lafjs/cloud") { return { url: "data:text/javascript,export default globalThis.__jungleCloudMock", shortCircuit: true, }; } if (aliases[specifier]) { return { url: aliases[specifier], shortCircuit: true }; } return nextResolve(specifier, context); }, }); const jungleModule = await import(functionUrl); const configModule = await import(configUrl); const { JUNGLE_TREASURE_CALENDAR_CONFIG, JUNGLE_TREASURE_CONFIG } = configModule; const { JUNGLE_TREASURE_DURATION_MS, claimJungleTier, createDefaultJungleState, getCurrentJungleCalendarPeriod, hasBlockingJungleClaims, isJungleActivityPurchasable, resolveJungleActivity, settleExpiredJungleTiers, unlockJungleTierByProductId, } = configModule; const { default: jungleTreasure } = jungleModule; const { default: payCallback } = await import(payCallbackUrl); const { default: orderPaySig } = await import(orderPaySigUrl); const { default: iosOrderPaySig } = await import(iosOrderPaySigUrl); function reset(user, orders = []) { databaseState.user = user; databaseState.orders = orders; databaseState.collections.length = 0; databaseState.updates.length = 0; databaseState.reads = 0; } function getStoredJungleState() { assert.equal(typeof databaseState.user.jungleTreasureState, "string"); return JSON.parse(databaseState.user.jungleTreasureState); } function matches(target, query) { return Object.entries(query || {}).every(([key, expected]) => getByPath(target, key) === expected); } function getByPath(target, path) { return path.split(".").reduce((value, key) => value == null ? undefined : value[key], target); } test("cloud function and yaml configuration exist", async () => { assert.equal(existsSync(functionUrl), true); assert.equal(existsSync(yamlUrl), true); assert.equal(existsSync(configUrl), true); assert.equal(existsSync(configYamlUrl), true); const yaml = await readFile(yamlUrl, "utf8"); const configYaml = await readFile(configYamlUrl, "utf8"); const functionSource = await readFile(functionUrl, "utf8"); assert.match(yaml, /^name:\s*jungleTreasure$/m); assert.match(configYaml, /^name:\s*jungleConfig$/m); assert.match(functionSource, /calendarUserId:\s*uid/); }); test("configuration contains 51 tiers and all eight product mappings", () => { assert.equal(JUNGLE_TREASURE_CALENDAR_CONFIG.enabled, true); assert.equal(JUNGLE_TREASURE_CALENDAR_CONFIG.dayOfMonth, 28); assert.equal(JUNGLE_TREASURE_CALENDAR_CONFIG.startHour, 0); assert.equal(JUNGLE_TREASURE_CALENDAR_CONFIG.startMinute, 0); assert.deepEqual(JUNGLE_TREASURE_CALENDAR_CONFIG.testUserIds, []); assert.equal(JUNGLE_TREASURE_CONFIG.totalTiers, 51); assert.equal(JUNGLE_TREASURE_CONFIG.tiers.length, 51); assert.deepEqual( JUNGLE_TREASURE_CONFIG.tiers.filter((tier) => tier.isPaid).map((tier) => tier.id), [4, 10, 16, 22, 28, 34, 40, 46], ); assert.deepEqual( JUNGLE_TREASURE_CONFIG.tiers.filter((tier) => tier.isPaid).map((tier) => tier.productId), Array.from({ length: 8 }, (_, index) => `jungle_treasure_${index + 1}`), ); assert.deepEqual( JUNGLE_TREASURE_CONFIG.tiers.filter((tier) => tier.isPaid).map((tier) => tier.price), [3, 6, 9, 15, 24, 42, 68, 98], ); }); test("online reward configuration remains structurally valid", () => { const rewardTypes = new Set(["coin", "freeze", "hammer", "magic_wand", "infinite_health"]); JUNGLE_TREASURE_CONFIG.tiers.forEach((tier, index) => { assert.equal(tier.id, index + 1); assert.ok(tier.rewards.length >= 1 && tier.rewards.length <= 5); tier.rewards.forEach((reward) => { assert.equal(rewardTypes.has(reward.type), true); assert.equal(Number.isFinite(reward.count) && reward.count > 0, true); }); }); }); test("default state is 1 for free tiers and 0 for paid tiers", () => { const state = createDefaultJungleState(100); assert.equal(state.tierStates.length, 51); assert.equal(state.expiresAt, 100 + 7 * 24 * 60 * 60 * 1000); assert.deepEqual(state.tierStates.slice(0, 10), [1, 1, 1, 0, 1, 1, 1, 1, 1, 0]); assert.deepEqual( state.tierStates.map((value, index) => value === 0 ? index + 1 : null).filter(Boolean), [4, 10, 16, 22, 28, 34, 40, 46], ); }); function calendarConfig(enabled = true, dayOfMonth = 28, testUserIds = []) { return { enabled, dayOfMonth, startHour: 0, startMinute: 0, timezoneOffsetMinutes: 8 * 60, durationMs: JUNGLE_TREASURE_DURATION_MS, testUserIds, }; } function completedState(expiresAt) { const state = createDefaultJungleState(expiresAt - JUNGLE_TREASURE_DURATION_MS); state.expiresAt = expiresAt; state.tierStates[0] = 2; state.tierStates[1] = 2; state.tierStates[2] = 2; return state; } test("calendar period starts at day 28 00:00 Asia/Shanghai with a common seven-day end", () => { const startsAt = Date.parse("2026-08-27T16:00:00.000Z"); assert.equal(getCurrentJungleCalendarPeriod(startsAt - 1, calendarConfig()), null); const period = getCurrentJungleCalendarPeriod(startsAt + 60 * 60 * 1000, calendarConfig()); assert.deepEqual(period, { key: "2026-08", startsAt, expiresAt: startsAt + JUNGLE_TREASURE_DURATION_MS, }); assert.equal( getCurrentJungleCalendarPeriod(period.expiresAt, calendarConfig()), null, ); }); test("a player without history starts the personal activity before the current calendar period", () => { const startsAt = Date.parse("2026-08-27T16:00:00.000Z"); const now = startsAt + 60 * 60 * 1000; const result = resolveJungleActivity(undefined, now, { calendarConfig: calendarConfig() }); assert.equal(result.available, true); assert.equal(result.reason, "personal_started"); assert.equal(result.state.activityType, "personal"); assert.equal(result.state.startsAt, now); assert.equal(result.state.expiresAt, now + JUNGLE_TREASURE_DURATION_MS); }); test("an existing completed player can join late and receives the common calendar end", () => { const startsAt = Date.parse("2026-08-27T16:00:00.000Z"); const state = completedState(startsAt - 1); // 旧线上状态没有新元数据,也必须视为首次活动已经触发。 delete state.activityType; delete state.periodKey; delete state.startsAt; delete state.firstTriggered; delete state.lastCalendarPeriodKey; const result = resolveJungleActivity( JSON.stringify(state), startsAt + 2 * 24 * 60 * 60 * 1000, { calendarConfig: calendarConfig() }, ); assert.equal(result.available, true); assert.equal(result.reason, "calendar_started"); assert.equal(result.state.activityType, "calendar"); assert.equal(result.state.periodKey, "2026-08"); assert.equal(result.state.startsAt, startsAt); assert.equal(result.state.expiresAt, startsAt + JUNGLE_TREASURE_DURATION_MS); assert.equal(result.state.firstTriggered, true); }); test("an activity active at the calendar boundary blocks the whole period", () => { const startsAt = Date.parse("2026-08-27T16:00:00.000Z"); const state = completedState(startsAt + 24 * 60 * 60 * 1000); const result = resolveJungleActivity( state, startsAt + 2 * 24 * 60 * 60 * 1000, { calendarConfig: calendarConfig() }, ); assert.equal(result.available, false); assert.equal(result.reason, "blocked_at_calendar_start"); assert.equal(result.state.lastCalendarPeriodKey, "2026-08"); }); test("expired pre-lock claims remain claim-only and still skip the current period", () => { const startsAt = Date.parse("2026-08-27T16:00:00.000Z"); const state = createDefaultJungleState(startsAt - JUNGLE_TREASURE_DURATION_MS - 1); state.expiresAt = startsAt - 1; assert.equal(hasBlockingJungleClaims(state), true); const claimOnly = resolveJungleActivity(state, startsAt + 1000, { calendarConfig: calendarConfig(), }); assert.equal(claimOnly.available, true); assert.equal(claimOnly.reason, "claim_only"); assert.equal(claimOnly.state.lastCalendarPeriodKey, "2026-08"); claimOnly.state.tierStates[0] = 2; claimOnly.state.tierStates[1] = 2; claimOnly.state.tierStates[2] = 2; const afterClaim = resolveJungleActivity(claimOnly.state, startsAt + 2000, { calendarConfig: calendarConfig(), }); assert.equal(afterClaim.available, false); assert.equal(afterClaim.reason, "calendar_period_handled"); }); test("an expired settlement skips claimed tiers, collects state 1, and stops at the first state 0", () => { const now = Date.now(); const state = createDefaultJungleState(now - JUNGLE_TREASURE_DURATION_MS - 1000); state.expiresAt = now - 1; state.tierStates[0] = 2; const result = settleExpiredJungleTiers(state, now); assert.equal(result.ok, true); assert.deepEqual(result.claimedTierIds, [2, 3]); assert.deepEqual(result.rewards, { coin: 200, freeze: 1, hammer: 1, magic_wand: 1, infinite_health: 0, }); assert.deepEqual(result.state.tierStates.slice(0, 5), [2, 2, 2, 0, 1]); }); test("an unlocked paid tier lets expired settlement continue until the next locked paid tier", () => { const now = Date.now(); let state = createDefaultJungleState(now - JUNGLE_TREASURE_DURATION_MS - 1000); state.expiresAt = now - 1; state = unlockJungleTierByProductId(state, "jungle_treasure_1", "paid_1", now - 500).state; const result = settleExpiredJungleTiers(state, now); assert.equal(result.ok, true); assert.deepEqual(result.claimedTierIds, [1, 2, 3, 4, 5, 6, 7, 8, 9]); assert.equal(result.rewards.infinite_health, 900); assert.equal(result.state.tierStates[9], 0); }); test("settling an old activity does not consume the next calendar month", () => { const augustStart = Date.parse("2026-08-27T16:00:00.000Z"); const state = createDefaultJungleState(augustStart - JUNGLE_TREASURE_DURATION_MS - 1); state.expiresAt = augustStart - 1; const claimOnly = resolveJungleActivity(state, augustStart + 1000, { calendarConfig: calendarConfig(), }); const settled = settleExpiredJungleTiers(claimOnly.state, augustStart + 2000); const septemberStart = Date.parse("2026-09-27T16:00:00.000Z"); const nextMonth = resolveJungleActivity(settled.state, septemberStart + 1000, { calendarConfig: calendarConfig(), }); assert.equal(claimOnly.state.lastCalendarPeriodKey, "2026-08"); assert.equal(nextMonth.available, true); assert.equal(nextMonth.reason, "calendar_started"); assert.equal(nextMonth.state.periodKey, "2026-09"); }); test("a disabled period stays skipped when enabled later in the same window", () => { const startsAt = Date.parse("2026-08-27T16:00:00.000Z"); const state = completedState(startsAt - 1); const disabled = resolveJungleActivity(state, startsAt + 1000, { calendarConfig: calendarConfig(false), }); assert.equal(disabled.available, false); assert.equal(disabled.reason, "calendar_disabled"); assert.equal(disabled.state.lastCalendarPeriodKey, "2026-08"); const enabledLater = resolveJungleActivity(disabled.state, startsAt + 2000, { calendarConfig: calendarConfig(true), }); assert.equal(enabledLater.available, false); assert.equal(enabledLater.reason, "calendar_period_handled"); }); test("a disabled global calendar starts only for configured test users", () => { const startsAt = Date.parse("2026-08-27T16:00:00.000Z"); const config = calendarConfig(false, 28, ["jungle-test-user"]); const testUser = resolveJungleActivity(completedState(startsAt - 1), startsAt + 1000, { calendarConfig: config, calendarUserId: "jungle-test-user", }); assert.equal(testUser.available, true); assert.equal(testUser.reason, "calendar_started"); const ordinaryUser = resolveJungleActivity(completedState(startsAt - 1), startsAt + 1000, { calendarConfig: config, calendarUserId: "ordinary-user", }); assert.equal(ordinaryUser.available, false); assert.equal(ordinaryUser.reason, "calendar_disabled"); }); test("disabling or changing calendar config does not stop an activity already started", () => { const startsAt = Date.parse("2026-08-27T16:00:00.000Z"); const started = resolveJungleActivity(completedState(startsAt - 1), startsAt + 1000, { calendarConfig: calendarConfig(true), }); assert.equal(started.reason, "calendar_started"); const continued = resolveJungleActivity(started.state, startsAt + 2000, { calendarConfig: calendarConfig(false, 15), }); assert.equal(continued.available, true); assert.equal(continued.reason, "active"); assert.equal(continued.state.periodKey, "2026-08"); assert.equal(continued.state.expiresAt, started.state.expiresAt); }); test("invalid calendar configuration fails closed without replacing an ended state", () => { const startsAt = Date.parse("2026-08-27T16:00:00.000Z"); const result = resolveJungleActivity(completedState(startsAt - 1), startsAt + 1000, { calendarConfig: calendarConfig(true, 29), }); assert.equal(result.available, false); assert.equal(result.reason, "invalid_calendar_config"); assert.equal(result.state.activityType, "personal"); }); test("Jungle purchases are allowed only before the stored activity expiry", () => { const now = Date.now(); const state = createDefaultJungleState(now); assert.equal(isJungleActivityPurchasable(state, now + 1), true); assert.equal(isJungleActivityPurchasable(state, state.expiresAt), false); assert.equal(isJungleActivityPurchasable(null, now), false); }); test("read initializes storage and returns the state array with configuration", async () => { reset({ _id: "u1", openid: "o1", token: "secret" }); const result = await jungleTreasure({ body: { action: "read", uid: "u1", token: "secret" }, }); assert.equal(result.code, 1); assert.equal(databaseState.collections[0], "users"); assert.equal(result.data.config.tiers.length, 51); assert.equal(result.data.config.version, 2); assert.equal(result.data.config.endAt, result.data.expiresAt); assert.equal(typeof result.data.serverNow, "number"); assert.deepEqual(result.data.tierStates.slice(0, 10), [1, 1, 1, 0, 1, 1, 1, 1, 1, 0]); const storedState = getStoredJungleState(); assert.equal(storedState.tierStates.length, 51); assert.equal(storedState.expiresAt, result.data.expiresAt); }); test("claim does not require earlier tiers to be claimed", async () => { reset({ _id: "u1", openid: "o1", token: "secret", jungleTreasureState: JSON.stringify(createDefaultJungleState()), }); const result = await jungleTreasure({ body: { action: "claim", uid: "u1", token: "secret", tierId: 5 }, }); assert.equal(result.code, 1); assert.equal(result.data.claimedTierId, 5); assert.equal(result.data.tierStates[4], 2); assert.equal(result.data.tierStates[0], 1); assert.equal(result.data.tierStates[3], 0); }); test("an expired single-tier claim cannot cross the first locked paid tier", async () => { const now = Date.now(); const expired = createDefaultJungleState(now - JUNGLE_TREASURE_DURATION_MS - 1000); expired.expiresAt = now - 1; reset({ _id: "u1", openid: "o1", token: "secret", jungleTreasureState: JSON.stringify(expired), }); const result = await jungleTreasure({ body: { action: "claim", uid: "u1", token: "secret", tierId: 5 }, }); assert.equal(result.code, 0); assert.match(result.msg, /未解锁付费档之后/); }); test("expired batch settlement uses the first client resource snapshot and is idempotent", async () => { const now = Date.now(); const expired = createDefaultJungleState(now - JUNGLE_TREASURE_DURATION_MS - 1000); expired.expiresAt = now - 1; reset({ _id: "u1", openid: "o1", token: "secret", coinAmount: 5, freezeAmount: 5, hammerAmount: 5, magicAmount: 5, userPowerTime: 0, jungleTreasureState: JSON.stringify(expired), }); const resources = { coinAmount: 1000, freezeAmount: 10, hammerAmount: 20, magicAmount: 30, userPowerTime: 40, }; const first = await jungleTreasure({ body: { action: "settle_expired", uid: "u1", token: "secret", resources: JSON.stringify(resources) }, }); assert.equal(first.code, 1); assert.equal(first.data.reason, "settlement_pending"); assert.deepEqual(first.data.pendingSettlement.claimedTierIds, [1, 2, 3]); assert.deepEqual(first.data.pendingSettlement.rewards, { coin: 300, freeze: 1, hammer: 1, magic_wand: 1, infinite_health: 0, }); assert.deepEqual(first.data.pendingSettlement.balances, { coinAmount: 1300, freezeAmount: 11, hammerAmount: 21, magicAmount: 31, userPowerTime: 40, }); assert.equal(databaseState.user.coinAmount, 1300); assert.deepEqual(getStoredJungleState().tierStates.slice(0, 5), [2, 2, 2, 0, 1]); const second = await jungleTreasure({ body: { action: "settle_expired", uid: "u1", token: "secret", resources: { ...resources, coinAmount: 999999 }, }, }); assert.equal(second.code, 1); assert.equal(second.data.pendingSettlement.settlementId, first.data.pendingSettlement.settlementId); assert.deepEqual(second.data.pendingSettlement.balances, first.data.pendingSettlement.balances); assert.equal(databaseState.user.coinAmount, 1300); }); test("expired batch settlement stacks infinite health on the client cutoff time", async () => { const now = Date.now(); let expired = createDefaultJungleState(now - JUNGLE_TREASURE_DURATION_MS - 1000); expired.expiresAt = now - 1; expired = unlockJungleTierByProductId(expired, "jungle_treasure_1", "paid_1", now - 500).state; const clientPowerTime = Math.floor(now / 1000) + 3600; reset({ _id: "u1", openid: "o1", token: "secret", jungleTreasureState: JSON.stringify(expired), }); const result = await jungleTreasure({ body: { action: "settle_expired", uid: "u1", token: "secret", resources: { coinAmount: 0, freezeAmount: 0, hammerAmount: 0, magicAmount: 0, userPowerTime: clientPowerTime, }, }, }); assert.equal(result.code, 1); assert.equal(result.data.pendingSettlement.rewards.infinite_health, 900); assert.equal(result.data.pendingSettlement.balances.userPowerTime, clientPowerTime + 900); }); test("pending settlement is returned by read and acknowledgement is retry-safe", async () => { const pending = { settlementId: "jungle:1:2", rewards: { coin: 10, freeze: 0, hammer: 0, magic_wand: 0, infinite_health: 0 }, balances: { coinAmount: 20, freezeAmount: 1, hammerAmount: 2, magicAmount: 3, userPowerTime: 0, }, claimedTierIds: [1], }; reset({ _id: "u1", openid: "o1", token: "secret", pendingJungleSettlement: pending, jungleTreasureState: JSON.stringify(createDefaultJungleState()), }); const read = await jungleTreasure({ body: { action: "read", uid: "u1", token: "secret" } }); assert.equal(read.code, 1); assert.equal(read.data.reason, "settlement_pending"); assert.equal(read.data.pendingSettlement.settlementId, pending.settlementId); const ack = await jungleTreasure({ body: { action: "ack_settlement", uid: "u1", token: "secret", settlementId: pending.settlementId }, }); assert.equal(ack.code, 1); assert.equal(databaseState.user.pendingJungleSettlement, null); const retry = await jungleTreasure({ body: { action: "ack_settlement", uid: "u1", token: "secret", settlementId: pending.settlementId }, }); assert.equal(retry.code, 1); assert.equal(retry.data.acknowledged, true); }); test("a locked paid tier cannot be claimed", async () => { reset({ _id: "u1", openid: "o1", token: "secret", jungleTreasureState: JSON.stringify(createDefaultJungleState()), }); const result = await jungleTreasure({ body: { action: "claim", uid: "u1", token: "secret", tierId: 4 }, }); assert.equal(result.code, 0); assert.match(result.msg, /尚未解锁/); }); test("all product IDs unlock their configured paid tier and remember the order", () => { let state = createDefaultJungleState(100); const paidTierIds = [4, 10, 16, 22, 28, 34, 40, 46]; paidTierIds.forEach((tierId, index) => { const productId = `jungle_treasure_${index + 1}`; const result = unlockJungleTierByProductId(state, productId, `order_${index + 1}`, 200 + index); assert.equal(result.ok, true); assert.equal(result.tierId, tierId); assert.equal(result.state.tierStates[tierId - 1], 1); assert.equal(result.state.paidOrderNos[String(tierId)], `order_${index + 1}`); state = result.state; }); }); test("WeChat paid callback unlocks the tier matched by productId", async () => { const order = { outTradeNo: "order_1", openid: "o1", itemid: "jungle_treasure_1", itemCount: 1, goodsPrice: 300, type: "and", state: 0, }; reset({ _id: "u1", openid: "o1", token: "secret" }, [order]); const result = await payCallback({ body: { MiniGame: { Payload: JSON.stringify({ OpenId: "o1", OutTradeNo: "order_1", GoodsInfo: { ProductId: "jungle_treasure_1", Quantity: 1, OrigPrice: 300 }, }), }, }, headers: {}, socket: { remoteAddress: "127.0.0.1" }, }); assert.equal(result.ErrCode, 0); // 订单状态完全跟随当前线上回调逻辑,不由周期活动需求改写。 assert.equal(order.state, 2); const storedState = getStoredJungleState(); assert.equal(storedState.tierStates[3], 1); assert.equal(storedState.paidOrderNos["4"], "order_1"); }); test("Jungle order signing uses server product price in cents and quantity one", async () => { reset({ _id: "u1", openid: "o1", token: "secret", session_key: "session-key", jungleTreasureState: JSON.stringify(createDefaultJungleState()), }); const result = await orderPaySig({ body: { uid: "u1", itemid: "jungle_treasure_1", itemCount: 99, itemPrice: 1, }, }); assert.equal(result.code, 1); const signData = JSON.parse(result.data.signData); assert.equal(signData.productId, "jungle_treasure_1"); assert.equal(signData.buyQuantity, 1); assert.equal(signData.goodsPrice, 300); assert.equal(databaseState.orders[0].goodsPrice, 300); assert.equal(databaseState.orders[0].jungleTierId, 4); }); test("both payment platforms reject a Jungle order created after activity expiry", async () => { const expired = createDefaultJungleState(Date.now() - JUNGLE_TREASURE_DURATION_MS - 1000); expired.expiresAt = Date.now() - 1000; const user = { _id: "u1", openid: "o1", token: "secret", session_key: "session-key", jungleTreasureState: JSON.stringify(expired), }; const body = { uid: "u1", itemid: "jungle_treasure_1", itemCount: 1, itemPrice: 300, }; reset({ ...user }); const android = await orderPaySig({ body }); assert.equal(android.code, 0); assert.match(android.msg, /活动已结束/); assert.equal(databaseState.orders.length, 0); reset({ ...user }); const ios = await iosOrderPaySig({ body }); assert.equal(ios.code, 0); assert.match(ios.msg, /活动已结束/); assert.equal(databaseState.orders.length, 0); }); test("claiming an unlocked paid tier marks its payment order delivered", async () => { const unlocked = unlockJungleTierByProductId(null, "jungle_treasure_1", "order_1", 100).state; const order = { outTradeNo: "order_1", openid: "o1", itemid: "jungle_treasure_1", state: 1 }; reset({ _id: "u1", openid: "o1", token: "secret", jungleTreasureState: unlocked, }, [order]); const result = await jungleTreasure({ body: { action: "claim", uid: "u1", token: "secret", tierId: 4 }, }); assert.equal(result.code, 1); assert.equal(result.data.tierStates[3], 2); assert.equal(order.state, 2); assert.ok(order.getTime instanceof Date); }); test("a claimed tier cannot return success again", () => { const state = createDefaultJungleState(100); const first = claimJungleTier(state, 5, 200); const second = claimJungleTier(first.state, 5, 300); assert.equal(first.ok, true); assert.equal(second.ok, false); assert.match(second.message, /已经领取/); }); test("save is disabled, token is checked, and IAA is excluded", async () => { reset({ _id: "u1", openid: "o1", token: "secret" }); const save = await jungleTreasure({ body: { action: "save", uid: "u1", token: "secret", tierStates: [] }, }); assert.equal(save.code, 0); reset({ _id: "u1", openid: "o1", token: "secret" }); const badToken = await jungleTreasure({ body: { action: "read", uid: "u1", token: "wrong" }, }); assert.equal(badToken.code, 0); reset({ _id: "u1", openid: "o1", token: "secret" }); const iaa = await jungleTreasure({ body: { action: "read", uid: "u1", token: "secret", gameName: "iaa" }, }); assert.equal(iaa.code, 1); assert.equal(iaa.data.available, false); assert.equal(iaa.data.reason, "iaa_excluded"); assert.equal(databaseState.collections.length, 0); });