/*
Copyright 2013 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
http://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 io.neba.core.mvc;
import io.neba.core.web.WebApplicationContextAdapter;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.ServletResolver;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatcher;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.servlet.HandlerAdapter;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping;
import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter;
import org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver;
import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.anyList;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.argThat;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.*;
import static org.springframework.web.servlet.DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME;
/**
* @author Olaf Otto
*/
@RunWith(MockitoJUnitRunner.class)
public class BundleSpecificDispatcherServletTest {
@Mock
private ConfigurableListableBeanFactory factory;
@Mock
private ApplicationContext applicationContext;
@Mock
private ContextRefreshedEvent event;
@Mock
private ServletConfig servletConfig;
@Mock
private ServletResolver servletResolver;
@Mock
private SlingMvcServletRequest request;
@Mock
private SlingHttpServletResponse response;
private List<?> registeredArgumentResolvers = new ArrayList<>();
private HandlerMapping handlerMapping;
private BundleSpecificDispatcherServlet testee;
@SuppressWarnings("unchecked")
@Before
public void setUp() throws Exception {
doThrow(new NoSuchBeanDefinitionException("THIS IS AN EXPECTED TEST EXCEPTION"))
.when(this.applicationContext).getBean(anyString(), isA(Class.class));
doReturn(this.applicationContext).when(this.event).getApplicationContext();
doReturn(this.factory).when(this.applicationContext).getAutowireCapableBeanFactory();
Answer<Object> createMock = invocation -> {
final Class<?> beanType = (Class<?>) invocation.getArguments()[0];
return mockExistingBean(beanType);
};
doAnswer(createMock).when(this.factory).createBean(isA(Class.class));
this.testee = new BundleSpecificDispatcherServlet(this.servletConfig, this.servletResolver, this.factory);
}
@Test(expected = IllegalArgumentException.class)
public void testHandlingOfNullFactoryInConstructor() throws Exception {
new BundleSpecificDispatcherServlet(mock(ServletConfig.class), this.servletResolver, null);
}
@Test(expected = IllegalArgumentException.class)
public void testHandlingOfNullServletConfigInConstructor() throws Exception {
new BundleSpecificDispatcherServlet(null, this.servletResolver, mock(ConfigurableListableBeanFactory.class));
}
@Test(expected = IllegalArgumentException.class)
public void testHandlingOfNullServletResolverInConstructor() throws Exception {
new BundleSpecificDispatcherServlet(mock(ServletConfig.class), null, mock(ConfigurableListableBeanFactory.class));
}
@Test
public void testApplicationContextIsProvidedAsWebApplicationContext() throws Exception {
signalContextRefreshed();
assertDispatcherServletIsInitializedWithWebApplicationContextAdapter();
}
@Test
public void testWebApplicationContextDispatchesToOriginalApplicationContext() throws Exception {
signalContextRefreshed();
getBeanFromWebApplicationContext("anyBean");
verifyBeanIsFetchedFromApplicationContext("anyBean");
}
private void verifyBeanIsFetchedFromApplicationContext(String beanName) {
verify(this.applicationContext).getBean(beanName);
}
private void getBeanFromWebApplicationContext(String beanName) {
this.testee.getWebApplicationContext().getBean(beanName);
}
@Test
public void testProvisioningOfMvcInfrastructure() throws Exception {
signalContextRefreshed();
verifyMultipartResolverIsRegistered();
verifyExceptionResolversAreRegistered();
verifyHandlerAdaptersAreRegistered();
verifyHandlerMappingsAreRegistered();
verifyViewResolverIsRegistered();
}
@Test
public void testInitializationIsIgnoredIfInfrastructureIsNotInitialized() throws Exception {
verifyApplicationContextIsNotUsed();
}
@Test
public void testInitializationIsPerformedWhenInfrastructureIsInitialized() throws Exception {
signalContextRefreshed();
verifyContextIsAskedFor(MULTIPART_RESOLVER_BEAN_NAME, MultipartResolver.class);
}
@Test
public void testHandlingOfExistingMultipartResolver() throws Exception {
withBeanAlreadyExistingInApplicationContext(MultipartResolver.class);
signalContextRefreshed();
verifyMultipartResolverIsNotRegistered();
verifyExceptionResolversAreRegistered();
verifyHandlerAdaptersAreRegistered();
verifyHandlerMappingsAreRegistered();
verifyViewResolverIsRegistered();
}
@Test
public void testHandlingOfExistingExceptionResolverButNoDefaultResolver() throws Exception {
withBeanAlreadyExistingInApplicationContext(HandlerExceptionResolver.class);
signalContextRefreshed();
verifyExceptionResolversAreNotRegistered();
verifyDispatcherServletAttemptsToObtainDefaultResolver();
verifyMultipartResolverIsRegistered();
verifyHandlerAdaptersAreRegistered();
verifyHandlerMappingsAreRegistered();
verifyViewResolverIsRegistered();
}
@Test
public void testHandlingOfExistingDefaultExceptionResolver() throws Exception {
withBeanAlreadyExistingInApplicationContext(DefaultHandlerExceptionResolver.class);
signalContextRefreshed();
verifyExceptionResolversAreNotRegistered();
verifyDispatcherServletAttemptsToObtainDefaultResolver();
verifyDispatcherServletConfiguresWarnLogCategory("mvc");
verifyMultipartResolverIsRegistered();
verifyHandlerAdaptersAreRegistered();
verifyHandlerMappingsAreRegistered();
verifyViewResolverIsRegistered();
}
@Test
public void testHandlingOfExistingRequestMappingHandlerAdapter() throws Exception {
withBeanAlreadyExistingInApplicationContext(HandlerAdapter.class);
withRequestMappingHandlerAlreadyExistingInContext();
signalContextRefreshed();
verifyHandlerAdaptersAreNotRegistered();
verifyNebaArgumentResolversAreRegistered();
verifyMultipartResolverIsRegistered();
verifyExceptionResolversAreRegistered();
verifyHandlerMappingsAreRegistered();
verifyViewResolverIsRegistered();
}
@Test
public void testHandlingOfExistingHandlerAdaptersWithoutRequestMappingHandlerAdapter() throws Exception {
withBeanAlreadyExistingInApplicationContext(HandlerAdapter.class);
signalContextRefreshed();
verifyHandlerAdaptersAreNotRegistered();
verifyNebaArgumentResolversAreNotRegistered();
verifyMultipartResolverIsRegistered();
verifyExceptionResolversAreRegistered();
verifyHandlerMappingsAreRegistered();
verifyViewResolverIsRegistered();
}
@Test
public void testHandlingOfExistingHandlerMappings() throws Exception {
withBeanAlreadyExistingInApplicationContext(HandlerMapping.class);
signalContextRefreshed();
verifyHandlerMappingsAreNotRegistered();
verifyMultipartResolverIsRegistered();
verifyExceptionResolversAreRegistered();
verifyHandlerAdaptersAreRegistered();
verifyViewResolverIsRegistered();
}
@Test
public void testHandlingOfExistingViewResolver() throws Exception {
withBeanAlreadyExistingInApplicationContext(ViewResolver.class);
signalContextRefreshed();
verifyViewResolverIsRegistered();
verifyMultipartResolverIsRegistered();
verifyExceptionResolversAreRegistered();
verifyHandlerAdaptersAreRegistered();
verifyHandlerMappingsAreRegistered();
}
@Test
public void testRegistrationOfCustomArgumentResolvers() throws Exception {
withRequestMappingHandlerCreatedOnDemand(mockRequestMappingHandler());
signalContextRefreshed();
verifyNebaArgumentResolversAreRegistered();
}
@Test(expected = IllegalStateException.class)
public void testHandlingOfUninitializedArgumentsResolvers() throws Exception {
RequestMappingHandlerAdapter adapter = mock(RequestMappingHandlerAdapter.class);
doReturn(null).when(adapter).getArgumentResolvers();
withRequestMappingHandlerCreatedOnDemand(adapter);
signalContextRefreshed();
}
@Test
public void testHandlingOfUnsupportedApplicationEvent() throws Exception {
sendEvent(mock(ContextClosedEvent.class));
verifyMvcContextIgnoresEvent();
}
@Test
public void testOptionsRequestsArePassedToHandlers() throws Exception {
withExistingHandlerMapping();
signalContextRefreshed();
withMethod("OPTIONS");
service();
verifyHandlerMappingIsUsedForRequest();
}
@Test
public void testTraceRequestsArePassedToHandlers() throws Exception {
withExistingHandlerMapping();
signalContextRefreshed();
withMethod("TRACE");
// Mock expected response type to prevent the default trace behavior
// from executing, which would require superfluous mocking
withResponseContentType("message/http");
service();
verifyHandlerMappingIsUsedForRequest();
}
@Test
public void testHasHandlerForIsAlwaysFalseWhenServletIsNotInitialized() throws Exception {
withExistingHandlerMapping();
assertServletHasNoHandlerForRequest();
verifyHandlerMappingIsNotUsedForRequest();
}
@Test
public void testHasHandlerForRequestChecksHandlerMappingsWhenServletIsInitialized() throws Exception {
withExistingHandlerMapping();
signalContextRefreshed();
assertServletHasHandlerForRequest();
verifyHandlerMappingIsUsedForRequest();
}
private void assertServletHasHandlerForRequest() {
assertThat(this.testee.hasHandlerFor(this.request)).isTrue();
}
private void assertServletHasNoHandlerForRequest() {
assertThat(this.testee.hasHandlerFor(this.request)).isFalse();
}
private void withResponseContentType(String type) {
doReturn(type).when(this.response).getContentType();
}
private void verifyHandlerMappingIsUsedForRequest() throws Exception {
verify(this.handlerMapping).getHandler(eq(this.request));
}
private void verifyHandlerMappingIsNotUsedForRequest() throws Exception {
verify(this.handlerMapping, never()).getHandler(eq(this.request));
}
private void withExistingHandlerMapping() {
this.handlerMapping = mockExistingBean(HandlerMapping.class);
}
private void service() throws ServletException, IOException {
this.testee.service(this.request, this.response);
}
private void withMethod(String method) {
doReturn(method).when(this.request).getMethod();
}
private void sendEvent(ApplicationEvent event) {
this.testee.onApplicationEvent(event);
}
private void withRequestMappingHandlerAlreadyExistingInContext() {
doReturn(mockRequestMappingHandler()).when(this.factory).getBean(eq(RequestMappingHandlerAdapter.class));
}
private void withRequestMappingHandlerCreatedOnDemand(final RequestMappingHandlerAdapter handler) {
Answer<RequestMappingHandlerAdapter> mockBeanCreation = invocation -> {
doReturn(handler).when(factory).getBean(eq(RequestMappingHandlerAdapter.class));
return handler;
};
doAnswer(mockBeanCreation).when(this.factory).createBean(eq(RequestMappingHandlerAdapter.class));
}
@SuppressWarnings("unchecked")
private RequestMappingHandlerAdapter mockRequestMappingHandler() {
RequestMappingHandlerAdapter requestMappingHandlerAdapter = mock(RequestMappingHandlerAdapter.class);
Answer<Object> verifyList = invocation -> {
registeredArgumentResolvers = (List<?>) invocation.getArguments()[0];
return null;
};
doAnswer(verifyList).when(requestMappingHandlerAdapter).setArgumentResolvers(anyList());
return requestMappingHandlerAdapter;
}
private void verifyNebaArgumentResolversAreRegistered() {
assertThat(this.registeredArgumentResolvers).describedAs("The list of registered NEBA argument resolvers").hasSize(3);
assertThat(this.registeredArgumentResolvers.get(0)).isInstanceOf(RequestPathInfoArgumentResolver.class);
assertThat(this.registeredArgumentResolvers.get(1)).isInstanceOf(ResourceResolverArgumentResolver.class);
assertThat(this.registeredArgumentResolvers.get(2)).isInstanceOf(ResourceParamArgumentResolver.class);
}
private void verifyNebaArgumentResolversAreNotRegistered() {
assertThat(this.registeredArgumentResolvers).describedAs("The list of registered NEBA argument resolvers").isEmpty();
}
private void verifyMvcContextIgnoresEvent() {
verifyNoMoreInteractions(this.factory);
verifyNoMoreInteractions(this.applicationContext);
}
private void withBeanAlreadyExistingInApplicationContext(Class<?> type) {
mockExistingBean(type);
}
private void verifyContextIsAskedFor(String beanName, Class<MultipartResolver> beanType) {
verify(this.applicationContext).getBean(eq(beanName), eq(beanType));
}
private void verifyViewResolverIsRegistered() {
verify(this.factory).registerSingleton(anyString(), isA(NebaViewResolver.class));
}
private void verifyHandlerMappingsAreRegistered() {
verifyContextDefinesBean(BeanNameUrlHandlerMapping.class);
verifyContextDefinesBean(RequestMappingHandlerMapping.class);
}
private void verifyHandlerMappingsAreNotRegistered() {
verifyBeanIsNeverCreatedInFactory(BeanNameUrlHandlerMapping.class);
verifyBeanIsNeverCreatedInFactory(RequestMappingHandlerMapping.class);
}
private void verifyHandlerAdaptersAreRegistered() {
verifyContextDefinesBean(HttpRequestHandlerAdapter.class);
verifyContextDefinesBean(RequestMappingHandlerAdapter.class);
}
private void verifyHandlerAdaptersAreNotRegistered() {
verifyBeanIsNeverCreatedInFactory(HttpRequestHandlerAdapter.class);
verifyBeanIsNeverCreatedInFactory(RequestMappingHandlerAdapter.class);
}
private void verifyExceptionResolversAreRegistered() {
verifyContextDefinesBean(ExceptionHandlerExceptionResolver.class);
verifyContextDefinesBean(ResponseStatusExceptionResolver.class);
verifyContextDefinesBean(DefaultHandlerExceptionResolver.class);
}
private void verifyExceptionResolversAreNotRegistered() {
Class<?> type = ExceptionHandlerExceptionResolver.class;
verifyBeanIsNeverCreatedInFactory(type);
verifyBeanIsNeverCreatedInFactory(ResponseStatusExceptionResolver.class);
verifyBeanIsNeverCreatedInFactory(DefaultHandlerExceptionResolver.class);
}
private void verifyMultipartResolverIsRegistered() {
verifyContextDefinesBean(SlingMultipartResolver.class, MULTIPART_RESOLVER_BEAN_NAME);
}
private void verifyMultipartResolverIsNotRegistered() {
verifyBeanIsNeverCreatedInFactory(SlingMultipartResolver.class);
}
private void verifyBeanIsNeverCreatedInFactory(Class<?> type) {
verify(this.factory, never()).createBean(eq(type));
}
private void verifyContextDefinesBean(Class<?> type, String beanName) {
verify(this.factory).createBean(type);
verify(this.factory).registerSingleton(eq(beanName), isA(type));
}
private void verifyContextDefinesBean(Class<?> type) {
verify(this.factory).createBean(type);
verify(this.factory).registerSingleton(anyString(), isA(type));
}
private void verifyDispatcherServletAttemptsToObtainDefaultResolver() {
verify(this.factory).getBean(eq(DefaultHandlerExceptionResolver.class));
}
private void verifyDispatcherServletConfiguresWarnLogCategory(String category) {
verify(this.factory.getBean(DefaultHandlerExceptionResolver.class)).setWarnLogCategory(category);
}
private void verifyApplicationContextIsNotUsed() {
verifyZeroInteractions(this.applicationContext);
}
private void signalContextRefreshed() {
this.testee.onApplicationEvent((ApplicationEvent) this.event);
}
private void assertDispatcherServletIsInitializedWithWebApplicationContextAdapter() {
assertThat(this.testee.getWebApplicationContext()).isInstanceOf(WebApplicationContextAdapter.class);
}
private <T> T mockExistingBean(final Class<T> beanType) {
T bean = mock(beanType, Mockito.RETURNS_MOCKS);
Map<String, Object> matchingBeans = new HashMap<>();
matchingBeans.put("name", bean);
ArgumentMatcher<Class<?>> isAssignableFromBeanType = new ArgumentMatcher<Class<?>>() {
@Override
public boolean matches(Object argument) {
return ((Class<?>) argument).isAssignableFrom(beanType);
}
};
doReturn(matchingBeans).when(applicationContext).getBeansOfType(argThat(isAssignableFromBeanType));
doReturn(matchingBeans).when(applicationContext).getBeansOfType(argThat(isAssignableFromBeanType), anyBoolean(), anyBoolean());
doReturn(bean).when(applicationContext).getBean(anyString(), argThat(isAssignableFromBeanType));
@SuppressWarnings("unchecked")
Map<String, Object> m = mock(Map.class);
doReturn(false).when(m).isEmpty();
doReturn(m).when(this.factory).getBeansOfType(argThat(isAssignableFromBeanType));
doReturn(bean).when(this.factory).getBean(beanType);
return bean;
}
}