【问题标题】:Inject @AuthenticationPrincipal when unit testing a Spring REST controller在对 Spring REST 控制器进行单元测试时注入 @AuthenticationPrincipal
【发布时间】:2016-11-14 19:07:33
【问题描述】:

我在尝试测试接收 UserDetails 作为带有 @AuthenticationPrincipal. 注释的参数的 REST 端点时遇到问题

似乎没有使用在测试场景中创建的用户实例,而是尝试使用默认构造函数进行实例化:org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.andrucz.app.AppUserDetails]: No default constructor found;

REST 端点:

@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.

【问题讨论】:

  • 那么没有办法使用standaloneSetup结合认证?
  • 它在哪里说的?
  • 我不确定,但是我怎么能得到一个FilterChainProxy,这是必需的?
  • 您也可以使用 webAppContextSetup,同时仍然通过@ContextConfiguration 保持对 bean 的完全控制。

标签: java spring spring-mvc spring-security spring-test


【解决方案1】:

这可以通过将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 带注释的方法参数被您的解析器中的详细信息填充。

【讨论】:

  • 这是个好方法,只是要小心!默认情况下注册了大约 20 个参数解析器来处理有趣的类型。在我的例子中,一个 Authentication 参数由 ServletRequestMethodArgumentResolver 处理,而我的处理程序被忽略了。
【解决方案2】:

由于某种原因,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!
       ...
    }
}

【讨论】:

    【解决方案3】:

    没有很好的文档记录,但是有一种方法可以将Authentication 对象作为独立 MockMvc 中的 MVC 方法的参数注入。如果你在SecurityContextHolder中设置Authentication,过滤器SecurityContextHolderAwareRequestFilter通常由Spring Security实例化并为你注入身份验证。

    您只需将该过滤器添加到您的 MockMvc 设置中,如下所示:

    @Before
    public void before() throws Exception {
        SecurityContextHolder.getContext().setAuthentication(myAuthentication);
        SecurityContextHolderAwareRequestFilter authInjector = new SecurityContextHolderAwareRequestFilter();
        authInjector.afterPropertiesSet();
        mvc = MockMvcBuilders.standaloneSetup(myController).addFilters(authInjector).build();
    }
    

    【讨论】:

      【解决方案4】:

      我知道这个问题已经过时了,但对于仍在寻找的人来说,对我使用 @AuthenticationPrincipal 编写 Spring Boot 测试(这可能不适用于所有实例)有用的是注释测试 @WithMockUser("testuser1")

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

      Here is a link @WithMockUser 上的 Spring 文档

      【解决方案5】:

      @pzeszko answer的简化:

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

      见:

      【讨论】:

        【解决方案6】:

        这个解决方案对我有用,我发现它真的很方便。

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

        【讨论】:

          猜你喜欢
          • 2022-10-18
          • 2012-05-25
          • 2011-08-12
          • 1970-01-01
          • 2020-08-27
          • 2012-01-31
          • 2020-08-07
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多