Issue
I am trying to write a Unit tests for all of my service classes, but I cannot find a solution on how to mock a @PreAuthorize above my controller methods. As an example:
I have this function in controller:
@GetMapping("/users")
@PreAuthorize("hasAuthority('ADMIN')")
public ResponseEntity<List<User>> getUsers() {
return service.getUsers();
}
And this in my service class:
public ResponseEntity<List<User>> getUsers() {
return new ResponseEntity<>(userRepository.findAll(), HttpStatus.OK);
}
WebSecurity class:
protected void configure(HttpSecurity http) throws Exception {
http = http.cors().and().csrf().disable();
http = http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and();
http = http
.exceptionHandling()
.authenticationEntryPoint(
(request, response, ex) -> response.sendError(
HttpServletResponse.SC_UNAUTHORIZED,
ex.getMessage()
)
)
.and();
http.authorizeRequests()
.antMatchers(HttpMethod.GET, "/").permitAll()
.anyRequest().authenticated();
http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(jwtAuthenticationConverter());
}
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(jwt ->
Optional.ofNullable(jwt.getClaimAsStringList("permissions"))
.stream()
.flatMap(Collection::stream)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList())
);
return converter;
}
Now I am trying to write a unit test:
@SpringBootTest
@AutoConfigureMockMvc
public class UserControllerTests {
@Autowired
private MockMvc mvc;
@MockBean
private UserService userService;
@Test
@WithMockJwtAuth(claims = @OpenIdClaims(otherClaims
= @Claims(stringClaims = @StringClaim(name = "permissions", value = "{ADMIN}"))))
public void getAllUsers_shouldBeSuccess() throws Exception {
ArrayList<User> users = new ArrayList<>();
users.add(new User("0", true, new Role("USER")));
when(userService.getUsers()).thenReturn(new ResponseEntity<>(users, HttpStatus.OK));
mvc.perform(get("/users"))
.andExpect(status().isOk());
}
}
But I receive an error on mvc.perform
call:
java.lang.NoClassDefFoundError: `org/springframework/security/web/context/SecurityContextHolderFilter`
UPDATE:
I've tried https://github.com/ch4mpy/spring-addons and added @WithMockBearerTokenAuthentication
, but I still receive the same error. Also to note: if I removed all @
and left only with @Test
above the method, I receive 401
error.
@Test
@WithMockBearerTokenAuthentication(attributes = @OpenIdClaims(otherClaims
= @Claims(stringClaims = @StringClaim(name = "permissions", value = "{ADMIN}"))))
public void getAllUsers_shouldBeSuccess() throws Exception {
ArrayList<User> users = new ArrayList<>();
users.add(new User("0", true, new Role("USER")));
when(userService.getUsers()).thenReturn(new ResponseEntity<>(users, HttpStatus.OK));
mvc.perform(get("/users"))
.andExpect(status().isOk());
}
Solution
Sadly, spring-security team chose to include in test framework MockMvc request post-processors and WebTestClient request mutators only, which limits OAuth2 authentication mocking to controllers unit-tests.
Hopefully, I kept my work on test annotations in a set of libs I publish on maven-central: https://github.com/ch4mpy/spring-addons. You can test any @Component
with it (sample adapted from here):
//---------------------------------------------------//
// Test secured @Component which isn't a @Controller //
//---------------------------------------------------//
// Import web-security configuration and tested component class
@Import({ SampleApi.WebSecurityConfig.class, MessageService.class })
@ExtendWith(SpringExtension.class)
class MessageServiceTests {
// Auto-wire tested component
@Autowired
private MessageService messageService;
// Mock tested component dependencies
@MockBean
GreetingRepo greetingRepo;
@BeforeEach
public void setUp() {
when(greetingRepo.findUserSecret("ch4mpy")).thenReturn("Don't tel it");
}
@Test()
void greetWitoutAuthentication() {
// Call tested component methods directly (don't use MockMvc nor WebTestClient)
assertThrows(Exception.class, () -> messageService.getSecret("ch4mpy"));
}
@Test
@WithMockJwtAuth(authorities = "ROLE_AUTHORIZED_PERSONNEL", claims = @OpenIdClaims(preferredUsername = "ch4mpy"))
void greetWithMockJwtAuth() {
assertThat(messageService.getSecret("ch4mpy")).isEqualTo("Don't tel it");
}
}
Edit for updated question
Only @Controller
unit-tests should run within the context of an HTTP request.
This means that @WebMvcTest
(or @WebFluxTest
) and MockMvc
(or WebTestClient
) must be used in @Controller
tests only.
Unit-tests for any other type of @Component
(@Service
or @Repository
for instance) should be written without the context of a request. This means that none of @WebMvcTest
, @WebFluxTest
, MockMvc
and WebTestClient
should be used in such tests.
The sample above shows how to structure such tests:
- Trigger spring-context configuration
@Import
web-security configuration and your@Component
class- provide
@MockBean
for all of your@Component
dependencies - in the test, call tested component methods directly (do not try to create a mockMvc request)
Edit for the 2nd question modification
I am trying to write a Unit tests for all of my service classes
Apparently, this first statement is not your main concern anymore as you're trying to write integration-tests for @Controller
along with the @Services
, @Repositories
and other @Components
it is injected. This is actually a completely different question than unit-testing each of those separately (mocking others).
NoClassDefFoundError
on SecurityContextHolderFilter
means that spring-security-web
is not on your classpath. It should be, even during the tests. Check your dependencies (pom or gradle file that you did not include in your question)
Please also note that:
- you might want to write
value = "ADMIN"
instead ofvalue = "{ADMIN}"
(unless you really want curly-braces in your authority name) - you can use just
@WithMockJwtAuth("ADMIN")
instead of@WithMockJwtAuth(claims = @OpenIdClaims(otherClaims = @Claims(stringClaims = @StringClaim(name = "permissions", value = "ADMIN"))))
- you are allowed to read docs (you and I would save quite some time). This includes Spring doc and mine: home one and more importantly tutorials
Answered By - ch4mp
Answer Checked By - Senaida (JavaFixing Volunteer)