server/laf-cloud/tests/wucai-transfer.test.mjs

439 lines
14 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";
import { isDeepStrictEqual } from "node:util";
const functionUrl = new URL("../functions/wucaiTransfer.ts", import.meta.url);
const configUrl = new URL("../functions/wucaiTransfer.yaml", import.meta.url);
const migrationUrl = new URL("../functions/wucaiMigration.ts", import.meta.url);
const state = {
user: null,
source: null,
sourceFails: false,
fetchCalls: 0,
collections: [],
updates: [],
beforeWrite: null,
};
globalThis.__wucaiTransferCloudMock = {
database() {
return {
collection(name) {
state.collections.push(name);
return {
where(query) {
return {
async getOne() {
return { data: structuredClone(state.user) };
},
async update(payload) {
if (query.wucai_type !== undefined
&& Number(state.user?.wucai_type) !== Number(query.wucai_type)) {
return { updated: 0 };
}
if (query.wucai_reward_popup_pending !== undefined
&& state.user?.wucai_reward_popup_pending !== query.wucai_reward_popup_pending) {
return { updated: 0 };
}
state.updates.push(payload);
Object.assign(state.user, payload);
return { updated: 1 };
},
};
},
};
},
};
},
mongo: { db: { collection() { return {
async updateOne(query, update) {
if (state.beforeWrite) {
const beforeWrite = state.beforeWrite;
state.beforeWrite = null;
beforeWrite();
}
const matches = Object.entries(query).every(([key, value]) =>
value && typeof value === "object" && "$exists" in value
? Object.hasOwn(state.user, key) === value.$exists
: isDeepStrictEqual(state.user[key], value));
if (!matches) return { matchedCount: 0 };
state.updates.push(update.$set);
Object.assign(state.user, structuredClone(update.$set));
return { matchedCount: 1 };
},
}; } } },
};
globalThis.__wucaiTransferUtilsMock = {
checkToken(received, expected) {
return received === expected;
},
};
globalThis.fetch = async () => {
state.fetchCalls++;
await new Promise(resolve => setImmediate(resolve));
if (state.sourceFails) {
throw new Error("source unavailable");
}
return {
ok: true,
async json() {
return { data: state.source };
},
};
};
registerHooks({
resolve(specifier, context, nextResolve) {
if (specifier === "@lafjs/cloud") {
return {
url: "data:text/javascript,export default globalThis.__wucaiTransferCloudMock",
shortCircuit: true,
};
}
if (specifier === "@/Utils") {
return {
url: "data:text/javascript,export default globalThis.__wucaiTransferUtilsMock",
shortCircuit: true,
};
}
if (specifier === "./wucaiMigration") {
return { url: migrationUrl.href, shortCircuit: true };
}
return nextResolve(specifier, context);
},
});
const { default: wucaiTransfer } = await import(functionUrl);
function reset(user, source = null) {
state.user = { ...user };
state.source = source;
state.sourceFails = false;
state.fetchCalls = 0;
state.collections.length = 0;
state.updates.length = 0;
state.beforeWrite = null;
}
test("wucai transfer cloud function is POST only", async () => {
assert.equal(existsSync(functionUrl), true);
assert.equal(existsSync(configUrl), true);
const yaml = await readFile(configUrl, "utf8");
assert.match(yaml, /^name:\s*wucaiTransfer$/m);
assert.match(yaml, /methods:\s*\n\s*- POST/m);
assert.doesNotMatch(yaml, /- GET/);
});
test("choose_wucai uses the latest source and adds the reward once", async () => {
reset({
_id: "u1",
token: "secret",
unionid: "union-1",
wucai_type: 1,
coinAmount: 999,
}, {
levelAmount: 8,
coinAmount: 100,
healthAmount: 2,
freezeAmount: 1,
hammerAmount: 2,
magicAmount: 3,
monthCardTime: 999,
pay_user: true,
});
const result = await wucaiTransfer({
body: { uid: "u1", token: "secret", action: "choose_wucai" },
});
assert.equal(result.code, 1);
assert.equal(state.fetchCalls, 1);
assert.equal(state.user.levelAmount, 8);
assert.equal(state.user.coinAmount, 2100);
assert.equal(state.user.freezeAmount, 4);
assert.equal(state.user.hammerAmount, 5);
assert.equal(state.user.magicAmount, 6);
assert.equal(state.user.pay_user, true);
assert.equal(state.user.wucai_type, 2);
assert.equal(state.user.wucai_reward_popup_pending, true);
assert.equal(state.user.forcedUpdate, true);
});
test("claim_reward is idempotent", async () => {
reset({
_id: "u1",
token: "secret",
wucai_type: 0,
coinAmount: 10,
freezeAmount: 1,
hammerAmount: 2,
magicAmount: 3,
});
const first = await wucaiTransfer({
body: { uid: "u1", token: "secret", action: "claim_reward" },
});
const retry = await wucaiTransfer({
body: { uid: "u1", token: "secret", action: "claim_reward" },
});
assert.equal(first.code, 1);
assert.equal(retry.code, 1);
assert.deepEqual(
{
coinAmount: first.data.coinAmount,
freezeAmount: first.data.freezeAmount,
hammerAmount: first.data.hammerAmount,
magicAmount: first.data.magicAmount,
},
{ coinAmount: 2010, freezeAmount: 4, hammerAmount: 5, magicAmount: 6 }
);
assert.deepEqual(
{
coinAmount: retry.data.coinAmount,
freezeAmount: retry.data.freezeAmount,
hammerAmount: retry.data.hammerAmount,
magicAmount: retry.data.magicAmount,
},
{ coinAmount: 2010, freezeAmount: 4, hammerAmount: 5, magicAmount: 6 }
);
assert.equal(state.updates.length, 1);
assert.equal(state.user.coinAmount, 2010);
assert.equal(state.user.freezeAmount, 4);
assert.equal(state.user.wucai_type, 2);
assert.equal(state.user.wucai_reward_popup_pending, false);
});
test("choose_current keeps current progress and adds the reward", async () => {
reset({
_id: "u1",
token: "secret",
unionid: "union-1",
wucai_type: 1,
levelAmount: 22,
coinAmount: 50,
freezeAmount: 5,
hammerAmount: 6,
magicAmount: 7,
});
const result = await wucaiTransfer({
body: { uid: "u1", token: "secret", action: "choose_current" },
});
assert.equal(result.code, 1);
assert.equal(state.fetchCalls, 0);
assert.equal(state.user.levelAmount, 22);
assert.equal(state.user.coinAmount, 2050);
assert.equal(state.user.wucai_type, 3);
assert.equal(state.user.wucai_reward_popup_pending, true);
assert.equal(state.user.forcedUpdate, true);
});
test("reward popup acknowledgement is idempotent and never grants resources", async () => {
reset({
_id: "u1",
token: "secret",
wucai_type: 3,
wucai_reward_popup_pending: true,
coinAmount: 2050,
freezeAmount: 8,
hammerAmount: 9,
magicAmount: 10,
});
const first = await wucaiTransfer({
body: { uid: "u1", token: "secret", action: "ack_reward_popup" },
});
const retry = await wucaiTransfer({
body: { uid: "u1", token: "secret", action: "ack_reward_popup" },
});
assert.equal(first.code, 1);
assert.equal(retry.code, 1);
assert.equal(state.updates.length, 1);
assert.equal(state.user.wucai_reward_popup_pending, false);
assert.equal(state.user.coinAmount, 2050);
assert.equal(state.user.freezeAmount, 8);
assert.equal(state.user.hammerAmount, 9);
assert.equal(state.user.magicAmount, 10);
});
test("source failure leaves a pending choice unchanged", async () => {
reset({
_id: "u1",
token: "secret",
unionid: "union-1",
wucai_type: 1,
});
state.sourceFails = true;
const result = await wucaiTransfer({
body: { uid: "u1", token: "secret", action: "choose_wucai" },
});
assert.equal(result.code, 0);
assert.equal(result.errorCode, "WUCAI_SOURCE_UNAVAILABLE");
assert.equal(state.user.wucai_type, 1);
assert.deepEqual(state.updates, []);
});
test("strict token validation and usersAd rejection happen before writes", async () => {
reset({ _id: "u1", token: "secret", wucai_type: 0 });
const tokenResult = await wucaiTransfer({
body: { uid: "u1", action: "claim_reward" },
});
const iaaResult = await wucaiTransfer({
body: { uid: "u1", token: "secret", action: "claim_reward", gameName: "iaa" },
});
assert.equal(tokenResult.errorCode, "TOKEN_INVALID");
assert.equal(iaaResult.errorCode, "WUCAI_UNSUPPORTED_GAME");
assert.deepEqual(state.updates, []);
});
test("concurrent conflicting choices produce only one terminal state", async () => {
reset({
_id: "u1",
token: "secret",
unionid: "union-1",
wucai_type: 1,
coinAmount: 10,
freezeAmount: 3,
hammerAmount: 3,
magicAmount: 3,
}, {
levelAmount: 0,
coinAmount: 100,
freezeAmount: 1,
hammerAmount: 1,
magicAmount: 1,
});
const [wucaiResult, currentResult] = await Promise.all([
wucaiTransfer({ body: { uid: "u1", token: "secret", action: "choose_wucai" } }),
wucaiTransfer({ body: { uid: "u1", token: "secret", action: "choose_current" } }),
]);
assert.equal(currentResult.code, 1);
assert.equal(wucaiResult.errorCode, "WUCAI_STATE_CHANGED");
assert.equal(state.user.wucai_type, 3);
assert.equal(state.updates.length, 1);
});
function repeatUser(extra = {}) {
return {
_id: "u1", token: "secret", unionid: "union-1", wucai_type: 1,
wucai_entry_id: "entry-2", wucai_reward_granted: true,
wucai_reward_popup_pending: false,
coinAmount: 3000, freezeAmount: 8, hammerAmount: 9, magicAmount: 10,
...extra,
};
}
function transfer(action, entry = "entry-2") {
return wucaiTransfer({ body: {
uid: "u1", token: "secret", action, wucai_entry_id: entry,
} });
}
test("repeat choices replace resources without awarding or reopening a confirmed popup", async () => {
reset(repeatUser(), { levelAmount: 4, coinAmount: 100, freezeAmount: 1, hammerAmount: 2, magicAmount: 3 });
const first = await transfer("choose_wucai");
const retry = await transfer("choose_wucai");
assert.equal(first.code, 1);
assert.equal(retry.code, 1);
assert.equal(state.fetchCalls, 1);
assert.equal(state.user.coinAmount, 100);
assert.equal(state.user.freezeAmount, 1);
assert.equal(state.user.wucai_reward_granted, true);
assert.equal(state.user.wucai_reward_popup_pending, false);
assert.equal(first.data.wucai_entry_id, "entry-2");
assert.equal(first.data.wucai_reward_granted, true);
state.user.wucai_type = 1;
state.user.wucai_entry_id = "entry-3";
assert.equal((await transfer("choose_current", "entry-3")).code, 1);
assert.equal(state.user.coinAmount, 100);
assert.equal(state.user.wucai_reward_popup_pending, false);
});
test("an unconfirmed reward popup survives a later choice without a second grant", async () => {
reset(repeatUser({ wucai_reward_popup_pending: true }));
const result = await transfer("choose_current");
assert.equal(result.code, 1);
assert.equal(state.user.coinAmount, 3000);
assert.equal(result.data.wucai_reward_popup_pending, true);
assert.equal((await transfer("ack_reward_popup")).code, 1);
assert.equal(state.user.wucai_reward_popup_pending, false);
assert.equal(state.user.wucai_reward_granted, true);
});
test("a still-unclaimed auto migration grants once when the next entry chooses", async () => {
reset(repeatUser({ wucai_reward_granted: false }));
const [first, retry] = await Promise.all([transfer("choose_current"), transfer("choose_current")]);
assert.equal(first.code, 1);
assert.equal(retry.code, 1);
assert.equal(state.user.coinAmount, 5000);
assert.equal(state.user.wucai_reward_granted, true);
assert.equal(state.user.wucai_reward_popup_pending, true);
assert.equal(state.updates.length, 1);
});
test("new rounds reject missing or stale entry identifiers before reading the source", async () => {
reset(repeatUser(), { levelAmount: 4 });
for (const entry of ["", "entry-1"]) {
for (const action of ["choose_wucai", "choose_current", "ack_reward_popup"]) {
assert.equal((await transfer(action, entry)).errorCode, "WUCAI_STATE_CHANGED");
}
}
assert.equal(state.fetchCalls, 0);
assert.equal(state.updates.length, 0);
});
test("a delayed claim retry reports the current round without granting or completing it", async () => {
reset(repeatUser());
const result = await transfer("claim_reward", "entry-1");
assert.equal(result.code, 1);
assert.equal(result.data.wucai_type, 1);
assert.equal(result.data.wucai_entry_id, "entry-2");
assert.equal(state.user.coinAmount, 3000);
assert.equal(state.updates.length, 0);
reset(repeatUser({ wucai_reward_granted: false }));
assert.equal((await transfer("claim_reward", "entry-1")).errorCode, "WUCAI_STATE_CHANGED");
assert.equal(state.user.wucai_reward_granted, false);
});
test("month card claims retain the newer timestamp while expiry still follows the chosen source", async () => {
for (const [current, source, expected] of [[1000, 500, 1000], [500, 1000, 1000]]) {
reset(repeatUser({ monthCardReward: current, monthCardTime: 9999 }), {
levelAmount: 1, monthCardReward: source, monthCardTime: 99,
});
assert.equal((await transfer("choose_wucai")).code, 1);
assert.equal(state.user.monthCardReward, expected);
assert.equal(state.user.monthCardTime, 99);
}
});
test("an in-flight old choice cannot write after a new round starts", async () => {
reset(repeatUser(), { levelAmount: 4, coinAmount: 100 });
state.beforeWrite = () => { state.user.wucai_entry_id = "entry-3"; };
assert.equal((await transfer("choose_wucai")).errorCode, "WUCAI_STATE_CHANGED");
assert.equal(state.user.wucai_entry_id, "entry-3");
assert.equal(state.user.wucai_type, 1);
assert.equal(state.user.coinAmount, 3000);
});
test("a concurrent monthly claim is not overwritten by an older imported timestamp", async () => {
reset(repeatUser({ monthCardReward: 500 }), { levelAmount: 1, monthCardReward: 1000 });
state.beforeWrite = () => { state.user.monthCardReward = 2000; };
assert.equal((await transfer("choose_wucai")).errorCode, "WUCAI_STATE_CHANGED");
assert.equal(state.user.monthCardReward, 2000);
assert.equal(state.user.wucai_type, 1);
});