Robert Strauch
Robert Strauch

Reputation: 12896

How to provide custom UserDetails with additional fields for testing a secured controller method?

Assume I have the following @WebMvcTest and @RestController in a Spring boot applcation (version 2.4.2).

// the test

@Test
@WithUserDetails
public void should_return_ok() throws Exception {
    mockMvc.perform(get("/api/products").andExpect(status().isOk());
}

// the controller

@GetMapping(path = "/api/products")
public ResponseEntity<List<Product>> getProducts(@AuthenticationPrincipal CustomUserDetails userDetails) {
    List<Product> products = productService.getProductsByUserId(userDetails.getUserId());
    return ResponseEntity.ok(products);
}

I also provided a CustomUserDetails class which adds a userId.

@Getter
@Setter
public class CustomUserDetails extends User {

    private static final long serialVersionUID = 5540615754152379571L;

    private Long userId;

    public CustomUserDetails(String username, String password, Collection<? extends GrantedAuthority> authorities) {
        super(username, password, authorities);
    }

    public CustomUserDetails(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
        super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
    }
}

I understand that Spring provides the @WithUserDetails annotation to provide an adequate object for testing. And this also allows specifying a custom username, password, etc. However I don't know how I could provide the userId which is necessary so that the controller method can extract it from the CustomUserDetails object.

Upvotes: 1

Views: 1025

Answers (2)

Marcus Hert da Coregio
Marcus Hert da Coregio

Reputation: 6288

In your implementation of UserDetailsService you should return your instance of UserDetails. For example:

@Override
public UserDetails loadByUsername(String username) throws UsernameNotFoundException {
   User user = userRepository.findByUsername(username);
   
   if (user == null) {
       throw new UsernameNotFoundException("Username " + username + " not found");
   }

   CustomUserDetails customUserDetails = new CustomUserDetails(user);
   customUserDetails.setUserId(user.getUserId());
   return customUserDetails;
}

public class CustomUserDetails implements UserDetails {
    private final Long userId;
    private final User user;

    ...constructors
    ...getters and setters
}

In your code, you can cast the Authentication object to your CustomUserDetails.

CustomUserDetails customUserDetails = (CustomUserDetails) SecurityContextHolder.getContext().getAuthentication();
Long userId = customUserDetails.getUserId();

Upvotes: 0

Lee Greiner
Lee Greiner

Reputation: 1082

You can create your own custom UserDetails object in your test class and do the following:


import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;

CustomUserDetails customUserDetails = new CustomUserDetails(...);

mockMvc.perform(get("/api/products").with(user(customUserDetails))).andExpect(status().isOk());    

Upvotes: 1

Related Questions