Files
photo-live/apps/mobile/test/features/uploads_models_test.dart
T
2026-09-19 02:50:40 +08:00

193 lines
6.9 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.UploadCredential / UploadInitRequest /
/// UploadInitResponse / UploadCompleteResponse / UploadSessionRecord /
/// UploadSessionPhoto / UploadSessionState;
/// - services/api/test/uploads.e2e-spec.ts 的凭证形状断言
/// (mode=presigned、method=PUT、headers={'Content-Type':'image/jpeg'}、
/// credential.expiresAt > now)与 staging objectKey 形态
/// (photo-live/{env}/staging/{uploadId}/{photoId}.{ext})。
library;
import 'package:flutter_test/flutter_test.dart';
import 'package:photo_live/features/photos/models.dart';
import 'package:photo_live/features/uploads/models.dart';
final Map<String, dynamic> credentialFixture = <String, dynamic>{
'mode': 'presigned',
'url':
'http://127.0.0.1:9000/photo-live-dev/staging/upl0000001/pho0000001.jpg'
'?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=600',
'method': 'PUT',
'headers': {'Content-Type': 'image/jpeg'},
'expiresAt': '2026-09-19T13:00:00.000Z',
};
final Map<String, dynamic> initResponseFixture = <String, dynamic>{
'uploadId': 'upl0000001',
'photoId': 'pho0000001',
'objectKey': 'photo-live/dev/staging/upl0000001/pho0000001.jpg',
'credential': credentialFixture,
};
/// UploadSessionRecord 最小形状(sourceDeviceId 测试基底)。
const Map<String, dynamic> _recordFixture = <String, dynamic>{
'uploadId': 'upl0000002',
'albumId': 'cktragt000001',
'clientTaskId': 'task-uuid-0002',
'state': 'confirmed',
'expectedBytes': 8192000,
'contentType': 'image/png',
'createdAt': '2026-09-19T09:15:30.000Z',
'photo': {
'photoId': 'pho0000002',
'processingState': 'uploaded',
'visibility': 'unpublished',
},
};
void main() {
test('UploadCredential fromJson/toJson 往返(e2e 凭证形状)', () {
final model = UploadCredential.fromJson(credentialFixture);
expect(model.mode, 'presigned');
expect(model.method, 'PUT');
expect(model.headers['Content-Type'], 'image/jpeg');
expect(model.expiresAt.isAfter(DateTime.utc(2026, 9, 19)), isTrue);
expect(model.toJson(), equals(credentialFixture));
});
test('UploadInitResponse fromJson/toJson 往返', () {
final model = UploadInitResponse.fromJson(initResponseFixture);
expect(model.uploadId, 'upl0000001');
expect(model.photoId, 'pho0000001');
expect(model.objectKey, 'photo-live/dev/staging/upl0000001/pho0000001.jpg');
expect(model.toJson(), equals(initResponseFixture));
});
test('UploadInitRequest toJson:categoryId 缺省不下发', () {
const req = UploadInitRequest(
albumId: 'cktragt000001',
clientTaskId: 'task-uuid-0001',
filename: 'IMG_20260919_0001.jpg',
bytes: 4096000,
contentType: 'image/jpeg',
);
expect(req.toJson(), {
'albumId': 'cktragt000001',
'clientTaskId': 'task-uuid-0001',
'filename': 'IMG_20260919_0001.jpg',
'bytes': 4096000,
'contentType': 'image/jpeg',
});
const withCategory = UploadInitRequest(
albumId: 'cktragt000001',
categoryId: 'ckucat0000001',
clientTaskId: 'task-uuid-0002',
filename: 'IMG_20260919_0002.png',
bytes: 8192000,
contentType: 'image/png',
);
expect(withCategory.toJson()['categoryId'], 'ckucat0000001');
});
test('UploadInitRequest sourceDeviceId(P3 Task 3 契约增补):'
'提供时下发、缺省不下发', () {
const withDevice = UploadInitRequest(
albumId: 'cktragt000001',
clientTaskId: 'task-uuid-0003',
filename: 'IMG_20260919_0003.jpg',
bytes: 1024,
contentType: 'image/jpeg',
sourceDeviceId: '0123456789abcdef0123456789abcdef',
);
expect(withDevice.toJson()['sourceDeviceId'], isNotNull);
const withoutDevice = UploadInitRequest(
albumId: 'cktragt000001',
clientTaskId: 'task-uuid-0004',
filename: 'IMG_20260919_0004.jpg',
bytes: 1024,
contentType: 'image/jpeg',
);
expect(withoutDevice.toJson().containsKey('sourceDeviceId'), isFalse);
});
test('UploadCompleteResponse fromJson/toJson 往返', () {
final fixture = <String, dynamic>{
'photoId': 'pho0000001',
'processingState': 'processing',
};
final model = UploadCompleteResponse.fromJson(fixture);
expect(model.processingState, PhotoProcessingState.processing);
expect(model.toJson(), equals(fixture));
});
test('UploadSessionRecord fromJson/toJson 往返(photo 内嵌)', () {
final fixture = <String, dynamic>{
'uploadId': 'upl0000002',
'albumId': 'cktragt000001',
'clientTaskId': 'task-uuid-0002',
'state': 'confirmed',
'expectedBytes': 8192000,
'contentType': 'image/png',
'createdAt': '2026-09-19T09:15:30.000Z',
'photo': {
'photoId': 'pho0000002',
'processingState': 'uploaded',
'visibility': 'unpublished',
},
};
final model = UploadSessionRecord.fromJson(fixture);
expect(model.state, UploadSessionState.confirmed);
expect(model.photo.processingState, PhotoProcessingState.uploaded);
expect(model.photo.visibility, PhotoVisibility.unpublished);
expect(model.toJson(), equals(fixture));
});
test('UploadSessionRecord sourceDeviceId:可空回显(A19 设备来源)', () {
final withDevice = UploadSessionRecord.fromJson({
..._recordFixture,
'sourceDeviceId': '0123456789abcdef0123456789abcdef',
});
expect(withDevice.sourceDeviceId, '0123456789abcdef0123456789abcdef');
expect(withDevice.toJson()['sourceDeviceId'], isNotNull);
// init 未携带时为 null(契约 type: [string, 'null'])。
final withoutDevice = UploadSessionRecord.fromJson(_recordFixture);
expect(withoutDevice.sourceDeviceId, isNull);
expect(withoutDevice.toJson().containsKey('sourceDeviceId'), isFalse);
});
test('UploadSessionState 全枚举解析(pending/confirmed/cancelled/expired)',
() {
for (final value in const ['pending', 'confirmed', 'cancelled', 'expired']) {
expect(UploadSessionState.fromJson(value).wire, value);
}
expect(() => UploadSessionState.fromJson('failed'), throwsFormatException);
});
test('未知凭证/状态字段值 fail-fast', () {
expect(
() => UploadCredential.fromJson({...credentialFixture, 'mode': 'sts'}),
returnsNormally, // mode 为 String 透传(P1a 仅 presigned,预留扩展)
);
expect(
() => UploadSessionRecord.fromJson({
'uploadId': 'u',
'albumId': 'a',
'clientTaskId': 'c',
'state': 'failed', // 契约无 failed 状态(核验失败是请求级错误)
'expectedBytes': 1,
'contentType': 'image/jpeg',
'createdAt': '2026-09-19T09:15:30.000Z',
'photo': {
'photoId': 'p',
'processingState': 'uploaded',
'visibility': 'unpublished',
},
}),
throwsFormatException,
);
});
}