From e5a49534ae2f405114befdf145a4d62720ca7aeb Mon Sep 17 00:00:00 2001 From: ChoiDongKuen Date: Mon, 12 Feb 2024 21:13:25 +0900 Subject: [PATCH] =?UTF-8?q?=08test:=20=EC=9D=B8=EC=A6=9D=20=EB=8B=A8?= =?UTF-8?q?=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?(#202)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: JwtAuthenticationFilter 불필요한 로직 제거 (#198) * refactor: SecurityValue 패키지 이동 (#198) * test: 인증 관련 단위 테스트 구현 (#198) * test: 인증 관련 단위 테스트 구현 (#198) * test: JwtAuthenticationFilterTest 리팩토링 (#198) --- .../filter/JwtAuthenticationFilter.java | 6 +- .../auth/controller/AuthControllerTest.java | 8 +- .../unit/auth/service/AuthServiceTest.java | 4 +- .../unit/{auth => }/common/SecurityValue.java | 2 +- .../JwtAuthenticationEntryPointTest.java | 64 +++++++++ .../security/JwtAuthenticationFilterTest.java | 136 ++++++++++++++++++ .../user/controller/UserControllerTest.java | 8 +- .../unit/user/service/UserServiceTest.java | 5 +- 8 files changed, 215 insertions(+), 18 deletions(-) rename src/test/java/net/teumteum/unit/{auth => }/common/SecurityValue.java (90%) create mode 100644 src/test/java/net/teumteum/unit/core/security/JwtAuthenticationEntryPointTest.java create mode 100644 src/test/java/net/teumteum/unit/core/security/JwtAuthenticationFilterTest.java diff --git a/src/main/java/net/teumteum/core/security/filter/JwtAuthenticationFilter.java b/src/main/java/net/teumteum/core/security/filter/JwtAuthenticationFilter.java index db33f357..20ef8e31 100644 --- a/src/main/java/net/teumteum/core/security/filter/JwtAuthenticationFilter.java +++ b/src/main/java/net/teumteum/core/security/filter/JwtAuthenticationFilter.java @@ -15,7 +15,6 @@ import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; -import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; @@ -31,7 +30,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtProperty jwtProperty; @Override - protected void doFilterInternal(HttpServletRequest request, + public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { if (request.getMethod().equals("OPTIONS")) { @@ -73,10 +72,9 @@ private void saveUserAuthentication(User user) { private String resolveTokenFromRequest(HttpServletRequest request) { String token = request.getHeader(jwtProperty.getAccess().getHeader()); - if (!ObjectUtils.isEmpty(token) && token.toLowerCase().startsWith(jwtProperty.getBearer().toLowerCase())) { + if (token.toLowerCase().startsWith(jwtProperty.getBearer().toLowerCase())) { return token.substring(7); } - setRequestAttribute(request, "요청에 대한 JWT 파싱 과정에서 문제가 발생했습니다."); return null; } diff --git a/src/test/java/net/teumteum/unit/auth/controller/AuthControllerTest.java b/src/test/java/net/teumteum/unit/auth/controller/AuthControllerTest.java index cea80e23..b5437bb1 100644 --- a/src/test/java/net/teumteum/unit/auth/controller/AuthControllerTest.java +++ b/src/test/java/net/teumteum/unit/auth/controller/AuthControllerTest.java @@ -1,10 +1,10 @@ package net.teumteum.unit.auth.controller; -import static net.teumteum.unit.auth.common.SecurityValue.INVALID_ACCESS_TOKEN; -import static net.teumteum.unit.auth.common.SecurityValue.INVALID_REFRESH_TOKEN; -import static net.teumteum.unit.auth.common.SecurityValue.VALID_ACCESS_TOKEN; -import static net.teumteum.unit.auth.common.SecurityValue.VALID_REFRESH_TOKEN; +import static net.teumteum.unit.common.SecurityValue.INVALID_ACCESS_TOKEN; +import static net.teumteum.unit.common.SecurityValue.INVALID_REFRESH_TOKEN; +import static net.teumteum.unit.common.SecurityValue.VALID_ACCESS_TOKEN; +import static net.teumteum.unit.common.SecurityValue.VALID_REFRESH_TOKEN; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.springframework.http.HttpHeaders.AUTHORIZATION; diff --git a/src/test/java/net/teumteum/unit/auth/service/AuthServiceTest.java b/src/test/java/net/teumteum/unit/auth/service/AuthServiceTest.java index 3163c935..ef7b5512 100644 --- a/src/test/java/net/teumteum/unit/auth/service/AuthServiceTest.java +++ b/src/test/java/net/teumteum/unit/auth/service/AuthServiceTest.java @@ -1,8 +1,8 @@ package net.teumteum.unit.auth.service; import static net.teumteum.core.security.Authenticated.네이버; -import static net.teumteum.unit.auth.common.SecurityValue.INVALID_ACCESS_TOKEN; -import static net.teumteum.unit.auth.common.SecurityValue.VALID_REFRESH_TOKEN; +import static net.teumteum.unit.common.SecurityValue.INVALID_ACCESS_TOKEN; +import static net.teumteum.unit.common.SecurityValue.VALID_REFRESH_TOKEN; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; diff --git a/src/test/java/net/teumteum/unit/auth/common/SecurityValue.java b/src/test/java/net/teumteum/unit/common/SecurityValue.java similarity index 90% rename from src/test/java/net/teumteum/unit/auth/common/SecurityValue.java rename to src/test/java/net/teumteum/unit/common/SecurityValue.java index e82b8cc6..4bbee619 100644 --- a/src/test/java/net/teumteum/unit/auth/common/SecurityValue.java +++ b/src/test/java/net/teumteum/unit/common/SecurityValue.java @@ -1,4 +1,4 @@ -package net.teumteum.unit.auth.common; +package net.teumteum.unit.common; public final class SecurityValue { diff --git a/src/test/java/net/teumteum/unit/core/security/JwtAuthenticationEntryPointTest.java b/src/test/java/net/teumteum/unit/core/security/JwtAuthenticationEntryPointTest.java new file mode 100644 index 00000000..42920f4b --- /dev/null +++ b/src/test/java/net/teumteum/unit/core/security/JwtAuthenticationEntryPointTest.java @@ -0,0 +1,64 @@ +package net.teumteum.unit.core.security; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import net.teumteum.core.error.ErrorResponse; +import net.teumteum.core.security.filter.JwtAuthenticationEntryPoint; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.DelegatingServletOutputStream; +import org.springframework.security.core.AuthenticationException; + +@ExtendWith(MockitoExtension.class) +@DisplayName("JwtAuthenticationEntryPoint 단위 테스트의") +public class JwtAuthenticationEntryPointTest { + + private static final String ATTRIBUTE_NAME = "exception"; + @Mock + private ObjectMapper objectMapper; + @Mock + private AuthenticationException authenticationException; + @Mock + private HttpServletRequest request; + @Mock + private HttpServletResponse response; + @InjectMocks + private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; + + @Nested + @DisplayName("JwtAuthenticationFilter 에서 인증 예외가 발생시") + class When_authentication_error_occurs_from_filter { + + @Test + @DisplayName("알맞은 예외 메시지와 관련 응답을 반환한다.") + void Return_error_response_with_message() throws IOException { + // given + var errorMessage = "Authentication Exception Occurred"; + var outputStream = new ByteArrayOutputStream(); + + given(request.getAttribute(ATTRIBUTE_NAME)).willReturn(errorMessage); + given(response.getOutputStream()).willReturn(new DelegatingServletOutputStream(outputStream)); + + // when + jwtAuthenticationEntryPoint.commence(request, response, authenticationException); + + // then + verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED); + verify(objectMapper, times(1)).writeValue(any(OutputStream.class), any(ErrorResponse.class)); + } + } +} diff --git a/src/test/java/net/teumteum/unit/core/security/JwtAuthenticationFilterTest.java b/src/test/java/net/teumteum/unit/core/security/JwtAuthenticationFilterTest.java new file mode 100644 index 00000000..ca8badb7 --- /dev/null +++ b/src/test/java/net/teumteum/unit/core/security/JwtAuthenticationFilterTest.java @@ -0,0 +1,136 @@ +package net.teumteum.unit.core.security; + +import static net.teumteum.unit.common.SecurityValue.INVALID_ACCESS_TOKEN; +import static net.teumteum.unit.common.SecurityValue.VALID_ACCESS_TOKEN; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import java.io.IOException; +import net.teumteum.auth.service.AuthService; +import net.teumteum.core.property.JwtProperty; +import net.teumteum.core.security.UserAuthentication; +import net.teumteum.core.security.filter.JwtAuthenticationFilter; +import net.teumteum.core.security.service.JwtService; +import net.teumteum.user.domain.UserFixture; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.core.context.SecurityContextHolder; + +@ExtendWith(MockitoExtension.class) +@DisplayName("JwtAuthenticationFilter 단위 테스트의") +public class JwtAuthenticationFilterTest { + + private static final String ATTRIBUTE_NAME = "exception"; + @Mock + private JwtService jwtService; + @Mock + private AuthService authService; + @Mock + private JwtProperty jwtProperty; + @Mock + private JwtProperty.Access access; + @Mock + private FilterChain filterChain; + @InjectMocks + private JwtAuthenticationFilter jwtAuthenticationFilter; + + @Nested + @DisplayName("API 요청시 JWT 파싱 및 회원 조회 로직은") + class Api_request_with_valid_jwt_unit { + + @BeforeEach + @AfterEach + void clearSecurityContextHolder() { + SecurityContextHolder.clearContext(); + } + + @Test + @DisplayName("유효한 JWT 인 경우, JWT 을 파싱하고 성공적으로 UserAuthentication 을 SecurityContext 에 저장한다.") + void Parsing_jwt_and_save_user_in_security_context() throws ServletException, IOException { + // given + var request = new MockHttpServletRequest(); + var response = new MockHttpServletResponse(); + + given(jwtProperty.getAccess()).willReturn(access); + given(jwtProperty.getAccess().getHeader()).willReturn("Authorization"); + given(jwtProperty.getBearer()).willReturn("Bearer"); + + request.addHeader(jwtProperty.getAccess().getHeader(), + jwtProperty.getBearer() + " " + VALID_ACCESS_TOKEN); + + var user = UserFixture.getIdUser(); + + given(jwtService.validateToken(anyString())).willReturn(true); + given(authService.findUserByAccessToken(anyString())).willReturn(user); + + // when + jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); + + // then + var authentication = SecurityContextHolder.getContext().getAuthentication(); + assertThat(authentication).isInstanceOf(UserAuthentication.class); + } + + @Test + @DisplayName("유효하지 않은 JWT 와 함께 요청이 들어오면, 요청 처리를 중단하고 에러 메세지를 반환한다.") + void Return_error_when_jwt_is_invalid() throws ServletException, IOException { + // given + var request = new MockHttpServletRequest(); + var response = new MockHttpServletResponse(); + + given(jwtProperty.getAccess()).willReturn(access); + given(jwtProperty.getAccess().getHeader()).willReturn("Authorization"); + given(jwtProperty.getBearer()).willReturn("Bearer"); + + request.addHeader(jwtProperty.getAccess().getHeader(), + jwtProperty.getBearer() + " " + INVALID_ACCESS_TOKEN); + + given(jwtService.validateToken(anyString())).willReturn(false); + + // when + jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); + + // then + assertThat(request.getAttribute(ATTRIBUTE_NAME)).isEqualTo("요청에 대한 JWT 가 유효하지 않습니다."); + var authentication = SecurityContextHolder.getContext().getAuthentication(); + assertThat(authentication).isNull(); + verify(filterChain, times(1)).doFilter(request, response); + } + + @Test + @DisplayName("JWT 가 존재하지 않는 경우, 요청 처리를 중단하고 에러 메세지를 반환한다.") + void Return_error_when_jwt_is_not_exist() throws ServletException, IOException { + // given + var request = new MockHttpServletRequest(); + var response = new MockHttpServletResponse(); + + given(jwtProperty.getAccess()).willReturn(access); + given(jwtProperty.getAccess().getHeader()).willReturn("Authorization"); + given(jwtProperty.getBearer()).willReturn("Bearer"); + + request.addHeader(jwtProperty.getAccess().getHeader(), + jwtProperty.getBearer() + " "); + + // when + jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); + + // then + assertThat(request.getAttribute(ATTRIBUTE_NAME)).isEqualTo("요청에 대한 JWT 정보가 존재하지 않습니다."); + verify(jwtService, times(0)).validateToken(anyString()); + } + } +} diff --git a/src/test/java/net/teumteum/unit/user/controller/UserControllerTest.java b/src/test/java/net/teumteum/unit/user/controller/UserControllerTest.java index caee082c..f425e053 100644 --- a/src/test/java/net/teumteum/unit/user/controller/UserControllerTest.java +++ b/src/test/java/net/teumteum/unit/user/controller/UserControllerTest.java @@ -1,7 +1,7 @@ package net.teumteum.unit.user.controller; -import static net.teumteum.unit.auth.common.SecurityValue.VALID_ACCESS_TOKEN; -import static net.teumteum.unit.auth.common.SecurityValue.VALID_REFRESH_TOKEN; +import static net.teumteum.unit.common.SecurityValue.VALID_ACCESS_TOKEN; +import static net.teumteum.unit.common.SecurityValue.VALID_REFRESH_TOKEN; import static net.teumteum.user.domain.Review.별로에요; import static net.teumteum.user.domain.Review.최고에요; import static org.hamcrest.Matchers.hasSize; @@ -267,11 +267,9 @@ void Get_user_reviews_with_200_ok() throws Exception { class Logout_user_api_unit { @Test - @DisplayName("") + @DisplayName("로그인한 회원의 로그아웃을 진행하고, 200 OK 을 반환합니다.") void Logout_user_with_200_ok() throws Exception { // given - var userId = 1L; - doNothing().when(userService).logout(anyLong()); // when && then diff --git a/src/test/java/net/teumteum/unit/user/service/UserServiceTest.java b/src/test/java/net/teumteum/unit/user/service/UserServiceTest.java index b92508fe..f6823533 100644 --- a/src/test/java/net/teumteum/unit/user/service/UserServiceTest.java +++ b/src/test/java/net/teumteum/unit/user/service/UserServiceTest.java @@ -1,7 +1,8 @@ package net.teumteum.unit.user.service; -import static net.teumteum.unit.auth.common.SecurityValue.VALID_ACCESS_TOKEN; -import static net.teumteum.unit.auth.common.SecurityValue.VALID_REFRESH_TOKEN; + +import static net.teumteum.unit.common.SecurityValue.VALID_ACCESS_TOKEN; +import static net.teumteum.unit.common.SecurityValue.VALID_REFRESH_TOKEN; import static net.teumteum.user.domain.Review.별로에요; import static net.teumteum.user.domain.Review.최고에요; import static org.assertj.core.api.Assertions.assertThat;