Search code examples
javaspring-bootunit-testingspring-security

mockMvc cannot mock service for unit test


I am writing a unit test for spring security and JWT validation. There are 3 basic cases I want to start the test with:

  1. When no token -> expect 401
  2. When token but wrong scope -> expect 403
  3. When token and scope -> expect 200

I tested my code using postman and they return expected responses. In my unit test I have this:

@SpringBootTest
@ContextConfiguration(classes = SecurityConfig.class)
@AutoConfigureMockMvc
@EnableAutoConfiguration
@Import(DataImporterControllerConfig.class)
public class SecurityConfigTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void doImportExpect200() throws Exception {
        mockMvc.perform(put(URI).with(jwt().authorities(new SimpleGrantedAuthority(
                "SCOPE_data:write"))).contentType(APPLICATION_JSON_VALUE).accept(APPLICATION_JSON)
                        .content(BODY)).andExpect(status().isOk());
    }

It does pass validation part and try to return some value from the controller:

@RestController
@RequestMapping(value = "${data.uriPrefix}")
@Loggable(value = INFO, trim = false)
public class DataImporterController {

    private final DataImporterService dataImporterService;

    @PutMapping(path = "/someurl", produces = APPLICATION_JSON_VALUE, consumes = APPLICATION_JSON_VALUE)
    public dataImportResponse doImport(@RequestHeader(name = ACCEPT, required = true) final String acceptHeader,
        final @RequestBody @NotBlank String body) {
        return new DataImportResponse(dataImporterService.doImport(body));
    }

The logic inside dataImporterService.doImport(body) require some db operation, so ideally, I want to mock it and make it return some value (something like when(dataImporterServiceMock.doImport(body)).thenReturn(something).

However, when I try it, it doesn't work. I think it is because I am not creating a controller with mocked service. I tried to create one, but due to configuration for SecurityConfig class, it is not that easy. Here is SecurityConfig class:

@EnableWebSecurity
public class SecurityConfig {

    @Value("${auth0.audience}")
    private String audience;

    @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
    private String issuer;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        /*
        This is where we configure the security required for our endpoints and setup our app to serve as
        an OAuth2 Resource Server, using JWT validation.
        */
        http
            .csrf().disable()
            .authorizeRequests()
            .antMatchers(HttpMethod.GET, "/actuator/**").permitAll()
            .antMatchers(HttpMethod.PUT, "/dataimporter/**").hasAuthority("SCOPE_data:write")
            .anyRequest().authenticated()
            .and().cors()
            .and().oauth2ResourceServer().jwt();
        return http.build();
    }

    @Bean
    JwtDecoder jwtDecoder() {
        /*
        By default, Spring Security does not validate the "aud" claim of the token, to ensure that this token is
        indeed intended for our app. Adding our own validator is easy to do:
        */
        NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder)
                JwtDecoders.fromOidcIssuerLocation(issuer);

        OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator(audience);
        OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuer);
        OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);
        jwtDecoder.setJwtValidator(withAudience);
        return jwtDecoder;
    }
}

I got this code from Auth0 and just modified antMatcher portion. How can I test it so it returns 200 (mock service or something)?


Solution

  • As already stated in the answer to your other question "How to write unit test for SecurityConfig for spring security", for unit-testing a @Controller, use @WebMvcTest (with mocked dependencies), not @SpringBootTest which is intended for integration testing, loads much more config, and instantiate & autowire actual components (and, as a consequence, is slower and less focused)

    @WebMvcTest(controllers = DataImporterController.class)
    public class DataImporterControllerUnitTest {
        @MockBean
        DataImporterService dataImporterService;
    
        @Autowired
        private MockMvc mockMvc;
    
        @BeforeEach
        public void setUp() {
            when(dataImporterService.doImport(body)).thenReturn(something);
        }
    
        @Test
        void doImportExpect200() throws Exception {
            mockMvc.perform(put(URI).with(jwt().authorities(new SimpleGrantedAuthority(
                    "SCOPE_data:write"))).contentType(APPLICATION_JSON_VALUE).accept(APPLICATION_JSON)
                            .content(BODY)).andExpect(status().isOk());
        }
    }
    

    If you had followed the link I already provided, you'd have find that in the main README, in the many samples, and in the tutorials

    Notes

    I am writing a unit test for spring security and JWT validation.

    This are completely separate concerns and is to be done separately:

    • spring security access-control is unit-tested with secured components: @Controller of course, but also @Service, @Repository, etc. where method-security is used (refer to your previous question for instructions and to my repo for samples)
    • Authorization header is not considered when building @WebMvcTest or @WebfluxTest security-context. Authentication instances are built based on MockMvc request post-processors, WebTestClient mutators or annotations and this does not involve any token decoding or validation. JWT validation has to be tested in a JwtDecoder unit-test (only if you keep your own, which you shouldn't, see below)

    Spring Security does not validate the "aud" claim of the token

    This is wrong with spring-boot since 2.7: spring.security.oauth2.resourceserver.jwt.audiences property does just that. There is no need for you to override the JwtDecoder provided by spring-boot (actually, you shouldn't), and, as a consequence to unit-test your own.

    1. When no token -> expect 401
    2. When token but wrong scope -> expect 403
    3. When token and scope -> expect 200

    To be exact, the phrasing would better be:

    • "anonymous" rather than "no token"
    • "authenticated" rather than just "token"
    • "authorities" rather than "scope"

    The reason for that are:

    • the OAuth2 token is validated and then turned into an Authentication instance: by default AnonymousAuthenticationToken if token is missing or validation fails, and whatever the configured authentication-converter returns if validation is successful
    • spring-security Role-Based Access-Control is completely generic (nothing specific to OAuth2) and relies on GrantedAuthority, not scope. This common confusion is due to the fact that there is nothing related to RBAC in OAuth2 nor OpenID standards and the default authentication-converter had to choose a claim that is always there to map authorities from. scope claim was picked as default, adding the SCOPE_ prefix. You should refer to how RBAC is implemented by your own authorization server and provide an authentication-converter bean to map authorities from the right claim(s).

    Auth0 uses roles and permissions claims when RBAC is enabled, Keycloak uses realm_access.roles and resource_access.{client-id}.roles, etc., reason for me implementing a configurable authorities-converter, which picks the claims that should be used as authorities source from application properties (and how to map it: prefix and case transformation).

    Last, your configuration is still risky (enabled sessions with disabled CSRF protection and poor CORS config). You should really consider using "my" starters, or follow the tutorial I wrote for configuring a resource-server with just spring-boot-oauth2-resource-server.