/// 照片域模型往返测试(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 photoRecordFixture = { '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': '李四', }, 'preview': null, }; final model = AdminPhoto.fromJson(fixture); // e2e:uploader 映射取的是资料 displayName,而非账号。 expect(model.uploader.displayName, '李四'); expect(model.uploader.userId, model.uploaderId); expect(model.preview, isNull); expect(model.toJson(), equals(fixture)); }); test('AdminPhoto preview(UI-01):非空往返与缺键容错', () { final withPreview = { ...photoRecordFixture, 'uploader': { 'userId': 'usr-col-02', 'account': 'collab01', 'displayName': '李四', }, 'preview': { 'thumb': { 'url': 'http://minio:9000/bucket/variants/thumb/p1.webp?X-Amz-Signature=t', 'expiresAt': '2026-09-21T09:00:00.000Z', }, 'browse': { 'url': 'http://minio:9000/bucket/variants/browse/p1.webp?X-Amz-Signature=b', 'expiresAt': '2026-09-21T09:00:00.000Z', }, }, }; final model = AdminPhoto.fromJson(withPreview); expect(model.preview, isNotNull); expect(model.preview!.thumb.url, contains('/thumb/')); expect(model.preview!.browse.expiresAt, DateTime.utc(2026, 9, 21, 9)); expect(model.toJson(), equals(withPreview)); // 旧响应缺 preview 键 → null(旧响应不 break)。 final legacy = {...withPreview}..remove('preview'); expect(AdminPhoto.fromJson(legacy).preview, isNull); }); 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, ); }); }