Skip to content

Commit cc427a4

Browse files
authored
Add latestdeptest for spring boot 2 (#8622)
1 parent a7d732d commit cc427a4

20 files changed

+1274
-0
lines changed

dd-java-agent/instrumentation/spring-webmvc-3.1/build.gradle

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ muzzle {
3838

3939
apply from: "$rootDir/gradle/java.gradle"
4040

41+
addTestSuite("latestDepTest")
42+
addTestSuiteExtendingForDir("latestDepForkedTest", "latestDepTest", "latestDepTest")
43+
4144
dependencies {
4245
compileOnly group: 'org.springframework', name: 'spring-webmvc', version: '3.1.0.RELEASE'
4346
compileOnly group: 'javax.servlet', name: 'javax.servlet-api', version: '3.1.0'
@@ -81,4 +84,10 @@ dependencies {
8184
testImplementation "org.glassfish.jaxb:jaxb-runtime:2.3.2"
8285

8386
testImplementation group: 'org.jboss.resteasy', name: 'resteasy-spring', version: '3.0.0.Final'
87+
88+
latestDepTestImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-test', version: '2.+'
89+
latestDepTestImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: '2.+'
90+
latestDepTestImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-websocket', version: '2.+'
91+
latestDepTestImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-security', version: '2.+'
92+
latestDepTestImplementation group: 'org.springframework.security.oauth', name: 'spring-security-oauth2', version: '2.+'
8493
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package datadog.trace.instrumentation.springweb
2+
3+
import datadog.trace.agent.test.AgentTestRunner
4+
import org.springframework.beans.factory.annotation.Autowired
5+
import org.springframework.context.annotation.ComponentScan
6+
import org.springframework.context.annotation.Configuration
7+
import org.springframework.mock.web.MockHttpServletRequest
8+
import org.springframework.test.context.ContextConfiguration
9+
import org.springframework.test.context.web.AnnotationConfigWebContextLoader
10+
import org.springframework.test.context.web.WebAppConfiguration
11+
import org.springframework.web.servlet.config.annotation.EnableWebMvc
12+
13+
import javax.servlet.FilterChain
14+
import javax.servlet.http.HttpServletResponse
15+
16+
import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace
17+
import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activeSpan
18+
import static datadog.trace.bootstrap.instrumentation.decorator.HttpServerDecorator.DD_SPAN_ATTRIBUTE
19+
20+
@WebAppConfiguration // TODO this doesn't exist in 3.1.0 so will need rework if we want to test that version
21+
// https://mvnrepository.com/artifact/org.springframework/spring-test-mvc?repo=springio-plugins-release
22+
// https://github.com/spring-attic/spring-test-mvc
23+
@ContextConfiguration(classes = TestConfiguration, loader = AnnotationConfigWebContextLoader)
24+
class HandlerMappingResourceNameFilterForkedTest extends AgentTestRunner {
25+
@EnableWebMvc
26+
@Configuration
27+
@ComponentScan(basePackages = "datadog.trace.instrumentation.springweb")
28+
static class TestConfiguration {
29+
}
30+
31+
@Autowired
32+
HandlerMappingResourceNameFilter filter
33+
34+
def "test filter doesn't make externally visible changes to request object - url: #url"() {
35+
given:
36+
def request = new MockHttpServletRequest("GET", url)
37+
def response = Mock(HttpServletResponse)
38+
def filterChain = Mock(FilterChain)
39+
40+
when:
41+
runUnderTrace("test-servlet", {
42+
request.setAttribute(DD_SPAN_ATTRIBUTE, activeSpan())
43+
filter.doFilterInternal(request, response, filterChain)
44+
})
45+
46+
then:
47+
assert request.getAttributeNames().toList() == [DD_SPAN_ATTRIBUTE]
48+
0 * response._
49+
50+
where:
51+
url << ["/single", "/not-found"]
52+
}
53+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package datadog.trace.instrumentation.springweb
2+
3+
import datadog.trace.agent.test.AgentTestRunner
4+
import org.jboss.resteasy.plugins.spring.SpringBeanProcessor
5+
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory
6+
import org.springframework.beans.factory.support.BeanDefinitionRegistry
7+
import org.springframework.beans.factory.support.GenericBeanDefinition
8+
import test.boot.TestProvider
9+
10+
class ResteasySpringBeanProcessorForkedTest extends AgentTestRunner {
11+
12+
interface TestBeanFactory extends ConfigurableListableBeanFactory, BeanDefinitionRegistry {}
13+
14+
def "resteasy-spring should ignore our intercepting filter bean"() {
15+
given:
16+
def beanFactory = Mock(TestBeanFactory)
17+
def originalDefinition = new HandlerMappingResourceNameFilter.BeanDefinition()
18+
// trigger capture of the original 'beanClass' value
19+
beanFactory.registerBeanDefinition('filter', originalDefinition)
20+
// mimic situation where the original 'beanClass' value might be lost
21+
def filterDefinition = originalDefinition.clone()
22+
filterDefinition.setBeanClassName(originalDefinition.getBeanClassName()) // discards 'beanClass'
23+
// simple test bean to make sure resteasy isn't ignoring all bean definitions
24+
def testDefinition = Mock(GenericBeanDefinition)
25+
testDefinition.getBeanClassName() >> TestProvider.name
26+
testDefinition.isSingleton() >> true
27+
28+
and:
29+
beanFactory.getBeanDefinitionNames() >> ['filter', 'test']
30+
beanFactory.getBeanDefinition('filter') >> filterDefinition
31+
beanFactory.getBeanDefinition('test') >> testDefinition
32+
33+
and:
34+
def originalContext = Thread.currentThread().contextClassLoader
35+
// mimic isolated setup where our filter bean is not visible to the resteasy-spring context
36+
def isolatedContext = new ClassLoader(null) {
37+
@Override
38+
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
39+
if (name == TestProvider.name) {
40+
return TestProvider
41+
}
42+
throw new ClassNotFoundException(name)
43+
}
44+
}
45+
46+
and:
47+
def resteasyProcessor = new SpringBeanProcessor()
48+
49+
when:
50+
Thread.currentThread().contextClassLoader = isolatedContext
51+
resteasyProcessor.postProcessBeanFactory(beanFactory)
52+
53+
then:
54+
// should have skipped our filter bean, which is not of interest to resteasy
55+
noExceptionThrown()
56+
// simple test bean should still have been picked up
57+
resteasyProcessor.providerNames == ['test'] as Set
58+
59+
cleanup:
60+
Thread.currentThread().contextClassLoader = originalContext
61+
}
62+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package datadog.trace.instrumentation.springweb;
2+
3+
import org.springframework.stereotype.Controller;
4+
import org.springframework.web.bind.annotation.GetMapping;
5+
import org.springframework.web.bind.annotation.RequestMapping;
6+
import org.springframework.web.bind.annotation.ResponseBody;
7+
8+
@Controller
9+
@RequestMapping("/single")
10+
public class SingleEndpointController {
11+
@GetMapping
12+
public @ResponseBody String get() {
13+
return "Hello World";
14+
}
15+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package test
2+
3+
import datadog.appsec.api.blocking.BlockingContentType
4+
import datadog.trace.api.function.TriFunction
5+
import datadog.trace.api.gateway.BlockResponseFunction
6+
import datadog.trace.api.gateway.CallbackProvider
7+
import datadog.trace.api.gateway.EventType
8+
import datadog.trace.api.gateway.Events
9+
import datadog.trace.api.gateway.Flow
10+
import datadog.trace.api.gateway.RequestContext
11+
import datadog.trace.api.gateway.RequestContextSlot
12+
import datadog.trace.api.internal.TraceSegment
13+
import datadog.trace.bootstrap.instrumentation.api.AgentTracer
14+
import datadog.trace.bootstrap.instrumentation.api.URIDataAdapter
15+
import datadog.trace.instrumentation.servlet.ServletBlockingHelper
16+
import org.springframework.web.context.request.RequestContextHolder
17+
import org.springframework.web.context.request.ServletRequestAttributes
18+
19+
class SetupSpecHelper {
20+
static void provideBlockResponseFunction() {
21+
22+
EventType<TriFunction<RequestContext, String, URIDataAdapter, Flow<Void>>> uriEvent = Events.get().requestMethodUriRaw()
23+
24+
// get original callback
25+
CallbackProvider provider = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC)
26+
TriFunction<RequestContext, String, URIDataAdapter, Flow<Void>> origUriCallback = provider.getCallback(uriEvent)
27+
28+
// wrap the original IG callbacks
29+
def ss = AgentTracer.get().getSubscriptionService(RequestContextSlot.APPSEC)
30+
ss.reset(uriEvent)
31+
ss.registerCallback(uriEvent, new TriFunction<RequestContext, String, URIDataAdapter, Flow<Void>>() {
32+
@Override
33+
Flow<Void> apply(RequestContext requestContext, String s, URIDataAdapter uriDataAdapter) {
34+
requestContext.setBlockResponseFunction(TestSpringBlockResponseFunction.INSTANCE)
35+
origUriCallback.apply(requestContext, s, uriDataAdapter)
36+
}
37+
})
38+
}
39+
40+
enum TestSpringBlockResponseFunction implements BlockResponseFunction {
41+
INSTANCE
42+
43+
@Override
44+
boolean tryCommitBlockingResponse(TraceSegment segment, int statusCode, BlockingContentType templateType, Map<String, String> extraHeaders) {
45+
ServletRequestAttributes attributes = RequestContextHolder.requestAttributes
46+
if (attributes) {
47+
ServletBlockingHelper
48+
.commitBlockingResponse(segment, attributes.request, attributes.response, statusCode, templateType, extraHeaders)
49+
}
50+
true
51+
}
52+
}
53+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package test.boot
2+
3+
import org.apache.catalina.connector.Connector
4+
import org.springframework.boot.autoconfigure.SpringBootApplication
5+
import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer
6+
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory
7+
import org.springframework.boot.web.server.WebServerFactoryCustomizer
8+
import org.springframework.boot.web.servlet.filter.OrderedRequestContextFilter
9+
import org.springframework.context.annotation.Bean
10+
import org.springframework.core.Ordered
11+
import org.springframework.web.filter.RequestContextFilter
12+
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer
13+
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter
14+
import org.springframework.web.util.UrlPathHelper
15+
16+
// Component scan defeats the purpose of configuring with specific classes
17+
@SpringBootApplication(scanBasePackages = "doesnotexist")
18+
class AppConfig extends WebMvcConfigurerAdapter {
19+
20+
@Bean
21+
WebServerFactoryCustomizer webServerFactoryCustomizer() {
22+
def factory = new WebServerFactoryCustomizer<TomcatServletWebServerFactory> () {
23+
@Override
24+
void customize(TomcatServletWebServerFactory factory) {
25+
factory.addConnectorCustomizers(new TomcatConnectorCustomizer() {
26+
@Override
27+
void customize(Connector connector) {
28+
connector.setEnableLookups(true)
29+
}
30+
})
31+
}
32+
}
33+
return factory
34+
}
35+
36+
@Override
37+
void configurePathMatch(PathMatchConfigurer configurer) {
38+
configurer.urlPathHelper = new UrlPathHelper(
39+
removeSemicolonContent: false
40+
)
41+
}
42+
43+
// Replace the default request context filter so that it has higher precedence
44+
// and that the following events are correctly ordered:
45+
// * server attributes are registered by the RequestContextFilter. HttpServletRequest and
46+
// HttpServletResponse are available on thread locals
47+
// * HiddenHttpMethodFilter tries to read the parameter _method
48+
// * org.apache.tomcat.util.http.Parameters.processParameters, an instrumented method, is called
49+
// * the instrumentation gives instructions to block
50+
// * the BlockResponseFunction is called. The BlockResponseFunction has access to the server attributes
51+
@Bean
52+
RequestContextFilter requestContextFilter() {
53+
new OrderedRequestContextFilter(order: Ordered.HIGHEST_PRECEDENCE)
54+
}
55+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package test.boot
2+
3+
import org.springframework.context.annotation.Configuration
4+
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter
5+
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer
6+
7+
@Configuration
8+
@EnableAuthorizationServer
9+
class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
10+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package test.boot
2+
3+
import org.springframework.boot.SpringApplication
4+
5+
class CustomBeanClassloaderTest extends SpringBootBasedTest {
6+
7+
@Override
8+
SpringApplication application() {
9+
return new SpringApplication(AppConfig, SecurityConfig, AuthServerConfig, CustomClassloaderConfig, TestController, WebsocketConfig)
10+
}
11+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package test.boot
2+
3+
import groovy.transform.CompileStatic
4+
import org.springframework.beans.BeansException
5+
import org.springframework.beans.factory.config.BeanFactoryPostProcessor
6+
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory
7+
import org.springframework.context.annotation.Bean
8+
import org.springframework.context.annotation.Configuration
9+
10+
/**
11+
* Sets the bean classloader to one that explicitly filters datadog classes
12+
* The goal is to test a classloader that we didn't inject. With our test
13+
* setup, its easier to filter than create a new one
14+
*/
15+
@Configuration
16+
class CustomClassloaderConfig {
17+
18+
@Bean
19+
BeanFactoryPostProcessor postProcessor() {
20+
return new BeanFactoryPostProcessor() {
21+
@Override
22+
void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
23+
beanFactory.setBeanClassLoader(new DatadogFilteringClassloader(this.getClass().getClassLoader()))
24+
}
25+
}
26+
}
27+
}
28+
29+
@CompileStatic // avoid references to java.lang.Module (in super$2$loadClass)
30+
class DatadogFilteringClassloader extends URLClassLoader {
31+
DatadogFilteringClassloader(ClassLoader parent) {
32+
super(new URL[0], parent)
33+
}
34+
35+
protected Class<?> loadClass(String className, boolean resolve)
36+
throws ClassNotFoundException {
37+
if (className.startsWith("datadog")) {
38+
throw new ClassNotFoundException()
39+
}
40+
41+
return super.loadClass(className, resolve)
42+
}
43+
}

0 commit comments

Comments
 (0)