Home > front end >  WebMvcTest not working after removing deprecated WebSecurityConfigurerAdapter
WebMvcTest not working after removing deprecated WebSecurityConfigurerAdapter

Time:01-08

Upgrading the SecurityConfig of my application, I got working version but destroyed so many tests.

Current, deprecated version is here and differecence between files is here

The new version works very well (I tried it with Postman).

With difference version I got every test passed at 100%, now I have several errors that I cannot solve myself.

E.g. this test:

@WebMvcTest(PingController.class)
class AuthTokenFilterTest {

    @Autowired
    private MockMvc mvc;

    @Autowired
    AuthTokenFilter authTokenFilter;

    @MockBean
    JwtUtils jwtUtils;

    @MockBean
    UserDetailsServiceImpl userDetailsServiceImpl;

    @MockBean
    UserDetails userDetails;

    @MockBean
    private PingService pingService;

    @MockBean
    AuthEntryPointJwt authEntryPointJwt;

    @MockBean
    UsersRepository usersRepository;

    @Test
    void testCanReturnNullIfJwtIsMissing() throws Exception {
        mvc.perform(get("/api/v1/ping")).andExpect(status().isOk());
    }

    /**
     * Test can validate the token.
     * 
     * Use /ping route because it is the route out of security, so we can
     * concentrate to the AuthTokenFilter class.
     * 
     * @throws Exception
     */
    @Test
    void testCanValidateToken() throws Exception {
        String token = "a1.b2.c3";

        when(jwtUtils.validateJwtToken(token)).thenReturn(true);
        when(jwtUtils.getUserNameFromJwtToken(token)).thenReturn("username");
        when(userDetailsServiceImpl.loadUserByUsername("username")).thenReturn(userDetails);
        when(userDetails.getAuthorities()).thenReturn(null);

        mvc.perform(get("/api/v1/ping").header("Authorization", "Bearer "   token)).andExpect(status().isOk());
    }

    /**
     * Test cannot validate the token.
     * 
     * Use /ping route because it is the route out of security, so we can
     * concentrate to the AuthTokenFilter class.
     * 
     * @throws Exception
     */
    @Test
    void testCannotValidateToken() throws Exception {
        String token = "a1.b2.c3";
        when(jwtUtils.validateJwtToken(token)).thenReturn(false);
        mvc.perform(get("/api/v1/ping").header("Authorization", "Bearer "   token)).andExpect(status().isOk());
    }

    /**
     * Test cannot validate the token if the header is missing.
     * 
     * Use /ping route because it is the route out of security, so we can
     * concentrate to the AuthTokenFilter class.
     * 
     * @throws Exception
     */
    @Test
    void testCanReturnNullIfJwtIsMissingButOtherHeaderIsInPlace() throws Exception {
        String token = "a1.b2.c3";
        mvc.perform(get("/api/v1/ping").header("Authorization", "NotStartWithBearer "   token))
                .andExpect(status().isOk());
    }

}

I get

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'com.bitbank.config.AuthTokenFilterTest': Unsatisfied dependency expressed through field 'authTokenFilter'; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.bitbank.config.AuthTokenFilter' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}

(The test is here)

Or, in the RegisterControllerTest

@WebMvcTest(AuthController.class)
@TestPropertySource(locations = "classpath:application.properties", properties = "app.enableSubscription=false")
class RegisterControllerTest {

    @Autowired
    private MockMvc mvc;

    @Autowired
    AuthTokenFilter authTokenFilter;

    @MockBean
    AuthEntryPointJwt authEntryPointJwt;

    @MockBean
    JwtUtils jwtUtils;

    @Autowired
    private ObjectMapper objectMapper;

    @MockBean
    UserDetailsServiceImpl userDetailsServiceImpl;

    @MockBean
    AuthenticationManager authenticationManager;

    @MockBean
    Authentication authentication;

    @MockBean
    SecurityContext securityContext;

    @MockBean
    private RolesService rolesService;

    @Test
    @WithMockUser(username = "username", authorities = { "ROLE_ADMIN" })
    void cannotRegisterUserWhenSubscriptionsAreDisabled() throws Exception {

        var userToSave = validUserEntity("username", "password");
        var savedUser = validUserEntity("username", "a1.b2.c3");

        when(userDetailsServiceImpl.post(userToSave)).thenReturn(savedUser);

        mvc.perform(post("/api/v1/auth/register/").contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsBytes(userToSave))).andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.status", is("error")))
                .andExpect(jsonPath("$.message", is("subscriptions disabled")));
    }

    private static UsersEntity validUserEntity(String username, String password) {
        return UsersEntity.builder().username(username).password(password).build();
    }
}

I get Error creating bean with name 'authController': Unsatisfied dependency expressed through field 'passwordEncoder'

SecurityConfig

