CircuitBreakerAdapterDecoratorTests.java

/*
 * Copyright 2012-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.Test;

import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
import org.springframework.cloud.client.circuitbreaker.NoFallbackAvailableException;
import org.springframework.web.service.invoker.HttpExchangeAdapter;
import org.springframework.web.service.invoker.HttpRequestValues;

import static org.assertj.core.api.Assertions.assertThat;
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 CircuitBreakerAdapterDecorator}.
 *
 * @author Olga Maciaszek-Sharma
 */
class CircuitBreakerAdapterDecoratorTests {

	private final HttpExchangeAdapter adapter = mock(HttpExchangeAdapter.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 CircuitBreakerAdapterDecorator decorator = new CircuitBreakerAdapterDecorator(adapter, circuitBreaker,
			Map.of(DEFAULT_FALLBACK_KEY, Fallbacks.class, UnusedTestService.class.getCanonicalName(),
					EmptyFallbacks.class));

	@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[] { "testDescription", 5 });
		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("testDescription: 5");
	}

	@Test
	void shouldCreateFallbackHandlerFromPerClassFallbackClassNames() {
		Map<String, Class<?>> perClassFallbackClassNames = Map.of(DEFAULT_FALLBACK_KEY, EmptyFallbacks.class,
				TestService.class.getCanonicalName(), Fallbacks.class);
		CircuitBreakerAdapterDecorator decorator = new CircuitBreakerAdapterDecorator(adapter, circuitBreaker,
				perClassFallbackClassNames);
		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[] { "testDescription", 5 });
		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("testDescription: 5");
	}

	@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!"), "testDescription", 5 });
		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! testDescription: 5");
	}

	@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")));
	}

}