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.