ReactiveCircuitBreakerAdapterDecoratorTests.java
/*
* Copyright 2013-present 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.client.circuitbreaker.httpservice;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
import org.springframework.cloud.client.circuitbreaker.NoFallbackAvailableException;
import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreaker;
import org.springframework.http.HttpHeaders;
import org.springframework.web.service.invoker.HttpRequestValues;
import org.springframework.web.service.invoker.ReactorHttpExchangeAdapter;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.cloud.client.circuitbreaker.httpservice.CircuitBreakerConfigurerUtils.DEFAULT_FALLBACK_KEY;
import static org.springframework.cloud.client.circuitbreaker.httpservice.CircuitBreakerRequestValueProcessor.ARGUMENTS_ATTRIBUTE_NAME;
import static org.springframework.cloud.client.circuitbreaker.httpservice.CircuitBreakerRequestValueProcessor.DECLARING_CLASS_ATTRIBUTE_NAME;
import static org.springframework.cloud.client.circuitbreaker.httpservice.CircuitBreakerRequestValueProcessor.METHOD_ATTRIBUTE_NAME;
import static org.springframework.cloud.client.circuitbreaker.httpservice.CircuitBreakerRequestValueProcessor.PARAMETER_TYPES_ATTRIBUTE_NAME;
import static org.springframework.cloud.client.circuitbreaker.httpservice.CircuitBreakerRequestValueProcessor.RETURN_TYPE_ATTRIBUTE_NAME;
/**
* Tests for {@link ReactiveCircuitBreakerAdapterDecorator}.
*
* @author Olga Maciaszek-Sharma
*/
class ReactiveCircuitBreakerAdapterDecoratorTests {
private static final String TEST_DESCRIPTION = "testDescription";
private static final int TEST_VALUE = 5;
private final ReactorHttpExchangeAdapter adapter = mock(ReactorHttpExchangeAdapter.class);
private final ReactiveCircuitBreaker reactiveCircuitBreaker = mock(ReactiveCircuitBreaker.class);
private final CircuitBreaker circuitBreaker = mock(CircuitBreaker.class);
private final HttpRequestValues httpRequestValues = mock(HttpRequestValues.class);
// Also verifies class fallback won't override default fallback for other classes
private final ReactiveCircuitBreakerAdapterDecorator decorator = new ReactiveCircuitBreakerAdapterDecorator(adapter,
reactiveCircuitBreaker, circuitBreaker, Map.of(DEFAULT_FALLBACK_KEY, Fallbacks.class,
UnusedTestService.class.getCanonicalName(), EmptyFallbacks.class));
@BeforeEach
void setUp() {
when(adapter.exchangeForBodyMono(any(), any())).thenReturn(Mono.just("test"));
}
@Test
void shouldWrapAdapterCallsWithCircuitBreakerInvocation() {
decorator.exchange(httpRequestValues);
verify(circuitBreaker).run(any(), any());
}
@Test
void shouldCreateFallbackHandler() {
Map<String, Object> attributes = new HashMap<>();
attributes.put(METHOD_ATTRIBUTE_NAME, "test");
attributes.put(PARAMETER_TYPES_ATTRIBUTE_NAME, new Class<?>[] { String.class, Integer.class });
attributes.put(ARGUMENTS_ATTRIBUTE_NAME, new Object[] { TEST_DESCRIPTION, TEST_VALUE });
attributes.put(RETURN_TYPE_ATTRIBUTE_NAME, String.class);
attributes.put(DECLARING_CLASS_ATTRIBUTE_NAME, TestService.class.getCanonicalName());
when(httpRequestValues.getAttributes()).thenReturn(attributes);
Function<Throwable, Object> fallbackHandler = decorator.createFallbackHandler(httpRequestValues);
Object fallback = fallbackHandler.apply(new RuntimeException("test"));
assertThat(fallback).isEqualTo(TEST_DESCRIPTION + ": " + TEST_VALUE);
}
@Test
void shouldCreateBodyMonoFallbackHandler() {
Map<String, Object> attributes = new HashMap<>();
attributes.put(METHOD_ATTRIBUTE_NAME, "testMono");
attributes.put(PARAMETER_TYPES_ATTRIBUTE_NAME, new Class<?>[] { String.class, Integer.class });
attributes.put(ARGUMENTS_ATTRIBUTE_NAME, new Object[] { TEST_DESCRIPTION, TEST_VALUE });
attributes.put(RETURN_TYPE_ATTRIBUTE_NAME, Mono.class);
attributes.put(DECLARING_CLASS_ATTRIBUTE_NAME, TestService.class.getCanonicalName());
when(httpRequestValues.getAttributes()).thenReturn(attributes);
Function<Throwable, Mono<Object>> fallbackHandler = decorator.createBodyMonoFallbackHandler(httpRequestValues);
Object fallback = fallbackHandler.apply(new RuntimeException("test")).block();
assertThat(fallback).isEqualTo(TEST_DESCRIPTION + ": " + TEST_VALUE);
}
@Test
void shouldCreateBodyMonoFallbackHandlerFromPerClassFallbackClassNames() {
Map<String, Class<?>> perClassFallbackClassNames = Map.of(DEFAULT_FALLBACK_KEY, EmptyFallbacks.class,
TestService.class.getCanonicalName(), Fallbacks.class);
ReactiveCircuitBreakerAdapterDecorator decorator = new ReactiveCircuitBreakerAdapterDecorator(adapter,
reactiveCircuitBreaker, circuitBreaker, perClassFallbackClassNames);
Map<String, Object> attributes = new HashMap<>();
attributes.put(METHOD_ATTRIBUTE_NAME, "testMono");
attributes.put(PARAMETER_TYPES_ATTRIBUTE_NAME, new Class<?>[] { String.class, Integer.class });
attributes.put(ARGUMENTS_ATTRIBUTE_NAME, new Object[] { TEST_DESCRIPTION, TEST_VALUE });
attributes.put(RETURN_TYPE_ATTRIBUTE_NAME, Mono.class);
attributes.put(DECLARING_CLASS_ATTRIBUTE_NAME, TestService.class.getCanonicalName());
when(httpRequestValues.getAttributes()).thenReturn(attributes);
Function<Throwable, Mono<Object>> fallbackHandler = decorator.createBodyMonoFallbackHandler(httpRequestValues);
Object fallback = fallbackHandler.apply(new RuntimeException("test")).block();
assertThat(fallback).isEqualTo(TEST_DESCRIPTION + ": " + TEST_VALUE);
}
@Test
void shouldCreateBodyMonoFallbackHandlerForNonReactiveReturnType() {
assertThatCode(() -> {
Map<String, Object> attributes = new HashMap<>();
attributes.put(METHOD_ATTRIBUTE_NAME, "test");
attributes.put(PARAMETER_TYPES_ATTRIBUTE_NAME, new Class<?>[] { String.class, Integer.class });
attributes.put(ARGUMENTS_ATTRIBUTE_NAME, new Object[] { TEST_DESCRIPTION, TEST_VALUE });
attributes.put(RETURN_TYPE_ATTRIBUTE_NAME, String.class);
attributes.put(DECLARING_CLASS_ATTRIBUTE_NAME, TestService.class.getCanonicalName());
when(httpRequestValues.getAttributes()).thenReturn(attributes);
Function<Throwable, Mono<Object>> fallbackHandler = decorator
.createBodyMonoFallbackHandler(httpRequestValues);
fallbackHandler.apply(new RuntimeException("test")).block();
}).doesNotThrowAnyException();
}
@Test
void shouldCreateBodyFluxFallbackHandlerForNonReactiveReturnType() {
assertThatCode(() -> {
Map<String, Object> attributes = new HashMap<>();
attributes.put(METHOD_ATTRIBUTE_NAME, "test");
attributes.put(PARAMETER_TYPES_ATTRIBUTE_NAME, new Class<?>[] { String.class, Integer.class });
attributes.put(ARGUMENTS_ATTRIBUTE_NAME, new Object[] { TEST_DESCRIPTION, TEST_VALUE });
attributes.put(RETURN_TYPE_ATTRIBUTE_NAME, String.class);
attributes.put(DECLARING_CLASS_ATTRIBUTE_NAME, TestService.class.getCanonicalName());
when(httpRequestValues.getAttributes()).thenReturn(attributes);
Function<Throwable, Flux<Object>> fallbackHandler = decorator
.createBodyFluxFallbackHandler(httpRequestValues);
fallbackHandler.apply(new RuntimeException("test")).blockFirst();
}).doesNotThrowAnyException();
}
@Test
void shouldCreateBodyMonoFallbackHandlerForVoidReturnType() {
Map<String, Object> attributes = new HashMap<>();
attributes.put(METHOD_ATTRIBUTE_NAME, "post");
attributes.put(PARAMETER_TYPES_ATTRIBUTE_NAME, new Class<?>[] { String.class });
attributes.put(ARGUMENTS_ATTRIBUTE_NAME, new Object[] { TEST_DESCRIPTION });
attributes.put(RETURN_TYPE_ATTRIBUTE_NAME, Void.class);
attributes.put(DECLARING_CLASS_ATTRIBUTE_NAME, TestService.class.getCanonicalName());
when(httpRequestValues.getAttributes()).thenReturn(attributes);
Function<Throwable, Mono<Object>> fallbackHandler = decorator.createBodyMonoFallbackHandler(httpRequestValues);
Object fallback = fallbackHandler.apply(new RuntimeException("test")).block();
assertThat(fallback).isNull();
}
@Test
void shouldCreateBodyFluxFallbackHandler() {
Map<String, Object> attributes = new HashMap<>();
attributes.put(METHOD_ATTRIBUTE_NAME, "testFlux");
attributes.put(PARAMETER_TYPES_ATTRIBUTE_NAME, new Class<?>[] { String.class, Integer.class });
attributes.put(ARGUMENTS_ATTRIBUTE_NAME, new Object[] { TEST_DESCRIPTION, TEST_VALUE });
attributes.put(RETURN_TYPE_ATTRIBUTE_NAME, Flux.class);
attributes.put(DECLARING_CLASS_ATTRIBUTE_NAME, TestService.class.getCanonicalName());
when(httpRequestValues.getAttributes()).thenReturn(attributes);
Function<Throwable, Flux<Object>> fallbackHandler = decorator.createBodyFluxFallbackHandler(httpRequestValues);
Object fallback = fallbackHandler.apply(new RuntimeException("test")).blockFirst();
assertThat(fallback).isEqualTo(TEST_DESCRIPTION + ": " + TEST_VALUE);
}
@Test
void shouldCreateBodyFluxFallbackHandlerFromNonReactiveReturnType() {
Map<String, Object> attributes = new HashMap<>();
attributes.put(METHOD_ATTRIBUTE_NAME, "test");
attributes.put(PARAMETER_TYPES_ATTRIBUTE_NAME, new Class<?>[] { String.class, Integer.class });
attributes.put(ARGUMENTS_ATTRIBUTE_NAME, new Object[] { TEST_DESCRIPTION, TEST_VALUE });
attributes.put(RETURN_TYPE_ATTRIBUTE_NAME, String.class);
attributes.put(DECLARING_CLASS_ATTRIBUTE_NAME, TestService.class.getCanonicalName());
when(httpRequestValues.getAttributes()).thenReturn(attributes);
Function<Throwable, Flux<Object>> fallbackHandler = decorator.createBodyFluxFallbackHandler(httpRequestValues);
Object fallback = fallbackHandler.apply(new RuntimeException("test")).blockFirst();
assertThat(fallback).isEqualTo(TEST_DESCRIPTION + ": " + TEST_VALUE);
}
@Test
void shouldCreateBodyFluxFallbackHandlerFromReactiveReturnType() {
Map<String, Object> attributes = new HashMap<>();
attributes.put(METHOD_ATTRIBUTE_NAME, "testFlux");
attributes.put(PARAMETER_TYPES_ATTRIBUTE_NAME, new Class<?>[] { String.class, Integer.class });
attributes.put(ARGUMENTS_ATTRIBUTE_NAME, new Object[] { TEST_DESCRIPTION, TEST_VALUE });
attributes.put(RETURN_TYPE_ATTRIBUTE_NAME, Flux.class);
attributes.put(DECLARING_CLASS_ATTRIBUTE_NAME, TestService.class.getCanonicalName());
when(httpRequestValues.getAttributes()).thenReturn(attributes);
Function<Throwable, Flux<Object>> fallbackHandler = decorator.createBodyFluxFallbackHandler(httpRequestValues);
Object fallback = fallbackHandler.apply(new RuntimeException("test")).blockFirst();
assertThat(fallback).isEqualTo(TEST_DESCRIPTION + ": " + TEST_VALUE);
}
@SuppressWarnings("DataFlowIssue")
@Test
void shouldCreateHttpHeadersMonoFallbackHandler() {
Map<String, Object> attributes = new HashMap<>();
attributes.put(METHOD_ATTRIBUTE_NAME, "testHttpHeadersMono");
attributes.put(PARAMETER_TYPES_ATTRIBUTE_NAME, new Class<?>[] { String.class, Integer.class });
attributes.put(ARGUMENTS_ATTRIBUTE_NAME, new Object[] { TEST_DESCRIPTION, TEST_VALUE });
attributes.put(RETURN_TYPE_ATTRIBUTE_NAME, Mono.class);
attributes.put(DECLARING_CLASS_ATTRIBUTE_NAME, TestService.class.getCanonicalName());
when(httpRequestValues.getAttributes()).thenReturn(attributes);
Function<Throwable, Mono<HttpHeaders>> fallbackHandler = decorator
.createHttpHeadersMonoFallbackHandler(httpRequestValues);
HttpHeaders fallback = fallbackHandler.apply(new RuntimeException("test")).block();
assertThat(fallback.get(TEST_DESCRIPTION).get(0)).isEqualTo(String.valueOf(TEST_VALUE));
}
@Test
void shouldCreateFallbackHandlerWithCause() {
Map<String, Object> attributes = new HashMap<>();
attributes.put(METHOD_ATTRIBUTE_NAME, "testThrowable");
attributes.put(PARAMETER_TYPES_ATTRIBUTE_NAME, new Class<?>[] { Throwable.class, String.class, Integer.class });
attributes.put(ARGUMENTS_ATTRIBUTE_NAME, new Object[] { new Throwable("test!"), TEST_DESCRIPTION, TEST_VALUE });
attributes.put(RETURN_TYPE_ATTRIBUTE_NAME, String.class);
attributes.put(DECLARING_CLASS_ATTRIBUTE_NAME, TestService.class.getCanonicalName());
when(httpRequestValues.getAttributes()).thenReturn(attributes);
Function<Throwable, Object> fallbackHandler = decorator.createFallbackHandler(httpRequestValues);
Object fallback = fallbackHandler.apply(new RuntimeException("test"));
assertThat(fallback).isEqualTo("java.lang.Throwable: test! " + TEST_DESCRIPTION + ": " + TEST_VALUE);
}
@Test
void shouldCreateReactiveFallbackHandlerWithCause() {
Map<String, Object> attributes = new HashMap<>();
attributes.put(METHOD_ATTRIBUTE_NAME, "testThrowableMono");
attributes.put(PARAMETER_TYPES_ATTRIBUTE_NAME, new Class<?>[] { Throwable.class, String.class, Integer.class });
attributes.put(ARGUMENTS_ATTRIBUTE_NAME, new Object[] { new Throwable("test!"), TEST_DESCRIPTION, TEST_VALUE });
attributes.put(RETURN_TYPE_ATTRIBUTE_NAME, Mono.class);
attributes.put(DECLARING_CLASS_ATTRIBUTE_NAME, TestService.class.getCanonicalName());
when(httpRequestValues.getAttributes()).thenReturn(attributes);
Function<Throwable, Mono<Object>> fallbackHandler = decorator.createBodyMonoFallbackHandler(httpRequestValues);
Object fallback = fallbackHandler.apply(new RuntimeException("test")).block();
assertThat(fallback).isEqualTo("java.lang.Throwable: test! " + TEST_DESCRIPTION + ": " + TEST_VALUE);
}
@Test
void shouldThrowExceptionWhenNoFallbackAvailable() {
Map<String, Object> attributes = new HashMap<>();
attributes.put(DECLARING_CLASS_ATTRIBUTE_NAME, TestService.class.getCanonicalName());
when(httpRequestValues.getAttributes()).thenReturn(attributes);
Function<Throwable, Object> fallbackHandler = decorator.createFallbackHandler(httpRequestValues);
assertThatExceptionOfType(NoFallbackAvailableException.class)
.isThrownBy(() -> fallbackHandler.apply(new RuntimeException("test")));
}
}