Old:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    UserDetailsServiceImpl userDetailsServiceImpl;

    @Autowired
    private AuthEntryPointJwt authEntryPointJwt;

    @Bean
    AuthTokenFilter authenticationJwtTokenFilter() {
        return new AuthTokenFilter();
    }

    @Value("${app.allowedOrigins}")
    private String allowedOriginsFromApplicationProperties;

    /**
     * Return allowedOrigins from application properties
     */
    private String getAllowedOriginsFromApplicationProperties() {
        return this.allowedOriginsFromApplicationProperties;
    }

    /**
     * Return the allowed origins.
     * 
     * @return
     */
    private List<String> getAllowedOrigins() {
        String[] allowedOrigins = this.getAllowedOriginsFromApplicationProperties().split(",");
        return Arrays.asList(allowedOrigins);
    }

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

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

    @Override
    public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
        authenticationManagerBuilder.userDetailsService(userDetailsServiceImpl).passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable().exceptionHandling().authenticationEntryPoint(authEntryPointJwt).and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()
                .mvcMatchers("/api/v1/ping").permitAll().mvcMatchers("/api/v1/auth/register").permitAll()
                .mvcMatchers("/api/v1/auth/login").permitAll().mvcMatchers("/api-docs/**").permitAll()
                .mvcMatchers("/swagger-ui/**").permitAll().mvcMatchers("/swagger-ui.html").permitAll()
                .mvcMatchers("/documentation").permitAll().mvcMatchers("/").permitAll().anyRequest().authenticated();
        http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);

    }

    @Bean
    CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.setAllowedOriginPatterns(getAllowedOrigins());
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }

}

and new

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfiguration {

    @Autowired
    UserDetailsServiceImpl userDetailsServiceImpl;

    @Autowired
    AuthEntryPointJwt authEntryPointJwt;

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

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration)
            throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    AuthTokenFilter authenticationJwtTokenFilter() {
        return new AuthTokenFilter();
    }

    @Value("${app.allowedOrigins}")
    private String allowedOriginsFromApplicationProperties;

    /**
     * Return allowedOrigins from application properties
     */
    private String getAllowedOriginsFromApplicationProperties() {
        return this.allowedOriginsFromApplicationProperties;
    }

    /**
     * Return the allowed origins.
     * 
     * @return
     */
    private List<String> getAllowedOrigins() {
        String[] allowedOrigins = this.getAllowedOriginsFromApplicationProperties().split(",");
        return Arrays.asList(allowedOrigins);
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable().exceptionHandling().authenticationEntryPoint(authEntryPointJwt).and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()
                .mvcMatchers("/api/v1/ping").permitAll().mvcMatchers("/api/v1/auth/register").permitAll()
                .mvcMatchers("/api/v1/auth/login").permitAll().mvcMatchers("/api-docs/**").permitAll()
                .mvcMatchers("/swagger-ui/**").permitAll().mvcMatchers("/swagger-ui.html").permitAll()
                .mvcMatchers("/documentation").permitAll().mvcMatchers("/").permitAll().anyRequest().authenticated();
        http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/**").allowedMethods("*");
            }
        };
    }

    @Bean
    CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.setAllowedOriginPatterns(getAllowedOrigins());
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }

}

CodePudding user response:

I cite the documentation for @WebMvcTest:

Annotation that can be used for a Spring MVC test that focuses only on Spring MVC components.

Using this annotation will disable full auto-configuration and instead apply only configuration relevant to MVC tests (i.e. @Controller, @ControllerAdvice, @JsonComponent, Converter/GenericConverter, Filter, WebMvcConfigurer and HandlerMethodArgumentResolver beans but not @Component, @Service or @Repository beans).

This means that your SecurityConfiguration is never created because Spring only focuses on the web layer and not your custom components.

You have two options for fixing this:

1. Importing necessary beans

@WebMvcTest(PingController.class)
@Import(SecurityConfiguration.class)
class AuthTokenFilterTest {

Note, that you also need to import the configurations / components that your SecurityConfiguration relies on and all the classes these beans rely on, and so forth. So it would more likely have to look like this:

@WebMvcTest(PingController.class)
@Import({SecurityConfiguration.class, UserDetailsServiceImpl.class, AuthEntryPointJwt.class})
class AuthTokenFilterTest {

This will however decrease the maintainability of your test. If you added a new dependency to say UserDetailsServiceImpl, your test would break again because the bean can't be found.

2. Using @SpringBootTest

@SpringBootTest will create your whole application context with slight modifications for tests. The replacements would look like this:

@AutoConfigureMockMvc // <- included in WebMvcTest but not in SpringBootTest
@SpringBootTest
class AuthTokenFilterTest {

The disadvantage of this approach is that the tests are slower because the whole application context is created and not only the web layer. I prefer this approach as I think that maintainability is more important than the speed of the execution of the tests.

  • Related