0

I am trying to write unit test cases for Mycontroller using WebTestClient in my web-flux Spring boot application.

It is always returning 401 while try to execute my unit test case. I already go through some old answers of stackoverflow but no luck.

Below are the approaches that I followed till now:

  1. Disable security using @WebFluxTest(controllers = MyController.class,excludeAutoConfiguration = {ReactiveSecurityAutoConfiguration.class}))

    Above one disabled security perfecty but the issue is I am getting java.security.Principal null in MyController and controller throwing null pointer exception while trying to fetch value from Principal.

  2. Add default bearer token in my webTestClient test. In this case I am getting 401.

  3. Add valid bearer token in WebTestClient test that we are using to hit APIs normally. It this case as well I am getting 401[unauthorized] error.

  4. Tried to mock Principal object but didn't got success. below is the code that I tried:

    Authentication authentication = Mockito.mock(Authentication.class);
    SecurityContext securityContext = Mockito.mock(SecurityContext.class);
    Mockito.when(securityContext.getAuthentication()).thenReturn(authentication);
    SecurityContextHolder.setContext(securityContext);
    SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_GLOBAL);
    SecurityContextHolder.getContext().setAuthentication(authentication);
    Collection<GrantedAuthority> authorities = new ArrayList<>();
    UsernamePasswordAuthenticationToken principal1 = new UsernamePasswordAuthenticationToken("testUser", "testPass");
    
    Mockito.when(authentication.getPrincipal()).thenReturn(principal1);
    

Below is MyController and MyControllerTest class for your reference:

  1. MyColroller.java

    @GetMapping(produces = APPLICATION_JSON_VALUE, value = "/myController/{transactionId}")
    @Operation(
        security = @SecurityRequirement(name = OAUTH2),
        responses = {
                @ApiResponse(responseCode = "200",
                        content = @Content(mediaType = APPLICATION_JSON_VALUE,
                                schema = @Schema(implementation = TransactionDocumentResponse.class)))
        })
    public ResponseEntity<TransactionDocumentResponse> get(
        @PathVariable(value = "transactionId") String transactionId, Principal principal) {
    
        Map<String, String> userInfo = (Map) ((Authentication) principal).getDetails();
        utility.checkForMissingToken(userInfo, xTestHeader);
        return service.get(transactionId, userInfo);
    }
    
  2. MyControllerTest.java

    @RunWith(SpringRunner.class)
    @WebFluxTest(controllers = {MyController.class})
    @AutoConfigureWebTestClient
    @TestPropertySource(
            properties = {
                    "default-jwt-token = my default jwt token",
            }
    )
    public class MyControllerTests {
    
        private static final String testTransactionId = "abc_def";
        private static final BigInteger testTransactionIdBigInteger = BigInteger.valueOf(123);
    
        @Value("${default-jwt-token}")
        private String jwtToken;
    
        @MockBean
        MyServiceImpl service;
    
        @MockBean
        Utility utility;
    
        @Autowired
        private WebTestClient webTestClient;
    
        @Test
        public void test_success() {
    
            TransactionDocumentResponse transactionDocumentResponse = new TransactionDocumentResponse();
            transactionDocumentResponse.setTransactionId(testTransactionIdBigInteger);
            ResponseEntity<TransactionDocumentResponse> response = ResponseEntity.of(Optional.of(transactionDocumentResponse));
    
            Mockito.doNothing().when(utility).checkForMissingToken(anyMap(), anyString());
            when(service.getRevolvingTransactions(anyString(), anyMap())).thenAnswer(createAnswer(response));
    
            WebTestClient.BodySpec<JSONObject, ?> stringBodySpec = webTestClient
                    .method(HttpMethod.GET)
                    .uri("/myController/{transactionId}", testTransactionId)
                    .headers(h -> {
                        h.setBearerAuth(jwtToken);
                    })
            .exchange()
            .expectBody(JSONObject.class);
    
            assertEquals(123, stringBodySpec.returnResult().getResponseBody().get("transactionId"));
        }
    }
    

Basically I need some mechanism to mock Spring security so that 401 error gone and I got some value in my Principal object.

Also I already tried older solutions so please don't mark it duplicate :).

Thanks

  • Spring Security provides integration with `WebTestClient`. Check https://docs.spring.io/spring-security/site/docs/5.2.12.RELEASE/reference/html/test-webflux.html for details – Alex Jun 30 '22 at 06:26
  • As I can check on google. We can use WebTestCilent as a replacement of MockMvc to perform unit testing on web Flux Controller. https://howtodoinjava.com/spring-webflux/webfluxtest-with-webtestclient/ Also as shared docs mentioned : "Spring Security provides integration with WebTestClient". I think it only means spring provide support for WebTestClient. – Shubham Garg Jun 30 '22 at 06:41

2 Answers2

0

Please have a look here: https://docs.spring.io/spring-security/site/docs/4.2.x/reference/html/test-method.html

@WithMockUser and related annotations might be useful here and you won't need to do ugly mocking security context stuff.

vladtkachuk
  • 656
  • 4
  • 13
0

I created a custom annotation to mock Spring security part for my unit test cases because I need some extra fields while fetching value from Principal object in my controller.

Below is the code:

  1. Create an annotation with name WithMockCustomUser

    @Retention(RetentionPolicy.RUNTIME)
    @WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class)
    public @interface WithMockCustomUser {}
    
  2. Annotation implementation part:

    public class WithMockCustomUserSecurityContextFactory
    implements WithSecurityContextFactory<WithMockCustomUser> {
    
    @Override
    public SecurityContext createSecurityContext(WithMockCustomUser customUser) {
        SecurityContext context = SecurityContextHolder.createEmptyContext();
    
        CustomUserDetails customUserDetails = new CustomUserDetails();
    
        AbstractAuthenticationToken auth = new UsernamePasswordAuthenticationToken(customUserDetails.getPrincipal(),
            customUserDetails.getCredentials(), customUserDetails.getAuthorities());
    
        auth.setDetails(customUserDetails.getDetails());
    
        context.setAuthentication(auth);
        return context;
        }
    }
    
  3. Create CustomUserDetails class that override Authentication class also override all abstract methods of Authntication and provide my custom implementation:

    public class CustomUserDetails implements Authentication {
        //Override Authentication class methods and return some cutstom values
    }
    

Hurray, Now my Custom annotation @MockWithCustomUser is ready to use with my unit test cases like below:

@Test
@MockWithCustomUser
public void myTest() {
   //Unit test code
}

Hope this help someone in mocking Spring Security with Custom implementation :)