package com.nike.wingtips.servlet; import com.nike.internal.util.StringUtils; import com.nike.wingtips.Span; import com.nike.wingtips.Span.SpanPurpose; import com.nike.wingtips.TraceAndSpanIdGenerator; import com.nike.wingtips.TraceHeaders; import com.nike.wingtips.Tracer; import com.nike.wingtips.servlet.tag.ServletRequestTagAdapter; import com.nike.wingtips.tags.HttpTagAndSpanNamingAdapter; import com.nike.wingtips.tags.HttpTagAndSpanNamingStrategy; import com.nike.wingtips.tags.KnownZipkinTags; import com.nike.wingtips.tags.NoOpHttpTagStrategy; import com.nike.wingtips.tags.OpenTracingHttpTagStrategy; import com.nike.wingtips.tags.ZipkinHttpTagStrategy; import com.nike.wingtips.testutils.ArgCapturingHttpTagAndSpanNamingStrategy; import com.nike.wingtips.testutils.ArgCapturingHttpTagAndSpanNamingStrategy.InitialSpanNameArgs; import com.nike.wingtips.testutils.ArgCapturingHttpTagAndSpanNamingStrategy.RequestTaggingArgs; import com.nike.wingtips.testutils.ArgCapturingHttpTagAndSpanNamingStrategy.ResponseTaggingArgs; import com.nike.wingtips.util.TracingState; import com.tngtech.java.junit.dataprovider.DataProvider; import com.tngtech.java.junit.dataprovider.DataProviderRunner; import com.tngtech.java.junit.dataprovider.UseDataProvider; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.slf4j.MDC; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import javax.servlet.AsyncContext; import javax.servlet.AsyncListener; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import static com.nike.wingtips.servlet.RequestTracingFilter.TAG_AND_SPAN_NAMING_ADAPTER_INIT_PARAM_NAME; import static com.nike.wingtips.servlet.RequestTracingFilter.TAG_AND_SPAN_NAMING_STRATEGY_INIT_PARAM_NAME; import static com.nike.wingtips.servlet.ServletRuntime.ASYNC_LISTENER_CLASSNAME; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; import static org.assertj.core.api.Fail.fail; import static org.mockito.BDDMockito.given; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; /** * Tests the functionality of {@link RequestTracingFilter} */ @RunWith(DataProviderRunner.class) public class RequestTracingFilterTest { private HttpServletRequest requestMock; private HttpServletResponse responseMock; private FilterChain filterChainMock; private SpanCapturingFilterChain spanCapturingFilterChain; @SuppressWarnings("FieldCanBeLocal") private AsyncContext listenerCapturingAsyncContext; private List<AsyncListener> capturedAsyncListeners; private FilterConfig filterConfigMock; private ServletRuntime servletRuntimeMock; private HttpTagAndSpanNamingStrategy<HttpServletRequest, HttpServletResponse> tagAndNamingStrategy; private HttpTagAndSpanNamingAdapter<HttpServletRequest, HttpServletResponse> tagAndNamingAdapterMock; private AtomicReference<String> initialSpanNameFromStrategy; private AtomicBoolean strategyInitialSpanNameMethodCalled; private AtomicBoolean strategyRequestTaggingMethodCalled; private AtomicBoolean strategyResponseTaggingAndFinalSpanNameMethodCalled; private AtomicReference<InitialSpanNameArgs> strategyInitialSpanNameArgs; private AtomicReference<RequestTaggingArgs> strategyRequestTaggingArgs; private AtomicReference<ResponseTaggingArgs> strategyResponseTaggingArgs; private static final String USER_ID_HEADER_KEY = "userId"; private static final String ALT_USER_ID_HEADER_KEY = "altUserId"; private static final List<String> USER_ID_HEADER_KEYS = Arrays.asList(USER_ID_HEADER_KEY, ALT_USER_ID_HEADER_KEY); private static final String USER_ID_HEADER_KEYS_INIT_PARAM_VALUE_STRING = USER_ID_HEADER_KEYS.toString().replace("[", "").replace("]", ""); private RequestTracingFilter getBasicFilter() { RequestTracingFilter filter = new RequestTracingFilter(); try { filter.init(filterConfigMock); filter.tagAndNamingStrategy = tagAndNamingStrategy; filter.tagAndNamingAdapter = tagAndNamingAdapterMock; } catch (ServletException e) { throw new RuntimeException(e); } return filter; } private void setupAsyncContextWorkflow() { listenerCapturingAsyncContext = mock(AsyncContext.class); capturedAsyncListeners = new ArrayList<>(); doReturn(listenerCapturingAsyncContext).when(requestMock).getAsyncContext(); doReturn(true).when(requestMock).isAsyncStarted(); doAnswer(invocation -> { capturedAsyncListeners.add((AsyncListener) invocation.getArguments()[0]); return null; }).when(listenerCapturingAsyncContext).addListener( any(AsyncListener.class), any(ServletRequest.class), any(ServletResponse.class) ); } @Before public void setupMethod() { requestMock = mock(HttpServletRequest.class); responseMock = mock(HttpServletResponse.class); filterChainMock = mock(FilterChain.class); spanCapturingFilterChain = new SpanCapturingFilterChain(); initialSpanNameFromStrategy = new AtomicReference<>("span-name-from-strategy-" + UUID.randomUUID().toString()); strategyInitialSpanNameMethodCalled = new AtomicBoolean(false); strategyRequestTaggingMethodCalled = new AtomicBoolean(false); strategyResponseTaggingAndFinalSpanNameMethodCalled = new AtomicBoolean(false); strategyInitialSpanNameArgs = new AtomicReference<>(null); strategyRequestTaggingArgs = new AtomicReference<>(null); strategyResponseTaggingArgs = new AtomicReference<>(null); tagAndNamingStrategy = new ArgCapturingHttpTagAndSpanNamingStrategy( initialSpanNameFromStrategy, strategyInitialSpanNameMethodCalled, strategyRequestTaggingMethodCalled, strategyResponseTaggingAndFinalSpanNameMethodCalled, strategyInitialSpanNameArgs, strategyRequestTaggingArgs, strategyResponseTaggingArgs ); tagAndNamingAdapterMock = mock(HttpTagAndSpanNamingAdapter.class); filterConfigMock = mock(FilterConfig.class); doReturn(USER_ID_HEADER_KEYS_INIT_PARAM_VALUE_STRING) .when(filterConfigMock) .getInitParameter(RequestTracingFilter.USER_ID_HEADER_KEYS_LIST_INIT_PARAM_NAME); servletRuntimeMock = mock(ServletRuntime.class); resetTracing(); } @After public void afterMethod() { resetTracing(); } private void resetTracing() { MDC.clear(); Tracer.getInstance().unregisterFromThread(); } // VERIFY filter init, // initializeUserIdHeaderKeys, getUserIdHeaderKeys, // initializeTagAndNamingStrategy, getTagStrategyFromName, // initializeTagAndNamingAdapter, getTagAdapterFromName, // all the get*Strategy() methods, // getDefaultTagAdapter // and destroy ======================= @Test public void init_method_delegates_to_helpers_to_initialize_fields() throws ServletException { // given RequestTracingFilter filterSpy = spy(new RequestTracingFilter()); List<String> expectedUserIdHeaderKeys = Arrays.asList(UUID.randomUUID().toString(), UUID.randomUUID().toString()); doReturn(expectedUserIdHeaderKeys).when(filterSpy).initializeUserIdHeaderKeys(any(FilterConfig.class)); doReturn(tagAndNamingStrategy).when(filterSpy).initializeTagAndNamingStrategy(any(FilterConfig.class)); doReturn(tagAndNamingAdapterMock).when(filterSpy).initializeTagAndNamingAdapter(any(FilterConfig.class)); // when filterSpy.init(filterConfigMock); // then assertThat(filterSpy.userIdHeaderKeysFromInitParam).isSameAs(expectedUserIdHeaderKeys); assertThat(filterSpy.tagAndNamingStrategy).isSameAs(tagAndNamingStrategy); assertThat(filterSpy.tagAndNamingAdapter).isSameAs(tagAndNamingAdapterMock); verify(filterSpy).init(filterConfigMock); verify(filterSpy).initializeUserIdHeaderKeys(filterConfigMock); verify(filterSpy).initializeTagAndNamingStrategy(filterConfigMock); verify(filterSpy).initializeTagAndNamingAdapter(filterConfigMock); verifyNoMoreInteractions(filterSpy); } @DataProvider public static Object[][] userIdHeaderKeysInitParamDataProvider() { return new Object[][]{ {null, null}, {"", Collections.emptyList()}, {" \t \n ", Collections.emptyList()}, {"asdf", Collections.singletonList("asdf")}, {" , \n\t, asdf , \t\n ", Collections.singletonList("asdf")}, {"ASDF,QWER", Arrays.asList("ASDF", "QWER")}, {"ASDF, QWER, ZXCV", Arrays.asList("ASDF", "QWER", "ZXCV")} }; } @Test @UseDataProvider("userIdHeaderKeysInitParamDataProvider") public void initializeUserIdHeaderKeys_gets_user_id_header_key_list_from_init_params( String userIdHeaderKeysInitParamValue, List<String> expectedUserIdHeaderKeysList ) { // given RequestTracingFilter filter = new RequestTracingFilter(); doReturn(userIdHeaderKeysInitParamValue) .when(filterConfigMock) .getInitParameter(RequestTracingFilter.USER_ID_HEADER_KEYS_LIST_INIT_PARAM_NAME); // when List<String> actualUserIdHeaderKeysList = filter.initializeUserIdHeaderKeys(filterConfigMock); // then assertThat(actualUserIdHeaderKeysList).isEqualTo(expectedUserIdHeaderKeysList); if (actualUserIdHeaderKeysList != null) { Exception caughtEx = null; try { actualUserIdHeaderKeysList.add("foo"); } catch (Exception ex) { caughtEx = ex; } assertThat(caughtEx).isNotNull(); assertThat(caughtEx).isInstanceOf(UnsupportedOperationException.class); } } @Test public void getUserIdHeaderKeys_returns_userIdHeaderKeysFromInitParam_field() { // given RequestTracingFilter filter = new RequestTracingFilter(); List<String> expectedUserIdHeaderKeys = Arrays.asList(UUID.randomUUID().toString(), UUID.randomUUID().toString()); filter.userIdHeaderKeysFromInitParam = expectedUserIdHeaderKeys; // when List<String> result = filter.getUserIdHeaderKeys(); // then assertThat(result).isSameAs(expectedUserIdHeaderKeys); } @DataProvider(value = { "true", "false" }) @Test public void initializeTagAndNamingStrategy_delegates_to_getTagStrategyFromName_and_returns_default_if_exception_is_thrown( boolean throwException ) throws IllegalAccessException, InstantiationException, ClassNotFoundException { // given RequestTracingFilter filterSpy = spy(new RequestTracingFilter()); String tagStrategyFromFilterConfig = UUID.randomUUID().toString(); doReturn(tagStrategyFromFilterConfig) .when(filterConfigMock).getInitParameter(TAG_AND_SPAN_NAMING_STRATEGY_INIT_PARAM_NAME); HttpTagAndSpanNamingStrategy<HttpServletRequest, HttpServletResponse> strategyFromDesiredMethodMock = mock(HttpTagAndSpanNamingStrategy.class); HttpTagAndSpanNamingStrategy<HttpServletRequest, HttpServletResponse> fallbackDefaultStrategyMock = mock(HttpTagAndSpanNamingStrategy.class); if (throwException) { doThrow(new RuntimeException("intentional exception")).when(filterSpy).getTagStrategyFromName(anyString()); } else { doReturn(strategyFromDesiredMethodMock).when(filterSpy).getTagStrategyFromName(anyString()); } doReturn(fallbackDefaultStrategyMock).when(filterSpy).getDefaultTagStrategy(); // when HttpTagAndSpanNamingStrategy<HttpServletRequest, HttpServletResponse> result = filterSpy.initializeTagAndNamingStrategy(filterConfigMock); // then verify(filterSpy).getTagStrategyFromName(tagStrategyFromFilterConfig); if (throwException) { assertThat(result).isSameAs(fallbackDefaultStrategyMock); verify(filterSpy).getDefaultTagStrategy(); } else { assertThat(result).isSameAs(strategyFromDesiredMethodMock); verify(filterSpy, never()).getDefaultTagStrategy(); } } @DataProvider(value = { "ZIPKIN", "Zipkin", "opentracing", "OpenTracing", "NONE", "NoNe", "NOOP", "null", "", " ", " \t\r\n " }) @Test public void getTagStrategyFromName_returns_expected_strategies_for_known_short_names( String knownStrategyShortName ) throws IllegalAccessException, InstantiationException, ClassNotFoundException { // given RequestTracingFilter filterSpy = spy(new RequestTracingFilter()); HttpTagAndSpanNamingStrategy<HttpServletRequest, HttpServletResponse> zipkinStrategyMock = mock(HttpTagAndSpanNamingStrategy.class); HttpTagAndSpanNamingStrategy<HttpServletRequest, HttpServletResponse> openTracingStrategyMock = mock(HttpTagAndSpanNamingStrategy.class); HttpTagAndSpanNamingStrategy<HttpServletRequest, HttpServletResponse> noOpStrategyMock = mock(HttpTagAndSpanNamingStrategy.class); doReturn(zipkinStrategyMock).when(filterSpy).getZipkinHttpTagStrategy(); doReturn(openTracingStrategyMock).when(filterSpy).getOpenTracingHttpTagStrategy(); doReturn(noOpStrategyMock).when(filterSpy).getNoOpTagStrategy(); // when HttpTagAndSpanNamingStrategy<HttpServletRequest, HttpServletResponse> result = filterSpy.getTagStrategyFromName(knownStrategyShortName); // then // Default is Zipkin if (StringUtils.isBlank(knownStrategyShortName) || "zipkin".equalsIgnoreCase(knownStrategyShortName)) { assertThat(result).isSameAs(zipkinStrategyMock); } else if ("opentracing".equalsIgnoreCase(knownStrategyShortName)) { assertThat(result).isSameAs(openTracingStrategyMock); } else if ("none".equalsIgnoreCase(knownStrategyShortName) || "noop".equalsIgnoreCase(knownStrategyShortName)) { assertThat(result).isSameAs(noOpStrategyMock); } } @DataProvider(value = { "true", "false" }) @Test public void getTagStrategyFromName_returns_expected_strategy_for_fully_qualified_classname( boolean useClassThatExists ) { // given RequestTracingFilter filter = new RequestTracingFilter(); String classname = (useClassThatExists) ? TagStrategyExtension.class.getName() : "foo.doesnotexist.BlahStrategy" + UUID.randomUUID().toString(); AtomicReference<HttpTagAndSpanNamingStrategy<HttpServletRequest, HttpServletResponse>> resultHolder = new AtomicReference<>(); // when Throwable ex = catchThrowable(() -> resultHolder.set(filter.getTagStrategyFromName(classname))); // then if (useClassThatExists) { assertThat(ex).isNull(); assertThat(resultHolder.get()) .isNotNull() .isInstanceOf(TagStrategyExtension.class); } else { assertThat(ex).isInstanceOf(ClassNotFoundException.class); assertThat(resultHolder.get()).isNull(); } } @DataProvider(value = { "true", "false" }) @Test public void initializeTagAndNamingAdapter_delegates_to_getTagAdapterFromName_and_returns_default_if_exception_is_thrown( boolean throwException ) throws IllegalAccessException, InstantiationException, ClassNotFoundException { // given RequestTracingFilter filterSpy = spy(new RequestTracingFilter()); String tagAdapterFromFilterConfig = UUID.randomUUID().toString(); doReturn(tagAdapterFromFilterConfig) .when(filterConfigMock).getInitParameter(TAG_AND_SPAN_NAMING_ADAPTER_INIT_PARAM_NAME); HttpTagAndSpanNamingAdapter<HttpServletRequest, HttpServletResponse> adapterFromDesiredMethodMock = mock(HttpTagAndSpanNamingAdapter.class); HttpTagAndSpanNamingAdapter<HttpServletRequest, HttpServletResponse> fallbackDefaultAdapterMock = mock(HttpTagAndSpanNamingAdapter.class); if (throwException) { doThrow(new RuntimeException("intentional exception")).when(filterSpy).getTagAdapterFromName(anyString()); } else { doReturn(adapterFromDesiredMethodMock).when(filterSpy).getTagAdapterFromName(anyString()); } doReturn(fallbackDefaultAdapterMock).when(filterSpy).getDefaultTagAdapter(); // when HttpTagAndSpanNamingAdapter<HttpServletRequest, HttpServletResponse> result = filterSpy.initializeTagAndNamingAdapter(filterConfigMock); // then verify(filterSpy).getTagAdapterFromName(tagAdapterFromFilterConfig); if (throwException) { assertThat(result).isSameAs(fallbackDefaultAdapterMock); verify(filterSpy).getDefaultTagAdapter(); } else { assertThat(result).isSameAs(adapterFromDesiredMethodMock); verify(filterSpy, never()).getDefaultTagAdapter(); } } @DataProvider(value = { "null", "", " ", " \t\r\n " }) @Test public void getTagAdapterFromName_returns_default_adapter_if_passed_null_or_blank_string( String adapterName ) throws IllegalAccessException, InstantiationException, ClassNotFoundException { // given RequestTracingFilter filterSpy = spy(new RequestTracingFilter()); HttpTagAndSpanNamingAdapter<HttpServletRequest, HttpServletResponse> defaultAdapterMock = mock(HttpTagAndSpanNamingAdapter.class); doReturn(defaultAdapterMock).when(filterSpy).getDefaultTagAdapter(); // when HttpTagAndSpanNamingAdapter<HttpServletRequest, HttpServletResponse> result = filterSpy.getTagAdapterFromName(adapterName); // then assertThat(result).isSameAs(defaultAdapterMock); verify(filterSpy).getDefaultTagAdapter(); } @DataProvider(value = { "true", "false" }) @Test public void getTagAdapterFromName_returns_expected_strategy_for_fully_qualified_classname( boolean useClassThatExists ) { // given RequestTracingFilter filter = new RequestTracingFilter(); String classname = (useClassThatExists) ? TagAdapterExtension.class.getName() : "foo.doesnotexist.BlahAdapter" + UUID.randomUUID().toString(); AtomicReference<HttpTagAndSpanNamingAdapter<HttpServletRequest, HttpServletResponse>> resultHolder = new AtomicReference<>(); // when Throwable ex = catchThrowable(() -> resultHolder.set(filter.getTagAdapterFromName(classname))); // then if (useClassThatExists) { assertThat(ex).isNull(); assertThat(resultHolder.get()) .isNotNull() .isInstanceOf(TagAdapterExtension.class); } else { assertThat(ex).isInstanceOf(ClassNotFoundException.class); assertThat(resultHolder.get()).isNull(); } } @Test public void getZipkinHttpTagStrategy_works_as_expected() { // given RequestTracingFilter filter = new RequestTracingFilter(); // expect assertThat(filter.getZipkinHttpTagStrategy()) .isNotNull() .isSameAs(ZipkinHttpTagStrategy.getDefaultInstance()); } @Test public void getOpenTracingHttpTagStrategy_works_as_expected() { // given RequestTracingFilter filter = new RequestTracingFilter(); // expect assertThat(filter.getOpenTracingHttpTagStrategy()) .isNotNull() .isSameAs(OpenTracingHttpTagStrategy.getDefaultInstance()); } @Test public void getNoOpTagStrategy_works_as_expected() { // given RequestTracingFilter filter = new RequestTracingFilter(); // expect assertThat(filter.getNoOpTagStrategy()) .isNotNull() .isSameAs(NoOpHttpTagStrategy.getDefaultInstance()); } @Test public void getDefaultTagStrategy_delegates_to_getZipkinHttpTagStrategy() { // given RequestTracingFilter filterSpy = spy(new RequestTracingFilter()); HttpTagAndSpanNamingStrategy<HttpServletRequest, HttpServletResponse> zipkinStrategyMock = mock(HttpTagAndSpanNamingStrategy.class); doReturn(zipkinStrategyMock).when(filterSpy).getZipkinHttpTagStrategy(); // when HttpTagAndSpanNamingStrategy<HttpServletRequest, HttpServletResponse> result = filterSpy.getDefaultTagStrategy(); // then assertThat(result).isSameAs(zipkinStrategyMock); verify(filterSpy).getDefaultTagStrategy(); } @Test public void getDefaultTagAdapter_works_as_expected() { // given RequestTracingFilter filter = new RequestTracingFilter(); // expect assertThat(filter.getDefaultTagAdapter()) .isNotNull() .isSameAs(ServletRequestTagAdapter.getDefaultInstance()); } @Test public void destroy_does_nothing() { // given RequestTracingFilter filterSpy = spy(new RequestTracingFilter()); // when Throwable ex = catchThrowable(filterSpy::destroy); // then assertThat(ex).isNull(); verify(filterSpy).destroy(); verifyNoMoreInteractions(filterSpy); } // VERIFY doFilter =================================== @Test(expected = ServletException.class) public void doFilter_should_explode_if_request_is_not_HttpServletRequest() throws IOException, ServletException { // expect getBasicFilter().doFilter(mock(ServletRequest.class), mock(HttpServletResponse.class), mock(FilterChain.class)); fail("Expected ServletException but no exception was thrown"); } @Test(expected = ServletException.class) public void doFilter_should_explode_if_response_is_not_HttpServletResponse() throws IOException, ServletException { // expect getBasicFilter().doFilter(mock(HttpServletRequest.class), mock(ServletResponse.class), mock(FilterChain.class)); fail("Expected ServletException but no exception was thrown"); } @Test public void doFilter_should_not_explode_if_request_and_response_are_HttpServletRequests_and_HttpServletResponses( ) throws IOException, ServletException { // expect getBasicFilter().doFilter( mock(HttpServletRequest.class), mock(HttpServletResponse.class), mock(FilterChain.class) ); // No explosion no problem } @Test public void doFilter_should_call_doFilterInternal_and_set_ALREADY_FILTERED_ATTRIBUTE_KEY_if_not_already_filtered_and_skipDispatch_returns_false() throws IOException, ServletException { // given: filter that returns false for skipDispatch and request that returns null for already-filtered attribute RequestTracingFilter spyFilter = spy(getBasicFilter()); given(requestMock.getAttribute( RequestTracingFilter.FILTER_HAS_ALREADY_EXECUTED_ATTRIBUTE)).willReturn(null); // when: doFilter() is called spyFilter.doFilter(requestMock, responseMock, filterChainMock); // then: doFilterInternal should be called and ALREADY_FILTERED_ATTRIBUTE_KEY should be set on the request verify(spyFilter).doFilterInternal(requestMock, responseMock, filterChainMock); verify(requestMock).setAttribute(RequestTracingFilter.FILTER_HAS_ALREADY_EXECUTED_ATTRIBUTE, Boolean.TRUE); } @Test public void doFilter_should_not_unset_ALREADY_FILTERED_ATTRIBUTE_KEY_after_running_doFilterInternal( ) throws IOException, ServletException { // given: filter that will run doFilterInternal and a FilterChain we can use to verify state when called final RequestTracingFilter spyFilter = spy(getBasicFilter()); given(requestMock.getAttribute( RequestTracingFilter.FILTER_HAS_ALREADY_EXECUTED_ATTRIBUTE)).willReturn(null); final List<Boolean> ifObjectAddedThenSmartFilterChainCalled = new ArrayList<>(); FilterChain smartFilterChain = new FilterChain() { @Override public void doFilter( ServletRequest request, ServletResponse response ) throws IOException, ServletException { // Verify that when the filter chain is called we're in doFilterInternal, and that the request has ALREADY_FILTERED_ATTRIBUTE_KEY set verify(spyFilter).doFilterInternal(requestMock, responseMock, this); verify(requestMock).setAttribute( RequestTracingFilter.FILTER_HAS_ALREADY_EXECUTED_ATTRIBUTE, Boolean.TRUE ); verify(requestMock, times(0)).removeAttribute( RequestTracingFilter.FILTER_HAS_ALREADY_EXECUTED_ATTRIBUTE ); ifObjectAddedThenSmartFilterChainCalled.add(true); } }; // when: doFilter() is called spyFilter.doFilter(requestMock, responseMock, smartFilterChain); // then: smartFilterChain's doFilter should have been called and ALREADY_FILTERED_ATTRIBUTE_KEY should still be set on the request assertThat(ifObjectAddedThenSmartFilterChainCalled).hasSize(1); verify(requestMock, never()).removeAttribute(RequestTracingFilter.FILTER_HAS_ALREADY_EXECUTED_ATTRIBUTE); } @Test public void doFilter_should_not_unset_ALREADY_FILTERED_ATTRIBUTE_KEY_even_if_filter_chain_explodes( ) throws IOException, ServletException { // given: filter that will run doFilterInternal and a FilterChain we can use to verify state when called and then explodes final RequestTracingFilter spyFilter = spy(getBasicFilter()); given(requestMock.getAttribute( RequestTracingFilter.FILTER_HAS_ALREADY_EXECUTED_ATTRIBUTE)).willReturn(null); final List<Boolean> ifObjectAddedThenSmartFilterChainCalled = new ArrayList<>(); FilterChain smartFilterChain = new FilterChain() { @Override public void doFilter( ServletRequest request, ServletResponse response ) throws IOException, ServletException { // Verify that when the filter chain is called we're in doFilterInternal, and that the request has ALREADY_FILTERED_ATTRIBUTE_KEY set verify(spyFilter).doFilterInternal(requestMock, responseMock, this); verify(requestMock).setAttribute( RequestTracingFilter.FILTER_HAS_ALREADY_EXECUTED_ATTRIBUTE, Boolean.TRUE ); verify(requestMock, times(0)).removeAttribute( RequestTracingFilter.FILTER_HAS_ALREADY_EXECUTED_ATTRIBUTE ); ifObjectAddedThenSmartFilterChainCalled.add(true); throw new IllegalStateException("boom"); } }; // when: doFilter() is called boolean filterChainExploded = false; try { spyFilter.doFilter(requestMock, responseMock, smartFilterChain); } catch (IllegalStateException ex) { if ("boom".equals(ex.getMessage())) { filterChainExploded = true; } } // then: smartFilterChain's doFilter should have been called, it should have exploded, and ALREADY_FILTERED_ATTRIBUTE_KEY should still be set on the request assertThat(ifObjectAddedThenSmartFilterChainCalled).hasSize(1); assertThat(filterChainExploded).isTrue(); verify(requestMock, never()).removeAttribute(RequestTracingFilter.FILTER_HAS_ALREADY_EXECUTED_ATTRIBUTE); } @Test public void doFilter_should_not_call_doFilterInternal_if_already_filtered() throws IOException, ServletException { // given: filter that returns false for skipDispatch but request that returns non-null for already-filtered attribute RequestTracingFilter spyFilter = spy(getBasicFilter()); given(requestMock.getAttribute( RequestTracingFilter.FILTER_HAS_ALREADY_EXECUTED_ATTRIBUTE)).willReturn(Boolean.TRUE); // when: doFilter() is called spyFilter.doFilter(requestMock, responseMock, filterChainMock); // then: doFilterInternal should not be called verify(spyFilter, times(0)).doFilterInternal(requestMock, responseMock, filterChainMock); } @Test public void doFilter_should_not_call_doFilterInternal_if_not_already_filtered_but_skipDispatch_returns_true( ) throws IOException, ServletException { // given: request that returns null for already-filtered attribute but filter that returns true for skipDispatch RequestTracingFilter spyFilter = spy(getBasicFilter()); doReturn(true).when(spyFilter).skipDispatch(any(HttpServletRequest.class)); given(requestMock.getAttribute(RequestTracingFilter.FILTER_HAS_ALREADY_EXECUTED_ATTRIBUTE)).willReturn(null); // when: doFilter() is called spyFilter.doFilter(requestMock, responseMock, filterChainMock); // then: doFilterInternal should not be called verify(spyFilter, times(0)).doFilterInternal(requestMock, responseMock, filterChainMock); verify(spyFilter).skipDispatch(requestMock); } // VERIFY doFilterInternal =================================== @Test public void doFilterInternal_should_create_new_sampleable_span_if_no_parent_in_request_and_it_should_be_completed_and_tags_should_be_handled( ) throws ServletException, IOException { // given: filter RequestTracingFilter filter = getBasicFilter(); // when: doFilterInternal is called with a request that does not have a parent span filter.doFilterInternal(requestMock, responseMock, spanCapturingFilterChain); // then: a new valid sampleable span should be created and completed, // and tagging should have been done as expected Span span = spanCapturingFilterChain.capturedSpan; assertThat(span).isNotNull(); assertThat(span.getTraceId()).isNotNull(); assertThat(span.getSpanId()).isNotNull(); assertThat(span.getSpanName()).isNotNull(); assertThat(span.getParentSpanId()).isNull(); assertThat(span.isSampleable()).isTrue(); assertThat(span.isCompleted()).isTrue(); assertThat(strategyRequestTaggingMethodCalled.get()).isTrue(); strategyRequestTaggingArgs.get().verifyArgs(span, requestMock, filter.tagAndNamingAdapter); assertThat(strategyResponseTaggingAndFinalSpanNameMethodCalled.get()).isTrue(); strategyResponseTaggingArgs.get().verifyArgs( span, requestMock, responseMock, null, filter.tagAndNamingAdapter ); } @Test public void doFilterInternal_should_not_complete_span_or_response_tags_until_after_filter_chain_runs( ) throws ServletException, IOException { // given: filter and filter chain that can tell us whether or not the span is complete at the time it is called RequestTracingFilter filter = getBasicFilter(); AtomicBoolean spanCompletedHolder = new AtomicBoolean(false); AtomicReference<Span> spanHolder = new AtomicReference<>(); AtomicReference<Boolean> requestTagsExecutedAtTimeOfFilterChain = new AtomicReference<>(); AtomicReference<Boolean> responseTagsExecutedAtTimeOfFilterChain = new AtomicReference<>(); FilterChain smartFilterChain = (request, response) -> { Span span = Tracer.getInstance().getCurrentSpan(); spanHolder.set(span); if (span != null) { spanCompletedHolder.set(span.isCompleted()); } requestTagsExecutedAtTimeOfFilterChain.set(strategyRequestTaggingMethodCalled.get()); responseTagsExecutedAtTimeOfFilterChain.set(strategyResponseTaggingAndFinalSpanNameMethodCalled.get()); }; // when: doFilterInternal is called filter.doFilterInternal(requestMock, responseMock, smartFilterChain); // then: we should be able to validate that the smartFilterChain was called, and when it was called the span // had not yet been completed, and after doFilterInternal finished it was completed. Similarly, when // the chain is being run, request tags should be done but response tags should not. assertThat(spanHolder.get()).isNotNull(); assertThat(spanCompletedHolder.get()).isFalse(); assertThat(spanHolder.get().isCompleted()).isTrue(); assertThat(requestTagsExecutedAtTimeOfFilterChain.get()).isTrue(); assertThat(responseTagsExecutedAtTimeOfFilterChain.get()).isFalse(); } @DataProvider(value = { "true", "false" }) @Test public void doFilterInternal_should_complete_span_and_response_tags_even_if_filter_chain_explodes( boolean isAsyncRequest ) throws ServletException, IOException { // given: filter and filter chain that will explode when called RequestTracingFilter filterSpy = spy(getBasicFilter()); AtomicReference<Span> spanContextHolder = new AtomicReference<>(); FilterChain explodingFilterChain = (request, response) -> { // Verify that the span is not yet completed, keep track of it for later, then explode Span span = Tracer.getInstance().getCurrentSpan(); assertThat(span).isNotNull(); assertThat(span.isCompleted()).isFalse(); spanContextHolder.set(span); throw new IllegalStateException("boom"); }; if (isAsyncRequest) { setupAsyncContextWorkflow(); } // when: doFilterInternal is called boolean filterChainExploded = false; Throwable errorThrown = null; try { filterSpy.doFilterInternal(requestMock, responseMock, explodingFilterChain); } catch (IllegalStateException ex) { errorThrown = ex; if ("boom".equals(ex.getMessage())) { filterChainExploded = true; } } // then: we should be able to validate that the filter chain exploded and the span is still completed, // or setup for completion in the case of an async request if (isAsyncRequest) { assertThat(filterChainExploded).isTrue(); verify(filterSpy).isAsyncRequest(requestMock); verify(filterSpy).setupTracingCompletionWhenAsyncRequestCompletes( eq(requestMock), eq(responseMock), any(TracingState.class), any(HttpTagAndSpanNamingStrategy.class), any(HttpTagAndSpanNamingAdapter.class) ); assertThat(spanContextHolder.get()).isNotNull(); // The span should not be *completed* for an async request, but the // setupTracingCompletionWhenAsyncRequestCompletes verification above represents the equivalent for // async requests. The response tagging happens in there as well. assertThat(spanContextHolder.get().isCompleted()).isFalse(); } else { assertThat(filterChainExploded).isTrue(); assertThat(spanContextHolder.get()).isNotNull(); assertThat(spanContextHolder.get().isCompleted()).isTrue(); assertThat(strategyResponseTaggingAndFinalSpanNameMethodCalled.get()).isTrue(); // Response tags should be executed with the error that was thrown. strategyResponseTaggingArgs.get().verifyArgs( spanContextHolder.get(), requestMock, responseMock, errorThrown, filterSpy.tagAndNamingAdapter ); } // No matter what, the request tagging should have been done. assertThat(strategyRequestTaggingMethodCalled.get()).isTrue(); strategyRequestTaggingArgs.get().verifyArgs( spanContextHolder.get(), requestMock, filterSpy.tagAndNamingAdapter ); } @Test public void doFilterInternal_should_set_request_attributes_to_new_span_info_with_user_id( ) throws ServletException, IOException { // given: filter RequestTracingFilter spyFilter = spy(getBasicFilter()); given(requestMock.getHeader(USER_ID_HEADER_KEY)).willReturn("testUserId"); // when: doFilterInternal is called spyFilter.doFilterInternal(requestMock, responseMock, spanCapturingFilterChain); // then: request attributes should be set with the new span's info assertThat(spanCapturingFilterChain.capturedSpan).isNotNull(); Span newSpan = spanCapturingFilterChain.capturedSpan; assertThat(newSpan.getUserId()).isEqualTo("testUserId"); } @Test public void doFilterInternal_should_set_request_attributes_to_new_span_info_with_alt_user_id( ) throws ServletException, IOException { // given: filter RequestTracingFilter spyFilter = spy(getBasicFilter()); given(requestMock.getHeader(ALT_USER_ID_HEADER_KEY)).willReturn("testUserId"); // when: doFilterInternal is called spyFilter.doFilterInternal(requestMock, responseMock, spanCapturingFilterChain); // then: request attributes should be set with the new span's info assertThat(spanCapturingFilterChain.capturedSpan).isNotNull(); Span newSpan = spanCapturingFilterChain.capturedSpan; assertThat(newSpan.getUserId()).isEqualTo("testUserId"); } @Test public void doFilterInternal_should_set_request_attributes_to_new_span_info() throws ServletException, IOException { // given: filter RequestTracingFilter filter = getBasicFilter(); // when: doFilterInternal is called filter.doFilterInternal(requestMock, responseMock, spanCapturingFilterChain); // then: request attributes should be set with the new span's info assertThat(spanCapturingFilterChain.capturedSpan).isNotNull(); Span newSpan = spanCapturingFilterChain.capturedSpan; verify(requestMock).setAttribute(TraceHeaders.TRACE_SAMPLED, newSpan.isSampleable()); verify(requestMock).setAttribute(TraceHeaders.TRACE_ID, newSpan.getTraceId()); verify(requestMock).setAttribute(TraceHeaders.SPAN_ID, newSpan.getSpanId()); verify(requestMock).setAttribute(TraceHeaders.PARENT_SPAN_ID, newSpan.getParentSpanId()); verify(requestMock).setAttribute(TraceHeaders.SPAN_NAME, newSpan.getSpanName()); verify(requestMock).setAttribute(Span.class.getName(), newSpan); } @Test public void doFilterInternal_should_set_trace_id_in_response_header() throws ServletException, IOException { // given: filter RequestTracingFilter filter = getBasicFilter(); // when: doFilterInternal is called filter.doFilterInternal(requestMock, responseMock, spanCapturingFilterChain); // then: response header should be set with the span's trace ID assertThat(spanCapturingFilterChain.capturedSpan).isNotNull(); verify(responseMock).setHeader(TraceHeaders.TRACE_ID, spanCapturingFilterChain.capturedSpan.getTraceId()); } @Test public void doFilterInternal_should_use_parent_span_info_if_present_in_request_headers( ) throws ServletException, IOException { // given: filter and request that has parent span info RequestTracingFilter filter = getBasicFilter(); Span parentSpan = Span.newBuilder("someParentSpan", null) .withParentSpanId(TraceAndSpanIdGenerator.generateId()) .withSampleable(false) .withUserId("someUser") .build(); given(requestMock.getHeader(TraceHeaders.TRACE_ID)).willReturn(parentSpan.getTraceId()); given(requestMock.getHeader(TraceHeaders.SPAN_ID)).willReturn(parentSpan.getSpanId()); given(requestMock.getHeader(TraceHeaders.PARENT_SPAN_ID)).willReturn(parentSpan.getParentSpanId()); given(requestMock.getHeader(TraceHeaders.SPAN_NAME)).willReturn(parentSpan.getSpanName()); given(requestMock.getHeader(TraceHeaders.TRACE_SAMPLED)).willReturn(String.valueOf(parentSpan.isSampleable())); given(requestMock.getServletPath()).willReturn("/some/path"); given(requestMock.getMethod()).willReturn("GET"); // when: doFilterInternal is called filter.doFilterInternal(requestMock, responseMock, spanCapturingFilterChain); // then: the span that is created should use the parent span info as its parent assertThat(spanCapturingFilterChain.capturedSpan).isNotNull(); Span newSpan = spanCapturingFilterChain.capturedSpan; assertThat(newSpan.getTraceId()).isEqualTo(parentSpan.getTraceId()); assertThat(newSpan.getSpanId()).isNotEqualTo(parentSpan.getSpanId()); assertThat(newSpan.getParentSpanId()).isEqualTo(parentSpan.getSpanId()); assertThat(newSpan.getSpanName()).isEqualTo( filter.getInitialSpanName(requestMock, filter.tagAndNamingStrategy, filter.tagAndNamingAdapter) ); assertThat(newSpan.isSampleable()).isEqualTo(parentSpan.isSampleable()); assertThat(newSpan.getSpanPurpose()).isEqualTo(SpanPurpose.SERVER); } @Test public void doFilterInternal_should_use_user_id_from_parent_span_info_if_present_in_request_headers( ) throws ServletException, IOException { // given: filter and request that has parent span info RequestTracingFilter spyFilter = spy(getBasicFilter()); given(requestMock.getHeader(ALT_USER_ID_HEADER_KEY)).willReturn("testUserId"); Span parentSpan = Span.newBuilder("someParentSpan", null) .withParentSpanId(TraceAndSpanIdGenerator.generateId()) .withSampleable(false) .withUserId("someUser") .build(); given(requestMock.getHeader(TraceHeaders.TRACE_ID)).willReturn(parentSpan.getTraceId()); given(requestMock.getHeader(TraceHeaders.SPAN_ID)).willReturn(parentSpan.getSpanId()); given(requestMock.getHeader(TraceHeaders.PARENT_SPAN_ID)).willReturn(parentSpan.getParentSpanId()); given(requestMock.getHeader(TraceHeaders.SPAN_NAME)).willReturn(parentSpan.getSpanName()); given(requestMock.getHeader(TraceHeaders.TRACE_SAMPLED)).willReturn(String.valueOf(parentSpan.isSampleable())); given(requestMock.getServletPath()).willReturn("/some/path"); given(requestMock.getMethod()).willReturn("GET"); // when: doFilterInternal is called spyFilter.doFilterInternal(requestMock, responseMock, spanCapturingFilterChain); // then: the span that is created should use the parent span info as its parent assertThat(spanCapturingFilterChain.capturedSpan).isNotNull(); Span newSpan = spanCapturingFilterChain.capturedSpan; assertThat(newSpan.getUserId()).isEqualTo("testUserId"); } @DataProvider(value = { "true", "false" }) @Test public void doFilterInternal_should_use_getInitialSpanName_for_span_name( boolean parentSpanExists ) throws ServletException, IOException { // given RequestTracingFilter filterSpy = spy(getBasicFilter()); filterSpy.tagAndNamingStrategy = tagAndNamingStrategy; filterSpy.tagAndNamingAdapter = tagAndNamingAdapterMock; String expectedSpanName = UUID.randomUUID().toString(); doReturn(expectedSpanName).when(filterSpy).getInitialSpanName( any(HttpServletRequest.class), any(HttpTagAndSpanNamingStrategy.class), any(HttpTagAndSpanNamingAdapter.class) ); if (parentSpanExists) { given(requestMock.getHeader(TraceHeaders.TRACE_ID)).willReturn(TraceAndSpanIdGenerator.generateId()); given(requestMock.getHeader(TraceHeaders.SPAN_ID)).willReturn(TraceAndSpanIdGenerator.generateId()); } // when filterSpy.doFilterInternal(requestMock, responseMock, spanCapturingFilterChain); // then assertThat(spanCapturingFilterChain.captureSpanCopyAtTimeOfDoFilter).isNotNull(); assertThat(spanCapturingFilterChain.captureSpanCopyAtTimeOfDoFilter.getSpanName()).isEqualTo(expectedSpanName); verify(filterSpy).getInitialSpanName(requestMock, tagAndNamingStrategy, tagAndNamingAdapterMock); } @DataProvider(value = { "true | true", "true | false", "false | true", "false | false", }, splitBy = "\\|") @Test public void doFilterInternal_should_reset_tracing_info_to_whatever_was_on_the_thread_originally( boolean isAsync, boolean throwExceptionInInnerFinallyBlock ) { // given final RequestTracingFilter filter = getBasicFilter(); if (isAsync) { setupAsyncContextWorkflow(); } RuntimeException exToThrowInInnerFinallyBlock = null; if (throwExceptionInInnerFinallyBlock) { exToThrowInInnerFinallyBlock = new RuntimeException("kaboom"); doThrow(exToThrowInInnerFinallyBlock).when(requestMock).isAsyncStarted(); } Tracer.getInstance().startRequestWithRootSpan("someOutsideSpan"); TracingState originalTracingState = TracingState.getCurrentThreadTracingState(); // when Throwable ex = catchThrowable( () -> filter.doFilterInternal(requestMock, responseMock, spanCapturingFilterChain) ); // then if (throwExceptionInInnerFinallyBlock) { assertThat(ex).isSameAs(exToThrowInInnerFinallyBlock); } assertThat(TracingState.getCurrentThreadTracingState()).isEqualTo(originalTracingState); assertThat(spanCapturingFilterChain.capturedSpan).isNotNull(); // The original tracing state was replaced on the thread before returning, but the span used by the filter chain // should *not* come from the original tracing state - it should have come from the incoming headers or // a new one generated. assertThat(spanCapturingFilterChain.capturedSpan.getTraceId()) .isNotEqualTo(originalTracingState.spanStack.peek().getTraceId()); } @Test public void doFilterInternal_should_call_setupTracingCompletionWhenAsyncRequestCompletes_when_isAsyncRequest_returns_true( ) throws ServletException, IOException { // given RequestTracingFilter filterSpy = spy(getBasicFilter()); setupAsyncContextWorkflow(); doReturn(true).when(filterSpy).isAsyncRequest(any(HttpServletRequest.class)); // when filterSpy.doFilterInternal(requestMock, responseMock, spanCapturingFilterChain); // then assertThat(spanCapturingFilterChain.capturedSpan).isNotNull(); assertThat(spanCapturingFilterChain.capturedSpan.isCompleted()).isFalse(); verify(filterSpy).setupTracingCompletionWhenAsyncRequestCompletes( eq(requestMock), eq(responseMock), any(TracingState.class), any(HttpTagAndSpanNamingStrategy.class), any(HttpTagAndSpanNamingAdapter.class) ); } @Test public void doFilterInternal_should_not_call_setupTracingCompletionWhenAsyncRequestCompletes_when_isAsyncRequest_returns_false( ) throws ServletException, IOException { // given RequestTracingFilter filterSpy = spy(getBasicFilter()); doReturn(false).when(filterSpy).isAsyncRequest(any(HttpServletRequest.class)); // when filterSpy.doFilterInternal(requestMock, responseMock, spanCapturingFilterChain); // then assertThat(spanCapturingFilterChain.capturedSpan).isNotNull(); assertThat(spanCapturingFilterChain.capturedSpan.isCompleted()).isTrue(); verify(filterSpy, never()).setupTracingCompletionWhenAsyncRequestCompletes( any(HttpServletRequest.class), any(HttpServletResponse.class), any(TracingState.class), any(HttpTagAndSpanNamingStrategy.class), any(HttpTagAndSpanNamingAdapter.class) ); } @Test public void doFilterInternal_should_add_async_listener_but_not_complete_span_when_async_request_is_detected( ) throws ServletException, IOException { // given RequestTracingFilter filterSpy = spy(getBasicFilter()); setupAsyncContextWorkflow(); // when filterSpy.doFilterInternal(requestMock, responseMock, spanCapturingFilterChain); // then assertThat(spanCapturingFilterChain.capturedSpan).isNotNull(); assertThat(spanCapturingFilterChain.capturedSpan.isCompleted()).isFalse(); assertThat(capturedAsyncListeners).hasSize(1); assertThat(capturedAsyncListeners.get(0)).isInstanceOf(WingtipsRequestSpanCompletionAsyncListener.class); verify(filterSpy).setupTracingCompletionWhenAsyncRequestCompletes( eq(requestMock), eq(responseMock), any(TracingState.class), any(HttpTagAndSpanNamingStrategy.class), any(HttpTagAndSpanNamingAdapter.class) ); } @Test public void doFilterInternal_should_not_add_async_listener_when_isAsyncRequest_returns_false( ) throws ServletException, IOException { // given RequestTracingFilter filterSpy = spy(getBasicFilter()); doReturn(false).when(filterSpy).isAsyncRequest(any(HttpServletRequest.class)); setupAsyncContextWorkflow(); // when filterSpy.doFilterInternal(requestMock, responseMock, spanCapturingFilterChain); // then assertThat(spanCapturingFilterChain.capturedSpan).isNotNull(); assertThat(spanCapturingFilterChain.capturedSpan.isCompleted()).isTrue(); assertThat(capturedAsyncListeners).hasSize(0); verify(filterSpy, never()).setupTracingCompletionWhenAsyncRequestCompletes( any(HttpServletRequest.class), any(HttpServletResponse.class), any(TracingState.class), any(HttpTagAndSpanNamingStrategy.class), any(HttpTagAndSpanNamingAdapter.class) ); } // VERIFY getInitialSpanName ======================== @DataProvider(value = { // Name from strategy always wins "someStrategyName | GET | /some/http/route | someStrategyName", // Null/blank name from strategy defers to HttpSpanFactory.getSpanName(). "null | GET | /some/http/route | GET /some/http/route", " | GET | /some/http/route | GET /some/http/route", "[whitespace] | GET | /some/http/route | GET /some/http/route", "null | null | /some/http/route | UNKNOWN_HTTP_METHOD /some/http/route", "null | null | null | UNKNOWN_HTTP_METHOD" }, splitBy = "\\|") @Test public void getInitialSpanName_works_as_expected( String strategyResult, String httpMethod, String httpRoute, String expectedResult ) { // given RequestTracingFilter filter = getBasicFilter(); if ("[whitespace]".equals(strategyResult)) { strategyResult = " \t\r\n "; } initialSpanNameFromStrategy.set(strategyResult); doReturn(httpMethod).when(requestMock).getMethod(); doReturn(httpRoute).when(requestMock).getAttribute(KnownZipkinTags.HTTP_ROUTE); // when String result = filter.getInitialSpanName(requestMock, tagAndNamingStrategy, tagAndNamingAdapterMock); // then assertThat(result).isEqualTo(expectedResult); assertThat(strategyInitialSpanNameMethodCalled.get()).isTrue(); strategyInitialSpanNameArgs.get().verifyArgs(requestMock, tagAndNamingAdapterMock); } // VERIFY getServletRuntime ========================= @Test public void getServletRuntime_returns_value_of_ServletRuntime_determineServletRuntime_method_and_caches_result() { // given Class<? extends ServletRuntime> expectedServletRuntimeClass = ServletRuntime.determineServletRuntime(requestMock.getClass(), ASYNC_LISTENER_CLASSNAME).getClass(); RequestTracingFilter filter = getBasicFilter(); assertThat(filter.servletRuntime).isNull(); // when ServletRuntime result = filter.getServletRuntime(requestMock); // then assertThat(result.getClass()).isEqualTo(expectedServletRuntimeClass); assertThat(filter.servletRuntime).isSameAs(result); } @Test public void getServletRuntime_uses_cached_value_if_possible() { // given RequestTracingFilter filterSpy = spy(getBasicFilter()); ServletRuntime servletRuntimeMock = mock(ServletRuntime.class); filterSpy.servletRuntime = servletRuntimeMock; // when ServletRuntime result = filterSpy.getServletRuntime(mock(HttpServletRequest.class)); // then assertThat(result).isSameAs(servletRuntimeMock); } // VERIFY isAsyncRequest ============================== @DataProvider(value = { "true", "false" }, splitBy = "\\|") @Test public void isAsyncRequest_delegates_to_ServletRuntime(boolean servletRuntimeResult) { // given RequestTracingFilter filterSpy = spy(getBasicFilter()); doReturn(servletRuntimeMock).when(filterSpy).getServletRuntime(any(HttpServletRequest.class)); doReturn(servletRuntimeResult).when(servletRuntimeMock).isAsyncRequest(any(HttpServletRequest.class)); // when boolean result = filterSpy.isAsyncRequest(requestMock); // then assertThat(result).isEqualTo(servletRuntimeResult); verify(filterSpy).getServletRuntime(requestMock); verify(servletRuntimeMock).isAsyncRequest(requestMock); } // VERIFY setupTracingCompletionWhenAsyncRequestCompletes ============ @Test public void setupTracingCompletionWhenAsyncRequestCompletes_delegates_to_ServletRuntime() { // given RequestTracingFilter filterSpy = spy(getBasicFilter()); doReturn(servletRuntimeMock).when(filterSpy).getServletRuntime(any(HttpServletRequest.class)); TracingState tracingStateMock = mock(TracingState.class); // when filterSpy.setupTracingCompletionWhenAsyncRequestCompletes( requestMock, responseMock, tracingStateMock, tagAndNamingStrategy, tagAndNamingAdapterMock ); // then verify(filterSpy).setupTracingCompletionWhenAsyncRequestCompletes( requestMock, responseMock, tracingStateMock, tagAndNamingStrategy, tagAndNamingAdapterMock ); verify(filterSpy).getServletRuntime(requestMock); verify(servletRuntimeMock).setupTracingCompletionWhenAsyncRequestCompletes( requestMock, responseMock, tracingStateMock, tagAndNamingStrategy, tagAndNamingAdapterMock ); verifyNoMoreInteractions(filterSpy, servletRuntimeMock, requestMock, tracingStateMock); } // VERIFY isAsyncDispatch =========================== @DataProvider(value = { "true", "false" }) @Test @SuppressWarnings("deprecation") public void isAsyncDispatch_delegates_to_ServletRuntime(boolean servletRuntimeResult) { // given RequestTracingFilter filterSpy = spy(getBasicFilter()); doReturn(servletRuntimeMock).when(filterSpy).getServletRuntime(any(HttpServletRequest.class)); doReturn(servletRuntimeResult).when(servletRuntimeMock).isAsyncDispatch(any(HttpServletRequest.class)); // when boolean result = filterSpy.isAsyncDispatch(requestMock); // then assertThat(result).isEqualTo(servletRuntimeResult); verify(filterSpy).getServletRuntime(requestMock); verify(servletRuntimeMock).isAsyncDispatch(requestMock); } // VERIFY skipDispatch ============================== @Test public void skipDispatch_should_return_false() { // given: filter RequestTracingFilter filter = getBasicFilter(); // when: skipDispatchIsCalled boolean result = filter.skipDispatch(requestMock); // then: the result should be false assertThat(result).isFalse(); } private static class SpanCapturingFilterChain implements FilterChain { Span capturedSpan; Span captureSpanCopyAtTimeOfDoFilter; @Override public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { capturedSpan = Tracer.getInstance().getCurrentSpan(); captureSpanCopyAtTimeOfDoFilter = Span.newBuilder(capturedSpan).build(); } } public static class TagStrategyExtension extends HttpTagAndSpanNamingStrategy<HttpServletRequest, HttpServletResponse> { @Override protected void doHandleRequestTagging( @NotNull Span span, @NotNull HttpServletRequest request, @NotNull HttpTagAndSpanNamingAdapter<HttpServletRequest, ?> adapter ) { } @Override protected void doHandleResponseAndErrorTagging( @NotNull Span span, @Nullable HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Throwable error, @NotNull HttpTagAndSpanNamingAdapter<HttpServletRequest, HttpServletResponse> adapter ) { } } public static class TagAdapterExtension extends HttpTagAndSpanNamingAdapter<HttpServletRequest, HttpServletResponse> { @Override public @Nullable String getRequestUrl(@Nullable HttpServletRequest request) { return null; } @Override public @Nullable String getRequestPath( @Nullable HttpServletRequest request ) { return null; } @Override public @Nullable String getRequestUriPathTemplate( @Nullable HttpServletRequest request, @Nullable HttpServletResponse response ) { return null; } @Override public @Nullable Integer getResponseHttpStatus( @Nullable HttpServletResponse response ) { return null; } @Override public @Nullable String getRequestHttpMethod( @Nullable HttpServletRequest request ) { return null; } @Override public @Nullable String getHeaderSingleValue( @Nullable HttpServletRequest request, @NotNull String headerKey ) { return null; } @Override public @Nullable List<String> getHeaderMultipleValue( @Nullable HttpServletRequest request, @NotNull String headerKey ) { return null; } @Override public @Nullable String getSpanHandlerTagValue( @Nullable HttpServletRequest request, @Nullable HttpServletResponse response ) { return null; } } }