diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/test/mockmvc.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/test/mockmvc.adoc index 2ef6e234ce..f28c866ae0 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/test/mockmvc.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/test/mockmvc.adoc @@ -1889,3 +1889,65 @@ mvc } ---- ==== + +=== SecurityMockMvcResultHandlers + +Spring Security provides a few ``ResultHandler``s implementations. +In order to use Spring Security's ``ResultHandler``s implementations ensure the following static import is used: + +[source,java] +---- +import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultHandlers.*; +---- + +==== Exporting the SecurityContext + +Often times we want to query a repository to see if some `MockMvc` request actually persisted in the database. +In some cases our repository query uses the <> to filter the results based on current user's username or any other property. +Let's see an example: + +A repository interface: +[source,java] +---- +private interface MessageRepository extends JpaRepository { + @Query("SELECT m.content FROM Message m WHERE m.sentBy = ?#{ principal?.name }") + List findAllUserMessages(); +} +---- + +Our test scenario: + +[source,java] +---- +mvc + .perform(post("/message") + .content("New Message") + .contentType(MediaType.TEXT_PLAIN) + ) + .andExpect(status().isOk()); + +List userMessages = messageRepository.findAllUserMessages(); +assertThat(userMessages).hasSize(1); +---- + +This test won't pass because after our request finishes, the `SecurityContextHolder` will be cleared out by the filter chain. +We can then export the `TestSecurityContextHolder` to our `SecurityContextHolder` and use it as we want: + +[source,java] +---- +mvc + .perform(post("/message") + .content("New Message") + .contentType(MediaType.TEXT_PLAIN) + ) + .andDo(exportTestSecurityContext()) + .andExpect(status().isOk()); + +List userMessages = messageRepository.findAllUserMessages(); +assertThat(userMessages).hasSize(1); +---- + +[NOTE] +==== +Remember to clear the `SecurityContextHolder` between your tests, or it may leak amongst them +==== diff --git a/etc/checkstyle/checkstyle.xml b/etc/checkstyle/checkstyle.xml index 48399d6ea3..e8ea50e0d6 100644 --- a/etc/checkstyle/checkstyle.xml +++ b/etc/checkstyle/checkstyle.xml @@ -14,6 +14,7 @@ + diff --git a/test/src/main/java/org/springframework/security/test/web/servlet/response/SecurityMockMvcResultHandlers.java b/test/src/main/java/org/springframework/security/test/web/servlet/response/SecurityMockMvcResultHandlers.java new file mode 100644 index 0000000000..d1231bcfbe --- /dev/null +++ b/test/src/main/java/org/springframework/security/test/web/servlet/response/SecurityMockMvcResultHandlers.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.test.web.servlet.response; + +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.test.context.TestSecurityContextHolder; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.ResultHandler; + +/** + * Security related {@link MockMvc} {@link ResultHandler}s + * + * @author Marcus da Coregio + * @since 5.6 + */ +public final class SecurityMockMvcResultHandlers { + + private SecurityMockMvcResultHandlers() { + } + + /** + * Exports the {@link SecurityContext} from {@link TestSecurityContextHolder} to + * {@link SecurityContextHolder} + */ + public static ResultHandler exportTestSecurityContext() { + return new ExportTestSecurityContextHandler(); + } + + /** + * A {@link ResultHandler} that copies the {@link SecurityContext} from + * {@link TestSecurityContextHolder} to {@link SecurityContextHolder} + * + * @author Marcus da Coregio + * @since 5.6 + */ + private static class ExportTestSecurityContextHandler implements ResultHandler { + + @Override + public void handle(MvcResult result) { + SecurityContextHolder.setContext(TestSecurityContextHolder.getContext()); + } + + } + +} diff --git a/test/src/test/java/org/springframework/security/test/web/servlet/response/SecurityMockMvcResultHandlersTest.java b/test/src/test/java/org/springframework/security/test/web/servlet/response/SecurityMockMvcResultHandlersTest.java new file mode 100644 index 0000000000..3f089b6032 --- /dev/null +++ b/test/src/test/java/org/springframework/security/test/web/servlet/response/SecurityMockMvcResultHandlersTest.java @@ -0,0 +1,106 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.test.web.servlet.response; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultHandlers.exportTestSecurityContext; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; + +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = SecurityMockMvcResultHandlersTest.Config.class) +@WebAppConfiguration +public class SecurityMockMvcResultHandlersTest { + + @Autowired + private WebApplicationContext context; + + private MockMvc mockMvc; + + @BeforeEach + public void setup() { + // @formatter:off + this.mockMvc = MockMvcBuilders + .webAppContextSetup(this.context) + .apply(springSecurity()) + .build(); + // @formatter:on + } + + @AfterEach + public void tearDown() { + SecurityContextHolder.clearContext(); + } + + @Test + @WithMockUser + public void withTestSecurityContextCopiedToSecurityContextHolder() throws Exception { + this.mockMvc.perform(get("/")).andDo(exportTestSecurityContext()); + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + assertThat(authentication.getName()).isEqualTo("user"); + assertThat(authentication.getAuthorities()).hasSize(1).first().hasToString("ROLE_USER"); + } + + @Test + @WithMockUser + public void withTestSecurityContextNotCopiedToSecurityContextHolder() throws Exception { + this.mockMvc.perform(get("/")); + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + assertThat(authentication).isNull(); + } + + @EnableWebSecurity + @EnableWebMvc + static class Config { + + @RestController + static class Controller { + + @RequestMapping("/") + String ok() { + return "ok"; + } + + } + + } + +}