Reputation: 113
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 = "[email protected]";
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 = "[email protected]";
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 = "[email protected]";
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 = "[email protected]";
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();
}
}
Upvotes: 4
Views: 1958
Reputation: 761
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.
Upvotes: 1
Reputation: 201
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 {
}
Upvotes: 1
Reputation: 111
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?
Upvotes: 1