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