fix(sign-in): 序列化签到状态并新增测试管理接口
This commit is contained in:
parent
92fd96a7fd
commit
ca1e9012c5
|
|
@ -146,7 +146,7 @@ export async function activateSignInActivity(user: any, now = Date.now()) {
|
||||||
};
|
};
|
||||||
const result = await db.collection(USERS_COLLECTION)
|
const result = await db.collection(USERS_COLLECTION)
|
||||||
.where({ _id: user._id })
|
.where({ _id: user._id })
|
||||||
.update({ signInActivity: state });
|
.update({ signInActivity: JSON.stringify(state) });
|
||||||
const activated = getUpdatedCount(result) > 0;
|
const activated = getUpdatedCount(result) > 0;
|
||||||
if (activated) {
|
if (activated) {
|
||||||
user.signInActivity = state;
|
user.signInActivity = state;
|
||||||
|
|
@ -176,7 +176,9 @@ export async function addSignInClaim(payload: any) {
|
||||||
|
|
||||||
export async function markSignInCompleted(uid: string, state: SignInActivityState, completedAt: number) {
|
export async function markSignInCompleted(uid: string, state: SignInActivityState, completedAt: number) {
|
||||||
const nextState = { ...state, completedAt };
|
const nextState = { ...state, completedAt };
|
||||||
await db.collection(USERS_COLLECTION).where({ _id: uid }).update({ signInActivity: nextState });
|
await db.collection(USERS_COLLECTION).where({ _id: uid }).update({
|
||||||
|
signInActivity: JSON.stringify(nextState),
|
||||||
|
});
|
||||||
return nextState;
|
return nextState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
180
laf-cloud/functions/signInTestAdmin.ts
Normal file
180
laf-cloud/functions/signInTestAdmin.ts
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
import cloud from '@lafjs/cloud'
|
||||||
|
import {
|
||||||
|
buildClaimId,
|
||||||
|
getActiveSignInConfig,
|
||||||
|
getChinaDateKey,
|
||||||
|
getChinaDayStart,
|
||||||
|
getSignInClaims,
|
||||||
|
normalizeSignInState,
|
||||||
|
} from "@/SignInActivity";
|
||||||
|
|
||||||
|
const db = cloud.database();
|
||||||
|
const USERS_COLLECTION = "users";
|
||||||
|
const CLAIM_COLLECTION = "sign_in_activity_claims";
|
||||||
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
export default async function (ctx: FunctionContext) {
|
||||||
|
const adminToken = ctx.body?.adminToken || ctx.query?.adminToken;
|
||||||
|
if (!checkAdminToken(adminToken)) {
|
||||||
|
return { code: 0, data: null, msg: "测试接口未启用或管理密钥错误" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const action = String(ctx.body?.action || ctx.query?.action || "inspect");
|
||||||
|
const uid = ctx.body?.uid || ctx.query?.uid;
|
||||||
|
if (!uid) {
|
||||||
|
return { code: 0, data: null, msg: "未获取到uid" };
|
||||||
|
}
|
||||||
|
const userResult = await db.collection(USERS_COLLECTION).where({ _id: uid }).getOne();
|
||||||
|
const user = userResult.data;
|
||||||
|
if (!user) {
|
||||||
|
return { code: 0, data: null, msg: "未获取到用户信息" };
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case "inspect":
|
||||||
|
return await inspectUser(user);
|
||||||
|
case "reset":
|
||||||
|
await clearClaims(uid);
|
||||||
|
await updateUser(uid, { signInActivity: null });
|
||||||
|
return await inspectUser({ ...user, signInActivity: null });
|
||||||
|
case "set_level":
|
||||||
|
return await setLevel(user, ctx.body?.levelAmount);
|
||||||
|
case "prepare":
|
||||||
|
return await prepareScenario(user, String(ctx.body?.scenario || ""));
|
||||||
|
default:
|
||||||
|
return { code: 0, data: null, msg: "无效action,请使用 inspect、reset、set_level 或 prepare" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkAdminToken(adminToken: string) {
|
||||||
|
const env = (globalThis as any).process?.env || {};
|
||||||
|
const expected = String(env.SIGN_IN_TEST_ADMIN_TOKEN || "");
|
||||||
|
return !!expected && !!adminToken && adminToken === expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setLevel(user: any, rawLevel: any) {
|
||||||
|
const levelAmount = Number(rawLevel);
|
||||||
|
if (!Number.isInteger(levelAmount) || levelAmount < 0) {
|
||||||
|
return { code: 0, data: null, msg: "levelAmount必须是大于等于0的整数" };
|
||||||
|
}
|
||||||
|
await updateUser(user._id, { levelAmount });
|
||||||
|
return await inspectUser({ ...user, levelAmount });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function prepareScenario(user: any, scenario: string) {
|
||||||
|
const config = await getActiveSignInConfig();
|
||||||
|
if (!config) {
|
||||||
|
return { code: 0, data: null, msg: "签到活动配置不存在或配置无效" };
|
||||||
|
}
|
||||||
|
const now = Date.now();
|
||||||
|
const triggerLevel = Number(config.triggerLevel);
|
||||||
|
|
||||||
|
if (scenario === "locked" || scenario === "eligible") {
|
||||||
|
const levelAmount = scenario === "locked" ? Math.max(0, triggerLevel - 1) : triggerLevel;
|
||||||
|
await clearClaims(user._id);
|
||||||
|
await updateUser(user._id, { levelAmount, signInActivity: null });
|
||||||
|
return await inspectUser({ ...user, levelAmount, signInActivity: null }, scenario);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dayReadyMatch = /^day_([1-7])_ready$/.exec(scenario);
|
||||||
|
if (dayReadyMatch) {
|
||||||
|
const claimedCount = Number(dayReadyMatch[1]) - 1;
|
||||||
|
return await prepareActivity(user, config, now, scenario, claimedCount, false, false);
|
||||||
|
}
|
||||||
|
if (scenario === "claimed_today") {
|
||||||
|
return await prepareActivity(user, config, now, scenario, 1, true, false);
|
||||||
|
}
|
||||||
|
if (scenario === "expired") {
|
||||||
|
return await prepareActivity(user, config, now, scenario, 0, false, true);
|
||||||
|
}
|
||||||
|
if (scenario === "completed") {
|
||||||
|
return await prepareActivity(user, config, now, scenario, 7, false, false, true);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
code: 0,
|
||||||
|
data: null,
|
||||||
|
msg: "无效scenario,请使用 locked、eligible、day_1_ready至day_7_ready、claimed_today、expired或completed",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function prepareActivity(
|
||||||
|
user: any,
|
||||||
|
config: any,
|
||||||
|
now: number,
|
||||||
|
scenario: string,
|
||||||
|
claimedCount: number,
|
||||||
|
todayClaimed: boolean,
|
||||||
|
expired: boolean,
|
||||||
|
completed = false,
|
||||||
|
) {
|
||||||
|
await clearClaims(user._id);
|
||||||
|
const todayStart = getChinaDayStart(now);
|
||||||
|
const startAt = expired
|
||||||
|
? todayStart - Number(config.durationDays) * DAY_MS
|
||||||
|
: todayStart - Math.min(claimedCount, 7) * DAY_MS;
|
||||||
|
const endAt = expired
|
||||||
|
? todayStart
|
||||||
|
: startAt + Number(config.durationDays) * DAY_MS;
|
||||||
|
const signInActivity = {
|
||||||
|
activityId: config.activityId,
|
||||||
|
startAt,
|
||||||
|
endAt,
|
||||||
|
activatedAt: startAt,
|
||||||
|
completedAt: completed ? now : null,
|
||||||
|
};
|
||||||
|
await updateUser(user._id, {
|
||||||
|
levelAmount: Number(config.triggerLevel),
|
||||||
|
signInActivity: JSON.stringify(signInActivity),
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let index = 0; index < claimedCount; index++) {
|
||||||
|
const rewardDay = index + 1;
|
||||||
|
const daysAgo = todayClaimed && index === claimedCount - 1
|
||||||
|
? 0
|
||||||
|
: claimedCount - index;
|
||||||
|
const claimedAt = now - daysAgo * DAY_MS;
|
||||||
|
const dateKey = getChinaDateKey(claimedAt);
|
||||||
|
const claimId = buildClaimId(user._id, config.activityId, dateKey);
|
||||||
|
await db.collection(CLAIM_COLLECTION).add({
|
||||||
|
_id: claimId,
|
||||||
|
claimId,
|
||||||
|
uid: user._id,
|
||||||
|
activityId: config.activityId,
|
||||||
|
dateKey,
|
||||||
|
rewardDay,
|
||||||
|
rewards: config.rewards[index].items,
|
||||||
|
claimedAt,
|
||||||
|
testPrepared: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return await inspectUser({
|
||||||
|
...user,
|
||||||
|
levelAmount: Number(config.triggerLevel),
|
||||||
|
signInActivity,
|
||||||
|
}, scenario);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function inspectUser(user: any, scenario?: string) {
|
||||||
|
const state = normalizeSignInState(user.signInActivity);
|
||||||
|
const claims = state ? await getSignInClaims(user._id, state.activityId) : [];
|
||||||
|
return {
|
||||||
|
code: 1,
|
||||||
|
data: {
|
||||||
|
scenario: scenario || null,
|
||||||
|
uid: user._id,
|
||||||
|
levelAmount: Number(user.levelAmount) || 0,
|
||||||
|
signInActivity: state,
|
||||||
|
claimedCount: claims.length,
|
||||||
|
claims,
|
||||||
|
},
|
||||||
|
msg: "测试状态准备成功",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearClaims(uid: string) {
|
||||||
|
await db.collection(CLAIM_COLLECTION).where({ uid }).remove({ multi: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateUser(uid: string, data: any) {
|
||||||
|
await db.collection(USERS_COLLECTION).where({ _id: uid }).update(data);
|
||||||
|
}
|
||||||
7
laf-cloud/functions/signInTestAdmin.yaml
Normal file
7
laf-cloud/functions/signInTestAdmin.yaml
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
name: signInTestAdmin
|
||||||
|
desc: "仅用于测试环境准备七日签到账号状态"
|
||||||
|
methods:
|
||||||
|
- POST
|
||||||
|
tags:
|
||||||
|
- activity
|
||||||
|
- test
|
||||||
|
|
@ -234,7 +234,7 @@ interface RewardItem {
|
||||||
- 当前可导入配置文件:`laf-cloud/sign-in-activity-config.json`。
|
- 当前可导入配置文件:`laf-cloud/sign-in-activity-config.json`。
|
||||||
- 用户开始活动后通过 `activityId` 继续读取配置,因此旧配置可以禁用,但在参与用户全部结束前不能删除。
|
- 用户开始活动后通过 `activityId` 继续读取配置,因此旧配置可以禁用,但在参与用户全部结束前不能删除。
|
||||||
|
|
||||||
用户活动状态写入 `users.signInActivity`:
|
用户活动状态写入 `users.signInActivity`。数据库中使用 JSON 字符串保存,读取时由服务端解析为下述结构;这样可以兼容字段原值为 `null` 的用户,并避免 Laf 将对象更新展开成点路径后触发 MongoDB 错误:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
interface SignInActivityState {
|
interface SignInActivityState {
|
||||||
|
|
@ -261,3 +261,7 @@ interface SignInActivityState {
|
||||||
- `infinite_health.count` 单位是秒。
|
- `infinite_health.count` 单位是秒。
|
||||||
- Day 7 的 `cat_skin.itemId` 当前为 12,客户端必须先支持猫 12 的资源、展示和幂等写入 `catArr`。
|
- Day 7 的 `cat_skin.itemId` 当前为 12,客户端必须先支持猫 12 的资源、展示和幂等写入 `catArr`。
|
||||||
- 倒计时使用 `serverNow` 和 `endAt`,不要完全依赖客户端本地时间。
|
- 倒计时使用 `serverNow` 和 `endAt`,不要完全依赖客户端本地时间。
|
||||||
|
|
||||||
|
## 9. 单账号测试接口
|
||||||
|
|
||||||
|
测试环境可发布 `POST /signInTestAdmin`,用一个账号快速准备未达标、待领取 Day 1~7、当天已领、过期和完成状态。该接口的密钥配置、参数和测试顺序见 `laf-cloud/signInActivity.TESTING.md`;禁止在正式环境发布。
|
||||||
|
|
|
||||||
80
laf-cloud/signInActivity.TESTING.md
Normal file
80
laf-cloud/signInActivity.TESTING.md
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
# 七日签到单账号快速测试
|
||||||
|
|
||||||
|
## 安全配置
|
||||||
|
|
||||||
|
只在测试环境发布 `signInTestAdmin`,并在 Laf 应用环境变量中设置一个高强度随机值:
|
||||||
|
|
||||||
|
```text
|
||||||
|
SIGN_IN_TEST_ADMIN_TOKEN=<随机管理密钥>
|
||||||
|
```
|
||||||
|
|
||||||
|
环境变量未配置或请求密钥不匹配时,接口完全不可用。不要把该密钥写入前端代码,也不要在正式环境发布此函数。
|
||||||
|
|
||||||
|
接口使用 `POST /signInTestAdmin` 和 `application/x-www-form-urlencoded`。
|
||||||
|
|
||||||
|
公共参数:
|
||||||
|
|
||||||
|
| 参数 | 说明 |
|
||||||
|
| --- | --- |
|
||||||
|
| `adminToken` | 与环境变量 `SIGN_IN_TEST_ADMIN_TOKEN` 相同 |
|
||||||
|
| `uid` | 唯一测试账号的用户 `_id` |
|
||||||
|
| `action` | `inspect`、`reset`、`set_level` 或 `prepare` |
|
||||||
|
|
||||||
|
## 操作接口
|
||||||
|
|
||||||
|
### 查看当前测试状态
|
||||||
|
|
||||||
|
```text
|
||||||
|
action=inspect&uid=<uid>&adminToken=<token>
|
||||||
|
```
|
||||||
|
|
||||||
|
返回玩家 `levelAmount`、`signInActivity`、领取数量和领取记录。
|
||||||
|
|
||||||
|
### 清空签到状态
|
||||||
|
|
||||||
|
```text
|
||||||
|
action=reset&uid=<uid>&adminToken=<token>
|
||||||
|
```
|
||||||
|
|
||||||
|
清空该账号的 `users.signInActivity` 和所有 `sign_in_activity_claims` 记录,不修改关卡和资产。
|
||||||
|
|
||||||
|
### 设置测试关卡
|
||||||
|
|
||||||
|
```text
|
||||||
|
action=set_level&uid=<uid>&levelAmount=23&adminToken=<token>
|
||||||
|
```
|
||||||
|
|
||||||
|
只修改 `users.levelAmount`,不修改签到状态和资产。
|
||||||
|
|
||||||
|
### 准备测试场景
|
||||||
|
|
||||||
|
```text
|
||||||
|
action=prepare&uid=<uid>&scenario=<场景>&adminToken=<token>
|
||||||
|
```
|
||||||
|
|
||||||
|
| scenario | 准备结果 | 下一步验证 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `locked` | 关卡设为门槛减 1,清空活动和领取记录 | 请求活动信息应返回 `data: false` |
|
||||||
|
| `eligible` | 关卡设为门槛,清空活动和领取记录 | 请求活动信息应创建活动并返回奖励列表 |
|
||||||
|
| `day_1_ready` | 活动已开启且尚未领取 | 领取 Day 1 |
|
||||||
|
| `day_2_ready` | 已预置 Day 1 历史记录 | 领取 Day 2 |
|
||||||
|
| `day_3_ready` | 已预置 Day 1~2 历史记录 | 领取 Day 3 |
|
||||||
|
| `day_4_ready` | 已预置 Day 1~3 历史记录 | 领取 Day 4 |
|
||||||
|
| `day_5_ready` | 已预置 Day 1~4 历史记录 | 领取 Day 5 |
|
||||||
|
| `day_6_ready` | 已预置 Day 1~5 历史记录 | 领取 Day 6 |
|
||||||
|
| `day_7_ready` | 已预置 Day 1~6 历史记录 | 领取 Day 7 并验证完成状态和猫 12 |
|
||||||
|
| `claimed_today` | 已预置今天的 Day 1 领取记录 | 再次领取应返回“今天已经领取” |
|
||||||
|
| `expired` | 活动结束时间设为当前自然日零点 | 活动状态应为 `expired`,领取应失败 |
|
||||||
|
| `completed` | 已预置七天领取记录及完成时间 | 活动状态应为 `completed`,领取应失败 |
|
||||||
|
|
||||||
|
每次 `prepare` 都会先删除该测试账号原有的签到领取记录,并覆盖该账号的签到活动状态,但不会发放或扣除任何实际资产。
|
||||||
|
|
||||||
|
## 推荐测试顺序
|
||||||
|
|
||||||
|
1. `prepare locked`,验证未达标不展示。
|
||||||
|
2. `prepare eligible`,验证首次请求创建活动且重复请求不重置时间。
|
||||||
|
3. 依次使用 `day_1_ready` 至 `day_7_ready`,逐档验证前端奖励。
|
||||||
|
4. `prepare claimed_today`,验证重复点击和重复请求。
|
||||||
|
5. `prepare expired`,验证过期页面和禁止领取。
|
||||||
|
6. `prepare completed`,验证完成页面和禁止领取。
|
||||||
|
7. 测试结束执行 `reset`。
|
||||||
|
|
@ -6,6 +6,7 @@ import { registerHooks } from "node:module";
|
||||||
|
|
||||||
const infoUrl = new URL("../functions/signInActivityInfo.ts", import.meta.url);
|
const infoUrl = new URL("../functions/signInActivityInfo.ts", import.meta.url);
|
||||||
const claimUrl = new URL("../functions/signInClaim.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 domainUrl = new URL("../functions/SignInActivity.ts", import.meta.url);
|
||||||
const productionConfigUrl = new URL("../sign-in-activity-config.json", import.meta.url);
|
const productionConfigUrl = new URL("../sign-in-activity-config.json", import.meta.url);
|
||||||
|
|
||||||
|
|
@ -13,6 +14,7 @@ const state = {
|
||||||
user: null,
|
user: null,
|
||||||
configs: [],
|
configs: [],
|
||||||
claims: [],
|
claims: [],
|
||||||
|
lastUserUpdate: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
globalThis.__signInCloudMock = {
|
globalThis.__signInCloudMock = {
|
||||||
|
|
@ -36,9 +38,24 @@ globalThis.__signInCloudMock = {
|
||||||
},
|
},
|
||||||
async update(payload) {
|
async update(payload) {
|
||||||
const targets = getCollection(name).filter(item => matches(item, query));
|
const targets = getCollection(name).filter(item => matches(item, query));
|
||||||
targets.forEach(item => Object.assign(item, structuredClone(payload)));
|
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 };
|
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 };
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -70,6 +87,7 @@ registerHooks({
|
||||||
|
|
||||||
const { default: activityInfo } = await import(infoUrl);
|
const { default: activityInfo } = await import(infoUrl);
|
||||||
const { default: claimReward } = await import(claimUrl);
|
const { default: claimReward } = await import(claimUrl);
|
||||||
|
const { default: signInTestAdmin } = await import(testAdminUrl);
|
||||||
const { getChinaDayStart, isValidSignInConfig } = await import(domainUrl);
|
const { getChinaDayStart, isValidSignInConfig } = await import(domainUrl);
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
|
|
@ -95,6 +113,7 @@ function reset(userOverrides = {}) {
|
||||||
};
|
};
|
||||||
state.configs = [structuredClone(config)];
|
state.configs = [structuredClone(config)];
|
||||||
state.claims = [];
|
state.claims = [];
|
||||||
|
state.lastUserUpdate = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCollection(name) {
|
function getCollection(name) {
|
||||||
|
|
@ -118,8 +137,10 @@ function withNow(now, callback) {
|
||||||
test("cloud functions and yaml files exist", () => {
|
test("cloud functions and yaml files exist", () => {
|
||||||
assert.equal(existsSync(infoUrl), true);
|
assert.equal(existsSync(infoUrl), true);
|
||||||
assert.equal(existsSync(claimUrl), 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/signInActivityInfo.yaml", import.meta.url)), true);
|
||||||
assert.equal(existsSync(new URL("../functions/signInClaim.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 () => {
|
test("production configuration contains the approved level and rewards", async () => {
|
||||||
|
|
@ -154,7 +175,18 @@ test("activity info activates an eligible player and returns rewards", async ()
|
||||||
assert.equal(result.data.triggerLevel, 10);
|
assert.equal(result.data.triggerLevel, 10);
|
||||||
assert.equal(result.data.triggerReached, true);
|
assert.equal(result.data.triggerReached, true);
|
||||||
assert.equal(result.data.rewards.length, 7);
|
assert.equal(result.data.rewards.length, 7);
|
||||||
assert.equal(state.user.signInActivity.startAt, getChinaDayStart(now));
|
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("claims accumulate across non-consecutive days and reject same-day repeats", async () => {
|
test("claims accumulate across non-consecutive days and reject same-day repeats", async () => {
|
||||||
|
|
@ -250,7 +282,7 @@ test("seven different days complete the activity", async () => {
|
||||||
assert.equal(result.code, 1);
|
assert.equal(result.code, 1);
|
||||||
assert.equal(result.data.completed, true);
|
assert.equal(result.data.completed, true);
|
||||||
assert.equal(result.data.claimedCount, 7);
|
assert.equal(result.data.claimedCount, 7);
|
||||||
assert.equal(typeof state.user.signInActivity.completedAt, "number");
|
assert.equal(typeof JSON.parse(state.user.signInActivity).completedAt, "number");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("invalid token and usersAd requests are rejected without writes", async () => {
|
test("invalid token and usersAd requests are rejected without writes", async () => {
|
||||||
|
|
@ -263,3 +295,96 @@ test("invalid token and usersAd requests are rejected without writes", async ()
|
||||||
assert.equal(state.claims.length, 0);
|
assert.equal(state.claims.length, 0);
|
||||||
assert.equal(state.user.signInActivity, undefined);
|
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 claimReward({ body: { uid: "u1", token: "secret" } });
|
||||||
|
assert.equal(claim.code, 1);
|
||||||
|
assert.equal(claim.data.rewardDay, day);
|
||||||
|
assert.equal(claim.data.completed, day === 7);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
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 info = await activityInfo({ body: { uid: "u1", token: "secret" } });
|
||||||
|
assert.equal(info.data.status, expected.status);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user