server/laf-cloud/tests/jungle-treasure.test.mjs
guanchao e2a5c3d2f6 Squash local into main while preserving battle pass compatibility
Integrate starter pack timing, reactivation, purchase analytics, payment routing and atomic user IDs. Resolve iOS conflicts by preserving both battle pass delivery and starter pack snapshots. Add coexistence regression checks and record remaining compatibility risks. Validation: 118/125 tests passed; 7 existing failures remain, plus a separately reproduced pre-existing V2 task login failure.
2026-09-10 16:31:14 +08:00

392 lines
13 KiB
JavaScript

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,
"@/limitedTimeEvent": new URL("../functions/limitedTimeEvent.ts", import.meta.url).href,
"@/passCheckUpgrade": new URL("../functions/passCheckUpgrade.ts", import.meta.url).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");
});