如何解决为什么启用了弹簧安全性的MockMvc在有效路径上返回404错误?
我正在尝试使用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();
}
}
要测试的控制器: 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
);
}
其实现: 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());
}
}
我在控制器(users()
)中调用的方法已修改,只是为了确保它始终返回200。
由于以下原因,测试失败:
java.lang.AssertionError: Status expected:<200> but was:<404>
Expected :200
Actual :404
我非常怀疑我定义的Filter Bean可能是罪魁祸首。
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;
}
}
因此,我决定手动配置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,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();
}
}
但没有成功,仍然获得404。
我还考虑通过使用@WebMvcTest
将测试范围限制为仅显示Web层,但是如上所述,我的AuthCookieFilter
需要完整的Spring上下文。
我注意到,当我在doFilter
的{{1}}内放置一个断点时,调试器将停止,而不论MockMvc是自动配置还是手动配置。但是我无法到达AuthCookieFilter
内部设置的断点。
此应用程序中启用了网络安全性,这是要添加其他过滤器的原因: SecurityConfiguration.class
UserControllerImpl.users()
以下是应用程序启动和测试执行的日志:
@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);
}
}
如何使MockMvc工作并模拟控制器?
解决方法
问题在于/user
确实找不到,因此404响应是完全合理的。
更改后:
mockMvc.perform(get("/user"))
收件人:
mockMvc.perform(get("/user/"))
(请注意结尾的/
)
我能够收到实际的答复并继续进行测试。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。