삽질의 과정을 쭉 써두기는 했으나,
이 모든게 다 mockMvc초기화 방식에 따른 차이였다는걸 깨달았다!
사실 안해도 되는 거였다!
//테스트 코드
@WebMvcTest(UserController.class)
public class UserControllerTest {
@Autowired
private MockMvc mockMvc;
private final JsonUtil jsonUtil = new JsonUtil();
@MockBean
UserService userService;
@MockBean
LoginService loginService;
@Test
@DisplayName("로그인에 성공할 경우 ResultResponse(")
void loginTest() throws Exception {
//given - 회원가입
when(loginService.isValidUser(any())).thenReturn(true);
when(userService.findByUsername(any())).thenReturn(DEFAULT_USER);
doNothing().when(loginService).login(anyLong());
mockMvc.perform(post("/api/v1/users/login")
.contentType(MediaType.APPLICATION_JSON)
.content(jsonUtil.toJsonString(USER_LOGIN_REQUEST))
)
.andExpect(status().isOk())
.andExpect(MockMvcResultMatchers.content().string(jsonUtil.toJsonString(ResultResponse.of(USER_LOGIN_SUCCESS))))
.andDo(print()); //전체 메시지 확인
System.out.println(content());
}
}
//로그인 컨트롤러
@ApiOperation(value = "로그인")
@PostMapping("/login")
public ResponseEntity<ResultResponse> login(
@RequestBody @Valid UserDto.LoginRequest userRequest) {
boolean isValidUser = loginService.isValidUser(userRequest); //when().thenReturn(true)에 의해 무조건 true로 넘어감
if (isValidUser) {
User user = userService.findByUsername(userRequest.getUsername()); //when()으로 인해 무조건 DEFAULT_USER리턴
loginService.login(user.getId()); //doNothing()으로 인해 넘어감
}
return ResponseEntity.ok(ResultResponse.of(USER_LOGIN_SUCCESS));
}
내가 작성한 로그인 로직은
Exception이 터지지 않는 이상 무조건 200 ok로 응답하게 되어있는데,
이상하게 403에러가 떴다.
그래서 찾아본 결과
SpringSecurity를 사용할 경우, 기본적으로 csrf를 체크하기 때문에
(참고)
post요청을 보냈을 때 403 forbidden에러가 뜬다는 것
@EnableWebSecuriy를 달지 않았는데도 Security가 동작하는건가?!
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
사실 security 를 넣어두긴 했지만 진짜 보안 설정을 사용하고 있는 것은 아니라,
현재 SecurityConfig는 이 상태였다.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().permitAll();
http.csrf().disable();
return http.build();
}
일단 csrf체크를 막기 위해서 한번 SecurityFilterChain을 넣고 @EnableWebSecurity를 달았다.
csrf.disable()을 넣었으나 여전히 403 Forbidden
왜 403 Error가 뜬 걸까?
CSRF?
그러니까 원래 security에서 비정상적인 요청을 관리하기 위해서 csrf 토큰을 사용하여,
사용자의 세션에 임의의 값을 저장하고 모든 요청마다 그 값을 포함해서 전송한다.
그리고 요청이 들어올 때 마다 백엔드에서 세션에 저장된 값과 요청으로 전송된 값이 일치하는지 검증하여 방어한다.
csrf토큰이 없었기에 403 forbidden에러가 떴던 것이다
나는 지금 테스트하면서 이런 처리가 필요하지 않으므로 ,
testImplementation 'org.springframework.security:spring-security-test'
일단 spring security test의존성을 추가하고
기존 테스트 코드에 .with(csrf())만 추가해주면
내 요청에 csrf토큰이 알아서 만들어져서 들어가는것같다
보면 요청에 csrf가 들어간것 확인 가능!!
이렇게 하면 배포시에 csrf체크를 해제하지 않아서 안전하면서,
테스트에서도 간단하게 csrf체크를 피해갈 수 있다
403이 해결되자 401 UnAuthorized Error
이렇게 수정하고 테스트를 돌려보자....
401에러가 뜬다........
이 역시 Spring Security로 인한 건데, 아마도 로그인 api에 대한 권한 설정이 되어있지 않아서 그런듯하다.
이 api에 대한 권한을 모든 사람에게 주면 되겠지만,
나는 Spring Security를 테스트에서 사용하고 싶지 않았으므로
api 요청에 권한을 주는 방식은 사용하고 싶지 않았다.
링크된 블로그를 참고하여
@WithMockUser를 붙여서 일단 해결했으나,
로그인은 사실 권한이 없어도 수행할 수 있어야 하므로
아마 SecurityConfig설정에서 로그인은 모두에게 허용되도록 바꾸던가,
아니면 테스트 환경에서는 Security가 동작하지 않도록 하는 어떤 방법이 필요할 것 같다.
일단@WithMockUser로 대충 넘기고 나중에 수정하자! 했다.
@WithMockUser는 위처럼 요청 세션에 authentication Token을 만들어서 넣어준다.
그런데 팀원은 나와 같은 환경인데 따로 401/403에 대한 처리를 해주지 않았다고 한다.
왜인지 살펴보자, 둘이 MockMvc를 초기화하는 방법이 다름을 확인했다.
팀원: 테스트 클래스에 @WebMvcTest만 달고 MockMvc는
@BeforeEach로 Mockbuilder를 이용해 설정. 인코딩도 builder로 함
나: 인코딩은 application.yml에 지정하고, mockMvc는 @Autowired로 주입받음
그리고 mockMvc를 초기화하는 방법은 @Autowired로 자동 주입받는 법이 있고
mockMvcBuilder를 사용해서 초기화하는방법이 있는데, 이건 커스텀 가능함
팀원이 한 방식은 아래처럼 MockMvcBuilder를 이용하여 mocMvc를 초기화하는 방법.
@WebMvcTest(UserController.class)
public class UserControllerTest {
private final JsonUtil jsonUtil = new JsonUtil();
@MockBean
UserService userService;
@MockBean
LoginService loginService;
private MockMvc mockMvc;
@BeforeEach
void setUp(WebApplicationContext applicationContext) {
mockMvc =
MockMvcBuilders.webAppContextSetup(applicationContext)
.addFilter(new CharacterEncodingFilter(StandardCharsets.UTF_8.name(), true))
.build();
}
...
인코딩을 application.yml에 설정하고
setdup메소드 내의 인코딩을 설정하는 코드인
.addFilter(...)를 지우면 인코딩이 정상적으로 되지 않는다.
application.yml에 설정한 인코딩은
@Autowired로 mockMvc를 주입받을 때에만 동작했다.
만약 @Autowired도 달아놓고 setup메소드도 지우지 않아 MockBuilder를 남겨두면,
처음에 mockMvc에는 자동으로 주입받은 값이 들어가지만 이후 다시 초기화하므로
addFilter(...)를 지울시 역시나 인코딩 비정상적
@WebMvcTest(UserController.class)
public class UserControllerTest {
private final JsonUtil jsonUtil = new JsonUtil();
@MockBean
UserService userService;
@MockBean
LoginService loginService;
@Autowired //주입받기
private MockMvc mockMvc;
//Mock build하는 코드 전부 삭제
...
인코딩은 application.yml에서 설정한대로 잘 된다.
다만 위 팀원의 코드와 다른 점이
내 코드에서는 spring security가 동작한다는 점이다
그래서 어쩔수없이 csrf토큰을 넣어주고 @withmockuser를 사용해서 session attrs에 authentication 토큰이 들어가도록 했던 것이다
MockMvc의 autowired가 어떤식으로 동작하는지 알면 이해가 될 것 같은데...
내가 만든 bean이 아니라 스프링부트가 자동으로 만들어서 넣어주는 것 같은데...
나중에 이어서 정리하기!!