|
| 1 | +import 'dart:convert'; |
| 2 | +import 'dart:typed_data'; |
| 3 | + |
| 4 | +import 'package:http/http.dart'; |
| 5 | +import 'package:storage_client/storage_client.dart'; |
| 6 | +import 'package:test/test.dart'; |
| 7 | + |
| 8 | +import 'custom_http_client.dart'; |
| 9 | + |
| 10 | +const storageUrl = 'http://localhost/storage/v1'; |
| 11 | +const headers = {'Authorization': 'Bearer token'}; |
| 12 | + |
| 13 | +/// Client that finalizes (reads) the request body before failing, mimicking a |
| 14 | +/// real HTTP client. This exercises [MultipartFile] finalization on every retry |
| 15 | +/// attempt, which previously crashed because the same file instance was reused. |
| 16 | +class FinalizingRetryHttpClient extends BaseClient { |
| 17 | + FinalizingRetryHttpClient({this.failuresBeforeSuccess = 1}); |
| 18 | + |
| 19 | + final int failuresBeforeSuccess; |
| 20 | + int attempts = 0; |
| 21 | + |
| 22 | + @override |
| 23 | + Future<StreamedResponse> send(BaseRequest request) async { |
| 24 | + attempts++; |
| 25 | + await request.finalize().drain<void>(); |
| 26 | + if (attempts <= failuresBeforeSuccess) { |
| 27 | + throw ClientException('Offline'); |
| 28 | + } |
| 29 | + return StreamedResponse( |
| 30 | + Stream.value(utf8.encode(jsonEncode({'Key': 'public/a.txt'}))), |
| 31 | + 201, |
| 32 | + request: request, |
| 33 | + ); |
| 34 | + } |
| 35 | +} |
| 36 | + |
| 37 | +void main() { |
| 38 | + group('multipart uploads', () { |
| 39 | + test('retries a binary upload after a failure that finalized the request', |
| 40 | + () async { |
| 41 | + final retryClient = FinalizingRetryHttpClient(failuresBeforeSuccess: 1); |
| 42 | + final client = SupabaseStorageClient( |
| 43 | + storageUrl, |
| 44 | + headers, |
| 45 | + httpClient: retryClient, |
| 46 | + retryAttempts: 3, |
| 47 | + ); |
| 48 | + |
| 49 | + final result = await client |
| 50 | + .from('bucket') |
| 51 | + .uploadBinary('folder/file.png', Uint8List.fromList([1, 2, 3])); |
| 52 | + |
| 53 | + expect(result, 'public/a.txt'); |
| 54 | + expect(retryClient.attempts, 2); |
| 55 | + }); |
| 56 | + |
| 57 | + test('detects content type from the path of a binary signed url upload', |
| 58 | + () async { |
| 59 | + final mockClient = CustomHttpClient(); |
| 60 | + mockClient.response = <String, dynamic>{}; |
| 61 | + mockClient.statusCode = 200; |
| 62 | + final client = SupabaseStorageClient( |
| 63 | + storageUrl, |
| 64 | + headers, |
| 65 | + httpClient: mockClient, |
| 66 | + ); |
| 67 | + |
| 68 | + await client.from('bucket').uploadBinaryToSignedUrl( |
| 69 | + 'folder/image.png', |
| 70 | + 'signed-token', |
| 71 | + Uint8List.fromList([1, 2, 3]), |
| 72 | + ); |
| 73 | + |
| 74 | + final request = mockClient.receivedRequests.single as MultipartRequest; |
| 75 | + expect(request.files.single.contentType.mimeType, 'image/png'); |
| 76 | + }); |
| 77 | + }); |
| 78 | + |
| 79 | + group('path normalization', () { |
| 80 | + test('removes leading, trailing and duplicate slashes from the path', |
| 81 | + () async { |
| 82 | + final mockClient = CustomHttpClient(); |
| 83 | + mockClient.response = <String, dynamic>{}; |
| 84 | + mockClient.statusCode = 200; |
| 85 | + final client = SupabaseStorageClient( |
| 86 | + storageUrl, |
| 87 | + headers, |
| 88 | + httpClient: mockClient, |
| 89 | + ); |
| 90 | + |
| 91 | + final cleanPath = await client.from('bucket').uploadBinaryToSignedUrl( |
| 92 | + '/folder//image.png/', |
| 93 | + 'signed-token', |
| 94 | + Uint8List.fromList([1]), |
| 95 | + ); |
| 96 | + |
| 97 | + expect(cleanPath, 'folder/image.png'); |
| 98 | + |
| 99 | + final requestPath = mockClient.receivedRequests.single.url.path; |
| 100 | + expect(requestPath, endsWith('/bucket/folder/image.png')); |
| 101 | + expect(requestPath.contains('//'), isFalse); |
| 102 | + }); |
| 103 | + }); |
| 104 | +} |
0 commit comments