1 /* 2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"). 5 * You may not use this file except in compliance with the License. 6 * A copy of the License is located at 7 * 8 * http://aws.amazon.com/apache2.0 9 * 10 * or in the "license" file accompanying this file. This file is distributed 11 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 * express or implied. See the License for the specific language governing 13 * permissions and limitations under the License. 14 */ 15 16 package software.amazon.awssdk.services.s3.internal.crossregion; 17 18 import static org.assertj.core.api.Assertions.assertThat; 19 import static org.assertj.core.api.Assertions.assertThatExceptionOfType; 20 21 import java.util.Collections; 22 import java.util.List; 23 import org.junit.jupiter.api.Test; 24 import org.junit.jupiter.params.ParameterizedTest; 25 import org.junit.jupiter.params.provider.ValueSource; 26 import org.mockito.ArgumentCaptor; 27 import software.amazon.awssdk.awscore.exception.AwsErrorDetails; 28 import software.amazon.awssdk.awscore.exception.AwsServiceException; 29 import software.amazon.awssdk.endpoints.EndpointProvider; 30 import software.amazon.awssdk.http.SdkHttpFullResponse; 31 import software.amazon.awssdk.regions.Region; 32 import software.amazon.awssdk.services.s3.S3ServiceClientConfiguration; 33 import software.amazon.awssdk.services.s3.endpoints.S3EndpointProvider; 34 import software.amazon.awssdk.services.s3.internal.crossregion.endpointprovider.BucketEndpointProvider; 35 import software.amazon.awssdk.services.s3.model.ListBucketsRequest; 36 import software.amazon.awssdk.services.s3.model.ListBucketsResponse; 37 import software.amazon.awssdk.services.s3.model.ListObjectsRequest; 38 import software.amazon.awssdk.services.s3.model.ListObjectsResponse; 39 import software.amazon.awssdk.services.s3.model.S3Exception; 40 import software.amazon.awssdk.services.s3.model.S3Object; 41 42 public abstract class S3CrossRegionRedirectTestBase { 43 44 public static final String X_AMZ_BUCKET_REGION = "x-amz-bucket-region"; 45 protected static final String CROSS_REGION_BUCKET = "anyBucket"; 46 protected static final Region CROSS_REGION = Region.EU_CENTRAL_1; 47 protected static final Region CHANGED_CROSS_REGION = Region.US_WEST_1; 48 49 public static final Region OVERRIDE_CONFIGURED_REGION = Region.US_WEST_2; 50 51 protected static final List<S3Object> S3_OBJECTS = Collections.singletonList(S3Object.builder().key("keyObject").build()); 52 53 protected static final S3ServiceClientConfiguration CONFIGURED_ENDPOINT_PROVIDER = 54 S3ServiceClientConfiguration.builder().endpointProvider(S3EndpointProvider.defaultProvider()).build(); 55 56 @ParameterizedTest 57 @ValueSource(ints = {301, 307}) decoratorAttemptsToRetryWithRegionNameInErrorResponse(Integer redirect)58 void decoratorAttemptsToRetryWithRegionNameInErrorResponse(Integer redirect) throws Throwable { 59 stubServiceClientConfiguration(); 60 stubClientAPICallWithFirstRedirectThenSuccessWithRegionInErrorResponse(redirect); 61 // Assert retrieved listObject 62 ListObjectsResponse listObjectsResponse = apiCallToService(); 63 assertThat(listObjectsResponse.contents()).isEqualTo(S3_OBJECTS); 64 65 ArgumentCaptor<ListObjectsRequest> requestArgumentCaptor = ArgumentCaptor.forClass(ListObjectsRequest.class); 66 verifyTheApiServiceCall(2, requestArgumentCaptor); 67 68 assertThat(requestArgumentCaptor.getAllValues().get(0).overrideConfiguration().get().endpointProvider().get()) 69 .isInstanceOf(BucketEndpointProvider.class); 70 verifyTheEndPointProviderOverridden(1, requestArgumentCaptor, CROSS_REGION.id()); 71 72 verifyHeadBucketServiceCall(0); 73 } 74 75 @ParameterizedTest 76 @ValueSource(ints = {301, 307}) decoratorUsesCache_when_CrossRegionAlreadyPresent(Integer redirect)77 void decoratorUsesCache_when_CrossRegionAlreadyPresent(Integer redirect) throws Throwable { 78 stubServiceClientConfiguration(); 79 stubRedirectSuccessSuccess(redirect); 80 81 ListObjectsResponse firstListObjectCall = apiCallToService(); 82 assertThat(firstListObjectCall.contents()).isEqualTo(S3_OBJECTS); 83 84 ListObjectsResponse secondListObjectCall = apiCallToService(); 85 assertThat(secondListObjectCall.contents()).isEqualTo(S3_OBJECTS); 86 87 ArgumentCaptor<ListObjectsRequest> requestArgumentCaptor = ArgumentCaptor.forClass(ListObjectsRequest.class); 88 verifyTheApiServiceCall(3, requestArgumentCaptor); 89 90 assertThat(requestArgumentCaptor.getAllValues().get(0).overrideConfiguration().get().endpointProvider().get()) 91 .isInstanceOf(BucketEndpointProvider.class); 92 verifyTheEndPointProviderOverridden(1, requestArgumentCaptor, CROSS_REGION.id()); 93 verifyTheEndPointProviderOverridden(2, requestArgumentCaptor, CROSS_REGION.id()); 94 verifyHeadBucketServiceCall(0); 95 } 96 97 /** 98 * Call is redirected to actual end point 99 * The redirected call fails because of incorrect parameters passed 100 * This exception should be reported correctly 101 */ 102 @ParameterizedTest 103 @ValueSource(ints = {301, 307}) apiCallFailure_when_CallFailsAfterRedirection(Integer redirectError)104 void apiCallFailure_when_CallFailsAfterRedirection(Integer redirectError) { 105 stubServiceClientConfiguration(); 106 stubRedirectThenError(redirectError); 107 assertThatExceptionOfType(S3Exception.class) 108 .isThrownBy(() -> apiCallToService()) 109 .withMessageContaining("Invalid id (Service: S3, Status Code: 400, Request ID: 1, Extended Request ID: A1)"); 110 verifyHeadBucketServiceCall(0); 111 } 112 113 @ParameterizedTest 114 @ValueSource(ints = {301, 307}) headBucketCalled_when_RedirectDoesNotHasRegionName(Integer redirect)115 void headBucketCalled_when_RedirectDoesNotHasRegionName(Integer redirect) throws Throwable { 116 stubServiceClientConfiguration(); 117 stubRedirectWithNoRegionAndThenSuccess(redirect); 118 stubHeadBucketRedirect(); 119 ListObjectsResponse listObjectsResponse = apiCallToService(); 120 assertThat(listObjectsResponse.contents()).isEqualTo(S3_OBJECTS); 121 122 ArgumentCaptor<ListObjectsRequest> requestArgumentCaptor = ArgumentCaptor.forClass(ListObjectsRequest.class); 123 verifyTheApiServiceCall(2, requestArgumentCaptor); 124 125 assertThat(requestArgumentCaptor.getAllValues().get(0).overrideConfiguration().get().endpointProvider().get()) 126 .isInstanceOf(BucketEndpointProvider.class); 127 verifyTheEndPointProviderOverridden(1, requestArgumentCaptor, CROSS_REGION.id()); 128 verifyHeadBucketServiceCall(1); 129 } 130 131 @ParameterizedTest 132 @ValueSource(ints = {301, 307}) headBucketCalledAndCached__when_RedirectDoesNotHasRegionName(Integer redirect)133 void headBucketCalledAndCached__when_RedirectDoesNotHasRegionName(Integer redirect) throws Throwable { 134 stubServiceClientConfiguration(); 135 stubRedirectWithNoRegionAndThenSuccess(redirect); 136 stubHeadBucketRedirect(); 137 138 ListObjectsResponse firstListObjectCall = apiCallToService(); 139 assertThat(firstListObjectCall.contents()).isEqualTo(S3_OBJECTS); 140 ArgumentCaptor<ListObjectsRequest> preCacheCaptor = ArgumentCaptor.forClass(ListObjectsRequest.class); 141 verifyTheApiServiceCall(2, preCacheCaptor); 142 // We need to get the BucketEndpointProvider in order to update the cache 143 verifyTheEndPointProviderOverridden(1, preCacheCaptor, CROSS_REGION.id()); 144 145 ListObjectsResponse secondListObjectCall = apiCallToService(); 146 assertThat(secondListObjectCall.contents()).isEqualTo(S3_OBJECTS); 147 // We need to captor again so that we get the args used in second API Call 148 ArgumentCaptor<ListObjectsRequest> overAllPostCacheCaptor = ArgumentCaptor.forClass(ListObjectsRequest.class); 149 verifyTheApiServiceCall(3, overAllPostCacheCaptor); 150 assertThat(overAllPostCacheCaptor.getAllValues().get(0).overrideConfiguration().get().endpointProvider().get()) 151 .isInstanceOf(BucketEndpointProvider.class); 152 verifyTheEndPointProviderOverridden(1, overAllPostCacheCaptor, CROSS_REGION.id()); 153 verifyTheEndPointProviderOverridden(2, overAllPostCacheCaptor, CROSS_REGION.id()); 154 verifyHeadBucketServiceCall(1); 155 } 156 157 @Test requestsAreNotOverridden_when_NoBucketInRequest()158 void requestsAreNotOverridden_when_NoBucketInRequest() throws Throwable { 159 stubServiceClientConfiguration(); 160 stubApiWithNoBucketField(); 161 stubHeadBucketRedirect(); 162 verifyNoBucketCall(); 163 ArgumentCaptor<ListBucketsRequest> requestArgumentCaptor = ArgumentCaptor.forClass(ListBucketsRequest.class); 164 verifyHeadBucketServiceCall(0); 165 verifyNoBucketApiCall(1, requestArgumentCaptor); 166 assertThat(requestArgumentCaptor.getAllValues().get(0).overrideConfiguration().get().endpointProvider()).isNotPresent(); 167 verifyHeadBucketServiceCall(0); 168 } 169 stubApiWithAuthorizationHeaderWithInternalSoftwareError()170 protected abstract void stubApiWithAuthorizationHeaderWithInternalSoftwareError(); 171 verifyNoBucketCall()172 protected abstract void verifyNoBucketCall(); 173 verifyNoBucketApiCall(int i, ArgumentCaptor<ListBucketsRequest> requestArgumentCaptor)174 protected abstract void verifyNoBucketApiCall(int i, ArgumentCaptor<ListBucketsRequest> requestArgumentCaptor); 175 noBucketCallToService()176 protected abstract ListBucketsResponse noBucketCallToService() throws Throwable; 177 stubApiWithNoBucketField()178 protected abstract void stubApiWithNoBucketField(); 179 stubHeadBucketRedirect()180 protected abstract void stubHeadBucketRedirect(); 181 stubRedirectWithNoRegionAndThenSuccess(Integer redirect)182 protected abstract void stubRedirectWithNoRegionAndThenSuccess(Integer redirect); 183 stubRedirectThenError(Integer redirect)184 protected abstract void stubRedirectThenError(Integer redirect); 185 stubRedirectSuccessSuccess(Integer redirect)186 protected abstract void stubRedirectSuccessSuccess(Integer redirect); 187 redirectException(int statusCode, String region, String errorCode, String errorMessage)188 protected AwsServiceException redirectException(int statusCode, String region, String errorCode, String errorMessage) { 189 SdkHttpFullResponse.Builder sdkHttpFullResponseBuilder = SdkHttpFullResponse.builder(); 190 if (region != null) { 191 sdkHttpFullResponseBuilder.appendHeader(X_AMZ_BUCKET_REGION, region); 192 } 193 return S3Exception.builder() 194 .statusCode(statusCode) 195 .requestId("1") 196 .extendedRequestId("A1") 197 .awsErrorDetails(AwsErrorDetails.builder() 198 .errorMessage(errorMessage) 199 .sdkHttpResponse(sdkHttpFullResponseBuilder.build()) 200 .errorCode(errorCode) 201 .serviceName("S3") 202 .build()) 203 .build(); 204 } 205 verifyTheEndPointProviderOverridden(int attempt, ArgumentCaptor<ListObjectsRequest> requestArgumentCaptor, String expectedRegion)206 void verifyTheEndPointProviderOverridden(int attempt, 207 ArgumentCaptor<ListObjectsRequest> requestArgumentCaptor, 208 String expectedRegion) throws Exception { 209 EndpointProvider overridenEndpointProvider = 210 requestArgumentCaptor.getAllValues().get(attempt).overrideConfiguration().get().endpointProvider().get(); 211 assertThat(overridenEndpointProvider).isInstanceOf(BucketEndpointProvider.class); 212 assertThat(((S3EndpointProvider) overridenEndpointProvider).resolveEndpoint(e -> e.region(Region.US_WEST_2) 213 .bucket(CROSS_REGION_BUCKET) 214 .build()) 215 .get().url().getHost()) 216 .isEqualTo("s3." + expectedRegion + ".amazonaws.com"); 217 } 218 apiCallToService()219 protected abstract ListObjectsResponse apiCallToService() throws Throwable; 220 verifyTheApiServiceCall(int times, ArgumentCaptor<ListObjectsRequest> requestArgumentCaptor)221 protected abstract void verifyTheApiServiceCall(int times, ArgumentCaptor<ListObjectsRequest> requestArgumentCaptor); 222 verifyHeadBucketServiceCall(int times)223 protected abstract void verifyHeadBucketServiceCall(int times); 224 stubServiceClientConfiguration()225 protected abstract void stubServiceClientConfiguration(); 226 stubClientAPICallWithFirstRedirectThenSuccessWithRegionInErrorResponse(Integer redirect)227 protected abstract void stubClientAPICallWithFirstRedirectThenSuccessWithRegionInErrorResponse(Integer redirect); 228 } 229