Why does MockMvc in return 404 error on valid path with Spring Security enabled?

Viewed 4022

I'm trying to test my Spring Boot REST Controller with MockMvc. Following is my test class:

UserControllerImplTest (version 1)

@SpringBootTest
@AutoConfigureMockMvc
class UserControllerImplTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private UserDTOService userDTOService;

    @MockBean
    private ImageDTOService imageDTOService;

    @Test
    public void whenUsers_shouldReturnUsers() throws Exception {

        UserDTO user1 = getSampleUser("user1");
        UserDTO user2 = getSampleUser("user2");
        UserDTO user3 = getSampleUser("user3");
        List<UserDTO> users = List.of(user1, user2, user3);

        Mockito.when(userDTOService.getAll()).thenReturn(users);

        mockMvc.perform(get("/user"))
                .andExpect(status().isOk())
                .andExpect(content().string(users.toString()));
    }

    private UserDTO getSampleUser(String username) {
        return UserDTO.builder()
                .username(username)
                .email(username + "@example.com")
                .password("password")
                .registerTime(LocalDateTime.now())
                .isActive(true)
                .build();
    }
}

Controller to be tested: UserController:

@Api(tags={"User info"})
@RequestMapping("/user")
public interface UserController {

    @ApiOperation(value = "Returns list of users")
    @GetMapping("/")
    @PreAuthorize("hasRole('ROLE_ADMIN')")
    ResponseEntity<List<UserDTO>> users();

    @ApiOperation(value = "Returns single user")
    @GetMapping("/{username}")
    ResponseEntity<UserDTO> userInfo(@PathVariable String username);

    @ApiOperation(value = "Returns list of images uploaded by user authenticated with session cookie")
    @GetMapping("/images")
    @PreAuthorize("isFullyAuthenticated()")
    ResponseEntity<List<ImageDTO>> userImages(@AuthenticationPrincipal CustomUserDetails userDetails
    );
}

its implementation: UserControllerImpl

@Service
@RequiredArgsConstructor
public class UserControllerImpl implements UserController {

    private final UserDTOService userDTOService;
    private final ImageDTOService imageDTOService;

    @Override
    public ResponseEntity<List<UserDTO>> users() {
        return ResponseEntity.status(HttpStatus.OK).body(List.of(UserDTO.builder().username("aaa").build()));
//        Optional<List<UserDTO>> users = Optional.ofNullable(userDTOService.getAll());
//        if (users.isEmpty()) {
//            return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
//        }
//
//        return ResponseEntity.status(HttpStatus.OK).body(users.get());
    }

    @Override
    public ResponseEntity<UserDTO> userInfo(String username) {
        Optional<UserDTO> requestedUser = Optional.ofNullable(userDTOService.findByUsername(username));
        if (requestedUser.isEmpty()) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
        }

        return ResponseEntity.status(HttpStatus.OK).body(requestedUser.get());
    }

    @Override
    public ResponseEntity<List<ImageDTO>> userImages(@AuthenticationPrincipal CustomUserDetails userDetails) {
        UserDTO user = userDetails.getUser();

        Optional<List<ImageDTO>> images = Optional.ofNullable(imageDTOService.findAllUploadedBy(user));
        if (images.isEmpty()) {
            return ResponseEntity.status(HttpStatus.OK).body(Collections.emptyList());
        }

        return ResponseEntity.status(HttpStatus.OK).body(images.get());
    }
}

The method I'm invoked in the controller (users()) was modified, just to be sure it'll always return 200.

The test fails because of:

java.lang.AssertionError: Status expected:<200> but was:<404>
Expected :200
Actual   :404

I had a strong suspicion that the Filter Bean I defined might be a culprit here.

AuthCookieFilter

@RequiredArgsConstructor
@Component
public class AuthCookieFilter extends GenericFilterBean {

    private final SessionDTOService sessionDTOService;
    private final AppConfig appConfig;

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

        Optional<String> sessionId = Optional.ofNullable(extractAuthCookie((HttpServletRequest) servletRequest));

        if (sessionId.isPresent()) {
            Optional<SessionDTO> sessionDTO = Optional.ofNullable(sessionDTOService.findByIdentifier(sessionId.get()));

            if (sessionDTO.isPresent()) {
                UserDTO userDTO = sessionDTO.get().getUser();
                CustomUserDetails customUserDetails = new CustomUserDetails(userDTO);

                SecurityContextHolder.getContext().setAuthentication(new UserAuthentication(customUserDetails));
            }
        }

