import 'dart:io'; import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:patbond_flutter/features/community/media_direct_upload.dart'; /// 预签名直传客户端(T3-13):本地 HttpServer 模拟 MinIO /// (埋点队列测试先例)——200 成功、403 签名过期、传输中断三线路。 void main() { Future startServer( Future Function(HttpRequest request) handler, ) async { final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0); server.listen((request) async => handler(request)); return server; } test('200:PUT 本体逐字节到达,requiredHeaders 原样携带,无鉴权头泄漏', () async { late Map seenHeaders; late List seenBody; late String seenMethod; final server = await startServer((request) async { seenMethod = request.method; seenHeaders = { 'content-type': request.headers.value('content-type'), 'authorization': request.headers.value('authorization'), 'x-device-id': request.headers.value('x-device-id'), }; seenBody = await request.fold>( [], (all, chunk) => all..addAll(chunk), ); request.response.statusCode = 200; await request.response.close(); }); addTearDown(() => server.close(force: true)); final bytes = Uint8List.fromList(List.generate(4096, (i) => i % 251)); final progress = <(int, int)>[]; await DioMediaDirectUploadClient().put( url: 'http://127.0.0.1:${server.port}/patbond-media/post_image/a-1?X-Amz-Signature=sig', headers: const {'Content-Type': 'image/jpeg'}, bytes: bytes, onProgress: (sent, total) => progress.add((sent, total)), ); expect(seenMethod, 'PUT'); expect(seenBody, bytes); // Content-Type 已签进签名,必须原样携带。 expect(seenHeaders['content-type'], 'image/jpeg'); // 裸客户端:预签名 URL 即鉴权,业务侧 Bearer/设备头不得外漏给存储。 expect(seenHeaders['authorization'], isNull); expect(seenHeaders['x-device-id'], isNull); expect(progress.last.$1, progress.last.$2); expect(progress.last.$2, bytes.length); }); test('403(签名过期/被改动)→ isCredentialRejected 异常', () async { final server = await startServer((request) async { await request.drain(); request.response.statusCode = 403; await request.response.close(); }); addTearDown(() => server.close(force: true)); await expectLater( DioMediaDirectUploadClient().put( url: 'http://127.0.0.1:${server.port}/patbond-media/a-1', headers: const {'Content-Type': 'image/jpeg'}, bytes: Uint8List(16), ), throwsA( isA() .having((e) => e.statusCode, 'statusCode', 403) .having( (e) => e.isCredentialRejected, 'isCredentialRejected', true, ), ), ); }); test('传输中断(服务端半途断连)→ 网络型异常(statusCode null,可重试)', () async { final server = await startServer((request) async { // 读取少量字节后直接断开 socket,不回任何响应。 await request.take(1).drain(); final socket = await request.response.detachSocket(writeHeaders: false); await socket.close(); socket.destroy(); }); addTearDown(() => server.close(force: true)); await expectLater( DioMediaDirectUploadClient().put( url: 'http://127.0.0.1:${server.port}/patbond-media/a-1', headers: const {'Content-Type': 'image/jpeg'}, bytes: Uint8List.fromList(List.filled(1 << 20, 7)), ), throwsA( isA() .having((e) => e.statusCode, 'statusCode', isNull) .having( (e) => e.isCredentialRejected, 'isCredentialRejected', false, ), ), ); }); }