MatchMaster/server/laf-cloud/tests/jungle-treasure.test.mjs
2026-08-07 10:59:22 +08:00

390 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,
};
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");
});