I'm trying to run an integration test for my controller but I am running into issues if I don't authenticate. Here's my controller:
@RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @TestPropertySource(properties = {"security.basic.enabled=false", "management.security.enabled=false"}) @EnableAutoConfiguration(exclude = {org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration.class}) public class HelloControllerIT { private final ObjectMapper mapper = new ObjectMapper(); @Autowired private TestRestTemplate template; @Test public void test1() throws Exception { ObjectNode loginRequest = mapper.createObjectNode(); loginRequest.put("username","name"); loginRequest.put("password","password"); JsonNode loginResponse = template.postForObject("/authenticate", loginRequest.toString(), JsonNode.class); HttpHeaders headers = new HttpHeaders(); headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON)); headers.add("X-Authorization", "Bearer " + loginResponse.get("token").textValue()); headers.add("Content-Type", "application/json"); return new HttpEntity<>(null, headers); HttpEntity request = getRequestEntity(); ResponseEntity response = template.exchange("/get", HttpMethod.GET, request, new ParameterizedTypeReference<List<Foo>>() {}); //assert stuff } }
When I run this, everything works. But if I comment out the line:
headers.add("X-Authorization", "Bearer " + loginResponse.get("token").textValue());
I get the error:
org.springframework.http.converter.HttpMessageNotReadableException: Could not read JSON document: Can not deserialize instance of java.util.ArrayList out of START_OBJECT token at [Source: java.io.PushbackInputStream@272a5bc6; line: 1, column: 1]; nested exception is com.fasterxml.jackson.databind.JsonMappingException: Can not deserialize instance of java.util.ArrayList out of START_OBJECT token at [Source: java.io.PushbackInputStream@272a5bc6; line: 1, column: 1] at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:234) at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.read(AbstractJackson2HttpMessageConverter.java:219) at org.springframework.web.client.HttpMessageConverterExtractor.extractData(HttpMessageConverterExtractor.java:95) at org.springframework.web.client.RestTemplate$ResponseEntityResponseExtractor.extractData(RestTemplate.java:917) at org.springframework.web.client.RestTemplate$ResponseEntityResponseExtractor.extractData(RestTemplate.java:901) at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:655) at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:613) at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:559) at org.springframework.boot.test.web.client.TestRestTemplate.exchange(TestRestTemplate.java:812) at com.test.HelloControllerIT.test1(HelloControllerIT.java:75) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47) at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26) at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75) at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86) at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84) at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:252) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:94) at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288) at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268) at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61) at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70) at org.junit.runners.ParentRunner.run(ParentRunner.java:363) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:191) at org.junit.runner.JUnitCore.run(JUnitCore.java:137) at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68) at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:51) at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242) at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70) Caused by: com.fasterxml.jackson.databind.JsonMappingException: Can not deserialize instance of java.util.ArrayList out of START_OBJECT token at [Source: java.io.PushbackInputStream@272a5bc6; line: 1, column: 1] at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:270) at com.fasterxml.jackson.databind.DeserializationContext.reportMappingException(DeserializationContext.java:1234) at com.fasterxml.jackson.databind.DeserializationContext.handleUnexpectedToken(DeserializationContext.java:1122) at com.fasterxml.jackson.databind.DeserializationContext.handleUnexpectedToken(DeserializationContext.java:1075) at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.handleNonArray(CollectionDeserializer.java:338) at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:269) at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:259) at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:26) at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3798) at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2922) at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:231) ... 38 more
Obviously the security annotations at the top are not working. So what exactly is the issue and how do I fix it?
Edit 1: I tried doing:
Object response = template.exchange("/get", HttpMethod.GET, request, Object.class);
And got:
<401 Unauthorized,{status=401, message=Authentication failed, errorCode=10, timestamp=1497654855545},{X-Content-Type-Options=[nosniff], X-XSS-Protection=[1; mode=block], Cache-Control=[no-cache, no-store, max-age=0, must-revalidate], Pragma=[no-cache], Expires=[0], X-Frame-Options=[DENY], Content-Type=[application/json;charset=ISO-8859-1], Content-Length=[89], Date=[Fri, 16 Jun 2017 23:14:15 GMT]}>
For our security we're using org.springframework.security.authentication.AuthenticationProvider
and org.springframework.security.authentication.AuthenticationManager
Edit 2: Per skadya's suggestion I created a new class like so:
@Configuration public class AnonymousConfig extends WebSecurityConfigurerAdapter { @Override public void configure(HttpSecurity web) throws Exception { web.antMatcher("**/*").anonymous(); } }
But now when I run my integration test I get the following error:
java.lang.IllegalStateException: Failed to load ApplicationContext at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:124) at org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:83) at org.springframework.boot.test.autoconfigure.SpringBootDependencyInjectionTestExecutionListener.prepareTestInstance(SpringBootDependencyInjectionTestExecutionListener.java:47) at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:230) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.createTest(SpringJUnit4ClassRunner.java:228) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner$1.runReflectiveCall(SpringJUnit4ClassRunner.java:287) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.methodBlock(SpringJUnit4ClassRunner.java:289) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:247) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:94) at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288) at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268) at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61) at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70) at org.junit.runners.ParentRunner.run(ParentRunner.java:363) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:191) at org.junit.runner.JUnitCore.run(JUnitCore.java:137) at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68) at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:51) at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242) at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70) Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration': Injection of autowired dependencies failed; nested exception is java.lang.IllegalStateException: @Order on WebSecurityConfigurers must be unique. Order of 100 was already used on config.AnonymousConfig$$EnhancerBySpringCGLIB$$ba18b8d7@6291f725, so it cannot be used on security.WebSecurityConfig$$EnhancerBySpringCGLIB$$9d88e7e@1bfaaae1 too. at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:372) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1264) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:553) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:483) at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306) at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230) at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302) at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197) at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:761) at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:866) at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:542) at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.refresh(EmbeddedWebApplicationContext.java:122) at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:737) at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:370) at org.springframework.boot.SpringApplication.run(SpringApplication.java:314) at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:120) at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:98) at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:116) ... 23 more Caused by: java.lang.IllegalStateException: @Order on WebSecurityConfigurers must be unique. Order of 100 was already used on config.AnonymousConfig$$EnhancerBySpringCGLIB$$ba18b8d7@6291f725, so it cannot be used on security.WebSecurityConfig$$EnhancerBySpringCGLIB$$9d88e7e@1bfaaae1 too. at org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration.setFilterChainProxySecurityConfigurer(WebSecurityConfiguration.java:148) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement.inject(AutowiredAnnotationBeanPostProcessor.java:701) at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:88) at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:366) ... 40 more
Looks like it's clashing with the websecurity config we have in the normal project. Here's that file:
@EnableWebSecurity @EnableWebMvc @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { //configuration }
I tried adding @Order(1000)
which fixed the above issue but still ended up in a 401 Unauthorized
You can try excluding few more auto configurations:
@EnableAutoConfiguration(exclude = { org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration.class, org.springframework.boot.autoconfigure.security.SecurityFilterAutoConfiguration.class, org.springframework.boot.autoconfigure.security.FallbackWebSecurityAutoConfiguration.class, org.springframework.boot.autoconfigure.security.oauth2.OAuth2AutoConfiguration.class })
Btw, more elegant way of excluding stuff is by defining application-test.properties
in your test sources and marking your test with @Profile("test")
. Then just add this to your config:
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration,org.springframework.boot.autoconfigure.security.SecurityFilterAutoConfiguration,org.springframework.boot.autoconfigure.security.FallbackWebSecurityAutoConfiguration,org.springframework.boot.autoconfigure.security.oauth2.OAuth2AutoConfiguration
All the possible configurations that can be excluded you can find here: spring.factories
You have couple of options to provide authentication in the spring boot integration test. You may need to adjust a few things to make it all work at your end.
Mock based approach
This uses test WebApplicationContext
injected into MockMvc with @WithMockUser annotation to provide authentication user and WithMockUserSecurityContextFactory
creating the security context for the mock user.
SecurityMockMvcConfigurers
registers the security filter springSecurityFilterChain
with MockMvc
.
import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) public class HelloControllerIT { @Autowired private WebApplicationContext context; private MockMvc mvc; @Before public void setup() { mvc = MockMvcBuilders .webAppContextSetup(context) .apply(springSecurity()) // enable security for the mock set up .build(); } @WithMockUser(value = "test", password = "pass") @Test public void test() throws Exception { String contentType = MediaType.APPLICATION_JSON + ";charset=UTF-8"; String authzToken = mvc .perform( post("/authenticate") .contentType( MediaType.APPLICATION_JSON). content("")). andExpect(status().isOk()) .andExpect(content().contentType(contentType)) .andExpect(jsonPath("$.token", is(notNullValue()))) .andReturn().getResponse().getContentAsString(); System.out.print(authzToken);//{"token":"1a3434a"} } }
In-memory auth provider based approach
This uses in-memory auth provider with basic authentication user.
Register in-memory auth provider and enable basic auth, disable anonymous access in HttpSecurity
in the WebSecurityConfigurerAdapter
.
When in-memory provider is registered, DefaultInMemoryUserDetailsManagerConfigurer
creates the basic auth user in the memory.
When basic authentication is enabled, HttpBasicConfigurer
configures BasicAuthenticationFilter
. Authenticates the test user and creates the security context.
Security Configuration
@EnableWebSecurity @EnableWebMvc @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override public void configure(AuthenticationManagerBuilder auth) throws Exception { // register test user with in memory authentication provider auth.inMemoryAuthentication().withUser("test").password("pass").roles("ROLES"); } @Override public void configure(HttpSecurity http) throws Exception { // enable basic authentication & disable anoymous access http.authorizeRequests().anyRequest().authenticated().and().httpBasic().and().anonymous().disable(); } }
Authentication Endpoint
@Controller @RequestMapping("/authenticate") public class AuthenticationController { @RequestMapping(method = RequestMethod.POST) @ResponseBody public TokenClass getToken() { TokenClass tokenClass = new TokenClass(); tokenClass.setToken("1a3434a"); return tokenClass; } }
Pojo
public class TokenClass { private String token; public String getToken() { return token; } public void setToken(String token) { this.token = token; } }
Test Controller
import com.fasterxml.jackson.databind.JsonNode; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.http.*; import org.springframework.test.context.junit4.SpringRunner; import java.util.Arrays; import java.util.Base64; @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class HelloControllerIT { @Autowired private TestRestTemplate template; @Test public void test() throws Exception { HttpHeaders authHeaders = new HttpHeaders(); String token = new String(Base64.getEncoder().encode( ("test" + ":" + "pass").getBytes())); authHeaders.set("Authorization", "Basic " + token); JsonNode loginResponse = template.postForObject("/authenticate", new HttpEntity<>(null, authHeaders), JsonNode.class); HttpHeaders authzHeaders = new HttpHeaders(); authzHeaders.setAccept(Arrays.asList(MediaType.APPLICATION_JSON)); authzHeaders.add("X-Authorization", "Bearer " + loginResponse.get("token").textValue()); authzHeaders.add("Content-Type", "application/json"); ResponseEntity response = template.exchange("/secure", HttpMethod.GET, new HttpEntity<>(null, authzHeaders), String.class ); } }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With