精简游戏评价回归测试与读取实现
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m0s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m55s
Project CI / Backend tests (pull_request) Successful in 5m33s
Project CI / Native shell tests (pull_request) Successful in 6m10s
Project CI / Frontend tests (pull_request) Successful in 2m18s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 10m51s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 11m16s
Project CI / Repository checks (pull_request) Successful in 2m19s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m41s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m0s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m55s
Project CI / Backend tests (pull_request) Successful in 5m33s
Project CI / Native shell tests (pull_request) Successful in 6m10s
Project CI / Frontend tests (pull_request) Successful in 2m18s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 10m51s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 11m16s
Project CI / Repository checks (pull_request) Successful in 2m19s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m41s
将后台评价功能回归与旧库迁移验收解耦,独立准备隔离账号凭据 收窄重复输入矩阵和测试专用竞态场景,保留安全与幂等覆盖 复用公开版本判断,减少评价列表的重复过滤和中间集合 同步回归步骤与本轮验证证据
This commit is contained in:
@@ -236,14 +236,9 @@ async function main() {
|
||||
);
|
||||
await invisible(privateGame, author.token, '未公开游戏');
|
||||
for (const [label, body, status] of [
|
||||
['未选择评分', { comment: '仅文字' }, 400],
|
||||
['非整数', { score: 1.5 }, 400],
|
||||
['字符串评分', { score: '5' }, 400],
|
||||
['评论类型', { score: 5, comment: 42 }, 400],
|
||||
['0 分', { score: 0 }, 422],
|
||||
['11 分', { score: 11 }, 422],
|
||||
['4001 emoji', { score: 5, comment: '😀'.repeat(4001) }, 422],
|
||||
['4001 空白先校验', { score: 5, comment: ' '.repeat(4001) }, 422],
|
||||
])
|
||||
check(
|
||||
`${label}拒绝 ${status}`,
|
||||
@@ -405,14 +400,7 @@ async function main() {
|
||||
(await list(gameId, '?pageSize=1')).data.reviews.length === 1 &&
|
||||
(await list(gameId, '?pageSize=50')).data.reviews.length === 21,
|
||||
);
|
||||
for (const query of [
|
||||
'?page=0',
|
||||
'?page=-1',
|
||||
'?page=1.5',
|
||||
'?page=abc',
|
||||
'?pageSize=0',
|
||||
'?pageSize=51',
|
||||
]) {
|
||||
for (const query of ['?page=0', '?pageSize=51']) {
|
||||
check(
|
||||
`非法分页 ${query} 返回 400`,
|
||||
(await list(gameId, query)).status === 400,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
// 后台评价管理真实验收:旧库snapshot → 无损发布新模块 → migration → check。
|
||||
// E2E_REVIEWS_DATABASE=genarrative-reviews-e2e node scripts/check-game-distribution-review-moderation-e2e.mjs snapshot|migration|check
|
||||
// 当前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, readFileSync, writeFileSync } from 'node:fs';
|
||||
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;
|
||||
@@ -17,14 +18,17 @@ assert(
|
||||
['127.0.0.1', 'localhost'].includes(new URL(base).hostname),
|
||||
'只允许本机API',
|
||||
);
|
||||
const fixture = JSON.parse(
|
||||
readFileSync('.app/reviews-e2e-browser.json', 'utf8'),
|
||||
);
|
||||
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 = `/api/game-distribution/games/${fixture.gameId}`;
|
||||
const publicPrefix =
|
||||
fixture && `/api/game-distribution/games/${fixture.gameId}`;
|
||||
let checks = 0;
|
||||
function check(label, ok) {
|
||||
assert(ok, label);
|
||||
@@ -68,6 +72,27 @@ const oldFields = (review) =>
|
||||
(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(
|
||||
@@ -78,6 +103,7 @@ async function snapshot() {
|
||||
reviews.data.reviews.every((review) => !Object.hasOwn(review, 'isHidden')),
|
||||
'此模式只接受未追加isHidden的旧schema API',
|
||||
);
|
||||
mkdirSync(dir, { recursive: true });
|
||||
writeFileSync(
|
||||
snapshotPath,
|
||||
JSON.stringify(
|
||||
@@ -90,31 +116,8 @@ async function snapshot() {
|
||||
2,
|
||||
),
|
||||
);
|
||||
if (!existsSync(credentialPath))
|
||||
writeFileSync(
|
||||
credentialPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
owner: {
|
||||
username: 'reviews_e2e_owner',
|
||||
password: `Rev-${randomUUID()}!`,
|
||||
},
|
||||
allowed: {
|
||||
username: `reviews_allowed_${Date.now()}`,
|
||||
password: `Rev-${randomUUID()}!`,
|
||||
},
|
||||
denied: {
|
||||
username: `reviews_denied_${Date.now()}`,
|
||||
password: `Rev-${randomUUID()}!`,
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
{ mode: 0o600 },
|
||||
);
|
||||
console.log(
|
||||
`旧schema快照已保存:${reviews.data.total}条;凭据仅在ignored文件。`,
|
||||
`旧schema快照已保存:${reviews.data.total}条;快照仅在ignored文件。`,
|
||||
);
|
||||
}
|
||||
async function migration() {
|
||||
@@ -152,10 +155,9 @@ async function run() {
|
||||
const health = await fetch(`${base}/healthz`);
|
||||
check('API健康', health.ok);
|
||||
assert(
|
||||
existsSync(resolve(dir, 'migration-verified.json')),
|
||||
'先运行migration保存存量验证证据',
|
||||
existsSync(credentialPath),
|
||||
'先运行prepare,并向API注入owner凭据后重启',
|
||||
);
|
||||
const old = JSON.parse(readFileSync(snapshotPath, 'utf8'));
|
||||
const credentials = JSON.parse(readFileSync(credentialPath, 'utf8'));
|
||||
const ownerLogin = await login(credentials.owner, true);
|
||||
const owner = ownerLogin.token;
|
||||
@@ -163,6 +165,10 @@ async function run() {
|
||||
['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,
|
||||
@@ -176,6 +182,9 @@ async function run() {
|
||||
});
|
||||
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;
|
||||
@@ -226,6 +235,7 @@ async function run() {
|
||||
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与跨页',
|
||||
@@ -268,13 +278,8 @@ async function run() {
|
||||
);
|
||||
for (const [label, request, key, expected] of [
|
||||
['隐藏空原因', body('hide', ' '), randomUUID(), 422],
|
||||
['删除缺原因', body('delete'), randomUUID(), 422],
|
||||
['原因4001码点', body('hide', '😀'.repeat(4001)), randomUUID(), 422],
|
||||
['非法动作', body('unknown', 'x'), randomUUID(), 400],
|
||||
['无创建时间', { action: 'hide', reason: 'x' }, randomUUID(), 400],
|
||||
['错误时间', body('hide', 'x', 'not-a-date'), randomUUID(), 400],
|
||||
['缺key', body('hide', 'x'), undefined, 400],
|
||||
['过长key', body('hide', 'x'), 'k'.repeat(129), 400],
|
||||
]) {
|
||||
const response =
|
||||
key === undefined
|
||||
@@ -287,6 +292,13 @@ async function run() {
|
||||
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(
|
||||
@@ -465,19 +477,14 @@ async function run() {
|
||||
(await query(listPath + '&pageSize=50')).data.reviews
|
||||
.filter((row) => row.id !== before.id)
|
||||
.every((row) => {
|
||||
const previous = old.reviews.find((value) => value.id === row.id);
|
||||
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',
|
||||
'?pageSize=51',
|
||||
'?page=1.5',
|
||||
])
|
||||
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`,
|
||||
@@ -596,13 +603,16 @@ async function run() {
|
||||
`全部${checks}项通过;browser fixture 已写入ignored .app/review-moderation-e2e/browser.json。`,
|
||||
);
|
||||
}
|
||||
await (
|
||||
process.argv[2] === 'snapshot'
|
||||
? snapshot()
|
||||
: process.argv[2] === 'migration'
|
||||
? migration()
|
||||
: run()
|
||||
).catch((error) => {
|
||||
console.error(`FAIL ${error.message}`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
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;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user