Files
photo-live/apps/mobile/test/features/photos_models_test.dart
T
浅念 8f4ad11bd5 feat(contracts,api): UI-01 cover, preview, scope=own and embed config extensions
契约扩展(全部可选/可空,旧客户端零感知)+ 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>
2026-09-21 14:09:35 +08:00

143 lines
4.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.
/// 照片域模型往返测试(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,
);
});
}