Stack Exchange
Unit Testing Controller Layer in Spring Boot 3 | Getting Error in Authentication
Hi I am trying to unit test a POST endpoint in my Controller Layer, but it gives me below error:
java.lang.NullPointerException: Cannot invoke "org.springframework.security.authentication.UsernamePasswordAuthenticationToken.getPrincipal()" because "principal" is null
at com.lululemon.product.sl.controller.SMVConfigController.getUserInfo(SMVConfigController.java:67) ~[classes/:na]
at com.lululemon.product.sl.controller.SMVConfigController.addSmvConfig(SMVConfigController.java:52) ~[classes/:na]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:578) ~[na:na]

Below is my Web Security Config Settings, Controller Layer and Unit Test Class:
I am doing it with MockMvc Autowired
Web Security Config:
package com.lululemon.product.sl.config;

import com.lululemon.product.sl.helper.WebSecurityHelper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

import static org.springframework.security.config.Customizer.withDefaults;

@Slf4j
@Configuration
@EnableWebSecurity
@EnableCaching
@EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true,prePostEnabled=false)
public class WebSecurityConfig {

@Value("${spring.security.enabled:false}")
private boolean securityEnabled;

@Autowired
private WebSecurityHelper webSecurityHelper;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

    if (securityEnabled) {

        http.csrf(AbstractHttpConfigurer::disable)
                .headers(headers -> headers.defaultsDisabled().cacheControl(withDefaults()))
                .authorizeHttpRequests(authorize -> authorize
                        .requestMatchers("/v1/configuration/**").authenticated().anyRequest().permitAll())
                .oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(webSecurityHelper.jwtAuthenticationConverter())));
    } else {
        http.csrf(AbstractHttpConfigurer::disable).cors(AbstractHttpConfigurer::disable);
        log.warn("Running app without security");
    }

    return http.build();
}

@ConditionalOnProperty(value = "cors.enabled", havingValue = "false")
@Configuration
@EnableWebMvc
public class CorsConfigCustomizer implements WebMvcConfigurer {
    @Value("${cors.allowedOrigins}")
    List<String> allowedOrigin;

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        log.warn("Disabling CORS for: {}", allowedOrigin);
        registry.addMapping("/**")
                .allowedMethods("GET", "POST", "PUT")
                .allowedOrigins(allowedOrigin.toArray(new String[0]));
    }

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return web -> web.ignoring().requestMatchers(HttpMethod.OPTIONS);
    }
}

}
Controller Layer:
package com.lululemon.product.sl.controller;

import com.lululemon.product.rmcosting.common.vo.UserInfo;
import com.lululemon.product.rmcosting.response.Response;
import com.lululemon.product.sl.dto.SmvDTO;
import com.lululemon.product.sl.helper.UserInfoHelper;
import com.lululemon.product.sl.service.SMVConfigService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import lombok.extern.slf4j.Slf4j;

import java.security.Principal;
import java.util.List;


@Slf4j
@RestController
@RequestMapping("/v1/configuration")
public class SMVConfigController {

@Autowired
private SMVConfigService smvConfigService;

@Autowired
UserInfoHelper userInfoHelper;

@GetMapping("/getAllConfigs")
public ResponseEntity<Response<List<SmvDTO>>> getAllSmvConfigs(){

    List<SmvDTO> smvDTOs;
    smvDTOs = smvConfigService.getAllConfigs();
    return ResponseEntity.ok(Response.<List<SmvDTO>>builder().data(smvDTOs).build());
}

@GetMapping("/{smvCode}")
public ResponseEntity<Response<SmvDTO>> getConfigBySmvCode(@PathVariable String smvCode){

    SmvDTO smvDTO;
    smvDTO = smvConfigService.findBySmvCode(smvCode);
    return ResponseEntity.ok(Response.<SmvDTO>builder().data(smvDTO).build());
}

@PostMapping("/")
public ResponseEntity<Response<SmvDTO>> addSmvConfig(@Validated @RequestBody SmvDTO smvDTO, Principal principal){

    SmvDTO savedSmvDTO = smvConfigService.saveSmvConfig(smvDTO, getUserInfo(principal));
    return ResponseEntity.ok(Response.<SmvDTO>builder().data(savedSmvDTO).build());
}

@PutMapping("/{smvCode}")
public ResponseEntity<Response<SmvDTO>> updateSmvConfig(@PathVariable String smvCode, @Validated @RequestBody SmvDTO smvDTO, Principal principal){

    SmvDTO response = smvConfigService.updateConfig(smvCode, smvDTO, getUserInfo(principal));
    return ResponseEntity.ok(Response.<SmvDTO>builder().data(response).build());
}

private UserInfo getUserInfo(Principal principal){
    if(principal instanceof JwtAuthenticationToken) {
        return userInfoHelper.getUserInfo(((JwtAuthenticationToken) principal).getToken().getTokenValue());
    }else{
        User user = (User)((UsernamePasswordAuthenticationToken) principal).getPrincipal();
        return UserInfo.builder().username(user.getUsername()).build();
    }

}

}

