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:
浅念
2026-09-19 05:35:16 +08:00
parent 8f3966d7b5
commit fb3c278e50
18 changed files with 1409 additions and 3 deletions
+1
View File
@@ -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",
+102
View File
@@ -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;
});
}