Spring-Boot WebMvcTest:如何测试带有Authentication对象参数的控制器方法?

huangapple go评论81阅读模式
英文:

Spring-Boot WebMvcTest: How to test controller method with Authentication object parameter?

问题

这是问题的续篇。
链接:https://stackoverflow.com/questions/62799664/spring-webmvctest-how-to-mock-authentication

我试图测试一个 Spring Boot 中的控制器方法,该方法接受一个 Authentication 对象作为参数。该控制器是一个带有 @CrossOrigin 注解的 RestController。方法如下所示:

@GetMapping("/authentication")
public String testAuthentication(Authentication authentication) {
    UserDetailsStub userDetailsStub = (UserDetailsStub) authentication.getPrincipal();
    return userDetailsStub.getUsername();
}

正如你所看到的,我从参数中的 Authentication 对象中获取了主体。

问题是,在我的 WebMvcTest 测试用例中,我得到了一个 NullPointerException,因为在测试用例中,authentication 似乎为 null。我的问题是为什么会这样?

我尝试过在测试用例中添加一个 given 调用,在 @PostConstruct 注解的方法中返回一个自定义的 UserDetails 对象,但我仍然得到了 NullPointerException

我的测试用例如下所示:

@Import(SecurityConfiguration.class)
@RunWith(SpringRunner.class)
@WebMvcTest(PDPController.class)
@AutoConfigureMockMvc(addFilters = false)
public class PDPControllerTests {

    @Autowired
    private MockMvc mvc;

    @MockBean(name = "userDetailsService")
    private MyUserDetailsService userDetailsService;
	
    //..

    @PostConstruct
    public void setup() {
        given(userDetailsService.loadUserByUsername(anyString()))
                .willReturn(new UserDetailsStub());
    }
	
    //..

    @Test
    @WithUserDetails(value = "username", userDetailsServiceBeanName = "userDetailsService")
    public void testAuthentication() throws Exception {
        mvc.perform(get("/pdps/authentication").secure(true)
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk());
    }

}

为什么在测试用例中 authentication 为空,即使我在 @PostConstruct 方法中提供了它呢?

一个包含了复现错误的最小代码的 GitHub 项目可以在这里找到:
https://github.com/Kars1090/SpringSecurityTest

谢谢!

英文:

This is a continuation of this question
https://stackoverflow.com/questions/62799664/spring-webmvctest-how-to-mock-authentication

I'm trying to test a controller method in Spring-boot that receives an Authentication object as parameter. The controller is a RestController with @CrossOrigin annotation. The method looks like this:

@GetMapping("/authentication")
public String testAuthentication(Authentication authentication) {
    UserDetailsStub userDetailsStub = (UserDetailsStub) authentication.getPrincipal();
    return userDetailsStub.getUsername();
}

As you can see i get the principal from the Authentication out of the parameters.

The problem is, in my WebMvcTest test case i get a NullPointerException because in the test case, authentication seems to be null. My question is why?

I have tried adding a given call which will return a custom UserDetails object in a @PostConstruct annotated metho in the test case, but still i get the NullPointerException.

My test case looks like this:

@Import(SecurityConfiguration.class)
@RunWith(SpringRunner.class)
@WebMvcTest(PDPController.class)
@AutoConfigureMockMvc(addFilters = false)
public class PDPControllerTests {

    @Autowired
    private MockMvc mvc;

    @MockBean(name = "userDetailsService")
    private MyUserDetailsService userDetailsService;
	
	//..

    @PostConstruct
    public void setup() {
        given(userDetailsService.loadUserByUsername(anyString()))
                .willReturn(new UserDetailsStub());
    }
	
	//..

    @Test
    @WithUserDetails(value = "username", userDetailsServiceBeanName = "userDetailsService")
    public void testAuthentication() throws Exception {
        mvc.perform(get("/pdps/authentication").secure(true)
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk());
    }
	
}

Why is authentication null in the test case, even when i supply it in the @PostConstruct method?

A GitHub project with minimal code that reproduces the error can be found here.
https://github.com/Kars1090/SpringSecurityTest

Thanks!

答案1

得分: 1

在克隆您的项目后,我成功地在您的控制器方法中获得了一个有效的 Authentication 对象。基本上,您的测试中存在两个主要问题:

  1. 不必要的额外配置
  2. 对您的过滤器 JwtRequestFilter 进行了错误的模拟配置

总之,更改如下:

public class UserDetailsStub implements UserDetails {

  private String username;
  private String password;
  private Collection<? extends GrantedAuthority> authorities;

  public UserDetailsStub() {}

