Mock SecurityContextHolder / Аутентификация всегда возвращает ноль

Я знаю, что этот вопрос часто задают, но, может быть, у меня есть кое-что особенное к этому. Я пытаюсь сделать некоторые интеграционные тесты в приложении Spring Boot, которое поддерживает REST (не Spring MVC) и по какой-то причине SecurityContextHolder.getContext().getAuthentication() всегда возвращает ноль, даже при использовании @WithMockUser на тесте. Я не уверен, связано ли это с использованием профилей в классах конфигурации, но пока у нас не было проблем с этим.

Учебный класс

@Override
public ResponseEntity<EmployeeDTO> meGet() {
    Principal principal = SecurityContextHolder.getContext().getAuthentication();
    logger.debug("Endpoint called: me({})", principal);
    EmployeeDTO result;

    // Get user email from security context
    String email = principal.getName(); // NPE here

// ...
}

Тестовое задание

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
        properties = {"eureka.client.enabled:false"})
@WithMockUser
@ActiveProfiles(value = "test")
public class MeControllerTest extends IntegrationSpringBootTest {

@Autowired
private TestRestTemplate restTemplate;

@MockBean
private SecurityContext securityContext;

@MockBean
private Authentication authentication;

@MockBean
private EmployeeRepository employeeRepository;

@BeforeClass
public static void setUp() {

}

@Before
@Override
public void resetMocks() {
    reset(employeeRepository);
}

@Test
public void meGet() throws Exception {
    when(securityContext.getAuthentication()).thenReturn(authentication);
    securityContext.setAuthentication(authentication);
    when(authentication.getPrincipal()).thenReturn(mockEmployee());
    SecurityContextHolder.setContext(securityContext);
    when(employeeRepository.findByEmail(anyString())).thenReturn(mockEmployee());

    ResponseEntity<EmployeeDTO> employeeDTOResponseEntity =
            this.restTemplate.getForEntity("/me", EmployeeDTO.class);
// ...
}

Если я верну макет Principal вместо mockEmployee() тест не может даже начаться, потому что это происходит:

org.springframework.beans.factory.BeanCreationException: Could not inject field: private org.springframework.security.core.Authentication com.gft.employee.controller.MeControllerTest.authentication; nested exception is org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named 'org.springframework.security.core.Authentication#0' is expected to be of type 'org.springframework.security.core.Authentication' but was actually of type '$java.security.Principal$$EnhancerByMockitoWithCGLIB$$657040e6'

Дополнительные пояснения: это приложение Spring Boot также использует OAuth2 для авторизации, но для этих тестов его необходимо отключить. Вот почему мы используем профили. Опуская @ActiveProfiles аннотация дает нам 401 несанкционированную ошибку по запросу конечной точки.

Я мог бы использовать PowerMock, но я хотел бы избежать этого, если это возможно.

3 ответа

Решение

Я в конечном итоге с помощью MockMvc несмотря на то, что приложение не основано на Spring MVC. Кроме того, я отделил SecurityContext звонки в другой сервис, но прежде чем сделать это, я могу утверждать, что @WithMockUser аннотация работала правильно.

Для этого нужно использовать следующие фрагменты на уровне класса:

@WebMvcTest(MeController.class)
@Import({ControllerConfiguration.class, BeanConfiguration.class})
public class MeControllerTest {
    // ...
}

С помощью @WebMvcTest облегчает отсутствие необходимости инициализировать SecurityContext на первом месте. Вам даже не нужно звонить springSecurity(), Вы можете просто mockMvc.perform() как обычно, и любые звонки на SecurityContext вернет любого пользователя, которого вы указали, либо с @WithMockUser или издеваться над сервисом, который обрабатывает такой вызов.

Более простым способом написания Junit для аутентификации SecurityContextHolder было бы издеваться над ними. Ниже приводится рабочая реализация этого. Вы можете добавить фиктивные классы в соответствии с вашими потребностями, а затем установить контекст SecurityContextHolder и затем использовать when() для дальнейшей имитации и возврата правильного значения макета.

    AccessToken mockAccessToken = mock(AccessToken.class);
    Authentication authentication = mock(Authentication.class);
    SecurityContext securityContext = mock(SecurityContext.class);

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

    SecurityContextHolder.setContext(securityContext);

    when(SecurityContextHolder.getContext().getAuthentication().getDetails()).thenReturn(mockSimpleUserObject);

Этот пример кода мне подходит. Этот код использует .

      @SpringBootTest(classes = Application.class)
@AutoConfigureMockMvc //need this in Spring Boot test
public class LoginControllerIntegrationTest {

    // mockMvc is not @Autowired because I am customizing it @BeforeEach
    private MockMvc mockMvc;

    @Autowired
    private WebApplicationContext context;

    @Mock
    DefaultOidcUser principal;

    @BeforeEach
    public void beforeEach() {
        Authentication authentication = mock(OAuth2AuthenticationToken.class);
        // Mockito.whens() for your authorization object
        SecurityContext securityContext = mock(SecurityContext.class);
        when(securityContext.getAuthentication()).thenReturn(authentication);
        when(authentication.getPrincipal()).thenReturn(principal);
        SecurityContextHolder.setContext(securityContext);
        // setting mockMvc with custom securityContext
        this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context).build();
    }

    @Test
    public void given_any_OAuth2AuthenticationToken_when_login_then_redirect_to_logout() throws Exception {

        final String loginName = "admin";
        // given
        // manipulate the principal as needed
        when(principal.getAttribute("unique_name")).thenReturn(loginName);

        // @formatter:off
        // when
        this.mockMvc.perform(get("/login"))
                .andDo(print())
        //then
                .andExpect(status().isFound())
                .andExpect(redirectedUrl("/logout"));
        // @formatter:off
    }
}
Другие вопросы по тегам