[英]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.