151 lines
6.2 KiB
JavaScript
151 lines
6.2 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { registerHooks } from "node:module";
|
|
import { readFile } from "node:fs/promises";
|
|
|
|
const state = { users: [], calls: [], failUpdate: false };
|
|
const clone = value => structuredClone(value);
|
|
const valueAt = (row, path) => path.split(".").reduce((value, key) => value?.[key], row);
|
|
const matches = (row, query) => Object.entries(query).every(([key, value]) =>
|
|
value?.$ne !== undefined ? valueAt(row, key) !== value.$ne : valueAt(row, key) === value);
|
|
|
|
globalThis.__welfareCloudMock = {
|
|
database: () => ({
|
|
command: { neq: value => ({ $ne: value }) },
|
|
collection(name) {
|
|
assert.equal(name, "users");
|
|
return {
|
|
where(query) {
|
|
return {
|
|
async update(payload) {
|
|
state.calls.push({ query: clone(query), payload: clone(payload) });
|
|
// Yield before matching so concurrent callers all reach the database operation.
|
|
await Promise.resolve();
|
|
if (state.failUpdate) return { ok: false, updated: 0 };
|
|
const user = state.users.find(row => matches(row, query));
|
|
if (!user) return { ok: true, updated: 0 };
|
|
for (const [path, value] of Object.entries(payload)) {
|
|
const parts = path.split(".");
|
|
const key = parts.pop();
|
|
const target = parts.reduce((row, part) => row[part] ??= {}, user);
|
|
target[key] = clone(value);
|
|
}
|
|
return { ok: true, updated: 1 };
|
|
},
|
|
async getOne() {
|
|
return { data: clone(state.users.find(row => matches(row, query)) ?? null) };
|
|
},
|
|
};
|
|
},
|
|
};
|
|
},
|
|
}),
|
|
};
|
|
|
|
registerHooks({
|
|
resolve(specifier, context, nextResolve) {
|
|
if (specifier === "@lafjs/cloud") {
|
|
return { url: "data:text/javascript,export default globalThis.__welfareCloudMock", shortCircuit: true };
|
|
}
|
|
return nextResolve(specifier, context);
|
|
},
|
|
});
|
|
const { default: record } = await import("../functions/miniProgramWelfare.ts");
|
|
|
|
function reset(extra = {}) {
|
|
state.users = [{
|
|
_id: "u1", token: "secret", coinAmount: 123,
|
|
freezeAmount: 4, hammerAmount: 5, magicAmount: 6, ...extra,
|
|
}, { _id: "u2", token: "other" }];
|
|
state.calls = [];
|
|
state.failUpdate = false;
|
|
}
|
|
const call = (extra = {}) => record({ body: { uid: "u1", token: "secret", type: "favorite", ...extra } });
|
|
|
|
test("legacy user: record each reward independently without awarding resources", async () => {
|
|
for (const firstType of ["favorite", "desktop"]) {
|
|
reset();
|
|
const before = clone(state.users);
|
|
const first = await call({ type: firstType });
|
|
assert.equal(first.code, 1);
|
|
assert.deepEqual(first.data, {
|
|
miniProgramWelfare: { favorite: firstType === "favorite", desktop: firstType === "desktop" },
|
|
changed: true,
|
|
});
|
|
const second = await call({ type: firstType === "favorite" ? "desktop" : "favorite" });
|
|
assert.deepEqual(second.data, { miniProgramWelfare: { favorite: true, desktop: true }, changed: true });
|
|
const { miniProgramWelfare, ...unchanged } = state.users[0];
|
|
assert.deepEqual(miniProgramWelfare, { [firstType]: true, [firstType === "favorite" ? "desktop" : "favorite"]: true });
|
|
assert.deepEqual(unchanged, before[0]);
|
|
assert.deepEqual(state.users[1], before[1]);
|
|
}
|
|
});
|
|
|
|
test("repeat calls succeed without resetting either reward or accepting client state", async () => {
|
|
reset({ miniProgramWelfare: { favorite: true, desktop: true } });
|
|
const before = clone(state.users);
|
|
const result = await call({ miniProgramWelfare: { favorite: false, desktop: false }, claimed: false });
|
|
assert.deepEqual(result.data, { miniProgramWelfare: { favorite: true, desktop: true }, changed: false });
|
|
assert.deepEqual(state.users, before);
|
|
});
|
|
|
|
test("simultaneous retries change the same reward exactly once", async () => {
|
|
reset({ miniProgramWelfare: { favorite: false, desktop: false } });
|
|
const results = await Promise.all(Array.from({ length: 20 }, () => call()));
|
|
assert.ok(results.every(result => result.code === 1 && result.data.miniProgramWelfare.favorite));
|
|
assert.equal(results.filter(result => result.data.changed).length, 1);
|
|
assert.deepEqual(state.users[0].miniProgramWelfare, { favorite: true, desktop: false });
|
|
});
|
|
|
|
test("simultaneous different rewards retain both marks for a legacy user", async () => {
|
|
reset();
|
|
const results = await Promise.all([call(), call({ type: "desktop" }), call(), call({ type: "desktop" })]);
|
|
assert.ok(results.every(result => result.code === 1));
|
|
assert.equal(results.filter(result => result.data.changed).length, 2);
|
|
assert.deepEqual(state.users[0].miniProgramWelfare, { favorite: true, desktop: true });
|
|
});
|
|
|
|
test("invalid inputs and unsupported game are rejected before any write", async () => {
|
|
for (const input of [
|
|
{ uid: "" }, { uid: { $ne: null } }, { uid: 1 },
|
|
{ token: "" }, { token: null }, { token: { $ne: null } },
|
|
{ type: "" }, { type: "reset" }, { type: "__proto__" }, { type: ["favorite"] },
|
|
{ gameName: "iaa" },
|
|
]) {
|
|
reset();
|
|
const before = clone(state.users);
|
|
assert.equal((await call(input)).code, 0);
|
|
assert.deepEqual(state.users, before);
|
|
assert.equal(state.calls.length, 0);
|
|
}
|
|
assert.equal((await record({})).code, 0);
|
|
});
|
|
|
|
test("missing users, missing stored tokens and mismatched tokens cannot write or read state", async () => {
|
|
for (const [extra, input] of [
|
|
[{}, { uid: "missing" }], [{}, { uid: "u2" }],
|
|
[{}, { token: "wrong" }], [{ token: undefined }, {}],
|
|
]) {
|
|
reset(extra);
|
|
const before = clone(state.users);
|
|
const result = await call(input);
|
|
assert.equal(result.code, 0);
|
|
assert.equal(result.data, null);
|
|
assert.deepEqual(state.users, before);
|
|
}
|
|
});
|
|
|
|
test("database update failures do not report success", async () => {
|
|
reset();
|
|
state.failUpdate = true;
|
|
const before = clone(state.users);
|
|
assert.equal((await call()).code, 0);
|
|
assert.deepEqual(state.users, before);
|
|
});
|
|
|
|
test("function configuration exposes POST only", async () => {
|
|
const yaml = await readFile(new URL("../functions/miniProgramWelfare.yaml", import.meta.url), "utf8");
|
|
assert.match(yaml, /^name: miniProgramWelfare$/m);
|
|
assert.match(yaml, /methods:\s*\n\s*- POST\s*\ntags:/);
|
|
});
|