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.
Related
I'm trying to make an integration test going all the way from the top (controller) down to the repositories (mocked). I'm having issue with getting an actual service (not mock) injected into a controller and mocking the service's repositories. Most examples I've seen either call the endpoint via MockMvc and test the controller that contains all the logic, so no services, or they just mock the service and return a set response which I see no point in testing at all. My classes work as such: Controller contains the service which contains the repository
Currently I have the following test class for that:
#WebMvcTest(PatientRecordController.class)
public class PatientRecordControllerTests {
#Autowired
MockMvc mockMvc;
#Autowired
ObjectMapper mapper;
#MockBean
PatientRecordService patientRecordService;
#Test
public void createRecord_WhenDtoValid_CreateRecord() throws Exception {
PatientRecordDto recordDto = PatientRecordDto.builder()
.name("John Doe")
.age(47)
.address("New York USA")
.build();
PatientRecord record = ModelMapperUtil.convertTo(recordDto, PatientRecord.class);
Mockito.when(patientRecordService.saveRecord(recordDto)).thenReturn(record);
MockHttpServletRequestBuilder mockRequest = MockMvcRequestBuilders.post("/patient")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.content(this.mapper.writeValueAsString(recordDto));
mockMvc.perform(mockRequest)
.andExpect(status().isOk())
.andExpect(jsonPath("$", notNullValue()))
.andExpect(jsonPath("$.name", is("John Doe")));
}
}
What I would like to be able to do is something akin to following:
#Autowired
#InjectMocks
private PatientRecordService patientRecordService;
#Mock
private PatientRecordRepository patientRecordRepository;
Theoretically, with this I'd get a bean of PatientRecordService that has PatientRecordRepository mock injected, and then instead of doing
Mockito.when(patientRecordService.saveRecord(recordDto)).thenReturn(record);
I could do
Mockito.when(patientRecordRepository.save(recordDto)).thenReturn(record);
And then the whole logic inside of the service would be tested and only repository response would be mocked. Obviously this is not how it works, so how would I go about in achieving this?
You should be able to
#WebMvcTest({PatientRecordController.class, PatientRecordService.class})
#MockBean
private PatientRecordRepository patientRecordRepository;
Does that not achieve what you need?
I'm trying to unit test a controller class, but have been stuck with the following error. I tried changing notations and following some online tutorials but it has not been working, I always get this same error.
Here's the stackTrace:
java.lang.IllegalArgumentException: WebApplicationContext is required
at org.springframework.util.Assert.notNull(Assert.java:201)
at org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder.<init>(DefaultMockMvcBuilder.java:52)
at org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup(MockMvcBuilders.java:51)
at br.com.gwcloud.smartplace.catalog.controller.test.ItemControllerTest.setUp(ItemControllerTest.java:66)
...
And this is my controller test class:
#SpringBootTest
#WebMvcTest(controllers = ItemController.class)
#ActiveProfiles("test")
#WebAppConfiguration
public class ItemControllerTest {
#MockBean
private ItemRepository ir;
#Autowired
private MockMvc mockMvc;
#Autowired
private ModelMapper modelMapper;
#Autowired
private WebApplicationContext webApplicationContext;
#Autowired
private ObjectMapper objectMapper;
#Before
public void setUp() {
this.mockMvc = webAppContextSetup(webApplicationContext).build();
DefaultMockMvcBuilder builder = MockMvcBuilders.webAppContextSetup(this.webApplicationContext);
this.mockMvc = builder.build();
}
#Test
public void shouldCreateNewItem() throws Exception {
ItemDTO itemDTO = new ItemDTO();
itemDTO.setName("Leo");
itemDTO.setDescription("abc description");
itemDTO.setEnabled(true);
itemDTO.setPartNumber("leo123");
Item item = itemDTO.convertToEntity(modelMapper);
mockMvc.perform(
post("/api/item/").contentType(
MediaType.APPLICATION_JSON).content(objectMapper.writeValueAsString(item))).andExpect(
status().isOk());
}
}
The error you are encountering might be solved by adding:
#RunWith(SpringRunner.class)
Just below #SpringBootTest. Or you could instead extend AbstractJUnit4SpringContextTests.
Another problem is that the WebApplicationContext might not be available in an #Before annotated method. Try moving it into the test method itself.
That said, I usually avoid unit testing controllers, since I don't put any business logic in them. The stuff controllers do is specifying paths, mapping request arguments, error handling (although that is better handled in a separate ControllerAdvice class), setting up the view model and view target etc. These are all what I call 'plumbing' and tie in heavily with the framework you are using. I don't unit test that.
Instead, this plumbing can be validated by having a couple of high level integration tests that actually do a remote call to the controller and execute a complete flow, including all the plumbing.
Any business logic should be taken outside of the controller (typically in services) and unit tested there, in isolation.
Have you tried removing #SpringBootTest and #WebAppConfiguration. If you are only interested in testing controller you don't need a full-blown application through these annotations.
I have a Spring 5 web application which does validations only on service level DTOs. That is to say, the incoming request is not validated in controller, but the DTO going into a service layer method is validated on that level. This is a requirement because this is how this is done on other applications and other people want to keep it like that for consistency.
I have tests that use Spring MockMvc to test the controller methods. I'd like to test the error response on validation errors but when I run my tests, bean validation is not done. It works fine when I build and deploy the app, though. Validation also works for tests if I put the validation annotations on the class representing the incoming request and add #Valid to the controller method argument corresponding to the request. It just doesn't work when its done on the service layer.
So my question is is this even supposed to work with MockMvc, that is to say, should validation occur normally when the controller calls the service method? Or is there some configuration I need to have in place for this to work during tests? I doubt it since validation works if I try adding it to controller level...
Here is my test class setup:
#ContextConfiguration(classes = {MyTestConfig.class})
#WebAppConfiguration
#RunWith(SpringJUnit4ClassRunner.class)
public class MyTest {
private MockMvc mvc;
#Autowired
private WebApplicationContext wac;
#Before
public void setup() throws Exception {
this.mvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
}
The MyTestConfig class just has #EnableWebMvc annotation, base packages for scanning and some unrelated bean definitions.
My DTO has #NotNull annotation on a property, the service method has a #Validated annotation on the class and a #Valid annotation on the method parameter which is the DTO.
With these settings, when I make a request in a test to the controller endpoint using the MockMvc instance mvc above, validation does not occur on the service method.
So is this even supposed to work?
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.