新增:媒体上传客户端——MediaUploader 编排状态机 + 预签名直传 + 进度组件,修正 media 端点误挂 community 服务(T3-13)
CI / flutter-gates (push) Successful in 2m30s
CI / flutter-gates (push) Successful in 2m30s
- MediaUploader:选图→压缩(80→60 降质阶梯,10 MiB 超限拒绝)→createUpload →预签名 PUT 直传(进度回调)→confirm→ready assetId 交付;并发槽位 2、 顺序即 position、单图失败不拖垮整批;凭据过期预检与存储侧 403 各自动 换新凭据一次;失败重试复用压缩产物、从 createUpload 全新开始 - 孤儿防护:未 confirm 的 assetId 只以管线局部变量存在,快照 assetId 与 ready 态断言绑定,buildAttachRequests 非全员 ready 即抛 StateError - 线路修正:media 两步上传端点由 user 服务(:8082 MediaController)提供, T3-12 误挂 community 客户端(:8084 无 media 路由),补 mediaApi 分端口 直连,compose 真链路实测证实 - UploadProgressOverlay 四态进度层(05 号规范 §3.3);直传层本地 HttpServer 三线路测试(200/403/断连);compose 冒烟测试 (PATBOND_MEDIA_SMOKE=1 启用,默认跳过) - 新依赖:image_picker、flutter_image_compress(03 号评估选型) - flutter test 379 全绿(基线 347,+32)、analyze 0、format 无 diff Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ import '../../helpers/community_test_helpers.dart';
|
||||
void main() {
|
||||
late SessionManager session;
|
||||
late FakeHttpAdapter adapter;
|
||||
late FakeHttpAdapter mediaAdapter;
|
||||
late ApiCommunityRepository repository;
|
||||
|
||||
Future<void> setUpWith(
|
||||
@@ -28,39 +29,58 @@ void main() {
|
||||
adapter = FakeHttpAdapter(handler);
|
||||
dio.httpClientAdapter = adapter;
|
||||
final refresher = TokenRefresher(dio: dio, session: session);
|
||||
// media 两步上传端点由 user 服务提供(13 号报告 §2),走独立客户端;
|
||||
// 两 adapter 分开记录以断言线路不串。
|
||||
final mediaDio = buildPatbondDio(
|
||||
session: session,
|
||||
baseUrl: 'http://user.local',
|
||||
);
|
||||
mediaAdapter = FakeHttpAdapter(handler);
|
||||
mediaDio.httpClientAdapter = mediaAdapter;
|
||||
repository = ApiCommunityRepository(
|
||||
api: ApiClient(dio: dio, session: session, refresher: refresher),
|
||||
mediaApi: ApiClient(
|
||||
dio: mediaDio,
|
||||
session: session,
|
||||
refresher: refresher,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
group('请求线路(路径 / 方法 / 鉴权 / 参数)', () {
|
||||
test('createMediaUpload:POST /api/v1/media/uploads,携带 Bearer', () async {
|
||||
await setUpWith(
|
||||
(options) async =>
|
||||
jsonResponse(201, okEnvelope(sampleUploadCredentialsJson())),
|
||||
);
|
||||
test(
|
||||
'createMediaUpload:POST /api/v1/media/uploads 走 user 服务客户端,携带 Bearer',
|
||||
() async {
|
||||
await setUpWith(
|
||||
(options) async =>
|
||||
jsonResponse(201, okEnvelope(sampleUploadCredentialsJson())),
|
||||
);
|
||||
|
||||
final credentials = await repository.createMediaUpload(
|
||||
const CreateMediaUploadRequest(
|
||||
kind: MediaKind.image,
|
||||
purpose: MediaPurpose.postImage,
|
||||
mimeType: 'image/jpeg',
|
||||
byteSize: 204800,
|
||||
),
|
||||
);
|
||||
final credentials = await repository.createMediaUpload(
|
||||
const CreateMediaUploadRequest(
|
||||
kind: MediaKind.image,
|
||||
purpose: MediaPurpose.postImage,
|
||||
mimeType: 'image/jpeg',
|
||||
byteSize: 204800,
|
||||
),
|
||||
);
|
||||
|
||||
final request = adapter.requests.single;
|
||||
expect(request.path, '/api/v1/media/uploads');
|
||||
expect(request.method, 'POST');
|
||||
expect(request.headers['Authorization'], 'Bearer community-access');
|
||||
expect(request.data, {
|
||||
'kind': 'image',
|
||||
'purpose': 'post_image',
|
||||
'mimeType': 'image/jpeg',
|
||||
'byteSize': 204800,
|
||||
});
|
||||
expect(credentials.uploadUrl, contains('X-Amz-Signature'));
|
||||
});
|
||||
// 线路:media 端点不打 community 客户端。
|
||||
expect(adapter.requests, isEmpty);
|
||||
final request = mediaAdapter.requests.single;
|
||||
expect(request.baseUrl, 'http://user.local');
|
||||
expect(request.path, '/api/v1/media/uploads');
|
||||
expect(request.method, 'POST');
|
||||
expect(request.headers['Authorization'], 'Bearer community-access');
|
||||
expect(request.data, {
|
||||
'kind': 'image',
|
||||
'purpose': 'post_image',
|
||||
'mimeType': 'image/jpeg',
|
||||
'byteSize': 204800,
|
||||
});
|
||||
expect(credentials.uploadUrl, contains('X-Amz-Signature'));
|
||||
},
|
||||
);
|
||||
|
||||
test('completeMediaUpload:POST /complete 无请求体(服务端幂等)', () async {
|
||||
await setUpWith(
|
||||
@@ -70,7 +90,8 @@ void main() {
|
||||
|
||||
final asset = await repository.completeMediaUpload('a-1');
|
||||
|
||||
final request = adapter.requests.single;
|
||||
expect(adapter.requests, isEmpty);
|
||||
final request = mediaAdapter.requests.single;
|
||||
expect(request.path, '/api/v1/media/uploads/a-1/complete');
|
||||
expect(request.method, 'POST');
|
||||
expect(asset.status, MediaAssetStatus.ready);
|
||||
|
||||
Reference in New Issue
Block a user