diff --git a/build.gradle b/build.gradle index f1f0568..6749a14 100644 --- a/build.gradle +++ b/build.gradle @@ -33,6 +33,11 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' + implementation 'org.springframework.boot:spring-boot-starter-security' + } tasks.named('test') { diff --git a/src/main/java/com/dasom/MemoReal/domain/UserManager/controller/UserController.java b/src/main/java/com/dasom/MemoReal/domain/UserManager/controller/UserController.java new file mode 100644 index 0000000..8db42c7 --- /dev/null +++ b/src/main/java/com/dasom/MemoReal/domain/UserManager/controller/UserController.java @@ -0,0 +1,45 @@ +package com.dasom.MemoReal.domain.UserManager.controller; + +import com.dasom.MemoReal.domain.UserManager.dto.*; +import com.dasom.MemoReal.domain.UserManager.entity.User; +import com.dasom.MemoReal.domain.UserManager.service.UserService; +import com.dasom.MemoReal.global.security.util.JwtUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@RestController +@RequestMapping("/api/users") +public class UserController { + @Autowired + private UserService userService; + + @PostMapping("/register") + public CommonResponse register(@RequestBody UserRegisterRequest request) { + User user = userService.register(request); + return new CommonResponse<>(true, user); + } + + @PostMapping("/login") + public CommonResponse login(@RequestBody LoginDTO.Request request) { + User user = userService.login(request.getEmail(), request.getPassword()); + + String token = JwtUtil.generateToken(user.getUsername()); + LoginDTO.Response response = new LoginDTO.Response(token); + + return new CommonResponse<>(true, response); + } + + @PutMapping("/update") + public CommonResponse updateUserInfo( + @RequestHeader("Authorization") String authHeader, + @RequestBody Map updates) { + String token = authHeader.startsWith("Bearer ") ? authHeader.substring(7) : authHeader; + String email = JwtUtil.extractEmail(token); + + User updatedUser = userService.updateUserInfoByMap(email, updates); + + return new CommonResponse<>(true, updatedUser); + } +} diff --git a/src/main/java/com/dasom/MemoReal/domain/UserManager/dto/CommonResponse.java b/src/main/java/com/dasom/MemoReal/domain/UserManager/dto/CommonResponse.java new file mode 100644 index 0000000..5aec41b --- /dev/null +++ b/src/main/java/com/dasom/MemoReal/domain/UserManager/dto/CommonResponse.java @@ -0,0 +1,13 @@ +package com.dasom.MemoReal.domain.UserManager.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CommonResponse { // 데이터 반환값 형식 + private boolean success;// 성공 여부 + private T data; // 데이터 or 오류 코드 +} diff --git a/src/main/java/com/dasom/MemoReal/domain/UserManager/dto/LoginDTO.java b/src/main/java/com/dasom/MemoReal/domain/UserManager/dto/LoginDTO.java new file mode 100644 index 0000000..ac96d4a --- /dev/null +++ b/src/main/java/com/dasom/MemoReal/domain/UserManager/dto/LoginDTO.java @@ -0,0 +1,23 @@ +package com.dasom.MemoReal.domain.UserManager.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +public class LoginDTO { + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class Request { + private String email; + private String password; + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class Response { + private String jwt; + } +} \ No newline at end of file diff --git a/src/main/java/com/dasom/MemoReal/domain/UserManager/dto/UserRegisterRequest.java b/src/main/java/com/dasom/MemoReal/domain/UserManager/dto/UserRegisterRequest.java new file mode 100644 index 0000000..dcd1b29 --- /dev/null +++ b/src/main/java/com/dasom/MemoReal/domain/UserManager/dto/UserRegisterRequest.java @@ -0,0 +1,18 @@ +package com.dasom.MemoReal.domain.UserManager.dto; +import com.dasom.MemoReal.domain.UserManager.entity.User; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class UserRegisterRequest { + private String username; + private String email; + private String password; + + public User toEntity(String encodedPassword) { + return new User(username, email, encodedPassword); + } +} diff --git a/src/main/java/com/dasom/MemoReal/domain/UserManager/entity/User.java b/src/main/java/com/dasom/MemoReal/domain/UserManager/entity/User.java new file mode 100644 index 0000000..9e6e6e7 --- /dev/null +++ b/src/main/java/com/dasom/MemoReal/domain/UserManager/entity/User.java @@ -0,0 +1,38 @@ +package com.dasom.MemoReal.domain.UserManager.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.CreationTimestamp; +import com.fasterxml.jackson.annotation.JsonFormat; + +import java.time.LocalDate; + +@Entity +@Table(name = "users") +@Data +@AllArgsConstructor +@NoArgsConstructor +public class User { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long uid; + + @Column(unique = true) + private String username; + + private String email; + private String password; + + @CreationTimestamp + @Column(updatable = false) + @JsonFormat(pattern = "yyyy-MM-dd", timezone = "Asia/Seoul") // JSON 응답 형식 지정 + private LocalDate createdAt; + + public User(String username, String email, String password) { + this.username = username; + this.email = email; + this.password = password; + } +} diff --git a/src/main/java/com/dasom/MemoReal/domain/UserManager/repository/UserRepository.java b/src/main/java/com/dasom/MemoReal/domain/UserManager/repository/UserRepository.java new file mode 100644 index 0000000..6db7845 --- /dev/null +++ b/src/main/java/com/dasom/MemoReal/domain/UserManager/repository/UserRepository.java @@ -0,0 +1,13 @@ +package com.dasom.MemoReal.domain.UserManager.repository; + +import com.dasom.MemoReal.domain.UserManager.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserRepository extends JpaRepository { + Optional findByEmail(String email); + Optional findByUsername(String username); + boolean existsByEmail(String email); + boolean existsByUsername(String username); +} diff --git a/src/main/java/com/dasom/MemoReal/domain/UserManager/service/UserService.java b/src/main/java/com/dasom/MemoReal/domain/UserManager/service/UserService.java new file mode 100644 index 0000000..d61c5cf --- /dev/null +++ b/src/main/java/com/dasom/MemoReal/domain/UserManager/service/UserService.java @@ -0,0 +1,82 @@ +package com.dasom.MemoReal.domain.UserManager.service; + +import com.dasom.MemoReal.domain.UserManager.dto.UserRegisterRequest; +import com.dasom.MemoReal.domain.UserManager.entity.User; +import com.dasom.MemoReal.domain.UserManager.repository.UserRepository; +import com.dasom.MemoReal.global.exception.CustomException; +import com.dasom.MemoReal.global.exception.ErrorCode; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Map; + +@Service +public class UserService { + + private final UserRepository repository; + private final PasswordEncoder passwordEncoder; + + public UserService(UserRepository repository, PasswordEncoder passwordEncoder) { + this.repository = repository; + this.passwordEncoder = passwordEncoder; + } + + @Transactional + public User register(UserRegisterRequest request) { + if (repository.existsByUsername(request.getUsername())) { + throw new CustomException(ErrorCode.DUPLICATE_USERNAME); + } + + if (repository.existsByEmail(request.getEmail())) { + throw new CustomException(ErrorCode.DUPLICATE_EMAIL); + } + + String encodedPassword = passwordEncoder.encode(request.getPassword()); + return repository.save(request.toEntity(encodedPassword)); + } + + @Transactional(readOnly = true) + public User login(String email, String rawPassword) { + User user = repository.findByEmail(email) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + + if (!passwordEncoder.matches(rawPassword, user.getPassword())) { + throw new CustomException(ErrorCode.INVALID_PASSWORD); + } + + return user; + } + + @Transactional + public User updateUserInfoByMap(String email, Map updates) { + User user = repository.findByEmail(email) + .orElseThrow(() -> new CustomException(ErrorCode.USER_UPDATE_NOT_FOUND)); + + for (Map.Entry entry : updates.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + + switch (key) { + case "username": + String newUsername = (String) value; + if (repository.existsByUsername(newUsername) && !user.getUsername().equals(newUsername)) { + throw new CustomException(ErrorCode.DUPLICATE_USERNAME); + } + user.setUsername(newUsername); + break; + case "email": + throw new CustomException(ErrorCode.INVALID_UPDATE_FIELD, "이메일은 수정할 수 없습니다."); + case "password": + String encodedNewPassword = passwordEncoder.encode((String) value); + user.setPassword(encodedNewPassword); + break; + default: + throw new CustomException(ErrorCode.INVALID_UPDATE_FIELD, "수정할 수 없는 필드: " + key); + } + } + + return repository.save(user); + } + +} \ No newline at end of file diff --git a/src/main/java/com/dasom/MemoReal/global/exception/CustomException.java b/src/main/java/com/dasom/MemoReal/global/exception/CustomException.java new file mode 100644 index 0000000..d629a43 --- /dev/null +++ b/src/main/java/com/dasom/MemoReal/global/exception/CustomException.java @@ -0,0 +1,18 @@ +package com.dasom.MemoReal.global.exception; + +import lombok.Getter; + +@Getter +public class CustomException extends RuntimeException { + private final ErrorCode errorCode; + + public CustomException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } + + public CustomException(ErrorCode errorCode, String customMessage) { + super(customMessage); + this.errorCode = errorCode; + }// 에러코드는 공유하되 메시지 커스텀 해서 사용 +} diff --git a/src/main/java/com/dasom/MemoReal/global/exception/ErrorCode.java b/src/main/java/com/dasom/MemoReal/global/exception/ErrorCode.java new file mode 100644 index 0000000..5aaf5f1 --- /dev/null +++ b/src/main/java/com/dasom/MemoReal/global/exception/ErrorCode.java @@ -0,0 +1,32 @@ +package com.dasom.MemoReal.global.exception; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public enum ErrorCode { + // 유저 등록, 수정 + DUPLICATE_USERNAME("USER_001", HttpStatus.CONFLICT, "이미 존재하는 사용자명입니다."), + DUPLICATE_EMAIL("USER_002", HttpStatus.CONFLICT, "이미 존재하는 이메일입니다."), + USER_UPDATE_NOT_FOUND("USER_003", HttpStatus.NOT_FOUND, "사용자를 찾을 수 없습니다."), + INVALID_UPDATE_FIELD("USER_004", HttpStatus.BAD_REQUEST, "수정할 수 없는 필드입니다."), + + // 사용자 로그인 관련 에러 + USER_NOT_FOUND("AUTH_001", HttpStatus.NOT_FOUND, "이메일이 존재하지 않습니다."), + INVALID_PASSWORD("AUTH_002", HttpStatus.UNAUTHORIZED, "비밀번호가 일치하지 않습니다."), + + // 일반적인 에러(유효성 검사 등) + INVALID_INPUT_VALUE("COMMON_001", HttpStatus.BAD_REQUEST, "유효하지 않은 입력 값입니다."), + UNAUTHORIZED("COMMON_002", HttpStatus.UNAUTHORIZED, "인증되지 않은 접근입니다."), + INTERNAL_SERVER_ERROR("COMMON_999", HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류가 발생했습니다."); + + private final String code; + private final HttpStatus httpStatus; + private final String message; + + ErrorCode(String code, HttpStatus httpStatus, String message) { + this.code = code; + this.httpStatus = httpStatus; + this.message = message; + } +} diff --git a/src/main/java/com/dasom/MemoReal/global/exception/GlobalExceptionHandler.java b/src/main/java/com/dasom/MemoReal/global/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..cca5dde --- /dev/null +++ b/src/main/java/com/dasom/MemoReal/global/exception/GlobalExceptionHandler.java @@ -0,0 +1,38 @@ +package com.dasom.MemoReal.global.exception; + +import com.dasom.MemoReal.domain.UserManager.dto.CommonResponse; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +@RestControllerAdvice +public class GlobalExceptionHandler { + + private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); + + // 사용자 정의 예외 처리 (비즈니스, 서버 예외) + @ExceptionHandler(CustomException.class) + protected ResponseEntity> handleCustomException(CustomException e) { + ErrorCode code = e.getErrorCode(); + HttpStatus status = code.getHttpStatus(); + log.warn("CustomException occurred: {}", code.getMessage(), e); + return ResponseEntity + .status(status) + .body(new CommonResponse<>(false, code.getMessage())); // 오류라 success=false + } + + // 예기치 못한 예외 처리 + @ExceptionHandler(Exception.class) + protected ResponseEntity> handleException(Exception e) { + log.error("Unhandled Exception: {}", e.getMessage(), e); + return ResponseEntity + .status(ErrorCode.INTERNAL_SERVER_ERROR.getHttpStatus()) + .body(new CommonResponse<>(false, ErrorCode.INTERNAL_SERVER_ERROR.getMessage())); // 오류라 success=false + } + +} + diff --git a/src/main/java/com/dasom/MemoReal/global/security/config/SecurityConfig.java b/src/main/java/com/dasom/MemoReal/global/security/config/SecurityConfig.java new file mode 100644 index 0000000..3a937a8 --- /dev/null +++ b/src/main/java/com/dasom/MemoReal/global/security/config/SecurityConfig.java @@ -0,0 +1,15 @@ +package com.dasom.MemoReal.global.security.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +public class SecurityConfig { + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/src/main/java/com/dasom/MemoReal/global/security/util/JwtUtil.java b/src/main/java/com/dasom/MemoReal/global/security/util/JwtUtil.java new file mode 100644 index 0000000..2bb4981 --- /dev/null +++ b/src/main/java/com/dasom/MemoReal/global/security/util/JwtUtil.java @@ -0,0 +1,30 @@ +package com.dasom.MemoReal.global.security.util; + +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; + +import java.security.Key; +import java.util.Date; + +public class JwtUtil { + private static final Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256); + private static final long EXPIRATION_TIME = 86400000L; // 1일 + + public static String generateToken(String email) { + return Jwts.builder() + .setSubject(email) + .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) + .signWith(key) + .compact(); + } + + public static String extractEmail(String token) { + return Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(token) + .getBody() + .getSubject(); + } +} diff --git a/src/test/java/com/dasom/MemoReal/UserTest/UserServiceIntegrationTest.java b/src/test/java/com/dasom/MemoReal/UserTest/UserServiceIntegrationTest.java new file mode 100644 index 0000000..77f77e4 --- /dev/null +++ b/src/test/java/com/dasom/MemoReal/UserTest/UserServiceIntegrationTest.java @@ -0,0 +1,276 @@ +package com.dasom.MemoReal.UserTest; + +import com.dasom.MemoReal.domain.UserManager.dto.UserRegisterRequest; +import com.dasom.MemoReal.domain.UserManager.entity.User; +import com.dasom.MemoReal.domain.UserManager.repository.UserRepository; +import com.dasom.MemoReal.domain.UserManager.service.UserService; +import com.dasom.MemoReal.global.exception.CustomException; +import com.dasom.MemoReal.global.exception.ErrorCode; +import org.junit.jupiter.api.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Map; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@SpringBootTest +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class UserServiceIntegrationTest { + + private static final Logger log = LoggerFactory.getLogger(UserServiceIntegrationTest.class); + + @Autowired + private UserService userService; + + @Autowired + private UserRepository userRepository; + + @Autowired + private PasswordEncoder passwordEncoder; + + private static final String TEST_USERNAME = "integrationTestUser"; + private static final String TEST_EMAIL = "integration@example.com"; + private static final String TEST_PASSWORD = "integrationPass"; + + @BeforeEach + void cleanupBeforeEach() { + userRepository.findByEmail(TEST_EMAIL).ifPresent(userRepository::delete); + userRepository.flush(); + log.info("BeforeEach: 이전 테스트 데이터 (이메일: {}) 정리 완료", TEST_EMAIL); + } + + @Test + @Order(1) + @Transactional + @DisplayName("✅ 1. 실제 DB에 유저 등록 및 조회 테스트") + void registerAndFindUserInRealDB() { + log.info("---- [1] 유저 등록 및 조회 테스트 시작 ----"); + + UserRegisterRequest request = new UserRegisterRequest(TEST_USERNAME, TEST_EMAIL, TEST_PASSWORD); + User savedUser = userService.register(request); + log.info("유저 등록 완료. User ID: {}, Username: {}", savedUser.getUid(), savedUser.getUsername()); + + assertThat(savedUser).isNotNull(); + assertThat(savedUser.getUsername()).isEqualTo(TEST_USERNAME); + assertThat(savedUser.getEmail()).isEqualTo(TEST_EMAIL); + assertThat(passwordEncoder.matches(TEST_PASSWORD, savedUser.getPassword())).isTrue(); + + Optional foundUserOptional = userRepository.findByUsername(TEST_USERNAME); + assertThat(foundUserOptional).isPresent(); + User foundUser = foundUserOptional.get(); + + assertThat(foundUser.getUid()).isEqualTo(savedUser.getUid()); + assertThat(foundUser.getEmail()).isEqualTo(TEST_EMAIL); + assertThat(passwordEncoder.matches(TEST_PASSWORD, foundUser.getPassword())).isTrue(); + log.info("유저 DB 조회 및 검증 완료. Found User ID: {}", foundUser.getUid()); + + log.info("---- [1] 유저 등록 및 조회 테스트 종료 ----\n"); + } + + @Test + @Order(2) + @Transactional + @DisplayName("❌ 2. 이미 존재하는 유저명으로 등록 시도 - CustomException 발생") + void registerUserWithDuplicateUsername() { + log.info("---- [2] 중복 유저명 등록 테스트 시작 ----"); + + userService.register(new UserRegisterRequest(TEST_USERNAME, "unique@example.com", "pass")); + + UserRegisterRequest duplicateRequest = new UserRegisterRequest(TEST_USERNAME, "another@example.com", "anotherpass"); + assertThatThrownBy(() -> userService.register(duplicateRequest)) + .isInstanceOf(CustomException.class) + .hasFieldOrPropertyWithValue("errorCode", ErrorCode.DUPLICATE_USERNAME); + + log.warn("중복 유저명 등록 시도 -> CustomException(DUPLICATE_USERNAME) 발생 확인."); + log.info("---- [2] 중복 유저명 등록 테스트 종료 ----\n"); + } + + @Test + @Order(3) + @Transactional + @DisplayName("❌ 3. 이미 존재하는 이메일로 등록 시도 - CustomException 발생") + void registerUserWithDuplicateEmail() { + log.info("---- [3] 중복 이메일 등록 테스트 시작 ----"); + + userService.register(new UserRegisterRequest("anotherUser", TEST_EMAIL, "pass")); + + UserRegisterRequest duplicateRequest = new UserRegisterRequest("anotherUser2", TEST_EMAIL, "anotherpass"); + assertThatThrownBy(() -> userService.register(duplicateRequest)) + .isInstanceOf(CustomException.class) + .hasFieldOrPropertyWithValue("errorCode", ErrorCode.DUPLICATE_EMAIL); + + log.warn("중복 이메일 등록 시도 -> CustomException(DUPLICATE_EMAIL) 발생 확인."); + log.info("---- [3] 중복 이메일 등록 테스트 종료 ----\n"); + } + + @Test + @Order(4) + @DisplayName("✅ 4. 로그인 기능 테스트") + void loginUserTest() { + log.info("---- [4] 로그인 테스트 시작 ----"); + + userService.register(new UserRegisterRequest(TEST_USERNAME, TEST_EMAIL, TEST_PASSWORD)); + User loggedInUser = userService.login(TEST_EMAIL, TEST_PASSWORD); + log.info("로그인 성공. User Email: {}", loggedInUser.getEmail()); + + assertThat(loggedInUser).isNotNull(); + assertThat(loggedInUser.getEmail()).isEqualTo(TEST_EMAIL); + assertThat(passwordEncoder.matches(TEST_PASSWORD, loggedInUser.getPassword())).isTrue(); + + log.info("---- [4] 로그인 테스트 종료 ----\n"); + } + + @Test + @Order(5) + @DisplayName("❌ 5. 존재하지 않는 이메일로 로그인 시도 - CustomException 발생") + void loginWithNonExistentEmail() { + log.info("---- [5] 존재하지 않는 이메일로 로그인 테스트 시작 ----"); + + assertThatThrownBy(() -> userService.login("nonexistent@example.com", "anypass")) + .isInstanceOf(CustomException.class) + .hasFieldOrPropertyWithValue("errorCode", ErrorCode.USER_NOT_FOUND); + + log.warn("존재하지 않는 이메일 로그인 시도 -> CustomException(USER_NOT_FOUND) 발생 확인."); + log.info("---- [5] 존재하지 않는 이메일로 로그인 테스트 종료 ----\n"); + } + + @Test + @Order(6) + @DisplayName("❌ 6. 잘못된 비밀번호로 로그인 시도 - CustomException 발생") + void loginWithWrongPassword() { + log.info("---- [6] 잘못된 비밀번호로 로그인 테스트 시작 ----"); + + userService.register(new UserRegisterRequest(TEST_USERNAME, TEST_EMAIL, TEST_PASSWORD)); + + assertThatThrownBy(() -> userService.login(TEST_EMAIL, "wrongpass")) + .isInstanceOf(CustomException.class) + .hasFieldOrPropertyWithValue("errorCode", ErrorCode.INVALID_PASSWORD); + + log.warn("잘못된 비밀번호 로그인 시도 -> CustomException(INVALID_PASSWORD) 발생 확인."); + log.info("---- [6] 잘못된 비밀번호로 로그인 테스트 종료 ----\n"); + } + + @Test + @Order(7) + @Transactional + @DisplayName("✅ 7. 회원정보 수정 테스트 (username, password 변경)") + void updateUserInfoTest() { + log.info("---- [7] 회원정보 수정 테스트 시작 ----"); + + userService.register(new UserRegisterRequest(TEST_USERNAME, TEST_EMAIL, TEST_PASSWORD)); + + String newUsername = "updatedUserIntegration"; + String newPassword = "newIntegrationPass"; + + Map updates = Map.of("username", newUsername, "password", newPassword); + User updatedUser = userService.updateUserInfoByMap(TEST_EMAIL, updates); + + assertThat(updatedUser.getUsername()).isEqualTo(newUsername); + assertThat(passwordEncoder.matches(newPassword, updatedUser.getPassword())).isTrue(); + + log.info("회원정보 수정 결과 확인 완료."); + log.info("---- [7] 회원정보 수정 테스트 종료 ----\n"); + } + + @Test + @Order(8) + @Transactional + @DisplayName("❌ 8. 이미 존재하는 username으로 수정 시도 - CustomException 발생") + void updateUserInfoWithDuplicateUsername() { + log.info("---- [8] 중복 username으로 수정 시도 테스트 시작 ----"); + + String dummyUsername = "dummyUser"; + String dummyEmail = "dummy@example.com"; + userRepository.save(new User(dummyUsername, dummyEmail, passwordEncoder.encode("dummy"))); + + userService.register(new UserRegisterRequest(TEST_USERNAME, TEST_EMAIL, TEST_PASSWORD)); + + Map updates = Map.of("username", dummyUsername); + assertThatThrownBy(() -> userService.updateUserInfoByMap(TEST_EMAIL, updates)) + .isInstanceOf(CustomException.class) + .hasFieldOrPropertyWithValue("errorCode", ErrorCode.DUPLICATE_USERNAME); + + log.warn("중복 username으로 수정 시도 -> CustomException(DUPLICATE_USERNAME) 발생 확인."); + + userRepository.findByEmail(dummyEmail).ifPresent(userRepository::delete); + log.info("---- [8] 중복 username으로 수정 시도 테스트 종료 ----\n"); + } + + @Test + @Order(9) + @Transactional + @DisplayName("❌ 9. 이메일 수정 시도 - CustomException 발생") + void updateUserInfoEmailForbidden() { + log.info("---- [9] 이메일 수정 시도 테스트 시작 ----"); + + userService.register(new UserRegisterRequest(TEST_USERNAME, TEST_EMAIL, TEST_PASSWORD)); + + Map updates = Map.of("email", "newemail@example.com"); + assertThatThrownBy(() -> userService.updateUserInfoByMap(TEST_EMAIL, updates)) + .isInstanceOf(CustomException.class) + .hasFieldOrPropertyWithValue("errorCode", ErrorCode.INVALID_UPDATE_FIELD); + + log.warn("이메일 수정 시도 -> CustomException(INVALID_UPDATE_FIELD) 발생 확인."); + log.info("---- [9] 이메일 수정 시도 테스트 종료 ----\n"); + } + + @Test + @Order(10) + @Transactional + @DisplayName("❌ 10. 허용되지 않은 필드 수정 시도 - CustomException 발생") + void updateUserInfoInvalidField() { + log.info("---- [10] 허용되지 않은 필드 수정 시도 테스트 시작 ----"); + + userService.register(new UserRegisterRequest(TEST_USERNAME, TEST_EMAIL, TEST_PASSWORD)); + + Map updates = Map.of("role", "admin"); + assertThatThrownBy(() -> userService.updateUserInfoByMap(TEST_EMAIL, updates)) + .isInstanceOf(CustomException.class) + .hasFieldOrPropertyWithValue("errorCode", ErrorCode.INVALID_UPDATE_FIELD); + + log.warn("허용되지 않은 필드 수정 시도 -> CustomException(INVALID_UPDATE_FIELD) 발생 확인."); + log.info("---- [10] 허용되지 않은 필드 수정 시도 테스트 종료 ----\n"); + } + + @Test + @Order(11) + @Transactional + @DisplayName("❌ 11. 존재하지 않는 사용자의 정보 수정 시도 - CustomException 발생") + void updateUserInfoUserNotFound() { + log.info("---- [11] 존재하지 않는 사용자 정보 수정 테스트 시작 ----"); + + Map updates = Map.of("username", "newname"); + assertThatThrownBy(() -> userService.updateUserInfoByMap("notfound@example.com", updates)) + .isInstanceOf(CustomException.class) + .hasFieldOrPropertyWithValue("errorCode", ErrorCode.USER_UPDATE_NOT_FOUND); + + log.warn("존재하지 않는 사용자 정보 수정 시도 -> CustomException(USER_UPDATE_NOT_FOUND) 발생 확인."); + log.info("---- [11] 존재하지 않는 사용자 정보 수정 테스트 종료 ----\n"); + } + + @Test + @Order(12) + @Transactional + @DisplayName("🧹 12. 테스트 유저 데이터 정리") + void cleanupTestUser() { + log.info("---- [12] 테스트 유저 데이터 정리 시작 ----"); + + userRepository.findByEmail(TEST_EMAIL).ifPresent(user -> { + userRepository.delete(user); + log.info("🗑️ 테스트 유저 삭제 완료 - email: {}", user.getEmail()); + }); + + boolean exists = userRepository.existsByEmail(TEST_EMAIL); + assertThat(exists).isFalse(); + log.info("삭제 후 사용자 존재 여부: {}", exists); + + log.info("---- [12] 테스트 유저 데이터 정리 종료 ----\n"); + } +} diff --git a/src/test/java/com/dasom/MemoReal/UserTest/UserServiceTest.java b/src/test/java/com/dasom/MemoReal/UserTest/UserServiceTest.java new file mode 100644 index 0000000..6aceb4a --- /dev/null +++ b/src/test/java/com/dasom/MemoReal/UserTest/UserServiceTest.java @@ -0,0 +1,272 @@ +package com.dasom.MemoReal.UserTest; + +import com.dasom.MemoReal.domain.UserManager.dto.UserRegisterRequest; +import com.dasom.MemoReal.domain.UserManager.entity.User; +import com.dasom.MemoReal.domain.UserManager.repository.UserRepository; +import com.dasom.MemoReal.domain.UserManager.service.UserService; +import com.dasom.MemoReal.global.exception.CustomException; +import com.dasom.MemoReal.global.exception.ErrorCode; + +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.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.crypto.password.PasswordEncoder; + +import java.util.Map; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +class UserServiceTest { + + private static final Logger log = LoggerFactory.getLogger(UserServiceTest.class); + + private UserRepository repository; + private PasswordEncoder passwordEncoder; + private UserService userService; + + @BeforeEach + void setUp() { + repository = mock(UserRepository.class); + passwordEncoder = mock(PasswordEncoder.class); + userService = new UserService(repository, passwordEncoder); + } + + @Nested + @DisplayName("회원가입 테스트") + class RegisterTest { + + @Test + @DisplayName("성공") + void register_success() { + log.info("──────────── 회원가입 성공 테스트 시작 ────────────"); + + UserRegisterRequest request = new UserRegisterRequest("user1", "user@example.com", "pass"); + + when(repository.existsByUsername("user1")).thenReturn(false); + when(repository.existsByEmail("user@example.com")).thenReturn(false); + when(passwordEncoder.encode("pass")).thenReturn("encoded_pass"); + when(repository.save(any(User.class))).thenAnswer(i -> i.getArguments()[0]); + + User user = userService.register(request); + + log.info("✅ 회원가입 성공 - username: {}", user.getUsername()); + + assertThat(user.getUsername()).isEqualTo("user1"); + assertThat(user.getPassword()).isEqualTo("encoded_pass"); + } + + @Test + @DisplayName("이미 존재하는 사용자명") + void register_duplicateUsername() { + log.info("──────────── 사용자명 중복 테스트 시작 ────────────"); + + UserRegisterRequest request = new UserRegisterRequest("user1", "user@example.com", "pass"); + when(repository.existsByUsername("user1")).thenReturn(true); + + assertThatThrownBy(() -> userService.register(request)) + .isInstanceOf(CustomException.class) + .hasFieldOrPropertyWithValue("errorCode", ErrorCode.DUPLICATE_USERNAME) + .satisfies(e -> { + log.warn("▶ 예외 메시지: {}", e.getMessage()); + log.info("✅ 예외가 정상적으로 처리되었습니다."); + }); + } + + @Test + @DisplayName("이미 존재하는 이메일") + void register_duplicateEmail() { + log.info("──────────── 이메일 중복 테스트 시작 ────────────"); + + UserRegisterRequest request = new UserRegisterRequest("user1", "user@example.com", "pass"); + + when(repository.existsByUsername("user1")).thenReturn(false); + when(repository.existsByEmail("user@example.com")).thenReturn(true); + + assertThatThrownBy(() -> userService.register(request)) + .isInstanceOf(CustomException.class) + .hasFieldOrPropertyWithValue("errorCode", ErrorCode.DUPLICATE_EMAIL) + .satisfies(e -> { + log.warn("▶ 예외 메시지: {}", e.getMessage()); + log.info("✅ 예외가 정상적으로 처리되었습니다."); + }); + } + } + + @Nested + @DisplayName("로그인 테스트") + class LoginTest { + + @Test + @DisplayName("성공") + void login_success() { + log.info("──────────── 로그인 성공 테스트 시작 ────────────"); + + User user = new User("user1", "user@example.com", "encoded_pass"); + + when(repository.findByEmail("user@example.com")).thenReturn(Optional.of(user)); + when(passwordEncoder.matches("pass", "encoded_pass")).thenReturn(true); + + User result = userService.login("user@example.com", "pass"); + + log.info("✅ 로그인 성공 - username: {}", result.getUsername()); + + assertThat(result).isNotNull(); + assertThat(result.getUsername()).isEqualTo("user1"); + } + + @Test + @DisplayName("이메일이 존재하지 않음") + void login_with_nonexistent_email() { + log.info("──────────── 로그인 실패 테스트 (이메일 없음) 시작 ────────────"); + + when(repository.findByEmail("nonexistent@example.com")).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> userService.login("nonexistent@example.com", "any_password")) + .isInstanceOf(CustomException.class) + .hasFieldOrPropertyWithValue("errorCode", ErrorCode.USER_NOT_FOUND); + + log.warn("❌ 로그인 실패 - 이메일 없음으로 예외 발생"); + } + + @Test + @DisplayName("비밀번호가 일치하지 않음") + void login_with_wrong_password() { + log.info("──────────── 로그인 실패 테스트 (비밀번호 불일치) 시작 ────────────"); + + User user = new User("user1", "user@example.com", "encoded_pass"); + when(repository.findByEmail("user@example.com")).thenReturn(Optional.of(user)); + when(passwordEncoder.matches("wrongpass", "encoded_pass")).thenReturn(false); + + assertThatThrownBy(() -> userService.login("user@example.com", "wrongpass")) + .isInstanceOf(CustomException.class) + .hasFieldOrPropertyWithValue("errorCode", ErrorCode.INVALID_PASSWORD); + + log.warn("❌ 로그인 실패 - 비밀번호 불일치로 예외 발생"); + } + } + + @Nested + @DisplayName("유저 정보 수정 테스트") + class UpdateUserInfoTest { + + @Test + @DisplayName("username 수정 성공") + void update_usernameChange() { + log.info("──────────── username 수정 테스트 시작 ────────────"); + + User user = new User("user1", "user@example.com", "pass"); + when(repository.findByEmail("user@example.com")).thenReturn(Optional.of(user)); + when(repository.save(any(User.class))).thenAnswer(i -> i.getArguments()[0]); + + Map updates = Map.of("username", "newUser"); + + User result = userService.updateUserInfoByMap("user@example.com", updates); + + log.info("✅ username 수정 성공 - username: {}", result.getUsername()); + + assertThat(result.getUsername()).isEqualTo("newUser"); + } + + @Test + @DisplayName("이미 존재하는 username으로 수정 시도") + void update_duplicateUsername() { + log.info("──────────── 이미 존재하는 username으로 수정 시도 테스트 시작 ────────────"); + + User currentUser = new User("user1", "user@example.com", "pass"); + String existingUsername = "existingUser"; + + when(repository.findByEmail("user@example.com")).thenReturn(Optional.of(currentUser)); + when(repository.existsByUsername(existingUsername)).thenReturn(true); + + Map updates = Map.of("username", existingUsername); + + assertThatThrownBy(() -> userService.updateUserInfoByMap("user@example.com", updates)) + .isInstanceOf(CustomException.class) + .hasFieldOrPropertyWithValue("errorCode", ErrorCode.DUPLICATE_USERNAME) + .satisfies(e -> { + log.warn("▶ 예외 메시지: {}", e.getMessage()); + log.info("✅ 예외가 정상적으로 처리되었습니다."); + }); + } + + @Test + @DisplayName("email 수정 시도") + void update_emailChange() { + log.info("──────────── email 수정 시도 테스트 시작 ────────────"); + + User user = new User("user1", "user@example.com", "pass"); + when(repository.findByEmail("user@example.com")).thenReturn(Optional.of(user)); + + Map updates = Map.of("email", "new@example.com"); + + assertThatThrownBy(() -> userService.updateUserInfoByMap("user@example.com", updates)) + .isInstanceOf(CustomException.class) + .hasFieldOrPropertyWithValue("errorCode", ErrorCode.INVALID_UPDATE_FIELD) + .satisfies(e -> { + log.warn("▶ 예외 메시지: {}", e.getMessage()); + log.info("✅ 예외가 정상적으로 처리되었습니다."); + }); + } + + @Test + @DisplayName("password 수정 성공") + void update_passwordChange() { + log.info("──────────── password 수정 테스트 시작 ────────────"); + + User user = new User("user1", "user@example.com", "oldpass"); + when(repository.findByEmail("user@example.com")).thenReturn(Optional.of(user)); + when(passwordEncoder.encode("newpass")).thenReturn("encoded_newpass"); + when(repository.save(any(User.class))).thenAnswer(i -> i.getArguments()[0]); + + Map updates = Map.of("password", "newpass"); + + User result = userService.updateUserInfoByMap("user@example.com", updates); + + log.info("✅ password 수정 성공 - password: {}", result.getPassword()); + + assertThat(result.getPassword()).isEqualTo("encoded_newpass"); + } + + @Test + @DisplayName("허용되지 않은 필드 수정 시도") + void update_invalidField() { + log.info("──────────── 허용되지 않은 필드 수정 테스트 시작 ────────────"); + + User user = new User("user1", "user@example.com", "pass"); + when(repository.findByEmail("user@example.com")).thenReturn(Optional.of(user)); + + Map updates = Map.of("role", "admin"); + + assertThatThrownBy(() -> userService.updateUserInfoByMap("user@example.com", updates)) + .isInstanceOf(CustomException.class) + .hasFieldOrPropertyWithValue("errorCode", ErrorCode.INVALID_UPDATE_FIELD) + .satisfies(e -> { + log.warn("▶ 예외 메시지: {}", e.getMessage()); + log.info("✅ 예외가 정상적으로 처리되었습니다."); + }); + } + + @Test + @DisplayName("사용자 없음") + void update_userNotFound() { + log.info("──────────── 사용자 없음 테스트 시작 ────────────"); + + when(repository.findByEmail("unknown@example.com")).thenReturn(Optional.empty()); + + Map updates = Map.of("intro", "intro"); + + assertThatThrownBy(() -> userService.updateUserInfoByMap("unknown@example.com", updates)) + .isInstanceOf(CustomException.class) + .hasFieldOrPropertyWithValue("errorCode", ErrorCode.USER_UPDATE_NOT_FOUND) + .satisfies(e -> { + log.warn("▶ 예외 메시지: {}", e.getMessage()); + log.info("✅ 예외가 정상적으로 처리되었습니다."); + }); + } + } +}