How to prevent NestedServletException when testing Spring endpoints?

Viewed 19327

I am trying to test the security configuration of some of my endpoints which are secured with @PreAuthorize(#oauth2.hasScope('scope'). When accessing such an endpoint via Postman with a access token that does not have the required scope, the following is returned with HTTP status code 403 (forbidden):

{
    "error": "insufficient_scope",
    "error_description": "Insufficient scope for this resource",
    "scope": "scope"
}

Which is the expected behaviour that I want.

When trying to test this configuration, Springs NestedServletException interferes with my test case before it can complete with my expected result.

This is a simplified version of the controller I want to test:

@RestController
@RequestMapping(value = "/api")
public class OauthTestingResource {

    @PreAuthorize(#oauth2.hasScope('scope'))
    @RequestMapping(value = "/scope", method = RequestMethod.GET)
    public void endpoint() {
        // ...
    }
}

And this is the corresponding test case:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = MyApplication.class)
@WebAppConfiguration
public class AuthorizationTest {

    @Autowired
    protected WebApplicationContext webApplicationContext;

    protected SecurityContext securityContext = Mockito.mock(SecurityContext.class);

    @Before
    public void setup() throws Exception {

        this.mvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
        SecurityContextHolder.setContext(securityContext);
    }

    protected Authentication createMockAuth(Client client) {

        final List<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(new SimpleGrantedAuthority("ROLE_USER"));

        final Authentication pwAuth = new UsernamePasswordAuthenticationToken("testuser", "testpw", authorities);

        final TokenRequest request = new TokenRequest(new HashMap<>(), client.getClientId(), client.getScopes(), "password");

        final OAuthClient oauthClient = new OAuthClient(client, GrantType.PASSWORD);

        return new OAuth2Authentication(request.createOAuth2Request(oauthClient), pwAuth);
    }
    @Test
    public void testAppScope() throws Exception {

        final Client client = new Client("id1", "secret1");

        client.setScope("scope");
        Mockito.when(securityContext.getAuthentication()).thenReturn(createMockAuth(client));
        // this test passes
        mvc.perform(get("/api/scope")).andExpect(status().isOk()); 

        client.setScope("other_scope");
        Mockito.when(securityContext.getAuthentication()).thenReturn(createMockAuth(client));
        // NestedServletException thrown here
        mvc.perform(get("/api/scope")).andExpect(status().isForbidden()); 
    }
}

The exception that is thrown is the following (which is expected):

org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.springframework.security.access.AccessDeniedException: Insufficient scope for this resource

My question is how can I prevent this exception from interfering with my test case?

4 Answers

I had a similar case and suppressed the NestedServletException by using @Test(expected = NestedServletException.class) and then I was able to get hold of the MvcResult and do further asserts on it as in other tests like:

// then
MvcResult result = resultActions.andExpect(status().isServiceUnavailable()).andReturn();
String message = result.getResponse().getContentAsString();
assertThat(message).contains("ABC");
assertThat(result.getResolvedException().getClass()).isEqualTo(XYZ.class);

It seemed to work.

I fixed it by adding an @ExceptionHandler for that exception. Seems like if MockMvc throws an actual exception it means you don't "handle" this case which is not ideal.

If you are using a centralised class to do your error handling via AOP (using @ControllerAdvice annotation), then include this ControllerAdvice in your test setup.

 @BeforeEach
public void setup() {
    this.mockMvc = standaloneSetup(myController).setControllerAdvice(MyErrorHandlingAdvice.class).build();
}
Related