簡體   English   中英

在對 Spring REST 控制器進行單元測試時注入 @AuthenticationPrincipal

[英]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 上下文或獨立設置來完成。 假設您的@AuthenticationPrincipalParticipantDetails類型:

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

我知道這個問題很老,但對於仍在尋找的人來說,使用@AuthenticationPrincipal編寫 Spring Boot 測試對我@AuthenticationPrincipal (這可能不適用於所有實例),是注釋測試@WithMockUser("testuser1")

@Test
@WithMockUser("testuser1")
public void successfullyMockUser throws Exception {
    mvc.perform(...));
}

這是@WithMockUser上 Spring 文檔的鏈接

@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包中創建一個實現 UserDetailsS​​ervce 的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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM