8f4ad11bd5
契约扩展(全部可选/可空,旧客户端零感知)+ cn/ov 实现 + e2e 权限矩阵:
- AlbumSummary += cover(kind=cover state=effective 最新行的短期签名 URL,
列表/详情按页批量组装非 N+1;未配置为 null)
- AdminPhoto += preview{thumb,browse}(变体齐备且未删除才签发;隐藏仅
admin/owner;协作者仅自己上传,scope=own 服务端过滤 uploaderId,
成员资格逐请求裁决)
- listAlbumPhotos += scope=all|own(缺省 all 行为不变:仅 owner/admin,
协作者 403;own 任何有效成员)
- EmbedSlotSettings += imageFit/offsetX/offsetBottom/zIndex(cn PATCH 校验
→ slot-state 投影 → ov 公开回读;settings 为 Json 列免迁移,读侧
normalizeStoredSettings 兜旧形状默认值)
- Flutter 模型同步(SignedUrl/PhotoPreview/AlbumPhotoScope,可空容错)
e2e:ui01-contracts 9 例(封面选取/字节取回/N+1 计数断言、preview 权限
矩阵、scope 过滤与 403/404/400)+ embed ⑨ 配置链路;全量 e2e 257 通过。
Co-Authored-By: Claude Code <noreply@anthropic.com>
143 lines
4.7 KiB
Dart
143 lines
4.7 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': '李四',
|
||
},
|
||
'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,
|
||
);
|
||
});
|
||
}
|