33using System . IO ;
44using System . Net ;
55using System . Net . Http ;
6+ using System . Reflection ;
67using System . Threading ;
78using System . Threading . Tasks ;
9+ using Amazon . Runtime ;
810using Amazon . S3 ;
911using Amazon . S3 . Model ;
1012using FluentAssertions ;
@@ -56,32 +58,81 @@ public async Task upload_async_should_buffer_non_seekable_stream_for_retries()
5658 }
5759
5860 [ Test ]
59- public async Task exists_async_should_treat_malformed_metadata_response_as_existing_object ( )
61+ public async Task exists_async_should_verify_with_presigned_head_when_metadata_unmarshalling_fails ( )
6062 {
6163 var s3Client = new Mock < IAmazonS3 > ( MockBehavior . Strict ) ;
6264 s3Client
6365 . Setup ( x => x . GetObjectMetadataAsync ( It . IsAny < GetObjectMetadataRequest > ( ) , It . IsAny < CancellationToken > ( ) ) )
64- . ThrowsAsync ( new FormatException ( "bad metadata expiration header" ) ) ;
66+ . ThrowsAsync ( CreateMetadataUnmarshallingException ( "bad metadata expiration header" ) ) ;
67+ s3Client
68+ . Setup ( x => x . GetPreSignedURL ( It . IsAny < GetPreSignedUrlRequest > ( ) ) )
69+ . Returns < GetPreSignedUrlRequest > ( request =>
70+ {
71+ request . BucketName . Should ( ) . Be ( "tts-bucket" ) ;
72+ request . Key . Should ( ) . Be ( "tts/audio.wav" ) ;
73+ request . Verb . Should ( ) . Be ( HttpVerb . HEAD ) ;
74+ request . Protocol . Should ( ) . Be ( Protocol . HTTP ) ;
75+ return "http://download.example.com/tts/audio.wav?signature=head" ;
76+ } ) ;
6577
66- var handler = new RecordingHttpMessageHandler ( ( _ , _ ) =>
67- Task . FromResult ( new HttpResponseMessage ( HttpStatusCode . OK ) ) ) ;
78+ var handler = new RecordingHttpMessageHandler ( ( request , _ ) =>
79+ {
80+ request . Method . Should ( ) . Be ( HttpMethod . Head ) ;
81+ request . RequestUri . Should ( ) . Be ( new Uri ( "http://download.example.com/tts/audio.wav?signature=head" ) ) ;
82+ return Task . FromResult ( new HttpResponseMessage ( HttpStatusCode . OK ) ) ;
83+ } ) ;
6884 var service = CreateService ( s3Client . Object , handler , useSsl : false ) ;
6985
7086 var exists = await service . ExistsAsync ( "tts/audio.wav" , CancellationToken . None ) ;
7187
7288 exists . Should ( ) . BeTrue ( ) ;
73- handler . Requests . Should ( ) . BeEmpty ( ) ;
89+ handler . Requests . Should ( ) . HaveCount ( 1 ) ;
7490 s3Client . Verify ( x => x . GetObjectMetadataAsync ( It . Is < GetObjectMetadataRequest > ( request => request . BucketName == "tts-bucket" && request . Key == "tts/audio.wav" ) , It . IsAny < CancellationToken > ( ) ) , Times . Once ) ;
75- s3Client . Verify ( x => x . GetPreSignedURL ( It . IsAny < GetPreSignedUrlRequest > ( ) ) , Times . Never ) ;
91+ s3Client . Verify ( x => x . GetPreSignedURL ( It . Is < GetPreSignedUrlRequest > ( request => request . BucketName == "tts-bucket" && request . Key == "tts/audio.wav" && request . Verb == HttpVerb . HEAD && request . Protocol == Protocol . HTTP ) ) , Times . Once ) ;
92+ }
93+
94+ [ Test ]
95+ public async Task exists_async_should_return_false_when_presigned_head_reports_missing_after_metadata_unmarshalling_failure ( )
96+ {
97+ var s3Client = new Mock < IAmazonS3 > ( MockBehavior . Strict ) ;
98+ s3Client
99+ . Setup ( x => x . GetObjectMetadataAsync ( It . IsAny < GetObjectMetadataRequest > ( ) , It . IsAny < CancellationToken > ( ) ) )
100+ . ThrowsAsync ( CreateMetadataUnmarshallingException ( "bad metadata expiration header" ) ) ;
101+ s3Client
102+ . Setup ( x => x . GetPreSignedURL ( It . IsAny < GetPreSignedUrlRequest > ( ) ) )
103+ . Returns < GetPreSignedUrlRequest > ( request =>
104+ {
105+ request . BucketName . Should ( ) . Be ( "tts-bucket" ) ;
106+ request . Key . Should ( ) . Be ( "tts/audio.wav" ) ;
107+ request . Verb . Should ( ) . Be ( HttpVerb . HEAD ) ;
108+ request . Protocol . Should ( ) . Be ( Protocol . HTTP ) ;
109+ return "http://download.example.com/tts/audio.wav?signature=head" ;
110+ } ) ;
111+
112+ var handler = new RecordingHttpMessageHandler ( ( request , _ ) =>
113+ {
114+ request . Method . Should ( ) . Be ( HttpMethod . Head ) ;
115+ return Task . FromResult ( new HttpResponseMessage ( HttpStatusCode . NotFound ) ) ;
116+ } ) ;
117+ var service = CreateService ( s3Client . Object , handler , useSsl : false ) ;
118+
119+ var exists = await service . ExistsAsync ( "tts/audio.wav" , CancellationToken . None ) ;
120+
121+ exists . Should ( ) . BeFalse ( ) ;
122+ handler . Requests . Should ( ) . HaveCount ( 1 ) ;
123+ s3Client . Verify ( x => x . GetPreSignedURL ( It . Is < GetPreSignedUrlRequest > ( request => request . BucketName == "tts-bucket" && request . Key == "tts/audio.wav" && request . Verb == HttpVerb . HEAD && request . Protocol == Protocol . HTTP ) ) , Times . Once ) ;
76124 }
77125
78126 [ Test ]
79- public async Task upload_async_should_treat_malformed_put_response_as_success_without_using_presigned_uploads ( )
127+ public async Task upload_async_should_treat_malformed_put_response_as_success_when_the_object_is_verified ( )
80128 {
81129 var s3Client = new Mock < IAmazonS3 > ( MockBehavior . Strict ) ;
82130 s3Client
83131 . Setup ( x => x . PutObjectAsync ( It . IsAny < PutObjectRequest > ( ) , It . IsAny < CancellationToken > ( ) ) )
84132 . ThrowsAsync ( new FormatException ( "bad expiration header" ) ) ;
133+ s3Client
134+ . Setup ( x => x . GetObjectMetadataAsync ( It . IsAny < GetObjectMetadataRequest > ( ) , It . IsAny < CancellationToken > ( ) ) )
135+ . ReturnsAsync ( new GetObjectMetadataResponse ( ) ) ;
85136
86137 var handler = new RecordingHttpMessageHandler ( ( _ , _ ) =>
87138 Task . FromResult ( new HttpResponseMessage ( HttpStatusCode . OK ) ) ) ;
@@ -93,15 +144,16 @@ public async Task upload_async_should_treat_malformed_put_response_as_success_wi
93144
94145 handler . Requests . Should ( ) . BeEmpty ( ) ;
95146 s3Client . Verify ( x => x . PutObjectAsync ( It . Is < PutObjectRequest > ( request => request . BucketName == "tts-bucket" && request . Key == "tts/audio.wav" && request . ContentType == "audio/wav" ) , It . IsAny < CancellationToken > ( ) ) , Times . Once ) ;
96- s3Client . Verify ( x => x . GetObjectMetadataAsync ( It . IsAny < GetObjectMetadataRequest > ( ) , It . IsAny < CancellationToken > ( ) ) , Times . Never ) ;
147+ s3Client . Verify ( x => x . GetObjectMetadataAsync ( It . Is < GetObjectMetadataRequest > ( request => request . BucketName == "tts-bucket" && request . Key == "tts/audio.wav" ) , It . IsAny < CancellationToken > ( ) ) , Times . Once ) ;
97148 s3Client . Verify ( x => x . GetPreSignedURL ( It . IsAny < GetPreSignedUrlRequest > ( ) ) , Times . Never ) ;
98149 }
99150
100151 [ Test ]
101- public async Task upload_async_should_treat_malformed_put_response_as_success_even_if_sdk_disposes_input_stream ( )
152+ public async Task upload_async_should_retry_with_a_presigned_put_when_verification_reports_the_object_missing ( )
102153 {
103154 var s3Client = new Mock < IAmazonS3 > ( MockBehavior . Strict ) ;
104155 byte [ ] capturedPayload = null ;
156+ byte [ ] presignedPayload = null ;
105157 s3Client
106158 . Setup ( x => x . PutObjectAsync ( It . IsAny < PutObjectRequest > ( ) , It . IsAny < CancellationToken > ( ) ) )
107159 . Returns < PutObjectRequest , CancellationToken > ( async ( request , _ ) =>
@@ -112,19 +164,40 @@ public async Task upload_async_should_treat_malformed_put_response_as_success_ev
112164 request . InputStream . Dispose ( ) ;
113165 throw new FormatException ( "bad expiration header" ) ;
114166 } ) ;
167+ s3Client
168+ . Setup ( x => x . GetObjectMetadataAsync ( It . IsAny < GetObjectMetadataRequest > ( ) , It . IsAny < CancellationToken > ( ) ) )
169+ . ThrowsAsync ( CreateNotFoundS3Exception ( ) ) ;
170+ s3Client
171+ . Setup ( x => x . GetPreSignedURL ( It . IsAny < GetPreSignedUrlRequest > ( ) ) )
172+ . Returns < GetPreSignedUrlRequest > ( request =>
173+ {
174+ request . BucketName . Should ( ) . Be ( "tts-bucket" ) ;
175+ request . Key . Should ( ) . Be ( "tts/audio.wav" ) ;
176+ request . Verb . Should ( ) . Be ( HttpVerb . PUT ) ;
177+ request . Protocol . Should ( ) . Be ( Protocol . HTTP ) ;
178+ request . ContentType . Should ( ) . Be ( "audio/wav" ) ;
179+ return "http://upload.example.com/tts/audio.wav?signature=put" ;
180+ } ) ;
115181
116- var handler = new RecordingHttpMessageHandler ( ( _ , _ ) =>
117- Task . FromResult ( new HttpResponseMessage ( HttpStatusCode . OK ) ) ) ;
118- var service = CreateService ( s3Client . Object , handler ) ;
182+ var handler = new RecordingHttpMessageHandler ( async ( request , _ ) =>
183+ {
184+ request . Method . Should ( ) . Be ( HttpMethod . Put ) ;
185+ request . RequestUri . Should ( ) . Be ( new Uri ( "http://upload.example.com/tts/audio.wav?signature=put" ) ) ;
186+ request . Content . Headers . ContentType ? . MediaType . Should ( ) . Be ( "audio/wav" ) ;
187+ presignedPayload = await request . Content . ReadAsByteArrayAsync ( ) ;
188+ return new HttpResponseMessage ( HttpStatusCode . OK ) ;
189+ } ) ;
190+ var service = CreateService ( s3Client . Object , handler , useSsl : false ) ;
119191
120192 await using var content = new MemoryStream ( new byte [ ] { 7 , 5 , 3 , 1 } , writable : false ) ;
121193
122194 await service . UploadAsync ( "tts/audio.wav" , content , "audio/wav" , CancellationToken . None ) ;
123195
124196 capturedPayload . Should ( ) . Equal ( 7 , 5 , 3 , 1 ) ;
125- handler . Requests . Should ( ) . BeEmpty ( ) ;
126- s3Client . Verify ( x => x . GetObjectMetadataAsync ( It . IsAny < GetObjectMetadataRequest > ( ) , It . IsAny < CancellationToken > ( ) ) , Times . Never ) ;
127- s3Client . Verify ( x => x . GetPreSignedURL ( It . IsAny < GetPreSignedUrlRequest > ( ) ) , Times . Never ) ;
197+ presignedPayload . Should ( ) . Equal ( 7 , 5 , 3 , 1 ) ;
198+ handler . Requests . Should ( ) . HaveCount ( 1 ) ;
199+ s3Client . Verify ( x => x . GetObjectMetadataAsync ( It . Is < GetObjectMetadataRequest > ( request => request . BucketName == "tts-bucket" && request . Key == "tts/audio.wav" ) , It . IsAny < CancellationToken > ( ) ) , Times . Once ) ;
200+ s3Client . Verify ( x => x . GetPreSignedURL ( It . Is < GetPreSignedUrlRequest > ( request => request . BucketName == "tts-bucket" && request . Key == "tts/audio.wav" && request . Verb == HttpVerb . PUT && request . Protocol == Protocol . HTTP && request . ContentType == "audio/wav" ) ) , Times . Once ) ;
128201 }
129202
130203 [ Test ]
@@ -172,6 +245,83 @@ public async Task get_object_url_async_should_prefer_absolute_endpoint_scheme_ov
172245 s3Client . Verify ( x => x . GetPreSignedURL ( It . IsAny < GetPreSignedUrlRequest > ( ) ) , Times . Once ) ;
173246 }
174247
248+ private static AmazonS3Exception CreateNotFoundS3Exception ( )
249+ {
250+ return new AmazonS3Exception (
251+ "Object was not found." ,
252+ ErrorType . Unknown ,
253+ "NoSuchKey" ,
254+ "request-id" ,
255+ HttpStatusCode . NotFound ) ;
256+ }
257+
258+ private static AmazonUnmarshallingException CreateMetadataUnmarshallingException ( string message )
259+ {
260+ var innerException = new FormatException ( message ) ;
261+
262+ foreach ( var constructor in typeof ( AmazonUnmarshallingException ) . GetConstructors ( BindingFlags . Instance | BindingFlags . Public | BindingFlags . NonPublic ) )
263+ {
264+ var parameters = constructor . GetParameters ( ) ;
265+ var arguments = new object [ parameters . Length ] ;
266+ var usedInnerException = false ;
267+ var usedMessage = false ;
268+ var supported = true ;
269+
270+ for ( var index = 0 ; index < parameters . Length ; index ++ )
271+ {
272+ var parameterType = parameters [ index ] . ParameterType ;
273+
274+ if ( ! usedInnerException && typeof ( Exception ) . IsAssignableFrom ( parameterType ) )
275+ {
276+ arguments [ index ] = innerException ;
277+ usedInnerException = true ;
278+ continue ;
279+ }
280+
281+ if ( parameterType == typeof ( string ) )
282+ {
283+ arguments [ index ] = usedMessage ? "/HeadObjectResult" : message ;
284+ usedMessage = true ;
285+ continue ;
286+ }
287+
288+ if ( parameterType == typeof ( bool ) )
289+ {
290+ arguments [ index ] = false ;
291+ continue ;
292+ }
293+
294+ if ( parameterType == typeof ( int ) )
295+ {
296+ arguments [ index ] = 0 ;
297+ continue ;
298+ }
299+
300+ supported = false ;
301+ break ;
302+ }
303+
304+ if ( ! supported || ! usedInnerException )
305+ {
306+ continue ;
307+ }
308+
309+ try
310+ {
311+ if ( constructor . Invoke ( arguments ) is AmazonUnmarshallingException exception
312+ && exception . InnerException is FormatException )
313+ {
314+ return exception ;
315+ }
316+ }
317+ catch
318+ {
319+ }
320+ }
321+
322+ throw new InvalidOperationException ( "Unable to construct AmazonUnmarshallingException for the test." ) ;
323+ }
324+
175325 private static S3StorageService CreateService ( IAmazonS3 s3Client , RecordingHttpMessageHandler handler = null , bool useSsl = true , string endpoint = null )
176326 {
177327 handler ??= new RecordingHttpMessageHandler ( ( _ , _ ) => Task . FromResult ( new HttpResponseMessage ( HttpStatusCode . OK ) ) ) ;
0 commit comments