Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Integration test for authentication filter with Spring Boot

I would like to implement an integration test to test my authentication filter, implemented with Spring Security, with Spring Boot. But... I am lost...

First, here is my "production" implementation:

I have my web configurer adapter creating an authentication manager and declaring my filter:

@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);
    }

Then, here is my filter implementation:

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

Now, I have implemented the two services (PersonService & IdentityService) which should be used as mock to prevent any database access:

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

    private static final Map<String, Person> USER_DB;

    static {
        Person valerian = new Student();
        valerian.setMail("[email protected]");

        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("[email protected]");

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

In the end, here is my "start of test" I wrote but that does not work because it seems it wants to retrieve the "production" implementation of the service instead of my fake one:

@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("[email protected]", "pa$$w0rd");

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

        Person person = new Student("valerian", "none", "[email protected]");
        UserInfo expectedUserInfo = new UserInfo(person, null);

        String expectedJSonContent = objectMapper.writeValueAsString(expectedUserInfo);

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

    }

}

Did I misunderstood something? Can you help me, please?

like image 649
tlegrand Avatar asked Oct 16 '16 14:10

tlegrand


1 Answers

OK. Never mind. It is just that I misunderstood some notion like mocking, faking & stubbing, even if mocking and stubbing are clearly linked in the unit/integration tests.

I modified my code to remove the different interfaces and the "mock" implementation of the services. This type of implementation is more like a "fake-behaviour" implementation than mocking.

In the end, I have this for my test class:

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

    private static final String KNOWN_USER_MAIL = "[email protected]";
    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));

   }

}

I am impressed by the magic of the annotation @MockBean and the stubs. :)

like image 159
tlegrand Avatar answered Dec 06 '22 14:12

tlegrand