Controller Test Class:
package com.lululemon.product.sl.controller;


import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

import com.lululemon.product.sl.helper.UserInfoHelper;
import com.lululemon.product.sl.dto.SmvDTO;

import com.lululemon.product.sl.service.SMVConfigService;
import com.lululemon.product.sl.util.LocalDateTimeSerializer;
import com.lululemon.product.sl.util.SmvConfigSampleData;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;


import org.mockito.MockitoAnnotations;

import org.mockito.junit.jupiter.MockitoExtension;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;


import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import java.time.LocalDateTime;


import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@ExtendWith(MockitoExtension.class)
@WebMvcTest(SMVConfigController.class)
class SMVConfigControllerTest {

@Autowired
MockMvc mockMvc;

@MockBean
private SMVConfigService smvConfigService;

@MockBean
UserInfoHelper userInfoHelper;

private String basePath = "/v1/configuration";
private String smvCode = "smv123";


@Test
void getAllConfigsTest() throws Exception{
    RequestBuilder getSmvConfigRequestBuilder = MockMvcRequestBuilders.get(basePath + "/getAllConfigs").accept(MediaType.APPLICATION_JSON);

    when(smvConfigService.getAllConfigs()).thenReturn(SmvConfigSampleData.createSmvDtoData());

    mockMvc.perform(getSmvConfigRequestBuilder).andExpect(status().isOk());
}

@Test
void getSmvConfigBySmvCodeTest() throws Exception{

    RequestBuilder getSmvConfigRequestBuilder = MockMvcRequestBuilders.get(basePath + "/" + smvCode).accept(MediaType.APPLICATION_JSON);

    when(smvConfigService.findBySmvCode(anyString())).thenReturn(SmvConfigSampleData.createSampleDto());

    mockMvc.perform(getSmvConfigRequestBuilder).andExpect(status().isOk());
}

@Test
@WithMockUser(value = "TEST_USER")
void addSmvConfigTest() throws Exception{
    Gson gson = new GsonBuilder()
            .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeSerializer())
            .create();
    SmvDTO smvDto = SmvConfigSampleData.createSampleDto();
    RequestBuilder addSmvConfigRequestBuilder = MockMvcRequestBuilders.post(basePath + "/")
            .contentType(MediaType.APPLICATION_JSON)
            .content(gson.toJson(smvDto))
            .accept(MediaType.APPLICATION_JSON);

    when(smvConfigService.saveSmvConfig(any(), any())).thenReturn(SmvConfigSampleData.createSampleDto());
    mockMvc.perform(addSmvConfigRequestBuilder).andExpect(status().isOk());
}

@Test
@WithMockUser(value = "TEST_USER")
void updateSmvConfigTest() throws Exception{
    Gson gson = new GsonBuilder()
            .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeSerializer())
            .create();
    SmvDTO smvDto = SmvConfigSampleData.createSampleDto();
    RequestBuilder addSmvConfigRequestBuilder = MockMvcRequestBuilders.put(basePath + "/sampleId")
            .contentType(MediaType.APPLICATION_JSON)
            .content(gson.toJson(smvDto))
            .accept(MediaType.APPLICATION_JSON);

    when(smvConfigService.updateConfig(anyString(), any(), any())).thenReturn(SmvConfigSampleData.createSampleDto());
    mockMvc.perform(addSmvConfigRequestBuilder).andExpect(status().isOk());
}

}

I am not sure how to add the UsernamePasswordAuthenticationToken in my requests, I can't do it from postman also.

Have you checked these answers? https://stackoverflow.com/questions/45241566/spring-boot-unit-tests-with-jwt-token-security https://stackoverflow.com/questions/41824885/use-withmockuser-with-springboottest-inside-an-oauth2-resource-server-applic/44502136#44502136

@SveKamenska both those links have faulty answers one is using a custom non recommended way of using JWTs, which is custom JWT filters and handing out JWTs to browsers, the other link you posted is handling if he has users, which he doesnt. The correct answer is in the documentation https://docs.spring.io/spring-security/reference/servlet/test/mockmvc/oauth2.html#testing-jwt where all tools are layed out to test resources servers.

@Toerktumlare, what do you mean by "if he has users, which he doesn't" ? There are users and the purpose of the method under test is to get user info and then pass it further.

read his code .oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(webSecurityHelper.jwtAuthenticationConverter()))); there is no underlying database, that looks up users. The service is stateless and builds a principal from the JWT that gets sent. Which he doesnt. The server is set up as a resource server and you test them by reading the link i posted.

also the exception does not match what his code is saying. The exception tells us he has configured his service using username and password authentication. But his security config has setup the server as a resource server that authorizes requests using jwts. So there information provided does not match.

Thanks @Toerktumlare the document helped and it's working now, can you please tell me how to simulate the same in Postman also.

no i cant good luck

No worries, thanks for the help.
