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
@@ -0,0 +1,66 @@
/// P3 Task 11 集成测试配置(dart-define 注入)。
///
/// 所有值经 `--dart-define KEY=value` 由 `scripts/run-mobile-it.sh` 传入:
/// 账号由脚本端 `seed-it-users` 预置(每次运行全新账号,避免污染共享
/// dev 库——集成测试【不做 DB reset】,靠账号 + 相册名时间戳隔离)。
library;
/// 一次集成测试运行的全部外部输入。
class ItConfig {
const ItConfig._();
/// 主账号(本次运行专属,脚本经 seed-it-users 新建)。
static const String mainAccount = String.fromEnvironment('IT_MAIN_ACCOUNT');
/// 主账号密码。
static const String mainPassword = String.fromEnvironment(
'IT_MAIN_PASSWORD',
);
/// 受邀协作者账号(脚本预建;密码不需要——App 侧仅邀请不登录它)。
static const String staffAccount = String.fromEnvironment('IT_STAFF_ACCOUNT');
/// ov 公开端点(viewer 侧 API 检查用;默认模拟器宿主回环 8081)。
static const String ovBase = String.fromEnvironment(
'OV_BASE',
defaultValue: 'http://10.0.2.2:8081',
);
/// ADB 推送的小图目录(设备端绝对路径;见 run-mobile-it.sh 的 push 步骤)。
static const String sourceDir = String.fromEnvironment(
'IT_SOURCE_DIR',
defaultValue: '/data/local/tmp/photolive-it',
);
/// 大图文件名(后台/杀进程场景用,同一目录下)。
static const String largeFileA = 'it_large_a.jpg';
static const String largeFileB = 'it_large_b.jpg';
/// 本次运行戳(脚本生成;相册名/账号后缀,保证唯一)。
static const String runStamp = String.fromEnvironment('IT_RUN_STAMP');
/// 修改密码场景的新密码(每轮不同:旧密码 + 固定后缀,≥8 位)。
static String get rotatedPassword => '$mainPassword-rot8';
/// 断言入参齐备(缺 dart-define 时给出可读失败而非空账号静默跑挂)。
static void assertValid() {
final missing = <String>[
if (mainAccount.isEmpty) 'IT_MAIN_ACCOUNT',
if (mainPassword.isEmpty) 'IT_MAIN_PASSWORD',
if (staffAccount.isEmpty) 'IT_STAFF_ACCOUNT',
if (runStamp.isEmpty) 'IT_RUN_STAMP',
];
if (missing.isNotEmpty) {
throw StateError(
'缺少 dart-define:${missing.join(', ')}(请经 scripts/run-mobile-it.sh '
'运行,脚本负责生成账号与戳)',
);
}
}
/// 本次运行的相册标题(zh)。
static String get albumTitleZh => '集成测试相册 $runStamp';
/// 本次运行的相册标题(en)。
static String get albumTitleEn => 'IT Album $runStamp';
}