  public static UserDetailsStub of(User user) {
    UserDetailsStub userDetails = new UserDetailsStub();
    if (null != user) {
        userDetails.username = user.getUsername();
        userDetails.password = user.getPassword();
        userDetails.authorities = user.getAuthorities();
    }
    return userDetails;
  }

  @Override
  public Collection<? extends GrantedAuthority> getAuthorities() {
    return authorities;
  }

  @Override
  public String getPassword() {
    return password;
  }

  @Override
  public String getUsername() {
    return username;
  }
  // 代码的其余部分与您的版本相同
}

您的控制器方法:

@GetMapping("/authentication")
public String testAuthentication(Authentication authentication) {
  UserDetailsStub userDetailsStub = UserDetailsStub.of((User) 
    authentication.getPrincipal());
  return userDetailsStub.getUsername();
}

以及测试部分:

@WebMvcTest(value = PDPController.class)
public class PDPControllerTests {

  @Autowired
  private MockMvc mvc;

  /** 您不必模拟过滤器,因为在这种情况下,Spring 将不知道如何处理它,
   * 当应该管理它们的列表时。
   *
   * 这就是您不得不包含
   * @AutoConfigureMockMvc(addFilters = false) 的原因,但这正是导致
   * 您的 Authentication 对象未被创建的原因,因为您的 JwtRequestFilter
   * 没有被执行。
   *
   * 使用当前的代码,您的过滤器将被执行,Authentication 对象将被创建。
   */
   //@MockBean
   //private JwtRequestFilter jwtRequestFilter;

   // 您需要模拟过滤器在内部使用的类
   @MockBean
   private MyUserDetailsService userDetailsService;

   @MockBean
   private JwtService jwtService;

   @Test
   @WithMockUser
   public void test() throws Exception {
     mvc.perform(
            get("/pdps/authentication").secure(true)
                    .contentType(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk());
   }
}
英文:

After clone your project I have achieved to receive a valid Authentication object in your controller method. Basically you have 2 main problems in your test:

  1. Unnecessary extra configuration
  2. Bad mock configuration of your filter: JwtRequestFilter

In summary, the changes were the following ones:

public class UserDetailsStub implements UserDetails {

  private String username;
  private String password;
  private Collection&lt;? extends GrantedAuthority&gt; authorities;

  public UserDetailsStub() {}
        
  public static UserDetailsStub of (User user) {
    UserDetailsStub userDetails = new UserDetailsStub();
    if (null != user) {
        userDetails.username = user.getUsername();
        userDetails.password = user.getPassword();
        userDetails.authorities = user.getAuthorities();
    }
    return userDetails;
  }

  @Override
  public Collection&lt;? extends GrantedAuthority&gt; getAuthorities() {
    return authorities;
  }

  @Override
  public String getPassword() {
    return password;
  }

  @Override
  public String getUsername() {
    return username;
  }
  // Rest of the code is equal to your version

Your controller method:

@GetMapping(&quot;/authentication&quot;)
public String testAuthentication(Authentication authentication) {
  UserDetailsStub userDetailsStub = UserDetailsStub.of((User) 
    authentication.getPrincipal());
  return userDetailsStub.getUsername();
}

And the test:

@WebMvcTest(value = PDPController.class)
public class PDPControllerTests {

  @Autowired
  private MockMvc mvc;

  /** You have not to mock the filter because in that case Spring
   * won&#39;t know how to deal with it, when the list of them
   * should be managed.
   *
   * That is the reason why you had to include
   * @AutoConfigureMockMvc(addFilters = false), but that
   * is preciselly what was avoiding the creation of your
   * Authentication object, because your JwtRequestFilter
   * was not being executed.
   *
   * With the current code, your filter will be executed and
   * the Authentication object created.
   */
   //@MockBean
   //private JwtRequestFilter jwtRequestFilter;

   // What you have to mock are the classes the filter uses internally
   @MockBean
   private MyUserDetailsService userDetailsService;

   @MockBean
   private JwtService jwtService;

   @Test
   @WithMockUser
   public void test() throws Exception {
     mvc.perform(
            get(&quot;/pdps/authentication&quot;).secure(true)
                    .contentType(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk());
   }
 }

答案2

得分: 0

一个(不好的)解决方法是从控制器参数中移除authentication,而是通过SecurityContextHolder.getContext().getAuthentication()来获取认证信息。

这将使测试正常工作,而无需改变其他任何内容。

英文:

A (bad) workaround would be to remove authentication from the controller parameters and instead get the authentication via SecurityContextHolder.getContext().getAuthentication().

This will make the test work without changing anything else.

huangapple
  • 本文由 发表于 2020年8月15日 04:27:04
  • 转载请务必保留本文链接:https://go.coder-hub.com/63419621.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定