server/laf-cloud/tests/sign-in-activity.test.mjs

286 lines
10 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 infoUrl = new URL("../functions/signInActivityInfo.ts", import.meta.url);
const claimUrl = new URL("../functions/signInClaim.ts", import.meta.url);
const levelUrl = new URL("../functions/setUserLevel.ts", import.meta.url);
const domainUrl = new URL("../functions/SignInActivity.ts", import.meta.url);
const productionConfigUrl = new URL("../sign-in-activity-config.json", import.meta.url);
const state = {
user: null,
configs: [],
claims: [],
};
globalThis.__signInCloudMock = {
database() {
return {
collection(name) {
return {
async add(payload) {
if (name !== "sign_in_activity_claims") throw new Error("unexpected add");
if (state.claims.some(item => item._id === payload._id)) throw new Error("duplicate key");
state.claims.push(structuredClone(payload));
return { ok: true };
},
where(query) {
return {
async get() {
return { data: getCollection(name).filter(item => matches(item, query)) };
},
async getOne() {
return { data: getCollection(name).find(item => matches(item, query)) || null };
},
async update(payload) {
const targets = getCollection(name).filter(item => matches(item, query));
targets.forEach(item => Object.assign(item, structuredClone(payload)));
return { updated: targets.length };
},
};
},
};
},
};
},
};
globalThis.__signInUtilsMock = {
checkToken(received, expected) {
return received === expected;
},
};
registerHooks({
resolve(specifier, context, nextResolve) {
if (specifier === "@lafjs/cloud") {
return { url: "data:text/javascript,export default globalThis.__signInCloudMock", shortCircuit: true };
}
if (specifier === "@/Utils") {
return { url: "data:text/javascript,export default globalThis.__signInUtilsMock", shortCircuit: true };
}
if (specifier === "@/SignInActivity") {
return { url: domainUrl.href, shortCircuit: true };
}
return nextResolve(specifier, context);
},
});
const { default: activityInfo } = await import(infoUrl);
const { default: claimReward } = await import(claimUrl);
const { default: setUserLevel } = await import(levelUrl);
const { getChinaDayStart, isValidSignInConfig } = await import(domainUrl);
const config = {
activityId: "sign_in_v1",
enabled: true,
triggerLevel: 10,
durationDays: 14,
timezone: "UTC+8",
rewards: Array.from({ length: 7 }, (_, index) => ({
day: index + 1,
items: [{ type: "coin", count: (index + 1) * 100 }],
})),
updatedAt: 1,
};
function reset(userOverrides = {}) {
state.user = {
_id: "u1",
token: "secret",
endLevelNum: 0,
userLevel: 1,
...userOverrides,
};
state.configs = [structuredClone(config)];
state.claims = [];
}
function getCollection(name) {
if (name === "users") return state.user ? [state.user] : [];
if (name === "sign_in_activity_config") return state.configs;
if (name === "sign_in_activity_claims") return state.claims;
if (name === "usersAd") return [];
return [];
}
function matches(item, query) {
return Object.entries(query || {}).every(([key, expected]) => item?.[key] === expected);
}
function withNow(now, callback) {
const original = Date.now;
Date.now = () => now;
return Promise.resolve(callback()).finally(() => { Date.now = original; });
}
test("cloud functions and yaml files exist", () => {
assert.equal(existsSync(infoUrl), true);
assert.equal(existsSync(claimUrl), true);
assert.equal(existsSync(new URL("../functions/signInActivityInfo.yaml", import.meta.url)), true);
assert.equal(existsSync(new URL("../functions/signInClaim.yaml", import.meta.url)), true);
});
test("production configuration contains the approved level and rewards", async () => {
const productionConfig = JSON.parse(await readFile(productionConfigUrl, "utf8"));
assert.equal(isValidSignInConfig(productionConfig), true);
assert.equal(productionConfig.triggerLevel, 23);
assert.deepEqual(productionConfig.rewards, [
{ day: 1, items: [{ type: "infinite_health", count: 900 }] },
{ day: 2, items: [{ type: "hammer", count: 1 }] },
{ day: 3, items: [{ type: "infinite_health", count: 1800 }] },
{ day: 4, items: [{ type: "freeze", count: 1 }, { type: "magic_wand", count: 1 }] },
{ day: 5, items: [{ type: "coin", count: 600 }] },
{ day: 6, items: [{ type: "hammer", count: 1 }, { type: "freeze", count: 1 }, { type: "magic_wand", count: 1 }] },
{ day: 7, items: [{ type: "infinite_health", count: 3600 }, { type: "cat_skin", count: 1, itemId: 12 }] },
]);
});
test("activity info returns false before the trigger level", async () => {
reset({ endLevelNum: 9 });
const result = await activityInfo({ body: { uid: "u1", token: "secret" } });
assert.equal(result.code, 1);
assert.equal(result.data, false);
assert.equal(state.user.signInActivity, undefined);
});
test("activity info activates an eligible player and returns rewards", async () => {
const now = Date.UTC(2026, 8, 2, 1);
reset({ endLevelNum: 10 });
const result = await withNow(now, () => activityInfo({ body: { uid: "u1", token: "secret" } }));
assert.equal(result.code, 1);
assert.equal(result.data.status, "active");
assert.equal(result.data.triggerLevel, 10);
assert.equal(result.data.triggerReached, true);
assert.equal(result.data.rewards.length, 7);
assert.equal(state.user.signInActivity.startAt, getChinaDayStart(now));
});
test("reaching the target level activates a fourteen-day activity", async () => {
const now = Date.UTC(2026, 8, 2, 15, 59, 59);
reset({ endLevelNum: 9 });
const result = await withNow(now, () => setUserLevel({
body: { uid: "u1", token: "secret", action: "save", userLevel: 1, endLevelNum: 10 },
}));
assert.equal(result.code, 1);
assert.equal(result.data.signInActivityActivated, true);
assert.equal(state.user.signInActivity.startAt, getChinaDayStart(now));
assert.equal(state.user.signInActivity.endAt, getChinaDayStart(now) + 14 * 24 * 60 * 60 * 1000);
const repeated = await withNow(now + 1000, () => setUserLevel({
body: { uid: "u1", token: "secret", action: "save", userLevel: 1, endLevelNum: 11 },
}));
assert.equal(repeated.data.signInActivityActivated, false);
assert.equal(state.user.signInActivity.startAt, getChinaDayStart(now));
});
test("claims accumulate across non-consecutive days and reject same-day repeats", async () => {
const firstDay = Date.UTC(2026, 8, 2, 1);
reset({
endLevelNum: 10,
signInActivity: {
activityId: config.activityId,
startAt: getChinaDayStart(firstDay),
endAt: getChinaDayStart(firstDay) + 14 * 24 * 60 * 60 * 1000,
activatedAt: firstDay,
completedAt: null,
},
});
const first = await withNow(firstDay, () => claimReward({ body: { uid: "u1", token: "secret" } }));
assert.equal(first.code, 1);
assert.equal(first.data.rewardDay, 1);
const duplicate = await withNow(firstDay + 1000, () => claimReward({ body: { uid: "u1", token: "secret" } }));
assert.equal(duplicate.code, 0);
assert.match(duplicate.msg, /今天已经领取/);
const thirdDay = await withNow(firstDay + 2 * 24 * 60 * 60 * 1000, () => claimReward({
body: { uid: "u1", token: "secret" },
}));
assert.equal(thirdDay.code, 1);
assert.equal(thirdDay.data.rewardDay, 2);
assert.equal(state.claims.length, 2);
});
test("concurrent same-day claims create only one claim record", async () => {
const firstDay = Date.UTC(2026, 8, 2, 1);
reset({
endLevelNum: 10,
signInActivity: {
activityId: config.activityId,
startAt: getChinaDayStart(firstDay),
endAt: getChinaDayStart(firstDay) + 14 * 24 * 60 * 60 * 1000,
activatedAt: firstDay,
completedAt: null,
},
});
const results = await withNow(firstDay, () => Promise.all([
claimReward({ body: { uid: "u1", token: "secret" } }),
claimReward({ body: { uid: "u1", token: "secret" } }),
]));
assert.deepEqual(results.map(result => result.code).sort(), [0, 1]);
assert.equal(state.claims.length, 1);
});
test("the activity expires exactly at UTC+8 midnight after day fourteen", async () => {
const startAt = Date.UTC(2026, 8, 1, 16);
reset({
endLevelNum: 10,
signInActivity: {
activityId: config.activityId,
startAt,
endAt: startAt + 14 * 24 * 60 * 60 * 1000,
activatedAt: startAt,
completedAt: null,
},
});
const beforeEnd = await withNow(state.user.signInActivity.endAt - 1, () => activityInfo({
body: { uid: "u1", token: "secret" },
}));
assert.equal(beforeEnd.data.status, "active");
const atEnd = await withNow(state.user.signInActivity.endAt, () => activityInfo({
body: { uid: "u1", token: "secret" },
}));
assert.equal(atEnd.data.status, "expired");
assert.equal(atEnd.data.canClaim, false);
});
test("seven different days complete the activity", async () => {
const firstDay = Date.UTC(2026, 8, 2, 1);
reset({
endLevelNum: 10,
signInActivity: {
activityId: config.activityId,
startAt: getChinaDayStart(firstDay),
endAt: getChinaDayStart(firstDay) + 14 * 24 * 60 * 60 * 1000,
activatedAt: firstDay,
completedAt: null,
},
});
let result;
for (let day = 0; day < 7; day++) {
result = await withNow(firstDay + day * 24 * 60 * 60 * 1000, () => claimReward({
body: { uid: "u1", token: "secret" },
}));
}
assert.equal(result.code, 1);
assert.equal(result.data.completed, true);
assert.equal(result.data.claimedCount, 7);
assert.equal(typeof state.user.signInActivity.completedAt, "number");
});
test("invalid token and usersAd requests are rejected without writes", async () => {
reset({ endLevelNum: 10 });
const badToken = await activityInfo({ body: { uid: "u1", token: "wrong" } });
assert.equal(badToken.code, 0);
const usersAd = await activityInfo({ body: { uid: "u1", token: "secret", gameName: "iaa" } });
assert.equal(usersAd.code, 0);
assert.equal(state.claims.length, 0);
assert.equal(state.user.signInActivity, undefined);
});