Cannot mock @PreAuthorize when using with OAuth2 in Spring Boot

Viewed 73

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());
}
1 Answers

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, MockMvcand 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 of value = "{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
Related