        HttpServletResponse response = (HttpServletResponse) servletResponse;
        response.setHeader("Access-Control-Allow-Origin", appConfig.getCorsHosts());
        response.setHeader("Access-Control-Allow-Methods", "POST, PUT, GET, OPTIONS, DELETE");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Headers", "Access-Control-Allow-Origin, Authorization, Content-Type, Cache-Control");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        filterChain.doFilter(servletRequest, response);
    }

    public static String extractAuthCookie(HttpServletRequest request) {
        List<Cookie> cookies = Arrays.asList(Optional.ofNullable(request.getCookies()).orElse(new Cookie[0]));

        if (!cookies.isEmpty()) {
            Optional<Cookie> authCookie = cookies.stream()
                    .filter(cookie -> cookie.getName().equals("authentication"))
                    .findFirst();

            if (authCookie.isPresent()) {
                return authCookie.get().getValue();
            }
        }

        return null;
    }
}

So I decided to configure MockMvc manually, explicitly adding it to the context: UserControllerImplTest (version 2)

@SpringBootTest
@WebAppConfiguration
class UserControllerImplTest {

    private MockMvc mockMvc;

    @Autowired
    private WebApplicationContext wac;

    @Autowired
    private AuthCookieFilter authCookieFilter;

    @MockBean
    private UserDTOService userDTOService;

    @MockBean
    private ImageDTOService imageDTOService;

    @BeforeEach
    public void setUp() {
        mockMvc = MockMvcBuilders.webAppContextSetup(wac)
                .addFilter(authCookieFilter).build();
    }

    @Test
    public void whenUsers_shouldReturnUsers() throws Exception {

        UserDTO user1 = getSampleUser("user1");
        UserDTO user2 = getSampleUser("user2");
        UserDTO user3 = getSampleUser("user3");
        List<UserDTO> users = List.of(user1, user2, user3);

        Mockito.when(userDTOService.getAll()).thenReturn(users);

        mockMvc.perform(get("/user"))
                .andExpect(status().isOk())
                .andExpect(content().string(users.toString()));
    }

    private UserDTO getSampleUser(String username) {
        return UserDTO.builder()
                .username(username)
                .email(username + "@example.com")
                .password("password")
                .registerTime(LocalDateTime.now())
                .isActive(true)
                .build();
    }
}

But had no success, still getting 404s.

I also considered limiting the test scope to bringing up only the Web layer, by using @WebMvcTest, but my AuthCookieFilter, as you can see above, requires full Spring context to be up.

I noticed, that debugger stops inside doFilter of AuthCookieFilter when I put a breakpoint there, disregarding whether MockMvc is configured automatically or manually. However I cannot reach a breakpoint set inside UserControllerImpl.users().

Web security is enabled in this app and that's were additional filter is being added: SecurityConfiguration.class

@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
@Configuration
@RequiredArgsConstructor
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    private final CustomUserDetailsService userDetailsService;

    private final CustomLogoutSuccessHandler customLogoutSuccessHandler;

    private final AuthCookieFilter authCookieFilter;

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

    @Bean
    @Override
    protected AuthenticationManager authenticationManager() {
        return authentication -> {
            throw new AuthenticationServiceException("Cannot authenticate " + authentication);
        };
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .csrf().disable()
                .logout(configurer -> {
                    configurer.addLogoutHandler(new HeaderWriterLogoutHandler(
                        new ClearSiteDataHeaderWriter(ClearSiteDataHeaderWriter.Directive.ALL)
                    ));
                    configurer.logoutSuccessHandler(customLogoutSuccessHandler);
                    configurer.logoutUrl("/auth/logout");
                    configurer.deleteCookies("authentication");
                })
                .exceptionHandling(configurer -> configurer.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)))
                .addFilterAfter(authCookieFilter, SecurityContextPersistenceFilter.class)
                .authorizeRequests()
                .antMatchers("/auth/*").permitAll()
                .and()
                .formLogin().permitAll()
        ;
    }

    @Bean
    public PasswordEncoder getPasswordEncoder() {
        String defaultEncodingId = "argon2";
        Map<String, PasswordEncoder> encoders = new HashMap<>();
        encoders.put(defaultEncodingId, new Argon2PasswordEncoder(16, 32, 8, 1 << 16, 4));
        return new DelegatingPasswordEncoder(defaultEncodingId, encoders);
    }
}

