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 testAdminUrl = new URL("../functions/signInTestAdmin.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: [], lastUserUpdate: null, }; 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)); if (name === "users") state.lastUserUpdate = structuredClone(payload); targets.forEach(item => { if (item.signInActivity === null && payload.signInActivity && typeof payload.signInActivity === "object") { throw new Error("Cannot create field 'activatedAt' in element {signInActivity: null}"); } Object.assign(item, structuredClone(payload)); }); return { updated: targets.length }; }, async remove() { if (name !== "sign_in_activity_claims") throw new Error("unexpected remove"); const kept = state.claims.filter(item => !matches(item, query)); const deleted = state.claims.length - kept.length; state.claims = kept; return { deleted }; }, }; }, }; }, }; }, }; 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 }; } if (specifier === "@/signInClaim") { return { url: claimUrl.href, shortCircuit: true }; } return nextResolve(specifier, context); }, }); const { default: activityInfo } = await import(infoUrl); const { default: claimReward } = await import(claimUrl); const { default: signInTestAdmin } = await import(testAdminUrl); 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", levelAmount: 0, userLevel: 1, ...userOverrides, }; state.configs = [structuredClone(config)]; state.claims = []; state.lastUserUpdate = null; } 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(testAdminUrl), 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); assert.equal(existsSync(new URL("../functions/signInTestAdmin.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 uses levelAmount and returns false before the trigger level", async () => { reset({ levelAmount: 9, endLevelNum: 999 }); 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({ levelAmount: 10, endLevelNum: 0 }); 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(result.data.todayClaimed, false); assert.equal(result.data.canClaim, true); assert.equal(result.data.claimedCount, 0); assert.equal(result.data.nextRewardDay, 1); assert.equal(result.data.rewards[0].claimed, false); assert.equal(state.claims.length, 1); assert.equal(result.data.todayClaim.rewardDay, 1); assert.deepEqual(result.data.todayClaim.rewards, config.rewards[0].items); assert.equal(typeof state.lastUserUpdate.signInActivity, "string"); assert.equal(JSON.parse(state.lastUserUpdate.signInActivity).startAt, getChinaDayStart(now)); }); test("activity info activates when the persisted activity state is null", async () => { const now = Date.UTC(2026, 8, 2, 1); reset({ levelAmount: 10, signInActivity: null }); const result = await withNow(now, () => activityInfo({ body: { uid: "u1", token: "secret" } })); assert.equal(result.code, 1); assert.equal(result.data.status, "active"); assert.equal(typeof state.lastUserUpdate.signInActivity, "string"); assert.equal(JSON.parse(state.lastUserUpdate.signInActivity).activatedAt, now); }); test("automatic claims return the same receipt on retry and advance on a later day", async () => { const now = Date.UTC(2026, 8, 2, 1); reset({ levelAmount: 10 }); const request = () => activityInfo({ body: { uid: "u1", token: "secret" } }); const first = await withNow(now, request); const retry = await withNow(now + 1000, request); assert.deepEqual(retry.data.todayClaim, first.data.todayClaim); assert.equal(retry.data.claimedCount, 1); assert.equal(retry.data.canClaim, false); assert.equal(retry.data.todayClaimed, true); assert.equal(retry.data.rewards[0].claimed, true); assert.equal(retry.data.startAt, first.data.startAt); const later = await withNow(now + 2 * 86400000, request); assert.equal(later.data.todayClaim.rewardDay, 2); assert.notEqual(later.data.todayClaim.claimId, first.data.todayClaim.claimId); assert.equal(state.claims.length, 2); }); test("concurrent automatic and manual claims share the same daily unique record", async () => { const now = Date.UTC(2026, 8, 2, 1); reset({ levelAmount: 10, signInActivity: { activityId: config.activityId, startAt: getChinaDayStart(now), endAt: getChinaDayStart(now) + 14 * 86400000, activatedAt: now, } }); const results = await withNow(now, () => Promise.all([ activityInfo({ body: { uid: "u1", token: "secret" } }), activityInfo({ body: { uid: "u1", token: "secret" } }), claimReward({ body: { uid: "u1", token: "secret" } }), ])); assert.equal(state.claims.length, 1); for (const result of results.slice(0, 2)) { assert.equal(result.code, 1); assert.equal(result.data.canClaim, true); assert.equal(result.data.todayClaim.claimId, state.claims[0].claimId); } }); test("invalid configuration cannot create an automatic claim", async () => { reset({ levelAmount: 10 }); state.configs = []; const result = await activityInfo({ body: { uid: "u1", token: "secret" } }); assert.equal(result.code, 0); assert.equal(state.user.signInActivity, undefined); assert.equal(state.claims.length, 0); }); test("claims accumulate across non-consecutive days and reject same-day repeats", async () => { const firstDay = Date.UTC(2026, 8, 2, 1); reset({ levelAmount: 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({ levelAmount: 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({ levelAmount: 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({ levelAmount: 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 JSON.parse(state.user.signInActivity).completedAt, "number"); }); test("invalid token and usersAd requests are rejected without writes", async () => { reset({ levelAmount: 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); }); test("test admin is disabled without the server-side admin token", async () => { reset({ levelAmount: 10 }); const oldToken = process.env.SIGN_IN_TEST_ADMIN_TOKEN; delete process.env.SIGN_IN_TEST_ADMIN_TOKEN; try { const result = await signInTestAdmin({ body: { action: "inspect", uid: "u1", adminToken: "guess" }, query: {}, }); assert.equal(result.code, 0); assert.match(result.msg, /未启用|密钥错误/); } finally { if (oldToken === undefined) delete process.env.SIGN_IN_TEST_ADMIN_TOKEN; else process.env.SIGN_IN_TEST_ADMIN_TOKEN = oldToken; } }); test("one account can be prepared for locked and eligible scenarios", async () => { reset({ levelAmount: 99, signInActivity: { activityId: "old", startAt: 1, endAt: 2, activatedAt: 1 } }); await withAdminToken(async adminToken => { const locked = await signInTestAdmin({ body: { action: "prepare", scenario: "locked", uid: "u1", adminToken }, query: {}, }); assert.equal(locked.code, 1); assert.equal(state.user.levelAmount, 9); assert.equal(state.user.signInActivity, null); const eligible = await signInTestAdmin({ body: { action: "prepare", scenario: "eligible", uid: "u1", adminToken }, query: {}, }); assert.equal(eligible.code, 1); assert.equal(state.user.levelAmount, 10); assert.equal(state.user.signInActivity, null); const info = await activityInfo({ body: { uid: "u1", token: "secret" } }); assert.equal(info.data.status, "active"); }); }); test("test admin prepares every reward day for the same account", async () => { const now = Date.UTC(2026, 8, 4, 2); reset({ levelAmount: 10 }); await withNow(now, () => withAdminToken(async adminToken => { for (let day = 1; day <= 7; day++) { const prepared = await signInTestAdmin({ body: { action: "prepare", scenario: `day_${day}_ready`, uid: "u1", adminToken }, query: {}, }); assert.equal(prepared.code, 1); assert.equal(prepared.data.claimedCount, day - 1); const claim = await activityInfo({ body: { uid: "u1", token: "secret" } }); assert.equal(claim.code, 1); assert.equal(claim.data.todayClaim.rewardDay, day); assert.equal(claim.data.canClaim, true); assert.equal(claim.data.status, "active"); assert.equal(claim.data.claimedCount, day - 1); assert.equal(claim.data.nextRewardDay, day); assert.deepEqual(claim.data.rewards.map(reward => reward.claimed), config.rewards.map(reward => reward.day < day)); assert.equal(state.claims.length, day); const repeated = await activityInfo({ body: { uid: "u1", token: "secret" } }); assert.equal(repeated.data.canClaim, false); assert.equal(repeated.data.rewards[day - 1].claimed, true); assert.equal(repeated.data.status, day === 7 ? "completed" : "active"); } })); }); test("test admin prepares claimed, expired, and completed states", async () => { const now = Date.UTC(2026, 8, 4, 2); reset({ levelAmount: 10 }); await withNow(now, () => withAdminToken(async adminToken => { for (const expected of [ { scenario: "claimed_today", status: "active", claimMessage: /今天已经领取/ }, { scenario: "expired", status: "expired", claimMessage: /活动已结束/ }, { scenario: "completed", status: "completed", claimMessage: /全部领取/ }, ]) { const prepared = await signInTestAdmin({ body: { action: "prepare", scenario: expected.scenario, uid: "u1", adminToken }, query: {}, }); assert.equal(prepared.code, 1); const countBeforeInfo = state.claims.length; const info = await activityInfo({ body: { uid: "u1", token: "secret" } }); assert.equal(info.data.status, expected.status); assert.equal(state.claims.length, countBeforeInfo); const claim = await claimReward({ body: { uid: "u1", token: "secret" } }); assert.equal(claim.code, 0); assert.match(claim.msg, expected.claimMessage); } })); }); async function withAdminToken(callback) { const oldToken = process.env.SIGN_IN_TEST_ADMIN_TOKEN; process.env.SIGN_IN_TEST_ADMIN_TOKEN = "test-admin-secret"; try { return await callback("test-admin-secret"); } finally { if (oldToken === undefined) delete process.env.SIGN_IN_TEST_ADMIN_TOKEN; else process.env.SIGN_IN_TEST_ADMIN_TOKEN = oldToken; } }