4

In the following test class, I don't want the @EnableWebSecurity annotated class to be caught by the Spring Context:

@WebMvcTest(controllers = UserController.class)
class UserControllerTest {

    @MockBean
    private UserService userService;

    @Autowired
    private ObjectMapper jsonMapper;

    @Autowired
    private MockMvc mockMvc;

    @Test
    void create_should_return_registered_user_when_request_is_valid() throws Exception {
        // given
        final String EMAIL = "test@test.com";
        final String PASSWORD = "test_password";
        final UserDto userDto = buildDto(EMAIL, PASSWORD);
        final User expectedUser = buildUser(EMAIL, PASSWORD);

        // when
        when(userService.registerUser(userDto)).thenReturn(expectedUser);

        // then
        MvcResult response = mockMvc.perform(post(UserAPI.BASE_URL)
                .contentType(MediaType.APPLICATION_JSON)
                .content(jsonMapper.writeValueAsString(userDto)))
                .andExpect(status().isCreated())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andReturn();

        String responseBodyJson = response.getResponse().getContentAsString();
        User responseUser = jsonMapper.readValue(responseBodyJson, User.class);

        assertThat(responseUser, is(equalTo(expectedUser)));
        verify(userService, times(1)).registerUser(userDto);
        verifyNoMoreInteractions(userService);
    }

    @Test
    void create_should_return_conflict_when_request_valid_but_email_in_use() throws Exception {
        // given
        final String EMAIL = "test@test.com";
        final String PASSWORD = "test_password";
        final UserDto userDto = buildDto(EMAIL, PASSWORD);

        // when
        when(userService.registerUser(userDto)).thenThrow(new EmailAlreadyInUseException(EMAIL));

        // then
        mockMvc.perform(post(UserAPI.BASE_URL)
                .contentType(MediaType.APPLICATION_JSON)
                .content(jsonMapper.writeValueAsString(userDto)))
                .andExpect(status().isConflict());

        verify(userService, times(1)).registerUser(userDto);
        verifyNoMoreInteractions(userService);
    }

    @Test
    void create_should_return_bad_request_when_request_has_invalid_email() throws Exception {
        // given
        final String BAD_EMAIL = "test_test.com";
        final String PASSWORD = "test_password";
        final UserDto userDto = buildDto(BAD_EMAIL, PASSWORD);

        // when

        // then
        mockMvc.perform(post(UserAPI.BASE_URL)
                .contentType(MediaType.APPLICATION_JSON)
                .content(jsonMapper.writeValueAsString(userDto)))
                .andExpect(status().isBadRequest());

        verifyNoInteractions(userService);
    }

    @Test
    void create_should_return_bad_request_when_request_has_invalid_password() throws Exception {
        // given
        final String EMAIL = "test@test.com";
        final String BAD_PASSWORD = "";
        final UserDto userDto = buildDto(EMAIL, BAD_PASSWORD);

        // when

        // then
        mockMvc.perform(post(UserAPI.BASE_URL)
                .contentType(MediaType.APPLICATION_JSON)
                .content(jsonMapper.writeValueAsString(userDto)))
                .andExpect(status().isBadRequest());

        verifyNoInteractions(userService);
    }

    @Test
    void create_should_return_bad_request_when_request_is_missing_email() throws Exception {
        // given
        final String PASSWORD = "test_password";
        final UserDto userDto = buildDto(null, PASSWORD);

        // when

        // then
        mockMvc.perform(post(UserAPI.BASE_URL)
                .contentType(MediaType.APPLICATION_JSON)
                .content(jsonMapper.writeValueAsString(userDto)))
                .andExpect(status().isBadRequest());

        verifyNoInteractions(userService);
    }

    @Test
    void create_should_return_bad_request_when_request_is_missing_password() throws Exception {
        // given
        final String EMAIL = "test@test.com";
        final UserDto userDto = buildDto(EMAIL, null);

        // when

        // then
        mockMvc.perform(post(UserAPI.BASE_URL)
                .contentType(MediaType.APPLICATION_JSON)
                .content(jsonMapper.writeValueAsString(userDto)))
                .andExpect(status().isBadRequest());

        verifyNoInteractions(userService);
    }

    private UserDto buildDto(String email, String password) {
        UserDto userDto = new UserDto();
        userDto.setEmail(email);
        userDto.setPassword(password);
        return userDto;
    }

    private User buildUser(String email, String password){
        User user = new User();
        user.setId(1);
        user.setEmail(email);
        user.setPassword(password);
        return user;
    }

}

Right now it is being loaded by default and, because its dependencies are not loaded, it throws the error:

Parameter 0 of constructor in com.example.ordersapi.auth.configuration.SecurityConfiguration required a bean of type 'org.springframework.security.core.userdetails.UserDetailsService' that could not be found.

I've seen some solutions such as @WebMvcTest(controllers = SomeController.class, secure = false) but these seem to be deprecated.

I'm running Spring Boot v2.2.2.RELEASE.


Here's the Security Configuration class:

