• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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