779 lines
27 KiB
JavaScript
779 lines
27 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 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);
|
|
});
|