From 50cce2ad56bffb510603ec7ddcf664814af2575f Mon Sep 17 00:00:00 2001 From: oosedus Date: Tue, 12 Nov 2024 15:14:21 +0900 Subject: [PATCH 1/6] =?UTF-8?q?refactor=20:=20jwt=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20auth=20=ED=8C=A8=ED=82=A4=EC=A7=80?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/corecord/dev/common/config/SecurityConfig.java | 2 ++ .../dev/domain/auth/handler/OAuthLoginSuccessHandler.java | 2 +- .../java/corecord/dev/domain/auth/service/TokenService.java | 2 +- .../corecord/dev/{common => domain/auth}/util/JwtFilter.java | 3 ++- .../corecord/dev/{common => domain/auth}/util/JwtUtil.java | 3 ++- .../java/corecord/dev/domain/user/service/UserService.java | 2 +- 6 files changed, 9 insertions(+), 5 deletions(-) rename src/main/java/corecord/dev/{common => domain/auth}/util/JwtFilter.java (97%) rename src/main/java/corecord/dev/{common => domain/auth}/util/JwtUtil.java (98%) diff --git a/src/main/java/corecord/dev/common/config/SecurityConfig.java b/src/main/java/corecord/dev/common/config/SecurityConfig.java index 6946abd..af13baf 100644 --- a/src/main/java/corecord/dev/common/config/SecurityConfig.java +++ b/src/main/java/corecord/dev/common/config/SecurityConfig.java @@ -6,6 +6,8 @@ import corecord.dev.common.util.*; import corecord.dev.domain.auth.handler.OAuthLoginFailureHandler; import corecord.dev.domain.auth.handler.OAuthLoginSuccessHandler; +import corecord.dev.domain.auth.util.JwtFilter; +import corecord.dev.domain.auth.util.JwtUtil; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/src/main/java/corecord/dev/domain/auth/handler/OAuthLoginSuccessHandler.java b/src/main/java/corecord/dev/domain/auth/handler/OAuthLoginSuccessHandler.java index e14696d..5dad019 100644 --- a/src/main/java/corecord/dev/domain/auth/handler/OAuthLoginSuccessHandler.java +++ b/src/main/java/corecord/dev/domain/auth/handler/OAuthLoginSuccessHandler.java @@ -1,7 +1,7 @@ package corecord.dev.domain.auth.handler; import corecord.dev.common.util.CookieUtil; -import corecord.dev.common.util.JwtUtil; +import corecord.dev.domain.auth.util.JwtUtil; import corecord.dev.domain.auth.dto.KakaoUserInfo; import corecord.dev.domain.auth.dto.OAuth2UserInfo; import corecord.dev.domain.auth.entity.RefreshToken; diff --git a/src/main/java/corecord/dev/domain/auth/service/TokenService.java b/src/main/java/corecord/dev/domain/auth/service/TokenService.java index 4e770ca..5a84255 100644 --- a/src/main/java/corecord/dev/domain/auth/service/TokenService.java +++ b/src/main/java/corecord/dev/domain/auth/service/TokenService.java @@ -3,7 +3,7 @@ import corecord.dev.common.exception.GeneralException; import corecord.dev.common.status.ErrorStatus; import corecord.dev.common.util.CookieUtil; -import corecord.dev.common.util.JwtUtil; +import corecord.dev.domain.auth.util.JwtUtil; import corecord.dev.domain.auth.entity.RefreshToken; import corecord.dev.domain.auth.entity.TmpToken; import corecord.dev.domain.auth.exception.enums.TokenErrorStatus; diff --git a/src/main/java/corecord/dev/common/util/JwtFilter.java b/src/main/java/corecord/dev/domain/auth/util/JwtFilter.java similarity index 97% rename from src/main/java/corecord/dev/common/util/JwtFilter.java rename to src/main/java/corecord/dev/domain/auth/util/JwtFilter.java index ad372e4..ddc6197 100644 --- a/src/main/java/corecord/dev/common/util/JwtFilter.java +++ b/src/main/java/corecord/dev/domain/auth/util/JwtFilter.java @@ -1,5 +1,6 @@ -package corecord.dev.common.util; +package corecord.dev.domain.auth.util; +import corecord.dev.common.util.CookieUtil; import corecord.dev.domain.auth.exception.model.TokenException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; diff --git a/src/main/java/corecord/dev/common/util/JwtUtil.java b/src/main/java/corecord/dev/domain/auth/util/JwtUtil.java similarity index 98% rename from src/main/java/corecord/dev/common/util/JwtUtil.java rename to src/main/java/corecord/dev/domain/auth/util/JwtUtil.java index 66581b3..b36bbef 100644 --- a/src/main/java/corecord/dev/common/util/JwtUtil.java +++ b/src/main/java/corecord/dev/domain/auth/util/JwtUtil.java @@ -1,9 +1,10 @@ -package corecord.dev.common.util; +package corecord.dev.domain.auth.util; import corecord.dev.domain.auth.exception.enums.TokenErrorStatus; import corecord.dev.domain.auth.exception.model.TokenException; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/corecord/dev/domain/user/service/UserService.java b/src/main/java/corecord/dev/domain/user/service/UserService.java index 0c86596..651374b 100644 --- a/src/main/java/corecord/dev/domain/user/service/UserService.java +++ b/src/main/java/corecord/dev/domain/user/service/UserService.java @@ -3,7 +3,7 @@ import corecord.dev.common.exception.GeneralException; import corecord.dev.common.status.ErrorStatus; import corecord.dev.common.util.CookieUtil; -import corecord.dev.common.util.JwtUtil; +import corecord.dev.domain.auth.util.JwtUtil; import corecord.dev.domain.analysis.repository.AbilityRepository; import corecord.dev.domain.analysis.repository.AnalysisRepository; import corecord.dev.domain.folder.repository.FolderRepository; From 4b100b749c109f6f117b004f0ff5862796824a7b Mon Sep 17 00:00:00 2001 From: oosedus Date: Tue, 12 Nov 2024 15:15:13 +0900 Subject: [PATCH 2/6] =?UTF-8?q?fix=20:=20jwt=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/corecord/dev/domain/auth/util/JwtUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/corecord/dev/domain/auth/util/JwtUtil.java b/src/main/java/corecord/dev/domain/auth/util/JwtUtil.java index b36bbef..613e873 100644 --- a/src/main/java/corecord/dev/domain/auth/util/JwtUtil.java +++ b/src/main/java/corecord/dev/domain/auth/util/JwtUtil.java @@ -95,7 +95,7 @@ private boolean isTokenValid(String token, String claimKey, TokenErrorStatus err } catch (ExpiredJwtException e) { log.warn("토큰이 만료되었습니다: {}", e.getMessage()); throw new TokenException(errorStatus); - } catch (JwtException | IllegalArgumentException e) { + } catch (JwtException | MalformedJwtException e) { log.warn("유효하지 않은 토큰입니다: {}", e.getMessage()); throw new TokenException(errorStatus); } From 26a29acb87e34efdfcb8f870154f196f0e8e7ba2 Mon Sep 17 00:00:00 2001 From: oosedus Date: Tue, 12 Nov 2024 15:17:00 +0900 Subject: [PATCH 3/6] =?UTF-8?q?test=20:=20jwtUtil=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../corecord/dev/auth/util/JwtUtilTest.java | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 src/test/java/corecord/dev/auth/util/JwtUtilTest.java diff --git a/src/test/java/corecord/dev/auth/util/JwtUtilTest.java b/src/test/java/corecord/dev/auth/util/JwtUtilTest.java new file mode 100644 index 0000000..ddfaa94 --- /dev/null +++ b/src/test/java/corecord/dev/auth/util/JwtUtilTest.java @@ -0,0 +1,157 @@ +package corecord.dev.auth.util; + +import corecord.dev.domain.auth.util.JwtUtil; +import corecord.dev.domain.auth.exception.enums.TokenErrorStatus; +import corecord.dev.domain.auth.exception.model.TokenException; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import javax.crypto.SecretKey; + +import java.util.Date; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class JwtUtilTest { + + private JwtUtil jwtUtil; + private final String SECRET_KEY = "testsecretkeytestsecretkeytestsecretkeytestsecretkeytestsecretkeytestsecretkeytestsecretkeytestsecretkey"; + private final long REGISTER_TOKEN_EXPIRE_TIME = 1000 * 60 * 60; // 1 hour + private final long ACCESS_TOKEN_EXPIRE_TIME = 1000 * 60 * 60 * 24; // 24 hours + private final long REFRESH_TOKEN_EXPIRE_TIME = 1000 * 60 * 60 * 24 * 7; // 7 days + private SecretKey key; + + @BeforeEach + void setUp() { + jwtUtil = new JwtUtil(); + ReflectionTestUtils.setField(jwtUtil, "SECRET_KEY", SECRET_KEY); + ReflectionTestUtils.setField(jwtUtil, "REGISTER_TOKEN_EXPIRATION_TIME", REGISTER_TOKEN_EXPIRE_TIME); + ReflectionTestUtils.setField(jwtUtil, "ACCESS_TOKEN_EXPIRATION_TIME", ACCESS_TOKEN_EXPIRE_TIME); + ReflectionTestUtils.setField(jwtUtil, "REFRESH_TOKEN_EXPIRATION_TIME", REFRESH_TOKEN_EXPIRE_TIME); + key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(SECRET_KEY)); + } + + @Test + @DisplayName("액세스 토큰 생성 및 유효성 검사") + void generateAndValidateAccessToken() { + // given + Long userId = 1L; + + // when + String accessToken = jwtUtil.generateAccessToken(userId); + + // then + assertThat(accessToken).isNotNull().isNotEmpty(); + assertThat(accessToken.split("\\.")).hasSize(3); + + Claims payload = Jwts.parser() + .setSigningKey(key) + .build() + .parseClaimsJws(accessToken) + .getBody(); + + assertThat(payload.get("userId", String.class)).isEqualTo(userId.toString()); + } + + @Test + @DisplayName("리프레쉬 토큰 생성 및 유효성 검사") + void generateAndValidateRefreshToken() { + // given + Long userId = 1L; + + // when + String refreshToken = jwtUtil.generateRefreshToken(userId); + + // then + assertThat(refreshToken).isNotNull().isNotEmpty(); + assertThat(refreshToken.split("\\.")).hasSize(3); + + Claims payload = Jwts.parser() + .setSigningKey(key) + .build() + .parseClaimsJws(refreshToken) + .getBody(); + + assertThat(payload.get("userId", String.class)).isEqualTo(userId.toString()); + } + + @Test + @DisplayName("레지스터 토큰 생성 및 유효성 검사") + void generateAndValidateRegisterToken() { + // given + String providerId = "testProvider"; + + // when + String registerToken = jwtUtil.generateRegisterToken(providerId); + + // then + assertThat(registerToken).isNotNull().isNotEmpty(); + assertThat(registerToken.split("\\.")).hasSize(3); + + Claims payload = Jwts.parser() + .setSigningKey(key) + .build() + .parseClaimsJws(registerToken) + .getBody(); + + assertThat(payload.get("providerId", String.class)).isEqualTo(providerId); + } + + @Test + @DisplayName("임시 토큰 생성 및 유효성 검사") + void generateAndValidateTmpToken() { + // given + Long userId = 1L; + + // when + String tmpToken = jwtUtil.generateTmpToken(userId); + + // then + assertThat(tmpToken).isNotNull().isNotEmpty(); + assertThat(tmpToken.split("\\.")).hasSize(3); + + Claims payload = Jwts.parser() + .setSigningKey(key) + .build() + .parseClaimsJws(tmpToken) + .getBody(); + + assertThat(payload.get("userId", String.class)).isEqualTo(userId.toString()); + } + + @Test + @DisplayName("만료된 액세스 토큰 예외 발생") + void expiredAccessTokenThrowsException() { + // given + Long userId = 1L; + String expiredAccessToken = Jwts.builder() + .setSubject(userId.toString()) + .setExpiration(new Date(System.currentTimeMillis() - 1000)) // 이미 만료된 시간 설정 + .signWith(SignatureAlgorithm.HS256, key) + .compact(); + + // then + TokenException exception = assertThrows(TokenException.class, () -> jwtUtil.isAccessTokenValid(expiredAccessToken)); + assertThat(exception.getTokenErrorStatus()).isEqualTo(TokenErrorStatus.INVALID_ACCESS_TOKEN); + } + + + @Test + @DisplayName("유효하지 않은 토큰 예외 발생") + void invalidTokenThrowsException() { + // given + String invalidToken = "invalid.token"; + + // then + TokenException exception = assertThrows(TokenException.class, () -> jwtUtil.isAccessTokenValid(invalidToken)); + assertThat(exception.getTokenErrorStatus()).isEqualTo(TokenErrorStatus.INVALID_ACCESS_TOKEN); + } +} From 553a1415b6b710590cc1a8e84518e1a810c43b64 Mon Sep 17 00:00:00 2001 From: oosedus Date: Wed, 13 Nov 2024 15:01:18 +0900 Subject: [PATCH 4/6] =?UTF-8?q?fix=20:=20=EC=B6=A9=EB=8F=8C=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/corecord/dev/domain/user/service/UserService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/corecord/dev/domain/user/service/UserService.java b/src/main/java/corecord/dev/domain/user/service/UserService.java index d205a39..e3f7604 100644 --- a/src/main/java/corecord/dev/domain/user/service/UserService.java +++ b/src/main/java/corecord/dev/domain/user/service/UserService.java @@ -3,7 +3,7 @@ import corecord.dev.common.exception.GeneralException; import corecord.dev.common.status.ErrorStatus; import corecord.dev.common.util.CookieUtil; -import corecord.dev.common.util.JwtUtil; +import corecord.dev.domain.auth.util.JwtUtil; import corecord.dev.domain.ability.repository.AbilityRepository; import corecord.dev.domain.analysis.repository.AnalysisRepository; import corecord.dev.domain.folder.repository.FolderRepository; From 62ed901d1de66d998ecea71b682001970b0b670e Mon Sep 17 00:00:00 2001 From: oosedus Date: Fri, 15 Nov 2024 00:40:21 +0900 Subject: [PATCH 5/6] =?UTF-8?q?test=20:=20JwtUtil=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20setup=EC=9C=BC=EB=A1=9C=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=83=9D=EB=9E=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../corecord/dev/auth/util/JwtUtilTest.java | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/src/test/java/corecord/dev/auth/util/JwtUtilTest.java b/src/test/java/corecord/dev/auth/util/JwtUtilTest.java index ddfaa94..09002e7 100644 --- a/src/test/java/corecord/dev/auth/util/JwtUtilTest.java +++ b/src/test/java/corecord/dev/auth/util/JwtUtilTest.java @@ -28,9 +28,13 @@ public class JwtUtilTest { private final long ACCESS_TOKEN_EXPIRE_TIME = 1000 * 60 * 60 * 24; // 24 hours private final long REFRESH_TOKEN_EXPIRE_TIME = 1000 * 60 * 60 * 24 * 7; // 7 days private SecretKey key; + private Long userId; + private String providerId; @BeforeEach void setUp() { + userId = 1L; + providerId = "testProvider"; jwtUtil = new JwtUtil(); ReflectionTestUtils.setField(jwtUtil, "SECRET_KEY", SECRET_KEY); ReflectionTestUtils.setField(jwtUtil, "REGISTER_TOKEN_EXPIRATION_TIME", REGISTER_TOKEN_EXPIRE_TIME); @@ -42,9 +46,6 @@ void setUp() { @Test @DisplayName("액세스 토큰 생성 및 유효성 검사") void generateAndValidateAccessToken() { - // given - Long userId = 1L; - // when String accessToken = jwtUtil.generateAccessToken(userId); @@ -64,9 +65,6 @@ void generateAndValidateAccessToken() { @Test @DisplayName("리프레쉬 토큰 생성 및 유효성 검사") void generateAndValidateRefreshToken() { - // given - Long userId = 1L; - // when String refreshToken = jwtUtil.generateRefreshToken(userId); @@ -86,9 +84,6 @@ void generateAndValidateRefreshToken() { @Test @DisplayName("레지스터 토큰 생성 및 유효성 검사") void generateAndValidateRegisterToken() { - // given - String providerId = "testProvider"; - // when String registerToken = jwtUtil.generateRegisterToken(providerId); @@ -108,9 +103,6 @@ void generateAndValidateRegisterToken() { @Test @DisplayName("임시 토큰 생성 및 유효성 검사") void generateAndValidateTmpToken() { - // given - Long userId = 1L; - // when String tmpToken = jwtUtil.generateTmpToken(userId); @@ -131,7 +123,6 @@ void generateAndValidateTmpToken() { @DisplayName("만료된 액세스 토큰 예외 발생") void expiredAccessTokenThrowsException() { // given - Long userId = 1L; String expiredAccessToken = Jwts.builder() .setSubject(userId.toString()) .setExpiration(new Date(System.currentTimeMillis() - 1000)) // 이미 만료된 시간 설정 From ad20f897114bec7dce79ca47eeab1146acb3a96b Mon Sep 17 00:00:00 2001 From: oosedus Date: Fri, 15 Nov 2024 04:15:46 +0900 Subject: [PATCH 6/6] =?UTF-8?q?test=20:=20User=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/repository/UserRepositoryTest.java | 59 ++++++ .../dev/user/service/UserServiceTest.java | 184 ++++++++++++++++++ 2 files changed, 243 insertions(+) create mode 100644 src/test/java/corecord/dev/user/repository/UserRepositoryTest.java create mode 100644 src/test/java/corecord/dev/user/service/UserServiceTest.java diff --git a/src/test/java/corecord/dev/user/repository/UserRepositoryTest.java b/src/test/java/corecord/dev/user/repository/UserRepositoryTest.java new file mode 100644 index 0000000..203b15c --- /dev/null +++ b/src/test/java/corecord/dev/user/repository/UserRepositoryTest.java @@ -0,0 +1,59 @@ +package corecord.dev.user.repository; + +import corecord.dev.domain.user.entity.Status; +import corecord.dev.domain.user.entity.User; +import corecord.dev.domain.user.repository.UserRepository; +import jakarta.persistence.EntityManager; +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.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.Optional; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@DataJpaTest +@Transactional +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +public class UserRepositoryTest { + @Autowired + UserRepository userRepository; + + @Autowired + EntityManager entityManager; + + @Test + @DisplayName("UserId로 회원 삭제") + void deleteUserByUserId() { + // Given + User user = createTestUser(); + userRepository.save(user); + + // When + userRepository.deleteUserByUserId(user.getUserId()); + entityManager.flush(); + entityManager.clear(); + + // Then + Optional deletedUser = userRepository.findById(user.getUserId()); + assertThat(deletedUser).isEmpty(); + } + + + private User createTestUser() { + return User.builder() + .userId(1L) + .providerId("providerId") + .nickName("testUser") + .status(Status.UNIVERSITY_STUDENT) + .abilities(new ArrayList<>()) + .chatRooms(new ArrayList<>()) + .folders(new ArrayList<>()) + .records(new ArrayList<>()) + .build(); + } +} diff --git a/src/test/java/corecord/dev/user/service/UserServiceTest.java b/src/test/java/corecord/dev/user/service/UserServiceTest.java new file mode 100644 index 0000000..35f8323 --- /dev/null +++ b/src/test/java/corecord/dev/user/service/UserServiceTest.java @@ -0,0 +1,184 @@ +package corecord.dev.user.service; + +import corecord.dev.common.util.CookieUtil; +import corecord.dev.domain.auth.repository.RefreshTokenRepository; +import corecord.dev.domain.auth.util.JwtUtil; +import corecord.dev.domain.record.repository.RecordRepository; +import corecord.dev.domain.user.dto.request.UserRequest; +import corecord.dev.domain.user.dto.response.UserResponse; +import corecord.dev.domain.user.entity.Status; +import corecord.dev.domain.user.entity.User; +import corecord.dev.domain.user.exception.enums.UserErrorStatus; +import corecord.dev.domain.user.exception.model.UserException; +import corecord.dev.domain.user.repository.UserRepository; +import corecord.dev.domain.user.service.UserService; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.Assertions; +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.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.ResponseCookie; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class UserServiceTest { + + @Mock + private JwtUtil jwtUtil; + + @Mock + private CookieUtil cookieUtil; + + @Mock + private UserRepository userRepository; + + @Mock + private RefreshTokenRepository refreshTokenRepository; + + @Mock + private RecordRepository recordRepository; + + @Mock + private HttpServletResponse response; + + @InjectMocks + private UserService userService; + + private static final long ACCESS_TOKEN_EXPIRATION = 86400000L; + private static final long REFRESH_TOKEN_EXPIRATION = 2592000000L; + private static final String REGISTER_TOKEN = "validRegisterToken"; + private static final String PROVIDER_ID = "1234567890"; + private static final String REFRESH_TOKEN = "generatedRefreshToken"; + private static final String ACCESS_TOKEN = "generatedAccessToken"; + private User newUser; + + @BeforeEach + void setUp() { + ReflectionTestUtils.setField(userService, "accessTokenExpirationTime", ACCESS_TOKEN_EXPIRATION); + ReflectionTestUtils.setField(userService, "refreshTokenExpirationTime", REFRESH_TOKEN_EXPIRATION); + newUser = createTestUser(PROVIDER_ID); + } + + @Test + @DisplayName("유저 회원가입 테스트") + void registerUser() { + // Given + UserRequest.UserRegisterDto userRegisterDto = new UserRequest.UserRegisterDto(); + userRegisterDto.setNickName("testUser"); + userRegisterDto.setStatus("대학생"); + + when(jwtUtil.isRegisterTokenValid(REGISTER_TOKEN)).thenReturn(true); + when(jwtUtil.getProviderIdFromToken(REGISTER_TOKEN)).thenReturn(PROVIDER_ID); + when(jwtUtil.generateRefreshToken(anyLong())).thenReturn(REFRESH_TOKEN); + when(jwtUtil.generateAccessToken(anyLong())).thenReturn(ACCESS_TOKEN); + when(userRepository.existsByProviderId(PROVIDER_ID)).thenReturn(false); + when(userRepository.save(any(User.class))).thenReturn(newUser); + when(cookieUtil.createTokenCookie(eq("refreshToken"), eq(REFRESH_TOKEN), eq(REFRESH_TOKEN_EXPIRATION))) + .thenReturn(ResponseCookie.from("refreshToken", REFRESH_TOKEN).build()); + when(cookieUtil.createTokenCookie(eq("accessToken"), eq(ACCESS_TOKEN), eq(ACCESS_TOKEN_EXPIRATION))) + .thenReturn(ResponseCookie.from("accessToken", ACCESS_TOKEN).build()); + + // When + UserResponse.UserDto userDto = userService.registerUser(response, REGISTER_TOKEN, userRegisterDto); + + // Then + assertThat(userDto.getNickname()).isEqualTo(newUser.getNickName()); + assertThat(userDto.getStatus()).isEqualTo(newUser.getStatus().getValue()); + + ArgumentCaptor cookieCaptor = ArgumentCaptor.forClass(String.class); + verify(response, times(2)).addHeader(eq("Set-Cookie"), cookieCaptor.capture()); + + assertThat(cookieCaptor.getAllValues()).containsExactlyInAnyOrder( + ResponseCookie.from("refreshToken", REFRESH_TOKEN).build().toString(), + ResponseCookie.from("accessToken", ACCESS_TOKEN).build().toString() + ); + + } + + @Test + @DisplayName("회원 정보 조회 테스트") + void getUserInfo() { + // Given + when(userRepository.findById(newUser.getUserId())).thenReturn(Optional.of(newUser)); + + // When + UserResponse.UserInfoDto userInfoDto = userService.getUserInfo(newUser.getUserId()); + + // Then + assertThat(userInfoDto.getNickname()).isEqualTo(newUser.getNickName()); + assertThat(userInfoDto.getStatus()).isEqualTo(newUser.getStatus().getValue()); + } + + @Test + @DisplayName("회원 정보 수정 테스트") + void updateUser() { + // Given + UserRequest.UserUpdateDto updateDto = new UserRequest.UserUpdateDto(); + updateDto.setNickName("editName"); + updateDto.setStatus("인턴"); + + when(userRepository.findById(newUser.getUserId())).thenReturn(Optional.of(newUser)); + + // When + userService.updateUser(newUser.getUserId(), updateDto); + + // Then + assertThat(newUser.getNickName()).isEqualTo("editName"); + assertThat(newUser.getStatus()).isEqualTo(Status.INTERN); + verify(userRepository).findById(newUser.getUserId()); + } + + @Test + @DisplayName("닉네임 유효성 검증 - 닉네임이 길이 초과일 때 예외 발생") + void validateUserInfo_NickNameExceedsLength_ThrowsUserException() { + // Given + String invalidNickName = "VeryLongNickname"; + UserRequest.UserRegisterDto userRegisterDto = new UserRequest.UserRegisterDto(); + userRegisterDto.setNickName(invalidNickName); + userRegisterDto.setStatus("대학생"); + + when(jwtUtil.isRegisterTokenValid(REGISTER_TOKEN)).thenReturn(true); + + // When & Then + UserException exception = Assertions.assertThrows(UserException.class, + () -> userService.registerUser(response, REGISTER_TOKEN, userRegisterDto)); + assertThat(exception.getUserErrorStatus()).isEqualTo(UserErrorStatus.INVALID_USER_NICKNAME); + } + + @Test + @DisplayName("닉네임 유효성 검증 - 닉네임에 허용되지 않는 문자가 포함된 경우 예외 발생") + void validateUserInfo_NickNameHasInvalidCharacters_ThrowsUserException() { + // Given + String invalidNickName = "Invalid@Nickname!"; + UserRequest.UserRegisterDto userRegisterDto = new UserRequest.UserRegisterDto(); + userRegisterDto.setNickName(invalidNickName); + userRegisterDto.setStatus("대학생"); + + when(jwtUtil.isRegisterTokenValid(REGISTER_TOKEN)).thenReturn(true); + + // When & Then + UserException exception = Assertions.assertThrows(UserException.class, + () -> userService.registerUser(response, REGISTER_TOKEN, userRegisterDto)); + assertThat(exception.getUserErrorStatus()).isEqualTo(UserErrorStatus.INVALID_USER_NICKNAME); + } + + private User createTestUser(String providerId) { + return User.builder() + .userId(1L) + .providerId(providerId) + .nickName("testUser") + .status(Status.UNIVERSITY_STUDENT) + .build(); + } +}