How to prevent NestedServletException when testing Spring endpoints?

17,234

Solution 1

I did spring security test cases by following this link. Things worked fine except this issue of nesting original exception in NestedServletException. I did not find any direct way to figure this out but AspectJ helped me in handling this in a cleaner way.

We can use the static assertThatThrownBy() method of the Assertions class. This method returns an AbstractThrowableAssert object that we can use to write assertions for the thrown exception.

The code that captures an exception thrown by the methodThatThrowsException() method looks as follows:

assertThatThrownBy(() -> methodThatThrowsException())
.isExactlyInstanceOf(DuplicateEmailException.class);

Thanks to this excellent blog where you can find additional details.

The way in which I handled this in my test case would be (by taking your test case codeline):

org.assertj.core.api.Assertions.assertThatThrownBy(() -> mvc.perform(get("/api/scope")).andExpect(status().isOk())).hasCause(new AccessDeniedException("Access is denied"));

That way your test case would be able to assert actual AccessDeniedException that is nested in NestedServletException.

Solution 2

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.

Solution 3

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.

Solution 4

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();
}
Share:
17,234

Related videos on Youtube

Philipp Jahoda
Author by

Philipp Jahoda

Creator of the MPAndroidChart: https://github.com/PhilJay/MPAndroidChart iOS version: https://github.com/danielgindi/Charts I know some things about Android UI and Canvas and spent 10+ years developing with Java. More recently I am specializing in iOS development with Swift and backend development with Kotlin (Spring). Usually I am working with MongoDB as a database solution and Angular for frontend. For more information about me and current projects, please visit my website, LinkedIn profile or have a look at my GitHub account.

Updated on June 07, 2022

Comments

  • Philipp Jahoda
    Philipp Jahoda almost 2 years

    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?

    • Michal Borek
      Michal Borek almost 6 years
      In my case the problem was with two instances of @Configuration that extend WebSecurityConfigurerAdapter. Having this, it's init method is invoked and there might be a case when you have two SecurityFilterChains and ExceptionTranslationFilter is not being called by mockMvc. This filter is responsible for translation between AccessDeniedException and desired response code.