Mak*_*ski 6 java spring spring-security spring-boot mockmvc
我正在尝试使用 MockMvc 测试我的 Spring Boot REST 控制器。以下是我的测试课:
UserControllerImplTest(版本 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();
}
}
Run Code Online (Sandbox Code Playgroud)
待测试控制器: 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
);
}
Run Code Online (Sandbox Code Playgroud)
它的实现: 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());
}
}
Run Code Online (Sandbox Code Playgroud)
我在控制器 () 中调用的方法users()已被修改,只是为了确保它始终返回 200。
测试失败的原因是:
java.lang.AssertionError: Status expected:<200> but was:<404>
Expected :200
Actual :404
Run Code Online (Sandbox Code Playgroud)
我强烈怀疑我定义的 Filter Bean 可能是罪魁祸首。
AuthCookie过滤器
@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;
}
}
Run Code Online (Sandbox Code Playgroud)
所以我决定手动配置MockMvc,显式地将其添加到上下文中: UserControllerImplTest(版本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();
}
}
Run Code Online (Sandbox Code Playgroud)
但没有成功,仍然收到 404。
我还考虑过使用 来将测试范围限制为仅启动 Web 层,@WebMvcTest但AuthCookieFilter正如您在上面所看到的,我的 需要启动完整的 Spring 上下文。
我注意到,当我在那里放置断点时,调试器会停止doFilter,AuthCookieFilter无论 MockMvc 是自动配置还是手动配置。但是我无法到达内部设置的断点UserControllerImpl.users()。
此应用程序中启用了网络安全,并且添加了额外的过滤器: 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);
}
}
Run Code Online (Sandbox Code Playgroud)
以下是应用程序启动和测试执行的日志:
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
问题是/user确实无处可寻,所以404响应是完全合理的。
改变后:
mockMvc.perform(get("/user"))
到:
mockMvc.perform(get("/user/"))
(注意后面的/)
我能够收到实际的回复并继续测试。
| 归档时间: |
|
| 查看次数: |
9298 次 |
| 最近记录: |