b60aea4e71
Project CI / AI game creator shell Rust crates (push) Successful in 1m43s
Project CI / AI game creator shell Rust lane 2/2 (push) Failing after 2m7s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m55s
Project CI / AI game creator shell Rust lane 1/2 (push) Failing after 3m23s
Project CI / Frontend tests (push) Successful in 3m3s
Project CI / Repository checks (push) Successful in 3m42s
Project CI / AI game creator shell web tests (push) Successful in 2m3s
Project CI / Backend tests (push) Successful in 6m19s
Project CI / Native shell tests (push) Successful in 8m7s
公开游戏详情页新增评分与评价:登录用户可提交并修改自己对每款游戏的唯一一条 1–10 分评价,评论可空且最多 4000 个 Unicode 码点;游客可分页浏览评价并查看平均分与评分人数。 - 新增私有评价表及共享 DTO/API,事务内校验游戏公开可见性、保证唯一性,并计算分页与全量评分统计;下架或封禁后关闭读写,重新公开及更新版本保留评价。 - 详情页支持编辑预填、取消、提交期间冻结输入、失败保留草稿,以及保存、翻页和账号/游戏切换时的旧响应隔离;复用现有认证与公共组件。 - 同步主规范、里程碑、实施计划、生成绑定、表目录和验收脚本。不扩展 AGC、外部 API、评论审核或评分缓存。 验证: - 本轮审查复跑前端定向测试 40 项、评价领域测试 2 项,均通过。 - 网站 typecheck、Rust/TypeScript DTO 对齐、SpacetimeDB schema、文档索引、编码与 diff 检查通过。 - 实施文档另记录本地隔离数据库/API 49 项及桌面/移动视口浏览器 smoke 通过;本轮审查未重跑这些运行时验收,也未运行完整测试套件。 发布说明:新增 SpacetimeDB 表,部署时需同步 module 与 API。尚未部署 dev/production,待用户验收。 Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/561 Co-authored-by: Linghong <ink29535@proton.me> Co-committed-by: Linghong <ink29535@proton.me>
619 lines
20 KiB
JavaScript
619 lines
20 KiB
JavaScript
// 当前schema回归:prepare → 注入owner凭据并重启API → ratings-e2e新fixture → check。
|
|
// 旧库迁移独立验收:旧schema运行ratings-e2e → snapshot → 无损发布新模块/API → migration。
|
|
// E2E_REVIEWS_DATABASE=genarrative-reviews-e2e node scripts/check-game-distribution-review-moderation-e2e.mjs prepare|snapshot|migration|check
|
|
// 复用 ratings-e2e 产生的21条评价与 .app/reviews-e2e-browser.json;凭据/快照只保存ignored .app。
|
|
// API重启前从 .app/review-moderation-e2e/credentials.json 注入 owner 用户名/密码。
|
|
import assert from 'node:assert/strict';
|
|
import { randomUUID } from 'node:crypto';
|
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
import { resolve } from 'node:path';
|
|
const state = JSON.parse(readFileSync('.app/dev-stack.json', 'utf8'));
|
|
const database = process.env.E2E_REVIEWS_DATABASE;
|
|
assert(
|
|
database === state.database && database.includes('reviews-e2e'),
|
|
'必须显式指定当前 reviews-e2e 隔离数据库',
|
|
);
|
|
const base = state.services['api-server'].url;
|
|
assert(
|
|
['127.0.0.1', 'localhost'].includes(new URL(base).hostname),
|
|
'只允许本机API',
|
|
);
|
|
const mode = process.argv[2] ?? 'check';
|
|
const fixture =
|
|
mode === 'prepare'
|
|
? null
|
|
: JSON.parse(readFileSync('.app/reviews-e2e-browser.json', 'utf8'));
|
|
const dir = resolve('.app/review-moderation-e2e');
|
|
const credentialPath = resolve(dir, 'credentials.json');
|
|
const snapshotPath = resolve(dir, 'old-reviews.json');
|
|
const adminPrefix = '/admin/api/game-distribution';
|
|
const publicPrefix =
|
|
fixture && `/api/game-distribution/games/${fixture.gameId}`;
|
|
let checks = 0;
|
|
function check(label, ok) {
|
|
assert(ok, label);
|
|
checks += 1;
|
|
console.log(`PASS ${label}`);
|
|
}
|
|
async function api(path, { token, method = 'GET', body, key } = {}) {
|
|
const headers = { 'x-genarrative-response-envelope': 'v1' };
|
|
if (token) headers.Authorization = `Bearer ${token}`;
|
|
if (key !== undefined) headers['Idempotency-Key'] = key;
|
|
if (body !== undefined) headers['Content-Type'] = 'application/json';
|
|
const response = await fetch(`${base}${path}`, {
|
|
method,
|
|
headers,
|
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
signal: AbortSignal.timeout(30000),
|
|
});
|
|
const result = await response.json();
|
|
return {
|
|
status: response.status,
|
|
data: result.data,
|
|
headers: response.headers,
|
|
};
|
|
}
|
|
async function login(account, admin = false) {
|
|
const response = await api(admin ? '/admin/api/login' : '/api/auth/entry', {
|
|
method: 'POST',
|
|
body: admin
|
|
? account
|
|
: { purePhoneNumber: account.phone, password: account.password },
|
|
});
|
|
assert(
|
|
response.status === 200 && response.data.token,
|
|
`登录fixture失败 ${response.status}`,
|
|
);
|
|
return response.data;
|
|
}
|
|
const oldFields = (review) =>
|
|
Object.fromEntries(
|
|
['id', 'gameId', 'score', 'comment', 'createdAt', 'updatedAt'].map(
|
|
(key) => [key, review[key]],
|
|
),
|
|
);
|
|
function prepare() {
|
|
mkdirSync(dir, { recursive: true });
|
|
if (!existsSync(credentialPath))
|
|
writeFileSync(
|
|
credentialPath,
|
|
JSON.stringify(
|
|
{
|
|
owner: {
|
|
username: 'reviews_e2e_owner',
|
|
password: `Rev-${randomUUID()}!`,
|
|
},
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
{ mode: 0o600 },
|
|
);
|
|
console.log(
|
|
'owner凭据已准备;从ignored credentials.json注入API环境变量后重启API。',
|
|
);
|
|
}
|
|
async function snapshot() {
|
|
const reviews = await api(`${publicPrefix}/reviews?pageSize=50`);
|
|
assert(
|
|
reviews.status === 200 && reviews.data.total >= 21,
|
|
'先在旧schema运行ratings-e2e建立21条评价',
|
|
);
|
|
assert(
|
|
reviews.data.reviews.every((review) => !Object.hasOwn(review, 'isHidden')),
|
|
'此模式只接受未追加isHidden的旧schema API',
|
|
);
|
|
mkdirSync(dir, { recursive: true });
|
|
writeFileSync(
|
|
snapshotPath,
|
|
JSON.stringify(
|
|
{
|
|
database,
|
|
gameId: fixture.gameId,
|
|
reviews: reviews.data.reviews.map(oldFields),
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
console.log(
|
|
`旧schema快照已保存:${reviews.data.total}条;快照仅在ignored文件。`,
|
|
);
|
|
}
|
|
async function migration() {
|
|
const old = JSON.parse(readFileSync(snapshotPath, 'utf8'));
|
|
const current = await api(`${publicPrefix}/reviews?pageSize=50`);
|
|
check(
|
|
'旧schema存量无损升级,默认公开false',
|
|
current.status === 200 &&
|
|
current.data.total === old.reviews.length &&
|
|
old.reviews.every((review) => {
|
|
const row = current.data.reviews.find(
|
|
(value) => value.id === review.id,
|
|
);
|
|
return (
|
|
row?.isHidden === false &&
|
|
JSON.stringify(oldFields(row)) === JSON.stringify(review)
|
|
);
|
|
}),
|
|
);
|
|
writeFileSync(
|
|
resolve(dir, 'migration-verified.json'),
|
|
JSON.stringify(
|
|
{
|
|
database,
|
|
gameId: fixture.gameId,
|
|
count: old.reviews.length,
|
|
verifiedAt: new Date().toISOString(),
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
}
|
|
async function run() {
|
|
const health = await fetch(`${base}/healthz`);
|
|
check('API健康', health.ok);
|
|
assert(
|
|
existsSync(credentialPath),
|
|
'先运行prepare,并向API注入owner凭据后重启',
|
|
);
|
|
const credentials = JSON.parse(readFileSync(credentialPath, 'utf8'));
|
|
const ownerLogin = await login(credentials.owner, true);
|
|
const owner = ownerLogin.token;
|
|
for (const [name, tabs] of [
|
|
['allowed', ['game-reviews']],
|
|
['denied', ['game-management']],
|
|
]) {
|
|
credentials[name] = {
|
|
username: `reviews_${name}_${randomUUID().replaceAll('-', '')}`,
|
|
password: `Rev-${randomUUID()}!`,
|
|
};
|
|
const created = await api('/admin/api/accounts', {
|
|
method: 'POST',
|
|
token: owner,
|
|
body: {
|
|
...credentials[name],
|
|
displayName: `评价验收${name}`,
|
|
tabPermissions: tabs,
|
|
actionPermissions: [],
|
|
enabled: true,
|
|
},
|
|
});
|
|
assert(created.status === 200, `后台fixture账号创建失败 ${created.status}`);
|
|
}
|
|
writeFileSync(credentialPath, JSON.stringify(credentials, null, 2), {
|
|
mode: 0o600,
|
|
});
|
|
const allowedLogin = await login(credentials.allowed, true);
|
|
const allowed = allowedLogin.token;
|
|
const denied = (await login(credentials.denied, true)).token;
|
|
const userLogin = await login(fixture.accounts[0]);
|
|
const user = userLogin.token;
|
|
const userId = userLogin.user.id;
|
|
const query = (suffix = '', token = allowed) =>
|
|
api(`${adminPrefix}/user-reviews${suffix}`, { token });
|
|
const myReview = () => api(`${publicPrefix}/my-review`, { token: user });
|
|
const save = (body) =>
|
|
api(`${publicPrefix}/my-review`, { token: user, method: 'PUT', body });
|
|
const publicReviews = () => api(`${publicPrefix}/reviews?pageSize=50`);
|
|
const before = (await myReview()).data.review;
|
|
const reviewPath = `${adminPrefix}/user-reviews/${encodeURIComponent(before.id)}`;
|
|
const detail = () => api(reviewPath, { token: allowed });
|
|
const moderation = (body, key = randomUUID(), token = allowed) =>
|
|
api(`${reviewPath}/moderation`, { token, method: 'POST', key, body });
|
|
const body = (action, reason, expectedCreatedAt = before.createdAt) => ({
|
|
action,
|
|
expectedCreatedAt,
|
|
...(reason !== undefined ? { reason } : {}),
|
|
});
|
|
const listPath = '?gameId=' + encodeURIComponent(fixture.gameId);
|
|
for (const path of [
|
|
'/user-reviews',
|
|
'/user-review-games',
|
|
`/user-reviews/${encodeURIComponent(before.id)}`,
|
|
]) {
|
|
check(`匿名${path}401`, (await api(adminPrefix + path)).status === 401);
|
|
check(
|
|
`无权限${path}403`,
|
|
(await api(adminPrefix + path, { token: denied })).status === 403,
|
|
);
|
|
}
|
|
check(
|
|
'无权限写403',
|
|
(await moderation(body('hide', '权限检查'), randomUUID(), denied))
|
|
.status === 403,
|
|
);
|
|
check(
|
|
'普通账号不能进入后台',
|
|
[401, 403].includes((await query('', user)).status),
|
|
);
|
|
const all = await query(listPath + '&pageSize=50');
|
|
check(
|
|
'21+全量记录与no-store',
|
|
all.status === 200 &&
|
|
all.data.total >= 21 &&
|
|
all.headers.get('cache-control') === 'no-store',
|
|
);
|
|
const baseline = all.data.reviews.map(oldFields);
|
|
const page2 = await query(listPath + '&page=2');
|
|
check(
|
|
'后台默认分页20与跨页',
|
|
(await query(listPath)).data.reviews.length === 20 &&
|
|
page2.data.reviews.length >= 1,
|
|
);
|
|
const target = page2.data.reviews[0];
|
|
check('组合查询目标原来在第二页', target.id === before.id);
|
|
const keyword = `CrossPageNeedle[.*]${randomUUID()}`;
|
|
const edited = await save({ score: before.score, comment: keyword });
|
|
assert(edited.status === 200);
|
|
const combined = await query(
|
|
`${listPath}&userId=${encodeURIComponent(userId)}&keyword=${encodeURIComponent(keyword)}&status=visible&pageSize=1`,
|
|
);
|
|
check(
|
|
'四条件AND在全量数据筛选,关键词按字面量',
|
|
combined.data.total === 1 && combined.data.reviews[0].id === before.id,
|
|
);
|
|
check(
|
|
'区分大小写与空评论不命中',
|
|
(
|
|
await query(
|
|
listPath + '&keyword=' + encodeURIComponent(keyword.toLowerCase()),
|
|
)
|
|
).data.total === 0,
|
|
);
|
|
const gameSearch = await api(
|
|
`${adminPrefix}/user-review-games?query=${encodeURIComponent(fixture.gameId)}`,
|
|
{ token: allowed },
|
|
);
|
|
check(
|
|
'游戏ID精确选择',
|
|
gameSearch.status === 200 &&
|
|
gameSearch.data.games.some((game) => game.gameId === fixture.gameId),
|
|
);
|
|
check(
|
|
'详情no-store且正文完整',
|
|
(await detail()).headers.get('cache-control') === 'no-store' &&
|
|
(await detail()).data.review.comment === keyword,
|
|
);
|
|
for (const [label, request, key, expected] of [
|
|
['隐藏空原因', body('hide', ' '), randomUUID(), 422],
|
|
['非法动作', body('unknown', 'x'), randomUUID(), 400],
|
|
['缺key', body('hide', 'x'), undefined, 400],
|
|
]) {
|
|
const response =
|
|
key === undefined
|
|
? await api(`${reviewPath}/moderation`, {
|
|
token: allowed,
|
|
method: 'POST',
|
|
body: request,
|
|
})
|
|
: await moderation(request, key);
|
|
check(`${label}拒绝`, response.status === expected);
|
|
}
|
|
const preHide = (await myReview()).data.review;
|
|
check(
|
|
'无效管理请求没有变更或操作记录',
|
|
JSON.stringify(oldFields(preHide)) ===
|
|
JSON.stringify(oldFields(edited.data.review)) &&
|
|
!preHide.isHidden &&
|
|
(await detail()).data.operations.length === 0,
|
|
);
|
|
const total = (await publicReviews()).data.total;
|
|
const hideKey = randomUUID();
|
|
const hidden = await moderation(
|
|
{ ...body('hide', ' 秘密管理原因 '), adminUserId: 'forged-admin' },
|
|
hideKey,
|
|
);
|
|
check(
|
|
'隐藏成功原因trim管理员来自会话',
|
|
hidden.status === 200 &&
|
|
hidden.data.review.isHidden &&
|
|
hidden.data.operation.reason === '秘密管理原因' &&
|
|
hidden.data.operation.adminUserId === allowedLogin.admin.subject,
|
|
);
|
|
check(
|
|
'隐藏不改用户内容时间',
|
|
hidden.data.review.createdAt === preHide.createdAt &&
|
|
hidden.data.review.updatedAt === preHide.updatedAt,
|
|
);
|
|
const hiddenMine = (await myReview()).data.review;
|
|
const hiddenPublic = await publicReviews();
|
|
check(
|
|
'个人显示隐藏,公共排除统计',
|
|
hiddenMine.isHidden &&
|
|
hiddenPublic.data.total === total - 1 &&
|
|
hiddenPublic.data.ratingSummary.ratingCount === total - 1 &&
|
|
!hiddenPublic.data.reviews.some((row) => row.id === before.id),
|
|
);
|
|
const hiddenAverage =
|
|
Math.round(
|
|
(hiddenPublic.data.reviews.reduce((sum, row) => sum + row.score, 0) /
|
|
hiddenPublic.data.total) *
|
|
10,
|
|
) / 10;
|
|
check(
|
|
'隐藏后均分基于公开全集',
|
|
hiddenPublic.data.ratingSummary.averageScore === hiddenAverage,
|
|
);
|
|
check(
|
|
'普通DTO不泄漏管理原因或管理员',
|
|
!JSON.stringify(hiddenMine).includes('秘密管理原因') &&
|
|
!Object.hasOwn(hiddenMine, 'operations') &&
|
|
!Object.hasOwn(hiddenMine, 'adminUserId'),
|
|
);
|
|
const newContent = await save({
|
|
score: 10,
|
|
comment: '隐藏后编辑',
|
|
isHidden: false,
|
|
adminUserId: 'forged',
|
|
});
|
|
check(
|
|
'用户编辑保留隐藏且摘要不计入',
|
|
newContent.status === 200 &&
|
|
newContent.data.review.isHidden &&
|
|
newContent.data.ratingSummary.ratingCount === total - 1,
|
|
);
|
|
check(
|
|
'隐藏状态组合筛选',
|
|
(
|
|
await query(
|
|
listPath + '&status=hidden&userId=' + encodeURIComponent(userId),
|
|
)
|
|
).data.total === 1,
|
|
);
|
|
check(
|
|
'同key重试不重复日志',
|
|
(await moderation(body('hide', '秘密管理原因'), hideKey)).data.replayed &&
|
|
(await detail()).data.operations.length === 1,
|
|
);
|
|
check(
|
|
'同key不同请求409',
|
|
(await moderation(body('hide', '另一原因'), hideKey)).status === 409,
|
|
);
|
|
const noOpKey = randomUUID();
|
|
const noOpHide = await moderation(
|
|
body('hide', '重复隐藏也记录请求'),
|
|
noOpKey,
|
|
);
|
|
check(
|
|
'已隐藏的新key请求仍记录操作',
|
|
noOpHide.status === 200 &&
|
|
!noOpHide.data.replayed &&
|
|
(await detail()).data.operations.length === 2,
|
|
);
|
|
const restored = await moderation(body('restore'));
|
|
check(
|
|
'恢复无原因,统计重新加入',
|
|
restored.status === 200 &&
|
|
!restored.data.review.isHidden &&
|
|
restored.data.operation.reason === null &&
|
|
(await publicReviews()).data.total === total,
|
|
);
|
|
const replayHide = await moderation(body('hide', '秘密管理原因'), hideKey);
|
|
check(
|
|
'旧hide key重放不重新隐藏并返回现状态',
|
|
replayHide.data.replayed &&
|
|
replayHide.data.review.isHidden === false &&
|
|
(await myReview()).data.review.isHidden === false,
|
|
);
|
|
const replayNoOp = await moderation(
|
|
body('hide', '重复隐藏也记录请求'),
|
|
noOpKey,
|
|
);
|
|
check(
|
|
'旧无变化操作key不重新执行',
|
|
replayNoOp.data.replayed && replayNoOp.data.review.isHidden === false,
|
|
);
|
|
const longReason = await moderation(body('hide', '😀'.repeat(4000)));
|
|
check(
|
|
'原因4000码点成功',
|
|
longReason.status === 200 &&
|
|
[...longReason.data.operation.reason].length === 4000,
|
|
);
|
|
await moderation(body('restore'));
|
|
const deletionKey = randomUUID();
|
|
const deleted = await moderation(body('delete', '物理删除验收'), deletionKey);
|
|
check(
|
|
'删除成功且个人null',
|
|
deleted.status === 200 &&
|
|
deleted.data.review === null &&
|
|
(await myReview()).data.review === null &&
|
|
(await detail()).status === 404,
|
|
);
|
|
check(
|
|
'删除后无目标新key404',
|
|
(await moderation(body('hide', '已删除目标'))).status === 404,
|
|
);
|
|
const recreated = await save({ score: 7, comment: '删除后重新评价' });
|
|
check(
|
|
'删除后重建同唯一ID新创建时间',
|
|
recreated.status === 200 &&
|
|
recreated.data.review.id === before.id &&
|
|
recreated.data.review.createdAt !== before.createdAt &&
|
|
!recreated.data.review.isHidden,
|
|
);
|
|
check(
|
|
'旧createdAt新操作409',
|
|
(await moderation(body('delete', '旧页面'))).status === 409,
|
|
);
|
|
const replayDelete = await moderation(
|
|
body('delete', '物理删除验收'),
|
|
deletionKey,
|
|
);
|
|
check(
|
|
'旧delete key重试不删新评价,返回旧实例null',
|
|
replayDelete.status === 200 &&
|
|
replayDelete.data.replayed &&
|
|
replayDelete.data.review === null &&
|
|
(await myReview()).data.review.createdAt ===
|
|
recreated.data.review.createdAt,
|
|
);
|
|
check(
|
|
'当前实例详情不混旧日志',
|
|
(await detail()).data.operations.length === 0,
|
|
);
|
|
const logPath =
|
|
'/admin/api/database/tables/game_distribution_review_moderation_log/rows?limit=100&search=' +
|
|
encodeURIComponent(before.id);
|
|
const retainedLogs = await api(logPath, { token: owner });
|
|
check(
|
|
'删除操作记录独立留存且不复制评论',
|
|
retainedLogs.status === 200 &&
|
|
retainedLogs.data.rows.some(
|
|
(row) => row.cells.operation_id === deleted.data.operation.id,
|
|
) &&
|
|
!retainedLogs.data.columns.includes('comment'),
|
|
);
|
|
check(
|
|
'评价权限不越权读取私有通用表',
|
|
(await api(logPath, { token: allowed })).status === 403,
|
|
);
|
|
const recreatedBody = (action, reason) =>
|
|
body(action, reason, recreated.data.review.createdAt);
|
|
await moderation(recreatedBody('hide', '浏览器隐藏提示验收'));
|
|
check(
|
|
'存量其他评价ID内容时间未改变',
|
|
(await query(listPath + '&pageSize=50')).data.reviews
|
|
.filter((row) => row.id !== before.id)
|
|
.every((row) => {
|
|
const previous = baseline.find((value) => value.id === row.id);
|
|
return (
|
|
previous &&
|
|
JSON.stringify(oldFields(row)) === JSON.stringify(previous)
|
|
);
|
|
}),
|
|
);
|
|
for (const suffix of ['?status=unknown', '?page=0'])
|
|
check(`错误筛选${suffix}400`, (await query(suffix)).status === 400);
|
|
const privateGames = await api(
|
|
`${adminPrefix}/user-review-games?pageSize=50`,
|
|
{ token: allowed },
|
|
);
|
|
check(
|
|
'后台搜索覆盖未公开游戏',
|
|
privateGames.data.games.some((game) => game.status === 'unpublished'),
|
|
);
|
|
const secondPrefix = `/api/game-distribution/games/${fixture.secondGame}`;
|
|
const secondMine = (await api(secondPrefix + '/my-review', { token: user }))
|
|
.data.review;
|
|
assert(secondMine);
|
|
const secondModerationPath = `${adminPrefix}/user-reviews/${encodeURIComponent(secondMine.id)}/moderation`;
|
|
const secondHide = await api(secondModerationPath, {
|
|
token: allowed,
|
|
method: 'POST',
|
|
key: randomUUID(),
|
|
body: {
|
|
action: 'hide',
|
|
expectedCreatedAt: secondMine.createdAt,
|
|
reason: '仅有评价全部隐藏',
|
|
},
|
|
});
|
|
const allHidden = await api(secondPrefix + '/reviews');
|
|
check(
|
|
'全部隐藏为null/0且0页空列表',
|
|
secondHide.status === 200 &&
|
|
allHidden.data.reviews.length === 0 &&
|
|
allHidden.data.totalPages === 0 &&
|
|
allHidden.data.ratingSummary.averageScore === null &&
|
|
allHidden.data.ratingSummary.ratingCount === 0,
|
|
);
|
|
const secondGameDetail = await api(secondPrefix);
|
|
const revision = secondGameDetail.data.publicationRevision;
|
|
const suspend = await api(
|
|
`${adminPrefix}/games/${fixture.secondGame}/suspend`,
|
|
{
|
|
token: owner,
|
|
method: 'POST',
|
|
key: randomUUID(),
|
|
body: {
|
|
expectedPublicationRevision: revision,
|
|
reason: '后台不可见游戏管理验收',
|
|
},
|
|
},
|
|
);
|
|
assert(suspend.status === 200);
|
|
check(
|
|
'暂停后网站个人与公开404',
|
|
(await api(secondPrefix + '/my-review', { token: user })).status === 404 &&
|
|
(await api(secondPrefix + '/reviews')).status === 404,
|
|
);
|
|
const secondAdmin = await query(
|
|
'?gameId=' + encodeURIComponent(fixture.secondGame),
|
|
);
|
|
check(
|
|
'暂停游戏后台仍可读取隐藏评价',
|
|
secondAdmin.status === 200 &&
|
|
secondAdmin.data.total === 1 &&
|
|
secondAdmin.data.reviews[0].game.status === 'suspended',
|
|
);
|
|
const secondRestore = await api(secondModerationPath, {
|
|
token: allowed,
|
|
method: 'POST',
|
|
key: randomUUID(),
|
|
body: { action: 'restore', expectedCreatedAt: secondMine.createdAt },
|
|
});
|
|
check(
|
|
'暂停游戏后台仍可恢复评价',
|
|
secondRestore.status === 200 && !secondRestore.data.review.isHidden,
|
|
);
|
|
await api(secondModerationPath, {
|
|
token: allowed,
|
|
method: 'POST',
|
|
key: randomUUID(),
|
|
body: {
|
|
action: 'hide',
|
|
expectedCreatedAt: secondMine.createdAt,
|
|
reason: '验证游戏恢复不解除评价隐藏',
|
|
},
|
|
});
|
|
const gameRestored = await api(
|
|
`${adminPrefix}/games/${fixture.secondGame}/restore`,
|
|
{
|
|
token: owner,
|
|
method: 'POST',
|
|
key: randomUUID(),
|
|
body: { expectedPublicationRevision: revision + 1 },
|
|
},
|
|
);
|
|
check(
|
|
'恢复游戏不解除评价隐藏',
|
|
gameRestored.status === 200 &&
|
|
(await api(secondPrefix + '/my-review', { token: user })).data.review
|
|
.isHidden &&
|
|
(await api(secondPrefix + '/reviews')).data.total === 0,
|
|
);
|
|
writeFileSync(
|
|
resolve(dir, 'browser.json'),
|
|
JSON.stringify(
|
|
{
|
|
database,
|
|
gameId: fixture.gameId,
|
|
apiBase: base,
|
|
targetReviewId: before.id,
|
|
accounts: fixture.accounts,
|
|
admin: credentials,
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
{ mode: 0o600 },
|
|
);
|
|
console.log(
|
|
`全部${checks}项通过;browser fixture 已写入ignored .app/review-moderation-e2e/browser.json。`,
|
|
);
|
|
}
|
|
const modes = { prepare, snapshot, migration, check: run };
|
|
await Promise.resolve()
|
|
.then(() => {
|
|
assert(
|
|
Object.hasOwn(modes, mode),
|
|
'模式须为prepare、snapshot、migration或check',
|
|
);
|
|
return modes[mode]();
|
|
})
|
|
.catch((error) => {
|
|
console.error(`FAIL ${error.message}`);
|
|
process.exitCode = 1;
|
|
});
|