439 lines
14 KiB
JavaScript
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);
|
|
});
|