server/laf-cloud/tests/wucai-transfer.test.mjs
2026-09-01 16:04:15 +08:00

308 lines
8.6 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/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: [],
};
globalThis.__wucaiTransferCloudMock = {
database() {
return {
collection(name) {
state.collections.push(name);
return {
where(query) {
return {
async getOne() {
return { data: 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 };
},
};
},
};
},
};
},
};
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;
}
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);
});