From 7f83b1f547c9fbee0f50fb4d00eb29f97890ca22 Mon Sep 17 00:00:00 2001 From: marnee01 <20825300+marnee01@users.noreply.github.com> Date: Mon, 7 Mar 2022 16:07:41 -0600 Subject: [PATCH] Add support for multiple-url-strategy configuration for clients. (#1946) * Add support for multiple-url-strategy configuration for clients. Add support for new property spring.cloud.config.multiple-uri-strategy. The value must be one of: always, connection_timeout_only. The default value is connection_timeout_only. This default setting maintains existing behavior. If a client has multiple URLs in the spring.cloud.config.uri property, and if multiple-uri-strategy is set to "always", then if the client gets any error from config server whatsoever or gets no response, the client will try the next URL in the list. The existing and default behavior is that the other URLs in the list will be tried only if and when the client receives no response from config server. This is mainly to allow for a client failing over to secondary URLs when it receives a 404 due to config server being unable to reach its git server (and config server does not have the requested configs in its local git cache). With the default behavior, the client receives a 404 and never tries the next URL in the list(which might point to a different git server that is currently up). Another benefit is that it allows for a client failing over to a secondary config server if the first one returns a 503 (OUT_OF_SERVICE). Fixes gh-1845. * Fix code style after committing change for gh-1845. * Fix support for multiple-url-strategy configuration for clients. Fix issue with the new multiple URI strategy behavior. Even when strategy was ALWAYS, the client would not try the next URL when it received server-side errors. Fixed this by adding HttpServerErrorException to catch clause in ConfigServicePropertySourceLocator. Also, fix code style issues and add unit tests. Fix existing fail-fast unit tests that were not correct. (They were not mocking out raw status code method on the response. The error thrown back was actually due to RestTemplate.handleResponse being unable to to map raw status code 0 to an HttpStatus.) * Fix support for multiple-url-strategy configuration for clients. Merge with dev and undo unintentional changes that were done automatically. This is part of the pull request fixing gh-1845. * Fix support for multiple-url-strategy configuration for clients. Update ConfigServerConfigDataLoader to try multiple URLs even for server-side errors when strategy is ALWAYS. This was missed in previous commit. This is part of the pull request fixing gh-1845. * Add ConfigServerConfigDataLoaderTests. When ConfigServerConfigDataLoader was originally introduced for the new Spring Boot 2.4 way to import configuration data (https://github.com/spring-cloud/spring-cloud-config/pull/1656/files), no unit test was added. I needed to add tests to cover changes made for the new MultipleUriStrategy (gh-1845). I copied tests from ConfigServicePropertySourceLocatorTests and modified as needed for the new class. (There were 2-3 test cases from ConfigServicePropertySourceLocatorTests that I did not copy over because it wasn't clear to me expected behavior or how to set up test case). This is part of the pull request fixing gh-1845. * Update the default for multiple-uri-strategy to ALWAYS. (Per code review.) This is part of the pull request fixing gh-1845. * Update documentation for the new multiple-uri-strategy property. Also, add documentation comparing behavior of multiple URLs under spring.cloud.config.uri versus multiple URLs under spring.config.import. This is part of the pull request fixing gh-1845. Co-authored-by: UPINCMA --- .../main/asciidoc/spring-cloud-config.adoc | 10 +- .../config/client/ConfigClientProperties.java | 32 ++ .../client/ConfigServerConfigDataLoader.java | 11 +- .../ConfigServicePropertySourceLocator.java | 13 +- .../client/ConfigClientPropertiesTests.java | 17 + .../ConfigServerConfigDataLoaderTests.java | 543 ++++++++++++++++++ ...nfigServicePropertySourceLocatorTests.java | 281 ++++++++- 7 files changed, 873 insertions(+), 34 deletions(-) create mode 100644 spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServerConfigDataLoaderTests.java diff --git a/docs/src/main/asciidoc/spring-cloud-config.adoc b/docs/src/main/asciidoc/spring-cloud-config.adoc index 3467b400..b67a9754 100644 --- a/docs/src/main/asciidoc/spring-cloud-config.adoc +++ b/docs/src/main/asciidoc/spring-cloud-config.adoc @@ -1812,9 +1812,15 @@ In that case, the items in the list are tried one by one until one succeeds. This behavior can be useful when working on a feature branch. For instance, you might want to align the config label with your branch but make it optional (in that case, use `spring.cloud.config.label=myfeature,develop`). -=== Specifying Multiple Urls for the Config Server +=== Specifying Multiple URLs for the Config Server -To ensure high availability when you have multiple instances of Config Server deployed and expect one or more instances to be unavailable from time to time, you can either specify multiple URLs (as a comma-separated list under the `spring.cloud.config.uri` property) or have all your instances register in a Service Registry like Eureka ( if using Discovery-First Bootstrap mode ). Note that doing so ensures high availability only when the Config Server is not running (that is, when the application has exited) or when a connection timeout has occurred. For example, if the Config Server returns a 500 (Internal Server Error) response or the Config Client receives a 401 from the Config Server (due to bad credentials or other causes), the Config Client does not try to fetch properties from other URLs. An error of that kind indicates a user issue rather than an availability problem. +To ensure high availability when you have multiple instances of Config Server deployed and expect one or more instances to be unavailable or unable to honor requests from time to time (such as if the Git server is down), you can either specify multiple URLs (as a comma-separated list under the `spring.cloud.config.uri` property) or have all your instances register in a Service Registry like Eureka (if using Discovery-First Bootstrap mode). + +The URLs listed under `spring.cloud.config.uri` are tried in the order listed. By default, the Config Client will try to fetch properties from each URL until an attempt is successful to ensure high availability. + +However, if you want to ensure high availability only when the Config Server is not running (that is, when the application has exited) or when a connection timeout has occurred, set `spring.cloud.config.multiple-uri-strategy` to `connection-timeout-only`. (The default value of `spring.cloud.config.multiple-uri-strategy` is `always`.) For example, if the Config Server returns a 500 (Internal Server Error) response or the Config Client receives a 401 from the Config Server (due to bad credentials or other causes), the Config Client does not try to fetch properties from other URLs. A 400 error (except possibly 404) indicates a user issue rather than an availability problem. Note that if the Config Server is set to use a Git server and the call to Git server fails, a 404 error may occur. + +Several locations can be specified under a single `spring.config.import` key instead of `spring.cloud.config.uri`. Locations will be processed in the order that they are defined, with later imports taking precedence. However, if `spring.cloud.config.fail-fast` is `true`, the Config Client will fail if the first Config Server call is unsuccessful for any reason. If `fail-fast` is `false`, it will try all URLs until one call is successful, regardless of the reason for failure. (The `spring.cloud.config.multiple-uri-strategy` does not apply when specifying URLs under `spring.config.import`.) If you use HTTP basic security on your Config Server, it is currently possible to support per-Config Server auth credentials only if you embed the credentials in each URL you specify under the `spring.cloud.config.uri` property. If you use any other kind of security mechanism, you cannot (currently) support per-Config Server authentication and authorization. diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientProperties.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientProperties.java index c584068b..85af4ab2 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientProperties.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientProperties.java @@ -116,6 +116,12 @@ public class ConfigClientProperties { */ private String[] uri = { "http://localhost:8888" }; + /** + * The strategy to use when call to server fails and there are multiple URLs + * configured on the uri property (default {@link MultipleUriStrategy#ALWAYS}). + */ + private MultipleUriStrategy multipleUriStrategy = MultipleUriStrategy.ALWAYS; + /** * The Accept header media type to send to config server. */ @@ -188,6 +194,14 @@ public class ConfigClientProperties { this.uri = url; } + public MultipleUriStrategy getMultipleUriStrategy() { + return multipleUriStrategy; + } + + public void setMultipleUriStrategy(MultipleUriStrategy multipleUriStrategy) { + this.multipleUriStrategy = multipleUriStrategy; + } + public String getName() { return this.name; } @@ -458,4 +472,22 @@ public class ConfigClientProperties { } + /** + * Enumerates possible strategies to use when multiple URLs are provided and an error + * occurs. + */ + public enum MultipleUriStrategy { + + /** + * Try the next URL in the list on any error. + */ + ALWAYS, + + /** + * Try the next URL in the list only if no response was received. + */ + CONNECTION_TIMEOUT_ONLY + + } + } diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLoader.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLoader.java index ff385704..3f02ef63 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLoader.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLoader.java @@ -36,6 +36,7 @@ import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.env.OriginTrackedMapPropertySource; import org.springframework.boot.origin.Origin; import org.springframework.boot.origin.OriginTrackedValue; +import org.springframework.cloud.config.client.ConfigClientProperties.MultipleUriStrategy; import org.springframework.cloud.config.client.ConfigServerBootstrapper.LoadContext; import org.springframework.cloud.config.client.ConfigServerBootstrapper.LoaderInterceptor; import org.springframework.cloud.config.environment.Environment; @@ -301,13 +302,19 @@ public class ConfigServerConfigDataLoader implements ConfigDataLoader entity = new HttpEntity<>((Void) null, headers); response = restTemplate.exchange(uri + path, HttpMethod.GET, entity, Environment.class, args); } - catch (HttpClientErrorException e) { + catch (HttpClientErrorException | HttpServerErrorException e) { + if (i < noOfUrls - 1 && properties.getMultipleUriStrategy() == MultipleUriStrategy.ALWAYS) { + logger.info("Failed to fetch configs from server at : " + uri + + ". Will try the next url if available. Error : " + e.getMessage()); + continue; + } + if (e.getStatusCode() != HttpStatus.NOT_FOUND) { throw e; } } catch (ResourceAccessException e) { - logger.info("Connect Timeout Exception on Url - " + uri + ". Will be trying the next url if available"); + logger.info("Connect Timeout Exception on Url - " + uri + ". Will try the next url if available"); if (i == noOfUrls - 1) { throw e; } diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocator.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocator.java index d85ae801..cb42b235 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocator.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocator.java @@ -33,6 +33,7 @@ import org.springframework.boot.origin.OriginTrackedValue; import org.springframework.cloud.bootstrap.config.PropertySourceLocator; import org.springframework.cloud.bootstrap.support.OriginTrackedCompositePropertySource; import org.springframework.cloud.config.client.ConfigClientProperties.Credentials; +import org.springframework.cloud.config.client.ConfigClientProperties.MultipleUriStrategy; import org.springframework.cloud.config.client.validation.InvalidApplicationNameException; import org.springframework.cloud.config.environment.Environment; import org.springframework.cloud.config.environment.PropertySource; @@ -60,7 +61,7 @@ import static org.springframework.cloud.config.client.ConfigClientProperties.TOK /** * @author Dave Syer * @author Mathieu Ouellet - * + * @author Marnee DeRider */ @Order(0) public class ConfigServicePropertySourceLocator implements PropertySourceLocator { @@ -254,13 +255,19 @@ public class ConfigServicePropertySourceLocator implements PropertySourceLocator final HttpEntity entity = new HttpEntity<>((Void) null, headers); response = restTemplate.exchange(uri + path, HttpMethod.GET, entity, Environment.class, args); } - catch (HttpClientErrorException e) { + catch (HttpClientErrorException | HttpServerErrorException e) { + if (i < noOfUrls - 1 && defaultProperties.getMultipleUriStrategy() == MultipleUriStrategy.ALWAYS) { + logger.info("Failed to fetch configs from server at : " + uri + + ". Will try the next url if available. Error : " + e.getMessage()); + continue; + } + if (e.getStatusCode() != HttpStatus.NOT_FOUND) { throw e; } } catch (ResourceAccessException e) { - logger.info("Connect Timeout Exception on Url - " + uri + ". Will be trying the next url if available"); + logger.info("Connect Timeout Exception on Url - " + uri + ". Will try the next url if available"); if (i == noOfUrls - 1) { throw e; } diff --git a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigClientPropertiesTests.java b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigClientPropertiesTests.java index d1e5d942..86fcfe9d 100644 --- a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigClientPropertiesTests.java +++ b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigClientPropertiesTests.java @@ -22,6 +22,7 @@ import org.junit.rules.ExpectedException; import org.springframework.boot.test.util.TestPropertyValues; import org.springframework.cloud.config.client.ConfigClientProperties.Credentials; +import org.springframework.cloud.config.client.ConfigClientProperties.MultipleUriStrategy; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.StandardEnvironment; import org.springframework.mock.env.MockEnvironment; @@ -170,4 +171,20 @@ public class ConfigClientPropertiesTests { Credentials credentials = this.locator.getCredentials(2); } + @Test + public void testThatDefaultMultipleUriStrategyIsAlways() { + ConfigClientProperties properties = new ConfigClientProperties(new MockEnvironment()); + assertThat(properties.getMultipleUriStrategy()).isNotNull(); + assertThat(properties.getMultipleUriStrategy().name()).isEqualTo(MultipleUriStrategy.ALWAYS.name()); + } + + @Test + public void testThatExplicitMultipleUriStrategyTakesPrecedence() { + ConfigClientProperties properties = new ConfigClientProperties(new MockEnvironment()); + properties.setMultipleUriStrategy(MultipleUriStrategy.CONNECTION_TIMEOUT_ONLY); + assertThat(properties.getMultipleUriStrategy()).isNotNull(); + assertThat(properties.getMultipleUriStrategy().name()) + .isEqualTo(MultipleUriStrategy.CONNECTION_TIMEOUT_ONLY.name()); + } + } diff --git a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServerConfigDataLoaderTests.java b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServerConfigDataLoaderTests.java new file mode 100644 index 00000000..85b6f1aa --- /dev/null +++ b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServerConfigDataLoaderTests.java @@ -0,0 +1,543 @@ +/* + * Copyright 2022-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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 org.springframework.cloud.config.client; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.URI; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; +import org.mockito.Captor; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import org.springframework.boot.ConfigurableBootstrapContext; +import org.springframework.boot.context.config.ConfigDataLoaderContext; +import org.springframework.boot.test.util.TestPropertyValues; +import org.springframework.cloud.config.environment.Environment; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.mock.http.client.MockClientHttpRequest; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.client.ResourceAccessException; +import org.springframework.web.client.RestTemplate; + +import static java.lang.String.format; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.cloud.config.client.ConfigClientProperties.AUTHORIZATION; +import static org.springframework.cloud.config.environment.EnvironmentMediaType.V2_JSON; + +/** + * Unit Test for {@link ConfigServerConfigDataLoader}. + * + *

+ * This test was based on {@link ConfigServicePropertySourceLocatorTests}. The + * {@link ConfigServicePropertySourceLocator} is used only when legacy bootstrap is in + * use. Otherwise, {@link ConfigServerConfigDataLoader} is used. + *

+ * + * @author Marnee DeRider + */ +public class ConfigServerConfigDataLoaderTests { + + private static final Log logger = LogFactory.getLog(ConfigServerConfigDataLoaderTests.class); + + private static final String LABEL = "main"; + + private static final String NAME = "application"; + + private static final String PROFILES = "dev"; + + private static final String URI_TEMPLATE = "%s/%s/%s/%s"; + + @Captor + private ArgumentCaptor> httpEntityArgumentCaptor; + + private ConfigurableBootstrapContext bootstrapContext; + + private ConfigDataLoaderContext context; + + private ConfigurableEnvironment environment; + + private ConfigServerConfigDataLoader loader; + + private ConfigClientProperties properties; + + private ConfigServerConfigDataResource resource; + + private RestTemplate restTemplate; + + @BeforeEach + public void init() { + MockitoAnnotations.openMocks(this); + + environment = new StandardEnvironment(); + loader = new ConfigServerConfigDataLoader(logger); + restTemplate = mock(RestTemplate.class); + context = mock(ConfigDataLoaderContext.class); + bootstrapContext = mock(ConfigurableBootstrapContext.class); + resource = mock(ConfigServerConfigDataResource.class); + properties = new ConfigClientProperties(this.environment); + + properties.setName(NAME); + properties.setLabel(LABEL); + + when(context.getBootstrapContext()).thenReturn(bootstrapContext); + when(bootstrapContext.get(ConfigClientRequestTemplateFactory.class)) + .thenReturn(mock(ConfigClientRequestTemplateFactory.class)); + when(bootstrapContext.get(RestTemplate.class)).thenReturn(restTemplate); + when(resource.getProperties()).thenReturn(properties); + + when(resource.getProfiles()).thenReturn(PROFILES); + } + + @SuppressWarnings("unchecked") + @Test + public void sunnyDayWithoutLabel() { + Environment body = new Environment("app", "master"); + mockRequestResponseWithoutLabel(new ResponseEntity<>(body, HttpStatus.OK)); + + properties.setLabel(null); + + assertThat(this.loader.load(context, resource)).isNotNull(); + + Mockito.verify(this.restTemplate).exchange(anyString(), any(HttpMethod.class), + httpEntityArgumentCaptor.capture(), any(Class.class), anyString(), anyString()); + + HttpEntity httpEntity = httpEntityArgumentCaptor.getValue(); + assertThat(httpEntity.getHeaders().getAccept()).containsExactly(MediaType.parseMediaType(V2_JSON)); + } + + @Test + public void customMediaType() { + Environment body = new Environment("app", "master"); + mockRequestResponseWithoutLabel(new ResponseEntity<>(body, HttpStatus.OK)); + properties.setMediaType("application/json"); + properties.setLabel(null); + + assertThat(loader.load(context, resource)).isNotNull(); + + Mockito.verify(this.restTemplate).exchange(anyString(), any(HttpMethod.class), + httpEntityArgumentCaptor.capture(), ArgumentMatchers.>any(), anyString(), + anyString()); + + HttpEntity httpEntity = httpEntityArgumentCaptor.getValue(); + assertThat(httpEntity.getHeaders().getAccept()).containsExactly(MediaType.parseMediaType("application/json")); + } + + @Test + public void sunnyDayWithLabel() { + Environment body = new Environment("app", "master"); + properties.setLabel("v1.0.0"); + mockRequestResponseWithLabel(new ResponseEntity<>(body, HttpStatus.OK), "v1.0.0"); + TestPropertyValues.of("spring.cloud.config.label:v1.0.0").applyTo(this.environment); + assertThat(this.loader.load(context, resource)).isNotNull(); + } + + @Test + public void sunnyDayWithLabelThatContainsASlash() { + Environment body = new Environment("app", "master"); + String label = "release(_)v1.0.0"; + mockRequestResponseWithLabel(new ResponseEntity<>(body, HttpStatus.OK), label); + properties.setLabel(label); + TestPropertyValues.of("spring.cloud.config.label:release/v1.0.0").applyTo(this.environment); + assertThat(this.loader.load(context, resource)).isNotNull(); + } + + @Test + public void failFast() throws Exception { + ClientHttpRequestFactory requestFactory = mock(ClientHttpRequestFactory.class); + mockRequestResponse(requestFactory, null, HttpStatus.INTERNAL_SERVER_ERROR); + RestTemplate restTemplate = new RestTemplate(requestFactory); + properties.setFailFast(true); + when(bootstrapContext.get(RestTemplate.class)).thenReturn(restTemplate); + ConfigClientFailFastException exception = Assertions.assertThrows(ConfigClientFailFastException.class, + () -> this.loader.load(context, resource)); + assertThat(exception.getCause()).isInstanceOf(HttpServerErrorException.class); + assertThat(exception.getMessage()).contains("fail fast property is set"); + } + + @Test + public void failFastWhenNotFound() throws Exception { + ClientHttpRequestFactory requestFactory = mock(ClientHttpRequestFactory.class); + mockRequestResponse(requestFactory, null, HttpStatus.NOT_FOUND); + RestTemplate restTemplate = new RestTemplate(requestFactory); + properties.setFailFast(true); + properties.setLabel("WeSetUpToReturn_NOT_FOUND_ForThisLabel"); + when(bootstrapContext.get(RestTemplate.class)).thenReturn(restTemplate); + ConfigClientFailFastException exception = Assertions.assertThrows(ConfigClientFailFastException.class, + () -> this.loader.load(context, resource)); + assertThat(exception.getMessage()).contains( + "fail fast property is set, failing: None of labels [WeSetUpToReturn_NOT_FOUND_ForThisLabel] found"); + } + + @Test + public void failFastWhenRequestTimesOut() { + mockRequestTimedOut(); + properties.setFailFast(true); + ConfigClientFailFastException exception = Assertions.assertThrows(ConfigClientFailFastException.class, + () -> this.loader.load(context, resource)); + assertThat(exception.getCause()).isExactlyInstanceOf(ResourceAccessException.class); + assertThat(exception.getMessage()).contains("fail fast property is set"); + + } + + @Test + public void failFastWhenBothPasswordAndAuthorizationPropertiesSet() throws Exception { + ClientHttpRequestFactory requestFactory = mock(ClientHttpRequestFactory.class); + ClientHttpRequest request = mock(ClientHttpRequest.class); + when(requestFactory.createRequest(any(URI.class), any(HttpMethod.class))).thenReturn(request); + properties.setFailFast(true); + properties.setUsername("username"); + properties.setPassword("password"); + properties.getHeaders().put(AUTHORIZATION, "Basic dXNlcm5hbWU6cGFzc3dvcmQNCg=="); + IllegalStateException exception = Assertions.assertThrows(IllegalStateException.class, + () -> this.loader.load(context, resource)); + assertThat(exception.getMessage()) + .contains("Could not locate PropertySource and the fail fast property is set, failing"); + } + + @Test + public void interceptorShouldAddHeadersWhenHeadersPropertySet() throws Exception { + MockClientHttpRequest request = new MockClientHttpRequest(); + ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class); + byte[] body = new byte[] {}; + Map headers = new HashMap<>(); + headers.put("X-Example-Version", "2.1"); + new ConfigClientRequestTemplateFactory.GenericRequestHeaderInterceptor(headers).intercept(request, body, + execution); + Mockito.verify(execution).execute(request, body); + assertThat(request.getHeaders().getFirst("X-Example-Version")).isEqualTo("2.1"); + } + + @Test + public void shouldAddAuthorizationHeaderWhenPasswordSet() { + HttpHeaders headers = new HttpHeaders(); + String username = "user"; + String password = "pass"; + factory(properties).addAuthorizationToken(headers, username, password); + assertThat(headers).hasSize(1); + } + + @Test + public void shouldAddAuthorizationHeaderWhenAuthorizationSet() { + HttpHeaders headers = new HttpHeaders(); + properties.getHeaders().put(AUTHORIZATION, "Basic dXNlcm5hbWU6cGFzc3dvcmQNCg=="); + String username = "user"; + factory(properties).addAuthorizationToken(headers, username, null); + assertThat(headers).hasSize(1); + } + + @Test + public void shouldThrowExceptionWhenPasswordAndAuthorizationBothSet() { + HttpHeaders headers = new HttpHeaders(); + properties.getHeaders().put(AUTHORIZATION, "Basic dXNlcm5hbWU6cGFzc3dvcmQNCg=="); + String username = "user"; + String password = "pass"; + IllegalStateException exception = Assertions.assertThrows(IllegalStateException.class, + () -> factory(properties).addAuthorizationToken(headers, username, password)); + assertThat(exception.getMessage()).contains("You must set either 'password' or 'authorization'"); + } + + @Test + public void shouldThrowExceptionWhenNegativeReadTimeoutSet() { + properties.setRequestReadTimeout(-1); + IllegalStateException exception = Assertions.assertThrows(IllegalStateException.class, + () -> factory(properties).create()); + assertThat(exception.getMessage()).contains("Invalid Value for Read Timeout set."); + + } + + @Test + public void shouldThrowExceptionWhenNegativeConnectTimeoutSet() { + properties.setRequestConnectTimeout(-1); + IllegalStateException exception = Assertions.assertThrows(IllegalStateException.class, + () -> factory(properties).create()); + assertThat(exception.getMessage()).contains("Invalid Value for Connect Timeout set."); + } + + @Test + public void shouldNotUseNextUriFor_400_And_CONNECTION_TIMEOUT_ONLY_Strategy() throws Exception { + assertNextUriIsNotTriedForClientError(ConfigClientProperties.MultipleUriStrategy.CONNECTION_TIMEOUT_ONLY, + HttpStatus.BAD_REQUEST); + } + + @Test + public void shouldUseNextUriFor_400_And_ALWAYS_Strategy() throws Exception { + assertNextUriIsTried(ConfigClientProperties.MultipleUriStrategy.ALWAYS, HttpStatus.BAD_REQUEST); + } + + @Test + public void shouldNotUseNextUriFor_404_And_CONNECTION_TIMEOUT_ONLY_Strategy() throws Exception { + assertNextUriIsNotTriedForNotFoundError(ConfigClientProperties.MultipleUriStrategy.CONNECTION_TIMEOUT_ONLY, + HttpStatus.NOT_FOUND); + } + + @Test + public void shouldUseNextUriFor_404_And_ALWAYS_Strategy() throws Exception { + assertNextUriIsTried(ConfigClientProperties.MultipleUriStrategy.ALWAYS, HttpStatus.NOT_FOUND); + } + + @Test + public void shouldNotUseNextUriFor_500_And_CONNECTION_TIMEOUT_ONLY_Strategy() throws Exception { + assertNextUriIsNotTriedForServerError(ConfigClientProperties.MultipleUriStrategy.CONNECTION_TIMEOUT_ONLY, + HttpStatus.INTERNAL_SERVER_ERROR); + } + + @Test + public void shouldUseNextUriFor_500_And_ALWAYS_Strategy() throws Exception { + assertNextUriIsTried(ConfigClientProperties.MultipleUriStrategy.ALWAYS, HttpStatus.INTERNAL_SERVER_ERROR); + } + + @Test + public void shouldUseNextUriFor_TimeOut_And_ALWAYS_Strategy() throws Exception { + assertNextUriIsTriedOnTimeout(ConfigClientProperties.MultipleUriStrategy.ALWAYS); + } + + @Test + public void shouldUseNextUriFor_TimeOut_And_CONNECTION_TIMEOUT_ONLY_Strategy() throws Exception { + assertNextUriIsTriedOnTimeout(ConfigClientProperties.MultipleUriStrategy.CONNECTION_TIMEOUT_ONLY); + } + + @Test + public void shouldNotUseNextUriWhenOneIsSuccessful() throws Exception { + // Set up with three URIs. + String badURI1 = "http://baduri1"; + String goodURI = "http://localhost:8888"; + String badURI2 = "http://baduri2"; + String[] uris = new String[] { badURI1, goodURI, badURI2 }; + properties.setUri(uris); + properties.setFailFast(true); + properties.setMultipleUriStrategy(ConfigClientProperties.MultipleUriStrategy.ALWAYS); + this.loader = new ConfigServerConfigDataLoader(logger); + ClientHttpRequestFactory requestFactory = mock(ClientHttpRequestFactory.class); + RestTemplate restTemplate = new RestTemplate(requestFactory); + when(bootstrapContext.get(RestTemplate.class)).thenReturn(restTemplate); + + // Second URI will be successful, and the third one should never be called, so + // locateCollection + // should return a value. + mockRequestResponse(requestFactory, badURI1, HttpStatus.BAD_REQUEST); + mockRequestResponse(requestFactory, goodURI, HttpStatus.OK); + mockRequestResponse(requestFactory, badURI2, HttpStatus.INTERNAL_SERVER_ERROR); + + assertThat(this.loader.load(context, resource)).isNotNull(); + } + + @Test + public void shouldUseMultipleURIs() throws Exception { + // Set up with four URIs. All should be called until one is successful + String badURI1 = "http://baduri1"; + String badURI2 = "http://baduri2"; + String badURI3 = "http://baduri3"; + String goodURI = "http://localhost:8888"; + String[] uris = new String[] { badURI1, goodURI, badURI2 }; + properties.setUri(uris); + properties.setFailFast(true); + properties.setMultipleUriStrategy(ConfigClientProperties.MultipleUriStrategy.ALWAYS); + this.loader = new ConfigServerConfigDataLoader(logger); + ClientHttpRequestFactory requestFactory = mock(ClientHttpRequestFactory.class); + RestTemplate restTemplate = new RestTemplate(requestFactory); + when(bootstrapContext.get(RestTemplate.class)).thenReturn(restTemplate); + + // Second URI will be successful, and the third one should never be called, so + // locateCollection + // should return a value. + mockRequestResponse(requestFactory, badURI1, HttpStatus.BAD_REQUEST); + mockRequestResponse(requestFactory, badURI2, HttpStatus.INTERNAL_SERVER_ERROR); + mockRequestResponse(requestFactory, badURI3, HttpStatus.NOT_FOUND); + mockRequestResponse(requestFactory, goodURI, HttpStatus.OK); + + assertThat(this.loader.load(context, resource)).isNotNull(); + } + + private ConfigClientRequestTemplateFactory factory(ConfigClientProperties properties) { + return new ConfigClientRequestTemplateFactory(LogFactory.getLog(getClass()), properties); + } + + private void assertNextUriIsNotTried(ConfigClientProperties.MultipleUriStrategy multipleUriStrategy, + HttpStatus firstUriResponse, Class expectedCause) throws Exception { + // Set up with two URIs. + String badURI = "http://baduri"; + String goodURI = "http://localhost:8888"; + String[] uris = new String[] { badURI, goodURI }; + properties.setUri(uris); + properties.setFailFast(true); + // Strategy is CONNECTION_TIMEOUT_ONLY, so it should not try the next URI for + // INTERNAL_SERVER_ERROR + properties.setMultipleUriStrategy(multipleUriStrategy); + this.loader = new ConfigServerConfigDataLoader(logger); + ClientHttpRequestFactory requestFactory = mock(ClientHttpRequestFactory.class); + RestTemplate restTemplate = new RestTemplate(requestFactory); + mockRequestResponse(requestFactory, badURI, firstUriResponse); + mockRequestResponse(requestFactory, goodURI, HttpStatus.OK); + when(bootstrapContext.get(RestTemplate.class)).thenReturn(restTemplate); + + ConfigClientFailFastException exception = Assertions.assertThrows(ConfigClientFailFastException.class, + () -> this.loader.load(context, resource)); + if (expectedCause != null) { + assertThat(exception.getCause()).isInstanceOf(expectedCause); + } + assertThat(exception.getMessage()).contains("fail fast property is set"); + } + + @SuppressWarnings("SameParameterValue") + private void assertNextUriIsNotTriedForClientError(ConfigClientProperties.MultipleUriStrategy multipleUriStrategy, + HttpStatus firstUriResponse) throws Exception { + + assertNextUriIsNotTried(multipleUriStrategy, firstUriResponse, HttpClientErrorException.class); + } + + @SuppressWarnings("SameParameterValue") + private void assertNextUriIsNotTriedForNotFoundError(ConfigClientProperties.MultipleUriStrategy multipleUriStrategy, + HttpStatus firstUriResponse) throws Exception { + + // NOT_FOUND is treated differently + assertNextUriIsNotTried(multipleUriStrategy, firstUriResponse, null); + } + + @SuppressWarnings("SameParameterValue") + private void assertNextUriIsNotTriedForServerError(ConfigClientProperties.MultipleUriStrategy multipleUriStrategy, + HttpStatus firstUriResponse) throws Exception { + + assertNextUriIsNotTried(multipleUriStrategy, firstUriResponse, HttpServerErrorException.class); + } + + @SuppressWarnings("SameParameterValue") + private void assertNextUriIsTried(ConfigClientProperties.MultipleUriStrategy multipleUriStrategy, + HttpStatus firstUriResponse) throws Exception { + + // Set up with two URIs. + String badURI = "http://baduri"; + String goodURI = "http://localhost:8888"; + String[] uris = new String[] { badURI, goodURI }; + properties.setUri(uris); + properties.setFailFast(true); + // Strategy is ALWAYS, so it should try all URIs until successful + properties.setMultipleUriStrategy(multipleUriStrategy); + this.loader = new ConfigServerConfigDataLoader(logger); + ClientHttpRequestFactory requestFactory = mock(ClientHttpRequestFactory.class); + RestTemplate restTemplate = new RestTemplate(requestFactory); + mockRequestResponse(requestFactory, badURI, firstUriResponse); + mockRequestResponse(requestFactory, goodURI, HttpStatus.OK); + when(bootstrapContext.get(RestTemplate.class)).thenReturn(restTemplate); + + assertThat(this.loader.load(context, resource)).isNotNull(); + } + + private void assertNextUriIsTriedOnTimeout(ConfigClientProperties.MultipleUriStrategy multipleUriStrategy) + throws Exception { + // Set up with two URIs. + String badURI = "http://baduri"; + String goodURI = "http://localhost:8888"; + String[] uris = new String[] { badURI, goodURI }; + properties.setUri(uris); + properties.setFailFast(true); + // Strategy should not matter when the error is connection timed out + properties.setMultipleUriStrategy(multipleUriStrategy); + this.loader = new ConfigServerConfigDataLoader(logger); + ClientHttpRequestFactory requestFactory = mock(ClientHttpRequestFactory.class); + RestTemplate restTemplate = new RestTemplate(requestFactory); + when(bootstrapContext.get(RestTemplate.class)).thenReturn(restTemplate); + + // First URI times out. Second one is successful + mockRequestTimedOut(requestFactory, badURI); + mockRequestResponse(requestFactory, goodURI, HttpStatus.OK); + assertThat(this.loader.load(context, resource)).isNotNull(); + } + + private void mockRequestResponse(ClientHttpRequestFactory requestFactory, String baseURI, HttpStatus status) + throws Exception { + ClientHttpRequest request = mock(ClientHttpRequest.class); + ClientHttpResponse response = mock(ClientHttpResponse.class); + + if (baseURI == null) { + when(requestFactory.createRequest(any(URI.class), any(HttpMethod.class))).thenReturn(request); + } + else { + when(requestFactory.createRequest(eq(new URI(format(URI_TEMPLATE, baseURI, NAME, PROFILES, LABEL))), + any(HttpMethod.class))).thenReturn(request); + } + + when(request.getHeaders()).thenReturn(new HttpHeaders()); + when(request.execute()).thenReturn(response); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + when(response.getHeaders()).thenReturn(headers); + when(response.getRawStatusCode()).thenReturn(status.value()); + when(response.getBody()).thenReturn(new ByteArrayInputStream("{}".getBytes())); + } + + @SuppressWarnings("unchecked") + private void mockRequestResponseWithLabel(ResponseEntity response, String label) { + when(this.restTemplate.exchange(any(String.class), any(HttpMethod.class), any(HttpEntity.class), + any(Class.class), anyString(), anyString(), eq(label))).thenReturn(response); + } + + @SuppressWarnings("unchecked") + private void mockRequestResponseWithoutLabel(ResponseEntity response) { + when(this.restTemplate.exchange(any(String.class), any(HttpMethod.class), any(HttpEntity.class), + any(Class.class), ArgumentMatchers.any())).thenReturn(response); + } + + @SuppressWarnings("unchecked") + private void mockRequestTimedOut() { + when(this.restTemplate.exchange(any(String.class), any(HttpMethod.class), any(HttpEntity.class), + any(Class.class), ArgumentMatchers.any())).thenThrow(ResourceAccessException.class); + } + + private void mockRequestTimedOut(ClientHttpRequestFactory requestFactory, String baseURI) throws Exception { + ClientHttpRequest request = mock(ClientHttpRequest.class); + + if (baseURI == null) { + when(requestFactory.createRequest(any(URI.class), any(HttpMethod.class))).thenReturn(request); + } + else { + when(requestFactory.createRequest(eq(new URI(format(URI_TEMPLATE, baseURI, NAME, PROFILES, LABEL))), + any(HttpMethod.class))).thenReturn(request); + } + + when(request.getHeaders()).thenReturn(new HttpHeaders()); + when(request.execute()).thenThrow(IOException.class); + } + +} diff --git a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocatorTests.java b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocatorTests.java index 39d61edb..6148f4c7 100644 --- a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocatorTests.java +++ b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocatorTests.java @@ -17,6 +17,7 @@ package org.springframework.cloud.config.client; import java.io.ByteArrayInputStream; +import java.io.IOException; import java.net.URI; import java.util.ArrayList; import java.util.HashMap; @@ -52,6 +53,9 @@ import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.http.client.ClientHttpResponse; import org.springframework.mock.http.client.MockClientHttpRequest; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestTemplate; import static org.assertj.core.api.Assertions.assertThat; @@ -158,23 +162,14 @@ public class ConfigServicePropertySourceLocatorTests { @Test public void failFast() throws Exception { ClientHttpRequestFactory requestFactory = Mockito.mock(ClientHttpRequestFactory.class); - ClientHttpRequest request = Mockito.mock(ClientHttpRequest.class); - ClientHttpResponse response = Mockito.mock(ClientHttpResponse.class); - Mockito.when(requestFactory.createRequest(Mockito.any(URI.class), Mockito.any(HttpMethod.class))) - .thenReturn(request); + mockRequestResponse(requestFactory, null, HttpStatus.INTERNAL_SERVER_ERROR); RestTemplate restTemplate = new RestTemplate(requestFactory); ConfigClientProperties defaults = new ConfigClientProperties(this.environment); defaults.setFailFast(true); this.locator = new ConfigServicePropertySourceLocator(defaults); - Mockito.when(request.getHeaders()).thenReturn(new HttpHeaders()); - Mockito.when(request.execute()).thenReturn(response); - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); - Mockito.when(response.getHeaders()).thenReturn(headers); - Mockito.when(response.getStatusCode()).thenReturn(HttpStatus.INTERNAL_SERVER_ERROR); - Mockito.when(response.getBody()).thenReturn(new ByteArrayInputStream("{}".getBytes())); this.locator.setRestTemplate(restTemplate); - this.expected.expectCause(IsInstanceOf.instanceOf(IllegalArgumentException.class)); + this.expected.expect(IsInstanceOf.instanceOf(IllegalStateException.class)); + this.expected.expectCause(IsInstanceOf.instanceOf(HttpServerErrorException.class)); this.expected.expectMessage("fail fast property is set"); this.locator.locateCollection(this.environment); } @@ -182,23 +177,26 @@ public class ConfigServicePropertySourceLocatorTests { @Test public void failFastWhenNotFound() throws Exception { ClientHttpRequestFactory requestFactory = Mockito.mock(ClientHttpRequestFactory.class); - ClientHttpRequest request = Mockito.mock(ClientHttpRequest.class); - ClientHttpResponse response = Mockito.mock(ClientHttpResponse.class); - Mockito.when(requestFactory.createRequest(Mockito.any(URI.class), Mockito.any(HttpMethod.class))) - .thenReturn(request); + mockRequestResponse(requestFactory, null, HttpStatus.NOT_FOUND); RestTemplate restTemplate = new RestTemplate(requestFactory); ConfigClientProperties defaults = new ConfigClientProperties(this.environment); defaults.setFailFast(true); this.locator = new ConfigServicePropertySourceLocator(defaults); - Mockito.when(request.getHeaders()).thenReturn(new HttpHeaders()); - Mockito.when(request.execute()).thenReturn(response); - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); - Mockito.when(response.getHeaders()).thenReturn(headers); - Mockito.when(response.getStatusCode()).thenReturn(HttpStatus.NOT_FOUND); - Mockito.when(response.getBody()).thenReturn(new ByteArrayInputStream("".getBytes())); this.locator.setRestTemplate(restTemplate); - this.expected.expectCause(IsInstanceOf.instanceOf(IllegalArgumentException.class)); + this.expected.expect(IsInstanceOf.instanceOf(IllegalStateException.class)); + this.expected.expectMessage("fail fast property is set, failing: None of labels [] found"); + this.locator.locateCollection(this.environment); + } + + @Test + public void failFastWhenRequestTimesOut() { + mockRequestTimedOut(); + ConfigClientProperties defaults = new ConfigClientProperties(this.environment); + defaults.setFailFast(true); + this.locator = new ConfigServicePropertySourceLocator(defaults); + this.locator.setRestTemplate(this.restTemplate); + this.expected.expect(IsInstanceOf.instanceOf(IllegalStateException.class)); + this.expected.expectCause(IsInstanceOf.instanceOf(ResourceAccessException.class)); this.expected.expectMessage("fail fast property is set"); this.locator.locateCollection(this.environment); } @@ -283,6 +281,103 @@ public class ConfigServicePropertySourceLocatorTests { factory(defaults).create(); } + @Test + public void shouldNotUseNextUriFor_400_And_CONNECTION_TIMEOUT_ONLY_Strategy() throws Exception { + assertNextUriIsNotTriedForClientError(ConfigClientProperties.MultipleUriStrategy.CONNECTION_TIMEOUT_ONLY, + HttpStatus.BAD_REQUEST); + } + + @Test + public void shouldUseNextUriFor_400_And_ALWAYS_Strategy() throws Exception { + assertNextUriIsTried(ConfigClientProperties.MultipleUriStrategy.ALWAYS, HttpStatus.BAD_REQUEST); + } + + @Test + public void shouldNotUseNextUriFor_404_And_CONNECTION_TIMEOUT_ONLY_Strategy() throws Exception { + assertNextUriIsNotTriedForNotFoundError(ConfigClientProperties.MultipleUriStrategy.CONNECTION_TIMEOUT_ONLY, + HttpStatus.NOT_FOUND); + } + + @Test + public void shouldUseNextUriFor_404_And_ALWAYS_Strategy() throws Exception { + assertNextUriIsTried(ConfigClientProperties.MultipleUriStrategy.ALWAYS, HttpStatus.NOT_FOUND); + } + + @Test + public void shouldNotUseNextUriFor_500_And_CONNECTION_TIMEOUT_ONLY_Strategy() throws Exception { + assertNextUriIsNotTriedForServerError(ConfigClientProperties.MultipleUriStrategy.CONNECTION_TIMEOUT_ONLY, + HttpStatus.INTERNAL_SERVER_ERROR); + } + + @Test + public void shouldUseNextUriFor_500_And_ALWAYS_Strategy() throws Exception { + assertNextUriIsTried(ConfigClientProperties.MultipleUriStrategy.ALWAYS, HttpStatus.INTERNAL_SERVER_ERROR); + } + + @Test + public void shouldUseNextUriFor_TimeOut_And_ALWAYS_Strategy() throws Exception { + assertNextUriIsTriedOnTimeout(ConfigClientProperties.MultipleUriStrategy.ALWAYS); + } + + @Test + public void shouldUseNextUriFor_TimeOut_And_CONNECTION_TIMEOUT_ONLY_Strategy() throws Exception { + assertNextUriIsTriedOnTimeout(ConfigClientProperties.MultipleUriStrategy.CONNECTION_TIMEOUT_ONLY); + } + + @Test + public void shouldNotUseNextUriWhenOneIsSuccessful() throws Exception { + // Set up with three URIs. + ConfigClientProperties clientProperties = new ConfigClientProperties(this.environment); + String badURI1 = "http://baduri1"; + String goodURI = "http://localhost:8888"; + String badURI2 = "http://baduri2"; + String[] uris = new String[] { badURI1, goodURI, badURI2 }; + clientProperties.setUri(uris); + clientProperties.setFailFast(true); + clientProperties.setMultipleUriStrategy(ConfigClientProperties.MultipleUriStrategy.ALWAYS); + this.locator = new ConfigServicePropertySourceLocator(clientProperties); + ClientHttpRequestFactory requestFactory = Mockito.mock(ClientHttpRequestFactory.class); + RestTemplate restTemplate = new RestTemplate(requestFactory); + + // Second URI will be successful, and the third one should never be called, so + // locateCollection + // should return a value. + mockRequestResponse(requestFactory, badURI1, HttpStatus.BAD_REQUEST); + mockRequestResponse(requestFactory, goodURI, HttpStatus.OK); + mockRequestResponse(requestFactory, badURI2, HttpStatus.INTERNAL_SERVER_ERROR); + + this.locator.setRestTemplate(restTemplate); + assertThat(this.locator.locateCollection(this.environment)).isNotNull(); + } + + @Test + public void shouldUseMultipleURIs() throws Exception { + // Set up with four URIs. All should be called until one is successful + ConfigClientProperties clientProperties = new ConfigClientProperties(this.environment); + String badURI1 = "http://baduri1"; + String badURI2 = "http://baduri2"; + String badURI3 = "http://baduri3"; + String goodURI = "http://localhost:8888"; + String[] uris = new String[] { badURI1, goodURI, badURI2 }; + clientProperties.setUri(uris); + clientProperties.setFailFast(true); + clientProperties.setMultipleUriStrategy(ConfigClientProperties.MultipleUriStrategy.ALWAYS); + this.locator = new ConfigServicePropertySourceLocator(clientProperties); + ClientHttpRequestFactory requestFactory = Mockito.mock(ClientHttpRequestFactory.class); + RestTemplate restTemplate = new RestTemplate(requestFactory); + + // Second URI will be successful, and the third one should never be called, so + // locateCollection + // should return a value. + mockRequestResponse(requestFactory, badURI1, HttpStatus.BAD_REQUEST); + mockRequestResponse(requestFactory, badURI2, HttpStatus.INTERNAL_SERVER_ERROR); + mockRequestResponse(requestFactory, badURI3, HttpStatus.NOT_FOUND); + mockRequestResponse(requestFactory, goodURI, HttpStatus.OK); + + this.locator.setRestTemplate(restTemplate); + assertThat(this.locator.locateCollection(this.environment)).isNotNull(); + } + @Test public void checkInterceptorHasNoAuthorizationHeaderPresent() { ConfigClientProperties defaults = new ConfigClientProperties(this.environment); @@ -344,6 +439,98 @@ public class ConfigServicePropertySourceLocatorTests { assertThat(source).isInstanceOf(LinkedHashMap.class); } + private void assertNextUriIsNotTried(ConfigClientProperties.MultipleUriStrategy multipleUriStrategy, + HttpStatus firstUriResponse, Class expectedCause) throws Exception { + // Set up with two URIs. + ConfigClientProperties clientProperties = new ConfigClientProperties(this.environment); + String badURI = "http://baduri"; + String goodURI = "http://localhost:8888"; + String[] uris = new String[] { badURI, goodURI }; + clientProperties.setUri(uris); + clientProperties.setFailFast(true); + // Strategy is CONNECTION_TIMEOUT_ONLY, so it should not try the next URI for + // INTERNAL_SERVER_ERROR + clientProperties.setMultipleUriStrategy(multipleUriStrategy); + this.locator = new ConfigServicePropertySourceLocator(clientProperties); + ClientHttpRequestFactory requestFactory = Mockito.mock(ClientHttpRequestFactory.class); + RestTemplate restTemplate = new RestTemplate(requestFactory); + mockRequestResponse(requestFactory, badURI, firstUriResponse); + mockRequestResponse(requestFactory, goodURI, HttpStatus.OK); + this.locator.setRestTemplate(restTemplate); + this.expected.expect(IsInstanceOf.instanceOf(IllegalStateException.class)); + if (expectedCause != null) { + this.expected.expectCause(IsInstanceOf.instanceOf(expectedCause)); + } + this.expected.expectMessage("fail fast property is set"); + this.locator.locateCollection(this.environment); + } + + @SuppressWarnings("SameParameterValue") + private void assertNextUriIsNotTriedForClientError(ConfigClientProperties.MultipleUriStrategy multipleUriStrategy, + HttpStatus firstUriResponse) throws Exception { + + assertNextUriIsNotTried(multipleUriStrategy, firstUriResponse, HttpClientErrorException.class); + } + + @SuppressWarnings("SameParameterValue") + private void assertNextUriIsNotTriedForNotFoundError(ConfigClientProperties.MultipleUriStrategy multipleUriStrategy, + HttpStatus firstUriResponse) throws Exception { + + // NOT_FOUND is treated differently + assertNextUriIsNotTried(multipleUriStrategy, firstUriResponse, null); + } + + @SuppressWarnings("SameParameterValue") + private void assertNextUriIsNotTriedForServerError(ConfigClientProperties.MultipleUriStrategy multipleUriStrategy, + HttpStatus firstUriResponse) throws Exception { + + assertNextUriIsNotTried(multipleUriStrategy, firstUriResponse, HttpServerErrorException.class); + } + + @SuppressWarnings("SameParameterValue") + private void assertNextUriIsTried(ConfigClientProperties.MultipleUriStrategy multipleUriStrategy, + HttpStatus firstUriResponse) throws Exception { + + // Set up with two URIs. + ConfigClientProperties clientProperties = new ConfigClientProperties(this.environment); + String badURI = "http://baduri"; + String goodURI = "http://localhost:8888"; + String[] uris = new String[] { badURI, goodURI }; + clientProperties.setUri(uris); + clientProperties.setFailFast(true); + // Strategy is ALWAYS, so it should try all URIs until successful + clientProperties.setMultipleUriStrategy(multipleUriStrategy); + this.locator = new ConfigServicePropertySourceLocator(clientProperties); + ClientHttpRequestFactory requestFactory = Mockito.mock(ClientHttpRequestFactory.class); + RestTemplate restTemplate = new RestTemplate(requestFactory); + mockRequestResponse(requestFactory, badURI, firstUriResponse); + mockRequestResponse(requestFactory, goodURI, HttpStatus.OK); + this.locator.setRestTemplate(restTemplate); + assertThat(this.locator.locateCollection(this.environment)).isNotNull(); + } + + private void assertNextUriIsTriedOnTimeout(ConfigClientProperties.MultipleUriStrategy multipleUriStrategy) + throws Exception { + // Set up with two URIs. + ConfigClientProperties clientProperties = new ConfigClientProperties(this.environment); + String badURI = "http://baduri"; + String goodURI = "http://localhost:8888"; + String[] uris = new String[] { badURI, goodURI }; + clientProperties.setUri(uris); + clientProperties.setFailFast(true); + // Strategy should not matter when the error is connection timed out + clientProperties.setMultipleUriStrategy(multipleUriStrategy); + this.locator = new ConfigServicePropertySourceLocator(clientProperties); + ClientHttpRequestFactory requestFactory = Mockito.mock(ClientHttpRequestFactory.class); + RestTemplate restTemplate = new RestTemplate(requestFactory); + + // First URI times out. Second one is successful + mockRequestTimedOut(requestFactory, badURI); + mockRequestResponse(requestFactory, goodURI, HttpStatus.OK); + this.locator.setRestTemplate(restTemplate); + assertThat(this.locator.locateCollection(this.environment)).isNotNull(); + } + private Map originValue(String value, String origin) { HashMap map = new HashMap<>(); map.put("value", value); @@ -351,6 +538,30 @@ public class ConfigServicePropertySourceLocatorTests { return map; } + private void mockRequestResponse(ClientHttpRequestFactory requestFactory, String baseURI, HttpStatus status) + throws Exception { + ClientHttpRequest request = Mockito.mock(ClientHttpRequest.class); + ClientHttpResponse response = Mockito.mock(ClientHttpResponse.class); + + if (baseURI == null) { + Mockito.when(requestFactory.createRequest(Mockito.any(URI.class), Mockito.any(HttpMethod.class))) + .thenReturn(request); + } + else { + Mockito.when(requestFactory.createRequest(Mockito.eq(new URI(baseURI + "/application/default")), + Mockito.any(HttpMethod.class))).thenReturn(request); + } + + Mockito.when(request.getHeaders()).thenReturn(new HttpHeaders()); + Mockito.when(request.execute()).thenReturn(response); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + Mockito.when(response.getHeaders()).thenReturn(headers); + Mockito.when(response.getRawStatusCode()).thenReturn(status.value()); + Mockito.when(response.getBody()).thenReturn(new ByteArrayInputStream("{}".getBytes())); + } + @SuppressWarnings("unchecked") private void mockRequestResponseWithLabel(ResponseEntity response, String label) { Mockito.when(this.restTemplate.exchange(Mockito.any(String.class), Mockito.any(HttpMethod.class), @@ -366,10 +577,26 @@ public class ConfigServicePropertySourceLocatorTests { } @SuppressWarnings("unchecked") - private void mockRequestResponseWithoutLabelWithExpectedName(ResponseEntity response, String expectedName) { + private void mockRequestTimedOut() { Mockito.when(this.restTemplate.exchange(Mockito.any(String.class), Mockito.any(HttpMethod.class), - Mockito.any(HttpEntity.class), Mockito.any(Class.class), ArgumentMatchers.eq(expectedName), - anyString())).thenReturn(response); + Mockito.any(HttpEntity.class), Mockito.any(Class.class), anyString(), anyString())) + .thenThrow(ResourceAccessException.class); + } + + private void mockRequestTimedOut(ClientHttpRequestFactory requestFactory, String baseURI) throws Exception { + ClientHttpRequest request = Mockito.mock(ClientHttpRequest.class); + + if (baseURI == null) { + Mockito.when(requestFactory.createRequest(Mockito.any(URI.class), Mockito.any(HttpMethod.class))) + .thenReturn(request); + } + else { + Mockito.when(requestFactory.createRequest(Mockito.eq(new URI(baseURI + "/application/default")), + Mockito.any(HttpMethod.class))).thenReturn(request); + } + + Mockito.when(request.getHeaders()).thenReturn(new HttpHeaders()); + Mockito.when(request.execute()).thenThrow(IOException.class); } }