由于Spring安全性,我正在努力设置Rest控制器测试。
我正在尝试测试一个基本的休息端点:
@RestController
@RequestMapping(value = "/product")
public class ProductInformationController {
private final ProductInformationService productInformationService;
...
@DeleteMapping(value = "/{id}")
public ResponseEntity<Void> deleteProductById(@PathVariable int id) {
productInformationService.deleteProductById(id);
return ResponseEntity.noContent().build();
}
基本冒烟测试如下所示:
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WebMvcTest({ProductInformationController.class, CommonControllerAdvice.class})
class ProductInformationControllerTest {
private static final String SINGLE_RESOURCE_ENDPOINT_URL = "/product/{id}";
@Autowired
MockMvc mockMvc;
@MockBean
ProductInformationService service;
...
@Test
@WithMockUser(roles={"USER","ADMIN"})
void shouldReturn204ForDeleteProduct() throws Exception {
var productId = 1;
mockMvc.perform(delete(SINGLE_RESOURCE_ENDPOINT_URL, productId))
.andExpect(status().isNoContent());
}
我在依赖项中有Spring Security,但将SecurityFilterChain
暴露为@Bean
的配置不在为此WebMvcTest加载的类中。
然而,即使添加了理论上可能的用户角色,测试返回HTTP 403,甚至没有将调用传播到任何处理程序:
MockHttpServletRequest:
HTTP Method = DELETE
Request URI = /product/1
Parameters = {}
Headers = []
Body = null
Session Attrs = {org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.CSRF_TOKEN=org.springframework.security.web.csrf.DefaultCsrfToken@7ba1cdbe, SPRING_SECURITY_CONTEXT=SecurityContextImpl [Authentication=UsernamePasswordAuthenticationToken [Principal=org.springframework.security.core.userdetails.User [Username=user, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_ADMIN, ROLE_USER]], Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[ROLE_ADMIN, ROLE_USER]]]}
Handler:
Type = null
...
MockHttpServletResponse:
Status = 403
Error message = Forbidden
Headers = [X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY"]
Content type = null
Body =
Forwarded URL = null
Redirected URL = null
Cookies = []
端点的手动测试按预期工作。使用GET的任何测试都可以正常工作,其他HTTP动词也会遇到同样的问题。
我认为问题是这里Spring Security的部分设置。因为我只想测试Controller,所以我不想包含整个Spring Security配置。
我如何使这个控制器测试与最小的其他类集和最精简的应用程序上下文一起工作?
我想我也遇到过同样的问题。作为解决方案,我有条件地向安全链中添加了一些虚拟用户。
作为Spring-Security的mvc测试的基础,从Spring-Boot 2.7.0版本开始,我做了这个抽象基类:
import org.junit.jupiter.api.BeforeEach;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.RequestPostProcessor;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import javax.servlet.Filter;
import java.util.List;
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
public abstract class AbstractMvcTest {
@LocalServerPort
protected int port;
@Autowired
protected TestRestTemplate restTemplate;
protected MockMvc mockMvc;
@Autowired
private Filter springSecurityFilterChain;
@Autowired
private WebApplicationContext context;
@Autowired
private PasswordEncoder passwordEncoder;
private static final SimpleGrantedAuthority USER_AUTHORITY = new SimpleGrantedAuthority("USER");
private static final SimpleGrantedAuthority ADMIN_AUTHORITY = new SimpleGrantedAuthority("ADMIN");
@BeforeEach
void setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(context).addFilters(springSecurityFilterChain).build();
}
protected RequestPostProcessor makeAuthorizedAdminUser() {
return SecurityMockMvcRequestPostProcessors.user(new User(
"Admin@example.com",
passwordEncoder.encode("verys3cur3"),
List.of(ADMIN_AUTHORITY, USER_AUTHORITY)));
}
protected RequestPostProcessor makeAuthorizedUser() {
return SecurityMockMvcRequestPostProcessors.user(new User(
"User@example.com",
passwordEncoder.encode("foobar123"),
List.of(USER_AUTHORITY)));
}
}
一个具体的测试应该是这样的:
class IndexControllerTest extends AbstractMvcTest
{
@Autowired
private IndexController controller;
@Test
void contextLoads()
{
assertThat(controller).isNotNull();
}
@Test
void userLoggedIn_returnOk() throws Exception
{
this.mockMvc.perform(get("/")
.with(makeAuthorizedUser()))
.andDo(print())
.andExpect(status().isOk());
}
}
其中indexController只返回一个index.html目录下的服务。