@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Value("${spring.h2.console.enabled:false}")
    private boolean h2ConsoleEnabled;

    private final UserDetailsService userDetailsService;
    private final AuthorizationFilter authorizationFilter;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        if (h2ConsoleEnabled) {
            http.authorizeRequests()
                    .antMatchers("/h2-console", "/h2-console/**").permitAll()
                    .and()
                    .headers().frameOptions().sameOrigin();
        }

        http.cors().and().csrf().disable()
                .exceptionHandling()
                .authenticationEntryPoint(unauthorizedHandler())
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers(HttpMethod.POST, AuthenticationAPI.BASE_URL).permitAll()
                .anyRequest().authenticated();

        http.addFilterBefore(authorizationFilter, UsernamePasswordAuthenticationFilter.class);
    }

    private AuthenticationEntryPoint unauthorizedHandler() {
        return (request, response, e) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
    }

    /**
     * We have to create this bean otherwise we can't wire AuthenticationManager in our code.
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}
João Dias
  • 113
  • 10
  • Could you please share your com.example.ordersapi.auth.configuration.SecurityConfiguration ? Thanks – RUARO Thibault Jan 12 '20 at 15:54
  • Added to the question. Thanks! – João Dias Jan 12 '20 at 16:09
  • Have you created your own `UserDetailsService` implementation ? If so, please provide it. You are close, not missing much. – RUARO Thibault Jan 12 '20 at 20:13
  • Hi, I did try to provide it already, and it's dependencies as well but after that I run into another problem: spring security is up, apparently not using my configuration / services even though they are loaded into the context and when I try to use @WithMockService , I still get 403 Forbidden as the response status – João Dias Jan 12 '20 at 20:15
  • It is supposed to... Weird. Share it please, so that I can try to reproduce... Thanks – RUARO Thibault Jan 12 '20 at 20:17
  • Using exactly the same setup as above for the test class but adding @WithMockUser to the @Test method. For instance: ``` @Test @WithMockUser void create_should_return_registered_user_when_request_is_valid() throws Exception {...} ``` – João Dias Jan 12 '20 at 20:20
  • Oh btw I've now tried to disable my security settings with profiles, which works well, but spring security is still setting up with the defaults. And I was hoping the @WithUserMock would make the tests work but apparently not: Expected :201 Actual :403 – João Dias Jan 12 '20 at 20:22
  • The same things happens if I provide the necessary beans for my configuration to start up, but as I said it only puts it in the context, it doesn't use it whatsoever in that scenario. I've put a breakpoint in both the configure methods and my UserDetailsService implementation and none where hit – João Dias Jan 12 '20 at 20:23
  • 2020-01-12 20:32:49.283 DEBUG 26476 --- [ main] o.s.security.web.csrf.CsrfFilter : Invalid CSRF token found for http://localhost/api/v1/users 2020-01-12 20:32:49.283 DEBUG 26476 --- [ main] o.s.s.w.header.writers.HstsHeaderWriter : Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher@18d30e7 Hmmm... – João Dias Jan 12 '20 at 20:34
  • MvcResult response = mockMvc.perform(post(UserAPI.BASE_URL).with(csrf())... makes it pass. Which sucks because I didn't want security concerns to be polluting these test suites – João Dias Jan 12 '20 at 20:35
  • FYI, the solution is here: https://stackoverflow.com/questions/59776298/custom-filter-breaking-webmvctest-out-of-nowhere/59778286#59778286 – João Dias Jan 16 '20 at 21:56

3 Answers3

1

The easiest solution I’ve found is to, on your SecurityConfiguration class add @Profile(!test). This should completely prevent the class from being loaded during tests. By default tests run with the test profile, if you’re overriding that you might have to put in the profile you’re using. (The logs show which profile is active when the context starts). More about profiles: https://www.baeldung.com/spring-profiles.

You can also use @WithMockUser(roles = "MANAGER"). See this question for more info: Spring Test & Security: How to mock authentication?

Pseudos
  • 111
  • 1
  • 7
  • Hi! Thanks for the answer I've also tried to give another profile to my configuration and all other security beans and it solves the current problem, but even still web security will be set (with the defaults) and when I try to use @WithMockUser I still get 403 Forbbiden – João Dias Jan 12 '20 at 20:17
  • MvcResult response = mockMvc.perform(post(UserAPI.BASE_URL).with(csrf())... makes it pass. Which sucks because I didn't want security concerns to be polluting these test suites – João Dias Jan 12 '20 at 20:36
  • FYI, the solution is here: https://stackoverflow.com/questions/59776298/custom-filter-breaking-webmvctest-out-of-nowhere/59778286#59778286 – João Dias Jan 16 '20 at 21:57
1

I also got the same issue, that is 401 code, after migrating Spring Boot from 2.1.x to 2.2.x. Since then, the secure field was removed from @WebMvcTest annotation.

I fixed by adding this annotation that ignore filters including authentication filter:

@WebMvcTest(value = SomeResource.class)
@AutoConfigureMockMvc(addFilters = false)
class SomeTest  {
}
nordeen78
  • 181
  • 1
  • 3
1

The security configuration can be overridden using the @Configuration and @EnableWebSecurity attributes. Because you're not using @TestConfiguration, you will likely need to import the class using @Import, as shown below. I like this solution over scanning your host package for beans, because I feel like you have better control over what the framework is loading.

@RunWith(SpringRunner.class)
@WebMvcTest(controllers = MyController.class)
@Import(MyController.class)
public class MyControlleTests {

    @Autowired
    private MockMvc mvc;

    @MockBean
    private SomeDependency someDependencyNeeded;

    @Configuration
    @EnableWebSecurity
    static class SecurityConfig extends WebSecurityConfigurerAdapter {

        @Override
        protected void configure(HttpSecurity http) throws Exception
        {
            http
                    .csrf().disable()
                    .authorizeRequests().anyRequest().anonymous();
        }
    }

    @Test
    public void some_route_returns_ok() throws Exception {

        MockHttpServletRequestBuilder requestBuilder =
                MockMvcRequestBuilders.get("mycontrolleraction");

        mvc
                .perform(requestBuilder)
                .andExpect(MockMvcResultMatchers.status().isOk());

    }
}

Note that one could argue that you should just make security part of your test; however, my opinion is that you should test each component of your architecture in as much isolation as possible.

Markus
  • 761
  • 3
  • 6