I'm having troubles testing a Spring WebFlux controller secured by Spring Security's #Secured annotation. Here is my controller code :
#RestController
#RequestMapping("/endpoint")
public class MyController {
#GetMapping()
#Secured("ADMIN")
public Flux<MyOutputDto> getOutputDtos() {
return myService.getOutputDtos();
}
}
And here is my test code :
#WebFluxTest(MyController.class)
class MyControllerTest {
#Autowired
WebTestClient webTestClient;
#Test
#WithApplicationUser(roles = "ADMIN")
void should_work_fine() {
webTestClient.get()
.uri("/endpoint")
.exchange()
.expectStatus().isOk();
}
#Test
void should_return_401_unauthorized() {
webTestClient.get()
.uri("/endpoint")
.exchange()
.expectStatus().isUnauthorized();
}
#Test
#WithApplicationUser
void should_return_403_forbidden() {
webTestClient.get()
.uri("/endpoint")
.exchange()
.expectStatus().isForbidden();
}
}
The #WithApplicationUser annotation is a custom annotation that injects a mock authentication object in the security context with provided roles. If no roles are provided (like in the third test) then it defaults to no role at all.
The problem here is that the first 2 tests work fine, but the third fails by returning 200 OK instead of 403 Forbidden. My first thought went for Spring Security not processing #Secured annotation, so I followed many Spring WebFlux/Spring Security documentation and online articles, but none worked.
Does someone have an idea for this ? Thanks in advance
Okay so I figured out what was going on.
First, the #Secured annotation seems to not be processed by Spring Security for reactive applications (i.e Spring WebFlux). With #EnableReactiveMethodSecurity you must use #PreAuthorize annotations.
Then, I had to create a test configuration and import it to my test and it worked like a charm.
#TestConfiguration
#EnableReactiveMethodSecurity
public class WebFluxControllerSecurityTestConfig {
}
#WebFluxTest(MyController.class)
#Import(WebFluxControllerSecurityTestConfig.class)
class MyControllerTest {
// Same tests
}
Related
I currently have an app built with Spring Boot 2, Spring MVC, Spring Data/JPA and Thymeleaf.
I'm writing some unit/integration tests and I'd like to test the controller, which is secured by SpringSecurity backed by a database with registered users.
What would be the best approach here to test it? I've unsuccessfully tried a few of them like using annotations like #WithMockUser.
Edit: Just a reminder that I'm not testing #RestControllers. I'm directly injecting a #Controller on my test class and calling its methods. It works just fine without Spring Security.
One example:
#Controller
public class SecuredController {
#GetMapping("/")
public String index() {
return "index";
}
}
The / path is secured by Spring Security and would normally redirect to /login to authenticate the user.
My unit test would look like this:
#WebMvcTest(controllers = SecuredController.class)
class SecuredControllerTest {
#Autowired
private SecuredController controller;
#Autowired
private MockMvc mockMvc;
#Test
#WithMockUser(username = "user", password = "pass", roles = {"USER"})
public void testAuthenticatedIndex() throws Exception {
mockMvc.perform(get("/"))
.andExpect(status().isOk())
.andDo(print());
}
}
The first errors I get is that is asks me to inject my UserDetailsService implementation, which is something that I'd like to avoid. But if I do inject the service, the test works, but returns 404 instead of 200.
Any ideas?
You will need to add your security configurations to the Spring context by importing your WebSecurityConfigurerAdapter class.
#WebMvcTest(controllers = SecuredController.class)
#Import(SecuredControllerTest.Config.class)
class SecuredControllerTest {
#Configuration
#EnableWebSecurity
static class Config extends MyWebSecurityConfigurerAdapter {
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("user").password("pa$$").roles("USER");
auth.inMemoryAuthentication().withUser("admin").password("pa$$").roles("ADMIN");
}
}
...
}
The embedded static class Config is just to change where we get the users from, in this case an inMemoryAuthentication will be enough.
In test class, use annotations
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
in setup test method
#Before
In real test method
#WithMockUser("spring")
#Test
Testing Spring Security like these examples
https://spring.io/blog/2014/05/23/preview-spring-security-test-web-security
https://www.baeldung.com/spring-security-integration-tests
I have created a Custom annotation to version my APIs.
Everything works when running the application.
However when I try to test my controllers using MockMvc, the custom RequestMappingHandlerMapping I wrote isn't being applied.
I'm initializing MockMvc like this
#Before
public void setup() {
mockMvc = MockMvcBuilders
.webAppContextSetup(webApplicationContext)
.apply(documentationConfiguration(this.restDocumentation))
.apply(springSecurity())
.build();
}
I override the defaults to use my custom RequestMappingHandlerMapping like this
#Configuration
public class RoutingConfig {
#Bean
public WebMvcRegistrations webMvcRegistrationsPathHandlerMapping() {
return new WebMvcRegistrations() {
#Override
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
return new APIPathRequestHandlerMapping();
}
};
}
}
Any idea what's going on? I thought the web application context initialization of the MockMvc would pick up all the configuration changes by default.
EDIT 1:
I should also add that I'm using Spring Boot 2.1.2.RELEASE
EDIT 2:
To clarify, the versioning annotation when applied to a controller accepts request that starts with the version, i.e: /users becomes /v1/users
This works with normal requests are coming up, but for tests only /users work, /v1/users returns a 404 (Not found)
I've placed debug points in the configs and the custom RequestMappingHandlerMapping and am sure that this is not being picked up by MockMvc.
I've tried to autowire MockMvc, but the same behaviour persists, with the additional issue of not being able to configure Spring RestDocs.
#AutoConfigureMockMvc Annotation that can be applied to a test class to enable and configure auto-configuration of MockMvc.
#RunWith(SpringRunner.class)
#SpringBootTest
#AutoConfigureMockMvc
public class DemoApplicationTests {
#Autowired
private MockMvc mockMvc;
#Test
public void contextLoads() {
System.out.println("test "+mockMvc);
}
}
Note: I applied custom RequestMappingHandlerMapping, it is getting applied with MockMvc autoconfiguration successfully.
I'm trying to create a controller test with #WebMvcTest, and as I understand, when I put #WebMvcTest(ClientController.class) annotation of the test class it should not create a whole lot of beans, but just ones that this controller requires.
I'm mocking the bean this controller requires with #MockBean, but somehow it fails with an exception that there's 'No qualifying bean' of another service that does not required by this controller but by another.
So this test is failing:
#RunWith(SpringRunner.class)
#WebMvcTest(controllers = ClientController.class)
public class ClientControllerTest {
#MockBean
ClientService clientService;
#Test
public void getClient() {
assertEquals(1,1);
}
}
I've created an empty Spring Boot project of the same version (2.0.1) and tried to create test over there. It worked perfectly.
So my problem might be because of the dependencies that my project has many, but maybe there's some common practice where to look in this situation? What can mess #WebMvcTest logic?
I've found a workaround. Not to use #WebMvcTest and #MockBean, but to create everything by hand:
//#WebMvcTest(ClientController.class)
#RunWith(SpringRunner.class)
public class ClientControllerTest {
private MockMvc mockMvc;
#Mock
ClientService clientService;
#Before
public void setUp() {
mockMvc = MockMvcBuilders.standaloneSetup(
new ClientController(clientService)
).build();
}
works with Spring 1.4.X and with Spring Boot 2.X (had different exception there and there), but still doesn't explain why #WebMvcTest doesn't work
I'm testing a Spring MVC #RestController which in turn makes a call to an external REST service. I use MockMvc to simulate the spring environment but I expect my controller to make a real call to the external service. Testing the RestController manually works fine (with Postman etc.).
I found that if I setup the test in a particular way I get a completely empty response (except the status code):
#RunWith(SpringJUnit4ClassRunner.class)
#WebAppConfiguration
#ContextConfiguration(classes = AnywhereController.class)
public class AnywhereControllerTest{
#Autowired
private AnywhereController ac;
#Autowired
private WebApplicationContext wac;
private MockMvc mockMvc;
#Before
public void setup() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
}
#Test
public void testGetLocations() throws Exception {
...
MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/anywhere/locations").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().string(containsString("locations")))
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON));
.andReturn();
}
The test fails because the content and headers are empty. Then I tried adding this to the test class:
#Configuration
#EnableWebMvc
public static class TestConfiguration{
#Bean
public AnywhereController anywhereController(){
return new AnywhereController();
}
}
and additionally I changed the ContextConfiguration annotation (although I'd like to know what this actually does):
#RunWith(SpringJUnit4ClassRunner.class)
#WebAppConfiguration
#ContextConfiguration
public class AnywhereControllerTest{...}
Now suddenly all the checks succeed and when printing the content body I'm getting all the content.
What is happening here? What is the difference between these two approaches?
Someone in the comments mentioned #EnableWebMvc and it turned out this was the right lead.
I wasn't using #EnableWebMvc and therefore
If you don't use this annotation you might not initially notice any difference but things like content-type and accept header, generally content negotiation won't work. Source
My knowledge about the inner workings of the framework is limited but it a simple warning during startup could potentially save many hours of debugging. Chances are high that when people use #Configuration and/or #RestController that they also want to use #EnableWebMvc (or the xml version of it).
Making things even worse, Spring Boot for example adds this annotation automatically, which is why many tutorials on the internet (also the official ones) don't mention #EnableWebMvc.
I'm testing my Spring MVC controllers using JUnit, Mockito & spring-test. Unfortunately, the tests are ignoring the #PreAuthorize annotations on my controller methods, and I've been unable to resolve this. Key code snippets are below, though I've removed irrelevant logic for mocking responses from MyController's dependencies to keep it short. I'm using Spring 3.2 & JUnit 4, and I'm running the tests both via Maven and directly through Eclipse (Run as -> JUnit test).
Right now I am expecting the getAccounts_ReturnsOkStatus test to fail, as I have not provided any auth and the method that the /accounts route maps onto is annotated with a pre-auth annotation, however the method is being invoked and the pre-auth check bypassed.
MyControllerTest.java
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(locations = { "classpath:applicationContext-test.xml" })
public class MyControllerTest {
private MockMvc mockMvc;
#Mock
private MyService myService;
#InjectMocks
private MyController myController;
#Before
public void init() {
MockitoAnnotations.initMocks(this);
mockMvc = MockMvcBuilders.standaloneSetup(myController).build();
}
#Test
public void getAccounts_ReturnsOkStatus() throws Exception {
// Make the GET request, and verify that it returns HttpStatus.OK (200)
mockMvc.perform(MockMvcRequestBuilders.get("/accounts"))
.andExpect(MockMvcResultMatchers.status().isOk());
}
}
applicationContext-test.xml
<sec:global-method-security pre-post-annotations="enabled" />
<bean id="applicationContextProvider" class="com.myapp.springsupport.ApplicationContextProvider" />
<sec:authentication-manager alias="authenticationManager">
<sec:authentication-provider>
<sec:user-service>
<sec:user name="test-superuser" password="test-pwd" authorities="ROLE_SUPERUSER" />
</sec:user-service>
</sec:authentication-provider>
</sec:authentication-manager>
MyController.java
#PreAuthorize("isAuthenticated() and hasRole('ROLE_SUPERUSER')")
#RequestMapping(value = "/accounts", method = RequestMethod.GET)
#ResponseBody
public Collection<Account> getAccounts() {
return new ArrayList<Account>();
}
The applicationContext-test app context is definitely being used, since manually authenticating using
Authentication auth = new UsernamePasswordAuthenticationToken(name, password);
SecurityContextHolder.getContext().setAuthentication(am.authenticate(auth));
only works with the user credentials specified in the test config (in the absence of any other authentication-provider). Additionally, I can be sure that the pre-auth is being ignored, since I've tested using SEL to invoke a method & debugged.
What am I missing?
You have to enable spring security for testing when building mockMvc.
In Spring 4 it's like this:
mockMvc = MockMvcBuilders.webAppContextSetup(context)
.apply(springSecurity())
.build();
In Spring 3 it's like this:
#Autowired
private Filter springSecurityFilterChain;
...
mockMvc = MockMvcBuilders.webAppContextSetup(context)
.addFilters(springSecurityFilterChain)
.build();
For more details see:
https://spring.io/blog/2014/05/23/preview-spring-security-test-web-security
How do I unit test spring security #PreAuthorize(hasRole)?
Spring MVC integration tests with Spring Security
MockMvcBuilders.standaloneSetup gets a MyController instantiated manually ( without Spring and therefore without AOP). Therefore the #PreAuthorize is not intercepted and security check is skipped. You can therefore #Autowire your controller and pass it to MockMvcBuilders.standaloneSetup to Mock MyService use #MockBean so that every instance of the service gets replaced with the Mock.