Here are logs of the application start-up and test execution:

21:40:56.203 [main] DEBUG org.springframework.test.context.BootstrapUtils - Instantiating CacheAwareContextLoaderDelegate from class [org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate]
21:40:56.210 [main] DEBUG org.springframework.test.context.BootstrapUtils - Instantiating BootstrapContext using constructor [public org.springframework.test.context.support.DefaultBootstrapContext(java.lang.Class,org.springframework.test.context.CacheAwareContextLoaderDelegate)]
21:40:56.228 [main] DEBUG org.springframework.test.context.BootstrapUtils - Instantiating TestContextBootstrapper for test class [com.ewsie.allpic.user.controller.impl.UserControllerImplTest] from class [org.springframework.boot.test.context.SpringBootTestContextBootstrapper]
21:40:56.237 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Neither @ContextConfiguration nor @ContextHierarchy found for test class [com.ewsie.allpic.user.controller.impl.UserControllerImplTest], using SpringBootContextLoader
21:40:56.240 [main] DEBUG org.springframework.test.context.support.AbstractContextLoader - Did not detect default resource location for test class [com.ewsie.allpic.user.controller.impl.UserControllerImplTest]: class path resource [com/ewsie/allpic/user/controller/impl/UserControllerImplTest-context.xml] does not exist
21:40:56.240 [main] DEBUG org.springframework.test.context.support.AbstractContextLoader - Did not detect default resource location for test class [com.ewsie.allpic.user.controller.impl.UserControllerImplTest]: class path resource [com/ewsie/allpic/user/controller/impl/UserControllerImplTestContext.groovy] does not exist
21:40:56.240 [main] INFO org.springframework.test.context.support.AbstractContextLoader - Could not detect default resource locations for test class [com.ewsie.allpic.user.controller.impl.UserControllerImplTest]: no resource found for suffixes {-context.xml, Context.groovy}.
21:40:56.241 [main] INFO org.springframework.test.context.support.AnnotationConfigContextLoaderUtils - Could not detect default configuration classes for test class [com.ewsie.allpic.user.controller.impl.UserControllerImplTest]: UserControllerImplTest does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
21:40:56.276 [main] DEBUG org.springframework.test.context.support.ActiveProfilesUtils - Could not find an 'annotation declaring class' for annotation type [org.springframework.test.context.ActiveProfiles] and class [com.ewsie.allpic.user.controller.impl.UserControllerImplTest]
21:40:56.320 [main] DEBUG org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider - Identified candidate component class: file [/home/max/Projects/allpic/allpic-backend/target/classes/com/ewsie/allpic/AllpicApplication.class]
21:40:56.321 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Found @SpringBootConfiguration com.ewsie.allpic.AllpicApplication for test class com.ewsie.allpic.user.controller.impl.UserControllerImplTest
21:40:56.382 [main] DEBUG org.springframework.boot.test.context.SpringBootTestContextBootstrapper - @TestExecutionListeners is not present for class [com.ewsie.allpic.user.controller.impl.UserControllerImplTest]: using defaults.
21:40:56.382 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Loaded default TestExecutionListener class names from location [META-INF/spring.factories]: [org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener, org.springframework.boot.test.mock.mockito.ResetMocksTestExecutionListener, org.springframework.boot.test.autoconfigure.restdocs.RestDocsTestExecutionListener, org.springframework.boot.test.autoconfigure.web.client.MockRestServiceServerResetTestExecutionListener, org.springframework.boot.test.autoconfigure.web.servlet.MockMvcPrintOnlyOnFailureTestExecutionListener, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverTestExecutionListener, org.springframework.test.context.web.ServletTestExecutionListener, org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener, org.springframework.test.context.support.DependencyInjectionTestExecutionListener, org.springframework.test.context.support.DirtiesContextTestExecutionListener, org.springframework.test.context.transaction.TransactionalTestExecutionListener, org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener, org.springframework.test.context.event.EventPublishingTestExecutionListener, org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener, org.springframework.security.test.context.support.ReactorContextTestExecutionListener]
21:40:56.391 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Using TestExecutionListeners: [org.springframework.test.context.web.ServletTestExecutionListener@7e22550a, org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener@45e37a7e, org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener@62452cc9, org.springframework.boot.test.autoconfigure.SpringBootDependencyInjectionTestExecutionListener@6941827a, org.springframework.test.context.support.DirtiesContextTestExecutionListener@5a7005d, org.springframework.test.context.transaction.TransactionalTestExecutionListener@5bc9ba1d, org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener@1021f6c9, org.springframework.test.context.event.EventPublishingTestExecutionListener@7516e4e5, org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener@488eb7f2, org.springframework.security.test.context.support.ReactorContextTestExecutionListener@5e81e5ac, org.springframework.boot.test.mock.mockito.ResetMocksTestExecutionListener@4189d70b, org.springframework.boot.test.autoconfigure.restdocs.RestDocsTestExecutionListener@3fa2213, org.springframework.boot.test.autoconfigure.web.client.MockRestServiceServerResetTestExecutionListener@3e7634b9, org.springframework.boot.test.autoconfigure.web.servlet.MockMvcPrintOnlyOnFailureTestExecutionListener@6f0b0a5e, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverTestExecutionListener@6035b93b]
21:40:56.393 [main] DEBUG org.springframework.test.context.support.AbstractDirtiesContextTestExecutionListener - Before test class: context [DefaultTestContext@2a8d39c4 testClass = UserControllerImplTest, testInstance = [null], testMethod = [null], testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@25b2cfcb testClass = UserControllerImplTest, locations = '{}', classes = '{class com.ewsie.allpic.AllpicApplication}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[[ImportsContextCustomizer@72758afa key = [org.springframework.boot.test.autoconfigure.web.servlet.MockMvcAutoConfiguration, org.springframework.boot.test.autoconfigure.web.servlet.MockMvcWebClientAutoConfiguration, org.springframework.boot.test.autoconfigure.web.servlet.MockMvcWebDriverAutoConfiguration, org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration, org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration, org.springframework.boot.test.autoconfigure.web.servlet.MockMvcSecurityConfiguration]], org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@30b6ffe0, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@14f232c4, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@d324b662, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@f627d13, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@4b3fa0b3, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@479cbee5], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.web.ServletTestExecutionListener.activateListener' -> true]], class annotated with @DirtiesContext [false] with mode [null].
21:40:56.409 [main] DEBUG org.springframework.test.context.support.TestPropertySourceUtils - Adding inlined properties to environment: {spring.jmx.enabled=false, org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true, server.port=-1}
2020-09-13 21:40:56.599  INFO 52850 --- [           main] c.e.a.u.c.impl.UserControllerImplTest    : Starting UserControllerImplTest on Oyashiro-sama with PID 52850 (started by max in /home/max/Projects/allpic/allpic-backend)
2020-09-13 21:40:56.599  INFO 52850 --- [           main] c.e.a.u.c.impl.UserControllerImplTest    : No active profile set, falling back to default profiles: default
2020-09-13 21:40:57.143  INFO 52850 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2020-09-13 21:40:57.197  INFO 52850 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 49ms. Found 5 JPA repository interfaces.
2020-09-13 21:40:57.716  INFO 52850 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.security.config.annotation.configuration.ObjectPostProcessorConfiguration' of type [org.springframework.security.config.annotation.configuration.ObjectPostProcessorConfiguration] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2020-09-13 21:40:57.722  INFO 52850 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'objectPostProcessor' of type [org.springframework.security.config.annotation.configuration.AutowireBeanFactoryObjectPostProcessor] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2020-09-13 21:40:57.723  INFO 52850 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler@1de08775' of type [org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2020-09-13 21:40:57.727  INFO 52850 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration' of type [org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2020-09-13 21:40:57.731  INFO 52850 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'methodSecurityMetadataSource' of type [org.springframework.security.access.method.DelegatingMethodSecurityMetadataSource] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2020-09-13 21:40:57.872  INFO 52850 --- [           main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [name: default]
2020-09-13 21:40:57.909  INFO 52850 --- [           main] org.hibernate.Version                    : HHH000412: Hibernate ORM core version 5.4.12.Final
2020-09-13 21:40:57.931  INFO 52850 --- [           main] o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {5.1.0.Final}
2020-09-13 21:40:57.993  INFO 52850 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2020-09-13 21:40:58.098  INFO 52850 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2020-09-13 21:40:58.108  INFO 52850 --- [           main] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
(sql queries here)
2020-09-13 21:40:58.523  INFO 52850 --- [           main] o.h.e.t.j.p.i.JtaPlatformInitiator       : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
2020-09-13 21:40:58.527  INFO 52850 --- [           main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2020-09-13 21:40:59.025  WARN 52850 --- [           main] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2020-09-13 21:40:59.661  INFO 52850 --- [           main] o.s.s.web.DefaultSecurityFilterChain     : Creating filter chain: any request, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@647bd553, org.springframework.security.web.context.SecurityContextPersistenceFilter@16d7f503, com.ewsie.allpic.user.security.AuthCookieFilter@a47a011, org.springframework.security.web.header.HeaderWriterFilter@1b9785aa, org.springframework.security.web.authentication.logout.LogoutFilter@2ddb260e, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@4ceac22d, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@1203c259, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@15311af2, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@54602d5a, org.springframework.security.web.session.SessionManagementFilter@347074a9, org.springframework.security.web.access.ExceptionTranslationFilter@2eebb07, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@441a2d52]
2020-09-13 21:40:59.901  INFO 52850 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2020-09-13 21:41:00.036  INFO 52850 --- [           main] o.s.b.a.h2.H2ConsoleAutoConfiguration    : H2 console available at '/h2-console'. Database available at 'jdbc:h2:mem:default'
2020-09-13 21:41:00.070  INFO 52850 --- [           main] o.s.s.c.ThreadPoolTaskScheduler          : Initializing ExecutorService 'taskScheduler'
2020-09-13 21:41:00.111  INFO 52850 --- [           main] o.s.b.t.m.w.SpringBootMockServletContext : Initializing Spring TestDispatcherServlet ''
2020-09-13 21:41:00.111  INFO 52850 --- [           main] o.s.t.web.servlet.TestDispatcherServlet  : Initializing Servlet ''
2020-09-13 21:41:00.118  INFO 52850 --- [           main] o.s.t.web.servlet.TestDispatcherServlet  : Completed initialization in 7 ms
2020-09-13 21:41:00.428  INFO 52850 --- [           main] c.e.a.u.c.impl.UserControllerImplTest    : Started UserControllerImplTest in 4.013 seconds (JVM running for 4.7)

WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by org.springframework.cglib.core.ReflectUtils (file:/home/max/.m2/repository/org/springframework/spring-core/5.2.5.RELEASE/spring-core-5.2.5.RELEASE.jar) to method java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain)
WARNING: Please consider reporting this to the maintainers of org.springframework.cglib.core.ReflectUtils
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /user
       Parameters = {}
          Headers = []
             Body = null
    Session Attrs = {}

