Как предотвратить исключение NestedServletException при тестировании конечных точек Spring?

Я пытаюсь проверить конфигурацию безопасности некоторых из моих конечных точек, которые защищены с помощью @PreAuthorize(#oauth2.hasScope('scope'). При доступе к такой конечной точке через @PreAuthorize(#oauth2.hasScope('scope') с токеном доступа, который не имеет требуемой области, следующее возвращается с кодом статуса HTTP 403 (запрещено):

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

Какое ожидаемое поведение я хочу.

При попытке проверить эту конфигурацию, Springs NestedServletException мешает моему тестовому примеру, прежде чем он сможет завершить мой ожидаемый результат.

Это упрощенная версия контроллера, который я хочу проверить:

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

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

И это соответствующий тестовый пример:

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

Исключением является следующее (что ожидается):

org.springframework.web.util.NestedServletException: обработка запроса не выполнена; Вложенное исключение - org.springframework.security.access.AccessDeniedException: Недостаточная область для этого ресурса

Мой вопрос: как я могу помешать этому исключению вмешаться в мой тестовый пример?

Ответ 1

Я прошел весенние тесты безопасности, перейдя по этой ссылке. Все работало нормально, за исключением этой проблемы с вложением оригинального исключения в NestedServletException. Я не нашел прямого способа понять это, но AspectJ помог мне справиться с этим более чистым способом.

Мы можем использовать статический метод assertThatThrownBy() класса Assertions. Этот метод возвращает объект AbstractThrowableAssert, который мы можем использовать для записи утверждений для брошенного исключения.

Код, который захватывает исключение, вызванное методом methodThatThrowsException(), выглядит следующим образом:

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

Благодаря этому отличному блогу вы можете найти дополнительную информацию.

То, как я справился с этим в моем тестовом примере, было бы (взяв кодовую строку вашего теста):

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

Таким образом, ваш тестовый пример сможет утверждать фактическое исключение AccessDeniedException, вложенное в исключение NestedServletException.

Ответ 2

Я исправил это, добавив @ExceptionHandler для этого исключения. Похоже, если MockMvc выдает фактическое исключение, это означает, что вы не "обрабатываете" этот случай, который не идеален.