ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring Boot] mockMvc를 통해 담겨오는 HttpOnly Cookie 내용물 검증하는 테스트 코드 작성법
    Spring/Study 2023. 7. 23. 23:10

    JWT 토큰을 사용하여 로그인을 하면 Access토큰과 Refresh토큰을 응답으로 주는데, 이때 Refresh토큰을 HttpOnly 쿠키로 전달하는 상황이다.

    프론트 측에서는 담겨오는 쿠키를 임의로 설정할 수 없으므로 좀 더 보안에 유리하다고 생각한다.

    개발 단계에서 이를 검증하기 위해서 mockMvc를 통해 테스트 코드를 작성하는데 body에 담겨오는 값들은 jsonPath를 통해 검증을 진행할 수 있지만,

    헤더에 담겨오는 HttpOnly 쿠키는 따로 옵션이 설정되어 값 + 옵션 값까지 동시에 전달된다.

    그렇다면 테스트 코드를 어떻게 작성을 해야 하는가? 에 대한 해결 방법이다.

    가장 먼저 테스트 코드는 given when then 패턴으로 작성하였다.

    when까지의 패턴 코드이다

    when 까지의 코드

    @Test
        @WithMockUser
        @DisplayName("어드민 로그인 성공 테스트")
        void successAdminLogin() throws Exception {
            // given
            given(memberService.loginAdmin(any())).willReturn(member);
            given(jwtProvider.createAccessToken(any())).willReturn("ACCESS_TOKEN");
            given(jwtProvider.createRefreshToken(any())).willReturn("REFRESH_TOKEN");
    
            ResponseCookie responseCookie = ResponseCookie.from("refreshToken", "REFRESH_TOKEN")
                    .httpOnly(true)
                    .secure(true)
                    .sameSite("None")
                    .path("/")
                    .maxAge(3600000)
                    .build();
    
            given(memberService.createHttpOnlyCookie(any())).willReturn(responseCookie);
            // when
            ResultActions resultActions = mockMvc.perform(post("/admin/login")
                    .contentType("application/json")
                    .with(csrf())
                    .content(objectMapper.writeValueAsString(adminLoginRequest)));
    
            ...
        }

    여기서 눈 여겨볼 부분은 ResponseCookie 클래스를 만드는 과정이다. 옵션 값들이 따로 지정되어 있고, /admin/login이라는 경로로 요청을 할 경우
    헤더에는 쿠키로 저장된 refreshToken과 accessToken 그리고 Body에는 따로 지정한 DTO 클래스가 반환될 것이다.

    실패했던 코드

    @Test
        @WithMockUser
        @DisplayName("어드민 로그인 성공 테스트")
        void successAdminLogin() throws Exception {
            // given
            given(memberService.loginAdmin(any())).willReturn(member);
            given(jwtProvider.createAccessToken(any())).willReturn("ACCESS_TOKEN");
            given(jwtProvider.createRefreshToken(any())).willReturn("REFRESH_TOKEN");
    
            ResponseCookie responseCookie = ResponseCookie.from("refreshToken", "REFRESH_TOKEN")
                    .httpOnly(true)
                    .secure(true)
                    .sameSite("None")
                    .path("/")
                    .maxAge(3600000)
                    .build();
    
            given(memberService.createHttpOnlyCookie(any())).willReturn(responseCookie);
            // when
            ResultActions resultActions = mockMvc.perform(post("/admin/login")
                    .contentType("application/json")
                    .with(csrf())
                    .content(objectMapper.writeValueAsString(adminLoginRequest)));
    
            // then
            resultActions
                    .andExpect(status().isOk())
                    .andExpect(header().string(HttpHeaders.SET_COOKIE, responseCookie.getValue()))
                    .andExpect(header().string("accessToken", "ACCESS_TOKEN"))
                    .andExpect(jsonPath("$.success").value(true))
                    .andExpect(jsonPath("$.admin").value(true))
                    .andExpect(jsonPath("$.writer").value(true))
                    .andDo(print());
        }

    처음에는 단순하게 HttpHeaders.Set_COOKIE의 값을 단순 비교하였다.

    그러나 예상값과 다르게 실제 값은 옵션값 까지 전부 포함되어 있었다.

    Expected :REFRESH_TOKEN
    Actual   :refreshToken=REFRESH_TOKEN; Path=/; Max-Age=3600000; Expires=Sun, 3 Sep 2023 05:49:05 GMT; Secure; HttpOnly; SameSite=None

    여기서 어떤 메소드를 사용하면 refreshToken이라는 이름의 값만 비교할 수 있을까를 고민을 하다가

    람다식을 사용하여 ResultAction 방식 대신에 assertj를 사용하는 방식을 생각했다.

    그리하여 작성한 실제코드는 다음과 같다.

    성공 코드

    @Test
        @WithMockUser
        @DisplayName("어드민 로그인 성공 테스트")
        void successAdminLogin() throws Exception {
            // given
            given(memberService.loginAdmin(any())).willReturn(member);
            given(jwtProvider.createAccessToken(any())).willReturn("ACCESS_TOKEN");
            given(jwtProvider.createRefreshToken(any())).willReturn("REFRESH_TOKEN");
    
            ResponseCookie responseCookie = ResponseCookie.from("refreshToken", "REFRESH_TOKEN")
                    .httpOnly(true)
                    .secure(true)
                    .sameSite("None")
                    .path("/")
                    .maxAge(3600000)
                    .build();
    
            given(memberService.createHttpOnlyCookie(any())).willReturn(responseCookie);
            // when
            ResultActions resultActions = mockMvc.perform(post("/admin/login")
                    .contentType("application/json")
                    .with(csrf())
                    .content(objectMapper.writeValueAsString(adminLoginRequest)));
    
            // then
            resultActions
                    .andExpect(status().isOk())
                    .andExpect(result -> {
                        String setCookieValue = result.getResponse().getHeader(HttpHeaders.SET_COOKIE);
                        assertThat(setCookieValue).contains(responseCookie.getValue());
                    })
                    .andExpect(header().string("accessToken", "ACCESS_TOKEN"))
                    .andExpect(jsonPath("$.success").value(true))
                    .andExpect(jsonPath("$.admin").value(true))
                    .andExpect(jsonPath("$.writer").value(true))
                    .andDo(print());
        }

    따로 응답값의 헤더의 쿠키를 setCookieValue라는 변수병으로 저장하여, 예상되는 Cookie의 내용값을 비교하는 방법을 통해 테스트 코드를 작성하였다.

    실제로 테스트 코드 성공을 시켰으며 테스트 코드를 작성하는 새로운 방법을 알아낸거같아서 기록할 겸 정리해 봤다.

    댓글

Designed by black7375.