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 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_CONFIG } = configModule; const { claimJungleTier, createDefaultJungleState, unlockJungleTierByProductId, } = configModule; const { default: jungleTreasure } = jungleModule; const { default: payCallback } = await import(payCallbackUrl); const { default: orderPaySig } = await import(orderPaySigUrl); 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"); assert.match(yaml, /^name:\s*jungleTreasure$/m); assert.match(configYaml, /^name:\s*jungleConfig$/m); }); test("configuration contains 51 tiers and all eight product mappings", () => { 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("configuration rewards match the 51-tier planning table", () => { const actual = JUNGLE_TREASURE_CONFIG.tiers.map((tier) => `${tier.id}|${tier.price}|${tier.rewards.map((reward) => `${reward.type}:${reward.count}`).join(",")}` ); assert.deepEqual(actual, [ "1|0|hammer:1", "2|0|coin:500", "3|0|freeze:1,magic_wand:1", "4|3|coin:1000,hammer:1", "5|0|coin:500,magic_wand:1", "6|0|freeze:1", "7|0|freeze:1", "8|0|magic_wand:1", "9|0|magic_wand:1", "10|6|coin:2000,hammer:2,magic_wand:1", "11|0|coin:500,hammer:1,freeze:1", "12|0|coin:500", "13|0|freeze:1", "14|0|freeze:1,magic_wand:1", "15|0|magic_wand:1", "16|9|coin:3000,hammer:2,magic_wand:2", "17|0|coin:1000,hammer:1,magic_wand:1", "18|0|coin:500", "19|0|hammer:1", "20|0|freeze:2", "21|0|magic_wand:1,infinite_health:900", "22|15|coin:4000,hammer:3,magic_wand:2", "23|0|coin:1500,hammer:1,magic_wand:2", "24|0|coin:1000", "25|0|hammer:1,freeze:1", "26|0|freeze:2,magic_wand:1", "27|0|hammer:1,freeze:2,magic_wand:1", "28|24|coin:6500,hammer:5,magic_wand:3", "29|0|coin:2000,hammer:1,magic_wand:3", "30|0|coin:1500,infinite_health:3600", "31|0|hammer:1,freeze:2", "32|0|freeze:3,magic_wand:2", "33|0|hammer:1,freeze:3,magic_wand:2", "34|42|coin:10000,hammer:8,magic_wand:6", "35|0|coin:3500,hammer:3,magic_wand:4", "36|0|coin:3000", "37|0|hammer:2,freeze:2", "38|0|freeze:4,magic_wand:4", "39|0|hammer:1,freeze:4,magic_wand:6", "40|68|coin:18000,hammer:12,magic_wand:10", "41|0|coin:6000,hammer:4,magic_wand:8", "42|0|coin:4000,infinite_health:7200", "43|0|hammer:2,freeze:5", "44|0|freeze:7,magic_wand:5", "45|0|hammer:2,freeze:8,magic_wand:7", "46|98|coin:35000,hammer:18,magic_wand:20", "47|0|coin:12000,hammer:6,magic_wand:14", "48|0|coin:8000,infinite_health:10800", "49|0|hammer:3,freeze:8", "50|0|freeze:12,magic_wand:8", "51|0|hammer:3,freeze:15,magic_wand:12", ]); }); 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], ); }); 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" }); 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("a locked paid tier cannot be claimed", async () => { reset({ _id: "u1", openid: "o1", token: "secret" }); 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, 1); 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", }); 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("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 and token/usersAd routing follows backend conventions", 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" }); await jungleTreasure({ body: { action: "read", uid: "u1", token: "secret", gameName: "iaa" }, }); assert.equal(databaseState.collections[0], "usersAd"); });