fb3c278e50
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>
103 lines
3.6 KiB
TypeScript
103 lines
3.6 KiB
TypeScript
// 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;
|
||
});
|
||
}
|