3ddb0ea2d4
Co-Authored-By: Claude Code <noreply@anthropic.com>
193 lines
6.9 KiB
Dart
193 lines
6.9 KiB
Dart
/// 上传域模型往返测试(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,
|
||
);
|
||
});
|
||
}
|