Files
photo-live/apps/mobile/test/helpers/fakes.dart
T
浅念 8be21b856b feat(mobile): login, password change and logout screens
P3 Task 3 移动端(对接 Task 2 api client + 本任务新增的后端契约):

- 登录页实装:账号/密码表单(空值客户端校验)→ api.login(会话自动
  持久化)→ pushReplacement /home;错误码本地化(AUTH_INVALID_
  CREDENTIALS / AUTH_RATE_LIMITED(含稍后再试文案)/ 网络 / 校验);
  提交加载态防重复提交。
- 修改密码页(设置页入口):当前/新/确认三字段,客户端校验(≥8 位、
  两次一致)→ api.changePassword → toast + pop;成功后本地会话不变
  (服务端保留当前会话);旧密码错误(401 AUTH_INVALID_CREDENTIALS)
  显示「当前密码错误」且不触发 401 驱逐。
- 退出登录:设置页按钮 → api.logout 尽力而为 → 清会话 → 清栈回
  /login;App 壳 initState 注册 onUnauthorized(业务 401 → 清栈 /login),
  根 navigatorKey 接线;password-change 的 401 豁免会话驱逐(拦截器
  豁免集合与令牌附加分离)。
- l10n:新增 16 组 zh/en 文案;错误码 → 文案解析扩展
  (core/error_l10n.dart,errorCode→AppLocalizations)。
- 测试:ApiClient.changePassword 单测 ×2(Bearer+请求体;旧密码 401
  不驱逐);widget 测试 ×12(登录 5:空值/成功跳转/凭据错误/限速文案/
  加载禁用;改密 4:过短/不一致/旧密码错误文案+会话保留/成功 toast+pop
  且令牌不变;设置 2:改密入口跳转/登出清会话回登录;App 壳 1:401
  清栈回 /login)。flutter analyze 0 issue;flutter test 81/81。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-19 01:19:57 +08:00

101 lines
2.7 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// Widget/单元测试共用替身(P3 计划 Task 3)。
///
/// 与 test/core/api_client_test.dart 中的手写打桩同款:FakeAdapter 为
/// dio HttpClientAdapter,按路由分发预设响应并记录全部请求;未引入
/// http_mock_adapter(零额外 dev 依赖,打桩面完全可控)。
library;
import 'dart:async';
import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:photo_live/core/session_store.dart';
/// 内存会话存储(测试替身)。
class FakeSessionStore implements SessionStore {
Session? session;
@override
Future<Session?> read() async => session;
@override
Future<void> save(Session value) async => session = value;
@override
Future<void> clear() async => session = null;
}
/// 记录到适配器的请求快照。
class RecordedRequest {
const RecordedRequest({
required this.method,
required this.path,
required this.headers,
required this.body,
});
final String method;
final String path;
final Map<String, dynamic> headers;
final Object? body;
}
/// 手写 dio 打桩适配器:按路由分发预设响应并记录全部请求。
///
/// [router] 返回 `(status, body)`(可为 async 以模拟慢响应/加载态);
/// body 为 null 表示无响应体(204)。
class FakeAdapter implements HttpClientAdapter {
FakeAdapter(this.router);
final FutureOr<(int, Object?)> Function(RequestOptions options) router;
final List<RecordedRequest> requests = <RecordedRequest>[];
@override
Future<ResponseBody> fetch(
RequestOptions options,
Stream? requestStream,
Future? cancelFuture,
) async {
requests.add(
RecordedRequest(
method: options.method,
path: options.path,
headers: Map<String, dynamic>.from(options.headers),
body: options.data,
),
);
final (status, body) = await router(options);
if (body == null) {
// 204 等:空体且不带 JSON content-type(transformer 不解码)。
return ResponseBody.fromString('', status);
}
return ResponseBody.fromString(
jsonEncode(body),
status,
headers: {
Headers.contentTypeHeader: [Headers.jsonContentType],
},
);
}
@override
void close({bool force = false}) {}
}
/// 常用响应体:登录成功(openapi LoginResponse 形状)。
Map<String, dynamic> loginOkBody({
String token = 'tok-new',
String expiresAt = '2026-09-20T12:00:00Z',
}) => <String, dynamic>{
'sessionToken': token,
'expiresAt': expiresAt,
};
/// 常用响应体:契约 Error 形状。
Map<String, dynamic> errorBody(String code) => <String, dynamic>{
'code': code,
'message': 'stub message',
'requestId': 'req-stub',
};