Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SpringBootTest mock Authentication Principal with a custom User does not work

I am using Spring Boot 1.4.2 and I am new with Spring Boot. I have an Authentication Filter to set the current user information when the user log in. In an advice for the controller, I made the call to get the current userId as below:

public static String getCurrentUserToken(){
    return ((AuthenticatedUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserId();
}

Here is my custom AuthenticatedUser:

public class AuthenticatedUser implements Serializable {

private final String userName;
private final String userId;
private final String sessionId;

public AuthenticatedUser(String userName, String userId, String sessionId) {
    super();
    this.userName = userName;
    this.userId = userId;
    this.sessionId = sessionId;
}

public String getUserName() {
    return userName;
}

public String getUserId() {
    return userId;
}

public String getSessionId() {
    return sessionId;
}

}

Everything works fine. However, the filter does not work in integration testing and I need to mock a current user. I searched a lot on how to mock a user but none of them help me. I finally found out this guide which is likely close to what I wanted: https://aggarwalarpit.wordpress.com/2017/05/17/mocking-spring-security-context-for-unit-testing/ Below is my testing class following that guideline:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.DEFINED_PORT)
public class PersonalLoanPreApprovalTest {

@Before
public void initDB() throws Exception {
    MockitoAnnotations.initMocks(this);
}

@Test
public void testRequestPersonalLoanPreApproval_Me() {
  AuthenticatedUser applicationUser = new 
  AuthenticatedUser("[email protected]", "2d1b5ae3", "123");
  UsernamePasswordAuthenticationToken authentication = new ApiKeyAuthentication(applicationUser);
  SecurityContext securityContext = mock(SecurityContext.class);

  when(securityContext.getAuthentication()).thenReturn(authentication);
  SecurityContextHolder.setContext(securityContext);

  // error at this line
  when(securityContext.getAuthentication().getPrincipal()) .thenReturn(applicationUser); 

  // The controller for this api has the advice to get the userId
  MyResponse response = restTemplate.getForObject(url.toString(), MyResponse.class);
}
}

I got this error:

org.mockito.exceptions.misusing.WrongTypeOfReturnValue: 
AuthenticatedUser cannot be returned by getAuthentication()
getAuthentication() should return Authentication

I spent couple days on this already, tried a lot of suggestions I found but still failed. I also tried to remove the line that cause error, but when the error gone, I still could not get the current user information in my controller advice.

Any suggestion is highly appreciated.

UPDATE1: Just wanted to have my results with some modification in the code.

With @glitch suggestion, I changed the code to mock the authentication and user in my test method as below:

 @Test
 public void testRequestPersonalLoanPreApproval_Me() {
    AuthenticatedUser applicationUser = new AuthenticatedUser("[email protected]", "2d1b5ae3-cf04-44f5-9493-f0518cab4554", "123");
    Authentication authentication = Mockito.mock(Authentication.class);
    SecurityContext securityContext = Mockito.mock(SecurityContext.class);
    Mockito.when(securityContext.getAuthentication()).thenReturn(authentication);
    SecurityContextHolder.setContext(securityContext);
    Mockito.when(authentication.getPrincipal()).thenReturn(applicationUser);      

  // The controller for this api has the advice to get the userId
  MyResponse response = restTemplate.getForObject(url.toString(), MyResponse.class);
}
}

I now can get rid of the error in the testing class. I debugged into the code, see that the securityContext has value when I am in the testing class. But when I jump to the code inside the controller advice, the below get returns null:

SecurityContextHolder.getContext().getAuthentication().getPrincipal()
like image 646
Sal Avatar asked Oct 07 '17 00:10

Sal


2 Answers

There a Spring test annotation (org.springframework.security.test.context.support.WithMockUser) which does this for you...

@Test
@WithMockUser(username = "myUser", roles = { "myAuthority" })
public void aTest(){
    // any usage of `Authentication` in this test invocation will get an instance with the user name "myUser" and a granted authority "myAuthority"
    // ...
}

Alternatively, you can continue with your current approach by mocking Spring's Authentication. For example, in your test case:

Authentication authentication = Mockito.mock(Authentication.class);

Then tell Spring's SecurityContextHolder to store this Authentication instance:

SecurityContext securityContext = Mockito.mock(SecurityContext.class);
Mockito.when(securityContext.getAuthentication()).thenReturn(auth);
SecurityContextHolder.setContext(securityContext);

Now, if your code needs Authentication to return something (the user name perhaps) you just set some expectations on the mocked Authentication instance in the usual way e.g.

Mockito.when(authentication.getName()).thenReturn("aName");

This is quite close to what you are already doing but you have just mocked the wrong type.

Update 1: in response to this update to the OP:

I now can get rid of the error in the testing class. I debugged into the code, see that the securityContext has value when I am in the testing class. But when I jump to the code inside the controller advice, the below get returns null:

SecurityContextHolder.getContext().getAuthentication().getPrincipal()

You just need to set an expectation on the mocked Authentication, for example:

UsernamePasswordAuthenticationToken principal = new UsernamePasswordAuthenticationToken("aUserName", "aPassword");
Mockito.when(authentication.getPrincipal()).thenReturn(principal);

With the above code this line ...

SecurityContextHolder.getContext().getAuthentication().getPrincipal();

... will return that UsernamePasswordAuthenticationToken.

Since you use a custom type (ApiKeyAuthentication, I think?) you should just let authentication.getPrincipal() return that type instead of a UsernamePasswordAuthenticationToken.

like image 177
glytching Avatar answered Sep 21 '22 20:09

glytching


In addition to glytching's answer I had to add the following line when mocking:

SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_GLOBAL);

That is because the spring boot test code doesn't run in the same thread as the spring boot application you're testing, and the default strategy is ThreadLocal. With MODE_GLOBAL you ensure that the mocked SecurityContext object is actually returned in your application.

like image 31
JavaDevOps Avatar answered Sep 23 '22 20:09

JavaDevOps