diff --git a/src/test/java/com/example/solidconnection/unit/service/ApplicationServiceTest.java b/src/test/java/com/example/solidconnection/unit/application/service/ApplicationServiceTest.java similarity index 99% rename from src/test/java/com/example/solidconnection/unit/service/ApplicationServiceTest.java rename to src/test/java/com/example/solidconnection/unit/application/service/ApplicationServiceTest.java index dd87a38..0b74f8b 100644 --- a/src/test/java/com/example/solidconnection/unit/service/ApplicationServiceTest.java +++ b/src/test/java/com/example/solidconnection/unit/application/service/ApplicationServiceTest.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.unit.service; +package com.example.solidconnection.unit.application.service; import com.example.solidconnection.application.domain.Application; import com.example.solidconnection.application.domain.Gpa; diff --git a/src/test/java/com/example/solidconnection/unit/auth/service/AuthServiceTest.java b/src/test/java/com/example/solidconnection/unit/auth/service/AuthServiceTest.java new file mode 100644 index 0000000..00ba657 --- /dev/null +++ b/src/test/java/com/example/solidconnection/unit/auth/service/AuthServiceTest.java @@ -0,0 +1,164 @@ +package com.example.solidconnection.unit.auth.service; + +import com.example.solidconnection.auth.dto.ReissueResponse; +import com.example.solidconnection.auth.service.AuthService; +import com.example.solidconnection.config.token.TokenService; +import com.example.solidconnection.config.token.TokenType; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.custom.exception.ErrorCode; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +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.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; + +import java.time.LocalDate; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@DisplayName("인증 서비스 테스트") +class AuthServiceTest { + + @InjectMocks + private AuthService authService; + + @Mock + private TokenService tokenService; + + @Mock + private SiteUserRepository siteUserRepository; + + @Mock + private RedisTemplate redisTemplate; + + @Mock + private ValueOperations valueOperations; + + private static final String TEST_ACCESS_TOKEN = "testAccessToken"; + private static final String TEST_REFRESH_TOKEN = "testRefreshToken"; + private static final String SIGN_OUT_VALUE = "signOut"; + + private SiteUser testUser; + + @BeforeEach + void setUp() { + testUser = createTestUser(); + } + + @Test + @DisplayName("로그아웃_요청시_리프레시_토큰을_무효화한다()") + void shouldInvalidateRefreshTokenWhenSignOut() { + // given + String refreshTokenKey = TokenType.REFRESH.addTokenPrefixToSubject(testUser.getEmail()); + when(redisTemplate.opsForValue()).thenReturn(valueOperations); + + // when + authService.signOut(testUser.getEmail()); + + // then & verify + verify(valueOperations).set( + eq(refreshTokenKey), + eq(SIGN_OUT_VALUE), + eq(604800000L), + eq(TimeUnit.MILLISECONDS) + ); + } + + + @Test + @DisplayName("회원탈퇴_요청시_탈퇴일자를_설정한다()") + void shouldSetQuitedAtWhenUserQuits() { + // given + when(siteUserRepository.getByEmail(testUser.getEmail())).thenReturn(testUser); + + // when + authService.quit(testUser.getEmail()); + + // then + assertThat(testUser.getQuitedAt()).isNotNull(); + assertThat(testUser.getQuitedAt()).isEqualTo(LocalDate.now().plusDays(1)); + + // verify + verify(siteUserRepository).getByEmail(testUser.getEmail()); + } + + @Test + @DisplayName("존재하지_않는_이메일로_회원탈퇴_요청시_예외를_반환한다()") + void shouldThrowExceptionWhenQuitWithNonExistentEmail() { + // given + when(siteUserRepository.getByEmail(testUser.getEmail())) + .thenThrow(new CustomException(ErrorCode.USER_NOT_FOUND)); + + // when & then + CustomException exception = assertThrows(CustomException.class, + () -> authService.quit(testUser.getEmail())); + assertThat(exception.getCode()).isEqualTo(ErrorCode.USER_NOT_FOUND.getCode()); + + // verify + verify(siteUserRepository).getByEmail(testUser.getEmail()); + } + + @Test + @DisplayName("유효한_리프레시_토큰으로_재발급_요청시_새로운_액세스_토큰을_반환한다()") + void shouldReturnNewAccessTokenWhenRefreshTokenValid() { + // given + String refreshTokenKey = TokenType.REFRESH.addTokenPrefixToSubject(testUser.getEmail()); + when(redisTemplate.opsForValue()).thenReturn(valueOperations); + when(valueOperations.get(refreshTokenKey)).thenReturn(TEST_REFRESH_TOKEN); + when(tokenService.generateToken(testUser.getEmail(), TokenType.ACCESS)).thenReturn(TEST_ACCESS_TOKEN); + + // when + ReissueResponse response = authService.reissue(testUser.getEmail()); + + // then + assertThat(response.accessToken()).isEqualTo(TEST_ACCESS_TOKEN); + + // verify + verify(valueOperations).get(refreshTokenKey); + verify(tokenService).generateToken(testUser.getEmail(), TokenType.ACCESS); + verify(tokenService).saveToken(TEST_ACCESS_TOKEN, TokenType.ACCESS); + } + + @Test + @DisplayName("만료된_리프레시_토큰으로_재발급_요청시_예외를_반환한다()") + void shouldThrowExceptionWhenRefreshTokenExpired() { + // given + String refreshTokenKey = TokenType.REFRESH.addTokenPrefixToSubject(testUser.getEmail()); + when(redisTemplate.opsForValue()).thenReturn(valueOperations); + when(valueOperations.get(refreshTokenKey)).thenReturn(null); + + // when & then + CustomException exception = assertThrows(CustomException.class, + () -> authService.reissue(testUser.getEmail())); + assertThat(exception.getCode()).isEqualTo(ErrorCode.REFRESH_TOKEN_EXPIRED.getCode()); + + // verify + verify(valueOperations).get(refreshTokenKey); + verify(tokenService, never()).generateToken(any(), any()); + } + + private SiteUser createTestUser() { + return new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-10-21", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + } +} \ No newline at end of file diff --git a/src/test/java/com/example/solidconnection/unit/auth/service/SignInServiceTest.java b/src/test/java/com/example/solidconnection/unit/auth/service/SignInServiceTest.java new file mode 100644 index 0000000..2da8641 --- /dev/null +++ b/src/test/java/com/example/solidconnection/unit/auth/service/SignInServiceTest.java @@ -0,0 +1,211 @@ +package com.example.solidconnection.unit.auth.service; + +import com.example.solidconnection.auth.client.KakaoOAuthClient; +import com.example.solidconnection.auth.dto.SignInResponse; +import com.example.solidconnection.auth.dto.kakao.FirstAccessResponse; +import com.example.solidconnection.auth.dto.kakao.KakaoCodeRequest; +import com.example.solidconnection.auth.dto.kakao.KakaoOauthResponse; +import com.example.solidconnection.auth.dto.kakao.KakaoUserInfoDto; +import com.example.solidconnection.auth.service.SignInService; +import com.example.solidconnection.config.token.TokenService; +import com.example.solidconnection.config.token.TokenType; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.custom.exception.ErrorCode; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +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.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDate; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@DisplayName("카카오 로그인 서비스 테스트") +class SignInServiceTest { + + @InjectMocks + private SignInService signInService; + + @Mock + private SiteUserRepository siteUserRepository; + + @Mock + private TokenService tokenService; + + @Mock + private KakaoOAuthClient kakaoOAuthClient; + + private static final String TEST_ACCESS_TOKEN = "testAccessToken"; + private static final String TEST_REFRESH_TOKEN = "testRefreshToken"; + private static final String TEST_KAKAO_OAUTH_TOKEN = "testKakaoOauthToken"; + private static final String VALID_CODE = "validCode"; + private static final String INVALID_CODE = "invalidCode"; + + private SiteUser testUser; + private KakaoUserInfoDto testKakaoUserInfo; + private KakaoCodeRequest validKakaoCodeRequest; + + @BeforeEach + void setUp() { + testUser = createTestUser(); + testKakaoUserInfo = createTestKakaoUserInfoDto(); + validKakaoCodeRequest = new KakaoCodeRequest(VALID_CODE); + } + + @Test + @DisplayName("기존_회원이_로그인하면_로그인_정보를_반환한다()") + void shouldReturnSignInInfoWhenUserAlreadyRegistered() { + // given + when(kakaoOAuthClient.processOauth(VALID_CODE)).thenReturn(testKakaoUserInfo); + when(siteUserRepository.existsByEmail(testUser.getEmail())).thenReturn(true); + when(siteUserRepository.getByEmail(testUser.getEmail())).thenReturn(testUser); + when(tokenService.generateToken(testUser.getEmail(), TokenType.ACCESS)).thenReturn(TEST_ACCESS_TOKEN); + when(tokenService.generateToken(testUser.getEmail(), TokenType.REFRESH)).thenReturn(TEST_REFRESH_TOKEN); + + // when + KakaoOauthResponse response = signInService.signIn(validKakaoCodeRequest); + + // then + assertThat(response).isInstanceOf(SignInResponse.class); + SignInResponse signInResponse = (SignInResponse) response; + assertThat(signInResponse.accessToken()).isEqualTo(TEST_ACCESS_TOKEN); + assertThat(signInResponse.refreshToken()).isEqualTo(TEST_REFRESH_TOKEN); + assertThat(signInResponse.isRegistered()).isTrue(); + + // verify + verify(kakaoOAuthClient).processOauth(VALID_CODE); + verify(siteUserRepository).existsByEmail(testUser.getEmail()); + verify(siteUserRepository).getByEmail(testUser.getEmail()); + verify(tokenService).generateToken(testUser.getEmail(), TokenType.ACCESS); + verify(tokenService).generateToken(testUser.getEmail(), TokenType.REFRESH); + verify(tokenService).saveToken(TEST_REFRESH_TOKEN, TokenType.REFRESH); + } + + @Test + @DisplayName("탈퇴한_회원이_로그인하면_탈퇴일자를_초기화하고_로그인_정보를_반환한다()") + void shouldResetQuitedAtAndReturnSignInInfoWhenQuitedUserSignsIn() { + // given + testUser.setQuitedAt(LocalDate.now().minusDays(1)); + + when(kakaoOAuthClient.processOauth(VALID_CODE)).thenReturn(testKakaoUserInfo); + when(siteUserRepository.existsByEmail(testUser.getEmail())).thenReturn(true); + when(siteUserRepository.getByEmail(testUser.getEmail())).thenReturn(testUser); + when(tokenService.generateToken(testUser.getEmail(), TokenType.ACCESS)).thenReturn(TEST_ACCESS_TOKEN); + when(tokenService.generateToken(testUser.getEmail(), TokenType.REFRESH)).thenReturn(TEST_REFRESH_TOKEN); + + // when + KakaoOauthResponse response = signInService.signIn(validKakaoCodeRequest); + + // then + assertThat(response).isInstanceOf(SignInResponse.class); + assertThat(testUser.getQuitedAt()).isNull(); + + // verify + verify(siteUserRepository).getByEmail(testUser.getEmail()); + } + + @Test + @DisplayName("신규_회원이_로그인하면_회원가입_정보를_반환한다()") + void shouldReturnSignUpInfoWhenUserNotRegistered() { + // given + when(kakaoOAuthClient.processOauth(VALID_CODE)).thenReturn(testKakaoUserInfo); + when(siteUserRepository.existsByEmail(testUser.getEmail())).thenReturn(false); + when(tokenService.generateToken(testUser.getEmail(), TokenType.KAKAO_OAUTH)).thenReturn(TEST_KAKAO_OAUTH_TOKEN); + + // when + KakaoOauthResponse response = signInService.signIn(validKakaoCodeRequest); + + // then + assertThat(response).isInstanceOf(FirstAccessResponse.class); + FirstAccessResponse firstAccessResponse = (FirstAccessResponse) response; + assertThat(firstAccessResponse.kakaoOauthToken()).isEqualTo(TEST_KAKAO_OAUTH_TOKEN); + assertThat(firstAccessResponse.isRegistered()).isFalse(); + assertThat(firstAccessResponse.email()).isEqualTo(testUser.getEmail()); + assertThat(firstAccessResponse.nickname()).isEqualTo("testNickname"); + assertThat(firstAccessResponse.profileImageUrl()).isEqualTo("testProfileImageUrl"); + + // verify + verify(siteUserRepository).existsByEmail(testUser.getEmail()); + verify(tokenService).generateToken(testUser.getEmail(), TokenType.KAKAO_OAUTH); + verify(tokenService).saveToken(TEST_KAKAO_OAUTH_TOKEN, TokenType.KAKAO_OAUTH); + } + + @Test + @DisplayName("유효하지_않은_인증_코드로_로그인_시도하면_예외를_반환한다()") + void shouldThrowExceptionWhenInvalidAuthCodeProvided() { + // given + KakaoCodeRequest invalidRequest = new KakaoCodeRequest(INVALID_CODE); + when(kakaoOAuthClient.processOauth(INVALID_CODE)) + .thenThrow(new CustomException(ErrorCode.INVALID_OR_EXPIRED_KAKAO_AUTH_CODE)); + + // when & then + CustomException exception = assertThrows(CustomException.class, + () -> signInService.signIn(invalidRequest)); + assertThat(exception.getCode()).isEqualTo(ErrorCode.INVALID_OR_EXPIRED_KAKAO_AUTH_CODE.getCode()); + + // verify + verify(kakaoOAuthClient).processOauth(INVALID_CODE); + } + + @Test + @DisplayName("카카오_리다이렉트_URI가_일치하지_않으면_예외를_반환한다()") + void shouldThrowExceptionWhenKakaoRedirectUriMismatch() { + // given + when(kakaoOAuthClient.processOauth(VALID_CODE)) + .thenThrow(new CustomException(ErrorCode.KAKAO_REDIRECT_URI_MISMATCH)); + + // when & then + CustomException exception = assertThrows(CustomException.class, + () -> signInService.signIn(validKakaoCodeRequest)); + assertThat(exception.getCode()).isEqualTo(ErrorCode.KAKAO_REDIRECT_URI_MISMATCH.getCode()); + + // verify + verify(kakaoOAuthClient).processOauth(VALID_CODE); + } + + @Test + @DisplayName("카카오_사용자_정보_조회에_실패하면_예외를_반환한다()") + void shouldThrowExceptionWhenKakaoUserInfoFetchFails() { + // given + when(kakaoOAuthClient.processOauth(VALID_CODE)) + .thenThrow(new CustomException(ErrorCode.KAKAO_USER_INFO_FAIL)); + + // when & then + CustomException exception = assertThrows(CustomException.class, + () -> signInService.signIn(validKakaoCodeRequest)); + assertThat(exception.getCode()).isEqualTo(ErrorCode.KAKAO_USER_INFO_FAIL.getCode()); + + // verify + verify(kakaoOAuthClient).processOauth(VALID_CODE); + } + + private SiteUser createTestUser() { + return new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-10-21", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + } + + private KakaoUserInfoDto createTestKakaoUserInfoDto() { + KakaoUserInfoDto.KakaoAccountDto kakaoAccountDto = new KakaoUserInfoDto.KakaoAccountDto( + new KakaoUserInfoDto.KakaoAccountDto.KakaoProfileDto("testProfileImageUrl", "testNickname"), + testUser.getEmail()); + return new KakaoUserInfoDto(kakaoAccountDto); + } +} \ No newline at end of file diff --git a/src/test/java/com/example/solidconnection/unit/auth/service/SignUpServiceTest.java b/src/test/java/com/example/solidconnection/unit/auth/service/SignUpServiceTest.java new file mode 100644 index 0000000..ce13305 --- /dev/null +++ b/src/test/java/com/example/solidconnection/unit/auth/service/SignUpServiceTest.java @@ -0,0 +1,213 @@ +package com.example.solidconnection.unit.auth.service; + +import com.example.solidconnection.auth.dto.SignUpRequest; +import com.example.solidconnection.auth.dto.SignUpResponse; +import com.example.solidconnection.auth.service.SignUpService; +import com.example.solidconnection.config.token.TokenService; +import com.example.solidconnection.config.token.TokenType; +import com.example.solidconnection.config.token.TokenValidator; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.custom.exception.ErrorCode; +import com.example.solidconnection.entity.Country; +import com.example.solidconnection.entity.Region; +import com.example.solidconnection.repositories.CountryRepository; +import com.example.solidconnection.repositories.InterestedCountyRepository; +import com.example.solidconnection.repositories.InterestedRegionRepository; +import com.example.solidconnection.repositories.RegionRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +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.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@DisplayName("회원가입 서비스 테스트") +class SignUpServiceTest { + + @InjectMocks + private SignUpService signUpService; + + @Mock + private SiteUserRepository siteUserRepository; + + @Mock + private TokenService tokenService; + + @Mock + private TokenValidator tokenValidator; + + @Mock + private RegionRepository regionRepository; + + @Mock + private CountryRepository countryRepository; + + @Mock + private InterestedRegionRepository interestedRegionRepository; + + @Mock + private InterestedCountyRepository interestedCountryRepository; + + private static final String TEST_KAKAO_TOKEN = "testKakaoToken"; + private static final String TEST_ACCESS_TOKEN = "testAccessToken"; + private static final String TEST_REFRESH_TOKEN = "testRefreshToken"; + private static final String TEST_NICKNAME = "testNickname"; + + private SignUpRequest validSignUpRequest; + private SiteUser testUser; + private Region testRegion; + private Country testCountry; + + @BeforeEach + void setUp() { + validSignUpRequest = createValidSignUpRequest(); + testUser = createTestUser(); + testRegion = createTestRegion(); + testCountry = createTestCountry(); + } + + @Test + @DisplayName("올바른_회원가입_요청시_회원가입에_성공하고_토큰을_반환한다()") + void shouldSuccessfullySignUpAndReturnTokens() { + // given + when(tokenService.getEmail(TEST_KAKAO_TOKEN)).thenReturn(testUser.getEmail()); + when(siteUserRepository.existsByEmail(testUser.getEmail())).thenReturn(false); + when(siteUserRepository.existsByNickname(TEST_NICKNAME)).thenReturn(false); + when(siteUserRepository.save(any(SiteUser.class))).thenReturn(testUser); + when(regionRepository.findByKoreanNames(any())).thenReturn(List.of(testRegion)); + when(countryRepository.findByKoreanNames(any())).thenReturn(List.of(testCountry)); + when(tokenService.generateToken(testUser.getEmail(), TokenType.ACCESS)).thenReturn(TEST_ACCESS_TOKEN); + when(tokenService.generateToken(testUser.getEmail(), TokenType.REFRESH)).thenReturn(TEST_REFRESH_TOKEN); + + // when + SignUpResponse response = signUpService.signUp(validSignUpRequest); + + // then + assertThat(response.accessToken()).isEqualTo(TEST_ACCESS_TOKEN); + assertThat(response.refreshToken()).isEqualTo(TEST_REFRESH_TOKEN); + + // verify + verify(tokenValidator).validateKakaoToken(TEST_KAKAO_TOKEN); + verify(siteUserRepository).save(any(SiteUser.class)); + verify(interestedRegionRepository).saveAll(any()); + verify(interestedCountryRepository).saveAll(any()); + verify(tokenService).saveToken(TEST_REFRESH_TOKEN, TokenType.REFRESH); + } + + @Test + @DisplayName("이미_사용중인_닉네임으로_회원가입_시도시_예외를_반환한다()") + void shouldThrowExceptionWhenNicknameAlreadyExists() { + // given + when(tokenService.getEmail(TEST_KAKAO_TOKEN)).thenReturn(testUser.getEmail()); + when(siteUserRepository.existsByNickname(TEST_NICKNAME)).thenReturn(true); + + // when & then + CustomException exception = assertThrows(CustomException.class, + () -> signUpService.signUp(validSignUpRequest)); + assertThat(exception.getCode()).isEqualTo(ErrorCode.NICKNAME_ALREADY_EXISTED.getCode()); + + // verify + verify(tokenValidator).validateKakaoToken(TEST_KAKAO_TOKEN); + verify(siteUserRepository).existsByNickname(TEST_NICKNAME); + verify(siteUserRepository, never()).save(any()); + } + + @Test + @DisplayName("이미_가입된_이메일로_회원가입_시도시_예외를_반환한다()") + void shouldThrowExceptionWhenEmailAlreadyExists() { + // given + when(tokenService.getEmail(TEST_KAKAO_TOKEN)).thenReturn(testUser.getEmail()); + when(siteUserRepository.existsByNickname(TEST_NICKNAME)).thenReturn(false); + when(siteUserRepository.existsByEmail(testUser.getEmail())).thenReturn(true); + + // when & then + CustomException exception = assertThrows(CustomException.class, + () -> signUpService.signUp(validSignUpRequest)); + assertThat(exception.getCode()).isEqualTo(ErrorCode.USER_ALREADY_EXISTED.getCode()); + + // verify + verify(siteUserRepository).existsByEmail(testUser.getEmail()); + verify(siteUserRepository, never()).save(any()); + } + + @Test + @DisplayName("만료된_카카오_토큰으로_회원가입_시도시_예외를_반환한다()") + void shouldThrowExceptionWhenKakaoTokenExpired() { + // given + doThrow(new CustomException(ErrorCode.INVALID_SERVICE_PUBLISHED_KAKAO_TOKEN)) + .when(tokenValidator).validateKakaoToken(TEST_KAKAO_TOKEN); + + // when & then + CustomException exception = assertThrows(CustomException.class, + () -> signUpService.signUp(validSignUpRequest)); + assertThat(exception.getCode()).isEqualTo(ErrorCode.INVALID_SERVICE_PUBLISHED_KAKAO_TOKEN.getCode()); + + // verify + verify(tokenValidator).validateKakaoToken(TEST_KAKAO_TOKEN); + verify(siteUserRepository, never()).save(any()); + } + + @Test + @DisplayName("이미_사용된_카카오_토큰으로_회원가입_시도시_예외를_반환한다()") + void shouldThrowExceptionWhenKakaoTokenAlreadyUsed() { + // given + doThrow(new CustomException(ErrorCode.INVALID_SERVICE_PUBLISHED_KAKAO_TOKEN)) + .when(tokenValidator).validateKakaoToken(TEST_KAKAO_TOKEN); + + // when & then + CustomException exception = assertThrows(CustomException.class, + () -> signUpService.signUp(validSignUpRequest)); + assertThat(exception.getCode()).isEqualTo(ErrorCode.INVALID_SERVICE_PUBLISHED_KAKAO_TOKEN.getCode()); + + // verify + verify(tokenValidator).validateKakaoToken(TEST_KAKAO_TOKEN); + verify(siteUserRepository, never()).save(any()); + } + + private SignUpRequest createValidSignUpRequest() { + return new SignUpRequest( + TEST_KAKAO_TOKEN, + List.of("미주권"), + List.of("브라질"), + PreparationStatus.CONSIDERING, + "https://example.com/profile.jpg", + Gender.PREFER_NOT_TO_SAY, + TEST_NICKNAME, + "1999-10-21" + ); + } + + private SiteUser createTestUser() { + return new SiteUser( + "test@example.com", + TEST_NICKNAME, + "https://example.com/profile.jpg", + "1999-10-21", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.PREFER_NOT_TO_SAY + ); + } + + private Region createTestRegion() { + return new Region("AMERICAS", "미주권"); + } + + private Country createTestCountry() { + return new Country("BR", "브라질", testRegion); + } +} \ No newline at end of file diff --git a/src/test/java/com/example/solidconnection/unit/repository/BoardRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/board/repository/BoardRepositoryTest.java similarity index 98% rename from src/test/java/com/example/solidconnection/unit/repository/BoardRepositoryTest.java rename to src/test/java/com/example/solidconnection/unit/board/repository/BoardRepositoryTest.java index 9ea7ee0..bec36a0 100644 --- a/src/test/java/com/example/solidconnection/unit/repository/BoardRepositoryTest.java +++ b/src/test/java/com/example/solidconnection/unit/board/repository/BoardRepositoryTest.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.unit.repository; +package com.example.solidconnection.unit.board.repository; import com.example.solidconnection.board.domain.Board; import com.example.solidconnection.board.repository.BoardRepository; diff --git a/src/test/java/com/example/solidconnection/unit/service/BoardServiceTest.java b/src/test/java/com/example/solidconnection/unit/board/service/BoardServiceTest.java similarity index 99% rename from src/test/java/com/example/solidconnection/unit/service/BoardServiceTest.java rename to src/test/java/com/example/solidconnection/unit/board/service/BoardServiceTest.java index 18c37b8..1df3e9e 100644 --- a/src/test/java/com/example/solidconnection/unit/service/BoardServiceTest.java +++ b/src/test/java/com/example/solidconnection/unit/board/service/BoardServiceTest.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.unit.service; +package com.example.solidconnection.unit.board.service; import com.example.solidconnection.board.domain.Board; import com.example.solidconnection.board.repository.BoardRepository; diff --git a/src/test/java/com/example/solidconnection/unit/repository/CommentRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/comment/repository/CommentRepositoryTest.java similarity index 98% rename from src/test/java/com/example/solidconnection/unit/repository/CommentRepositoryTest.java rename to src/test/java/com/example/solidconnection/unit/comment/repository/CommentRepositoryTest.java index a530373..56f677c 100644 --- a/src/test/java/com/example/solidconnection/unit/repository/CommentRepositoryTest.java +++ b/src/test/java/com/example/solidconnection/unit/comment/repository/CommentRepositoryTest.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.unit.repository; +package com.example.solidconnection.unit.comment.repository; import com.example.solidconnection.board.domain.Board; import com.example.solidconnection.board.repository.BoardRepository; diff --git a/src/test/java/com/example/solidconnection/unit/service/CommentServiceTest.java b/src/test/java/com/example/solidconnection/unit/comment/service/CommentServiceTest.java similarity index 99% rename from src/test/java/com/example/solidconnection/unit/service/CommentServiceTest.java rename to src/test/java/com/example/solidconnection/unit/comment/service/CommentServiceTest.java index 9ced8bc..d11faef 100644 --- a/src/test/java/com/example/solidconnection/unit/service/CommentServiceTest.java +++ b/src/test/java/com/example/solidconnection/unit/comment/service/CommentServiceTest.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.unit.service; +package com.example.solidconnection.unit.comment.service; import com.example.solidconnection.board.domain.Board; import com.example.solidconnection.comment.domain.Comment; diff --git a/src/test/java/com/example/solidconnection/unit/repository/PostLikeRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/post/repository/PostLikeRepositoryTest.java similarity index 98% rename from src/test/java/com/example/solidconnection/unit/repository/PostLikeRepositoryTest.java rename to src/test/java/com/example/solidconnection/unit/post/repository/PostLikeRepositoryTest.java index c39e284..9685a85 100644 --- a/src/test/java/com/example/solidconnection/unit/repository/PostLikeRepositoryTest.java +++ b/src/test/java/com/example/solidconnection/unit/post/repository/PostLikeRepositoryTest.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.unit.repository; +package com.example.solidconnection.unit.post.repository; import com.example.solidconnection.board.domain.Board; import com.example.solidconnection.board.repository.BoardRepository; diff --git a/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/post/repository/PostRepositoryTest.java similarity index 98% rename from src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java rename to src/test/java/com/example/solidconnection/unit/post/repository/PostRepositoryTest.java index ecc2c4f..a329cdb 100644 --- a/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java +++ b/src/test/java/com/example/solidconnection/unit/post/repository/PostRepositoryTest.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.unit.repository; +package com.example.solidconnection.unit.post.repository; import com.example.solidconnection.board.domain.Board; import com.example.solidconnection.board.repository.BoardRepository; diff --git a/src/test/java/com/example/solidconnection/unit/service/PostServiceTest.java b/src/test/java/com/example/solidconnection/unit/post/service/PostServiceTest.java similarity index 99% rename from src/test/java/com/example/solidconnection/unit/service/PostServiceTest.java rename to src/test/java/com/example/solidconnection/unit/post/service/PostServiceTest.java index 57c5916..900a3d0 100644 --- a/src/test/java/com/example/solidconnection/unit/service/PostServiceTest.java +++ b/src/test/java/com/example/solidconnection/unit/post/service/PostServiceTest.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.unit.service; +package com.example.solidconnection.unit.post.service; import com.example.solidconnection.board.domain.Board; import com.example.solidconnection.board.dto.PostFindBoardResponse; diff --git a/src/test/java/com/example/solidconnection/unit/repository/GpaScoreRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/score/repository/GpaScoreRepositoryTest.java similarity index 98% rename from src/test/java/com/example/solidconnection/unit/repository/GpaScoreRepositoryTest.java rename to src/test/java/com/example/solidconnection/unit/score/repository/GpaScoreRepositoryTest.java index e3fa680..dcb9853 100644 --- a/src/test/java/com/example/solidconnection/unit/repository/GpaScoreRepositoryTest.java +++ b/src/test/java/com/example/solidconnection/unit/score/repository/GpaScoreRepositoryTest.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.unit.repository; +package com.example.solidconnection.unit.score.repository; import com.example.solidconnection.application.domain.Gpa; import com.example.solidconnection.score.domain.GpaScore; diff --git a/src/test/java/com/example/solidconnection/unit/repository/LanguageTestScoreRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/score/repository/LanguageTestScoreRepositoryTest.java similarity index 98% rename from src/test/java/com/example/solidconnection/unit/repository/LanguageTestScoreRepositoryTest.java rename to src/test/java/com/example/solidconnection/unit/score/repository/LanguageTestScoreRepositoryTest.java index 7369f20..3760937 100644 --- a/src/test/java/com/example/solidconnection/unit/repository/LanguageTestScoreRepositoryTest.java +++ b/src/test/java/com/example/solidconnection/unit/score/repository/LanguageTestScoreRepositoryTest.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.unit.repository; +package com.example.solidconnection.unit.score.repository; import com.example.solidconnection.application.domain.LanguageTest; import com.example.solidconnection.score.domain.LanguageTestScore; diff --git a/src/test/java/com/example/solidconnection/unit/service/ScoreServiceTest.java b/src/test/java/com/example/solidconnection/unit/score/service/ScoreServiceTest.java similarity index 99% rename from src/test/java/com/example/solidconnection/unit/service/ScoreServiceTest.java rename to src/test/java/com/example/solidconnection/unit/score/service/ScoreServiceTest.java index 39deadb..2dc0d5e 100644 --- a/src/test/java/com/example/solidconnection/unit/service/ScoreServiceTest.java +++ b/src/test/java/com/example/solidconnection/unit/score/service/ScoreServiceTest.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.unit.service; +package com.example.solidconnection.unit.score.service; import com.example.solidconnection.application.domain.Gpa; import com.example.solidconnection.application.domain.LanguageTest; diff --git a/src/test/java/com/example/solidconnection/unit/service/SiteUserServiceTest.java b/src/test/java/com/example/solidconnection/unit/siteuser/service/SiteUserServiceTest.java similarity index 99% rename from src/test/java/com/example/solidconnection/unit/service/SiteUserServiceTest.java rename to src/test/java/com/example/solidconnection/unit/siteuser/service/SiteUserServiceTest.java index 860f76e..65abc3c 100644 --- a/src/test/java/com/example/solidconnection/unit/service/SiteUserServiceTest.java +++ b/src/test/java/com/example/solidconnection/unit/siteuser/service/SiteUserServiceTest.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.unit.service; +package com.example.solidconnection.unit.siteuser.service; import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.s3.S3Service; diff --git a/src/test/java/com/example/solidconnection/unit/university/service/UniversityLikeServiceTest.java b/src/test/java/com/example/solidconnection/unit/university/service/UniversityLikeServiceTest.java new file mode 100644 index 0000000..1ed438e --- /dev/null +++ b/src/test/java/com/example/solidconnection/unit/university/service/UniversityLikeServiceTest.java @@ -0,0 +1,209 @@ +package com.example.solidconnection.unit.university.service; + +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.custom.exception.ErrorCode; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import com.example.solidconnection.university.domain.LikedUniversity; +import com.example.solidconnection.university.domain.UniversityInfoForApply; +import com.example.solidconnection.university.dto.IsLikeResponse; +import com.example.solidconnection.university.dto.LikeResultResponse; +import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; +import com.example.solidconnection.university.service.UniversityService; +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.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; + +import static com.example.solidconnection.university.service.UniversityService.LIKE_CANCELED_MESSAGE; +import static com.example.solidconnection.university.service.UniversityService.LIKE_SUCCESS_MESSAGE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@DisplayName("대학교 좋아요 서비스 테스트") +class UniversityLikeServiceTest { + + @InjectMocks + private UniversityService universityService; + + @Mock + private UniversityInfoForApplyRepository universityInfoForApplyRepository; + + @Mock + private SiteUserRepository siteUserRepository; + + @Mock + private LikedUniversityRepository likedUniversityRepository; + + private SiteUser testUser; + private UniversityInfoForApply testUniversity; + + @BeforeEach + void setUp() { + testUser = createTestUser(); + testUniversity = createTestUniversity(); + } + + @Test + @DisplayName("사용자가_특정_대학을_좋아요_상태인지_확인하면_True를_반환한다()") + void shouldReturnTrueWhenUserLikedTheUniversity() { + // given + String email = testUser.getEmail(); + Long universityId = testUniversity.getId(); + + when(siteUserRepository.getByEmail(email)).thenReturn(testUser); + when(universityInfoForApplyRepository.getUniversityInfoForApplyById(universityId)).thenReturn(testUniversity); + when(likedUniversityRepository.findBySiteUserAndUniversityInfoForApply(testUser, testUniversity)) + .thenReturn(Optional.of(new LikedUniversity())); + + // when + IsLikeResponse response = universityService.getIsLiked(email, universityId); + + // then + assertThat(response).isNotNull(); + assertThat(response.isLike()).isTrue(); + + // verify + verify(siteUserRepository).getByEmail(email); + verify(universityInfoForApplyRepository).getUniversityInfoForApplyById(universityId); + verify(likedUniversityRepository).findBySiteUserAndUniversityInfoForApply(testUser, testUniversity); + } + + @Test + @DisplayName("사용자가_특정_대학을_좋아요_상태인지_확인하면_False를_반환한다()") + void shouldReturnFalseWhenUserNotLikedTheUniversity() { + // given + String email = testUser.getEmail(); + Long universityId = testUniversity.getId(); + + when(siteUserRepository.getByEmail(email)).thenReturn(testUser); + when(universityInfoForApplyRepository.getUniversityInfoForApplyById(universityId)).thenReturn(testUniversity); + when(likedUniversityRepository.findBySiteUserAndUniversityInfoForApply(testUser, testUniversity)) + .thenReturn(Optional.empty()); + + // when + IsLikeResponse response = universityService.getIsLiked(email, universityId); + + // then + assertThat(response).isNotNull(); + assertThat(response.isLike()).isFalse(); + + // verify + verify(siteUserRepository).getByEmail(email); + verify(universityInfoForApplyRepository).getUniversityInfoForApplyById(universityId); + verify(likedUniversityRepository).findBySiteUserAndUniversityInfoForApply(testUser, testUniversity); + } + + @Test + @DisplayName("사용자가_대학_좋아요를_추가하면_성공_메시지를_반환한다()") + void shouldAddLikeWhenUserNotLikedTheUniversity() { + // given + String email = testUser.getEmail(); + Long universityId = testUniversity.getId(); + + when(siteUserRepository.getByEmail(email)).thenReturn(testUser); + when(universityInfoForApplyRepository.getUniversityInfoForApplyById(universityId)).thenReturn(testUniversity); + when(likedUniversityRepository.findBySiteUserAndUniversityInfoForApply(testUser, testUniversity)) + .thenReturn(Optional.empty()); + + // when + LikeResultResponse response = universityService.likeUniversity(email, universityId); + + // then + assertThat(response).isNotNull(); + assertThat(response.result()).isEqualTo(LIKE_SUCCESS_MESSAGE); + + // verify + verify(likedUniversityRepository).save(any(LikedUniversity.class)); + } + + @Test + @DisplayName("사용자가_대학_좋아요를_취소하면_취소_메시지를_반환한다()") + void shouldRemoveLikeWhenUserAlreadyLikedTheUniversity() { + // given + String email = testUser.getEmail(); + Long universityId = testUniversity.getId(); + + when(siteUserRepository.getByEmail(email)).thenReturn(testUser); + when(universityInfoForApplyRepository.getUniversityInfoForApplyById(universityId)).thenReturn(testUniversity); + when(likedUniversityRepository.findBySiteUserAndUniversityInfoForApply(testUser, testUniversity)) + .thenReturn(Optional.of(new LikedUniversity(1L, testUniversity, testUser))); + + // when + LikeResultResponse response = universityService.likeUniversity(email, universityId); + + // then + assertThat(response).isNotNull(); + assertThat(response.result()).isEqualTo(LIKE_CANCELED_MESSAGE); + + // verify + verify(likedUniversityRepository).delete(any(LikedUniversity.class)); + } + + @Test + @DisplayName("존재하지_않는_대학_ID로_좋아요_요청_시_예외를_반환한다()") + void shouldThrowExceptionWhenUniversityNotFound() { + // given + String email = testUser.getEmail(); + Long invalidUniversityId = 999L; + + when(siteUserRepository.getByEmail(email)).thenReturn(testUser); + when(universityInfoForApplyRepository.getUniversityInfoForApplyById(invalidUniversityId)) + .thenThrow(new CustomException(ErrorCode.UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND)); + + // when & then + CustomException exception = assertThrows(CustomException.class, + () -> universityService.likeUniversity(email, invalidUniversityId)); + + assertThat(exception.getCode()).isEqualTo(ErrorCode.UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND.getCode()); + + // verify + verify(siteUserRepository).getByEmail(email); + verify(universityInfoForApplyRepository).getUniversityInfoForApplyById(invalidUniversityId); + } + + private SiteUser createTestUser() { + return new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-10-21", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + } + + private UniversityInfoForApply createTestUniversity() { + return new UniversityInfoForApply( + 1L, + "2025-1-a", + "Test University", + 3, + null, + null, + "4학기", + "어학 요구사항", + "3.0/4.5", + "4.5", + "지원 상세", + "전공 상세", + "기숙사 상세", + "영어 강좌 상세", + "기타 상세", + null, + null + ); + } +} diff --git a/src/test/java/com/example/solidconnection/unit/university/service/UniversityQueryServiceTest.java b/src/test/java/com/example/solidconnection/unit/university/service/UniversityQueryServiceTest.java new file mode 100644 index 0000000..bc17ff7 --- /dev/null +++ b/src/test/java/com/example/solidconnection/unit/university/service/UniversityQueryServiceTest.java @@ -0,0 +1,262 @@ +package com.example.solidconnection.unit.university.service; + +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.custom.exception.ErrorCode; +import com.example.solidconnection.entity.Country; +import com.example.solidconnection.entity.Region; +import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.LanguageTestType; +import com.example.solidconnection.type.SemesterAvailableForDispatch; +import com.example.solidconnection.type.TuitionFeeType; +import com.example.solidconnection.university.domain.LanguageRequirement; +import com.example.solidconnection.university.domain.University; +import com.example.solidconnection.university.domain.UniversityInfoForApply; +import com.example.solidconnection.university.dto.UniversityDetailResponse; +import com.example.solidconnection.university.dto.UniversityInfoForApplyPreviewResponse; +import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; +import com.example.solidconnection.university.repository.custom.UniversityFilterRepositoryImpl; +import com.example.solidconnection.university.service.UniversityService; +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.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.HashSet; +import java.util.List; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@DisplayName("대학교 조회 서비스 테스트") +class UniversityQueryServiceTest { + + @InjectMocks + private UniversityService universityService; + + @Mock + private UniversityFilterRepositoryImpl universityFilterRepository; + + @Mock + private UniversityInfoForApplyRepository universityInfoForApplyRepository; + + @Mock + private LikedUniversityRepository likedUniversityRepository; + + @Mock + private SiteUserRepository siteUserRepository; + + private UniversityInfoForApply testUniversityInfoForApply; + @BeforeEach + void setUp() { + universityService = new UniversityService( + universityInfoForApplyRepository, + likedUniversityRepository, + universityFilterRepository, + siteUserRepository + ); + ReflectionTestUtils.setField(universityService, "term", "2025-1-a"); + University testUniversity = createTestUniversity(); + testUniversityInfoForApply = createTestUniversityInfoForApply(testUniversity); + } + + + @Test + @DisplayName("유효한_ID로_상세_조회하면_대학_정보를_반환한다()") + void shouldReturnUniversityDetailsWhenValidIdProvided() { + // given + Long universityInfoForApplyId = testUniversityInfoForApply.getId(); + when(universityInfoForApplyRepository.getUniversityInfoForApplyById(universityInfoForApplyId)) + .thenReturn(testUniversityInfoForApply); + + // when + UniversityDetailResponse response = universityService.getUniversityDetail(universityInfoForApplyId); + + // then + assertThat(response).isNotNull(); + assertThat(response.id()).isEqualTo(universityInfoForApplyId); + assertThat(response.koreanName()).isEqualTo(testUniversityInfoForApply.getKoreanName()); + + // verify + verify(universityInfoForApplyRepository).getUniversityInfoForApplyById(universityInfoForApplyId); + } + + @Test + @DisplayName("존재하지_않는_ID로_조회하면_예외를_반환한다()") + void shouldThrowExceptionWhenInvalidIdProvided() { + // given + Long invalidId = 999L; + when(universityInfoForApplyRepository.getUniversityInfoForApplyById(invalidId)) + .thenThrow(new CustomException(ErrorCode.UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND)); + + // when & then + CustomException exception = assertThrows(CustomException.class, + () -> universityService.getUniversityDetail(invalidId)); + + assertThat(exception.getCode()).isEqualTo(ErrorCode.UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND.getCode()); + + // verify + verify(universityInfoForApplyRepository).getUniversityInfoForApplyById(invalidId); + } + + @Test + @DisplayName("검색_조건이_없는_경우_전체_대학_목록을_반환한다()") + void shouldReturnAllUniversitiesWhenNoSearchConditionProvided() { + // given + List universityList = List.of(testUniversityInfoForApply); + when(universityFilterRepository.findByRegionCodeAndKeywordsAndLanguageTestTypeAndTestScoreAndTerm( + "", List.of(), null, "", "2025-1-a")) + .thenReturn(universityList); + + // when + List response = universityService + .searchUniversity("", List.of(), null, "") + .universityInfoForApplyPreviewResponses(); + + // then + assertThat(response).isNotNull(); + assertThat(response).hasSize(1); + assertThat(response.get(0).id()).isEqualTo(testUniversityInfoForApply.getId()); + + // verify + verify(universityFilterRepository).findByRegionCodeAndKeywordsAndLanguageTestTypeAndTestScoreAndTerm( + "", List.of(), null, "", "2025-1-a"); + } + + @Test + @DisplayName("검색_조건(region,_keyword)을_만족하는_대학_목록을_반환한다()") + void shouldReturnFilteredUniversitiesWhenSearchConditionProvided() { + // given + String region = "ASIA"; + List keywords = List.of("서울", "대학"); + List universityList = List.of(testUniversityInfoForApply); + + when(universityFilterRepository.findByRegionCodeAndKeywordsAndLanguageTestTypeAndTestScoreAndTerm( + region, keywords, null, "", "2025-1-a")) + .thenReturn(universityList); + + // when + List response = universityService + .searchUniversity(region, keywords, null, "") + .universityInfoForApplyPreviewResponses(); + + // then + assertThat(response).isNotNull(); + assertThat(response).hasSize(1); + assertThat(response.get(0).koreanName()).isEqualTo(testUniversityInfoForApply.getKoreanName()); + + // verify + verify(universityFilterRepository).findByRegionCodeAndKeywordsAndLanguageTestTypeAndTestScoreAndTerm( + region, keywords, null, "", "2025-1-a"); + } + + + @Test + @DisplayName("검색_조건(region,_keyword,_testType,_testScore)을_만족하는_대학_목록을_반환한다()") + void shouldReturnFilteredUniversitiesWhenFullSearchConditionProvided() { + // given + String region = "ASIA"; + List keywords = List.of("서울", "대학"); + LanguageTestType testType = LanguageTestType.TOEFL_IBT; + String testScore = "90"; + List universityList = List.of(testUniversityInfoForApply); + + when(universityFilterRepository.findByRegionCodeAndKeywordsAndLanguageTestTypeAndTestScoreAndTerm( + region, keywords, testType, testScore, "2025-1-a")) + .thenReturn(universityList); + + // when + List response = universityService + .searchUniversity(region, keywords, testType, testScore) + .universityInfoForApplyPreviewResponses(); + + // then + assertThat(response).isNotNull(); + assertThat(response).hasSize(1); + assertThat(response.get(0).languageRequirements()).isNotEmpty(); + + // verify + verify(universityFilterRepository).findByRegionCodeAndKeywordsAndLanguageTestTypeAndTestScoreAndTerm( + region, keywords, testType, testScore, "2025-1-a"); + } + + @Test + @DisplayName("검색_조건을_만족하지_않을_경우_결과가_비어_있다()") + void shouldReturnEmptyListWhenSearchConditionNotMet() { + // given + String region = "EUROPE"; + List keywords = List.of("비현실적인 대학명"); + LanguageTestType testType = LanguageTestType.TOEFL_IBT; + String testScore = "150"; + + when(universityFilterRepository.findByRegionCodeAndKeywordsAndLanguageTestTypeAndTestScoreAndTerm( + region, keywords, testType, testScore, "2025-1-a")) + .thenReturn(List.of()); + + // when + List response = universityService + .searchUniversity(region, keywords, testType, testScore) + .universityInfoForApplyPreviewResponses(); + + // then + assertThat(response).isNotNull(); + assertThat(response).isEmpty(); + + // verify + verify(universityFilterRepository).findByRegionCodeAndKeywordsAndLanguageTestTypeAndTestScoreAndTerm( + region, keywords, testType, testScore, "2025-1-a"); + } + private University createTestUniversity() { + Region region = new Region("ASIA", "아시아권"); + Country country = new Country("KR", "대한민국", region); + return new University( + 1L, + "서울대학교", + "Seoul National University", + "SNU", + "https://www.snu.ac.kr", + "https://english.snu.ac.kr", + "https://accommodation.snu.ac.kr", + "https://logo.snu.ac.kr", + "https://background.snu.ac.kr", + "서울에 위치한 최고의 대학", + country, + region + ); + } + + private UniversityInfoForApply createTestUniversityInfoForApply(University university) { + LanguageRequirement languageRequirement = new LanguageRequirement( + 1L, + LanguageTestType.TOEFL_IBT, + "90", + null + ); + return new UniversityInfoForApply( + 1L, + "2025-1-a", + "서울대학교", + 100, + TuitionFeeType.HOME_UNIVERSITY_PAYMENT, + SemesterAvailableForDispatch.IRRELEVANT, + "4학기 이상", + "TOEFL iBT 90 이상", + "3.0/4.5", + "4.5", + "지원 상세 정보", + "전공 상세 정보", + "기숙사 상세 정보", + "영어 강좌 상세 정보", + "기타 상세 정보", + new HashSet<>(List.of(languageRequirement)), + university + ); + } +} \ No newline at end of file diff --git a/src/test/java/com/example/solidconnection/unit/university/service/UniversityRecommendServiceTest.java b/src/test/java/com/example/solidconnection/unit/university/service/UniversityRecommendServiceTest.java new file mode 100644 index 0000000..1594e91 --- /dev/null +++ b/src/test/java/com/example/solidconnection/unit/university/service/UniversityRecommendServiceTest.java @@ -0,0 +1,270 @@ +package com.example.solidconnection.unit.university.service; + +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.custom.exception.ErrorCode; +import com.example.solidconnection.entity.Country; +import com.example.solidconnection.entity.Region; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.*; +import com.example.solidconnection.university.domain.*; +import com.example.solidconnection.university.dto.UniversityInfoForApplyPreviewResponse; +import com.example.solidconnection.university.dto.UniversityRecommendsResponse; +import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; +import com.example.solidconnection.university.service.GeneralRecommendUniversities; +import com.example.solidconnection.university.service.UniversityRecommendService; +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.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.*; + +import static java.util.stream.IntStream.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@DisplayName("대학교 추천 서비스 테스트") +class UniversityRecommendServiceTest { + + @InjectMocks + private UniversityRecommendService universityRecommendService; + + @Mock + private UniversityInfoForApplyRepository universityInfoForApplyRepository; + + @Mock + private GeneralRecommendUniversities generalRecommendUniversities; + + @Mock + private SiteUserRepository siteUserRepository; + + private SiteUser testUser; + private List testUniversities; + + @BeforeEach + void setUp() { + ReflectionTestUtils.setField(universityRecommendService, "term", "2025-1-a"); + testUser = createTestUser(); + testUniversities = createTestUniversities(); + } + + @Test + @DisplayName("일반_추천_목록을_정상적으로_반환한다()") + void getGeneralRecommendsShouldReturnShuffledAndConvertedList() { + // given + List generalUniversities = + range(0, UniversityRecommendService.RECOMMEND_UNIVERSITY_NUM) + .mapToObj(this::createUniversityInfo) + .toList(); + when(generalRecommendUniversities.getRecommendUniversities()) + .thenReturn(generalUniversities); + + // when + UniversityRecommendsResponse response = universityRecommendService.getGeneralRecommends(); + + // then + assertThat(response.recommendedUniversities()).isNotNull(); + assertThat(response.recommendedUniversities()) + .hasSize(UniversityRecommendService.RECOMMEND_UNIVERSITY_NUM); + response.recommendedUniversities().forEach(preview -> { + assertThat(preview).isInstanceOf(UniversityInfoForApplyPreviewResponse.class); + assertThat(preview.studentCapacity()).isEqualTo(3); + assertThat(preview.region()).isEqualTo("유럽권"); + assertThat(preview.country()).isEqualTo("프랑스"); + }); + assertThat(response.recommendedUniversities()) + .extracting("koreanName") + .isNotEqualTo(generalUniversities.stream() + .map(UniversityInfoForApply::getKoreanName) + .toList()); + // verify + verify(generalRecommendUniversities).getRecommendUniversities(); + } + + @Test + @DisplayName("개인화_추천_목록을_정상적으로_반환한다()") + void getPersonalRecommendsShouldReturnPersonalizedAndShuffledList() { + + // given + String email = "test@example.com"; + List personalRecommends = new ArrayList<>( + range(0, UniversityRecommendService.RECOMMEND_UNIVERSITY_NUM) + .mapToObj(this::createUniversityInfo) + .toList() + ); + + + when(siteUserRepository.getByEmail(email)).thenReturn(testUser); + when(universityInfoForApplyRepository + .findUniversityInfoForAppliesBySiteUsersInterestedCountryOrRegionAndTerm(any(), anyString())) + .thenReturn(personalRecommends); + + // when + List originalList = new ArrayList<>(personalRecommends); + UniversityRecommendsResponse response = universityRecommendService.getPersonalRecommends(email); + + // then + assertThat(response.recommendedUniversities()).isNotNull(); + assertThat(response.recommendedUniversities()) + .hasSize(UniversityRecommendService.RECOMMEND_UNIVERSITY_NUM); + assertThat(response.recommendedUniversities()) + .extracting("koreanName") + .containsExactlyInAnyOrderElementsOf(originalList.stream() + .map(UniversityInfoForApply::getKoreanName) + .toList()); + List shuffledOrder = response.recommendedUniversities().stream() + .map(UniversityInfoForApplyPreviewResponse::koreanName) + .toList(); + assertThat(shuffledOrder) + .isNotEqualTo(originalList.stream() + .map(UniversityInfoForApply::getKoreanName) + .toList()); + + // verify + verify(siteUserRepository).getByEmail(email); + verify(universityInfoForApplyRepository) + .findUniversityInfoForAppliesBySiteUsersInterestedCountryOrRegionAndTerm(any(), anyString()); + } + + @Test + @DisplayName("존재하지_않는_사용자의_경우_예외를_반환한다()") + void getPersonalRecommendsWithInvalidUserShouldThrowException() { + // given + String email = "invalid@example.com"; + when(siteUserRepository.getByEmail(email)) + .thenThrow(new CustomException(ErrorCode.USER_NOT_FOUND)); + + // when & then + CustomException exception = assertThrows(CustomException.class, + () -> universityRecommendService.getPersonalRecommends(email)); + assertThat(exception.getCode()).isEqualTo(ErrorCode.USER_NOT_FOUND.getCode()); + + // verify + verify(siteUserRepository).getByEmail(email); + } + + @Test + @DisplayName("개인화_추천이_부족할_경우_일반_추천으로_보충한다()") + void getPersonalRecommendsWithInsufficientRecommendsShouldSupplementWithGeneral() { + // given + String email = "test@example.com"; + List personalRecommends = new ArrayList<>(); + personalRecommends.add(testUniversities.get(0)); + List generalRecommends = createAdditionalUniversities(); + when(siteUserRepository.getByEmail(email)).thenReturn(testUser); + when(universityInfoForApplyRepository + .findUniversityInfoForAppliesBySiteUsersInterestedCountryOrRegionAndTerm(any(), anyString())) + .thenReturn(personalRecommends); + when(generalRecommendUniversities.getRecommendUniversities()) + .thenReturn(generalRecommends); + + // when + UniversityRecommendsResponse response = universityRecommendService.getPersonalRecommends(email); + + // then + assertThat(response.recommendedUniversities()).isNotNull(); + assertThat(response.recommendedUniversities()) + .hasSize(UniversityRecommendService.RECOMMEND_UNIVERSITY_NUM); + List allKoreanNames = response.recommendedUniversities().stream() + .map(UniversityInfoForApplyPreviewResponse::koreanName) + .toList(); + assertThat(allKoreanNames).doesNotHaveDuplicates(); + response.recommendedUniversities().forEach(preview -> { + assertThat(preview).isInstanceOf(UniversityInfoForApplyPreviewResponse.class); + assertThat(preview.studentCapacity()).isEqualTo(3); + assertThat(preview.region()).isEqualTo("유럽권"); + assertThat(preview.country()).isEqualTo("프랑스"); + }); + + // verify + verify(siteUserRepository).getByEmail(email); + verify(universityInfoForApplyRepository) + .findUniversityInfoForAppliesBySiteUsersInterestedCountryOrRegionAndTerm(any(), anyString()); + verify(generalRecommendUniversities).getRecommendUniversities(); + } + + private SiteUser createTestUser() { + return new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-10-21", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + } + + private List createTestUniversities() { + List universities = new ArrayList<>(); + for(int i = 0; i < UniversityRecommendService.RECOMMEND_UNIVERSITY_NUM + 2; i++) { + universities.add(createUniversityInfo(i)); + } + return universities; + } + + private List createAdditionalUniversities() { + List universities = new ArrayList<>(); + for(int i = UniversityRecommendService.RECOMMEND_UNIVERSITY_NUM; i < UniversityRecommendService.RECOMMEND_UNIVERSITY_NUM * 2; i++) { + universities.add(createUniversityInfo(i)); + } + return universities; + } + + private UniversityInfoForApply createUniversityInfo(int index) { + Region region = new Region("EUROPE", "유럽권"); + Country country = new Country("FR", "프랑스", region); + + University university = new University( + (long) index, + "University" + index, + "University" + index, + "univ" + index, + "https://example.com/life" + index, + "https://example.com/courses" + index, + "https://example.com/accommodation" + index, + "https://example.com/logo" + index, + "https://example.com/background" + index, + "details" + index, + country, + region + ); + + Set requirements = new HashSet<>(); + requirements.add(new LanguageRequirement( + (long) index, + LanguageTestType.TOEFL_IBT, + "90", + null + )); + + return new UniversityInfoForApply( + (long) index, + "2025-1-a", + "University" + index, + 3, + TuitionFeeType.HOME_UNIVERSITY_PAYMENT, + SemesterAvailableForDispatch.IRRELEVANT, + "4학기", + "어학 요구사항" + index, + "3.0/4.5", + "4.5", + "지원 상세" + index, + "전공 상세" + index, + "기숙사 상세" + index, + "영어 강좌 상세" + index, + "기타 상세" + index, + requirements, + university + ); + } +} \ No newline at end of file