f975321b67
P3 计划 Task 2(apps/mobile):
- core/config.dart:API_BASE/VIEWER_BASE 经 --dart-define 覆盖,缺省
10.0.2.2 模拟器宿主回环;viewer 相册链接构造 {VIEWER_BASE}/a/{token}。
- core/error_codes.dart:errors.ts 17 码 1:1 镜像(含注释),另设 4 个
客户端本地伪码(NETWORK_ERROR/NETWORK_TIMEOUT/CANCELLED/HTTP_ERROR)。
- core/api_client.dart:dio 封装——Bearer 注入(登录端点豁免)、401
驱逐(清会话 + onUnauthorized 回调去重,App 壳注册跳 /login)、
DioException/错误体 {code,message,requestId} 归一 AppException、
通用游标分页助手;auth/albums/members/categories/admin/uploads/photos
全端点方法;riverpod provider 出口。
- core/session_store.dart:SessionStore 抽象 + flutter_secure_storage
实现(token/expiresAt/account 三键,encode/decode 纯函数可测)。
- features/{auth,albums,uploads,photos}/models.dart:手写 fromJson/
toJson,逐字段对齐 openapi.yaml(类注释标注契约 schema/operationId);
locale 按字符串透传(zh-CN 标识符不兼容),业务枚举 Dart 化并在
未知值 fail-fast。
- 测试 66 项:17 码表驱动 + 网络/超时/取消 + 兜底映射、游标边界
(含 CursorPageMeta 不变式违约容错)、模型往返(fixture 转写自
openapi.yaml schema 与 api e2e 断言,来源在测试头注释)、拦截器
行为(手写 FakeAdapter:Bearer 注入/401 驱逐/登录豁免/并发去重/
尽力而为 logout/refresh 滑动续期)。flutter analyze 零告警。
Co-Authored-By: Claude Code <noreply@anthropic.com>
111 lines
3.6 KiB
Dart
111 lines
3.6 KiB
Dart
/// 照片域模型往返测试(P3 计划 Task 2)。
|
||
///
|
||
/// fixture 来源(字段逐一转写):
|
||
/// - openapi.yaml components.schemas.PhotoRecord / AdminPhoto /
|
||
/// AdminPhotoUploader / PhotoVisibility / PhotoProcessingState;
|
||
/// - services/api/test/photos-admin.e2e-spec.ts 的 ADMIN_PHOTO_KEYS
|
||
/// (PhotoRecord 必返字段 + uploader 内嵌)与种子形态
|
||
/// (visibility=published / processingState=ready、categoryId 可空、
|
||
/// displayName 取资料字段而非账号)。
|
||
library;
|
||
|
||
import 'package:flutter_test/flutter_test.dart';
|
||
import 'package:photo_live/features/photos/models.dart';
|
||
|
||
final Map<String, dynamic> photoRecordFixture = <String, dynamic>{
|
||
'photoId': 'pa-photo-p1',
|
||
'albumId': 'cktragt000001',
|
||
'categoryId': 'ckucat0000001',
|
||
'uploaderId': 'usr-col-02',
|
||
'visibility': 'published',
|
||
'processingState': 'ready',
|
||
'bytes': 4096000,
|
||
'width': 4032,
|
||
'height': 3024,
|
||
'shotAt': '2026-09-12T07:55:00.000Z',
|
||
'uploadedAt': '2026-09-12T08:05:00.000Z',
|
||
'publishedAt': '2026-09-12T08:06:00.000Z',
|
||
'likeCount': 3,
|
||
};
|
||
|
||
void main() {
|
||
test('PhotoRecord fromJson/toJson 往返(管理端全字段)', () {
|
||
final model = PhotoRecord.fromJson(photoRecordFixture);
|
||
expect(model.visibility, PhotoVisibility.published);
|
||
expect(model.processingState, PhotoProcessingState.ready);
|
||
expect(model.likeCount, 3);
|
||
expect(model.toJson(), equals(photoRecordFixture));
|
||
});
|
||
|
||
test('PhotoRecord 可空字段:处理前宽高为 null、未发布为 null', () {
|
||
final fixture = {
|
||
...photoRecordFixture,
|
||
'categoryId': null,
|
||
'width': null,
|
||
'height': null,
|
||
'shotAt': null,
|
||
'publishedAt': null,
|
||
'visibility': 'unpublished',
|
||
'processingState': 'uploaded',
|
||
};
|
||
final model = PhotoRecord.fromJson(fixture);
|
||
expect(model.categoryId, isNull);
|
||
expect(model.width, isNull);
|
||
expect(model.height, isNull);
|
||
expect(model.shotAt, isNull);
|
||
expect(model.publishedAt, isNull);
|
||
expect(model.visibility, PhotoVisibility.unpublished);
|
||
expect(model.processingState, PhotoProcessingState.uploaded);
|
||
expect(model.toJson(), equals(fixture));
|
||
});
|
||
|
||
test('PhotoRecord 解析毫秒精度时间(服务端 Date.toISOString 形态)', () {
|
||
final model = PhotoRecord.fromJson({
|
||
...photoRecordFixture,
|
||
'uploadedAt': '2026-09-12T08:05:00.000Z',
|
||
});
|
||
expect(model.uploadedAt, DateTime.utc(2026, 9, 12, 8, 5));
|
||
});
|
||
|
||
test('AdminPhoto fromJson/toJson 往返(PhotoRecord + uploader 内嵌)', () {
|
||
final fixture = {
|
||
...photoRecordFixture,
|
||
'uploader': {
|
||
'userId': 'usr-col-02',
|
||
'account': 'collab01',
|
||
'displayName': '李四',
|
||
},
|
||
};
|
||
final model = AdminPhoto.fromJson(fixture);
|
||
// e2e:uploader 映射取的是资料 displayName,而非账号。
|
||
expect(model.uploader.displayName, '李四');
|
||
expect(model.uploader.userId, model.uploaderId);
|
||
expect(model.toJson(), equals(fixture));
|
||
});
|
||
|
||
test('PhotoVisibility / PhotoProcessingState 全枚举解析', () {
|
||
for (final value in const [
|
||
'unpublished',
|
||
'published',
|
||
'hidden',
|
||
'deleted',
|
||
]) {
|
||
expect(PhotoVisibility.fromJson(value).wire, value);
|
||
}
|
||
for (final value in const [
|
||
'pending_upload',
|
||
'uploaded',
|
||
'processing',
|
||
'ready',
|
||
'processing_failed',
|
||
]) {
|
||
expect(PhotoProcessingState.fromJson(value).wire, value);
|
||
}
|
||
expect(() => PhotoVisibility.fromJson('archived'), throwsFormatException);
|
||
expect(
|
||
() => PhotoProcessingState.fromJson('failed'),
|
||
throwsFormatException,
|
||
);
|
||
});
|
||
}
|