diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/endpointdiscovery/EndpointDiscoveryAsyncCacheLoaderGenerator.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/endpointdiscovery/EndpointDiscoveryAsyncCacheLoaderGenerator.java index 2ebf9a991d4d..a0042abb63b3 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/endpointdiscovery/EndpointDiscoveryAsyncCacheLoaderGenerator.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/endpointdiscovery/EndpointDiscoveryAsyncCacheLoaderGenerator.java @@ -27,6 +27,7 @@ import com.squareup.javapoet.TypeSpec; import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.List; import java.util.concurrent.CompletableFuture; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.codegen.emitters.GeneratorTaskParams; @@ -38,6 +39,7 @@ import software.amazon.awssdk.core.endpointdiscovery.EndpointDiscoveryCacheLoader; import software.amazon.awssdk.core.endpointdiscovery.EndpointDiscoveryEndpoint; import software.amazon.awssdk.core.endpointdiscovery.EndpointDiscoveryRequest; +import software.amazon.awssdk.utils.Validate; public class EndpointDiscoveryAsyncCacheLoaderGenerator implements ClassSpec { @@ -100,12 +102,15 @@ private MethodSpec discoverEndpoint(OperationModel opModel) { .returns(returnType); if (!opModel.getInputShape().isHasHeaderMember()) { + ClassName endpointClass = poetExtensions.getModelClass("Endpoint"); methodBuilder.addCode("return $L.$L($L.builder().build()).thenApply(r -> {", CLIENT_FIELD, opModel.getMethodName(), poetExtensions.getModelClass(opModel.getInputShape().getC2jName())) - .addStatement("$T endpoint = r.endpoints().get(0)", - poetExtensions.getModelClass("Endpoint")) + .addStatement("$T<$T> endpoints = r.endpoints()", List.class, endpointClass) + .addStatement("$T.notEmpty(endpoints, \"Endpoints returned by service for endpoint discovery must " + + "not be empty.\")", Validate.class) + .addStatement("$T endpoint = endpoints.get(0)", endpointClass) .addStatement("return $T.builder().endpoint(toUri(endpoint.address(), $L.defaultEndpoint()))" + ".expirationTime($T.now().plus(endpoint.cachePeriodInMinutes(), $T.MINUTES)).build()", EndpointDiscoveryEndpoint.class, diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/endpointdiscovery/EndpointDiscoveryCacheLoaderGenerator.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/endpointdiscovery/EndpointDiscoveryCacheLoaderGenerator.java index 5661979673c5..b52f1354b820 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/endpointdiscovery/EndpointDiscoveryCacheLoaderGenerator.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/endpointdiscovery/EndpointDiscoveryCacheLoaderGenerator.java @@ -27,6 +27,7 @@ import com.squareup.javapoet.TypeSpec; import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.List; import java.util.concurrent.CompletableFuture; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.codegen.emitters.GeneratorTaskParams; @@ -38,6 +39,7 @@ import software.amazon.awssdk.core.endpointdiscovery.EndpointDiscoveryCacheLoader; import software.amazon.awssdk.core.endpointdiscovery.EndpointDiscoveryEndpoint; import software.amazon.awssdk.core.endpointdiscovery.EndpointDiscoveryRequest; +import software.amazon.awssdk.utils.Validate; public class EndpointDiscoveryCacheLoaderGenerator implements ClassSpec { @@ -101,14 +103,18 @@ private MethodSpec discoverEndpoint(OperationModel opModel) { .returns(returnType); if (!opModel.getInputShape().isHasHeaderMember()) { + ClassName endpointClass = poetExtensions.getModelClass("Endpoint"); methodBuilder.addCode("return $T.supplyAsync(() -> {", CompletableFuture.class) .addStatement("$T response = $L.$L($L.builder().build())", poetExtensions.getModelClass(opModel.getOutputShape().getC2jName()), CLIENT_FIELD, opModel.getMethodName(), poetExtensions.getModelClass(opModel.getInputShape().getC2jName())) - .addStatement("$T endpoint = response.endpoints().get(0)", - poetExtensions.getModelClass("Endpoint")) + .addStatement("$T<$T> endpoints = response.endpoints()", List.class, endpointClass) + .addStatement("$T.notEmpty(endpoints, \"Endpoints returned by service for endpoint discovery must " + + "not be empty.\")", Validate.class) + .addStatement("$T endpoint = endpoints.get(0)", + endpointClass) .addStatement("return $T.builder().endpoint(toUri(endpoint.address(), $L.defaultEndpoint()))" + ".expirationTime($T.now().plus(endpoint.cachePeriodInMinutes(), $T.MINUTES)).build()", EndpointDiscoveryEndpoint.class, diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/endpointdiscovery/test-async-cache-loader.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/endpointdiscovery/test-async-cache-loader.java index 2b88b7a1a629..892c6cc8b45d 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/endpointdiscovery/test-async-cache-loader.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/endpointdiscovery/test-async-cache-loader.java @@ -2,6 +2,7 @@ import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.List; import java.util.concurrent.CompletableFuture; import software.amazon.awssdk.annotations.Generated; import software.amazon.awssdk.annotations.SdkInternalApi; @@ -9,6 +10,7 @@ import software.amazon.awssdk.core.endpointdiscovery.EndpointDiscoveryEndpoint; import software.amazon.awssdk.core.endpointdiscovery.EndpointDiscoveryRequest; import software.amazon.awssdk.services.endpointdiscoverytest.model.Endpoint; +import software.amazon.awssdk.utils.Validate; @SdkInternalApi @Generated("software.amazon.awssdk:codegen") @@ -26,14 +28,17 @@ public static EndpointDiscoveryTestAsyncEndpointDiscoveryCacheLoader create(Endp @Override public CompletableFuture discoverEndpoint(EndpointDiscoveryRequest endpointDiscoveryRequest) { return client.describeEndpoints( - software.amazon.awssdk.services.endpointdiscoverytest.model.DescribeEndpointsRequest.builder().build()) - .thenApply( - r -> { - Endpoint endpoint = r.endpoints().get(0); - return EndpointDiscoveryEndpoint.builder() - .endpoint(toUri(endpoint.address(), endpointDiscoveryRequest.defaultEndpoint())) - .expirationTime(Instant.now().plus(endpoint.cachePeriodInMinutes(), ChronoUnit.MINUTES)) - .build(); - }); + software.amazon.awssdk.services.endpointdiscoverytest.model.DescribeEndpointsRequest.builder().build()) + .thenApply( + r -> { + List endpoints = r.endpoints(); + Validate.notEmpty(endpoints, + "Endpoints returned by service for endpoint discovery must not be empty."); + Endpoint endpoint = endpoints.get(0); + return EndpointDiscoveryEndpoint.builder() + .endpoint(toUri(endpoint.address(), endpointDiscoveryRequest.defaultEndpoint())) + .expirationTime(Instant.now().plus(endpoint.cachePeriodInMinutes(), ChronoUnit.MINUTES)) + .build(); + }); } } diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/endpointdiscovery/test-sync-cache-loader.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/endpointdiscovery/test-sync-cache-loader.java index 9873fead9e8d..8262c0f52699 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/endpointdiscovery/test-sync-cache-loader.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/endpointdiscovery/test-sync-cache-loader.java @@ -2,6 +2,7 @@ import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.List; import java.util.concurrent.CompletableFuture; import software.amazon.awssdk.annotations.Generated; import software.amazon.awssdk.annotations.SdkInternalApi; @@ -10,6 +11,7 @@ import software.amazon.awssdk.core.endpointdiscovery.EndpointDiscoveryRequest; import software.amazon.awssdk.services.endpointdiscoverytest.model.DescribeEndpointsResponse; import software.amazon.awssdk.services.endpointdiscoverytest.model.Endpoint; +import software.amazon.awssdk.utils.Validate; @SdkInternalApi @Generated("software.amazon.awssdk:codegen") @@ -28,12 +30,14 @@ public static EndpointDiscoveryTestEndpointDiscoveryCacheLoader create(EndpointD public CompletableFuture discoverEndpoint(EndpointDiscoveryRequest endpointDiscoveryRequest) { return CompletableFuture.supplyAsync(() -> { DescribeEndpointsResponse response = client - .describeEndpoints(software.amazon.awssdk.services.endpointdiscoverytest.model.DescribeEndpointsRequest - .builder().build()); - Endpoint endpoint = response.endpoints().get(0); + .describeEndpoints(software.amazon.awssdk.services.endpointdiscoverytest.model.DescribeEndpointsRequest + .builder().build()); + List endpoints = response.endpoints(); + Validate.notEmpty(endpoints, "Endpoints returned by service for endpoint discovery must not be empty."); + Endpoint endpoint = endpoints.get(0); return EndpointDiscoveryEndpoint.builder() - .endpoint(toUri(endpoint.address(), endpointDiscoveryRequest.defaultEndpoint())) - .expirationTime(Instant.now().plus(endpoint.cachePeriodInMinutes(), ChronoUnit.MINUTES)).build(); + .endpoint(toUri(endpoint.address(), endpointDiscoveryRequest.defaultEndpoint())) + .expirationTime(Instant.now().plus(endpoint.cachePeriodInMinutes(), ChronoUnit.MINUTES)).build(); }); } } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/endpointdiscovery/EndpointDiscoveryCacheLoader.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/endpointdiscovery/EndpointDiscoveryCacheLoader.java index d740af90701d..5f9279c9f60a 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/endpointdiscovery/EndpointDiscoveryCacheLoader.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/endpointdiscovery/EndpointDiscoveryCacheLoader.java @@ -18,10 +18,10 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.concurrent.CompletableFuture; -import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.annotations.SdkProtectedApi; import software.amazon.awssdk.core.exception.SdkClientException; -@SdkInternalApi +@SdkProtectedApi public interface EndpointDiscoveryCacheLoader { CompletableFuture discoverEndpoint(EndpointDiscoveryRequest endpointDiscoveryRequest); diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/endpointdiscovery/EndpointDiscoveryFailedException.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/endpointdiscovery/EndpointDiscoveryFailedException.java new file mode 100644 index 000000000000..91ba0a8701ec --- /dev/null +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/endpointdiscovery/EndpointDiscoveryFailedException.java @@ -0,0 +1,88 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.core.endpointdiscovery; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.utils.Validate; + +/** + * This exception is thrown when the SDK was unable to retrieve an endpoint from AWS. The cause describes what specific part of + * the endpoint discovery process failed. + */ +@SdkPublicApi +public class EndpointDiscoveryFailedException extends SdkClientException { + + private static final long serialVersionUID = 1L; + + private EndpointDiscoveryFailedException(Builder b) { + super(b); + Validate.paramNotNull(b.cause(), "cause"); + } + + public static Builder builder() { + return new BuilderImpl(); + } + + public static EndpointDiscoveryFailedException create(Throwable cause) { + return builder().message("Failed when retrieving a required endpoint from AWS.") + .cause(cause) + .build(); + } + + @Override + public Builder toBuilder() { + return new BuilderImpl(this); + } + + public interface Builder extends SdkClientException.Builder { + @Override + Builder message(String message); + + @Override + Builder cause(Throwable cause); + + @Override + EndpointDiscoveryFailedException build(); + } + + protected static final class BuilderImpl extends SdkClientException.BuilderImpl implements Builder { + + protected BuilderImpl() { + } + + protected BuilderImpl(EndpointDiscoveryFailedException ex) { + super(ex); + } + + @Override + public Builder message(String message) { + this.message = message; + return this; + } + + @Override + public Builder cause(Throwable cause) { + this.cause = cause; + return this; + } + + @Override + public EndpointDiscoveryFailedException build() { + return new EndpointDiscoveryFailedException(this); + } + } +} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/endpointdiscovery/EndpointDiscoveryRefreshCache.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/endpointdiscovery/EndpointDiscoveryRefreshCache.java index 3730803d55b1..982b3427216d 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/endpointdiscovery/EndpointDiscoveryRefreshCache.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/endpointdiscovery/EndpointDiscoveryRefreshCache.java @@ -20,14 +20,11 @@ import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; import software.amazon.awssdk.annotations.SdkProtectedApi; -import software.amazon.awssdk.utils.Logger; @SdkProtectedApi public final class EndpointDiscoveryRefreshCache { - - private static final Logger log = Logger.loggerFor(EndpointDiscoveryRefreshCache.class); - private final Map cache = new ConcurrentHashMap<>(); private final EndpointDiscoveryCacheLoader client; @@ -63,7 +60,7 @@ public URI get(String accessKey, EndpointDiscoveryRequest request) { if (endpoint == null) { if (request.required()) { - return cache.computeIfAbsent(key, k -> discoverEndpoint(request).join()).endpoint(); + return cache.computeIfAbsent(key, k -> getAndJoin(request)).endpoint(); } else { EndpointDiscoveryEndpoint tempEndpoint = EndpointDiscoveryEndpoint.builder() .endpoint(request.defaultEndpoint()) @@ -90,6 +87,17 @@ public URI get(String accessKey, EndpointDiscoveryRequest request) { return endpoint.endpoint(); } + private EndpointDiscoveryEndpoint getAndJoin(EndpointDiscoveryRequest request) { + try { + return discoverEndpoint(request).get(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw EndpointDiscoveryFailedException.create(e); + } catch (ExecutionException e) { + throw EndpointDiscoveryFailedException.create(e.getCause()); + } + } + private void refreshCacheAsync(EndpointDiscoveryRequest request, String key) { discoverEndpoint(request).thenApply(v -> cache.put(key, v)); } diff --git a/test/codegen-generated-classes-test/src/main/resources/codegen-resources/endpointdiscovery/service-2.json b/test/codegen-generated-classes-test/src/main/resources/codegen-resources/endpointdiscovery/service-2.json new file mode 100644 index 000000000000..3ba86efbbb2d --- /dev/null +++ b/test/codegen-generated-classes-test/src/main/resources/codegen-resources/endpointdiscovery/service-2.json @@ -0,0 +1,135 @@ +{ + "version":"2.0", + "metadata":{ + "apiVersion":"2018-08-31", + "endpointPrefix":"awsendpointdiscoverytestservice", + "jsonVersion":"1.1", + "protocol":"json", + "serviceAbbreviation":"AwsEndpointDiscoveryTest", + "serviceFullName":"AwsEndpointDiscoveryTest", + "serviceId":"AwsEndpointDiscoveryTest", + "signatureVersion":"v4", + "signingName":"awsendpointdiscoverytestservice", + "targetPrefix":"AwsEndpointDiscoveryTestService" + }, + "operations":{ + "DescribeEndpoints":{ + "name":"DescribeEndpoints", + "http":{ + "method":"POST", + "requestUri":"/DescribeEndpoints" + }, + "input":{"shape":"DescribeEndpointsRequest"}, + "output":{"shape":"DescribeEndpointsResponse"}, + "endpointoperation":true + }, + "TestDiscoveryIdentifiersRequired":{ + "name":"TestDiscoveryIdentifiersRequired", + "http":{ + "method":"POST", + "requestUri":"/" + }, + "input":{"shape":"TestDiscoveryIdentifiersRequiredRequest"}, + "output":{"shape":"TestDiscoveryIdentifiersRequiredResponse"}, + "endpointdiscovery":{"required":true} + }, + "TestDiscoveryOptional":{ + "name":"TestDiscoveryOptional", + "http":{ + "method":"POST", + "requestUri":"/TestDiscoveryOptional" + }, + "input":{"shape":"TestDiscoveryOptionalRequest"}, + "output":{"shape":"TestDiscoveryOptionalResponse"}, + "endpointdiscovery":{ + } + }, + "TestDiscoveryRequired":{ + "name":"TestDiscoveryRequired", + "http":{ + "method":"POST", + "requestUri":"/TestDiscoveryRequired" + }, + "input":{"shape":"TestDiscoveryRequiredRequest"}, + "output":{"shape":"TestDiscoveryRequiredResponse"}, + "endpointdiscovery":{"required":true} + } + }, + "shapes":{ + "Boolean":{"type":"boolean"}, + "DescribeEndpointsRequest":{ + "type":"structure", + "members":{ + "Operation":{"shape":"String"}, + "Identifiers":{"shape":"Identifiers"} + } + }, + "DescribeEndpointsResponse":{ + "type":"structure", + "required":["Endpoints"], + "members":{ + "Endpoints":{"shape":"Endpoints"} + } + }, + "Endpoint":{ + "type":"structure", + "required":[ + "Address", + "CachePeriodInMinutes" + ], + "members":{ + "Address":{"shape":"String"}, + "CachePeriodInMinutes":{"shape":"Long"} + } + }, + "Endpoints":{ + "type":"list", + "member":{"shape":"Endpoint"} + }, + "Identifiers":{ + "type":"map", + "key":{"shape":"String"}, + "value":{"shape":"String"} + }, + "Long":{"type":"long"}, + "String":{"type":"string"}, + "TestDiscoveryIdentifiersRequiredRequest":{ + "type":"structure", + "required":["Sdk"], + "members":{ + "Sdk":{ + "shape":"String", + "endpointdiscoveryid":true + } + } + }, + "TestDiscoveryIdentifiersRequiredResponse":{ + "type":"structure", + "members":{ + "DiscoveredEndpoint":{"shape":"Boolean"} + } + }, + "TestDiscoveryOptionalRequest":{ + "type":"structure", + "members":{ + } + }, + "TestDiscoveryOptionalResponse":{ + "type":"structure", + "members":{ + "DiscoveredEndpoint":{"shape":"Boolean"} + } + }, + "TestDiscoveryRequiredRequest":{ + "type":"structure", + "members":{ + } + }, + "TestDiscoveryRequiredResponse":{ + "type":"structure", + "members":{ + "DiscoveredEndpoint":{"shape":"Boolean"} + } + } + } +} \ No newline at end of file diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/EndpointDiscoveryTest.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/EndpointDiscoveryTest.java new file mode 100644 index 000000000000..50fdd40116a7 --- /dev/null +++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/EndpointDiscoveryTest.java @@ -0,0 +1,161 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.services; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import java.net.URI; +import java.util.concurrent.ExecutionException; +import org.assertj.core.api.AbstractThrowableAssert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.core.endpointdiscovery.EndpointDiscoveryFailedException; +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.endpointdiscoverytest.EndpointDiscoveryTestAsyncClient; +import software.amazon.awssdk.services.endpointdiscoverytest.EndpointDiscoveryTestClient; +import software.amazon.awssdk.services.endpointdiscoverytest.model.EndpointDiscoveryTestException; + +public class EndpointDiscoveryTest { + + @Rule + public WireMockRule wireMock = new WireMockRule(0); + + private EndpointDiscoveryTestClient client; + + private EndpointDiscoveryTestAsyncClient asyncClient; + + @Before + public void setupClient() { + client = EndpointDiscoveryTestClient.builder() + .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("akid", "skid"))) + .region(Region.US_EAST_1) + .endpointOverride(URI.create("http://localhost:" + wireMock.port())) + .endpointDiscoveryEnabled(true) + .build(); + + asyncClient = EndpointDiscoveryTestAsyncClient.builder() + .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("akid", "skid"))) + .region(Region.US_EAST_1) + .endpointOverride(URI.create("http://localhost:" + wireMock.port())) + .endpointDiscoveryEnabled(true) + .build(); + } + + @Test + public void syncRequiredOperation_EmptyEndpointDiscoveryResponse_CausesEndpointDiscoveryFailedException() { + stubEmptyResponse(); + assertThatThrownBy(() -> client.testDiscoveryRequired(r -> {})) + .isInstanceOf(EndpointDiscoveryFailedException.class) + .hasCauseInstanceOf(IllegalArgumentException.class); + } + + @Test + public void asyncRequiredOperation_EmptyEndpointDiscoveryResponse_CausesEndpointDiscoveryFailedException() { + stubEmptyResponse(); + assertAsyncRequiredOperationCallThrowable() + .isInstanceOf(EndpointDiscoveryFailedException.class) + .hasCauseInstanceOf(IllegalArgumentException.class); + } + + @Test + public void syncRequiredOperation_NonRetryableEndpointDiscoveryResponse_CausesEndpointDiscoveryFailedException() { + stubDescribeEndpointsResponse(404); + assertThatThrownBy(() -> client.testDiscoveryRequired(r -> {})) + .isInstanceOf(EndpointDiscoveryFailedException.class) + .hasCauseInstanceOf(EndpointDiscoveryTestException.class); + } + + @Test + public void asyncRequiredOperation_NonRetryableEndpointDiscoveryResponse_CausesEndpointDiscoveryFailedException() { + stubDescribeEndpointsResponse(404); + assertAsyncRequiredOperationCallThrowable() + .isInstanceOf(EndpointDiscoveryFailedException.class) + .hasCauseInstanceOf(EndpointDiscoveryTestException.class); + } + + @Test + public void syncRequiredOperation_RetryableEndpointDiscoveryResponse_CausesEndpointDiscoveryFailedException() { + stubDescribeEndpointsResponse(500); + assertThatThrownBy(() -> client.testDiscoveryRequired(r -> {})) + .isInstanceOf(EndpointDiscoveryFailedException.class) + .hasCauseInstanceOf(EndpointDiscoveryTestException.class); + } + + @Test + public void asyncRequiredOperation_RetryableEndpointDiscoveryResponse_CausesEndpointDiscoveryFailedException() { + stubDescribeEndpointsResponse(500); + assertAsyncRequiredOperationCallThrowable() + .isInstanceOf(EndpointDiscoveryFailedException.class) + .hasCauseInstanceOf(EndpointDiscoveryTestException.class); + } + + @Test + public void syncRequiredOperation_InvalidEndpointEndpointDiscoveryResponse_CausesSdkException() { + stubDescribeEndpointsResponse(200, "invalid", 15); + assertThatThrownBy(() -> client.testDiscoveryRequired(r -> {})) + .isInstanceOf(SdkClientException.class); + } + + @Test + public void asyncRequiredOperation_InvalidEndpointEndpointDiscoveryResponse_CausesSdkException() { + stubDescribeEndpointsResponse(200, "invalid", 15); + assertAsyncRequiredOperationCallThrowable() + .isInstanceOf(SdkClientException.class); + } + + private void stubEmptyResponse() { + stubFor(post(anyUrl()) + .willReturn(aResponse().withStatus(200) + .withBody("{}"))); + } + + private void stubDescribeEndpointsResponse(int status) { + stubDescribeEndpointsResponse(status, "localhost", 60); + } + + private void stubDescribeEndpointsResponse(int status, String address, long cachePeriodInMinutes) { + stubFor(post(urlPathEqualTo("/DescribeEndpoints")) + .willReturn(aResponse().withStatus(status) + .withBody("{" + + " \"Endpoints\": [{" + + " \"Address\": \"" + address + "\"," + + " \"CachePeriodInMinutes\": " + cachePeriodInMinutes + + " }]" + + "}"))); + } + + private AbstractThrowableAssert assertAsyncRequiredOperationCallThrowable() { + try { + asyncClient.testDiscoveryRequired(r -> {}).get(); + throw new AssertionError(); + } catch (InterruptedException e) { + return assertThat(e); + } catch (ExecutionException e) { + return assertThat(e.getCause()); + } + } +}