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
}
}