MockMvc @AuthenticationPrincipal NullPointerException

Пишу тесты для приложения на Spring Boot и Spring Mvc, используя MockMvc.

При попытке протестировать этот метод, где через аннотацию @AuthenticationPrincipal PersonDetails pd извлекается объект пользователя и затем на нем вызывается метод (pd.getPerson()), получаю NullPointerException.

А именно тестирую этот метод:

    @GetMapping("/edit/{id}")
    public String editPage(@PathVariable long id, Model model, @AuthenticationPrincipal PersonDetails pd) {
        Task task = taskService.findById(id);
        if (task.getAuthor() == null || task.getAuthor().getId() != pd.getPerson().getId()) {
            throw new MismatchIdentifierException("ID автора не совпадает с Вашим. Похоже, что Вы не являетесь автором.");
        }
        model.addAttribute("task", task);
        model.addAttribute("executors", personDetailsService.findAll());
        return "editTask";
    }

этим тестом:

@Test
    @DisplayName("Get request at '/tasks/edit/5' should success")
    @WithMockUser("testuser")
    public void givenGetEditRequest_shouldReturnEditPage() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/tasks/edit/5").accept(MediaType.TEXT_HTML))
        .andExpect(status().isOk()).andExpect(view().name("editTask")).andExpect(model().attribute("task", task))
        .andExpect(model().attribute("executors", personDetailsService.findAll()));
    }

и получаю ошибку:

Caused by: java.lang.NullPointerException: Cannot invoke "ru.terentyev.EffectiveMobile.security.PersonDetails.getPerson()" because "pd" is null

Хотя следующий метод, где тоже есть @AuthenticationPrincipal PersonDetails pd и где на нем тоже вызывается метод pd.getPerson() проходит тест успешно:

    @PostMapping("/add")
    public String addTask(@ModelAttribute @Valid Task task, BindingResult br, Model model
            , @AuthenticationPrincipal PersonDetails pd) {
        if (br.hasErrors()) {
            model.addAttribute("executors", personDetailsService.findAll());
            return "addTask";       
        }
        task.setAuthor(pd.getPerson());
        return showTask(taskService.save(task).getId(), 0, model, pd);

    }

этим тестом:

    @Test
    @DisplayName("Post request at '/tasks/add' should receive new Task object")
    @WithMockUser("testuser")
    public void givenPostAddRequest_shouldReceiveNewObject() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.post("/tasks/add").with(csrf()).contentType(MediaType.TEXT_PLAIN)
        .flashAttr("task", task)).andExpect(status().isOk());
        
        
    }

Пробовал добавлять with(csrf()) (хоть это вроде и не нужно) - не работает.

Часть тестового класса:

@SpringBootTest
//@WebMvcTest(TaskController.class)
@ContextConfiguration(classes = EffectiveMobileApplication.class)
@WebAppConfiguration
@AutoConfigureMockMvc
@ExtendWith(MockitoExtension.class)
public class TaskControllerTest {

    @Autowired
    private MockMvc mockMvc;
    
    @InjectMocks
    private TaskController taskController;
    
    @Mock
    private Model model;
    
    @Spy
    private List<Person> people;
    
    @MockBean
    private TaskService taskService;
    
    @MockBean
    private PersonDetailsService personDetailsService;
    
    
    @Mock
    private Task newCorrectTask;
    @Spy
    private Task task;
    private Person person;
    private Comment comment;
    
    @BeforeEach
    public void init() {
        MockitoAnnotations.openMocks(this);
        people = new ArrayList<Person>();
        person = new Person();
        person.setId(5l);
        person.setName("Dmitriy");
        person.setPassword("randompassword");
        person.setEmail("[email protected]");  
        task = new Task();
        task.setAuthor(person);
        task.setExecutor(person);
        task.setId(5l);
        task.setStatus(Status.PROCESSING);
        task.setPriority(Priority.HIGH);    
        Mockito.when(taskService.findById(5)).thenReturn(task);
        Mockito.when(personDetailsService.findAll()).thenReturn(people);

    }

    // ...
}

Спасибо.


Ответы (1 шт):

Автор решения: LowPassenger

После четырёхчасового перекапывания StackOverFlow, где чего только не советовали, и аннотации, и создание класса-наследника Principal, в нем переопределение getName() с произвольной строкой внутри, и кучу бесполезного хлама, ChatGPT решил всё за минуту: В тестовом классе сначала

@BeforeEach
    public void setUp() {
        UserDetails userDetails = User.withUsername("user")
                .password("password")
                .roles("USER")
                .build();
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
    }

А потом этот authenticationToken засылаем в тестируемый метод вместо замоканного principal:

mockMvc.perform(get("https://mysite.com/units")
                        .principal(authenticationToken))
                .andExpect(status().isOk());
→ Ссылка