spring-projects / spring-security

Spring Security
http://spring.io/projects/spring-security
Apache License 2.0
8.72k stars 5.86k forks source link

SEC-3174: Combine Spring Security Test and MockMVC throws IllegalStateException #3383

Closed spring-projects-issues closed 8 years ago

spring-projects-issues commented 8 years ago

Rémi Doolaeghe (Migrated from SEC-3174) said:

I try to write a unit test for a controller in Spring MVC, involving Spring Security. For isolation purpose, I need to mock the service present in a Rest controller.

To do this, I use this piece of code:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = EasyCook.class)
@WebIntegrationTest
public class RecipesControllerCan  {

    @Mock
    private RecipesService recipesService;

    @InjectMocks
    private RecipesController controller;

    @Autowired
    private WebApplicationContext wac;

    @Before
    public void startMocks(){
        controller = wac.getBean(RecipesController.class);

        MockMvc mockMvc = MockMvcBuilders
                .standaloneSetup(controller)
                .alwaysDo(print())
                .apply(SecurityMockMvcConfigurers.springSecurity())
                .build();

        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void testMe(){
    }
}

This leads to this result:

java.lang.IllegalStateException: springSecurityFilterChain cannot be null. Ensure a Bean with the name springSecurityFilterChain implementing Filter is present or inject the Filter to be used. at org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurer.beforeMockMvcCreated(SecurityMockMvcConfigurer.java:62) at org.springframework.test.web.servlet.setup.AbstractMockMvcBuilder.build(AbstractMockMvcBuilder.java:133) at fr.easycompany.easycook.springboot.controllers.RecipesControllerCan.startMocks(RecipesControllerCan.java:51)

For more details, I joined the project simplified as much as possible to reproduce the issue.

spring-projects-issues commented 8 years ago

Rob Winch said:

You need to ensure a bean by the name of "springSecurityFilterChain" is available if you do not explicitly provide a FilterChainProxy. In the example that was provided, a standalone setup is being used which means MockMvc will not be aware of the WebApplicationContext (and thus not be able to look up the "springSecurityFilterChain" bean).

The easiest way to resolve this is to use something like this:

        MockMvc mockMvc = MockMvcBuilders
                // replace standaloneSetup with line below
                .webAppContextSetup(wac)
                .alwaysDo(print())
                .apply(SecurityMockMvcConfigurers.springSecurity())
                .build();

If you really want to use a standaloneSetup (doesn't really make sense since you already have a WebApplicationContext), you can explicitly provide the springSecurityFilterChain using:

    @Autowired
    FilterChainProxy springSecurityFilterChain;

    @Before
    public void startMocks(){
        controller = wac.getBean(RecipesController.class);

        MockMvc mockMvc = MockMvcBuilders
                .standaloneSetup(controller)
                .alwaysDo(print())
                .apply(SecurityMockMvcConfigurers.springSecurity(springSecurityFilterChain))
                .build();

        MockitoAnnotations.initMocks(this);
    }
spring-projects-issues commented 8 years ago

Rob Winch said:

I'm resolving this as works as designed per my previous comment.

ezraroi commented 8 years ago

If it is not an integration test, looks like the springSecurityFilterChain is not defined.

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@SpringApplicationConfiguration(classes = Application.class)
@ContextConfiguration(classes = MockServletContext.class)
public class LockControllerTest {

@Before
    public void setup() {
        MockitoAnnotations.initMocks(this);
        mockMvc = MockMvcBuilders
            .webAppContextSetup(wac)
            .apply(SecurityMockMvcConfigurers.springSecurity())
            .alwaysDo(print())
            .build();

//        mockMvc = MockMvcBuilders.standaloneSetup(lockController).apply(springSecurity()).build();
    }
lukkuz commented 7 years ago

@ezraroi Were you able to overcome this odd behavior somehow? I'm facing same issue with my unit test.

rwinch commented 7 years ago

@lukkuz Did you try my suggestion in https://github.com/spring-projects/spring-security/issues/3383#issuecomment-180607733

rwinch commented 7 years ago

@ezraroi If the configuration does not load the Spring Security configuration, then there is no security to apply. There must be a bean by the name of springSecurityFilterChain.

If you want a unit test, then there is no need to use the security features of MockMvc because Spring Security will not be invoked.

kappmeier commented 7 years ago

@rwinch I am currently unit testing a controller which has a method like this:

public String doSomething(@AuthenticationPrincipal Authentication authentication) {
...
}

I really just like to have a mocked user in that method in a standalone setup, no need for 'real' security.

        mockMvc.perform(MockMvcRequestBuilders.post("/doSomething")
                .with(authentication(authentication))
                .andExpect(status().isOk());

This fails, the authentication is not available within the method handling the request in the controller.

However, I do not know where to get the springSecurityFilterChain to use, as I have no context. I tried to mock it, but apparently it filters out every request then:

    @Mock
    private FilterChainProxy springSecurityFilterChain;    

    @BeforeTest
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);

        mockMvc = standaloneSetup(controller)
                .apply(SecurityMockMvcConfigurers.springSecurity(springSecurityFilterChain))                
                .setViewResolvers(viewResolver())
                .build();
    }
rwinch commented 7 years ago

@kappmeier The post processors are really only intended to work with Spring Security's FilterChainProxy added to the MockMvc instance. If you need to run the test without Spring Security's FilterChainProxy, then you can just set the SecurityContextHolder. For example:

@WithMockUser
@Test
public void doSomething() {
    mockMvc.perform(post("/doSomething")
                .andExpect(status().isOk());
}

or

@Test
public void doSomething() {
    SecurityContextHolder.getContext().setAuthentication(authentication);

    mockMvc.perform(post("/doSomething")
                .andExpect(status().isOk());
}
syakovyn commented 7 years ago

I'm trying the second example, but the controller method's parameter is being initialized with a newly created Principal (User) instead of the one I set in SecurityContext. I can see that AuthenticationPrincipalArgumentResolver is not added to the list of argument resolvers. How can I make it appear in that list?

syakovyn commented 7 years ago

Fixed by adding AuthenticationPrincipalArgumentResolver to the list of resolvers:

mockMvc = MockMvcBuilders.standaloneSetup(controller)
        .setCustomArgumentResolvers(new AuthenticationPrincipalArgumentResolver()).build()