Интеграционный тест для фильтра аутентификации с загрузкой Spring

#unit-testing #spring-security #spring-boot #mocking

#модульное тестирование #spring-безопасность #весенняя загрузка #издевательство

Вопрос:

Я хотел бы реализовать интеграционный тест для проверки моего фильтра аутентификации, реализованного с помощью Spring Security, с помощью Spring Boot. Но… Я потерялся…

Во-первых, вот моя «производственная» реализация:

У меня есть адаптер веб-конфигуратора, создающий менеджер аутентификации и объявляющий мой фильтр:

 @EnableWebSecurity
public class SecurityConfigurer extends WebSecurityConfigurerAdapter {
    @Autowired
    private IdentityService loginService;
    @Autowired
    private PersonService personService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(loginService).passwordEncoder(new BCryptPasswordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers(PATH_LOGIN).permitAll();
        http.authorizeRequests().antMatchers("/**").fullyAuthenticated();

        http.addFilterBefore(new AuthenticationFilter(PATH_LOGIN, authenticationManager(), personService),
            UsernamePasswordAuthenticationFilter.class);
    }
 

Затем, вот моя реализация фильтра:

 public class AuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    private ObjectMapper objectMapper = new ObjectMapper();

    private PersonService personService;

    protected AuthenticationFilter(String loginPath, AuthenticationManager authenticationManager,
        PersonService personService) {
        super(loginPath);
        this.personService = personService;
        setAuthenticationManager(authenticationManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
        throws AuthenticationException, IOException, ServletException {

        LoginInfo loginInfo = objectMapper.readValue(request.getInputStream(), LoginInfo.class);
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
            loginInfo.getUsername(), loginInfo.getPassword());

        Authentication authentication = getAuthenticationManager().authenticate(usernamePasswordAuthenticationToken);
        SecurityContextHolder.getContext().setAuthentication(authentication);

        return authentication;

    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
        Authentication authResult) throws IOException, ServletException {
        Identity identity = (Identity) authResult.getPrincipal();

        Person person = personService.getPersonByMail(identity.getUsername());

        UserInfo userInfos = new UserInfo();
        userInfos.setUser(person);
        userInfos.setRoles(identity.getRoles());

        objectMapper.writeValue(response.getWriter(), userInfos);
    }
}
 

Теперь я реализовал две службы (PersonService и IdentityService), которые следует использовать как макет для предотвращения любого доступа к базе данных:

 @Profile("test")
@Service
public class PersonServiceMock implements PersonService {

    private static final Map<String, Person> USER_DB;

    static {
        Person valerian = new Student();
        valerian.setMail("valerian@savetheuniverse.com");

        USER_DB = new HashMap<>();
        USER_DB.put(valerian.getMail(), valerian);
    }

    @Override
    public Person getPersonByMail(String mail) {
        return USER_DB.get(mail);
    }

}
 

 @Profile("test")
@Service
public class IdentityServiceMock implements IdentityService {

    private static final Map<String, Identity> USER_DB;

    static {
        Identity valerian = new Identity("valerian@savetheuniverse.com");

        USER_DB = new HashMap<>();
        USER_DB.put(valerian.getUsername(), valerian);

        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();

        USER_DB.forEach((key, value) -> {
            value.setEnabled(true);
            value.setLocked(false);
            value.setPassword(encoder.encode("pa$w0rd"));
        });
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserDetails ud = USER_DB.get(username);
        return ud;
    }
}
 

В конце концов, вот мой «старт теста», который я написал, но это не работает, потому что, похоже, он хочет получить «производственную» реализацию сервиса вместо моей поддельной:

 @ActiveProfiles("test")
@RunWith(SpringRunner.class)
@SpringBootTest
@WebAppConfiguration
public class AuthenticationTests {

    @Autowired
    private Filter filterChainProxy;

    @Autowired
    private WebApplicationContext context;

    private MockMvc mockMvc;

    @Before
    public void before() {
        mockMvc = MockMvcBuilders.webAppContextSetup(context).addFilters(filterChainProxy).build();
}

    @Test
    public void login() throws Exception {

        ObjectMapper objectMapper = new ObjectMapper();
        LoginInfo loginInfo = new LoginInfo("valerian@savetheworld.com", "pa$w0rd");

        MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.post("/login")
            .content(objectMapper.writeValueAsString(loginInfo));

        Person person = new Student("valerian", "none", "valerian@savetheworld.com");
        UserInfo expectedUserInfo = new UserInfo(person, null);

        String expectedJSonContent = objectMapper.writeValueAsString(expectedUserInfo);

        mockMvc.perform(requestBuilder).andExpect(MockMvcResultMatchers.status().isOk())
            .andExpect(MockMvcResultMatchers.content().json(expectedJSonContent));

    }

}
 

Я что-то неправильно понял? Не могли бы вы мне помочь, пожалуйста?

Ответ №1:

ОК. Неважно. Просто я неправильно понял некоторые понятия, такие как mocking, faking amp; stubbing, даже если mocking и stubbing четко связаны в модульных / интеграционных тестах.

Я изменил свой код, чтобы удалить различные интерфейсы и «макет» реализации сервисов. Этот тип реализации больше похож на реализацию «поддельного поведения», чем на издевательство.

В конце концов, у меня есть это для моего тестового класса:

 @RunWith(SpringRunner.class)
@SpringBootTest
@WebAppConfiguration
public class AuthenticationTests {

    private static final String KNOWN_USER_MAIL = "valerian@mail.com";
    private static final String KNOWN_USER_PASSWORD = "pa$w0rd";

    private static Person KNOWN_STUDENT = new Student("valerian", "none", KNOWN_USER_MAIL);
    private static Identity KNWON_IDENTITY = new Identity(KNOWN_USER_MAIL);

    static {
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();

        KNWON_IDENTITY.setEnabled(true);
        KNWON_IDENTITY.setLocked(false);
        KNWON_IDENTITY.setPassword(encoder.encode(KNOWN_USER_PASSWORD));
    }

    @Autowired
    // Attribute name very important
    private Filter springSecurityFilterChain;

    @Autowired
    private WebApplicationContext context;

    @MockBean // IdentityService automatically mocked when used
    private IdentityService identityService;

    @MockBean // PersonService automatically mocked when used
    private PersonService personService;

    private MockMvc mockMvc;

    @Before
    public void before() {

        mockMvc = MockMvcBuilders.webAppContextSetup(context).addFilters(springSecurityFilterChain).build();

        // Stub to define the behaviour of the services when they are used
        Mockito.when(identityService.loadUserByUsername(KNOWN_USER_MAIL)).thenReturn(KNWON_IDENTITY);
        Mockito.when(personService.getPersonByMail(KNOWN_USER_MAIL)).thenReturn(KNOWN_STUDENT);
    }

    @Test
    public void login_success() throws Exception {

        ObjectMapper objectMapper = new ObjectMapper();
        LoginInfo loginInfo = new LoginInfo(KNOWN_USER_MAIL, KNOWN_USER_PASSWORD);

        MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.post("/login")
                .content(objectMapper.writeValueAsString(loginInfo));

        UserInfo expectedUserInfo = new UserInfo(KNOWN_STUDENT, KNWON_IDENTITY.getRoles());

        String expectedJSonContent = objectMapper.writeValueAsString(expectedUserInfo);
        mockMvc.perform(requestBuilder).andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.content().json(expectedJSonContent));

   }

}
 

Я впечатлен магией аннотации @MockBean и заглушек. 🙂