Handler:
             Type = org.springframework.web.servlet.resource.ResourceHttpRequestHandler

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 404
    Error message = null
          Headers = [Access-Control-Allow-Origin:"http://localhost:4200", Access-Control-Allow-Methods:"POST, PUT, GET, OPTIONS, DELETE", Access-Control-Max-Age:"3600", Access-Control-Allow-Headers:"Access-Control-Allow-Origin, Authorization, Content-Type, Cache-Control", Access-Control-Allow-Credentials:"true", Vary:"Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers", X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY"]
     Content type = null
             Body = 
    Forwarded URL = null
   Redirected URL = null
          Cookies = []


java.lang.AssertionError: Status expected:<200> but was:<404>
Expected :200
Actual   :404

How can I make the MockMvc work and mock my controller?

2 Answers

The problem was that /user was indeed nowhere to be found, so 404 response was completely justified.

After chaniging:

mockMvc.perform(get("/user"))

to:

mockMvc.perform(get("/user/")) (note the trailing /)

I was able to recieve the actual response and carry on with the test.

In my case, I imported the controller class manually using @Import and it worked. It seems the controller bean had not been imported by @WebMvcTest. See below code :

@ContextConfiguration(
    classes = [TestConfigs::class]
)
@WebMvcTest
@Import(MyController::class)
class MyControllerTests {

    @Test
    fun testPost() {
        //test logic goes here
    }
}

Note that how I used @Import : @Import(MyController::class).

If I remove @Import, I would get 404.

Hope it helps

Related