From b44a80ae25787a747072605a693c217d4c0fde5e Mon Sep 17 00:00:00 2001 From: caniro Date: Fri, 6 Jun 2025 16:43:42 +0900 Subject: [PATCH 1/7] =?UTF-8?q?fix:=20=EA=B3=84=EC=A2=8C=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=ED=86=B5=ED=95=A9=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=8B=A4=ED=8C=A8=20=EC=A1=B0=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coin/user/info/application/AccountServiceTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/cleanengine/coin/user/info/application/AccountServiceTest.java b/src/test/java/com/cleanengine/coin/user/info/application/AccountServiceTest.java index 1a5efff8..46f3b595 100644 --- a/src/test/java/com/cleanengine/coin/user/info/application/AccountServiceTest.java +++ b/src/test/java/com/cleanengine/coin/user/info/application/AccountServiceTest.java @@ -10,7 +10,7 @@ import static org.assertj.core.api.Assertions.assertThat; -@ActiveProfiles({"dev", "it", "h2-mem"}) +@ActiveProfiles({"dev", "h2-mem"}) @DisplayName("계좌 서비스 - h2 통합테스트") @SpringBootTest class AccountServiceTest { @@ -22,7 +22,7 @@ class AccountServiceTest { @Test void createNewAccount() { // given - int userId = 3; + int userId = Integer.MAX_VALUE; double cash = CommonValues.INITIAL_USER_CASH; // when @@ -41,7 +41,7 @@ void createNewAccount() { @Test void retrieveAccountByInvalidUserId() { // given, when - Account account = accountService.retrieveAccountByUserId(1000); + Account account = accountService.retrieveAccountByUserId(Integer.MAX_VALUE - 1); // then assertThat(account).isNull(); From bd816bc3ff7801ee314098ad343c14245f76587b Mon Sep 17 00:00:00 2001 From: caniro Date: Fri, 6 Jun 2025 17:08:21 +0900 Subject: [PATCH 2/7] =?UTF-8?q?test:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/LoginControllerTest.java | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 src/test/java/com/cleanengine/coin/user/login/presentation/LoginControllerTest.java diff --git a/src/test/java/com/cleanengine/coin/user/login/presentation/LoginControllerTest.java b/src/test/java/com/cleanengine/coin/user/login/presentation/LoginControllerTest.java new file mode 100644 index 00000000..e5f297d4 --- /dev/null +++ b/src/test/java/com/cleanengine/coin/user/login/presentation/LoginControllerTest.java @@ -0,0 +1,103 @@ +package com.cleanengine.coin.user.login.presentation; + +import com.cleanengine.coin.user.login.application.JWTUtil; +import jakarta.servlet.http.Cookie; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@DisplayName("로그인 및 토큰 API 통합테스트") +@AutoConfigureMockMvc +@SpringBootTest +class LoginControllerTest { + + @Autowired + private MockMvc mvc; + + @Autowired + private JWTUtil jwtUtil; + + @Test + @DisplayName("헬스체크 성공 시 성공 응답 반환") + void healthcheckTest() throws Exception { + // when + ResultActions resultActions = mvc.perform(get("/api/healthcheck") + .accept(MediaType.APPLICATION_JSON)); + + // then + resultActions.andExpect(status().isOk()) + .andExpect(jsonPath("$.isSuccess").value(true)) + .andExpect(jsonPath("$.data").value("Health Check Completed")); + } + + @Test + @DisplayName("유효한 JWT 토큰과 함께 요청 시 토큰 검증에 성공한다.") + void validTokenCheckTest() throws Exception { + // given + Integer userId = 123; + Long expiredMs = 1000L * 60 * 60; + String token = jwtUtil.createJwt(userId, expiredMs); + Cookie cookie = new Cookie("access_token", token); + cookie.setPath("/"); + + // when + ResultActions resultActions = mvc.perform(get("/api/tokencheck") + .cookie(cookie) + .accept(MediaType.APPLICATION_JSON)); + + // then + resultActions.andExpect(status().isOk()) + .andExpect(jsonPath("$.isSuccess").value(true)) + .andExpect(jsonPath("$.data.message").value("Token is Valid")) + .andExpect(jsonPath("$.data.userId").value(userId)); + } + + @Test + @DisplayName("유효하지 않은 JWT 토큰으로 요청 시 토큰 검증에 실패한다.") + void invalidTokenCheckTest() throws Exception { + // given + String invalidToken = "invalid.jwt.token"; + Cookie cookie = new Cookie("access_token", invalidToken); + cookie.setPath("/"); + + // when + ResultActions resultActions = mvc.perform(get("/api/tokencheck") + .cookie(cookie) + .accept(MediaType.APPLICATION_JSON)); + + // then + resultActions.andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.isSuccess").value(false)) + .andExpect(jsonPath("$.error").exists()); + } + + @Test + @DisplayName("로그아웃 시 access_token 쿠키가 제거된다") + void logoutTest() throws Exception { + // given + Integer userId = 123; + Long expiredMs = 1000L * 60 * 60; + String token = jwtUtil.createJwt(userId, expiredMs); + Cookie cookie = new Cookie("access_token", token); + cookie.setPath("/"); + + // when + ResultActions resultActions = mvc.perform(get("/api/logout") + .cookie(cookie) + .accept(MediaType.APPLICATION_JSON)); + + // then + resultActions.andExpect(status().isOk()) + .andExpect(jsonPath("$.isSuccess").value(true)) + .andExpect(cookie().maxAge("access_token", 0)) + .andExpect(cookie().value("access_token", org.hamcrest.Matchers.emptyOrNullString())); + } +} \ No newline at end of file From c9d0668bde8bdeaa5689adca793c107bae5e8c5b Mon Sep 17 00:00:00 2001 From: caniro Date: Fri, 6 Jun 2025 17:29:27 +0900 Subject: [PATCH 3/7] =?UTF-8?q?test:=20JWT=20=ED=95=84=ED=84=B0=20?= =?UTF-8?q?=EB=8B=A8=EC=9C=84=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/CustomOAuth2UserService.java | 5 +- .../user/login/application/JWTFilter.java | 2 +- .../user/login/infra/CustomOAuth2User.java | 6 +- ...hCustomMockUserSecurityContextFactory.java | 2 +- .../user/login/application/JWTFilterTest.java | 116 ++++++++++++++++++ 5 files changed, 126 insertions(+), 5 deletions(-) create mode 100644 src/test/java/com/cleanengine/coin/user/login/application/JWTFilterTest.java diff --git a/src/main/java/com/cleanengine/coin/user/login/application/CustomOAuth2UserService.java b/src/main/java/com/cleanengine/coin/user/login/application/CustomOAuth2UserService.java index d39e163d..b03ae1df 100644 --- a/src/main/java/com/cleanengine/coin/user/login/application/CustomOAuth2UserService.java +++ b/src/main/java/com/cleanengine/coin/user/login/application/CustomOAuth2UserService.java @@ -64,7 +64,8 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic oAuthRepository.save(newOAuth); accountService.createNewAccount(newUser.getId(), CommonValues.INITIAL_USER_CASH); - return new CustomOAuth2User(UserOAuthDetails.of(newUser, newOAuth)); + UserOAuthDetails newUserOAuthDetails = UserOAuthDetails.of(newUser, newOAuth); + return CustomOAuth2User.of(newUserOAuthDetails); } else { OAuth existOAuth = oAuthRepository.findByProviderAndProviderUserId(provider, providerUserId); @@ -74,7 +75,7 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic // TODO : KAKAO Token 관련 정보 추가 oAuthRepository.save(existOAuth); - return new CustomOAuth2User(existData); + return CustomOAuth2User.of(existData); } } diff --git a/src/main/java/com/cleanengine/coin/user/login/application/JWTFilter.java b/src/main/java/com/cleanengine/coin/user/login/application/JWTFilter.java index 89e9e942..b6dc9dc4 100644 --- a/src/main/java/com/cleanengine/coin/user/login/application/JWTFilter.java +++ b/src/main/java/com/cleanengine/coin/user/login/application/JWTFilter.java @@ -83,7 +83,7 @@ protected void doFilterInternal(HttpServletRequest request, @NotNull HttpServlet private static Authentication getAuthentication(Integer userId) { UserOAuthDetails userOAuthDetails = UserOAuthDetails.of(userId); - CustomOAuth2User customOAuth2User = new CustomOAuth2User(userOAuthDetails); + CustomOAuth2User customOAuth2User = CustomOAuth2User.of(userOAuthDetails); // 스프링 시큐리티 인증 토큰 생성 return new UsernamePasswordAuthenticationToken(customOAuth2User, diff --git a/src/main/java/com/cleanengine/coin/user/login/infra/CustomOAuth2User.java b/src/main/java/com/cleanengine/coin/user/login/infra/CustomOAuth2User.java index 209826bf..da126a21 100644 --- a/src/main/java/com/cleanengine/coin/user/login/infra/CustomOAuth2User.java +++ b/src/main/java/com/cleanengine/coin/user/login/infra/CustomOAuth2User.java @@ -11,10 +11,14 @@ public class CustomOAuth2User implements OAuth2User { private final UserOAuthDetails userOAuthDetails; - public CustomOAuth2User(UserOAuthDetails userOAuthDetails) { + private CustomOAuth2User(UserOAuthDetails userOAuthDetails) { this.userOAuthDetails = userOAuthDetails; } + public static CustomOAuth2User of(UserOAuthDetails userOAuthDetails) { + return new CustomOAuth2User(userOAuthDetails); + } + @Override public Map getAttributes() { return null; diff --git a/src/test/java/com/cleanengine/coin/tool/helper/WithCustomMockUserSecurityContextFactory.java b/src/test/java/com/cleanengine/coin/tool/helper/WithCustomMockUserSecurityContextFactory.java index 96695066..4e16bcb1 100644 --- a/src/test/java/com/cleanengine/coin/tool/helper/WithCustomMockUserSecurityContextFactory.java +++ b/src/test/java/com/cleanengine/coin/tool/helper/WithCustomMockUserSecurityContextFactory.java @@ -15,7 +15,7 @@ public SecurityContext createSecurityContext(WithCustomMockUser annotation) { SecurityContext context = SecurityContextHolder.createEmptyContext(); UserOAuthDetails userOAuthDetails = UserOAuthDetails.of(annotation.id()); - CustomOAuth2User customOAuth2User = new CustomOAuth2User(userOAuthDetails); + CustomOAuth2User customOAuth2User = CustomOAuth2User.of(userOAuthDetails); Authentication authentication = new UsernamePasswordAuthenticationToken(customOAuth2User, null, customOAuth2User.getAuthorities()); diff --git a/src/test/java/com/cleanengine/coin/user/login/application/JWTFilterTest.java b/src/test/java/com/cleanengine/coin/user/login/application/JWTFilterTest.java new file mode 100644 index 00000000..008d18b7 --- /dev/null +++ b/src/test/java/com/cleanengine/coin/user/login/application/JWTFilterTest.java @@ -0,0 +1,116 @@ +package com.cleanengine.coin.user.login.application; + +import static org.mockito.Mockito.*; + +import com.cleanengine.coin.common.response.ErrorStatus; +import com.cleanengine.coin.configuration.SecurityEndpoints.EndpointConfig; +import jakarta.servlet.FilterChain; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.DisplayName; +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 java.io.PrintWriter; +import java.io.StringWriter; + +@DisplayName("JWTFilter 단위테스트") +@ExtendWith(MockitoExtension.class) +class JWTFilterTest { + + @Mock + private JWTUtil jwtUtil; + @Mock + private EndpointConfig endpointConfig; + @Mock + private HttpServletRequest request; + @Mock + private HttpServletResponse response; + @Mock + private FilterChain filterChain; + + @InjectMocks + private JWTFilter jwtFilter; + + private final String publicPath = "/api/public"; + private final String privatePath = "/api/private"; + private final int unauthorizedStatus = ErrorStatus.UNAUTHORIZED_RESOURCE.getHttpStatus().value(); + + @DisplayName("public path 접근 시 인증없이 통과한다.") + @Test + void whenPublicPath_thenProceedWithoutValidation() throws Exception { + // given + when(request.getRequestURI()).thenReturn(publicPath); + when(endpointConfig.isPublicPath(publicPath)).thenReturn(true); + + // when + jwtFilter.doFilterInternal(request, response, filterChain); + + // then + verify(filterChain).doFilter(request, response); + verify(jwtUtil, never()).getUserId(any()); + } + + @DisplayName("private path 접근 시 토큰이 없으면 401 응답을 반환한다.") + @Test + void whenNoToken_thenReturnUnauthorized() throws Exception { + // given + when(request.getRequestURI()).thenReturn("/api/private"); + when(endpointConfig.isPublicPath("/api/private")).thenReturn(false); + when(request.getCookies()).thenReturn(null); + StringWriter stringWriter = new StringWriter(); + PrintWriter writer = new PrintWriter(stringWriter); + when(response.getWriter()).thenReturn(writer); + + // when + jwtFilter.doFilterInternal(request, response, filterChain); + + // then + verify(response).setStatus(unauthorizedStatus); + verify(filterChain, never()).doFilter(request, response); + } + + @DisplayName("private path 접근 시 유효한 토큰인 경우 다음 필터체인으로 넘어간다.") + @Test + void whenValidToken_thenAuthenticate() throws Exception { + // given + when(request.getRequestURI()).thenReturn(privatePath); + when(endpointConfig.isPublicPath(privatePath)).thenReturn(false); + Cookie cookie = new Cookie("access_token", "valid_token"); + when(request.getCookies()).thenReturn(new Cookie[]{cookie}); + when(jwtUtil.getUserId("valid_token")).thenReturn(1); + when(jwtUtil.isExpired("valid_token")).thenReturn(false); + + // when + jwtFilter.doFilterInternal(request, response, filterChain); + + // then + verify(filterChain).doFilter(request, response); + } + + @DisplayName("private path 접근 시 만료된 토큰인 경우 401 응답을 반환한다.") + @Test + void whenExpiredToken_thenReturnUnauthorized() throws Exception { + // given + when(request.getRequestURI()).thenReturn(privatePath); + when(endpointConfig.isPublicPath(privatePath)).thenReturn(false); + Cookie cookie = new Cookie("access_token", "expired_token"); + when(request.getCookies()).thenReturn(new Cookie[]{cookie}); + when(jwtUtil.getUserId("expired_token")).thenReturn(1); + when(jwtUtil.isExpired("expired_token")).thenReturn(true); + StringWriter stringWriter = new StringWriter(); + PrintWriter writer = new PrintWriter(stringWriter); + when(response.getWriter()).thenReturn(writer); + + // when + jwtFilter.doFilterInternal(request, response, filterChain); + + // then + verify(response).setStatus(unauthorizedStatus); + verify(filterChain, never()).doFilter(request, response); + } +} \ No newline at end of file From 85efa12679635abafed21f252b3bc0464c822b11 Mon Sep 17 00:00:00 2001 From: caniro Date: Fri, 6 Jun 2025 17:47:58 +0900 Subject: [PATCH 4/7] =?UTF-8?q?test:=20=EC=9D=B8=EC=A6=9D=20=ED=95=B8?= =?UTF-8?q?=EB=93=A4=EB=9F=AC=20=EB=8B=A8=EC=9C=84=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/CustomSuccessHandler.java | 12 ++-- .../application/CustomSuccessHandlerTest.java | 60 +++++++++++++++++++ 2 files changed, 67 insertions(+), 5 deletions(-) create mode 100644 src/test/java/com/cleanengine/coin/user/login/application/CustomSuccessHandlerTest.java diff --git a/src/main/java/com/cleanengine/coin/user/login/application/CustomSuccessHandler.java b/src/main/java/com/cleanengine/coin/user/login/application/CustomSuccessHandler.java index c56bc052..463f4e02 100644 --- a/src/main/java/com/cleanengine/coin/user/login/application/CustomSuccessHandler.java +++ b/src/main/java/com/cleanengine/coin/user/login/application/CustomSuccessHandler.java @@ -16,14 +16,16 @@ public class CustomSuccessHandler extends SimpleUrlAuthenticationSuccessHandler private final JWTUtil jwtUtil; - @Value("${spring.security.cookie.secure}") - private boolean isCookieSecure; + private final boolean isCookieSecure; - @Value("${frontend.url}") - private String frontendUrl; + private final String frontendUrl; - public CustomSuccessHandler(JWTUtil jwtUtil) { + public CustomSuccessHandler(JWTUtil jwtUtil, + @Value("${spring.security.cookie.secure}") boolean isCookieSecure, + @Value("${frontend.url}") String frontendUrl) { this.jwtUtil = jwtUtil; + this.isCookieSecure = isCookieSecure; + this.frontendUrl = frontendUrl; } @Override diff --git a/src/test/java/com/cleanengine/coin/user/login/application/CustomSuccessHandlerTest.java b/src/test/java/com/cleanengine/coin/user/login/application/CustomSuccessHandlerTest.java new file mode 100644 index 00000000..3fb5a2e0 --- /dev/null +++ b/src/test/java/com/cleanengine/coin/user/login/application/CustomSuccessHandlerTest.java @@ -0,0 +1,60 @@ +package com.cleanengine.coin.user.login.application; + +import static org.mockito.Mockito.*; + +import com.cleanengine.coin.user.login.infra.CustomOAuth2User; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.core.Authentication; + +@DisplayName("인증 성공 핸들러 단위테스트") +@ExtendWith(MockitoExtension.class) +class CustomSuccessHandlerTest { + + @Mock + private HttpServletRequest request; + + @Mock + private HttpServletResponse response; + + @Mock + private Authentication authentication; + + @Mock + private JWTUtil jwtUtil; + + @Mock + private CustomOAuth2User customOAuth2User; + + private CustomSuccessHandler customSuccessHandler; + + @BeforeEach + void setUp() { + customSuccessHandler = new CustomSuccessHandler(jwtUtil, true, "frontend URL"); + } + + @DisplayName("인증 성공 시 JWT 토큰을 쿠키에 저장하고 FE로 리디렉션한다.") + @Test + void whenAuthenticationSuccess_thenSetCookieAndRedirect() throws Exception { + // given + int userId = 1; + when(authentication.getPrincipal()).thenReturn(customOAuth2User); + when(customOAuth2User.getUserId()).thenReturn(userId); + when(jwtUtil.createJwt(eq(userId), anyLong())).thenReturn("test.jwt.token"); + + // when + customSuccessHandler.onAuthenticationSuccess(request, response, authentication); + + // then + verify(response).addCookie(any(Cookie.class)); + verify(response).sendRedirect("frontend URL"); + } + +} \ No newline at end of file From ce1011883258c791ae1a9f8ce41ec40fd38f9de0 Mon Sep 17 00:00:00 2001 From: caniro Date: Fri, 6 Jun 2025 20:16:03 +0900 Subject: [PATCH 5/7] =?UTF-8?q?test:=20OAuth=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EB=8B=A8=EC=9C=84=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cleanengine/coin/user/domain/OAuth.java | 2 - .../cleanengine/coin/user/domain/User.java | 10 +- .../application/CustomOAuth2UserService.java | 11 +- .../user/login/infra/UserOAuthDetails.java | 5 + .../CustomOAuth2UserServiceTest.java | 144 ++++++++++++++++++ 5 files changed, 166 insertions(+), 6 deletions(-) create mode 100644 src/test/java/com/cleanengine/coin/user/login/application/CustomOAuth2UserServiceTest.java diff --git a/src/main/java/com/cleanengine/coin/user/domain/OAuth.java b/src/main/java/com/cleanengine/coin/user/domain/OAuth.java index 409974a3..ea214299 100644 --- a/src/main/java/com/cleanengine/coin/user/domain/OAuth.java +++ b/src/main/java/com/cleanengine/coin/user/domain/OAuth.java @@ -1,7 +1,6 @@ package com.cleanengine.coin.user.domain; import jakarta.persistence.*; -import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -13,7 +12,6 @@ @Entity @Table(name = "oauth") @NoArgsConstructor -@AllArgsConstructor public class OAuth { @Id diff --git a/src/main/java/com/cleanengine/coin/user/domain/User.java b/src/main/java/com/cleanengine/coin/user/domain/User.java index 0f12f3e8..fb36bd44 100644 --- a/src/main/java/com/cleanengine/coin/user/domain/User.java +++ b/src/main/java/com/cleanengine/coin/user/domain/User.java @@ -14,7 +14,6 @@ @Getter @Setter @NoArgsConstructor -@AllArgsConstructor public class User { @Id @@ -26,4 +25,13 @@ public class User { @Column(name = "created_at", nullable = false, updatable = false) private LocalDateTime createdAt; + private User(Integer id, LocalDateTime createdAt) { + this.id = id; + this.createdAt = createdAt; + } + + public static User of(Integer id, LocalDateTime createdAt) { + return new User(id, createdAt); + } + } diff --git a/src/main/java/com/cleanengine/coin/user/login/application/CustomOAuth2UserService.java b/src/main/java/com/cleanengine/coin/user/login/application/CustomOAuth2UserService.java index b03ae1df..22717307 100644 --- a/src/main/java/com/cleanengine/coin/user/login/application/CustomOAuth2UserService.java +++ b/src/main/java/com/cleanengine/coin/user/login/application/CustomOAuth2UserService.java @@ -31,7 +31,7 @@ public CustomOAuth2UserService(UserRepository userRepository, OAuthRepository oA @Override public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { - OAuth2User oAuth2User = super.loadUser(userRequest); + OAuth2User oAuth2User = doSuperLoadMethod(userRequest); OAuth2Response oAuth2Response = new KakaoResponse(oAuth2User.getAttributes()); /* 추후 OAuth 플랫폼 추가 시 이런 식으로 Response 분기처리 @@ -71,12 +71,17 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic OAuth existOAuth = oAuthRepository.findByProviderAndProviderUserId(provider, providerUserId); existOAuth.setEmail(email); - existOAuth.setNickname(oAuth2Response.getName()); - // TODO : KAKAO Token 관련 정보 추가 + existOAuth.setNickname(name); oAuthRepository.save(existOAuth); + existData.update(existOAuth); + return CustomOAuth2User.of(existData); } } + protected OAuth2User doSuperLoadMethod(OAuth2UserRequest userRequest) { + return super.loadUser(userRequest); + } + } diff --git a/src/main/java/com/cleanengine/coin/user/login/infra/UserOAuthDetails.java b/src/main/java/com/cleanengine/coin/user/login/infra/UserOAuthDetails.java index d90ef467..fc0b55b1 100644 --- a/src/main/java/com/cleanengine/coin/user/login/infra/UserOAuthDetails.java +++ b/src/main/java/com/cleanengine/coin/user/login/infra/UserOAuthDetails.java @@ -43,4 +43,9 @@ public static UserOAuthDetails of(int userId) { .build(); } + public void update(OAuth oauth) { + this.email = oauth.getEmail(); + this.name = oauth.getNickname(); + } + } diff --git a/src/test/java/com/cleanengine/coin/user/login/application/CustomOAuth2UserServiceTest.java b/src/test/java/com/cleanengine/coin/user/login/application/CustomOAuth2UserServiceTest.java new file mode 100644 index 00000000..13c3f7b1 --- /dev/null +++ b/src/test/java/com/cleanengine/coin/user/login/application/CustomOAuth2UserServiceTest.java @@ -0,0 +1,144 @@ +package com.cleanengine.coin.user.login.application; + +import com.cleanengine.coin.common.CommonValues; +import com.cleanengine.coin.user.domain.OAuth; +import com.cleanengine.coin.user.domain.User; +import com.cleanengine.coin.user.info.application.AccountService; +import com.cleanengine.coin.user.login.infra.CustomOAuth2User; +import com.cleanengine.coin.user.login.infra.UserOAuthDetails; +import com.cleanengine.coin.user.info.infra.OAuthRepository; +import com.cleanengine.coin.user.info.infra.UserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.OAuth2User; + +import java.time.LocalDateTime; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +@DisplayName("OAuth2 유저 서비스 단위테스트") +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class CustomOAuth2UserServiceTest { + + @Mock + private UserRepository userRepository; + @Mock + private OAuthRepository oAuthRepository; + @Mock + private AccountService accountService; + @Mock + private OAuth2UserRequest userRequest; + @Mock + private OAuth2User oAuth2UserFromSuper; + @Mock + private ClientRegistration clientRegistration; + + private CustomOAuth2UserService customOAuth2UserService; + + private final String provider = "kakao"; + + private final String providerId = "12345"; + + @BeforeEach + void setUp() { + // DefaultOAuth2UserService.loadUser만 mocking하기 위해 spy 사용 + customOAuth2UserService = Mockito.spy(new CustomOAuth2UserService(userRepository, oAuthRepository, accountService)); + + Map profile = Map.of("nickname", "Test User"); + Map kakaoAccount = Map.of( + "email", "test@example.com", + "profile", profile + ); + Map attributes = Map.of( + "id", providerId, + "kakao_account", kakaoAccount + ); + when(oAuth2UserFromSuper.getAttributes()).thenReturn(attributes); + + when(userRequest.getClientRegistration()).thenReturn(clientRegistration); + when(clientRegistration.getRegistrationId()).thenReturn(provider); + + try { + doReturn(oAuth2UserFromSuper) + .when(customOAuth2UserService) + .doSuperLoadMethod(userRequest); + } catch (OAuth2AuthenticationException e) { + fail("doSuperLoadMethod mocking 실패", e); + } + } + + @DisplayName("신규 유저로 인증 시 User와 OAuth를 새로 생성한다.") + @Test + void whenNewUser_thenCreateUserAndOAuth() { + // Given + int userId = 3; + when(userRepository.findUserByOAuthProviderAndProviderId(provider, providerId)).thenReturn(null); + when(userRepository.save(any(User.class))).thenAnswer(invocation -> { + User user = invocation.getArgument(0); + user.setId(userId); + return user; + }); + + // When + OAuth2User result = customOAuth2UserService.loadUser(userRequest); + + // Then + assertNotNull(result); + verify(userRepository).save(any(User.class)); + verify(oAuthRepository).save(any(OAuth.class)); + verify(accountService).createNewAccount(eq(userId), eq(CommonValues.INITIAL_USER_CASH)); + + assertInstanceOf(CustomOAuth2User.class, result); + assertEquals("Test User", result.getName()); + } + + @DisplayName("기존 유저로 인증 시 이메일과 닉네임을 변경한다.") + @Test + void loadUser_WhenExistingUser_ShouldUpdateOAuth() { + // Given + User existingUser = User.of(3, LocalDateTime.now()); + + OAuth existingOAuth = new OAuth(); + existingOAuth.setUserId(3); + existingOAuth.setProvider(provider); + existingOAuth.setProviderUserId(providerId); + existingOAuth.setEmail("old@example.com"); + existingOAuth.setNickname("Old User"); + + UserOAuthDetails userOAuthDetails = UserOAuthDetails.of(existingUser, existingOAuth); + + when(userRepository.findUserByOAuthProviderAndProviderId(provider, providerId)).thenReturn(userOAuthDetails); + when(oAuthRepository.findByProviderAndProviderUserId(provider, providerId)).thenReturn(existingOAuth); + when(oAuthRepository.save(any(OAuth.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + // When + OAuth2User result = customOAuth2UserService.loadUser(userRequest); + + // Then + assertNotNull(result); + verify(oAuthRepository).save(existingOAuth); + assertEquals("test@example.com", existingOAuth.getEmail()); + assertEquals("Test User", existingOAuth.getNickname()); + + verify(userRepository, never()).save(any(User.class)); + verify(accountService, never()).createNewAccount(anyInt(), anyLong()); + + assertInstanceOf(CustomOAuth2User.class, result); + assertEquals("Test User", result.getName()); + } +} \ No newline at end of file From d9919eb49d732894e027923fedc8bbc1efe4411c Mon Sep 17 00:00:00 2001 From: caniro Date: Fri, 6 Jun 2025 21:05:50 +0900 Subject: [PATCH 6/7] =?UTF-8?q?refactor:=20Wallet=20Service=EC=97=90?= =?UTF-8?q?=EC=84=9C=20Account=20Service=20=EB=8C=80=EC=8B=A0=20Repository?= =?UTF-8?q?=EB=A5=BC=20=EC=B0=B8=EC=A1=B0=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=ED=95=98=EC=97=AC=20=EC=88=9C=ED=99=98=EC=B0=B8=EC=A1=B0=20?= =?UTF-8?q?=EB=B0=A9=EC=A7=80=20-=20account=20=EB=8F=84=EB=A9=94=EC=9D=B8?= =?UTF-8?q?=EC=9D=98=20=EB=A1=9C=EC=A7=81=EC=9D=B4=20=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EA=B2=8C=20=EC=95=84=EB=8B=8C,=20=EB=8B=A8?= =?UTF-8?q?=EC=88=9C=20accountId=EA=B0=80=20=ED=95=84=EC=9A=94=ED=95=9C=20?= =?UTF-8?q?=EA=B2=83=EC=9D=B4=EB=AF=80=EB=A1=9C=20Repository=EB=A5=BC=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EB=8A=94=20=EA=B2=83=EC=9D=B4=20?= =?UTF-8?q?=EB=8D=94=20=EC=A0=81=ED=95=A9=ED=95=98=EB=8B=A4=EA=B3=A0=20?= =?UTF-8?q?=ED=8C=90=EB=8B=A8=EB=90=98=EC=96=B4=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=ED=95=98=EC=98=80=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/info/application/WalletService.java | 26 ++++++------------- .../info/presentation/UserController.java | 2 +- .../info/presentation/UserControllerTest.java | 2 +- 3 files changed, 10 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/cleanengine/coin/user/info/application/WalletService.java b/src/main/java/com/cleanengine/coin/user/info/application/WalletService.java index dc33b957..3a0822a1 100644 --- a/src/main/java/com/cleanengine/coin/user/info/application/WalletService.java +++ b/src/main/java/com/cleanengine/coin/user/info/application/WalletService.java @@ -1,7 +1,7 @@ package com.cleanengine.coin.user.info.application; -import com.cleanengine.coin.user.domain.Account; import com.cleanengine.coin.user.domain.Wallet; +import com.cleanengine.coin.user.info.infra.AccountRepository; import com.cleanengine.coin.user.info.infra.WalletRepository; import org.springframework.stereotype.Service; @@ -11,14 +11,14 @@ public class WalletService { private final WalletRepository walletRepository; - private final AccountService accountService; + private final AccountRepository accountRepository; - public WalletService(WalletRepository walletRepository, AccountService accountService) { + public WalletService(WalletRepository walletRepository, AccountRepository accountRepository) { this.walletRepository = walletRepository; - this.accountService = accountService; + this.accountRepository = accountRepository; } - public List retrieveWalletsByAccountId(Integer accountId) { + public List findByAccountId(Integer accountId) { return walletRepository.findByAccountId(accountId); } @@ -27,19 +27,9 @@ public Wallet save(Wallet wallet) { } public Wallet findWalletByUserIdAndTicker(Integer userId, String ticker) { - Account account = accountService.findAccountByUserId(userId).orElseThrow(); - return walletRepository.findByAccountIdAndTicker(account.getId(), ticker) - .orElseGet(() -> createNewWallet(account.getId(), ticker)); - } - - public Wallet createNewWallet(Integer accountId, String ticker) { - Wallet newWallet = new Wallet(); - newWallet.setAccountId(accountId); - newWallet.setTicker(ticker); - newWallet.setSize(0.0); - newWallet.setBuyPrice(0.0); - newWallet.setRoi(0.0); - return newWallet; + int accountId = accountRepository.findByUserId(userId).orElseThrow().getId(); + return walletRepository.findByAccountIdAndTicker(accountId, ticker) + .orElseGet(() -> Wallet.generateEmptyWallet(ticker, accountId)); } } diff --git a/src/main/java/com/cleanengine/coin/user/info/presentation/UserController.java b/src/main/java/com/cleanengine/coin/user/info/presentation/UserController.java index 60580b4c..0ad1bbf5 100644 --- a/src/main/java/com/cleanengine/coin/user/info/presentation/UserController.java +++ b/src/main/java/com/cleanengine/coin/user/info/presentation/UserController.java @@ -41,7 +41,7 @@ public ApiResponse retrieveUserInfo() { return ApiResponse.fail(ErrorResponse.of(ErrorStatus.UNAUTHORIZED_RESOURCE)); } Account account = accountService.retrieveAccountByUserId(userId); - List wallets = walletService.retrieveWalletsByAccountId(account.getId()); + List wallets = walletService.findByAccountId(account.getId()); userInfoDTO.setWallets(wallets); return ApiResponse.success(userInfoDTO, HttpStatus.OK); diff --git a/src/test/java/com/cleanengine/coin/user/info/presentation/UserControllerTest.java b/src/test/java/com/cleanengine/coin/user/info/presentation/UserControllerTest.java index 7e77629e..dcf80af7 100644 --- a/src/test/java/com/cleanengine/coin/user/info/presentation/UserControllerTest.java +++ b/src/test/java/com/cleanengine/coin/user/info/presentation/UserControllerTest.java @@ -101,7 +101,7 @@ public void testRetrieveUserInfoSuccess() throws Exception { verify(userService, times(1)).retrieveUserInfoByUserId(userId); verify(accountService, times(1)).retrieveAccountByUserId(userId); - verify(walletService, times(1)).retrieveWalletsByAccountId(account.getId()); + verify(walletService, times(1)).findByAccountId(account.getId()); } @Test From af25cf18c7991fea11b70e5f60a57e83f6efe14d Mon Sep 17 00:00:00 2001 From: caniro Date: Fri, 6 Jun 2025 21:25:51 +0900 Subject: [PATCH 7/7] =?UTF-8?q?test:=20Wallet=20=ED=86=B5=ED=95=A9?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cleanengine/coin/user/domain/Wallet.java | 34 ++++++- .../user/info/application/WalletService.java | 2 +- .../info/application/WalletServiceTest.java | 95 +++++++++++++++++++ 3 files changed, 126 insertions(+), 5 deletions(-) create mode 100644 src/test/java/com/cleanengine/coin/user/info/application/WalletServiceTest.java diff --git a/src/main/java/com/cleanengine/coin/user/domain/Wallet.java b/src/main/java/com/cleanengine/coin/user/domain/Wallet.java index df793cc8..b068b5fa 100644 --- a/src/main/java/com/cleanengine/coin/user/domain/Wallet.java +++ b/src/main/java/com/cleanengine/coin/user/domain/Wallet.java @@ -1,10 +1,7 @@ package com.cleanengine.coin.user.domain; import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.*; @Setter @Getter @@ -34,6 +31,35 @@ public class Wallet { @Column(name = "roi") private Double roi; // Return on Investment (수익률) + @Builder + private Wallet(String ticker, Integer accountId, Double size, Double buyPrice, Double roi) { + this.ticker = ticker; + this.accountId = accountId; + this.size = size; + this.buyPrice = buyPrice; + this.roi = roi; + } + + public static Wallet of(String ticker, Integer accountId) { + return Wallet.builder() + .ticker(ticker) + .accountId(accountId) + .size(0.0) + .buyPrice(0.0) + .roi(0.0) + .build(); + } + + public static Wallet of(String ticker, Integer accountId, Double size) { + return Wallet.builder() + .ticker(ticker) + .accountId(accountId) + .size(size) + .buyPrice(0.0) + .roi(0.0) + .build(); + } + public static Wallet generateEmptyWallet(String ticker, Integer accountId){ Wallet wallet = new Wallet(); wallet.setTicker(ticker); diff --git a/src/main/java/com/cleanengine/coin/user/info/application/WalletService.java b/src/main/java/com/cleanengine/coin/user/info/application/WalletService.java index 3a0822a1..2dd61722 100644 --- a/src/main/java/com/cleanengine/coin/user/info/application/WalletService.java +++ b/src/main/java/com/cleanengine/coin/user/info/application/WalletService.java @@ -29,7 +29,7 @@ public Wallet save(Wallet wallet) { public Wallet findWalletByUserIdAndTicker(Integer userId, String ticker) { int accountId = accountRepository.findByUserId(userId).orElseThrow().getId(); return walletRepository.findByAccountIdAndTicker(accountId, ticker) - .orElseGet(() -> Wallet.generateEmptyWallet(ticker, accountId)); + .orElseGet(() -> Wallet.of(ticker, accountId)); } } diff --git a/src/test/java/com/cleanengine/coin/user/info/application/WalletServiceTest.java b/src/test/java/com/cleanengine/coin/user/info/application/WalletServiceTest.java new file mode 100644 index 00000000..2ec3e644 --- /dev/null +++ b/src/test/java/com/cleanengine/coin/user/info/application/WalletServiceTest.java @@ -0,0 +1,95 @@ +package com.cleanengine.coin.user.info.application; + +import com.cleanengine.coin.user.domain.Account; +import com.cleanengine.coin.user.domain.Wallet; +import com.cleanengine.coin.user.info.infra.AccountRepository; +import com.cleanengine.coin.user.info.infra.WalletRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("지갑 서비스-Repository 통합테스트") +@ActiveProfiles({"dev", "h2-mem"}) +@Transactional +@SpringBootTest +class WalletServiceTest { + + @Autowired + private WalletService walletService; + + @Autowired + private WalletRepository walletRepository; + + @Autowired + private AccountRepository accountRepository; + + private Account testAccount; + + @BeforeEach + void setUp() { + // given + testAccount = Account.of(3, 0.0); + accountRepository.save(testAccount); + + Wallet testWallet = Wallet.of("BTC", testAccount.getId(), 1000.0); + walletRepository.save(testWallet); + } + + @DisplayName("계좌 ID로 조회 시 지갑이 정상 반환된다.") + @Test + void findByAccountId_thenReturnWallet() { + // when + List wallets = walletService.findByAccountId(testAccount.getId()); + + // then + assertThat(wallets).isNotEmpty(); + assertThat(wallets.getFirst().getTicker()).isEqualTo("BTC"); + } + + @DisplayName("지갑이 성공적으로 저장된다.") + @Test + void save_thenCreateNewWallet() { + // when + Wallet newWallet = Wallet.of("TRUMP", testAccount.getId(), 5000.0); + Wallet savedWallet = walletService.save(newWallet); + + // then + assertThat(savedWallet.getId()).isNotNull(); + assertThat(savedWallet.getTicker()).isEqualTo("TRUMP"); + assertThat(savedWallet.getSize()).isEqualTo(5000.0); + } + + @DisplayName("유저 ID, 티커로 존재하는 지갑 조회 시 정상적으로 반환된다.") + @Test + void findWalletByUserIdAndTicker_ExistingWallet_thenReturnWallet() { + // when + Wallet wallet = walletService.findWalletByUserIdAndTicker(testAccount.getUserId(), "BTC"); + + // then + assertThat(wallet).isNotNull(); + assertThat(wallet.getId()).isNotNull(); + assertThat(wallet.getTicker()).isEqualTo("BTC"); + assertThat(wallet.getSize()).isEqualTo(1000.0); + } + + @DisplayName("유저 ID, 티커로 존재하지 않는 지갑 조회 시 빈 지갑이 새로 반환된다.") + @Test + void findWalletByUserIdAndTicker_NonExistingWallet_thenReturnEmptyWallet() { + // when + Wallet wallet = walletService.findWalletByUserIdAndTicker(testAccount.getUserId(), "TRUMP"); + + // then + assertThat(wallet).isNotNull(); + assertThat(wallet.getTicker()).isEqualTo("TRUMP"); + assertThat(wallet.getSize()).isEqualTo(0.0); + } + +} \ No newline at end of file