javaspring-bootrestspring-securityspring-test-mvc

Unit Testing rest controller with @AuthenticationPrincipal


I am developing a rest api with spring boot and spring security.

the code looks like so:

@RestController
@RequestMapping(path = "/api")
@PreAuthorize("isAuthenticated()")
public class RestController {

  @GetMapping(path = "/get", produces = "application/json")
  public ResponseEntity<InDto> get(
      @AuthenticationPrincipal final CustomUser user) {

    // ...

    return ResponseEntity.ok(outDto);
  }
}


public class CustomUser {
    // does not inherit from UserDetails
}


public class CustomAuthenticationFilter extends OncePerRequestFilter {

  @Override
  protected void doFilterInternal(
    @NonNull final HttpServletRequest request,
    @NonNull final HttpServletResponse response,
    @NonNull final FilterChain filterChain)
    throws ServletException, IOException {

    if (/* condition */) {
      // ...
      final CustomUser user = new CustomUser(/* parameters */);

      final Authentication authentication =
          new PreAuthenticatedAuthenticationToken(user, "", new ArrayList<>());
      SecurityContextHolder.getContext().setAuthentication(authentication);
    }

    filterChain.doFilter(request, response);
  }
}

I would like to unit test the RestController class ideally without the security feature but I don't know how to inject a specific CustomUser object during test.

I have tried to manually add a user to the security context before each test (see below) but the user injected into the controller during test is not the mocked on.

@WebMvcTest(RestController.class)
@AutoConfigureMockMvc(addFilters = false)
class RestControllerTest {

  @Autowired private MockMvc mockMvc;
  private CustomerUser userMock;

  @BeforeEach
  public void skipSecurityFilter() {
    userMock = Mockito.mock(CustomUser.class);
    SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
    final Authentication auth = new PreAuthenticatedAuthenticationToken(userMock, null, List.of());
    SecurityContextHolder.getContext().setAuthentication(auth);
  }

  @Test
  void test() {
    mockMvc.perform(
            MockMvcRequestBuilders.get("/api/get")
                .contentType(MediaType.APPLICATION_JSON)).andExpect(MockMvcResultMatchers.status().isOk());

  }
}

What is wrong? How to inject the specific userMock into the controller to perform the test?

EDIT to test with @WithMockCustomUser

as suggested in the doc https://docs.spring.io/spring-security/reference/servlet/test/method.html#test-method-withsecuritycontext i have updated the test to:

@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class)
public @interface WithMockCustomUser {
}

@Service
public class WithMockCustomUserSecurityContextFactory
    implements WithSecurityContextFactory<WithMockCustomUser> {

  @Override
  public SecurityContext createSecurityContext(final WithMockCustomUser customUser) {
    final SecurityContext context = SecurityContextHolder.createEmptyContext();

    final Authentication auth =
        new PreAuthenticatedAuthenticationToken(Mockito.mock(IUser.class), null, List.of());
    context.setAuthentication(auth);
    return context;
  }
}

@WebMvcTest(RestController.class)
@AutoConfigureMockMvc(addFilters = false)
class RestControllerTest {

  @Autowired private MockMvc mockMvc;
  private CustomerUser userMock;

  @BeforeEach
  public void skipSecurityFilter() {
    userMock = Mockito.mock(CustomUser.class);
  }

  @Test
  @WithMockCustomUser
  void test() {
    mockMvc.perform(
            MockMvcRequestBuilders.get("/api/get")
                .contentType(MediaType.APPLICATION_JSON)).andExpect(MockMvcResultMatchers.status().isOk());

  }
}

but the user object in the controller is still not the mock (created in the factory)


Solution

  • I rewrote the test to initialise the security context within the test

    @WebMvcTest(RestController.class)
    @AutoConfigureMockMvc
    @Import(value = {
        CustomAuthenticationFilter.class
    })
    class RestControllerTest {
    
      @Autowired private MockMvc mockMvc;
      private CustomerUser userMock;
    
      @BeforeEach
      public void skipSecurityFilter() {
        userMock = Mockito.mock(CustomUser.class);
      }
    
      @Test
      void test() {
        PreAuthenticatedAuthenticationToken(userMock, null, List.of());
        SecurityContextHolder.getContext().setAuthentication(auth);
        mockMvc.perform(MockMvcRequestBuilders.get("/api/get").contentType(MediaType.APPLICATION_JSON)).andExpect(MockMvcResultMatchers.status().isOk());
      }
    }
    

    and it works.

    Not sure exactly why the it does not work with the @BeforeEach.