Reputation: 2725
following the documentation about using Spring Security Test to write tests for a spring MVC app that is wired behind Spring Security.
This is a vanilla spring-boot application employing a typical spring-security wiring. Here's the main Application.java
@SpringBootApplication
public class Application {
private static final Logger log = LoggerFactory.getLogger(Application.class);
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}
Here's the wiring for the spring-security:
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
@Qualifier("customUserDetailsService")
UserDetailsService userDetailsService;
@Autowired
public void configureGlobalSecurity(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/","/sign_up").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf()
.and()
.exceptionHandling()
.accessDeniedPage("/access_denied")
.and()
.logout()
.permitAll();
}
}
As you may see, all requests require to be authenticated except for those for "/" and "/sign_up". I have verified by deploying the application that the authentication schemes work just fine.
Now comes the interesting part: writing spring mvc tests. The link I provided gives some nice ways to write such tests where the spring-security-test framework allows inserting mock users/security-contexts. I've taken the approach of
The code for 1. is as follows:
@WithSecurityContext(factory=WithMockUserDetailsSecurityContextFactory.class)
public @interface WithMockUserDetails {
String firstName() default "apil";
String lastName() default "tamang";
String password() default "test";
long id() default 999;
String email() default "[email protected]";
}
The code for 2. is as follows:
final class WithMockUserDetailsSecurityContextFactory
implements WithSecurityContextFactory<WithMockUserDetails>{
@Override
public SecurityContext createSecurityContext(WithMockUserDetails mockUserDetails) {
/*
* Use an anonymous implementation for 'UserDetails' to return a
* mock authentication object, which is then set to the SecurityContext
* for the test runs.
*/
UserDetails principal=new UserDetails() {
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
//another anonmyous interface implementation.
GrantedAuthority auth=new GrantedAuthority() {
@Override
public String getAuthority() {
return "ROLE_USER";
}
};
List<GrantedAuthority> authorities=new ArrayList<>();
authorities.add(auth);
return authorities;
}
@Override
public String getPassword() {
return mockUserDetails.password();
}
@Override
public String getUsername() {
return mockUserDetails.email();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
};
Authentication authentication=new
UsernamePasswordAuthenticationToken(principal,principal.getPassword(),principal.getAuthorities());
SecurityContext context= SecurityContextHolder.createEmptyContext();
context.setAuthentication(authentication);
return context;
}
}
Finally, here's the test class:
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = {Application.class})
@WebAppConfiguration
public class UserControllerTest {
@Autowired
private WebApplicationContext context;
@Autowired
private Filter springSecurityFilterChain;
private MockMvc mvc;
@Before
public void setup(){
mvc= MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
}
@Test
public void testRootIsOk() throws Exception {
mvc.perform(get("/"))
.andExpect(status().isOk());
}
@Test
public void expectRedirectToLogin() throws Exception {
mvc.perform(get("/testConnect"))
.andExpect(redirectedUrl("http://localhost/login"));
}
@Test
@WithMockUserDetails
public void testAuthenticationOkay() throws Exception {
mvc.perform(get("/testConnect"))
.andExpect(content().string("good request."));
}
}
Output of Test Run:
Very likely Test 3 failed because of the 'SecurityContext' never got appropriately populated. As per the documentation, it should have worked. Not sure what I missed. Any help is much appreciated.
Upvotes: 3
Views: 3659
Reputation:
You'll need to add
@Retention(RetentionPolicy.RUNTIME)
to your custom annotation in order to retain annotation information at runtime. Otherwise WithSecurityContextTestExecutionListener
can't detect your annotation.
Upvotes: 2