diff --git a/techeerzip/build.gradle b/techeerzip/build.gradle index 2eaf6ee9..75132f66 100644 --- a/techeerzip/build.gradle +++ b/techeerzip/build.gradle @@ -108,7 +108,7 @@ dependencies { // GraphQL 클라이언트 (WebFlux) implementation 'org.springframework.boot:spring-boot-starter-webflux' - + // macOS DNS resolver for Netty runtimeOnly 'io.netty:netty-resolver-dns-native-macos:4.1.114.Final:osx-aarch_64' } @@ -190,43 +190,43 @@ def jacocoExcluded = [ '**/global/**' // Global 패키지 ] - // jacocoReport - tasks.named('jacocoTestReport', JacocoReport) { - dependsOn test - reports { - xml.required = true - html.required = true - } - classDirectories.setFrom(files( - classDirectories.files.collect { fileTree(dir: it, exclude: jacocoExcluded) } - )) - } - - // jacocoCoverage 확인 - tasks.named('jacocoTestCoverageVerification', JacocoCoverageVerification) { - classDirectories.setFrom(files( - classDirectories.files.collect { fileTree(dir: it, exclude: jacocoExcluded) } - )) - - violationRules { - rule { - element = 'CLASS' - limit { - counter = 'LINE' - value = 'COVEREDRATIO' - minimum = 0.00 - } - limit { - counter = 'BRANCH' - value = 'COVEREDRATIO' - minimum = 0.00 - } - } - } - } +// jacocoReport +tasks.named('jacocoTestReport', JacocoReport) { + dependsOn test + reports { + xml.required = true + html.required = true + } + classDirectories.setFrom(files( + classDirectories.files.collect { fileTree(dir: it, exclude: jacocoExcluded) } + )) +} + +// jacocoCoverage 확인 +tasks.named('jacocoTestCoverageVerification', JacocoCoverageVerification) { + classDirectories.setFrom(files( + classDirectories.files.collect { fileTree(dir: it, exclude: jacocoExcluded) } + )) + + violationRules { + rule { + element = 'CLASS' + limit { + counter = 'LINE' + value = 'COVEREDRATIO' + minimum = 0.00 + } + limit { + counter = 'BRANCH' + value = 'COVEREDRATIO' + minimum = 0.00 + } + } + } +} // 테스트 task 마무리 설정 tasks.test { useJUnitPlatform() finalizedBy tasks.jacocoTestReport -} +} \ No newline at end of file diff --git a/techeerzip/src/main/java/backend/techeerzip/domain/role/entity/RoleType.java b/techeerzip/src/main/java/backend/techeerzip/domain/role/entity/RoleType.java index 047f8de0..1f8bd645 100644 --- a/techeerzip/src/main/java/backend/techeerzip/domain/role/entity/RoleType.java +++ b/techeerzip/src/main/java/backend/techeerzip/domain/role/entity/RoleType.java @@ -1,19 +1,25 @@ package backend.techeerzip.domain.role.entity; public enum RoleType { - ADMIN("ROLE_ADMIN"), - MENTOR("ROLE_MENTOR"), - TECHEER("ROLE_TECHEER"), - COMPANY("ROLE_COMPANY"), - BOOTCAMP("ROLE_BOOTCAMP"); + ADMIN("ROLE_ADMIN", 1L), + MENTOR("ROLE_MENTOR", 2L), + TECHEER("ROLE_TECHEER", 3L), + COMPANY("ROLE_COMPANY", 4L), + BOOTCAMP("ROLE_BOOTCAMP", 5L); private final String roleName; + private final Long roleId; - RoleType(String roleName) { + RoleType(String roleName, Long roleId) { this.roleName = roleName; + this.roleId = roleId; } public String getRoleName() { return roleName; } + + public Long getRoleId() { + return roleId; + } } diff --git a/techeerzip/src/main/java/backend/techeerzip/domain/statistic/entity/WeeklyGitContributions.java b/techeerzip/src/main/java/backend/techeerzip/domain/statistic/entity/WeeklyGitContributions.java index a7fd3897..84507eb2 100644 --- a/techeerzip/src/main/java/backend/techeerzip/domain/statistic/entity/WeeklyGitContributions.java +++ b/techeerzip/src/main/java/backend/techeerzip/domain/statistic/entity/WeeklyGitContributions.java @@ -21,10 +21,10 @@ indexes = { @Index( name = "idx_WeeklyGitContributions_user_year_month_week", - columnList = "userId year month week"), + columnList = "userId,year,month,week"), @Index( name = "idx_WeeklyGitContributions_user_weekStart", - columnList = "userId weekStart") + columnList = "userId,weekStart") }) @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) diff --git a/techeerzip/src/main/java/backend/techeerzip/domain/user/controller/UserController.java b/techeerzip/src/main/java/backend/techeerzip/domain/user/controller/UserController.java index f3624a3d..8b4c3aae 100644 --- a/techeerzip/src/main/java/backend/techeerzip/domain/user/controller/UserController.java +++ b/techeerzip/src/main/java/backend/techeerzip/domain/user/controller/UserController.java @@ -84,6 +84,7 @@ public ResponseEntity signupExternal( } @DeleteMapping(value = "") + @PreAuthorize("hasPermission(#userId, 'User', 'DELETE')") @Override public ResponseEntity deleteUser( @Valid @Parameter(hidden = true) @CurrentUser Long userId, @@ -190,6 +191,7 @@ public ResponseEntity getProfile(@PathVariable Long userId) { } @PatchMapping("") + @PreAuthorize("hasPermission(#userId, 'User', 'UPDATE')") @Override public ResponseEntity updateProfile( @Valid @Parameter(hidden = true) @CurrentUser Long userId, @@ -201,6 +203,7 @@ public ResponseEntity updateProfile( } @DeleteMapping("/experience/{experienceId}") + @PreAuthorize("hasPermission(#experienceId, 'UserExperience', 'DELETE')") @Override public ResponseEntity deleteExperience(@PathVariable Long experienceId) { logger.info("경력 삭제 요청 처리 중 - experienceId: {}", experienceId, CONTEXT); @@ -210,6 +213,7 @@ public ResponseEntity deleteExperience(@PathVariable Long experienceId) { } @PatchMapping("/nickname") + @PreAuthorize("hasPermission(#userId, 'User', 'UPDATE_NICKNAME')") @Override public ResponseEntity updateNickname( @Valid @Parameter(hidden = true) @CurrentUser Long userId, @@ -261,6 +265,7 @@ public ResponseEntity getBootcampMemberProfiles( } @PatchMapping(value = "/techeer", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @PreAuthorize("hasPermission(#userId, 'User', 'CHANGE_TECHEER')") @Override public ResponseEntity changeTecheer( @RequestPart("file") MultipartFile file, @@ -272,6 +277,7 @@ public ResponseEntity changeTecheer( } @PostMapping("/github/username") + @PreAuthorize("hasPermission(#userId, 'User', 'UPDATE_GITHUB')") @Override public ResponseEntity syncGithubData( @CurrentUser Long userId, @Valid @RequestBody UpdateGithubUrlRequest request) { diff --git a/techeerzip/src/main/java/backend/techeerzip/domain/user/repository/PermissionRequestRepository.java b/techeerzip/src/main/java/backend/techeerzip/domain/user/repository/PermissionRequestRepository.java index ebeebea8..82321927 100644 --- a/techeerzip/src/main/java/backend/techeerzip/domain/user/repository/PermissionRequestRepository.java +++ b/techeerzip/src/main/java/backend/techeerzip/domain/user/repository/PermissionRequestRepository.java @@ -6,14 +6,17 @@ import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; import backend.techeerzip.domain.user.entity.PermissionRequest; import backend.techeerzip.global.entity.StatusCategory; +@Transactional public interface PermissionRequestRepository extends JpaRepository { + @Transactional(readOnly = true) List findByStatus(StatusCategory status); - @Modifying + @Modifying(clearAutomatically = true, flushAutomatically = true) @Query( "UPDATE PermissionRequest p SET p.status = :status WHERE p.user.id = :userId AND p.status = backend.techeerzip.global.entity.StatusCategory.PENDING") int updateStatusByUserId(@Param("userId") Long userId, @Param("status") StatusCategory status); diff --git a/techeerzip/src/main/java/backend/techeerzip/domain/user/repository/UserRepository.java b/techeerzip/src/main/java/backend/techeerzip/domain/user/repository/UserRepository.java index 3b1e876b..5ec833d4 100644 --- a/techeerzip/src/main/java/backend/techeerzip/domain/user/repository/UserRepository.java +++ b/techeerzip/src/main/java/backend/techeerzip/domain/user/repository/UserRepository.java @@ -14,7 +14,8 @@ @Repository public interface UserRepository extends JpaRepository, UserRepositoryCustom { - Optional findByEmail(String email); + @Query("SELECT u FROM User u WHERE u.email = :email AND u.isDeleted = false") + Optional findByEmail(@Param("email") String email); boolean existsByEmail(String email); diff --git a/techeerzip/src/main/java/backend/techeerzip/domain/user/service/UserService.java b/techeerzip/src/main/java/backend/techeerzip/domain/user/service/UserService.java index f0dfa84d..a1c276e2 100644 --- a/techeerzip/src/main/java/backend/techeerzip/domain/user/service/UserService.java +++ b/techeerzip/src/main/java/backend/techeerzip/domain/user/service/UserService.java @@ -69,7 +69,6 @@ import backend.techeerzip.domain.user.exception.UserNotFoundException; import backend.techeerzip.domain.user.exception.UserNotResumeException; import backend.techeerzip.domain.user.exception.UserProfileImgFailException; -import backend.techeerzip.domain.user.exception.UserUnauthorizedAdminException; import backend.techeerzip.domain.user.mapper.TechStackMapper; import backend.techeerzip.domain.user.mapper.UserMapper; import backend.techeerzip.domain.user.repository.PermissionRequestRepository; @@ -222,10 +221,17 @@ public void signUp( TaskType.SIGNUP_BLOG_FETCH, savedUser.getId(), blogUrls); } + userRepository.flush(); + + User managedUser = + userRepository + .findById(savedUser.getId()) + .orElseThrow(() -> new UserNotFoundException()); + CreateResumeRequest resumeRequest = createUserWithResumeRequest.getCreateResumeRequest(); // 이력서 저장 resumeService.createResumeByUser( - savedUser, + managedUser, resumeFile, resumeRequest.getTitle(), resumeRequest.getPosition(), @@ -666,19 +672,13 @@ public void deleteExperience(Long experienceId) { userExperienceRepository .findById(experienceId) .orElseThrow(UserExperienceNotFoundException::new); - userExperienceRepository.delete(experience); + experience.delete(); + userExperienceRepository.save(experience); } @Transactional public void updateNickname(Long userId, String nickname) { User user = userRepository.findById(userId).orElseThrow(UserNotFoundException::new); - - Long roleId = user.getRole().getId(); - if (roleId == 3) { - logger.warn("권한 없음 - userId: {}", userId); - throw new UserUnauthorizedAdminException(); - } - user.setNickname(nickname); userRepository.save(user); } diff --git a/techeerzip/src/main/java/backend/techeerzip/domain/userExperience/repository/UserExperienceRepository.java b/techeerzip/src/main/java/backend/techeerzip/domain/userExperience/repository/UserExperienceRepository.java index 19e4f8fd..21a55daf 100644 --- a/techeerzip/src/main/java/backend/techeerzip/domain/userExperience/repository/UserExperienceRepository.java +++ b/techeerzip/src/main/java/backend/techeerzip/domain/userExperience/repository/UserExperienceRepository.java @@ -4,11 +4,13 @@ import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; import backend.techeerzip.domain.userExperience.entity.UserExperience; +@Transactional public interface UserExperienceRepository extends JpaRepository { - @Modifying - @Query("UPDATE UserExperience ue SET ue.isDeleted = true WHERE ue.user.id = :userId") + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query("UPDATE UserExperience ue SET ue.isDeleted = true WHERE ue.userId = :userId") void updateIsDeletedByUserId(@Param("userId") Long userId); } diff --git a/techeerzip/src/main/java/backend/techeerzip/global/permission/UserExperiencePermissionEvaluator.java b/techeerzip/src/main/java/backend/techeerzip/global/permission/UserExperiencePermissionEvaluator.java new file mode 100644 index 00000000..9bcd3cba --- /dev/null +++ b/techeerzip/src/main/java/backend/techeerzip/global/permission/UserExperiencePermissionEvaluator.java @@ -0,0 +1,86 @@ +package backend.techeerzip.global.permission; + +import java.io.Serializable; + +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import backend.techeerzip.domain.auth.jwt.CustomUserPrincipal; +import backend.techeerzip.domain.userExperience.entity.UserExperience; +import backend.techeerzip.domain.userExperience.exception.UserExperienceNotFoundException; +import backend.techeerzip.domain.userExperience.repository.UserExperienceRepository; +import backend.techeerzip.global.exception.PermissionDeniedException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +@RequiredArgsConstructor +public class UserExperiencePermissionEvaluator implements DomainPermissionEvaluator { + + private final UserExperienceRepository userExperienceRepository; + + @Override + public boolean supports(String targetType) { + return "UserExperience".equals(targetType); + } + + @Override + @Transactional(readOnly = true) + public boolean hasPermission( + Authentication auth, Serializable targetId, String targetType, String permission) { + if (!supports(targetType)) { + return false; + } + + CustomUserPrincipal user = (CustomUserPrincipal) auth.getPrincipal(); + Long userId = user.getUserId(); + Long experienceId = (Long) targetId; + + if (!isSupportedPermission(permission)) { + return false; + } + + try { + // 경력이 존재하는지 확인 + UserExperience experience = + userExperienceRepository + .findById(experienceId) + .orElseThrow(UserExperienceNotFoundException::new); + + // 본인의 경력인지 확인 + if (!userId.equals(experience.getUserId())) { + log.warn( + "UserExperience 권한 없음 - userId: {}, experienceId: {}, experienceUserId: {}, permission: {}", + userId, + experienceId, + experience.getUserId(), + permission); + throw new PermissionDeniedException(); + } + + log.info( + "UserExperience 권한 확인 완료 - userId: {}, experienceId: {}, permission: {}", + userId, + experienceId, + permission); + return true; + } catch (PermissionDeniedException e) { + throw e; + } catch (Exception e) { + log.error( + "UserExperience 권한 검사 중 오류 - experienceId: {}, error: {}", + experienceId, + e.getMessage()); + throw new PermissionDeniedException(); + } + } + + private boolean isSupportedPermission(String permission) { + return switch (permission) { + case "DELETE" -> true; + default -> false; + }; + } +} diff --git a/techeerzip/src/main/java/backend/techeerzip/global/permission/UserPermissionEvaluator.java b/techeerzip/src/main/java/backend/techeerzip/global/permission/UserPermissionEvaluator.java new file mode 100644 index 00000000..33938f10 --- /dev/null +++ b/techeerzip/src/main/java/backend/techeerzip/global/permission/UserPermissionEvaluator.java @@ -0,0 +1,88 @@ +package backend.techeerzip.global.permission; + +import java.io.Serializable; + +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import backend.techeerzip.domain.auth.jwt.CustomUserPrincipal; +import backend.techeerzip.domain.role.entity.RoleType; +import backend.techeerzip.global.exception.PermissionDeniedException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +@RequiredArgsConstructor +public class UserPermissionEvaluator implements DomainPermissionEvaluator { + + @Override + public boolean supports(String targetType) { + return "User".equals(targetType); + } + + @Override + @Transactional(readOnly = true) + public boolean hasPermission( + Authentication auth, Serializable targetId, String targetType, String permission) { + if (!supports(targetType)) { + return false; + } + + CustomUserPrincipal user = (CustomUserPrincipal) auth.getPrincipal(); + Long userId = user.getUserId(); + Long targetUserId = (Long) targetId; + + if (!isSupportedPermission(permission)) { + return false; + } + + try { + // 본인인지 확인 + if (!userId.equals(targetUserId)) { + log.warn( + "User 권한 없음 - userId: {}, targetUserId: {}, permission: {}", + userId, + targetUserId, + permission); + throw new PermissionDeniedException(); + } + + // 닉네임 업데이트: ADMIN, MENTOR만 허용 + if ("UPDATE_NICKNAME".equals(permission)) { + RoleType roleType = user.getRole(); + Long roleId = roleType.getRoleId(); + + if (roleId == null || (roleId != 1L && roleId != 2L)) { + log.warn( + "UPDATE_NICKNAME 권한 없음 - userId: {}, roleType: {}", + targetUserId, + roleType, + roleId); + throw new PermissionDeniedException(); + } + } + + log.info( + "User 권한 확인 완료 - userId: {}, targetUserId: {}, permission: {}", + userId, + targetUserId, + permission); + return true; + } catch (PermissionDeniedException e) { + throw e; + } catch (Exception e) { + log.error( + "User 권한 검사 중 오류 - targetUserId: {}, error: {}", targetUserId, e.getMessage()); + throw new PermissionDeniedException(); + } + } + + private boolean isSupportedPermission(String permission) { + return switch (permission) { + case "DELETE", "UPDATE", "UPDATE_NICKNAME", "CHANGE_TECHEER", "UPDATE_GITHUB" -> true; + default -> false; + }; + } +} diff --git a/techeerzip/src/test/java/backend/techeerzip/config/TestExternalServiceConfig.java b/techeerzip/src/test/java/backend/techeerzip/config/TestExternalServiceConfig.java index e583f09e..26b27f0b 100644 --- a/techeerzip/src/test/java/backend/techeerzip/config/TestExternalServiceConfig.java +++ b/techeerzip/src/test/java/backend/techeerzip/config/TestExternalServiceConfig.java @@ -49,7 +49,8 @@ public class TestExternalServiceConfig { */ @MockBean private RedisConnectionFactory redisConnectionFactory; - @MockBean private RedisTemplate redisTemplate; + @MockBean(name = "stringRedisTemplate") + private RedisTemplate redisTemplate; @MockBean private RedisMessageListenerContainer redisMessageListenerContainer; diff --git a/techeerzip/src/test/java/backend/techeerzip/domain/role/entity/RoleTest.java b/techeerzip/src/test/java/backend/techeerzip/domain/role/entity/RoleTest.java new file mode 100644 index 00000000..1917bb6f --- /dev/null +++ b/techeerzip/src/test/java/backend/techeerzip/domain/role/entity/RoleTest.java @@ -0,0 +1,36 @@ +package backend.techeerzip.domain.role.entity; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@DisplayName("Role Entity Test") +class RoleTest { + + @Nested + @DisplayName("Role 생성 테스트") + class CreationTest { + + @Test + @DisplayName("Role 생성 성공") + void create_WithName_Success() { + Role role = new Role("USER"); + + assertThat(role.getName()).isEqualTo("USER"); + assertThat(role.getParent()).isNull(); + } + + @Test + @DisplayName("부모 Role 설정 + 생성 성공") + void create_WithNameAndParent_Success() { + Role parentRole = new Role("USER"); + + Role childRole = new Role("ADMIN", parentRole); + + assertThat(childRole.getName()).isEqualTo("ADMIN"); + assertThat(childRole.getParent()).isEqualTo(parentRole); + } + } +} diff --git a/techeerzip/src/test/java/backend/techeerzip/domain/role/helper/RoleTestHelper.java b/techeerzip/src/test/java/backend/techeerzip/domain/role/helper/RoleTestHelper.java new file mode 100644 index 00000000..d016cb83 --- /dev/null +++ b/techeerzip/src/test/java/backend/techeerzip/domain/role/helper/RoleTestHelper.java @@ -0,0 +1,63 @@ +package backend.techeerzip.domain.role.helper; + +import org.springframework.test.util.ReflectionTestUtils; + +import backend.techeerzip.domain.role.entity.Role; + +public class RoleTestHelper { + + private RoleTestHelper() {} + + // ========================================================================= + // 1. Service Test 전용 메서드 (컴파일 에러 해결용) + // ========================================================================= + + /** + * [Service Test용] ID와 이름으로 빠르게 Role을 생성합니다. + * + * @param id 설정할 ID (Reflection 사용) + * @param name Role 이름 + * @return ID가 세팅된 Role 객체 + */ + public static Role createRole(Long id, String name) { + return createRoleWithId(id, name); + } + + // ========================================================================= + // 2. Repository Test 및 공통 메서드 + // ========================================================================= + + public static Role createRole(String name) { + return new Role(name); + } + + /** 부모 Role을 가진 Role을 생성 */ + public static Role createRole(String name, Role parent) { + return new Role(name, parent); + } + + /** ID를 지정하여 Role을 생성 */ + public static Role createRoleWithId(Long id, String name) { + Role role = createRole(name); + ReflectionTestUtils.setField(role, "id", id); + return role; + } + + public static class Default { + public static final String ADMIN_TEST = "ADMIN_TEST"; + public static final String USER_TEST = "USER_TEST"; + public static final String MENTOR_TEST = "MENTOR_TEST"; + + public static Role admin() { + return createRole(ADMIN_TEST); + } + + public static Role user() { + return createRole(USER_TEST); + } + + public static Role mentor() { + return createRole(MENTOR_TEST); + } + } +} diff --git a/techeerzip/src/test/java/backend/techeerzip/domain/role/repository/RoleRepositoryTest.java b/techeerzip/src/test/java/backend/techeerzip/domain/role/repository/RoleRepositoryTest.java new file mode 100644 index 00000000..f4b54894 --- /dev/null +++ b/techeerzip/src/test/java/backend/techeerzip/domain/role/repository/RoleRepositoryTest.java @@ -0,0 +1,71 @@ +package backend.techeerzip.domain.role.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Optional; +import java.util.UUID; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +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.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; + +import backend.techeerzip.config.RepositoryTestSupport; +import backend.techeerzip.domain.role.entity.Role; +import backend.techeerzip.domain.role.helper.RoleTestHelper; +import backend.techeerzip.global.config.QueryDslConfig; + +@DataJpaTest( + properties = { + "spring.jpa.hibernate.ddl-auto=create", + "spring.jpa.properties.hibernate.globally_quoted_identifiers=true" + }) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@ActiveProfiles("test") +@Import(QueryDslConfig.class) +@DisplayName("RoleRepository 테스트") +class RoleRepositoryTest extends RepositoryTestSupport { + + @Autowired private RoleRepository roleRepository; + + @Nested + @DisplayName("Role 테스트") + class BasicCrudTest { + + @Test + @DisplayName("Role 저장 및 조회 성공") + void saveAndFind_Success() { + String uniqueRoleName = + RoleTestHelper.Default.MENTOR_TEST + + "-" + + UUID.randomUUID().toString().substring(0, 8); + Role newRole = RoleTestHelper.createRole(uniqueRoleName); + Role saved = roleRepository.save(newRole); + + Optional found = roleRepository.findById(saved.getId()); + assertThat(found).isPresent(); + assertThat(found.get().getName()).isEqualTo(uniqueRoleName); + } + + @Test + @DisplayName("Role 삭제 성공") + void delete_Success() { + String uniqueRoleName = + RoleTestHelper.Default.USER_TEST + + "-" + + UUID.randomUUID().toString().substring(0, 8); + Role userRole = RoleTestHelper.createRole(uniqueRoleName); + Role saved = roleRepository.save(userRole); + + Long roleId = saved.getId(); + roleRepository.delete(saved); + + Optional found = roleRepository.findById(roleId); + assertThat(found).isEmpty(); + } + } +} diff --git a/techeerzip/src/test/java/backend/techeerzip/domain/user/controller/UserControllerTest.java b/techeerzip/src/test/java/backend/techeerzip/domain/user/controller/UserControllerTest.java new file mode 100644 index 00000000..752befd0 --- /dev/null +++ b/techeerzip/src/test/java/backend/techeerzip/domain/user/controller/UserControllerTest.java @@ -0,0 +1,492 @@ +package backend.techeerzip.domain.user.controller; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.List; + +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import backend.techeerzip.domain.auth.jwt.CustomUserPrincipal; +import backend.techeerzip.domain.auth.jwt.JwtTokenProvider; +import backend.techeerzip.domain.bootcamp.service.BootcampService; +import backend.techeerzip.domain.role.entity.RoleType; +import backend.techeerzip.domain.user.dto.request.CreateExternalUserRequest; +import backend.techeerzip.domain.user.dto.request.CreateUserRequest; +import backend.techeerzip.domain.user.dto.request.CreateUserWithResumeRequest; +import backend.techeerzip.domain.user.dto.request.ResetUserPasswordRequest; +import backend.techeerzip.domain.user.dto.request.UpdateGithubUrlRequest; +import backend.techeerzip.domain.user.dto.request.UpdateUserNicknameRequest; +import backend.techeerzip.domain.user.dto.request.UpdateUserTecheerInfoRequest; +import backend.techeerzip.domain.user.dto.request.UpdateUserTecheerInfoWithResumeRequest; +import backend.techeerzip.domain.user.dto.request.UpdateUserWithExperienceRequest; +import backend.techeerzip.domain.user.dto.response.BootcampMemberListResponse; +import backend.techeerzip.domain.user.dto.response.GetPermissionResponse; +import backend.techeerzip.domain.user.dto.response.GetProfileImgResponse; +import backend.techeerzip.domain.user.dto.response.GetUserProfileListResponse; +import backend.techeerzip.domain.user.dto.response.GetUserResponse; +import backend.techeerzip.domain.user.entity.PermissionRequest; +import backend.techeerzip.domain.user.service.UserService; +import backend.techeerzip.global.logger.CustomLogger; +import backend.techeerzip.global.permission.DelegatingPermissionEvaluator; +import backend.techeerzip.global.permission.UserExperiencePermissionEvaluator; +import backend.techeerzip.global.permission.UserPermissionEvaluator; + +@WebMvcTest( + controllers = UserController.class, + excludeAutoConfiguration = { + org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class + }) +@Import(UserControllerTest.TestWebConfig.class) +@ActiveProfiles("test") +@DisplayName("UserController Test") +class UserControllerTest { + + @TestConfiguration + static class TestWebConfig implements WebMvcConfigurer { + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(new backend.techeerzip.global.resolver.UserArgumentResolver()); + } + } + + @Autowired private MockMvc mockMvc; + + @Autowired private ObjectMapper objectMapper; + + @MockBean private UserService userService; + + @MockBean private BootcampService bootcampService; + + @MockBean private CustomLogger logger; + + @MockBean private JwtTokenProvider jwtTokenProvider; + + @MockBean private UserPermissionEvaluator userPermissionEvaluator; + + @MockBean private UserExperiencePermissionEvaluator userExperiencePermissionEvaluator; + + @MockBean private DelegatingPermissionEvaluator delegatingPermissionEvaluator; + + private static final Long TEST_USER_ID = 1L; + private static final String TEST_EMAIL = "test@example.com"; + + private static final String BASE_URL = "/api/v3/users"; + + @BeforeEach + void setUpPermissionEvaluator() { + when(delegatingPermissionEvaluator.hasPermission(any(), any(), any())).thenReturn(true); + when(delegatingPermissionEvaluator.hasPermission(any(), any(), anyString(), any())) + .thenReturn(true); + } + + @AfterEach + void tearDownSecurityContext() { + SecurityContextHolder.clearContext(); + } + + private MockHttpServletRequestBuilder withSecurity( + MockHttpServletRequestBuilder builder, Long userId, RoleType roleType) { + CustomUserPrincipal principal = + new CustomUserPrincipal(userId, TEST_EMAIL, "", null, roleType); + Authentication authentication = + new UsernamePasswordAuthenticationToken(principal, "", principal.getAuthorities()); + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + securityContext.setAuthentication(authentication); + SecurityContextHolder.setContext(securityContext); + + return builder; + } + + @Nested + @DisplayName("회원가입 테스트") + class SignupTest { + + @Test + @DisplayName("회원가입 성공") + void signup_Success() throws Exception { + CreateUserRequest createUserRequest = + new CreateUserRequest( + "BACKEND", + null, + "테커집짱", + "https://github.com/test", + null, + false, + "인천대학교", + "1학년", + null, + null, + "Password123!", + TEST_EMAIL, + 6); + CreateUserWithResumeRequest request = + new CreateUserWithResumeRequest(createUserRequest, null, null); + doNothing().when(userService).signUp(any(), any()); + + MockMultipartFile emptyFile = + new MockMultipartFile("file", "resume.pdf", "application/pdf", new byte[0]); + MockMultipartFile requestPart = + new MockMultipartFile( + "createUserWithResumeRequest", + "", + "application/json", + objectMapper.writeValueAsBytes(request)); + + mockMvc.perform(multipart(BASE_URL + "/signup").file(emptyFile).file(requestPart)) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("외부인 회원가입 성공") + void signupExternal_Success() throws Exception { + CreateExternalUserRequest request = + new CreateExternalUserRequest("테스트", TEST_EMAIL, "Password123!", null); + doNothing().when(userService).signUpExternal(any()); + + mockMvc.perform( + post(BASE_URL + "/signup/external") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()); + } + } + + @Nested + @DisplayName("회원 탈퇴") + class DeleteUserTest { + + @Test + @DisplayName("회원 탈퇴 성공") + void deleteUser_Success() throws Exception { + doNothing().when(userService).deleteUser(anyLong(), any(HttpServletResponse.class)); + + mockMvc.perform(withSecurity(delete(BASE_URL), TEST_USER_ID, RoleType.TECHEER)) + .andExpect(status().isOk()); + } + } + + @Nested + @DisplayName("비밀번호 재설정 테스트") + class ResetPasswordTest { + + @Test + @DisplayName("비밀번호 재설정 성공") + void resetPassword_Success() throws Exception { + ResetUserPasswordRequest request = + new ResetUserPasswordRequest(TEST_EMAIL, "123456", "NewPassword123!"); + doNothing().when(userService).resetPassword(anyString(), anyString(), anyString()); + + mockMvc.perform( + patch(BASE_URL + "/password/reset") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()); + } + } + + @Nested + @DisplayName("유저 정보 조회") + class GetUserTest { + + @Test + @DisplayName("유저 정보 조회 성공") + void getUser_Success() throws Exception { + GetUserResponse response = GetUserResponse.builder().build(); + when(userService.getUserInfo(anyLong())).thenReturn(response); + + mockMvc.perform(withSecurity(get(BASE_URL), TEST_USER_ID, RoleType.TECHEER)) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("특정 프로필 조회 성공") + void getProfile_Success() throws Exception { + GetUserResponse response = GetUserResponse.builder().build(); + when(userService.getUserInfo(anyLong())).thenReturn(response); + + mockMvc.perform(get(BASE_URL + "/{userId}", TEST_USER_ID)).andExpect(status().isOk()); + } + } + + @Nested + @DisplayName("프로필 이미지 테스트") + class ProfileImageTest { + + @Test + @DisplayName("프로필 이미지 동기화 성공") + void updateProfileImage_Success() throws Exception { + String requestJson = "{\"email\":\"" + TEST_EMAIL + "\"}"; + GetProfileImgResponse response = + new GetProfileImgResponse("https://example.com/image.jpg"); + when(userService.updateProfileImg(anyString())).thenReturn(response); + + mockMvc.perform( + patch(BASE_URL + "/profileImage") + .contentType(MediaType.APPLICATION_JSON) + .content(requestJson)) + .andExpect(status().isOk()); + } + } + + @Nested + @DisplayName("권한 요청 테스트") + class PermissionRequestTest { + + @Test + @DisplayName("권한 요청 성공") + void requestPermission_Success() throws Exception { + String requestJson = "{\"roleId\":2}"; + when(userService.createUserPermissionRequest(anyLong(), anyLong())) + .thenReturn(PermissionRequest.builder().build()); + + mockMvc.perform( + withSecurity( + post(BASE_URL + "/permission/request") + .contentType(MediaType.APPLICATION_JSON) + .content(requestJson), + TEST_USER_ID, + RoleType.TECHEER)) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("[Admin] 권한 요청 목록 조회 성공") + void getPermissionRequests_Success() throws Exception { + List response = List.of(); + when(userService.getAllPendingPermissionRequests()).thenReturn(response); + + mockMvc.perform( + withSecurity( + get(BASE_URL + "/admin/permission/request"), + TEST_USER_ID, + RoleType.ADMIN)) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("[Admin] 권한 승인 성공") + void approvePermission_Success() throws Exception { + String requestJson = "{\"userId\":" + TEST_USER_ID + ",\"newRoleId\":2}"; + doNothing().when(userService).approveUserPermission(anyLong(), anyLong()); + + mockMvc.perform( + withSecurity( + patch(BASE_URL + "/admin/permission/approve") + .contentType(MediaType.APPLICATION_JSON) + .content(requestJson), + TEST_USER_ID, + RoleType.ADMIN)) + .andExpect(status().isOk()); + } + } + + @Nested + @DisplayName("프로필 업데이트 테스트") + class UpdateProfileTest { + + @Test + @DisplayName("프로필 업데이트 성공") + void updateProfile_Success() throws Exception { + UpdateUserWithExperienceRequest request = new UpdateUserWithExperienceRequest(); + doNothing().when(userService).updateProfile(anyLong(), any()); + + mockMvc.perform( + withSecurity( + patch(BASE_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)), + TEST_USER_ID, + RoleType.TECHEER)) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("닉네임 업데이트 성공") + void updateNickname_Success() throws Exception { + UpdateUserNicknameRequest request = new UpdateUserNicknameRequest(); + request.setNickname("새닉네임"); + doNothing().when(userService).updateNickname(anyLong(), anyString()); + + mockMvc.perform( + withSecurity( + patch(BASE_URL + "/nickname") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)), + TEST_USER_ID, + RoleType.TECHEER)) + .andExpect(status().isOk()); + } + } + + @Nested + @DisplayName("경력 삭제 테스트") + class DeleteExperienceTest { + + @Test + @DisplayName("경력 삭제 성공") + void deleteExperience_Success() throws Exception { + Long experienceId = 1L; + doNothing().when(userService).deleteExperience(anyLong()); + + mockMvc.perform( + withSecurity( + delete(BASE_URL + "/experience/{experienceId}", experienceId), + TEST_USER_ID, + RoleType.TECHEER)) + .andExpect(status().isOk()); + } + } + + @Nested + @DisplayName("프로필 목록 조회 테스트") + class GetProfilesTest { + + @Test + @DisplayName("모든 프로필 조회 성공") + void getAllProfiles_Success() throws Exception { + GetUserProfileListResponse response = + new GetUserProfileListResponse(List.of(), false, null); + when(userService.getAllProfiles(any())).thenReturn(response); + + mockMvc.perform(get(BASE_URL + "/profiles")).andExpect(status().isOk()); + } + + @Test + @DisplayName("부트캠프 멤버 프로필 조회 성공") + void getBootcampMemberProfiles_Success() throws Exception { + BootcampMemberListResponse response = + BootcampMemberListResponse.builder() + .profiles(List.of()) + .hasNext(false) + .nextCursor(null) + .build(); + when(userService.getBootcampMemberProfiles(any(), any(), any(), any())) + .thenReturn(response); + + mockMvc.perform(get(BASE_URL + "/profiles/bootcampMember")).andExpect(status().isOk()); + } + } + + @Nested + @DisplayName("Techeer 변경 테스트") + class ChangeTecheerTest { + + @Test + @DisplayName("Techeer 변경 성공") + void changeTecheer_Success() throws Exception { + UpdateUserTecheerInfoRequest infoRequest = new UpdateUserTecheerInfoRequest(); + java.lang.reflect.Field mainPositionField = + UpdateUserTecheerInfoRequest.class.getDeclaredField("mainPosition"); + mainPositionField.setAccessible(true); + mainPositionField.set(infoRequest, "BACKEND"); + + java.lang.reflect.Field githubUrlField = + UpdateUserTecheerInfoRequest.class.getDeclaredField("githubUrl"); + githubUrlField.setAccessible(true); + githubUrlField.set(infoRequest, "https://github.com/test"); + + java.lang.reflect.Field isLftField = + UpdateUserTecheerInfoRequest.class.getDeclaredField("isLft"); + isLftField.setAccessible(true); + isLftField.set(infoRequest, false); + + java.lang.reflect.Field schoolField = + UpdateUserTecheerInfoRequest.class.getDeclaredField("school"); + schoolField.setAccessible(true); + schoolField.set(infoRequest, "인천대학교"); + + java.lang.reflect.Field gradeField = + UpdateUserTecheerInfoRequest.class.getDeclaredField("grade"); + gradeField.setAccessible(true); + gradeField.set(infoRequest, "1학년"); + + UpdateUserTecheerInfoWithResumeRequest request = + new UpdateUserTecheerInfoWithResumeRequest(); + java.lang.reflect.Field wrapperField = + UpdateUserTecheerInfoWithResumeRequest.class.getDeclaredField( + "updateUserTecheerInfoRequest"); + wrapperField.setAccessible(true); + wrapperField.set(request, infoRequest); + doNothing().when(userService).changeTecheer(anyLong(), any(), any()); + + MockMultipartFile emptyFile = + new MockMultipartFile("file", "resume.pdf", "application/pdf", new byte[0]); + MockMultipartFile requestPart = + new MockMultipartFile( + "updateUserTecheerInfoWithResumeRequest", + "", + "application/json", + objectMapper.writeValueAsBytes(request)); + + mockMvc.perform( + withSecurity( + multipart(BASE_URL + "/techeer") + .file(emptyFile) + .file(requestPart) + .with( + req -> { + req.setMethod("PATCH"); + return req; + }), + TEST_USER_ID, + RoleType.TECHEER)) + .andExpect(status().isOk()); + } + } + + @Nested + @DisplayName("GitHub 동기화 테스트") + class SyncGithubDataTest { + + @Test + @DisplayName("GitHub 데이터 동기화 성공") + void syncGithubData_Success() throws Exception { + UpdateGithubUrlRequest request = new UpdateGithubUrlRequest(); + request.setGithubUrl("https://github.com/test"); + doNothing().when(userService).updateGithubUrlAndSync(anyLong(), anyString()); + + mockMvc.perform( + withSecurity( + post(BASE_URL + "/github/username") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)), + TEST_USER_ID, + RoleType.TECHEER)) + .andExpect(status().isOk()); + } + } +} diff --git a/techeerzip/src/test/java/backend/techeerzip/domain/user/entity/BootcampPeriodTest.java b/techeerzip/src/test/java/backend/techeerzip/domain/user/entity/BootcampPeriodTest.java new file mode 100644 index 00000000..217591f2 --- /dev/null +++ b/techeerzip/src/test/java/backend/techeerzip/domain/user/entity/BootcampPeriodTest.java @@ -0,0 +1,62 @@ +package backend.techeerzip.domain.user.entity; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.LocalDate; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +@DisplayName("BootcampPeriod Test") +class BootcampPeriodTest { + + @Nested + @DisplayName("기수 계산 테스트") + class CalculateGenerationTest { + + @Test + @DisplayName("상반기 기수 계산 성공") + void calculateGeneration_FirstHalf_Returns() { + // 25년 상반기 기준 10기 반환 + LocalDate joinDate = LocalDate.of(2025, 6, 1); + + Integer generation = BootcampPeriod.calculateGeneration(joinDate); + + assertThat(generation).isEqualTo(10); + } + + @Test + @DisplayName("하반기 기수 계산 성공") + void calculateGeneration_SecondHalf_Returns() { + LocalDate joinDate = LocalDate.of(2025, 12, 1); + + Integer generation = BootcampPeriod.calculateGeneration(joinDate); + + assertThat(generation).isEqualTo(11); + } + + @Test + @DisplayName("1월 -> 하반기로 계산") + void calculateGeneration_January_Returns() { + LocalDate joinDate = LocalDate.of(2026, 1, 15); + + Integer generation = BootcampPeriod.calculateGeneration(joinDate); + + assertThat(generation).isEqualTo(11); + } + + @ParameterizedTest + @CsvSource({"3", "4", "5", "9", "10", "11"}) + @DisplayName("유효하지 않은 월은 null 반환") + void calculateGeneration_InvalidMonth_ReturnsNull(int month) { + LocalDate joinDate = LocalDate.of(2025, month, 15); + + Integer generation = BootcampPeriod.calculateGeneration(joinDate); + + assertThat(generation).isNull(); + } + } +} diff --git a/techeerzip/src/test/java/backend/techeerzip/domain/user/entity/PermissionRequestTest.java b/techeerzip/src/test/java/backend/techeerzip/domain/user/entity/PermissionRequestTest.java new file mode 100644 index 00000000..ed31b417 --- /dev/null +++ b/techeerzip/src/test/java/backend/techeerzip/domain/user/entity/PermissionRequestTest.java @@ -0,0 +1,74 @@ +package backend.techeerzip.domain.user.entity; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import backend.techeerzip.domain.role.entity.Role; +import backend.techeerzip.domain.user.helper.UserTestHelper; +import backend.techeerzip.global.entity.StatusCategory; + +@DisplayName("PermissionRequest Test") +class PermissionRequestTest { + + private User testUser; + private PermissionRequest permissionRequest; + + @BeforeEach + void setUp() { + testUser = + UserTestHelper.createUser( + "테커집짱", "test@example.com", "Techeerzip1!", new Role("USER"), true); + + permissionRequest = PermissionRequest.builder().user(testUser).requestedRoleId(1L).build(); + } + + @Nested + @DisplayName("PermissionRequest 생성 테스트") + class CreationTest { + + @Test + @DisplayName("권한 승인 요청 시 PENDING 상태로 생성") + void create_WithBuilder_StatusIsPending() { + PermissionRequest request = + PermissionRequest.builder().user(testUser).requestedRoleId(1L).build(); + + assertThat(request.getStatus()).isEqualTo(StatusCategory.PENDING); + assertThat(request.getUser()).isEqualTo(testUser); + assertThat(request.getRequestedRoleId()).isEqualTo(1L); + } + } + + @Nested + @DisplayName("권한 승인 테스트") + class ApproveTest { + + @Test + @DisplayName("권한 상태 APPROVED로 변경") + void approve_ChangesStatusToApproved() { + assertThat(permissionRequest.getStatus()).isEqualTo(StatusCategory.PENDING); + + permissionRequest.approve(); + + assertThat(permissionRequest.getStatus()).isEqualTo(StatusCategory.APPROVED); + } + } + + @Nested + @DisplayName("권한 거부 테스트") + class RejectTest { + + @Test + @DisplayName("권한 상태 REJECT로 변경") + void reject_ChangesStatusToRejected() { + assertThat(permissionRequest.getStatus()).isEqualTo(StatusCategory.PENDING); + + permissionRequest.reject(); + + assertThat(permissionRequest.getStatus()).isEqualTo(StatusCategory.REJECT); + } + } +} diff --git a/techeerzip/src/test/java/backend/techeerzip/domain/user/entity/UserTest.java b/techeerzip/src/test/java/backend/techeerzip/domain/user/entity/UserTest.java new file mode 100644 index 00000000..e980eafb --- /dev/null +++ b/techeerzip/src/test/java/backend/techeerzip/domain/user/entity/UserTest.java @@ -0,0 +1,228 @@ +package backend.techeerzip.domain.user.entity; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import backend.techeerzip.domain.role.entity.Role; +import backend.techeerzip.domain.user.helper.UserTestHelper; + +@DisplayName("User Test") +class UserTest { + + private Role defaultRole; + + @BeforeEach + void setUp() { + defaultRole = new Role("USER"); + } + + private User createDefaultUser() { + return createUserWithRole(defaultRole); + } + + private User createUserWithRole(Role role) { + return UserTestHelper.createUser("테커집짱", "test@example.com", "Techeerzip1!", role, true); + } + + private User createUserWithAllFields() { + User user = UserTestHelper.createUserWithAllFields("", defaultRole); + user.setLft(true); + return user; + } + + private User createUserForUpdate() { + User user = UserTestHelper.createUserWithAllFields("", defaultRole); + user.setGithubUrl("https://github.com/old"); + user.setMainPosition("FRONTEND"); + user.setSubPosition("BACKEND"); + user.setProfileImage("https://example.com/old.jpg"); + user.setMediumUrl("https://medium.com/@old"); + user.setTistoryUrl("https://old.tistory.com"); + user.setVelogUrl("https://velog.io/@old"); + return user; + } + + @Nested + @DisplayName("User Create") + class CreationTest { + + @Test + @DisplayName("User 생성 성공") + void create_WithBuilder_Success() { + User user = createDefaultUser(); + + assertThat(user.getName()).isEqualTo("테커집짱"); + assertThat(user.getEmail()).isEqualTo("test@example.com"); + assertThat(user.getPassword()).isEqualTo("Techeerzip1!"); + assertThat(user.getRole()).isEqualTo(defaultRole); + assertThat(user.isAuth()).isTrue(); + assertThat(user.isDeleted()).isFalse(); + } + + @Test + @DisplayName("모든 필드: User 생성 성공") + void create_WithAllFields_Success() { + User user = createUserWithAllFields(); + + assertThat(user.getName()).isEqualTo("테커집짱"); + assertThat(user.getEmail()).isEqualTo("test@example.com"); + assertThat(user.getNickname()).isEqualTo("테커집짱짱"); + assertThat(user.getYear()).isEqualTo(1); + assertThat(user.isLft()).isTrue(); + assertThat(user.getGithubUrl()).isEqualTo("https://github.com/test"); + assertThat(user.getMainPosition()).isEqualTo("BACKEND"); + assertThat(user.getSubPosition()).isEqualTo("FRONTEND"); + assertThat(user.getSchool()).isEqualTo("테커집대학교"); + assertThat(user.getProfileImage()).isEqualTo("https://example.com/image.jpg"); + assertThat(user.getGrade()).isEqualTo("1학년"); + assertThat(user.getMediumUrl()).isEqualTo("https://medium.com/@test"); + assertThat(user.getTistoryUrl()).isEqualTo("https://test.tistory.com"); + assertThat(user.getVelogUrl()).isEqualTo("https://velog.io/@test"); + assertThat(user.getBootcampYear()).isEqualTo(10); + assertThat(user.getFeedbackNotes()).isEqualTo(""); + assertThat(user.isDeleted()).isFalse(); + } + + @Test + @DisplayName("최소 필수 필드: User 생성 성공") + void create_WithMinimalFields_Success() { + User user = createDefaultUser(); + + assertThat(user.getName()).isNotNull(); + assertThat(user.getEmail()).isNotNull(); + assertThat(user.getPassword()).isNotNull(); + assertThat(user.getRole()).isNotNull(); + assertThat(user.isDeleted()).isFalse(); + } + } + + @Nested + @DisplayName("User Update") + class UpdateTest { + + private User user; + + @BeforeEach + void setUp() { + user = createUserForUpdate(); + } + + @Test + @DisplayName("모든 필드 업데이트 성공") + void update_AllFields_Success() { + String newName = "테커집짱2"; + String newNickname = "테커집짱짱2"; + String newGithubUrl = "https://github.com/new"; + String newMainPosition = "BACKEND"; + String newSubPosition = "DEVOPS"; + String newSchool = "테커집초등학교"; + String newProfileImage = "https://example.com/new.jpg"; + String newGrade = "3학년"; + String newMediumUrl = "https://medium.com/@new"; + String newTistoryUrl = "https://new.tistory.com"; + String newVelogUrl = "https://velog.io/@new"; + + user.update( + newName, + newNickname, + newGithubUrl, + newMainPosition, + newSubPosition, + newSchool, + newProfileImage, + newGrade, + newMediumUrl, + newTistoryUrl, + newVelogUrl); + + assertThat(user.getName()).isEqualTo(newName); + assertThat(user.getNickname()).isEqualTo(newNickname); + assertThat(user.getGithubUrl()).isEqualTo(newGithubUrl); + assertThat(user.getMainPosition()).isEqualTo(newMainPosition); + assertThat(user.getSubPosition()).isEqualTo(newSubPosition); + assertThat(user.getSchool()).isEqualTo(newSchool); + assertThat(user.getProfileImage()).isEqualTo(newProfileImage); + assertThat(user.getGrade()).isEqualTo(newGrade); + assertThat(user.getMediumUrl()).isEqualTo(newMediumUrl); + assertThat(user.getTistoryUrl()).isEqualTo(newTistoryUrl); + assertThat(user.getVelogUrl()).isEqualTo(newVelogUrl); + } + + @Test + @DisplayName("일부 필드 업데이트 성공") + void update_PartialFields_Success() { + String newName = "테커집짱2"; + String newGithubUrl = "https://github.com/new"; + + user.update( + newName, + user.getNickname(), + newGithubUrl, + user.getMainPosition(), + user.getSubPosition(), + user.getSchool(), + user.getProfileImage(), + user.getGrade(), + user.getMediumUrl(), + user.getTistoryUrl(), + user.getVelogUrl()); + + assertThat(user.getName()).isEqualTo(newName); + assertThat(user.getGithubUrl()).isEqualTo(newGithubUrl); + assertThat(user.getNickname()).isEqualTo("테커집짱짱"); + } + } + + @Nested + @DisplayName("FeedbackNote Update") + class UpdateFeedbackNotesTest { + + private User user; + + @BeforeEach + void setUp() { + user = createDefaultUser(); + } + + @Test + @DisplayName("피드백 노트 업데이트 성공") + void updateFeedbackNotes_Success() { + String feedbackNotes = "새로운 피드백"; + + user.updateFeedbackNotes(feedbackNotes); + + assertThat(user.getFeedbackNotes()).isEqualTo(feedbackNotes); + } + + @Test + @DisplayName("feedbackNote -> null 변경 성공") + void updateFeedbackNotes_WithNull_Success() { + user.updateFeedbackNotes("기존 피드백"); + assertThat(user.getFeedbackNotes()).isEqualTo("기존 피드백"); + + user.updateFeedbackNotes(null); + + assertThat(user.getFeedbackNotes()).isNull(); + } + } + + @Nested + @DisplayName("User Delete") + class DeleteTest { + + @Test + @DisplayName("유저 삭제 성공") + void delete_Success() { + User user = createDefaultUser(); + assertThat(user.isDeleted()).isFalse(); + + user.delete(); + + assertThat(user.isDeleted()).isTrue(); + } + } +} diff --git a/techeerzip/src/test/java/backend/techeerzip/domain/user/helper/UserTestHelper.java b/techeerzip/src/test/java/backend/techeerzip/domain/user/helper/UserTestHelper.java new file mode 100644 index 00000000..f2a1f725 --- /dev/null +++ b/techeerzip/src/test/java/backend/techeerzip/domain/user/helper/UserTestHelper.java @@ -0,0 +1,104 @@ +package backend.techeerzip.domain.user.helper; + +import org.springframework.test.util.ReflectionTestUtils; + +import backend.techeerzip.domain.role.entity.Role; +import backend.techeerzip.domain.user.entity.User; + +public class UserTestHelper { + + private UserTestHelper() {} + + // ========================================================================= + // 1. Service Test 전용 메서드 (컴파일 에러 해결용) + // ========================================================================= + + /** + * [Service Test용] ID만으로 빠르게 User를 생성합니다. + * + * @param id 설정할 ID (Reflection 사용) + * @return ID가 세팅된 User 객체 + */ + public static User createUser(Long id) { + Role role = new Role("USER_TEST"); + User user = createUser("테커집짱", "test@example.com", "Techeerzip1!", role, true); + ReflectionTestUtils.setField(user, "id", id); + ReflectionTestUtils.setField(user.getRole(), "id", 1L); + return user; + } + + /** + * [Service Test용] 이메일과 ID로 User를 생성합니다. + * + * @param email 이메일 + * @param id 설정할 ID (Reflection 사용) + * @return ID가 세팅된 User 객체 + */ + public static User createUser(String email, Long id) { + Role role = new Role("USER_TEST"); + User user = createUser("테커집짱", email, "Techeerzip1!", role, true); + ReflectionTestUtils.setField(user, "id", id); + ReflectionTestUtils.setField(user.getRole(), "id", 1L); + return user; + } + + // ========================================================================= + // 2. Repository Test 및 공통 메서드 + // ========================================================================= + + public static User createUser(String name, String email, String password, Role role) { + return createUser(name, email, password, role, true); + } + + /** 인증 여부를 지정하여 User를 생성 */ + public static User createUser( + String name, String email, String password, Role role, boolean isAuth) { + return User.builder() + .name(name) + .email(email) + .password(password) + .role(role) + .isAuth(isAuth) + .build(); + } + + /** 모든 필드를 포함한 User를 생성 */ + public static User createUserWithAllFields(String suffix, Role role) { + return User.builder() + .name("테커집짱" + suffix) + .email("test" + suffix + "@example.com") + .nickname("테커집짱짱" + suffix) + .year(1) + .password("Techeerzip1!") + .isLft(false) + .githubUrl("https://github.com/test" + suffix) + .mainPosition("BACKEND") + .subPosition("FRONTEND") + .school("테커집대학교") + .profileImage("https://example.com/image.jpg") + .isAuth(true) + .role(role) + .grade("1학년") + .mediumUrl("https://medium.com/@test" + suffix) + .tistoryUrl("https://test" + suffix + ".tistory.com") + .velogUrl("https://velog.io/@test" + suffix) + .bootcampYear(10) + .feedbackNotes("") + .build(); + } + + /** '삭제된' 상태의 User를 생성 */ + public static User createDeletedUser(String name, String email, String password, Role role) { + User user = createUser(name, email, password, role, true); + user.setDeleted(true); + return user; + } + + /** ID를 지정하여 User를 생성 */ + public static User createUserWithId( + Long id, String name, String email, String password, Role role) { + User user = createUser(name, email, password, role, true); + ReflectionTestUtils.setField(user, "id", id); + return user; + } +} diff --git a/techeerzip/src/test/java/backend/techeerzip/domain/user/repository/PermissionRequestRepositoryTest.java b/techeerzip/src/test/java/backend/techeerzip/domain/user/repository/PermissionRequestRepositoryTest.java new file mode 100644 index 00000000..52180021 --- /dev/null +++ b/techeerzip/src/test/java/backend/techeerzip/domain/user/repository/PermissionRequestRepositoryTest.java @@ -0,0 +1,150 @@ +package backend.techeerzip.domain.user.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import backend.techeerzip.config.RepositoryTestSupport; +import backend.techeerzip.domain.role.entity.Role; +import backend.techeerzip.domain.role.helper.RoleTestHelper; +import backend.techeerzip.domain.role.repository.RoleRepository; +import backend.techeerzip.domain.user.entity.PermissionRequest; +import backend.techeerzip.domain.user.entity.User; +import backend.techeerzip.domain.user.helper.UserTestHelper; +import backend.techeerzip.global.config.QueryDslConfig; +import backend.techeerzip.global.entity.StatusCategory; + +@DataJpaTest( + properties = { + "spring.jpa.hibernate.ddl-auto=create", + "spring.jpa.properties.hibernate.globally_quoted_identifiers=true" + }) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@ActiveProfiles("test") +@Import({UserRepositoryImpl.class, QueryDslConfig.class}) +@DisplayName("PermissionRequestRepository Test") +class PermissionRequestRepositoryTest extends RepositoryTestSupport { + + @Autowired private PermissionRequestRepository permissionRequestRepository; + + @Autowired private RoleRepository roleRepository; + + @Autowired private UserRepository userRepository; + + private Role adminRole; + private Role userRole; + private User testUser; + + @BeforeEach + void setUp() { + String uniqueAdminRoleName = + RoleTestHelper.Default.ADMIN_TEST + + "-" + + UUID.randomUUID().toString().substring(0, 8); + String uniqueUserRoleName = + RoleTestHelper.Default.USER_TEST + + "-" + + UUID.randomUUID().toString().substring(0, 8); + adminRole = roleRepository.save(RoleTestHelper.createRole(uniqueAdminRoleName)); + userRole = roleRepository.save(RoleTestHelper.createRole(uniqueUserRoleName)); + + String uniqueEmail = "test-" + UUID.randomUUID() + "@example.com"; + String uniqueName = "테커집짱-" + UUID.randomUUID().toString().substring(0, 8); + testUser = + userRepository.save( + UserTestHelper.createUser( + uniqueName, uniqueEmail, "Techeerzip1!", userRole, true)); + } + + @Test + @DisplayName("권한 요청 저장 및 조회 성공") + void saveAndFind_Success() { + + PermissionRequest newRequest = + PermissionRequest.builder() + .user(testUser) + .requestedRoleId(adminRole.getId()) + .build(); + + PermissionRequest saved = permissionRequestRepository.save(newRequest); + + var found = permissionRequestRepository.findById(saved.getId()); + assertThat(found).isPresent(); + assertThat(found.get().getStatus()).isEqualTo(StatusCategory.PENDING); + assertThat(found.get().getUser().getId()).isEqualTo(testUser.getId()); + } + + @Test + @DisplayName("PENDING 상태의 요청만 조회") + void findByStatus_Pending_ReturnsOnlyPending() { + + PermissionRequest pendingRequest = + PermissionRequest.builder() + .user(testUser) + .requestedRoleId(adminRole.getId()) + .build(); + permissionRequestRepository.save(pendingRequest); + + List pendingRequests = + permissionRequestRepository.findByStatus(StatusCategory.PENDING); + + assertThat(pendingRequests).isNotEmpty(); + assertThat(pendingRequests) + .allMatch(request -> request.getStatus() == StatusCategory.PENDING); + } + + @Test + @Transactional + @DisplayName("사용자의 PENDING 권한 요청을 APPROVED로 일괄 업데이트") + void updateStatusByUserId_PendingRequests_UpdatesStatus() { + + PermissionRequest pendingRequest = + PermissionRequest.builder() + .user(testUser) + .requestedRoleId(adminRole.getId()) + .build(); + permissionRequestRepository.save(pendingRequest); + + var pendingBefore = + permissionRequestRepository.findByStatus(StatusCategory.PENDING).stream() + .filter(req -> req.getUser().getId().equals(testUser.getId())) + .toList(); + assertThat(pendingBefore).isNotEmpty(); + + int updatedCount = + permissionRequestRepository.updateStatusByUserId( + testUser.getId(), StatusCategory.APPROVED); + + assertThat(updatedCount).isGreaterThan(0); + + var pendingAfter = + permissionRequestRepository.findByStatus(StatusCategory.PENDING).stream() + .filter(req -> req.getUser().getId().equals(testUser.getId())) + .toList(); + assertThat(pendingAfter.size()).isLessThan(pendingBefore.size()); + } + + @Test + @Transactional + @DisplayName("존재하지 않는 사용자 ID로 업데이트 시 0 반환") + void updateStatusByUserId_NonExistentUser_ReturnsZero() { + Long nonExistentUserId = 999L; + + int updatedCount = + permissionRequestRepository.updateStatusByUserId( + nonExistentUserId, StatusCategory.APPROVED); + + assertThat(updatedCount).isEqualTo(0); + } +} diff --git a/techeerzip/src/test/java/backend/techeerzip/domain/user/repository/UserRepositoryTest.java b/techeerzip/src/test/java/backend/techeerzip/domain/user/repository/UserRepositoryTest.java new file mode 100644 index 00000000..a35b309f --- /dev/null +++ b/techeerzip/src/test/java/backend/techeerzip/domain/user/repository/UserRepositoryTest.java @@ -0,0 +1,85 @@ +package backend.techeerzip.domain.user.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.UUID; + +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.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; + +import backend.techeerzip.config.RepositoryTestSupport; +import backend.techeerzip.domain.role.entity.Role; +import backend.techeerzip.domain.role.helper.RoleTestHelper; +import backend.techeerzip.domain.role.repository.RoleRepository; +import backend.techeerzip.domain.user.entity.User; +import backend.techeerzip.domain.user.helper.UserTestHelper; +import backend.techeerzip.global.config.QueryDslConfig; + +@DataJpaTest( + properties = { + "spring.jpa.hibernate.ddl-auto=create", + "spring.jpa.properties.hibernate.globally_quoted_identifiers=true" + }) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@ActiveProfiles("test") +@Import({UserRepositoryImpl.class, QueryDslConfig.class}) +@DisplayName("UserRepository Test") +class UserRepositoryTest extends RepositoryTestSupport { + + @Autowired private UserRepository userRepository; + + @Autowired private RoleRepository roleRepository; + + @Test + @DisplayName("이메일로 사용자 조회 성공") + void findByEmail_ExistingUser_Success() { + String roleName = + RoleTestHelper.Default.USER_TEST + + "-" + + UUID.randomUUID().toString().substring(0, 8); + Role role = roleRepository.save(RoleTestHelper.createRole(roleName)); + + String email = "test-" + UUID.randomUUID() + "@example.com"; + String name = "테커집짱-" + UUID.randomUUID().toString().substring(0, 8); + User user = UserTestHelper.createUser(name, email, "Techeerzip1!", role); + userRepository.save(user); + + var result = userRepository.findByEmail(email); + + assertThat(result).isPresent(); + assertThat(result.get().getEmail()).isEqualTo(email); + assertThat(result.get().getName()).isEqualTo(name); + } + + @Test + @DisplayName("존재하지 않는 이메일 조회 시 빈 Optional 반환") + void findByEmail_NonExistentEmail_ReturnsEmpty() { + var result = userRepository.findByEmail("nonexistent@example.com"); + + assertThat(result).isEmpty(); + } + + @Test + @DisplayName("삭제된 사용자는 조회되지 않음") + void findByEmail_DeletedUser_ReturnsEmpty() { + String roleName = + RoleTestHelper.Default.USER_TEST + + "-" + + UUID.randomUUID().toString().substring(0, 8); + Role role = roleRepository.save(RoleTestHelper.createRole(roleName)); + + String email = "deleted-" + UUID.randomUUID() + "@example.com"; + String name = "삭제된사용자-" + UUID.randomUUID().toString().substring(0, 8); + User user = UserTestHelper.createDeletedUser(name, email, "password123", role); + userRepository.save(user); + + var result = userRepository.findByEmail(email); + + assertThat(result).isEmpty(); + } +} diff --git a/techeerzip/src/test/java/backend/techeerzip/domain/user/service/UserServiceTest.java b/techeerzip/src/test/java/backend/techeerzip/domain/user/service/UserServiceTest.java new file mode 100644 index 00000000..0c11d69f --- /dev/null +++ b/techeerzip/src/test/java/backend/techeerzip/domain/user/service/UserServiceTest.java @@ -0,0 +1,1538 @@ +package backend.techeerzip.domain.user.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.time.LocalDate; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; + +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.multipart.MultipartFile; + +import backend.techeerzip.domain.auth.exception.AuthNotVerifiedEmailException; +import backend.techeerzip.domain.auth.service.AuthService; +import backend.techeerzip.domain.blog.repository.BlogRepository; +import backend.techeerzip.domain.bookmark.repository.BookmarkRepository; +import backend.techeerzip.domain.bootcamp.entity.BootcampGeneration; +import backend.techeerzip.domain.event.repository.EventRepository; +import backend.techeerzip.domain.like.repository.LikeRepository; +import backend.techeerzip.domain.projectMember.repository.ProjectMemberRepository; +import backend.techeerzip.domain.resume.dto.request.CreateResumeRequest; +import backend.techeerzip.domain.resume.event.ResumeStackExtractionResponse; +import backend.techeerzip.domain.resume.event.ResumeStackResult; +import backend.techeerzip.domain.resume.repository.ResumeRepository; +import backend.techeerzip.domain.resume.service.ResumeService; +import backend.techeerzip.domain.role.entity.Role; +import backend.techeerzip.domain.role.repository.RoleRepository; +import backend.techeerzip.domain.session.repository.SessionRepository; +import backend.techeerzip.domain.stack.entity.Stack; +import backend.techeerzip.domain.stack.service.StackService; +import backend.techeerzip.domain.statistic.service.StatisticDataCollectionService; +import backend.techeerzip.domain.studyMember.repository.StudyMemberRepository; +import backend.techeerzip.domain.task.service.TaskService; +import backend.techeerzip.domain.user.dto.request.CreateExternalUserRequest; +import backend.techeerzip.domain.user.dto.request.CreateUserRequest; +import backend.techeerzip.domain.user.dto.request.CreateUserWithResumeRequest; +import backend.techeerzip.domain.user.dto.request.GetUserProfileListRequest; +import backend.techeerzip.domain.user.dto.request.UpdateUserInfoRequest; +import backend.techeerzip.domain.user.dto.request.UpdateUserTecheerInfoRequest; +import backend.techeerzip.domain.user.dto.request.UpdateUserTecheerInfoWithResumeRequest; +import backend.techeerzip.domain.user.dto.request.UpdateUserWithExperienceRequest; +import backend.techeerzip.domain.user.dto.response.BootcampMemberListResponse; +import backend.techeerzip.domain.user.dto.response.GetPermissionResponse; +import backend.techeerzip.domain.user.dto.response.GetProfileImgResponse; +import backend.techeerzip.domain.user.entity.BootcampPeriod; +import backend.techeerzip.domain.user.entity.JoinReason; +import backend.techeerzip.domain.user.entity.PermissionRequest; +import backend.techeerzip.domain.user.entity.User; +import backend.techeerzip.domain.user.exception.UserAlreadyExistsException; +import backend.techeerzip.domain.user.exception.UserInvalidRoleException; +import backend.techeerzip.domain.user.exception.UserNotFoundException; +import backend.techeerzip.domain.user.exception.UserNotResumeException; +import backend.techeerzip.domain.user.exception.UserProfileImgFailException; +import backend.techeerzip.domain.user.helper.UserTestHelper; +import backend.techeerzip.domain.user.repository.PermissionRequestRepository; +import backend.techeerzip.domain.user.repository.TechStackRepository; +import backend.techeerzip.domain.user.repository.UserRepository; +import backend.techeerzip.domain.userExperience.dto.request.UpdateUserExperienceListRequest; +import backend.techeerzip.domain.userExperience.dto.request.UpdateUserExperienceRequest; +import backend.techeerzip.domain.userExperience.entity.UserExperience; +import backend.techeerzip.domain.userExperience.exception.UserExperienceNotFoundException; +import backend.techeerzip.domain.userExperience.repository.UserExperienceRepository; +import backend.techeerzip.global.entity.StatusCategory; +import backend.techeerzip.global.logger.CustomLogger; +import backend.techeerzip.infra.github.GitHubApiService; +import backend.techeerzip.infra.slack.service.SlackService; + +@ExtendWith(MockitoExtension.class) +@DisplayName("UserService Test") +class UserServiceTest { + + @Mock private UserRepository userRepository; + @Mock private RoleRepository roleRepository; + @Mock private AuthService authService; + @Mock private PasswordEncoder passwordEncoder; + @Mock private ResumeService resumeService; + @Mock private MultipartFile resumeFile; + @Mock private PermissionRequestRepository permissionRequestRepository; + @Mock private UserExperienceRepository userExperienceRepository; + @Mock private ApplicationEventPublisher eventPublisher; + @Mock private SlackService slackService; + @Mock private TaskService taskService; + @Mock private CustomLogger logger; + @Mock private TechStackRepository techStackRepository; + @Mock private StackService stackService; + @Mock private RestTemplate restTemplate; + @Mock private StatisticDataCollectionService statisticDataCollectionService; + @Mock private GitHubApiService gitHubApiService; + @Mock private PlatformTransactionManager transactionManager; + @Mock private BlogRepository blogRepository; + @Mock private BookmarkRepository bookmarkRepository; + @Mock private EventRepository eventRepository; + @Mock private LikeRepository likeRepository; + @Mock private ProjectMemberRepository projectMemberRepository; + @Mock private ResumeRepository resumeRepository; + @Mock private SessionRepository sessionRepository; + @Mock private StudyMemberRepository studyMemberRepository; + + @InjectMocks private UserService userService; + + private CreateUserRequest createUserRequest; + private Role defaultRole; + private User existingUser; + + @BeforeEach + void setUp() { + createUserRequest = + new CreateUserRequest( + "BACKEND", + null, + "테커집짱", + "https://github.com/test", + null, + false, + "테커집대학교", + "1학년", + null, + null, + "Techeerzip1!", + "test@example.com", + 1); + + defaultRole = new Role("USER"); + existingUser = + UserTestHelper.createUser( + "테커집짱", "test@example.com", "Techeerzip1!", defaultRole, true); + existingUser.setDeleted(false); + } + + @Nested + @DisplayName("회원 가입") + class SignUpTest { + + @Test + @DisplayName("신규 회원 가입 성공") + void signUp_NewUser_Success() { + CreateResumeRequest resumeRequest = + new CreateResumeRequest("PORTFOLIO", "BACKEND", "테스트", true); + CreateUserWithResumeRequest request = + new CreateUserWithResumeRequest(createUserRequest, null, resumeRequest); + + User savedUser = + User.builder() + .email(createUserRequest.getEmail()) + .name(createUserRequest.getName()) + .password("hashedPassword") + .role(defaultRole) + .isAuth(true) + .build(); + ReflectionTestUtils.setField(savedUser, "id", 1L); + + doNothing().when(authService).checkTecheer(anyString()); + when(authService.checkEmailVerified(anyString())).thenReturn(true); + when(userRepository.findByEmail(anyString())).thenReturn(Optional.empty()); + when(roleRepository.findById(3L)).thenReturn(Optional.of(defaultRole)); + when(passwordEncoder.encode(anyString())).thenReturn("hashedPassword"); + when(resumeFile.isEmpty()).thenReturn(false); + when(slackService.getProfileMetadata(anyString())) + .thenReturn(Optional.of(Map.of("image", "https://example.com/profile.jpg"))); + lenient().doNothing().when(eventPublisher).publishEvent(any()); + doNothing() + .when(resumeService) + .createResumeByUser( + any(User.class), + any(), + anyString(), + anyString(), + anyString(), + anyBoolean()); + when(userRepository.save(any(User.class))).thenReturn(savedUser); + doNothing().when(userRepository).flush(); + when(userRepository.findById(savedUser.getId())).thenReturn(Optional.of(savedUser)); + + userService.signUp(request, resumeFile); + + verify(authService).checkTecheer(createUserRequest.getEmail()); + verify(authService).checkEmailVerified(createUserRequest.getEmail()); + verify(userRepository).findByEmail(createUserRequest.getEmail()); + verify(userRepository).save(any(User.class)); + verify(userRepository).flush(); + verify(userRepository).findById(savedUser.getId()); + verify(resumeService) + .createResumeByUser( + any(User.class), + any(), + anyString(), + anyString(), + anyString(), + anyBoolean()); + } + + @Test + @DisplayName("이메일 미인증 시 예외 발생") + void signUp_UnverifiedEmail_ThrowsException() { + CreateResumeRequest resumeRequest = + new CreateResumeRequest("PORTFOLIO", "BACKEND", "테스트", true); + CreateUserWithResumeRequest request = + new CreateUserWithResumeRequest(createUserRequest, null, resumeRequest); + + doNothing().when(authService).checkTecheer(anyString()); + when(authService.checkEmailVerified(anyString())).thenReturn(false); + + assertThatThrownBy(() -> userService.signUp(request, resumeFile)) + .isInstanceOf(AuthNotVerifiedEmailException.class); + + verify(userRepository, never()).save(any()); + verify(slackService, never()).getProfileMetadata(anyString()); + } + + @Test + @DisplayName("이력서 파일 없을 시 예외 발생") + void signUp_NoResumeFile_ThrowsException() { + CreateResumeRequest resumeRequest = + new CreateResumeRequest("PORTFOLIO", "BACKEND", "테스트", true); + CreateUserWithResumeRequest request = + new CreateUserWithResumeRequest(createUserRequest, null, resumeRequest); + + doNothing().when(authService).checkTecheer(anyString()); + when(authService.checkEmailVerified(anyString())).thenReturn(true); + + assertThatThrownBy(() -> userService.signUp(request, null)) + .isInstanceOf(UserNotResumeException.class); + + verify(userRepository, never()).save(any()); + verify(slackService, never()).getProfileMetadata(anyString()); + } + + @Test + @DisplayName("이미 존재하는 회원일 시 예외 발생") + void signUp_ExistingActiveUser_ThrowsException() { + CreateResumeRequest resumeRequest = + new CreateResumeRequest("PORTFOLIO", "BACKEND", "테스트", true); + CreateUserWithResumeRequest request = + new CreateUserWithResumeRequest(createUserRequest, null, resumeRequest); + + doNothing().when(authService).checkTecheer(anyString()); + when(authService.checkEmailVerified(anyString())).thenReturn(true); + when(resumeFile.isEmpty()).thenReturn(false); + when(slackService.getProfileMetadata(anyString())) + .thenReturn(Optional.of(Map.of("image", "https://example.com/profile.jpg"))); + when(roleRepository.findById(3L)).thenReturn(Optional.of(defaultRole)); + when(passwordEncoder.encode(anyString())).thenReturn("hashedPassword"); + when(userRepository.findByEmail(anyString())).thenReturn(Optional.of(existingUser)); + + assertThatThrownBy(() -> userService.signUp(request, resumeFile)) + .isInstanceOf(UserAlreadyExistsException.class); + + verify(userRepository, never()).save(any()); + } + + @Test + @DisplayName("삭제된 회원 재가입 성공") + void signUp_DeletedUser_RejoinSuccess() { + CreateResumeRequest resumeRequest = + new CreateResumeRequest("PORTFOLIO", "BACKEND", "테스트", true); + CreateUserWithResumeRequest request = + new CreateUserWithResumeRequest(createUserRequest, null, resumeRequest); + + User deletedUser = + User.builder() + .email("test@example.com") + .name("테커집짱") + .password("Techeerzip1!") + .role(defaultRole) + .isAuth(true) + .build(); + deletedUser.setDeleted(true); + + doNothing().when(authService).checkTecheer(anyString()); + when(authService.checkEmailVerified(anyString())).thenReturn(true); + when(userRepository.findByEmail(anyString())).thenReturn(Optional.of(deletedUser)); + when(roleRepository.findById(3L)).thenReturn(Optional.of(defaultRole)); + when(passwordEncoder.encode(anyString())).thenReturn("hashedPassword"); + when(resumeFile.isEmpty()).thenReturn(false); + when(slackService.getProfileMetadata(anyString())) + .thenReturn(Optional.of(Map.of("image", "https://example.com/profile.jpg"))); + lenient().doNothing().when(eventPublisher).publishEvent(any()); + doNothing() + .when(resumeService) + .createResumeByUser( + any(User.class), + any(), + anyString(), + anyString(), + anyString(), + anyBoolean()); + when(userRepository.save(any(User.class))).thenReturn(deletedUser); + doNothing().when(userRepository).flush(); + ReflectionTestUtils.setField(deletedUser, "id", 1L); + when(userRepository.findById(1L)).thenReturn(Optional.of(deletedUser)); + + userService.signUp(request, resumeFile); + + verify(userRepository).save(any(User.class)); + verify(userRepository).flush(); + verify(userRepository).findById(1L); + assertThat(deletedUser.isDeleted()).isFalse(); + } + } + + @Nested + @DisplayName("사용자 조회") + class GetUserInfoTest { + + @Test + @DisplayName("존재하는 사용자 조회 성공") + void getUserInfo_ExistingUser_Success() { + Long userId = 1L; + User user = + User.builder() + .email("test@example.com") + .name("테커집짱") + .password("Techeerzip1!") + .role(defaultRole) + .isAuth(true) + .build(); + ReflectionTestUtils.setField(user, "id", userId); + + when(userRepository.findByIdAndIsDeletedFalse(userId)).thenReturn(Optional.of(user)); + + var result = userService.getUserInfo(userId); + + assertThat(result).isNotNull(); + verify(userRepository).findByIdAndIsDeletedFalse(userId); + } + + @Test + @DisplayName("존재하지 않는 사용자 조회 시 예외 발생") + void getUserInfo_NotFoundUser_ThrowsException() { + Long userId = 999L; + when(userRepository.findByIdAndIsDeletedFalse(userId)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> userService.getUserInfo(userId)) + .isInstanceOf(UserNotFoundException.class); + } + } + + @Nested + @DisplayName("비밀번호 재설정") + class ResetPasswordTest { + + @Test + @DisplayName("비밀번호 재설정 성공") + void resetPassword_Success() { + String email = "test@example.com"; + String code = "123456"; + String newPassword = "newTecheerzip1!"; + + User user = + User.builder() + .email(email) + .name("테커집짱") + .password("Techeerzip1!") + .role(defaultRole) + .isAuth(true) + .build(); + + doNothing().when(authService).verifyCode(email, code); + when(userRepository.findByEmail(email)).thenReturn(Optional.of(user)); + when(passwordEncoder.encode(newPassword)).thenReturn("hashedNewPassword"); + + userService.resetPassword(email, code, newPassword); + + verify(authService).verifyCode(email, code); + verify(userRepository).findByEmail(email); + verify(passwordEncoder).encode(newPassword); + assertThat(user.getPassword()).isEqualTo("hashedNewPassword"); + } + + @Test + @DisplayName("존재하지 않는 사용자 비밀번호 재설정 시 예외 발생") + void resetPassword_UserNotFound_ThrowsException() { + String email = "nonexistent@example.com"; + String code = "123456"; + String newPassword = "newTecheerzip1!"; + + doNothing().when(authService).verifyCode(email, code); + when(userRepository.findByEmail(email)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> userService.resetPassword(email, code, newPassword)) + .isInstanceOf(UserNotFoundException.class); + } + } + + @Nested + @DisplayName("닉네임 업데이트") + class UpdateNicknameTest { + + @Test + @DisplayName("닉네임 업데이트 성공") + void updateNickname_Success() { + Long userId = 1L; + String nickname = "새로운 닉네임"; + Role userRole = new Role("USER"); + ReflectionTestUtils.setField(userRole, "id", 1L); + + User user = + User.builder() + .email("test@example.com") + .name("테커집짱") + .password("Techeerzip1!") + .role(userRole) + .isAuth(true) + .build(); + ReflectionTestUtils.setField(user, "id", userId); + + when(userRepository.findById(userId)).thenReturn(Optional.of(user)); + when(userRepository.save(any(User.class))).thenReturn(user); + + userService.updateNickname(userId, nickname); + + verify(userRepository).findById(userId); + verify(userRepository).save(user); + assertThat(user.getNickname()).isEqualTo(nickname); + } + + @Test + @DisplayName("ADMIN 권한일 시 닉네임 업데이트 성공") + void updateNickname_AdminRole_Success() { + Long userId = 1L; + String nickname = "새로운 닉네임"; + Role adminRole = new Role("ADMIN"); + ReflectionTestUtils.setField(adminRole, "id", 1L); + + User user = + UserTestHelper.createUser( + "테커집짱", "test@example.com", "Techeerzip1!", adminRole, true); + ReflectionTestUtils.setField(user, "id", userId); + + when(userRepository.findById(userId)).thenReturn(Optional.of(user)); + when(userRepository.save(any(User.class))).thenReturn(user); + + userService.updateNickname(userId, nickname); + + verify(userRepository).findById(userId); + verify(userRepository).save(user); + assertThat(user.getNickname()).isEqualTo(nickname); + } + } + + @Nested + @DisplayName("경력 삭제") + class DeleteExperienceTest { + + @Test + @DisplayName("경력 삭제 성공") + void deleteExperience_Success() { + Long experienceId = 1L; + UserExperience experience = + UserExperience.builder() + .userId(1L) + .position("BACKEND") + .companyName("테커집") + .build(); + ReflectionTestUtils.setField(experience, "id", experienceId); + + when(userExperienceRepository.findById(experienceId)) + .thenReturn(Optional.of(experience)); + when(userExperienceRepository.save(experience)).thenReturn(experience); + + userService.deleteExperience(experienceId); + + verify(userExperienceRepository).findById(experienceId); + verify(userExperienceRepository).save(experience); + assertThat(experience.isDeleted()).isTrue(); + } + + @Test + @DisplayName("존재하지 않는 경력 삭제 시 예외 발생") + void deleteExperience_NotFound_ThrowsException() { + Long experienceId = 999L; + when(userExperienceRepository.findById(experienceId)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> userService.deleteExperience(experienceId)) + .isInstanceOf(UserExperienceNotFoundException.class); + + verify(userExperienceRepository, never()).save(any()); + } + } + + @Nested + @DisplayName("권한 승인") + class ApproveUserPermissionTest { + + @Test + @DisplayName("권한 승인 성공") + void approveUserPermission_Success() { + Long userId = 1L; + Long newRoleId = 2L; + Role newRole = new Role("NEW_ROLE"); + ReflectionTestUtils.setField(newRole, "id", newRoleId); + + User user = + User.builder() + .email("test@example.com") + .name("테커집짱") + .password("Techeerzip1!") + .role(defaultRole) + .isAuth(true) + .build(); + ReflectionTestUtils.setField(user, "id", userId); + + when(userRepository.findById(userId)).thenReturn(Optional.of(user)); + when(roleRepository.findById(newRoleId)).thenReturn(Optional.of(newRole)); + when(userRepository.save(any(User.class))).thenReturn(user); + when(permissionRequestRepository.updateStatusByUserId(userId, StatusCategory.APPROVED)) + .thenReturn(1); + + userService.approveUserPermission(userId, newRoleId); + + verify(userRepository).findById(userId); + verify(roleRepository).findById(newRoleId); + verify(userRepository).save(user); + verify(permissionRequestRepository) + .updateStatusByUserId(userId, StatusCategory.APPROVED); + assertThat(user.getRole()).isEqualTo(newRole); + } + + @Test + @DisplayName("존재하지 않는 사용자 권한 승인 시 예외 발생") + void approveUserPermission_UserNotFound_ThrowsException() { + Long userId = 999L; + Long newRoleId = 2L; + + when(userRepository.findById(userId)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> userService.approveUserPermission(userId, newRoleId)) + .isInstanceOf(UserNotFoundException.class); + + verify(roleRepository, never()).findById(any()); + verify(permissionRequestRepository, never()).updateStatusByUserId(any(), any()); + } + + @Test + @DisplayName("존재하지 않는 역할로 권한 승인 시 예외 발생") + void approveUserPermission_RoleNotFound_ThrowsException() { + Long userId = 1L; + Long newRoleId = 999L; + + User user = + User.builder() + .email("test@example.com") + .name("테커집짱") + .password("Techeerzip1!") + .role(defaultRole) + .isAuth(true) + .build(); + ReflectionTestUtils.setField(user, "id", userId); + + when(userRepository.findById(userId)).thenReturn(Optional.of(user)); + when(roleRepository.findById(newRoleId)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> userService.approveUserPermission(userId, newRoleId)) + .isInstanceOf( + backend.techeerzip.domain.role.exception.RoleNotFoundException.class); + + verify(userRepository, never()).save(any()); + } + } + + @Nested + @DisplayName("외부 회원 가입") + class SignUpExternalTest { + + @Test + @DisplayName("외부 회원 가입 성공 - COMPANY") + void signUpExternal_Company_Success() { + CreateExternalUserRequest request = + new CreateExternalUserRequest( + "외부회원", "external@example.com", "Password123!", JoinReason.COMPANY); + + Role companyRole = new Role("COMPANY"); + ReflectionTestUtils.setField(companyRole, "id", 4L); + + when(authService.checkEmailVerified(anyString())).thenReturn(true); + when(userRepository.findByEmail(anyString())).thenReturn(Optional.empty()); + when(roleRepository.findById(4L)).thenReturn(Optional.of(companyRole)); + when(passwordEncoder.encode(anyString())).thenReturn("hashedPassword"); + when(userRepository.save(any(User.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + + userService.signUpExternal(request); + + verify(authService).checkEmailVerified(request.getEmail()); + verify(userRepository).findByEmail(request.getEmail()); + verify(roleRepository).findById(4L); + verify(userRepository).save(any(User.class)); + } + + @Test + @DisplayName("외부 회원 가입 성공 - BOOTCAMP (부트캠프 기간인 경우)") + void signUpExternal_Bootcamp_Success() { + CreateExternalUserRequest request = + new CreateExternalUserRequest( + "부트캠프회원", "bootcamp@example.com", "Password123!", JoinReason.BOOTCAMP); + + Role bootcampRole = new Role("BOOTCAMP"); + ReflectionTestUtils.setField(bootcampRole, "id", 5L); + + when(authService.checkEmailVerified(anyString())).thenReturn(true); + when(userRepository.findByEmail(anyString())).thenReturn(Optional.empty()); + when(roleRepository.findById(5L)).thenReturn(Optional.of(bootcampRole)); + when(passwordEncoder.encode(anyString())).thenReturn("hashedPassword"); + + try (MockedStatic mocked = mockStatic(BootcampPeriod.class)) { + mocked.when(() -> BootcampPeriod.calculateGeneration(any(LocalDate.class))) + .thenReturn(1); + + when(userRepository.save(any(User.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + + userService.signUpExternal(request); + + verify(authService).checkEmailVerified(request.getEmail()); + verify(userRepository).findByEmail(request.getEmail()); + verify(roleRepository).findById(5L); + verify(userRepository).save(any(User.class)); + } + } + + @Test + @DisplayName("부트캠프 기간이 아닐 시 예외 발생") + void signUpExternal_Bootcamp_NotPeriod_ThrowsException() { + CreateExternalUserRequest request = + new CreateExternalUserRequest( + "부트캠프회원", "bootcamp@example.com", "Password123!", JoinReason.BOOTCAMP); + + Role bootcampRole = new Role("BOOTCAMP"); + ReflectionTestUtils.setField(bootcampRole, "id", 5L); + + when(authService.checkEmailVerified(anyString())).thenReturn(true); + when(userRepository.findByEmail(anyString())).thenReturn(Optional.empty()); + when(roleRepository.findById(5L)).thenReturn(Optional.of(bootcampRole)); + when(passwordEncoder.encode(anyString())).thenReturn("hashedPassword"); + + try (MockedStatic mocked = mockStatic(BootcampPeriod.class)) { + mocked.when(() -> BootcampPeriod.calculateGeneration(any(LocalDate.class))) + .thenReturn(null); + + assertThatThrownBy(() -> userService.signUpExternal(request)) + .isInstanceOf( + backend.techeerzip.domain.user.exception + .UserNotBootcampPeriodException.class); + } + } + + @Test + @DisplayName("이메일 미인증 시 예외 발생") + void signUpExternal_UnverifiedEmail_ThrowsException() { + CreateExternalUserRequest request = + new CreateExternalUserRequest( + "외부회원", "external@example.com", "Password123!", JoinReason.COMPANY); + + when(authService.checkEmailVerified(anyString())).thenReturn(false); + + assertThatThrownBy(() -> userService.signUpExternal(request)) + .isInstanceOf(AuthNotVerifiedEmailException.class); + + verify(userRepository, never()).save(any()); + } + + @Test + @DisplayName("이미 존재하는 회원일 시 예외 발생") + void signUpExternal_ExistingUser_ThrowsException() { + CreateExternalUserRequest request = + new CreateExternalUserRequest( + "외부회원", "external@example.com", "Password123!", JoinReason.COMPANY); + + when(authService.checkEmailVerified(anyString())).thenReturn(true); + when(userRepository.findByEmail(anyString())).thenReturn(Optional.of(existingUser)); + + assertThatThrownBy(() -> userService.signUpExternal(request)) + .isInstanceOf(UserAlreadyExistsException.class); + + verify(userRepository, never()).save(any()); + } + } + + @Nested + @DisplayName("테커 전환") + class ChangeTecheerTest { + + @Test + @DisplayName("테커 전환 성공") + void changeTecheer_Success() { + Long userId = 1L; + Role bootcampRole = new Role("BOOTCAMP"); + ReflectionTestUtils.setField(bootcampRole, "id", 5L); + Role techeerRole = new Role("TECHEER"); + ReflectionTestUtils.setField(techeerRole, "id", 3L); + + User bootcampUser = + User.builder() + .email("bootcamp@example.com") + .name("부트캠프회원") + .password("Password123!") + .role(bootcampRole) + .isAuth(true) + .year(1) + .build(); + ReflectionTestUtils.setField(bootcampUser, "id", userId); + + UpdateUserTecheerInfoRequest techeerInfoRequest = new UpdateUserTecheerInfoRequest(); + ReflectionTestUtils.setField(techeerInfoRequest, "email", "techeer@example.com"); + ReflectionTestUtils.setField(techeerInfoRequest, "mainPosition", "BACKEND"); + ReflectionTestUtils.setField(techeerInfoRequest, "subPosition", "FRONTEND"); + ReflectionTestUtils.setField( + techeerInfoRequest, "githubUrl", "https://github.com/test"); + ReflectionTestUtils.setField(techeerInfoRequest, "school", "테커집대학교"); + ReflectionTestUtils.setField(techeerInfoRequest, "grade", "1학년"); + ReflectionTestUtils.setField(techeerInfoRequest, "isLft", false); + ReflectionTestUtils.setField(techeerInfoRequest, "year", 1); + + CreateResumeRequest resumeRequest = + new CreateResumeRequest("PORTFOLIO", "BACKEND", "테스트", true); + UpdateUserTecheerInfoWithResumeRequest request = + new UpdateUserTecheerInfoWithResumeRequest(); + ReflectionTestUtils.setField( + request, "updateUserTecheerInfoRequest", techeerInfoRequest); + ReflectionTestUtils.setField(request, "createResumeRequest", resumeRequest); + ReflectionTestUtils.setField(request, "createUserExperienceRequest", null); + + doNothing().when(authService).checkTecheer(anyString()); + when(userRepository.findById(userId)).thenReturn(Optional.of(bootcampUser)); + when(roleRepository.findById(3L)).thenReturn(Optional.of(techeerRole)); + when(slackService.getProfileMetadata(anyString())) + .thenReturn(Optional.of(Map.of("image", "https://example.com/profile.jpg"))); + when(resumeFile.isEmpty()).thenReturn(false); + doNothing() + .when(resumeService) + .createResumeByUser( + any(User.class), + any(), + anyString(), + anyString(), + anyString(), + anyBoolean()); + + userService.changeTecheer(userId, request, resumeFile); + + verify(authService).checkTecheer(anyString()); + verify(userRepository).findById(userId); + verify(roleRepository).findById(3L); + verify(resumeService) + .createResumeByUser( + any(User.class), + any(), + anyString(), + anyString(), + anyString(), + anyBoolean()); + } + + @Test + @DisplayName("잘못된 역할일 시 예외 발생") + void changeTecheer_InvalidRole_ThrowsException() { + Long userId = 1L; + Role userRole = new Role("USER"); + ReflectionTestUtils.setField(userRole, "id", 1L); + + User user = + User.builder() + .email("user@example.com") + .name("일반회원") + .password("Password123!") + .role(userRole) + .isAuth(true) + .build(); + ReflectionTestUtils.setField(user, "id", userId); + + UpdateUserTecheerInfoRequest techeerInfoRequest = new UpdateUserTecheerInfoRequest(); + CreateResumeRequest resumeRequest = + new CreateResumeRequest("PORTFOLIO", "BACKEND", "테스트", true); + UpdateUserTecheerInfoWithResumeRequest request = + new UpdateUserTecheerInfoWithResumeRequest(); + ReflectionTestUtils.setField( + request, "updateUserTecheerInfoRequest", techeerInfoRequest); + ReflectionTestUtils.setField(request, "createResumeRequest", resumeRequest); + + doNothing().when(authService).checkTecheer(anyString()); + when(userRepository.findById(userId)).thenReturn(Optional.of(user)); + + assertThatThrownBy(() -> userService.changeTecheer(userId, request, resumeFile)) + .isInstanceOf(UserInvalidRoleException.class); + + verify(userRepository, never()).save(any()); + } + + @Test + @DisplayName("이력서 파일 없을 시 예외 발생") + void changeTecheer_NoResumeFile_ThrowsException() { + Long userId = 1L; + Role bootcampRole = new Role("BOOTCAMP"); + ReflectionTestUtils.setField(bootcampRole, "id", 5L); + + User bootcampUser = + User.builder() + .email("bootcamp@example.com") + .name("부트캠프회원") + .password("Password123!") + .role(bootcampRole) + .isAuth(true) + .build(); + ReflectionTestUtils.setField(bootcampUser, "id", userId); + + UpdateUserTecheerInfoRequest techeerInfoRequest = new UpdateUserTecheerInfoRequest(); + CreateResumeRequest resumeRequest = + new CreateResumeRequest("PORTFOLIO", "BACKEND", "테스트", true); + UpdateUserTecheerInfoWithResumeRequest request = + new UpdateUserTecheerInfoWithResumeRequest(); + ReflectionTestUtils.setField( + request, "updateUserTecheerInfoRequest", techeerInfoRequest); + ReflectionTestUtils.setField(request, "createResumeRequest", resumeRequest); + + doNothing().when(authService).checkTecheer(anyString()); + when(userRepository.findById(userId)).thenReturn(Optional.of(bootcampUser)); + + assertThatThrownBy(() -> userService.changeTecheer(userId, request, null)) + .isInstanceOf(UserNotResumeException.class); + + verify(userRepository, never()).save(any()); + } + } + + @Nested + @DisplayName("사용자 삭제") + class DeleteUserTest { + + @Test + @DisplayName("사용자 삭제 성공") + void deleteUser_Success() { + Long userId = 1L; + User user = + User.builder() + .email("test@example.com") + .name("테커집짱") + .password("Password123!") + .role(defaultRole) + .isAuth(true) + .build(); + ReflectionTestUtils.setField(user, "id", userId); + + HttpServletResponse response = org.mockito.Mockito.mock(HttpServletResponse.class); + + when(userRepository.findById(userId)).thenReturn(Optional.of(user)); + when(userRepository.save(any(User.class))).thenReturn(user); + doNothing().when(blogRepository).updateIsDeletedByUserId(userId); + doNothing().when(bookmarkRepository).updateIsDeletedByUserId(userId); + doNothing().when(eventRepository).updateIsDeletedByUserId(userId); + doNothing().when(likeRepository).updateIsDeletedByUserId(userId); + doNothing().when(projectMemberRepository).updateIsDeletedByUserId(userId); + doNothing().when(resumeRepository).updateIsDeletedByUserId(userId); + doNothing().when(sessionRepository).updateIsDeletedByUserId(userId); + doNothing().when(studyMemberRepository).updateIsDeletedByUserId(userId); + doNothing().when(userExperienceRepository).updateIsDeletedByUserId(userId); + lenient().doNothing().when(eventPublisher).publishEvent(any()); + + userService.deleteUser(userId, response); + + verify(userRepository).findById(userId); + verify(userRepository).save(any(User.class)); + verify(blogRepository).updateIsDeletedByUserId(userId); + } + + @Test + @DisplayName("존재하지 않는 사용자 삭제 시 예외 발생") + void deleteUser_NotFound_ThrowsException() { + Long userId = 999L; + HttpServletResponse response = org.mockito.Mockito.mock(HttpServletResponse.class); + + when(userRepository.findById(userId)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> userService.deleteUser(userId, response)) + .isInstanceOf(UserNotFoundException.class); + + verify(userRepository, never()).save(any()); + } + } + + @Nested + @DisplayName("모든 사용자 조회") + class GetAllUsersTest { + + @Test + @DisplayName("모든 사용자 조회 성공") + void getAllUsers_Success() { + List users = Arrays.asList(existingUser); + when(userRepository.findAllByRoleInTecheer()).thenReturn(users); + + List result = userService.getAllUsers(); + + assertThat(result).isNotNull(); + assertThat(result).hasSize(1); + verify(userRepository).findAllByRoleInTecheer(); + } + } + + @Nested + @DisplayName("프로필 이미지 업데이트") + class UpdateProfileImgTest { + + @Test + @DisplayName("프로필 이미지 업데이트 성공") + void updateProfileImg_Success() { + String email = "test@example.com"; + User user = + User.builder() + .email(email) + .name("테커집짱") + .password("Password123!") + .role(defaultRole) + .isAuth(true) + .build(); + + when(userRepository.findByEmail(email)).thenReturn(Optional.of(user)); + when(slackService.getProfileMetadata(email)) + .thenReturn( + Optional.of(Map.of("image", "https://example.com/new-profile.jpg"))); + when(userRepository.save(any(User.class))).thenReturn(user); + + GetProfileImgResponse result = userService.updateProfileImg(email); + + assertThat(result).isNotNull(); + assertThat(result.getProfileImage()).isEqualTo("https://example.com/new-profile.jpg"); + verify(userRepository).findByEmail(email); + verify(userRepository).save(any(User.class)); + } + + @Test + @DisplayName("프로필 이미지 가져오기 실패 시 예외 발생") + void updateProfileImg_FetchFail_ThrowsException() { + String email = "test@example.com"; + User user = + User.builder() + .email(email) + .name("테커집짱") + .password("Password123!") + .role(defaultRole) + .isAuth(true) + .build(); + + when(userRepository.findByEmail(email)).thenReturn(Optional.of(user)); + when(slackService.getProfileMetadata(email)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> userService.updateProfileImg(email)) + .isInstanceOf(UserProfileImgFailException.class); + + verify(userRepository, never()).save(any()); + } + } + + @Nested + @DisplayName("프로필 이미지 동기화") + class SyncProfileImgTest { + + @Test + @DisplayName("프로필 이미지 동기화 성공") + void syncProfileImg_Success() { + Long userId = 1L; + String imageUrl = "https://example.com/synced-image.jpg"; + User user = + User.builder() + .email("test@example.com") + .name("테커집짱") + .password("Password123!") + .role(defaultRole) + .isAuth(true) + .build(); + ReflectionTestUtils.setField(user, "id", userId); + + when(userRepository.findById(userId)).thenReturn(Optional.of(user)); + + userService.syncProfileImg(userId, imageUrl); + + assertThat(user.getProfileImage()).isEqualTo(imageUrl); + verify(userRepository).findById(userId); + } + + @Test + @DisplayName("존재하지 않는 사용자 프로필 이미지 동기화 시 예외 발생") + void syncProfileImg_NotFound_ThrowsException() { + Long userId = 999L; + String imageUrl = "https://example.com/synced-image.jpg"; + + when(userRepository.findById(userId)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> userService.syncProfileImg(userId, imageUrl)) + .isInstanceOf(UserNotFoundException.class); + } + } + + @Nested + @DisplayName("권한 요청 생성") + class CreateUserPermissionRequestTest { + + @Test + @DisplayName("권한 요청 생성 성공") + void createUserPermissionRequest_Success() { + Long userId = 1L; + Long roleId = 2L; + User user = + User.builder() + .email("test@example.com") + .name("테커집짱") + .password("Password123!") + .role(defaultRole) + .isAuth(true) + .build(); + ReflectionTestUtils.setField(user, "id", userId); + + PermissionRequest permissionRequest = + PermissionRequest.builder().user(user).requestedRoleId(roleId).build(); + ReflectionTestUtils.setField(permissionRequest, "id", 1L); + + when(userRepository.findById(userId)).thenReturn(Optional.of(user)); + when(permissionRequestRepository.save(any(PermissionRequest.class))) + .thenReturn(permissionRequest); + + PermissionRequest result = userService.createUserPermissionRequest(userId, roleId); + + assertThat(result).isNotNull(); + assertThat(result.getRequestedRoleId()).isEqualTo(roleId); + verify(userRepository).findById(userId); + verify(permissionRequestRepository).save(any(PermissionRequest.class)); + } + + @Test + @DisplayName("존재하지 않는 사용자 권한 요청 생성 시 예외 발생") + void createUserPermissionRequest_NotFound_ThrowsException() { + Long userId = 999L; + Long roleId = 2L; + + when(userRepository.findById(userId)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> userService.createUserPermissionRequest(userId, roleId)) + .isInstanceOf(UserNotFoundException.class); + + verify(permissionRequestRepository, never()).save(any()); + } + } + + @Nested + @DisplayName("대기 중인 권한 요청 조회") + class GetAllPendingPermissionRequestsTest { + + @Test + @DisplayName("대기 중인 권한 요청 조회 성공") + void getAllPendingPermissionRequests_Success() { + User user = + User.builder() + .email("test@example.com") + .name("테커집짱") + .password("Password123!") + .role(defaultRole) + .isAuth(true) + .build(); + ReflectionTestUtils.setField(user, "id", 1L); + + PermissionRequest permissionRequest = + PermissionRequest.builder().user(user).requestedRoleId(2L).build(); + ReflectionTestUtils.setField(permissionRequest, "id", 1L); + + when(permissionRequestRepository.findByStatus(StatusCategory.PENDING)) + .thenReturn(Collections.singletonList(permissionRequest)); + + List result = userService.getAllPendingPermissionRequests(); + + assertThat(result).isNotNull(); + assertThat(result).hasSize(1); + verify(permissionRequestRepository).findByStatus(StatusCategory.PENDING); + } + } + + @Nested + @DisplayName("프로필 업데이트") + class UpdateProfileTest { + + @Test + @DisplayName("프로필 업데이트 성공") + void updateProfile_Success() { + Long userId = 1L; + User user = + User.builder() + .email("test@example.com") + .name("테커집짱") + .password("Password123!") + .role(defaultRole) + .isAuth(true) + .year(1) + .build(); + ReflectionTestUtils.setField(user, "id", userId); + + UpdateUserInfoRequest updateUserInfoRequest = new UpdateUserInfoRequest(); + ReflectionTestUtils.setField(updateUserInfoRequest, "mainPosition", "FRONTEND"); + ReflectionTestUtils.setField( + updateUserInfoRequest, "githubUrl", "https://github.com/new"); + + UpdateUserWithExperienceRequest request = new UpdateUserWithExperienceRequest(); + ReflectionTestUtils.setField(request, "updateUserInfoRequest", updateUserInfoRequest); + ReflectionTestUtils.setField(request, "updateUserExperienceRequest", null); + + when(userRepository.findById(userId)).thenReturn(Optional.of(user)); + lenient().doNothing().when(eventPublisher).publishEvent(any()); + + userService.updateProfile(userId, request); + + assertThat(user.getMainPosition()).isEqualTo("FRONTEND"); + assertThat(user.getGithubUrl()).isEqualTo("https://github.com/new"); + verify(userRepository).findById(userId); + } + + @Test + @DisplayName("경력 추가 성공") + void updateProfile_AddExperience_Success() { + Long userId = 1L; + User user = + User.builder() + .email("test@example.com") + .name("테커집짱") + .password("Password123!") + .role(defaultRole) + .isAuth(true) + .year(1) + .build(); + ReflectionTestUtils.setField(user, "id", userId); + + UpdateUserInfoRequest updateUserInfoRequest = new UpdateUserInfoRequest(); + UpdateUserExperienceRequest experienceRequest = new UpdateUserExperienceRequest(); + ReflectionTestUtils.setField(experienceRequest, "experienceId", null); + ReflectionTestUtils.setField(experienceRequest, "position", "BACKEND"); + ReflectionTestUtils.setField(experienceRequest, "companyName", "테커집"); + ReflectionTestUtils.setField(experienceRequest, "startDate", LocalDate.of(2023, 1, 1)); + ReflectionTestUtils.setField(experienceRequest, "endDate", null); + ReflectionTestUtils.setField(experienceRequest, "category", "인턴"); + ReflectionTestUtils.setField(experienceRequest, "isFinished", false); + ReflectionTestUtils.setField(experienceRequest, "description", "테스트"); + + UpdateUserExperienceListRequest experienceListRequest = + new UpdateUserExperienceListRequest(); + ReflectionTestUtils.setField( + experienceListRequest, + "experiences", + Collections.singletonList(experienceRequest)); + + UpdateUserWithExperienceRequest request = new UpdateUserWithExperienceRequest(); + ReflectionTestUtils.setField(request, "updateUserInfoRequest", updateUserInfoRequest); + ReflectionTestUtils.setField( + request, "updateUserExperienceRequest", experienceListRequest); + + when(userRepository.findById(userId)).thenReturn(Optional.of(user)); + when(userExperienceRepository.save(any(UserExperience.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + lenient().doNothing().when(eventPublisher).publishEvent(any()); + + userService.updateProfile(userId, request); + + verify(userRepository).findById(userId); + verify(userExperienceRepository).save(any(UserExperience.class)); + } + + @Test + @DisplayName("존재하지 않는 사용자 프로필 업데이트 시 예외 발생") + void updateProfile_NotFound_ThrowsException() { + Long userId = 999L; + UpdateUserWithExperienceRequest request = new UpdateUserWithExperienceRequest(); + + when(userRepository.findById(userId)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> userService.updateProfile(userId, request)) + .isInstanceOf(UserNotFoundException.class); + } + } + + @Nested + @DisplayName("프로필 목록 조회") + class GetAllProfilesTest { + + @Test + @DisplayName("프로필 목록 조회 성공") + void getAllProfiles_Success() { + GetUserProfileListRequest request = + GetUserProfileListRequest.builder().limit(10).sortBy("year").build(); + + List users = Arrays.asList(existingUser); + when(userRepository.findUsersWithCursor( + any(), any(), any(), any(), any(), anyInt(), anyString())) + .thenReturn(users); + + var result = userService.getAllProfiles(request); + + assertThat(result).isNotNull(); + verify(userRepository) + .findUsersWithCursor(any(), any(), any(), any(), any(), anyInt(), anyString()); + } + } + + @Nested + @DisplayName("부트캠프 멤버 프로필 조회") + class GetBootcampMemberProfilesTest { + + @Test + @DisplayName("부트캠프 멤버 프로필 조회 성공") + void getBootcampMemberProfiles_Success() { + Long cursorId = null; + Integer limit = 10; + String sortBy = "name"; + Integer bootcampYear = 1; + + List users = Arrays.asList(existingUser); + when(userRepository.findBootcampMembersWithCursor(any(), any(), anyInt(), anyString())) + .thenReturn(users); + + BootcampMemberListResponse result = + userService.getBootcampMemberProfiles(cursorId, limit, sortBy, bootcampYear); + + assertThat(result).isNotNull(); + verify(userRepository) + .findBootcampMembersWithCursor(any(), any(), anyInt(), anyString()); + } + } + + @Nested + @DisplayName("ID와 User 맵 생성") + class GetIdAndUserMapTest { + + @Test + @DisplayName("ID와 User 맵 생성 성공") + void getIdAndUserMap_Success() { + List userIds = Arrays.asList(1L, 2L); + Function idExtractor = id -> id; + + User user1 = + User.builder() + .email("user1@example.com") + .name("사용자1") + .password("Password123!") + .role(defaultRole) + .isAuth(true) + .build(); + ReflectionTestUtils.setField(user1, "id", 1L); + + User user2 = + User.builder() + .email("user2@example.com") + .name("사용자2") + .password("Password123!") + .role(defaultRole) + .isAuth(true) + .build(); + ReflectionTestUtils.setField(user2, "id", 2L); + + when(userRepository.findAllById(userIds)).thenReturn(Arrays.asList(user1, user2)); + + Map result = userService.getIdAndUserMap(userIds, idExtractor); + + assertThat(result).isNotNull(); + assertThat(result).hasSize(2); + assertThat(result.get(1L)).isEqualTo(user1); + assertThat(result.get(2L)).isEqualTo(user2); + verify(userRepository).findAllById(userIds); + } + + @Test + @DisplayName("존재하지 않는 사용자 ID 포함 시 예외 발생") + void getIdAndUserMap_NotFound_ThrowsException() { + List userIds = Arrays.asList(1L, 999L); + Function idExtractor = id -> id; + + User user1 = + User.builder() + .email("user1@example.com") + .name("사용자1") + .password("Password123!") + .role(defaultRole) + .isAuth(true) + .build(); + ReflectionTestUtils.setField(user1, "id", 1L); + + when(userRepository.findAllById(userIds)).thenReturn(Collections.singletonList(user1)); + + assertThatThrownBy(() -> userService.getIdAndUserMap(userIds, idExtractor)) + .isInstanceOf(UserNotFoundException.class); + } + } + + @Nested + @DisplayName("기술 스택 업데이트") + class UpdateTechStackTest { + + @Test + @DisplayName("기술 스택 업데이트 성공") + void updateTechStack_Success() { + Long userId = 1L; + User user = + User.builder() + .email("test@example.com") + .name("테커집짱") + .password("Password123!") + .role(defaultRole) + .isAuth(true) + .build(); + ReflectionTestUtils.setField(user, "id", userId); + + Stack stack1 = + Stack.builder() + .name("Java") + .category(backend.techeerzip.domain.stack.entity.StackCategory.BACKEND) + .build(); + ReflectionTestUtils.setField(stack1, "id", 1L); + + ResumeStackResult stackResult = + new ResumeStackResult( + Arrays.asList("React"), + Arrays.asList("Java", "Spring"), + Arrays.asList("MySQL"), + Arrays.asList("Docker"), + Arrays.asList("Git")); + + ResumeStackExtractionResponse response = + new ResumeStackExtractionResponse(userId, stackResult); + + when(userRepository.findById(userId)).thenReturn(Optional.of(user)); + when(stackService.getStacksByName(any())).thenReturn(Collections.singletonList(stack1)); + when(techStackRepository.saveAll(any())).thenReturn(Collections.emptyList()); + + userService.updateTechStack(response); + + verify(userRepository).findById(userId); + verify(stackService).getStacksByName(any()); + verify(techStackRepository).saveAll(any()); + } + } + + @Nested + @DisplayName("부트캠프 연도 조회") + class GetUserBootcampYearTest { + + @Test + @DisplayName("부트캠프 연도 조회 성공") + void getUserBootcampYear_Success() { + Long userId = 1L; + Integer bootcampYear = 1; + User user = + User.builder() + .email("test@example.com") + .name("테커집짱") + .password("Password123!") + .role(defaultRole) + .isAuth(true) + .bootcampYear(bootcampYear) + .build(); + ReflectionTestUtils.setField(user, "id", userId); + + when(userRepository.findById(userId)).thenReturn(Optional.of(user)); + + Integer result = userService.getUserBootcampYear(userId); + + assertThat(result).isEqualTo(bootcampYear); + verify(userRepository).findById(userId); + } + + @Test + @DisplayName("존재하지 않는 사용자 부트캠프 연도 조회 시 예외 발생") + void getUserBootcampYear_NotFound_ThrowsException() { + Long userId = 999L; + + when(userRepository.findById(userId)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> userService.getUserBootcampYear(userId)) + .isInstanceOf(UserNotFoundException.class); + } + } + + @Nested + @DisplayName("부트캠프 참여 여부 확인") + class IsParticipateTest { + + @Test + @DisplayName("부트캠프 참여 중") + void isParticipate_True() { + Long userId = 1L; + Integer bootcampYear = 3; + User user = + User.builder() + .email("test@example.com") + .name("테커집짱") + .password("Password123!") + .role(defaultRole) + .isAuth(true) + .bootcampYear(bootcampYear) + .build(); + ReflectionTestUtils.setField(user, "id", userId); + + when(userRepository.findById(userId)).thenReturn(Optional.of(user)); + + try (MockedStatic mocked = mockStatic(BootcampGeneration.class)) { + mocked.when( + () -> + BootcampGeneration.calculateBootcampGeneration( + any(LocalDate.class))) + .thenReturn(3); + Integer currentGeneration = 3; + + boolean result = userService.isParticipate(userId, currentGeneration); + + assertThat(result).isTrue(); + verify(userRepository).findById(userId); + } + } + + @Test + @DisplayName("부트캠프 미참여") + void isParticipate_False() { + Long userId = 1L; + User user = + User.builder() + .email("test@example.com") + .name("테커집짱") + .password("Password123!") + .role(defaultRole) + .isAuth(true) + .bootcampYear(null) + .build(); + ReflectionTestUtils.setField(user, "id", userId); + + when(userRepository.findById(userId)).thenReturn(Optional.of(user)); + + boolean result = userService.isParticipate(userId, null); + + assertThat(result).isFalse(); + verify(userRepository).findById(userId); + } + } + + @Nested + @DisplayName("부트캠프 참여 토글") + class ToggleBootcampParticipationTest { + + @Test + @DisplayName("부트캠프 참여 토글 성공 - 참여") + void toggleBootcampParticipation_Participate_Success() { + Long userId = 1L; + User user = + User.builder() + .email("test@example.com") + .name("테커집짱") + .password("Password123!") + .role(defaultRole) + .isAuth(true) + .bootcampYear(null) + .build(); + ReflectionTestUtils.setField(user, "id", userId); + + when(userRepository.findById(userId)).thenReturn(Optional.of(user)); + when(userRepository.save(any(User.class))).thenReturn(user); + + try (MockedStatic mocked = mockStatic(BootcampGeneration.class)) { + mocked.when( + () -> + BootcampGeneration.calculateBootcampGeneration( + any(LocalDate.class))) + .thenReturn(2); + Integer currentGeneration = 2; + + userService.toggleBootcampParticipation(userId, currentGeneration); + + verify(userRepository, atLeastOnce()).findById(userId); + verify(userRepository).save(any(User.class)); + assertThat(user.getBootcampYear()).isEqualTo(2); + } + } + + @Test + @DisplayName("부트캠프 참여 토글 성공 - 참여 취소") + void toggleBootcampParticipation_Cancel_Success() { + Long userId = 1L; + Integer bootcampYear = 2; + User user = + User.builder() + .email("test@example.com") + .name("테커집짱") + .password("Password123!") + .role(defaultRole) + .isAuth(true) + .bootcampYear(bootcampYear) + .build(); + ReflectionTestUtils.setField(user, "id", userId); + + when(userRepository.findById(userId)).thenReturn(Optional.of(user)); + when(userRepository.save(any(User.class))).thenReturn(user); + + try (MockedStatic mocked = mockStatic(BootcampGeneration.class)) { + mocked.when( + () -> + BootcampGeneration.calculateBootcampGeneration( + any(LocalDate.class))) + .thenReturn(2); + Integer currentGeneration = 2; + + userService.toggleBootcampParticipation(userId, currentGeneration); + + verify(userRepository, atLeastOnce()).findById(userId); + verify(userRepository).save(any(User.class)); + assertThat(user.getBootcampYear()).isEqualTo(0); + } + } + } +} diff --git a/techeerzip/src/test/java/backend/techeerzip/domain/userExperience/entity/UserExperienceTest.java b/techeerzip/src/test/java/backend/techeerzip/domain/userExperience/entity/UserExperienceTest.java new file mode 100644 index 00000000..adf02bb4 --- /dev/null +++ b/techeerzip/src/test/java/backend/techeerzip/domain/userExperience/entity/UserExperienceTest.java @@ -0,0 +1,106 @@ +package backend.techeerzip.domain.userExperience.entity; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.LocalDateTime; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import backend.techeerzip.domain.userExperience.helper.UserExperienceTestHelper; + +@DisplayName("UserExperience Test") +class UserExperienceTest { + + private UserExperience userExperience; + private LocalDateTime startDate; + private LocalDateTime endDate; + + @BeforeEach + void setUp() { + startDate = LocalDateTime.of(2025, 11, 21, 0, 0); + endDate = LocalDateTime.of(2025, 11, 23, 0, 0); + + userExperience = + UserExperienceTestHelper.createUserExperience( + 1L, "BACKEND", "테커집", startDate, endDate, "인턴", true, "서버 관리"); + } + + @Nested + @DisplayName("경력 업데이트") + class UpdateTest { + + @Test + @DisplayName("경력 정보 업데이트 성공") + void update_Success() { + String newPosition = "FRONTED"; + String newCompanyName = "뉴테커집"; + LocalDateTime newStartDate = LocalDateTime.of(2025, 11, 13, 0, 0); + LocalDateTime newEndDate = LocalDateTime.of(2025, 11, 21, 0, 0); + String newCategory = "인턴"; + boolean newIsFinished = false; + String newDescription = "UI/UX 개발"; + + userExperience.update( + newPosition, + newCompanyName, + newStartDate, + newEndDate, + newCategory, + newIsFinished, + newDescription); + + assertThat(userExperience.getPosition()).isEqualTo(newPosition); + assertThat(userExperience.getCompanyName()).isEqualTo(newCompanyName); + assertThat(userExperience.getStartDate()).isEqualTo(newStartDate); + assertThat(userExperience.getEndDate()).isEqualTo(newEndDate); + assertThat(userExperience.getCategory()).isEqualTo(newCategory); + assertThat(userExperience.isFinished()).isEqualTo(newIsFinished); + assertThat(userExperience.getDescription()).isEqualTo(newDescription); + assertThat(userExperience.getUpdatedAt()).isNotNull(); + } + } + + @Nested + @DisplayName("경력 삭제 테스트") + class DeleteTest { + + @Test + @DisplayName("경력 삭제 성공") + void delete_Success() { + assertThat(userExperience.isDeleted()).isFalse(); + + userExperience.delete(); + + assertThat(userExperience.isDeleted()).isTrue(); + assertThat(userExperience.getUpdatedAt()).isNotNull(); + } + } + + @Nested + @DisplayName("상태 변경 테스트") + class SetFinishedTest { + + @Test + @DisplayName("완료 상태로 변경 성공") + void setIsFinished_True_Success() { + userExperience.setIsFinished(false); + + userExperience.setIsFinished(true); + + assertThat(userExperience.isFinished()).isTrue(); + } + + @Test + @DisplayName("진행 중 상태로 변경 성공") + void setIsFinished_False_Success() { + userExperience.setIsFinished(true); + + userExperience.setIsFinished(false); + + assertThat(userExperience.isFinished()).isFalse(); + } + } +} diff --git a/techeerzip/src/test/java/backend/techeerzip/domain/userExperience/helper/UserExperienceTestHelper.java b/techeerzip/src/test/java/backend/techeerzip/domain/userExperience/helper/UserExperienceTestHelper.java new file mode 100644 index 00000000..fd79152e --- /dev/null +++ b/techeerzip/src/test/java/backend/techeerzip/domain/userExperience/helper/UserExperienceTestHelper.java @@ -0,0 +1,124 @@ +package backend.techeerzip.domain.userExperience.helper; + +import java.time.LocalDateTime; + +import org.springframework.test.util.ReflectionTestUtils; + +import backend.techeerzip.domain.userExperience.entity.UserExperience; + +public class UserExperienceTestHelper { + + private UserExperienceTestHelper() {} + + // ========================================================================= + // 1. Service Test 전용 메서드 (컴파일 에러 해결용) + // ========================================================================= + + /** + * [Service Test용] ID와 UserId만으로 빠르게 UserExperience를 생성합니다. + * + * @param id 설정할 ID (Reflection 사용) + * @param userId 사용자 ID + * @return ID가 세팅된 UserExperience 객체 + */ + public static UserExperience createUserExperience(Long id, Long userId) { + UserExperience experience = + createUserExperience( + userId, + "BACKEND", + "테커집", + LocalDateTime.of(2025, 1, 1, 0, 0), + LocalDateTime.of(2025, 12, 31, 0, 0), + "인턴", + true, + "테스트용 경력"); + ReflectionTestUtils.setField(experience, "id", id); + return experience; + } + + // ========================================================================= + // 2. Repository Test 및 공통 메서드 + // ========================================================================= + + public static UserExperience createUserExperience( + Long userId, + String position, + String companyName, + LocalDateTime startDate, + LocalDateTime endDate, + String category) { + return createUserExperience( + userId, position, companyName, startDate, endDate, category, true, null); + } + + /** 완료 여부를 지정하여 경력을 생성 */ + public static UserExperience createUserExperience( + Long userId, + String position, + String companyName, + LocalDateTime startDate, + LocalDateTime endDate, + String category, + boolean isFinished) { + return createUserExperience( + userId, position, companyName, startDate, endDate, category, isFinished, null); + } + + /** 모든 필드를 포함한 경력을 생성 */ + public static UserExperience createUserExperience( + Long userId, + String position, + String companyName, + LocalDateTime startDate, + LocalDateTime endDate, + String category, + boolean isFinished, + String description) { + return UserExperience.builder() + .userId(userId) + .position(position) + .companyName(companyName) + .startDate(startDate) + .endDate(endDate) + .category(category) + .isFinished(isFinished) + .description(description) + .build(); + } + + /** 진행 중인 경력을 생성 */ + public static UserExperience createOngoingExperience( + Long userId, + String position, + String companyName, + LocalDateTime startDate, + String category) { + return createUserExperience( + userId, position, companyName, startDate, null, category, false, null); + } + + /** ID를 지정하여 경력을 생성 */ + public static UserExperience createUserExperienceWithId( + Long id, + Long userId, + String position, + String companyName, + LocalDateTime startDate, + LocalDateTime endDate, + String category, + boolean isFinished, + String description) { + UserExperience experience = + createUserExperience( + userId, + position, + companyName, + startDate, + endDate, + category, + isFinished, + description); + ReflectionTestUtils.setField(experience, "id", id); + return experience; + } +} diff --git a/techeerzip/src/test/java/backend/techeerzip/domain/userExperience/repository/UserExperienceRepositoryTest.java b/techeerzip/src/test/java/backend/techeerzip/domain/userExperience/repository/UserExperienceRepositoryTest.java new file mode 100644 index 00000000..046837e1 --- /dev/null +++ b/techeerzip/src/test/java/backend/techeerzip/domain/userExperience/repository/UserExperienceRepositoryTest.java @@ -0,0 +1,166 @@ +package backend.techeerzip.domain.userExperience.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.LocalDateTime; +import java.util.UUID; + +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.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import backend.techeerzip.config.RepositoryTestSupport; +import backend.techeerzip.domain.role.entity.Role; +import backend.techeerzip.domain.role.helper.RoleTestHelper; +import backend.techeerzip.domain.role.repository.RoleRepository; +import backend.techeerzip.domain.user.entity.User; +import backend.techeerzip.domain.user.helper.UserTestHelper; +import backend.techeerzip.domain.user.repository.UserRepository; +import backend.techeerzip.domain.userExperience.entity.UserExperience; +import backend.techeerzip.domain.userExperience.helper.UserExperienceTestHelper; +import backend.techeerzip.global.config.QueryDslConfig; + +@DataJpaTest( + properties = { + "spring.jpa.hibernate.ddl-auto=create", + "spring.jpa.properties.hibernate.globally_quoted_identifiers=true" + }) +@ActiveProfiles("test") +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@Import(QueryDslConfig.class) +@DisplayName("UserExperienceRepository Test") +class UserExperienceRepositoryTest extends RepositoryTestSupport { + + @Autowired private UserExperienceRepository userExperienceRepository; + + @Autowired private RoleRepository roleRepository; + + @Autowired private UserRepository userRepository; + + @Test + @DisplayName("경력 저장 및 조회 성공") + void saveAndFind_Success() { + String roleName = + RoleTestHelper.Default.USER_TEST + + "-" + + UUID.randomUUID().toString().substring(0, 8); + Role userRole = roleRepository.save(RoleTestHelper.createRole(roleName)); + + String email = "test-" + UUID.randomUUID() + "@example.com"; + String name = "테커집짱-" + UUID.randomUUID().toString().substring(0, 8); + User testUser = + userRepository.save( + UserTestHelper.createUser(name, email, "Techeerzip1!", userRole, true)); + + UserExperience newExperience = + UserExperienceTestHelper.createOngoingExperience( + testUser.getId(), + "BACKEND", + "뉴테커집", + LocalDateTime.of(2025, 12, 1, 0, 0), + "인턴"); + newExperience.setDescription("서버 관리"); + + UserExperience saved = userExperienceRepository.save(newExperience); + + var found = userExperienceRepository.findById(saved.getId()); + assertThat(found).isPresent(); + assertThat(found.get().getPosition()).isEqualTo("BACKEND"); + assertThat(found.get().getCompanyName()).isEqualTo("뉴테커집"); + } + + @Test + @DisplayName("경력 삭제 성공") + void delete_Success() { + String roleName = + RoleTestHelper.Default.USER_TEST + + "-" + + UUID.randomUUID().toString().substring(0, 8); + Role userRole = roleRepository.save(RoleTestHelper.createRole(roleName)); + + String email = "test-" + UUID.randomUUID() + "@example.com"; + String name = "테커집짱-" + UUID.randomUUID().toString().substring(0, 8); + User testUser = + userRepository.save( + UserTestHelper.createUser(name, email, "Techeerzip1!", userRole, true)); + + UserExperience experience = + UserExperienceTestHelper.createUserExperience( + testUser.getId(), + "BACKEND", + "테커집", + LocalDateTime.of(2025, 4, 10, 0, 0), + LocalDateTime.of(2025, 10, 31, 0, 0), + "인턴", + true, + "서버 관리"); + experience = userExperienceRepository.save(experience); + + assertThat(experience.isDeleted()).isFalse(); + + experience.delete(); + userExperienceRepository.save(experience); + + var found = userExperienceRepository.findById(experience.getId()); + assertThat(found).isPresent(); + assertThat(found.get().isDeleted()).isTrue(); + } + + @Test + @Transactional + @DisplayName("사용자 ID로 경력 전체 삭제 성공") + void updateIsDeletedByUserId_Success() { + String roleName = + RoleTestHelper.Default.USER_TEST + + "-" + + UUID.randomUUID().toString().substring(0, 8); + Role userRole = roleRepository.save(RoleTestHelper.createRole(roleName)); + + String email = "test-" + UUID.randomUUID() + "@example.com"; + String name = "테커집짱-" + UUID.randomUUID().toString().substring(0, 8); + User testUser = + userRepository.save( + UserTestHelper.createUser(name, email, "Techeerzip1!", userRole, true)); + + UserExperience experience1 = + UserExperienceTestHelper.createUserExperience( + testUser.getId(), + "BACKEND", + "테커집", + LocalDateTime.of(2025, 4, 10, 0, 0), + LocalDateTime.of(2025, 10, 31, 0, 0), + "인턴", + true, + "서버 관리"); + + UserExperience experience2 = + UserExperienceTestHelper.createOngoingExperience( + testUser.getId(), + "FRONTEND", + "뉴테커집", + LocalDateTime.of(2025, 11, 20, 0, 0), + "인턴"); + experience2.setDescription("UI/UX 개발"); + + experience1 = userExperienceRepository.save(experience1); + experience2 = userExperienceRepository.save(experience2); + + assertThat(experience1.isDeleted()).isFalse(); + assertThat(experience2.isDeleted()).isFalse(); + + userExperienceRepository.updateIsDeletedByUserId(testUser.getId()); + + var found1 = userExperienceRepository.findById(experience1.getId()); + var found2 = userExperienceRepository.findById(experience2.getId()); + + assertThat(found1).isPresent(); + assertThat(found1.get().isDeleted()).isTrue(); + assertThat(found2).isPresent(); + assertThat(found2.get().isDeleted()).isTrue(); + } +}