Search code examples
javaunit-testingspring-securitymockitospring-boot-test

How to mock AuthenticationManager authenticate method using Spring Security and Mockito


I'm trying to use Mockito to test whether when the user hits the login api, it will respond with a JWT token. However, I keep getting the Bad Credentials error which comes from the authenticationManager.authenticate() method inside Spring Security. I'm now trying to mock this method, but I keep getting a variety of different errors and not sure if my approach is correct. This is my latest implementation, which now fails with You cannot use argument matchers outside of verification or stubbing as it's not liking how I'm using the mocks.

@WebMvcTest(value = UserCommandController.class, includeFilters = {
    // to include JwtUtil in spring context
    @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = JwtUtil.class)})
class UserCommandControllerTest {

Logger logger = LoggerFactory.getLogger(UserCommandControllerTest.class);

@MockBean
private UserCommandService userCommandService;

@Autowired
private MockMvc mockMvc;

@MockBean
private UserDetailsServiceImpl userDetailsServiceImpl;

@MockBean
private JwtUtil jwtUtil;

@Autowired
private JwtRequestFilter jwtRequestFilter;

@Autowired
AuthenticationManager authenticationManager;

private static UserDetails dummy;
private static String jwtToken;

@BeforeEach
public void setUp() {
    dummy = new User("[email protected]", "123456", new ArrayList<>());
    jwtToken = jwtUtil.generateToken(dummy);
}


@Test
void testLoginReturnsJwt() throws Exception {

    AuthenticationRequest authenticationRequest = new AuthenticationRequest("[email protected]", "123456");
    AuthenticationResponse authenticationResponse = new AuthenticationResponse("anyjwt");

    String jsonRequest = asJsonString(authenticationRequest);
    String jsonResponse = asJsonString(authenticationResponse);

    RequestBuilder request = MockMvcRequestBuilders
            .post("/api/adverts/user/login")
            .content(jsonRequest)
            .contentType(MediaType.APPLICATION_JSON_VALUE)
            .accept(MediaType.APPLICATION_JSON);

    Authentication authentication = mock(Authentication.class);
    authentication.setAuthenticated(true);
    when(authentication.isAuthenticated()).thenReturn(true);

    when(authenticationManager.authenticate(any())).thenReturn(authentication); // Failing here

    when(jwtUtil.generateToken(dummy)).thenReturn("124");
    when(userDetailsServiceImpl.loadUserByUsername(eq("[email protected]"))).thenReturn(dummy);

    MvcResult mvcResult = mockMvc.perform(request)
            .andExpect(status().is2xxSuccessful())
            .andExpect(content().json(jsonResponse, true))
            .andExpect(jsonPath("$.jwt").value(isNotNull()))
            .andReturn();
}

Here is my controller:

@PostMapping(value = "/login", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Object> loginUser(@Valid @RequestBody AuthenticationRequest authenticationRequest) throws Exception {

    try {
        Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(authenticationRequest.getUsername(), authenticationRequest.getPassword()));
    } catch (BadCredentialsException e) {
        throw new Exception("incorrect username or password", e);
    }

    UserDetails userDetails = userDetailsService.loadUserByUsername(authenticationRequest.getUsername());

    String jwt = jwtTokenUtil.generateToken(userDetails);

    return ResponseEntity.ok(new AuthenticationResponse(jwt));
}

Thank you.

PS: Here is my repo and where AuthenticationManager is set: https://github.com/francislainy/adverts-backend/blob/dev_jwt/src/main/java/com/example/adverts/MyWebSecurity.java#L30


Solution

  • You need the AuthenticationManager to be a mock and in your code it's not (@Autowired injects a "real" instance). You can mock beans using MockBean annotation:

    @MockBean
    AuthenticationManager authenticationManager;
    

    Now you can mock authenticationManager all you want.

    Another problem with your code is that you are misusing the ArgumentMatchers.isNotNull() matcher in your assertions causing an exception:

    org.mockito.exceptions.misusing.InvalidUseOfMatchersException: 
    Misplaced or misused argument matcher detected here:
    
    -> at com.example.adverts.controller.user.UserCommandControllerTest.testLoginReturnsJwt(UserCommandControllerTest.java:356)
    
    You cannot use argument matchers outside of verification or stubbing.
    Examples of correct usage of argument matchers:
        when(mock.get(anyInt())).thenReturn(null);
        doThrow(new RuntimeException()).when(mock).someVoidMethod(any());
        verify(mock).someMethod(contains("foo"))
    
    This message may appear after an NullPointerException if the last matcher is returning an object 
    like any() but the stubbed method signature expect a primitive argument, in this case,
    use primitive alternatives.
        when(mock.get(any())); // bad use, will raise NPE
        when(mock.get(anyInt())); // correct usage use
    
    Also, this error might show up because you use argument matchers with methods that cannot be mocked.
    Following methods *cannot* be stubbed/verified: final/private/equals()/hashCode().
    Mocking methods declared on non-public parent classes is not supported.
    

    ArgumentMatchers provides matchers to be used in method call stubbing and verification. You should use org.hamcrest.CoreMatchers#notNullValue() instead.

    With these fixes your test passes all in green.