[英]Inject @AuthenticationPrincipal when unit testing a Spring REST controller
我在尝试测试接收UserDetails
作为用@AuthenticationPrincipal.
似乎没有使用在测试场景中创建的用户实例,而是尝试使用默认构造函数进行实例化: org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.andrucz.app.AppUserDetails]: No default constructor found;
休息端点:
@RestController
@RequestMapping("/api/items")
class ItemEndpoint {
@Autowired
private ItemService itemService;
@RequestMapping(path = "/{id}",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public Callable<ItemDto> getItemById(@PathVariable("id") String id, @AuthenticationPrincipal AppUserDetails userDetails) {
return () -> {
Item item = itemService.getItemById(id).orElseThrow(() -> new ResourceNotFoundException(id));
...
};
}
}
测试类:
public class ItemEndpointTests {
@InjectMocks
private ItemEndpoint itemEndpoint;
@Mock
private ItemService itemService;
private MockMvc mockMvc;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
mockMvc = MockMvcBuilders.standaloneSetup(itemEndpoint)
.build();
}
@Test
public void findItem() throws Exception {
when(itemService.getItemById("1")).thenReturn(Optional.of(new Item()));
mockMvc.perform(get("/api/items/1").with(user(new AppUserDetails(new User()))))
.andExpect(status().isOk());
}
}
如何在不必切换到webAppContextSetup
的情况下解决该问题? 我想编写完全控制服务模拟的测试,所以我正在使用standaloneSetup.
这可以通过将HandlerMethodArgumentResolver
注入您的 Mock MVC 上下文或独立设置来完成。 假设您的@AuthenticationPrincipal
是ParticipantDetails
类型:
private HandlerMethodArgumentResolver putAuthenticationPrincipal = new HandlerMethodArgumentResolver() {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().isAssignableFrom(ParticipantDetails.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
return new ParticipantDetails(…);
}
};
这个参数解析器可以处理ParticipantDetails
类型并且只是凭空创建它,但是你会看到你得到了很多上下文。 稍后,此参数解析器附加到模拟 MVC 对象:
@BeforeMethod
public void beforeMethod() {
mockMvc = MockMvcBuilders
.standaloneSetup(…)
.setCustomArgumentResolvers(putAuthenticationPrincipal)
.build();
}
这将导致您的@AuthenticationPrincipal
注释方法参数被您的解析器的详细信息填充。
出于某种原因,Michael Piefel 的解决方案对我不起作用,所以我想出了另一个。
首先,创建抽象配置类:
@RunWith(SpringRunner.class)
@SpringBootTest
@TestExecutionListeners({
DependencyInjectionTestExecutionListener.class,
DirtiesContextTestExecutionListener.class,
WithSecurityContextTestExecutionListener.class})
public abstract MockMvcTestPrototype {
@Autowired
protected WebApplicationContext context;
protected MockMvc mockMvc;
protected org.springframework.security.core.userdetails.User loggedUser;
@Before
public voivd setUp() {
mockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
loggedUser = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
}
然后你可以写这样的测试:
public class SomeTestClass extends MockMvcTestPrototype {
@Test
@WithUserDetails("someUser@app.com")
public void someTest() throws Exception {
mockMvc.
perform(get("/api/someService")
.withUser(user(loggedUser)))
.andExpect(status().isOk());
}
}
并且@AuthenticationPrincipal 应该将您自己的 User 类实现注入到控制器方法中
public class SomeController {
...
@RequestMapping(method = POST, value = "/update")
public String update(UdateDto dto, @AuthenticationPrincipal CurrentUser user) {
...
user.getUser(); // works like a charm!
...
}
}
它没有很好的文档记录,但是有一种方法可以将Authentication
对象作为 MVC 方法的参数注入到独立的 MockMvc 中。 如果您在SecurityContextHolder
设置Authentication
,过滤器SecurityContextHolderAwareRequestFilter
通常由 Spring Security 实例化,并为您注入 auth。
您只需将该过滤器添加到您的 MockMvc 设置中,如下所示:
@Before
public void before() throws Exception {
SecurityContextHolder.getContext().setAuthentication(myAuthentication);
SecurityContextHolderAwareRequestFilter authInjector = new SecurityContextHolderAwareRequestFilter();
authInjector.afterPropertiesSet();
mvc = MockMvcBuilders.standaloneSetup(myController).addFilters(authInjector).build();
}
@pzeszko 答案的简化:
@ExtendWith(SpringExtension.class)
@SpringBootTest
@Transactional
@AutoConfigureMockMvc
public class ControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
@WithUserDetails(value = "user@gmail.com")
void get() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get(URL))
.andExpect(status().isOk())
.andDo(print());
}
见:
这个解决方案对我有用,我发现它真的很方便。
在test
包中创建一个实现 UserDetailsServce 的TestIUserDetails
服务:
@Service
@Primary
@Profile("test")
public class TestIUserDetails implements UserDetailsService {
public static final String ADMIN_USERNAME = "admin@example.com";
public static final String USERNAME = "user@example.com";
private User getUser() {
User user = new User();
user.setEmail(USERNAME);
user.setId(1L);
return user;
}
...
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if (Objects.equals(username, ADMIN_USERNAME))
return getAdminUser();
else if (Objects.equals(username, USERNAME))
return getUser();
return getPublicUser();
}
}
现在,在您的测试中:
@SpringMockWebEnvTestConfig
class AbcControllerTest {
@Autowired
private MockMvc mvc;
@Autowired
UserDetailsService userDetailsService;
private User user;
@BeforeEach
void setUp() {
user = (User) userDetailsService.loadUserByUsername(TestUserDetailsImpl.USERNAME);
}
@Test
public void testAbc(){
this.mvc.perform(post(endpoint).with(user(user))
...
.andExpect(status().isCreated())...
}
}
我的@AuthenticationPrincipal 需要一个 Jwt,因此必须实现一个实现 HandlerMethodArgumentResolver 的自定义参数解析器。
public class JwtArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().isAssignableFrom(Jwt.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
var jwtToken = Jwt.withTokenValue("token")
.header("alg", "none")
.claim("sub", "user")
.claim("scope", "read").build();
return jwtToken;
}
}
在测试中:
@BeforeAll
public void setup() {
MockitoAnnotations.initMocks(this);
mockMvc = MockMvcBuilders.standaloneSetup(myController).setCustomArgumentResolvers(new JwtArgumentResolver()).build();
}
并且 mockMvc.perform 可以在不传递RequestPostProcessor
的情况下使用。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.