test(mobile): integration suite against real backend on mumu
P3 Task 11 (WRITE & COMPILE phase; device run pending MuMu availability). - integration_test/p3_flow_test.dart: login -> album (zh+en) -> category -> invite -> enqueue 3 real JPEGs -> queue completed -> server history (sourceDeviceId) -> ov public API sees 3 -> hide via API + UI sync -> DIO fault-gate offline (waitingNetwork -> retry -> 4 completed) -> password change (session kept) -> relogin -> in-app locale smoke. - integration_test/p3_background_test.dart: IT_PHASE=prepare/verify spec for HOME-background continuation and force-stop kill-restart recovery (2 x ~14MB noise JPEGs give a deterministic mid-flight window). - helpers: dart-define config, fault-injecting DIO adapter, app harness (picker/notification/locale provider overrides), UI step library with live-binding polling (no pumpAndSettle). - scripts/run-mobile-it.sh: device detect (exit 2), backend ensure (compose + api/ov/worker/sync, exit 3), idempotent per-run user seed (no DB reset), APK build/install + adb push of testdata, phase 1 / --background phase 2 orchestration with server-side confirmed-count truth via curl. - services/api seed:it script (create-if-absent staff accounts). - debug-only network security config: cleartext HTTP permitted to 10.0.2.2/127.0.0.1/localhost only (release unaffected). - testdata: 3 x ~120KB + 2 x ~14MB generated JPEGs. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
"start": "tsc -p tsconfig.build.json && node dist/main-cn.js",
|
||||
"start:ov": "tsc -p tsconfig.build.json && node dist/main-ov.js",
|
||||
"seed": "tsc -p tsconfig.build.json && node dist/scripts/seed-admin.js",
|
||||
"seed:it": "tsc -p tsconfig.build.json && node dist/scripts/seed-it-users.js",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "eslint .",
|
||||
"test": "jest",
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
// services/api/src/scripts/seed-it-users.ts —— P3 Task 11 集成测试专用
|
||||
// 用户种子脚本(MuMu + 真实后端套件的预置步骤,由 scripts/run-mobile-it.sh
|
||||
// 调用)。运行方式:`IT_USERS="a:pw1,b:pw2" pnpm --filter @photo/api-cn seed:it`
|
||||
// (= tsc 构建后 node dist/scripts/seed-it-users.js)。
|
||||
//
|
||||
// 与共享 dev 库的隔离约定(集成测试【不做 DB reset】,见脚本头注释):
|
||||
// - 每次运行由编排脚本生成【全新唯一账号】(时间戳后缀)传入本脚本;
|
||||
// - 本脚本只做「不存在则建」,存在即跳过且【绝不改写密码/角色】——
|
||||
// 与 seed-admin 同幂等语义,重复调用安全;
|
||||
// - 角色固定 staff:相册创建/邀请查找在 cn 侧仅要求已认证(SessionGuard),
|
||||
// 测试账号无需 admin 角色。
|
||||
import { existsSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { config } from 'dotenv';
|
||||
import { PrismaClient } from '@photo/db-cn';
|
||||
import { hashPassword } from '../auth/password';
|
||||
|
||||
/** 解析 `account:password,...` 列表(冒号冲突风险可接受:账号不含冒号)。 */
|
||||
export function parseItUsers(raw: string): Array<{ account: string; password: string }> {
|
||||
return raw
|
||||
.split(',')
|
||||
.map((pair) => pair.trim())
|
||||
.filter((pair) => pair !== '')
|
||||
.map((pair) => {
|
||||
const sep = pair.indexOf(':');
|
||||
if (sep <= 0) {
|
||||
throw new Error(`seed-it-users: 非法条目 "${pair}"(应为 account:password)`);
|
||||
}
|
||||
const account = pair.slice(0, sep).trim();
|
||||
const password = pair.slice(sep + 1);
|
||||
if (account === '' || password === '') {
|
||||
throw new Error(`seed-it-users: 空账号/密码(${pair})`);
|
||||
}
|
||||
return { account, password };
|
||||
});
|
||||
}
|
||||
|
||||
/** 种子核心(可测):逐个建号,已存在跳过。返回账号 → 是否新建。 */
|
||||
export async function seedItUsers(
|
||||
prisma: PrismaClient,
|
||||
users: Array<{ account: string; password: string }>,
|
||||
): Promise<Map<string, boolean>> {
|
||||
const result = new Map<string, boolean>();
|
||||
for (const { account, password } of users) {
|
||||
const existing = await prisma.users.findUnique({ where: { account } });
|
||||
if (existing !== null) {
|
||||
result.set(account, false);
|
||||
continue;
|
||||
}
|
||||
await prisma.users.create({
|
||||
data: {
|
||||
account,
|
||||
passwordHash: await hashPassword(password),
|
||||
displayName: account,
|
||||
role: 'staff',
|
||||
credentialStatus: 'active',
|
||||
},
|
||||
});
|
||||
result.set(account, true);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 定位仓库根 .env(与 seed-admin 相同约定:不覆盖已有进程环境变量)。 */
|
||||
function loadRootEnv(): void {
|
||||
let dir = __dirname;
|
||||
while (!existsSync(join(dir, 'pnpm-workspace.yaml'))) {
|
||||
const parent = dirname(dir);
|
||||
if (parent === dir) {
|
||||
throw new Error('seed-it-users: pnpm-workspace.yaml not found');
|
||||
}
|
||||
dir = parent;
|
||||
}
|
||||
config({ path: join(dir, '.env') });
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
loadRootEnv();
|
||||
const raw = process.env.IT_USERS;
|
||||
if (!raw) {
|
||||
throw new Error('seed-it-users: IT_USERS 环境变量缺失(格式 account:password,...)');
|
||||
}
|
||||
const users = parseItUsers(raw);
|
||||
const prisma = new PrismaClient();
|
||||
try {
|
||||
const created = await seedItUsers(prisma, users);
|
||||
for (const [account, wasCreated] of created) {
|
||||
console.log(
|
||||
`seed-it-users: ${account} ${wasCreated ? 'created (staff)' : 'exists, skipped'}`,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
void main().catch((err: unknown) => {
|
||||
console.error(err instanceof Error ? err.message : err);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user