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 9185877..491580b 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 97% 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..613e873 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; @@ -94,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); } 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; 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..09002e7 --- /dev/null +++ b/src/test/java/corecord/dev/auth/util/JwtUtilTest.java @@ -0,0 +1,148 @@ +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; + 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); + 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() { + // 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() { + // 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() { + // 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() { + // 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 + 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); + } +} 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(); + } +}