From c2e0e851b5b7d5aafaf659293cf51de468c0406c Mon Sep 17 00:00:00 2001 From: joyewon0705 <77885098+joyewon0705@users.noreply.github.com> Date: Fri, 26 Sep 2025 11:40:08 +0900 Subject: [PATCH 1/3] =?UTF-8?q?Ref:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20API=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/AuthController.java | 5 +- .../user/controller/AuthControllerDocs.java | 3 +- .../back/domain/user/dto/LoginResponse.java | 6 + .../back/domain/user/service/UserService.java | 15 +- .../user/controller/AuthControllerTest.java | 1116 ++++++++--------- .../domain/user/service/UserServiceTest.java | 766 +++++------ 6 files changed, 960 insertions(+), 951 deletions(-) create mode 100644 src/main/java/com/back/domain/user/dto/LoginResponse.java diff --git a/src/main/java/com/back/domain/user/controller/AuthController.java b/src/main/java/com/back/domain/user/controller/AuthController.java index 2323366c..7f0e49c9 100644 --- a/src/main/java/com/back/domain/user/controller/AuthController.java +++ b/src/main/java/com/back/domain/user/controller/AuthController.java @@ -1,6 +1,7 @@ package com.back.domain.user.controller; import com.back.domain.user.dto.LoginRequest; +import com.back.domain.user.dto.LoginResponse; import com.back.domain.user.dto.UserRegisterRequest; import com.back.domain.user.dto.UserResponse; import com.back.domain.user.service.UserService; @@ -40,11 +41,11 @@ public ResponseEntity> register( // 로그인 @PostMapping("/login") - public ResponseEntity> login( + public ResponseEntity> login( @Valid @RequestBody LoginRequest request, HttpServletResponse response ) { - UserResponse loginResponse = userService.login(request, response); + LoginResponse loginResponse = userService.login(request, response); return ResponseEntity .ok(RsData.success( "로그인에 성공했습니다.", diff --git a/src/main/java/com/back/domain/user/controller/AuthControllerDocs.java b/src/main/java/com/back/domain/user/controller/AuthControllerDocs.java index dbcd5423..e9826c21 100644 --- a/src/main/java/com/back/domain/user/controller/AuthControllerDocs.java +++ b/src/main/java/com/back/domain/user/controller/AuthControllerDocs.java @@ -1,6 +1,7 @@ package com.back.domain.user.controller; import com.back.domain.user.dto.LoginRequest; +import com.back.domain.user.dto.LoginResponse; import com.back.domain.user.dto.UserRegisterRequest; import com.back.domain.user.dto.UserResponse; import com.back.global.common.dto.RsData; @@ -228,7 +229,7 @@ ResponseEntity> register( ) ) }) - ResponseEntity> login( + ResponseEntity> login( @Valid @RequestBody LoginRequest request, HttpServletResponse response ); diff --git a/src/main/java/com/back/domain/user/dto/LoginResponse.java b/src/main/java/com/back/domain/user/dto/LoginResponse.java new file mode 100644 index 00000000..a92b45e7 --- /dev/null +++ b/src/main/java/com/back/domain/user/dto/LoginResponse.java @@ -0,0 +1,6 @@ +package com.back.domain.user.dto; + +public record LoginResponse( + String accessToken, + UserResponse user +) {} diff --git a/src/main/java/com/back/domain/user/service/UserService.java b/src/main/java/com/back/domain/user/service/UserService.java index bfc6dd93..7a9dc80e 100644 --- a/src/main/java/com/back/domain/user/service/UserService.java +++ b/src/main/java/com/back/domain/user/service/UserService.java @@ -1,6 +1,7 @@ package com.back.domain.user.service; import com.back.domain.user.dto.LoginRequest; +import com.back.domain.user.dto.LoginResponse; import com.back.domain.user.dto.UserRegisterRequest; import com.back.domain.user.dto.UserResponse; import com.back.domain.user.entity.User; @@ -87,9 +88,9 @@ public UserResponse register(UserRegisterRequest request) { * 2. 사용자 상태 검증 (PENDING, SUSPENDED, DELETED) * 3. Access/Refresh Token 발급 * 4. Refresh Token을 HttpOnly 쿠키로, Access Token은 헤더로 설정 - * 5. UserResponse 반환 + * 5. LoginResponse 반환 */ - public UserResponse login(LoginRequest request, HttpServletResponse response) { + public LoginResponse login(LoginRequest request, HttpServletResponse response) { // 사용자 조회 User user = userRepository.findByUsername(request.username()) .orElseThrow(() -> new CustomException(ErrorCode.INVALID_CREDENTIALS)); @@ -127,11 +128,11 @@ public UserResponse login(LoginRequest request, HttpServletResponse response) { "/api/auth" ); - // Access Token을 응답 헤더에 설정 - response.setHeader("Authorization", "Bearer " + accessToken); - - // UserResponse 반환 - return UserResponse.from(user, user.getUserProfile()); + // LoginResponse 반환 + return new LoginResponse( + accessToken, + UserResponse.from(user, user.getUserProfile()) + ); } /** diff --git a/src/test/java/com/back/domain/user/controller/AuthControllerTest.java b/src/test/java/com/back/domain/user/controller/AuthControllerTest.java index 3961d6be..4898c353 100644 --- a/src/test/java/com/back/domain/user/controller/AuthControllerTest.java +++ b/src/test/java/com/back/domain/user/controller/AuthControllerTest.java @@ -1,558 +1,558 @@ -package com.back.domain.user.controller; - -import com.back.domain.user.entity.User; -import com.back.domain.user.entity.UserProfile; -import com.back.domain.user.entity.UserStatus; -import com.back.domain.user.repository.UserRepository; -import com.back.fixture.TestJwtTokenProvider; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; -import jakarta.servlet.http.Cookie; -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.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.MediaType; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.ResultActions; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Date; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@SpringBootTest -@AutoConfigureMockMvc -@ActiveProfiles("test") -@Transactional -class AuthControllerTest { - - @Autowired - private MockMvc mvc; - - @Autowired - private UserRepository userRepository; - - @Autowired - private TestJwtTokenProvider testJwtTokenProvider; - - @Test - @DisplayName("정상 회원가입 → 201 Created") - void register_success() throws Exception { - // given: 정상적인 회원가입 요청 JSON - String body = """ - { - "username": "testuser", - "email": "test@example.com", - "password": "P@ssw0rd!", - "nickname": "홍길동" - } - """; - - // when: 회원가입 API 호출 - ResultActions resultActions = mvc.perform( - post("/api/auth/register") - .contentType(MediaType.APPLICATION_JSON) - .content(body) - ).andDo(print()); - - // then: 응답 값과 DB 저장값 검증 - resultActions - .andExpect(status().isCreated()) - .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.data.username").value("testuser")) - .andExpect(jsonPath("$.data.email").value("test@example.com")) - .andExpect(jsonPath("$.data.nickname").value("홍길동")); - - // DB에서 저장된 User 상태 검증 - User saved = userRepository.findByUsername("testuser").orElseThrow(); - // TODO: 이메일 인증 기능 개발 후 기본 상태를 PENDING으로 변경 -// assertThat(saved.getUserStatus()).isEqualTo(UserStatus.PENDING); - } - - @Test - @DisplayName("중복 username → 409 Conflict") - void register_duplicateUsername() throws Exception { - // given: 이미 존재하는 username을 가진 User 저장 - User existing = User.createUser("dupuser", "dup@example.com", "password123!"); - existing.setUserProfile(new UserProfile(existing, "dupnick", null, null, null, 0)); - userRepository.save(existing); - - // 동일 username으로 회원가입 요청 - String body = """ - { - "username": "dupuser", - "email": "other@example.com", - "password": "P@ssw0rd!", - "nickname": "다른닉네임" - } - """; - - // when & then: 409 Conflict 응답 및 에러 코드 확인 - mvc.perform(post("/api/auth/register") - .contentType(MediaType.APPLICATION_JSON) - .content(body)) - .andDo(print()) - .andExpect(status().isConflict()) - .andExpect(jsonPath("$.code").value("USER_002")); - } - - @Test - @DisplayName("중복 email → 409 Conflict") - void register_duplicateEmail() throws Exception { - // given: 이미 존재하는 email을 가진 User 저장 - User existing = User.createUser("user1", "dup@example.com", "password123!"); - existing.setUserProfile(new UserProfile(existing, "nick1", null, null, null, 0)); - userRepository.save(existing); - - // 동일 email로 회원가입 요청 - String body = """ - { - "username": "otheruser", - "email": "dup@example.com", - "password": "P@ssw0rd!", - "nickname": "다른닉네임" - } - """; - - // when & then: 409 Conflict 응답 및 에러 코드 확인 - mvc.perform(post("/api/auth/register") - .contentType(MediaType.APPLICATION_JSON) - .content(body)) - .andDo(print()) - .andExpect(status().isConflict()) - .andExpect(jsonPath("$.code").value("USER_003")); - } - - @Test - @DisplayName("중복 nickname → 409 Conflict") - void register_duplicateNickname() throws Exception { - // given: 이미 존재하는 nickname을 가진 User 저장 - User existing = User.createUser("user2", "user2@example.com", "password123!"); - existing.setUserProfile(new UserProfile(existing, "dupnick", null, null, null, 0)); - userRepository.save(existing); - - // 동일 nickname으로 회원가입 요청 - String body = """ - { - "username": "newuser", - "email": "new@example.com", - "password": "P@ssw0rd!", - "nickname": "dupnick" - } - """; - - // when & then: 409 Conflict 응답 및 에러 코드 확인 - mvc.perform(post("/api/auth/register") - .contentType(MediaType.APPLICATION_JSON) - .content(body)) - .andDo(print()) - .andExpect(status().isConflict()) - .andExpect(jsonPath("$.code").value("USER_004")); - } - - @Test - @DisplayName("비밀번호 정책 위반 → 400 Bad Request") - void register_invalidPassword() throws Exception { - // given: 숫자/특수문자 포함 안 된 약한 비밀번호 - String body = """ - { - "username": "weakpw", - "email": "weak@example.com", - "password": "password", - "nickname": "닉네임" - } - """; - - // when & then: 400 Bad Request 응답 및 에러 코드 확인 - mvc.perform(post("/api/auth/register") - .contentType(MediaType.APPLICATION_JSON) - .content(body)) - .andDo(print()) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.code").value("USER_005")); - } - - @Test - @DisplayName("잘못된 요청값 (필수 필드 누락) → 400 Bad Request") - void register_invalidRequest_missingField() throws Exception { - // given: 필수 값 누락 (username, password, nickname 비어있음 / email 형식 잘못됨) - String body = """ - { - "username": "", - "email": "invalid", - "password": "", - "nickname": "" - } - """; - - // when & then: 400 Bad Request 응답 및 공통 에러 코드 확인 - mvc.perform(post("/api/auth/register") - .contentType(MediaType.APPLICATION_JSON) - .content(body)) - .andDo(print()) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.code").value("COMMON_400")); - } - - @Test - @DisplayName("정상 로그인 → 200 OK + Authorization 헤더 + refreshToken 쿠키") - void login_success() throws Exception { - // given: 회원가입 요청으로 DB에 정상 유저 저장 - String rawPassword = "P@ssw0rd!"; - String registerBody = """ - { - "username": "loginuser", - "email": "login@example.com", - "password": "%s", - "nickname": "홍길동" - } - """.formatted(rawPassword); - - mvc.perform(post("/api/auth/register") - .contentType(MediaType.APPLICATION_JSON) - .content(registerBody)) - .andExpect(status().isCreated()); - - // when: 로그인 요청 - String loginBody = """ - { - "username": "loginuser", - "password": "%s" - } - """.formatted(rawPassword); - - ResultActions resultActions = mvc.perform(post("/api/auth/login") - .contentType(MediaType.APPLICATION_JSON) - .content(loginBody)) - .andDo(print()); - - // then: 200 OK 응답 + username/Authorization 헤더/refreshToken 쿠키 확인 - resultActions - .andExpect(status().isOk()) - .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.data.username").value("loginuser")) - .andExpect(header().exists("Authorization")) - .andExpect(cookie().exists("refreshToken")); - } - - @Test - @DisplayName("잘못된 비밀번호 → 401 Unauthorized") - void login_invalidPassword() throws Exception { - // given: 정상 유저를 회원가입으로 저장 - String rawPassword = "P@ssw0rd!"; - String registerBody = """ - { - "username": "badpwuser", - "email": "badpw@example.com", - "password": "%s", - "nickname": "닉네임" - } - """.formatted(rawPassword); - mvc.perform(post("/api/auth/register") - .contentType(MediaType.APPLICATION_JSON) - .content(registerBody)); - - // when: 틀린 비밀번호로 로그인 요청 - String loginBody = """ - { - "username": "badpwuser", - "password": "WrongPass!" - } - """; - - // then: 401 Unauthorized 응답 + 에러 코드 USER_006 확인 - mvc.perform(post("/api/auth/login") - .contentType(MediaType.APPLICATION_JSON) - .content(loginBody)) - .andDo(print()) - .andExpect(status().isUnauthorized()) - .andExpect(jsonPath("$.code").value("USER_006")); - } - - @Test - @DisplayName("존재하지 않는 username → 401 Unauthorized") - void login_userNotFound() throws Exception { - // given: 존재하지 않는 username 사용 - String loginBody = """ - { - "username": "nouser", - "password": "P@ssw0rd!" - } - """; - - // when & then: 401 Unauthorized 응답 + 에러 코드 USER_006 확인 - mvc.perform(post("/api/auth/login") - .contentType(MediaType.APPLICATION_JSON) - .content(loginBody)) - .andDo(print()) - .andExpect(status().isUnauthorized()) - .andExpect(jsonPath("$.code").value("USER_006")); - } - - @Autowired - private PasswordEncoder passwordEncoder; - - @Test - @DisplayName("이메일 미인증(PENDING) 계정 로그인 → 403 Forbidden") - void login_pendingUser() throws Exception { - // given: 상태가 PENDING인 유저 저장 (비밀번호 인코딩 필수) - User pending = User.createUser("pending", "pending@example.com", - passwordEncoder.encode("P@ssw0rd!")); - pending.setUserProfile(new UserProfile(pending, "닉네임", null, null, null, 0)); - pending.setUserStatus(UserStatus.PENDING); - userRepository.save(pending); - - String body = """ - { - "username": "pending", - "password": "P@ssw0rd!" - } - """; - - // when & then: 403 Forbidden 응답 + 에러 코드 USER_007 확인 - mvc.perform(post("/api/auth/login") - .contentType(MediaType.APPLICATION_JSON) - .content(body)) - .andDo(print()) - .andExpect(status().isForbidden()) - .andExpect(jsonPath("$.code").value("USER_007")); - } - - @Test - @DisplayName("정지된 계정(SUSPENDED) 로그인 → 403 Forbidden") - void login_suspendedUser() throws Exception { - // given: 상태가 SUSPENDED인 유저 저장 - User suspended = User.createUser("suspended", "suspended@example.com", - passwordEncoder.encode("P@ssw0rd!")); - suspended.setUserProfile(new UserProfile(suspended, "닉네임", null, null, null, 0)); - suspended.setUserStatus(UserStatus.SUSPENDED); - userRepository.save(suspended); - - String body = """ - { - "username": "suspended", - "password": "P@ssw0rd!" - } - """; - - // when & then: 403 Forbidden 응답 + 에러 코드 USER_008 확인 - mvc.perform(post("/api/auth/login") - .contentType(MediaType.APPLICATION_JSON) - .content(body)) - .andDo(print()) - .andExpect(status().isForbidden()) - .andExpect(jsonPath("$.code").value("USER_008")); - } - - @Test - @DisplayName("탈퇴한 계정(DELETED) 로그인 → 410 Gone") - void login_deletedUser() throws Exception { - // given: 상태가 DELETED인 유저 저장 - User deleted = User.createUser("deleted", "deleted@example.com", - passwordEncoder.encode("P@ssw0rd!")); - deleted.setUserProfile(new UserProfile(deleted, "닉네임", null, null, null, 0)); - deleted.setUserStatus(UserStatus.DELETED); - userRepository.save(deleted); - - String body = """ - { - "username": "deleted", - "password": "P@ssw0rd!" - } - """; - - // when & then: 410 Gone 응답 + 에러 코드 USER_009 확인 - mvc.perform(post("/api/auth/login") - .contentType(MediaType.APPLICATION_JSON) - .content(body)) - .andDo(print()) - .andExpect(status().isGone()) - .andExpect(jsonPath("$.code").value("USER_009")); - } - - @Test - @DisplayName("정상 로그아웃 → 200 OK + RefreshToken 쿠키 만료") - void logout_success() throws Exception { - // given: 회원가입 + 로그인으로 refreshToken 쿠키 확보 - String rawPassword = "P@ssw0rd!"; - String registerBody = """ - { - "username": "logoutuser", - "email": "logout@example.com", - "password": "%s", - "nickname": "홍길동" - } - """.formatted(rawPassword); - - mvc.perform(post("/api/auth/register") - .contentType(MediaType.APPLICATION_JSON) - .content(registerBody)) - .andExpect(status().isCreated()); - - String loginBody = """ - { - "username": "logoutuser", - "password": "%s" - } - """.formatted(rawPassword); - - ResultActions loginResult = mvc.perform(post("/api/auth/login") - .contentType(MediaType.APPLICATION_JSON) - .content(loginBody)) - .andExpect(status().isOk()); - - // 로그인 응답에서 refreshToken 쿠키 추출 - String refreshCookie = loginResult.andReturn() - .getResponse() - .getCookie("refreshToken") - .getValue(); - - // when: 로그아웃 요청 (쿠키 포함) - ResultActions logoutResult = mvc.perform(post("/api/auth/logout") - .cookie(new Cookie("refreshToken", refreshCookie))) - .andDo(print()); - - // then: 200 OK + 성공 메시지 + 쿠키 만료 - logoutResult - .andExpect(status().isOk()) - .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.message").value("로그아웃 되었습니다.")) - .andExpect(cookie().maxAge("refreshToken", 0)); - } - - @Test - @DisplayName("RefreshToken 누락 → 400 Bad Request") - void logout_noToken() throws Exception { - // when & then - mvc.perform(post("/api/auth/logout")) - .andDo(print()) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.code").value("COMMON_400")); - } - - @Test - @DisplayName("유효하지 않은 RefreshToken → 401 Unauthorized") - void logout_invalidToken() throws Exception { - // given: 잘못된 refreshToken 쿠키 - Cookie invalid = new Cookie("refreshToken", "fake-token"); - - // when & then - mvc.perform(post("/api/auth/logout").cookie(invalid)) - .andDo(print()) - .andExpect(status().isUnauthorized()) - .andExpect(jsonPath("$.code").value("AUTH_401")); - } - - @Test - @DisplayName("정상 토큰 재발급 → 200 OK + 새로운 AccessToken 반환") - void refreshToken_success() throws Exception { - // given: 회원가입 + 로그인 → 기존 토큰 확보 - String rawPassword = "P@ssw0rd!"; - String registerBody = """ - { - "username": "refreshuser", - "email": "refresh@example.com", - "password": "%s", - "nickname": "홍길동" - } - """.formatted(rawPassword); - - mvc.perform(post("/api/auth/register") - .contentType(MediaType.APPLICATION_JSON) - .content(registerBody)) - .andExpect(status().isCreated()); - - String loginBody = """ - { - "username": "refreshuser", - "password": "%s" - } - """.formatted(rawPassword); - - ResultActions loginResult = mvc.perform(post("/api/auth/login") - .contentType(MediaType.APPLICATION_JSON) - .content(loginBody)) - .andExpect(status().isOk()); - - // 기존 AccessToken, RefreshToken 확보 - String oldAccessToken = loginResult.andReturn() - .getResponse() - .getHeader("Authorization") - .substring(7); // "Bearer " 제거 - String refreshCookie = loginResult.andReturn() - .getResponse() - .getCookie("refreshToken") - .getValue(); - - // Issued At(발급 시간) 분리를 위해 1초 대기 -// Thread.sleep(1000); - - // when: 재발급 요청 (RefreshToken 쿠키 포함) - ResultActions refreshResult = mvc.perform(post("/api/auth/refresh") - .cookie(new Cookie("refreshToken", refreshCookie))) - .andDo(print()); - - // then: 200 OK + 새로운 AccessToken 발급 - refreshResult - .andExpect(status().isOk()) - .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.data.accessToken").exists()) - .andExpect(header().exists("Authorization")); - - String newAccessToken = refreshResult.andReturn() - .getResponse() - .getHeader("Authorization") - .substring(7); - - // 새 토큰은 기존 토큰과 달라야 함 -// assertThat(newAccessToken).isNotEqualTo(oldAccessToken); - } - - @Test - @DisplayName("RefreshToken 누락 → 400 Bad Request") - void refreshToken_noToken() throws Exception { - mvc.perform(post("/api/auth/refresh")) - .andDo(print()) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.code").value("COMMON_400")); - } - - @Test - @DisplayName("유효하지 않은 RefreshToken → 401 Unauthorized") - void refreshToken_invalidToken() throws Exception { - Cookie invalid = new Cookie("refreshToken", "fake-token"); - - mvc.perform(post("/api/auth/refresh").cookie(invalid)) - .andDo(print()) - .andExpect(status().isUnauthorized()) - .andExpect(jsonPath("$.code").value("AUTH_401")); - } - - @Test - @DisplayName("만료된 RefreshToken → 401 Unauthorized") - void refreshToken_expiredToken() throws Exception { - // given: 이미 만료된 Refresh Token 발급 - User user = User.createUser("expiredUser", "expired@example.com", passwordEncoder.encode("P@ssw0rd!")); - user.setUserProfile(new UserProfile(user, "닉네임", null, null, null, 0)); - userRepository.save(user); - - // JwtTokenProvider에 테스트용 메서드 추가 필요 - String expiredRefreshToken = testJwtTokenProvider.createExpiredRefreshToken(user.getId()); - - Cookie expiredCookie = new Cookie("refreshToken", expiredRefreshToken); - - // when & then - mvc.perform(post("/api/auth/refresh").cookie(expiredCookie)) - .andDo(print()) - .andExpect(status().isUnauthorized()) - .andExpect(jsonPath("$.code").value("AUTH_401")) - .andExpect(jsonPath("$.message").value("만료된 리프레시 토큰입니다.")); - } -} \ No newline at end of file +//package com.back.domain.user.controller; +// +//import com.back.domain.user.entity.User; +//import com.back.domain.user.entity.UserProfile; +//import com.back.domain.user.entity.UserStatus; +//import com.back.domain.user.repository.UserRepository; +//import com.back.fixture.TestJwtTokenProvider; +//import io.jsonwebtoken.Jwts; +//import io.jsonwebtoken.SignatureAlgorithm; +//import jakarta.servlet.http.Cookie; +//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.web.servlet.AutoConfigureMockMvc; +//import org.springframework.boot.test.context.SpringBootTest; +//import org.springframework.http.MediaType; +//import org.springframework.security.crypto.password.PasswordEncoder; +//import org.springframework.test.context.ActiveProfiles; +//import org.springframework.test.web.servlet.MockMvc; +//import org.springframework.test.web.servlet.ResultActions; +//import org.springframework.transaction.annotation.Transactional; +// +//import java.util.Date; +// +//import static org.assertj.core.api.Assertions.assertThat; +//import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +//import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +//import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +// +//@SpringBootTest +//@AutoConfigureMockMvc +//@ActiveProfiles("test") +//@Transactional +//class AuthControllerTest { +// +// @Autowired +// private MockMvc mvc; +// +// @Autowired +// private UserRepository userRepository; +// +// @Autowired +// private TestJwtTokenProvider testJwtTokenProvider; +// +// @Test +// @DisplayName("정상 회원가입 → 201 Created") +// void register_success() throws Exception { +// // given: 정상적인 회원가입 요청 JSON +// String body = """ +// { +// "username": "testuser", +// "email": "test@example.com", +// "password": "P@ssw0rd!", +// "nickname": "홍길동" +// } +// """; +// +// // when: 회원가입 API 호출 +// ResultActions resultActions = mvc.perform( +// post("/api/auth/register") +// .contentType(MediaType.APPLICATION_JSON) +// .content(body) +// ).andDo(print()); +// +// // then: 응답 값과 DB 저장값 검증 +// resultActions +// .andExpect(status().isCreated()) +// .andExpect(jsonPath("$.success").value(true)) +// .andExpect(jsonPath("$.data.username").value("testuser")) +// .andExpect(jsonPath("$.data.email").value("test@example.com")) +// .andExpect(jsonPath("$.data.nickname").value("홍길동")); +// +// // DB에서 저장된 User 상태 검증 +// User saved = userRepository.findByUsername("testuser").orElseThrow(); +// // TODO: 이메일 인증 기능 개발 후 기본 상태를 PENDING으로 변경 +//// assertThat(saved.getUserStatus()).isEqualTo(UserStatus.PENDING); +// } +// +// @Test +// @DisplayName("중복 username → 409 Conflict") +// void register_duplicateUsername() throws Exception { +// // given: 이미 존재하는 username을 가진 User 저장 +// User existing = User.createUser("dupuser", "dup@example.com", "password123!"); +// existing.setUserProfile(new UserProfile(existing, "dupnick", null, null, null, 0)); +// userRepository.save(existing); +// +// // 동일 username으로 회원가입 요청 +// String body = """ +// { +// "username": "dupuser", +// "email": "other@example.com", +// "password": "P@ssw0rd!", +// "nickname": "다른닉네임" +// } +// """; +// +// // when & then: 409 Conflict 응답 및 에러 코드 확인 +// mvc.perform(post("/api/auth/register") +// .contentType(MediaType.APPLICATION_JSON) +// .content(body)) +// .andDo(print()) +// .andExpect(status().isConflict()) +// .andExpect(jsonPath("$.code").value("USER_002")); +// } +// +// @Test +// @DisplayName("중복 email → 409 Conflict") +// void register_duplicateEmail() throws Exception { +// // given: 이미 존재하는 email을 가진 User 저장 +// User existing = User.createUser("user1", "dup@example.com", "password123!"); +// existing.setUserProfile(new UserProfile(existing, "nick1", null, null, null, 0)); +// userRepository.save(existing); +// +// // 동일 email로 회원가입 요청 +// String body = """ +// { +// "username": "otheruser", +// "email": "dup@example.com", +// "password": "P@ssw0rd!", +// "nickname": "다른닉네임" +// } +// """; +// +// // when & then: 409 Conflict 응답 및 에러 코드 확인 +// mvc.perform(post("/api/auth/register") +// .contentType(MediaType.APPLICATION_JSON) +// .content(body)) +// .andDo(print()) +// .andExpect(status().isConflict()) +// .andExpect(jsonPath("$.code").value("USER_003")); +// } +// +// @Test +// @DisplayName("중복 nickname → 409 Conflict") +// void register_duplicateNickname() throws Exception { +// // given: 이미 존재하는 nickname을 가진 User 저장 +// User existing = User.createUser("user2", "user2@example.com", "password123!"); +// existing.setUserProfile(new UserProfile(existing, "dupnick", null, null, null, 0)); +// userRepository.save(existing); +// +// // 동일 nickname으로 회원가입 요청 +// String body = """ +// { +// "username": "newuser", +// "email": "new@example.com", +// "password": "P@ssw0rd!", +// "nickname": "dupnick" +// } +// """; +// +// // when & then: 409 Conflict 응답 및 에러 코드 확인 +// mvc.perform(post("/api/auth/register") +// .contentType(MediaType.APPLICATION_JSON) +// .content(body)) +// .andDo(print()) +// .andExpect(status().isConflict()) +// .andExpect(jsonPath("$.code").value("USER_004")); +// } +// +// @Test +// @DisplayName("비밀번호 정책 위반 → 400 Bad Request") +// void register_invalidPassword() throws Exception { +// // given: 숫자/특수문자 포함 안 된 약한 비밀번호 +// String body = """ +// { +// "username": "weakpw", +// "email": "weak@example.com", +// "password": "password", +// "nickname": "닉네임" +// } +// """; +// +// // when & then: 400 Bad Request 응답 및 에러 코드 확인 +// mvc.perform(post("/api/auth/register") +// .contentType(MediaType.APPLICATION_JSON) +// .content(body)) +// .andDo(print()) +// .andExpect(status().isBadRequest()) +// .andExpect(jsonPath("$.code").value("USER_005")); +// } +// +// @Test +// @DisplayName("잘못된 요청값 (필수 필드 누락) → 400 Bad Request") +// void register_invalidRequest_missingField() throws Exception { +// // given: 필수 값 누락 (username, password, nickname 비어있음 / email 형식 잘못됨) +// String body = """ +// { +// "username": "", +// "email": "invalid", +// "password": "", +// "nickname": "" +// } +// """; +// +// // when & then: 400 Bad Request 응답 및 공통 에러 코드 확인 +// mvc.perform(post("/api/auth/register") +// .contentType(MediaType.APPLICATION_JSON) +// .content(body)) +// .andDo(print()) +// .andExpect(status().isBadRequest()) +// .andExpect(jsonPath("$.code").value("COMMON_400")); +// } +// +// @Test +// @DisplayName("정상 로그인 → 200 OK + Authorization 헤더 + refreshToken 쿠키") +// void login_success() throws Exception { +// // given: 회원가입 요청으로 DB에 정상 유저 저장 +// String rawPassword = "P@ssw0rd!"; +// String registerBody = """ +// { +// "username": "loginuser", +// "email": "login@example.com", +// "password": "%s", +// "nickname": "홍길동" +// } +// """.formatted(rawPassword); +// +// mvc.perform(post("/api/auth/register") +// .contentType(MediaType.APPLICATION_JSON) +// .content(registerBody)) +// .andExpect(status().isCreated()); +// +// // when: 로그인 요청 +// String loginBody = """ +// { +// "username": "loginuser", +// "password": "%s" +// } +// """.formatted(rawPassword); +// +// ResultActions resultActions = mvc.perform(post("/api/auth/login") +// .contentType(MediaType.APPLICATION_JSON) +// .content(loginBody)) +// .andDo(print()); +// +// // then: 200 OK 응답 + username/Authorization 헤더/refreshToken 쿠키 확인 +// resultActions +// .andExpect(status().isOk()) +// .andExpect(jsonPath("$.success").value(true)) +// .andExpect(jsonPath("$.data.username").value("loginuser")) +// .andExpect(header().exists("Authorization")) +// .andExpect(cookie().exists("refreshToken")); +// } +// +// @Test +// @DisplayName("잘못된 비밀번호 → 401 Unauthorized") +// void login_invalidPassword() throws Exception { +// // given: 정상 유저를 회원가입으로 저장 +// String rawPassword = "P@ssw0rd!"; +// String registerBody = """ +// { +// "username": "badpwuser", +// "email": "badpw@example.com", +// "password": "%s", +// "nickname": "닉네임" +// } +// """.formatted(rawPassword); +// mvc.perform(post("/api/auth/register") +// .contentType(MediaType.APPLICATION_JSON) +// .content(registerBody)); +// +// // when: 틀린 비밀번호로 로그인 요청 +// String loginBody = """ +// { +// "username": "badpwuser", +// "password": "WrongPass!" +// } +// """; +// +// // then: 401 Unauthorized 응답 + 에러 코드 USER_006 확인 +// mvc.perform(post("/api/auth/login") +// .contentType(MediaType.APPLICATION_JSON) +// .content(loginBody)) +// .andDo(print()) +// .andExpect(status().isUnauthorized()) +// .andExpect(jsonPath("$.code").value("USER_006")); +// } +// +// @Test +// @DisplayName("존재하지 않는 username → 401 Unauthorized") +// void login_userNotFound() throws Exception { +// // given: 존재하지 않는 username 사용 +// String loginBody = """ +// { +// "username": "nouser", +// "password": "P@ssw0rd!" +// } +// """; +// +// // when & then: 401 Unauthorized 응답 + 에러 코드 USER_006 확인 +// mvc.perform(post("/api/auth/login") +// .contentType(MediaType.APPLICATION_JSON) +// .content(loginBody)) +// .andDo(print()) +// .andExpect(status().isUnauthorized()) +// .andExpect(jsonPath("$.code").value("USER_006")); +// } +// +// @Autowired +// private PasswordEncoder passwordEncoder; +// +// @Test +// @DisplayName("이메일 미인증(PENDING) 계정 로그인 → 403 Forbidden") +// void login_pendingUser() throws Exception { +// // given: 상태가 PENDING인 유저 저장 (비밀번호 인코딩 필수) +// User pending = User.createUser("pending", "pending@example.com", +// passwordEncoder.encode("P@ssw0rd!")); +// pending.setUserProfile(new UserProfile(pending, "닉네임", null, null, null, 0)); +// pending.setUserStatus(UserStatus.PENDING); +// userRepository.save(pending); +// +// String body = """ +// { +// "username": "pending", +// "password": "P@ssw0rd!" +// } +// """; +// +// // when & then: 403 Forbidden 응답 + 에러 코드 USER_007 확인 +// mvc.perform(post("/api/auth/login") +// .contentType(MediaType.APPLICATION_JSON) +// .content(body)) +// .andDo(print()) +// .andExpect(status().isForbidden()) +// .andExpect(jsonPath("$.code").value("USER_007")); +// } +// +// @Test +// @DisplayName("정지된 계정(SUSPENDED) 로그인 → 403 Forbidden") +// void login_suspendedUser() throws Exception { +// // given: 상태가 SUSPENDED인 유저 저장 +// User suspended = User.createUser("suspended", "suspended@example.com", +// passwordEncoder.encode("P@ssw0rd!")); +// suspended.setUserProfile(new UserProfile(suspended, "닉네임", null, null, null, 0)); +// suspended.setUserStatus(UserStatus.SUSPENDED); +// userRepository.save(suspended); +// +// String body = """ +// { +// "username": "suspended", +// "password": "P@ssw0rd!" +// } +// """; +// +// // when & then: 403 Forbidden 응답 + 에러 코드 USER_008 확인 +// mvc.perform(post("/api/auth/login") +// .contentType(MediaType.APPLICATION_JSON) +// .content(body)) +// .andDo(print()) +// .andExpect(status().isForbidden()) +// .andExpect(jsonPath("$.code").value("USER_008")); +// } +// +// @Test +// @DisplayName("탈퇴한 계정(DELETED) 로그인 → 410 Gone") +// void login_deletedUser() throws Exception { +// // given: 상태가 DELETED인 유저 저장 +// User deleted = User.createUser("deleted", "deleted@example.com", +// passwordEncoder.encode("P@ssw0rd!")); +// deleted.setUserProfile(new UserProfile(deleted, "닉네임", null, null, null, 0)); +// deleted.setUserStatus(UserStatus.DELETED); +// userRepository.save(deleted); +// +// String body = """ +// { +// "username": "deleted", +// "password": "P@ssw0rd!" +// } +// """; +// +// // when & then: 410 Gone 응답 + 에러 코드 USER_009 확인 +// mvc.perform(post("/api/auth/login") +// .contentType(MediaType.APPLICATION_JSON) +// .content(body)) +// .andDo(print()) +// .andExpect(status().isGone()) +// .andExpect(jsonPath("$.code").value("USER_009")); +// } +// +// @Test +// @DisplayName("정상 로그아웃 → 200 OK + RefreshToken 쿠키 만료") +// void logout_success() throws Exception { +// // given: 회원가입 + 로그인으로 refreshToken 쿠키 확보 +// String rawPassword = "P@ssw0rd!"; +// String registerBody = """ +// { +// "username": "logoutuser", +// "email": "logout@example.com", +// "password": "%s", +// "nickname": "홍길동" +// } +// """.formatted(rawPassword); +// +// mvc.perform(post("/api/auth/register") +// .contentType(MediaType.APPLICATION_JSON) +// .content(registerBody)) +// .andExpect(status().isCreated()); +// +// String loginBody = """ +// { +// "username": "logoutuser", +// "password": "%s" +// } +// """.formatted(rawPassword); +// +// ResultActions loginResult = mvc.perform(post("/api/auth/login") +// .contentType(MediaType.APPLICATION_JSON) +// .content(loginBody)) +// .andExpect(status().isOk()); +// +// // 로그인 응답에서 refreshToken 쿠키 추출 +// String refreshCookie = loginResult.andReturn() +// .getResponse() +// .getCookie("refreshToken") +// .getValue(); +// +// // when: 로그아웃 요청 (쿠키 포함) +// ResultActions logoutResult = mvc.perform(post("/api/auth/logout") +// .cookie(new Cookie("refreshToken", refreshCookie))) +// .andDo(print()); +// +// // then: 200 OK + 성공 메시지 + 쿠키 만료 +// logoutResult +// .andExpect(status().isOk()) +// .andExpect(jsonPath("$.success").value(true)) +// .andExpect(jsonPath("$.message").value("로그아웃 되었습니다.")) +// .andExpect(cookie().maxAge("refreshToken", 0)); +// } +// +// @Test +// @DisplayName("RefreshToken 누락 → 400 Bad Request") +// void logout_noToken() throws Exception { +// // when & then +// mvc.perform(post("/api/auth/logout")) +// .andDo(print()) +// .andExpect(status().isBadRequest()) +// .andExpect(jsonPath("$.code").value("COMMON_400")); +// } +// +// @Test +// @DisplayName("유효하지 않은 RefreshToken → 401 Unauthorized") +// void logout_invalidToken() throws Exception { +// // given: 잘못된 refreshToken 쿠키 +// Cookie invalid = new Cookie("refreshToken", "fake-token"); +// +// // when & then +// mvc.perform(post("/api/auth/logout").cookie(invalid)) +// .andDo(print()) +// .andExpect(status().isUnauthorized()) +// .andExpect(jsonPath("$.code").value("AUTH_401")); +// } +// +// @Test +// @DisplayName("정상 토큰 재발급 → 200 OK + 새로운 AccessToken 반환") +// void refreshToken_success() throws Exception { +// // given: 회원가입 + 로그인 → 기존 토큰 확보 +// String rawPassword = "P@ssw0rd!"; +// String registerBody = """ +// { +// "username": "refreshuser", +// "email": "refresh@example.com", +// "password": "%s", +// "nickname": "홍길동" +// } +// """.formatted(rawPassword); +// +// mvc.perform(post("/api/auth/register") +// .contentType(MediaType.APPLICATION_JSON) +// .content(registerBody)) +// .andExpect(status().isCreated()); +// +// String loginBody = """ +// { +// "username": "refreshuser", +// "password": "%s" +// } +// """.formatted(rawPassword); +// +// ResultActions loginResult = mvc.perform(post("/api/auth/login") +// .contentType(MediaType.APPLICATION_JSON) +// .content(loginBody)) +// .andExpect(status().isOk()); +// +// // 기존 AccessToken, RefreshToken 확보 +// String oldAccessToken = loginResult.andReturn() +// .getResponse() +// .getHeader("Authorization") +// .substring(7); // "Bearer " 제거 +// String refreshCookie = loginResult.andReturn() +// .getResponse() +// .getCookie("refreshToken") +// .getValue(); +// +// // Issued At(발급 시간) 분리를 위해 1초 대기 +//// Thread.sleep(1000); +// +// // when: 재발급 요청 (RefreshToken 쿠키 포함) +// ResultActions refreshResult = mvc.perform(post("/api/auth/refresh") +// .cookie(new Cookie("refreshToken", refreshCookie))) +// .andDo(print()); +// +// // then: 200 OK + 새로운 AccessToken 발급 +// refreshResult +// .andExpect(status().isOk()) +// .andExpect(jsonPath("$.success").value(true)) +// .andExpect(jsonPath("$.data.accessToken").exists()) +// .andExpect(header().exists("Authorization")); +// +// String newAccessToken = refreshResult.andReturn() +// .getResponse() +// .getHeader("Authorization") +// .substring(7); +// +// // 새 토큰은 기존 토큰과 달라야 함 +//// assertThat(newAccessToken).isNotEqualTo(oldAccessToken); +// } +// +// @Test +// @DisplayName("RefreshToken 누락 → 400 Bad Request") +// void refreshToken_noToken() throws Exception { +// mvc.perform(post("/api/auth/refresh")) +// .andDo(print()) +// .andExpect(status().isBadRequest()) +// .andExpect(jsonPath("$.code").value("COMMON_400")); +// } +// +// @Test +// @DisplayName("유효하지 않은 RefreshToken → 401 Unauthorized") +// void refreshToken_invalidToken() throws Exception { +// Cookie invalid = new Cookie("refreshToken", "fake-token"); +// +// mvc.perform(post("/api/auth/refresh").cookie(invalid)) +// .andDo(print()) +// .andExpect(status().isUnauthorized()) +// .andExpect(jsonPath("$.code").value("AUTH_401")); +// } +// +// @Test +// @DisplayName("만료된 RefreshToken → 401 Unauthorized") +// void refreshToken_expiredToken() throws Exception { +// // given: 이미 만료된 Refresh Token 발급 +// User user = User.createUser("expiredUser", "expired@example.com", passwordEncoder.encode("P@ssw0rd!")); +// user.setUserProfile(new UserProfile(user, "닉네임", null, null, null, 0)); +// userRepository.save(user); +// +// // JwtTokenProvider에 테스트용 메서드 추가 필요 +// String expiredRefreshToken = testJwtTokenProvider.createExpiredRefreshToken(user.getId()); +// +// Cookie expiredCookie = new Cookie("refreshToken", expiredRefreshToken); +// +// // when & then +// mvc.perform(post("/api/auth/refresh").cookie(expiredCookie)) +// .andDo(print()) +// .andExpect(status().isUnauthorized()) +// .andExpect(jsonPath("$.code").value("AUTH_401")) +// .andExpect(jsonPath("$.message").value("만료된 리프레시 토큰입니다.")); +// } +//} \ No newline at end of file diff --git a/src/test/java/com/back/domain/user/service/UserServiceTest.java b/src/test/java/com/back/domain/user/service/UserServiceTest.java index 7ee8d18f..a0a2cfda 100644 --- a/src/test/java/com/back/domain/user/service/UserServiceTest.java +++ b/src/test/java/com/back/domain/user/service/UserServiceTest.java @@ -1,383 +1,383 @@ -package com.back.domain.user.service; - -import com.back.domain.user.dto.LoginRequest; -import com.back.domain.user.dto.UserRegisterRequest; -import com.back.domain.user.dto.UserResponse; -import com.back.domain.user.entity.User; -import com.back.domain.user.entity.UserStatus; -import com.back.domain.user.repository.UserProfileRepository; -import com.back.domain.user.repository.UserRepository; -import com.back.domain.user.repository.UserTokenRepository; -import com.back.global.exception.CustomException; -import com.back.global.exception.ErrorCode; -import jakarta.servlet.http.Cookie; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.transaction.annotation.Transactional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -@SpringBootTest -@Transactional -@ActiveProfiles("test") -class UserServiceTest { - - @Autowired - private UserService userService; - - @Autowired - private UserRepository userRepository; - - @Autowired - private UserProfileRepository userProfileRepository; - - @Autowired - private UserTokenRepository userTokenRepository; - - @Autowired - private PasswordEncoder passwordEncoder; - - private User setupUser(String username, String email, String password, String nickname, UserStatus status) { - UserRegisterRequest request = new UserRegisterRequest(username, email, password, nickname); - UserResponse response = userService.register(request); - - User saved = userRepository.findById(response.userId()).orElseThrow(); - saved.setUserStatus(status); // 상태 변경 (PENDING, SUSPENDED, DELETED) - return saved; - } - - @Test - @DisplayName("정상 회원가입 성공") - void register_success() { - // given: 정상적인 회원가입 요청 생성 - UserRegisterRequest request = new UserRegisterRequest( - "testuser", "test@example.com", "P@ssw0rd!", "홍길동" - ); - - // when: 회원가입 실행 - UserResponse response = userService.register(request); - - // then: 반환된 값 검증 - assertThat(response.username()).isEqualTo("testuser"); - assertThat(response.email()).isEqualTo("test@example.com"); - assertThat(response.nickname()).isEqualTo("홍길동"); - // TODO: 이메일 인증 기능 개발 후 기본 상태를 PENDING으로 변경 -// assertThat(response.status()).isEqualTo(UserStatus.PENDING); - - // 비밀번호 인코딩 검증 - String encoded = userRepository.findById(response.userId()).get().getPassword(); - assertThat(passwordEncoder.matches("P@ssw0rd!", encoded)).isTrue(); - - // UserProfile도 함께 생성되었는지 확인 - assertThat(userProfileRepository.existsByNickname("홍길동")).isTrue(); - } - - @Test - @DisplayName("중복된 username이면 예외 발생") - void register_duplicateUsername() { - // given: 동일 username으로 첫 번째 가입 - userService.register(new UserRegisterRequest( - "dupuser", "dup@example.com", "P@ssw0rd!", "닉네임" - )); - - // when & then: 같은 username으로 가입 시 예외 발생 - assertThatThrownBy(() -> - userService.register(new UserRegisterRequest( - "dupuser", "other@example.com", "P@ssw0rd!", "다른닉네임" - )) - ).isInstanceOf(CustomException.class) - .hasMessage(ErrorCode.USERNAME_DUPLICATED.getMessage()); - } - - @Test - @DisplayName("중복된 email이면 예외 발생") - void register_duplicateEmail() { - // given: 동일 email로 첫 번째 가입 - userService.register(new UserRegisterRequest( - "user1", "dup@example.com", "P@ssw0rd!", "닉네임" - )); - - // when & then: 같은 email로 가입 시 예외 발생 - assertThatThrownBy(() -> - userService.register(new UserRegisterRequest( - "user2", "dup@example.com", "P@ssw0rd!", "다른닉네임" - )) - ).isInstanceOf(CustomException.class) - .hasMessage(ErrorCode.EMAIL_DUPLICATED.getMessage()); - } - - @Test - @DisplayName("중복된 nickname이면 예외 발생") - void register_duplicateNickname() { - // given: 동일 nickname으로 첫 번째 가입 - userService.register(new UserRegisterRequest( - "user1", "user1@example.com", "P@ssw0rd!", "dupnick" - )); - - // when & then: 같은 nickname으로 가입 시 예외 발생 - assertThatThrownBy(() -> - userService.register(new UserRegisterRequest( - "user2", "user2@example.com", "P@ssw0rd!", "dupnick" - )) - ).isInstanceOf(CustomException.class) - .hasMessage(ErrorCode.NICKNAME_DUPLICATED.getMessage()); - } - - @Test - @DisplayName("비밀번호 정책 위반(숫자/특수문자 없음) → 예외 발생") - void register_invalidPassword_noNumberOrSpecial() { - // given: 숫자, 특수문자 없는 비밀번호 - UserRegisterRequest request = new UserRegisterRequest( - "user1", "user1@example.com", "abcdefgh", "닉네임" - ); - - // when & then: 정책 위반으로 예외 발생 - assertThatThrownBy(() -> userService.register(request)) - .isInstanceOf(CustomException.class) - .hasMessage(ErrorCode.INVALID_PASSWORD.getMessage()); - } - - @Test - @DisplayName("비밀번호 정책 위반(길이 7자) → 예외 발생") - void register_invalidPassword_tooShort() { - // given: 7자리 비밀번호 (정책상 8자 이상 필요) - UserRegisterRequest request = new UserRegisterRequest( - "user2", "user2@example.com", "Abc12!", "닉네임" - ); - - // when & then: 정책 위반으로 예외 발생 - assertThatThrownBy(() -> userService.register(request)) - .isInstanceOf(CustomException.class) - .hasMessage(ErrorCode.INVALID_PASSWORD.getMessage()); - } - - @Test - @DisplayName("비밀번호 정책 통과(정상 8자 이상, 숫자/특수문자 포함) → 성공") - void register_validPassword() { - // given: 정책을 만족하는 정상 비밀번호 - UserRegisterRequest request = new UserRegisterRequest( - "user3", "user3@example.com", "Abcd123!", "닉네임" - ); - - // when: 회원가입 실행 - UserResponse response = userService.register(request); - - // then: username과 비밀번호 인코딩 검증 - assertThat(response.username()).isEqualTo("user3"); - assertThat(passwordEncoder.matches("Abcd123!", - userRepository.findById(response.userId()).get().getPassword())).isTrue(); - } - - @Test - @DisplayName("정상 로그인 성공") - void login_success() { - // given: 정상적인 사용자와 비밀번호 준비 - String rawPassword = "P@ssw0rd!"; - User user = setupUser("loginuser", "login@example.com", rawPassword, "닉네임", UserStatus.ACTIVE); - MockHttpServletResponse response = new MockHttpServletResponse(); - - // when: 로그인 요청 실행 - UserResponse userResponse = userService.login( - new LoginRequest("loginuser", rawPassword), response); - - // then: 응답에 username과 토큰/쿠키가 포함됨 - assertThat(userResponse.username()).isEqualTo("loginuser"); - assertThat(response.getHeader("Authorization")).startsWith("Bearer "); - Cookie refreshCookie = response.getCookie("refreshToken"); - assertThat(refreshCookie).isNotNull(); - assertThat(refreshCookie.isHttpOnly()).isTrue(); - } - - @Test - @DisplayName("잘못된 비밀번호 → INVALID_CREDENTIALS 예외 발생") - void login_invalidPassword() { - // given: 존재하는 사용자, 잘못된 비밀번호 입력 - User user = setupUser("loginuser", "login@example.com", "P@ssw0rd!", "닉네임", UserStatus.ACTIVE); - MockHttpServletResponse response = new MockHttpServletResponse(); - - // when & then: 로그인 시도 시 INVALID_CREDENTIALS 예외 발생 - assertThatThrownBy(() -> userService.login( - new LoginRequest("loginuser", "wrongPassword"), response - )) - .isInstanceOf(CustomException.class) - .hasMessage(ErrorCode.INVALID_CREDENTIALS.getMessage()); - } - - @Test - @DisplayName("존재하지 않는 username → INVALID_CREDENTIALS 예외 발생") - void login_userNotFound() { - // given: 존재하지 않는 username 사용 - MockHttpServletResponse response = new MockHttpServletResponse(); - - // when & then: 로그인 시도 시 INVALID_CREDENTIALS 예외 발생 - assertThatThrownBy(() -> userService.login( - new LoginRequest("nouser", "P@ssw0rd!"), response - )) - .isInstanceOf(CustomException.class) - .hasMessage(ErrorCode.INVALID_CREDENTIALS.getMessage()); - } - - @Test - @DisplayName("상태가 PENDING → USER_EMAIL_NOT_VERIFIED 예외 발생") - void login_pendingUser() { - // given: 상태가 PENDING인 사용자 - User user = setupUser("pendinguser", "pending@example.com", "P@ssw0rd!", "닉네임", UserStatus.PENDING); - MockHttpServletResponse response = new MockHttpServletResponse(); - - // when & then: 로그인 시도 시 USER_EMAIL_NOT_VERIFIED 예외 발생 - assertThatThrownBy(() -> userService.login( - new LoginRequest(user.getUsername(), "P@ssw0rd!"), response - )) - .isInstanceOf(CustomException.class) - .hasMessage(ErrorCode.USER_EMAIL_NOT_VERIFIED.getMessage()); - } - - @Test - @DisplayName("상태가 SUSPENDED → USER_SUSPENDED 예외 발생") - void login_suspendedUser() { - // given: 상태가 SUSPENDED인 사용자 - User user = setupUser("suspended", "suspended@example.com", "P@ssw0rd!", "닉네임", UserStatus.SUSPENDED); - MockHttpServletResponse response = new MockHttpServletResponse(); - - // when & then: 로그인 시도 시 USER_SUSPENDED 예외 발생 - assertThatThrownBy(() -> userService.login( - new LoginRequest(user.getUsername(), "P@ssw0rd!"), response - )) - .isInstanceOf(CustomException.class) - .hasMessage(ErrorCode.USER_SUSPENDED.getMessage()); - } - - @Test - @DisplayName("상태가 DELETED → USER_DELETED 예외 발생") - void login_deletedUser() { - // given: 상태가 DELETED인 사용자 - User user = setupUser("deleted", "deleted@example.com", "P@ssw0rd!", "닉네임", UserStatus.DELETED); - MockHttpServletResponse response = new MockHttpServletResponse(); - - // when & then: 로그인 시도 시 USER_DELETED 예외 발생 - assertThatThrownBy(() -> userService.login( - new LoginRequest(user.getUsername(), "P@ssw0rd!"), response - )) - .isInstanceOf(CustomException.class) - .hasMessage(ErrorCode.USER_DELETED.getMessage()); - } - @Test - @DisplayName("정상 로그아웃 성공 → RefreshToken DB 삭제 + 쿠키 만료") - void logout_success() { - // given: 정상 로그인된 사용자 - String rawPassword = "P@ssw0rd!"; - User user = setupUser("logoutuser", "logout@example.com", rawPassword, "닉네임", UserStatus.ACTIVE); - MockHttpServletResponse loginResponse = new MockHttpServletResponse(); - - userService.login(new LoginRequest("logoutuser", rawPassword), loginResponse); - Cookie refreshCookie = loginResponse.getCookie("refreshToken"); - assertThat(refreshCookie).isNotNull(); - - MockHttpServletResponse logoutResponse = new MockHttpServletResponse(); - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setCookies(refreshCookie); // 쿠키를 요청에 실어줌 - - // when: 로그아웃 실행 - userService.logout(request, logoutResponse); - - // then: DB에서 refreshToken 삭제됨 - assertThat(userTokenRepository.findByRefreshToken(refreshCookie.getValue())).isEmpty(); - - // 응답 쿠키는 만료 처리됨 - Cookie cleared = logoutResponse.getCookie("refreshToken"); - assertThat(cleared).isNotNull(); - assertThat(cleared.getMaxAge()).isZero(); - assertThat(cleared.getValue()).isNull(); - } - - @Test - @DisplayName("RefreshToken 없으면 INVALID_TOKEN 예외 발생") - void logout_noToken() { - // given: 쿠키 없이 로그아웃 요청 - MockHttpServletRequest request = new MockHttpServletRequest(); - MockHttpServletResponse response = new MockHttpServletResponse(); - - // when & then - assertThatThrownBy(() -> userService.logout(request, response)) - .isInstanceOf(CustomException.class) - .hasMessage(ErrorCode.BAD_REQUEST.getMessage()); - } - - @Test - @DisplayName("유효하지 않은 RefreshToken이면 INVALID_TOKEN 예외 발생") - void logout_invalidToken() { - // given: 잘못된 토큰 쿠키 세팅 - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setCookies(new Cookie("refreshToken", "invalidToken")); - MockHttpServletResponse response = new MockHttpServletResponse(); - - // when & then - assertThatThrownBy(() -> userService.logout(request, response)) - .isInstanceOf(CustomException.class) - .hasMessage(ErrorCode.INVALID_TOKEN.getMessage()); - } - - @Test - @DisplayName("정상 토큰 재발급 성공 → 새로운 AccessToken 반환 및 헤더 설정") - void refreshToken_success() throws InterruptedException { - // given: 로그인된 사용자 준비 - String rawPassword = "P@ssw0rd!"; - User user = setupUser("refreshuser", "refresh@example.com", rawPassword, "닉네임", UserStatus.ACTIVE); - MockHttpServletResponse loginResponse = new MockHttpServletResponse(); - - userService.login(new LoginRequest("refreshuser", rawPassword), loginResponse); - String oldAccessToken = loginResponse.getHeader("Authorization").substring(7); - Cookie refreshCookie = loginResponse.getCookie("refreshToken"); - assertThat(refreshCookie).isNotNull(); - - // 요청/응답 객체 준비 - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setCookies(refreshCookie); - MockHttpServletResponse response = new MockHttpServletResponse(); - - // Issued At(발급 시간) 분리를 위해 1초 대기 -// Thread.sleep(1000); - - // when: 토큰 재발급 실행 - String newAccessToken = userService.refreshToken(request, response); - - // then: 반환값 및 응답 헤더 검증 - assertThat(newAccessToken).isNotBlank(); -// assertThat(newAccessToken).isNotEqualTo(oldAccessToken); - assertThat(response.getHeader("Authorization")).isEqualTo("Bearer " + newAccessToken); - } - - @Test - @DisplayName("RefreshToken 없으면 BAD_REQUEST 예외 발생") - void refreshToken_noToken() { - // given: 쿠키 없는 요청 - MockHttpServletRequest request = new MockHttpServletRequest(); - MockHttpServletResponse response = new MockHttpServletResponse(); - - // when & then - assertThatThrownBy(() -> userService.refreshToken(request, response)) - .isInstanceOf(CustomException.class) - .hasMessage(ErrorCode.BAD_REQUEST.getMessage()); - } - - @Test - @DisplayName("유효하지 않은 RefreshToken이면 INVALID_TOKEN 예외 발생") - void refreshToken_invalidToken() { - // given: 잘못된 Refresh Token - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setCookies(new Cookie("refreshToken", "invalidToken")); - MockHttpServletResponse response = new MockHttpServletResponse(); - - // when & then - assertThatThrownBy(() -> userService.refreshToken(request, response)) - .isInstanceOf(CustomException.class) - .hasMessage(ErrorCode.INVALID_TOKEN.getMessage()); - } -} \ No newline at end of file +//package com.back.domain.user.service; +// +//import com.back.domain.user.dto.LoginRequest; +//import com.back.domain.user.dto.UserRegisterRequest; +//import com.back.domain.user.dto.UserResponse; +//import com.back.domain.user.entity.User; +//import com.back.domain.user.entity.UserStatus; +//import com.back.domain.user.repository.UserProfileRepository; +//import com.back.domain.user.repository.UserRepository; +//import com.back.domain.user.repository.UserTokenRepository; +//import com.back.global.exception.CustomException; +//import com.back.global.exception.ErrorCode; +//import jakarta.servlet.http.Cookie; +//import org.junit.jupiter.api.DisplayName; +//import org.junit.jupiter.api.Test; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.boot.test.context.SpringBootTest; +//import org.springframework.mock.web.MockHttpServletRequest; +//import org.springframework.mock.web.MockHttpServletResponse; +//import org.springframework.security.crypto.password.PasswordEncoder; +//import org.springframework.test.context.ActiveProfiles; +//import org.springframework.transaction.annotation.Transactional; +// +//import static org.assertj.core.api.Assertions.assertThat; +//import static org.assertj.core.api.Assertions.assertThatThrownBy; +// +//@SpringBootTest +//@Transactional +//@ActiveProfiles("test") +//class UserServiceTest { +// +// @Autowired +// private UserService userService; +// +// @Autowired +// private UserRepository userRepository; +// +// @Autowired +// private UserProfileRepository userProfileRepository; +// +// @Autowired +// private UserTokenRepository userTokenRepository; +// +// @Autowired +// private PasswordEncoder passwordEncoder; +// +// private User setupUser(String username, String email, String password, String nickname, UserStatus status) { +// UserRegisterRequest request = new UserRegisterRequest(username, email, password, nickname); +// UserResponse response = userService.register(request); +// +// User saved = userRepository.findById(response.userId()).orElseThrow(); +// saved.setUserStatus(status); // 상태 변경 (PENDING, SUSPENDED, DELETED) +// return saved; +// } +// +// @Test +// @DisplayName("정상 회원가입 성공") +// void register_success() { +// // given: 정상적인 회원가입 요청 생성 +// UserRegisterRequest request = new UserRegisterRequest( +// "testuser", "test@example.com", "P@ssw0rd!", "홍길동" +// ); +// +// // when: 회원가입 실행 +// UserResponse response = userService.register(request); +// +// // then: 반환된 값 검증 +// assertThat(response.username()).isEqualTo("testuser"); +// assertThat(response.email()).isEqualTo("test@example.com"); +// assertThat(response.nickname()).isEqualTo("홍길동"); +// // TODO: 이메일 인증 기능 개발 후 기본 상태를 PENDING으로 변경 +//// assertThat(response.status()).isEqualTo(UserStatus.PENDING); +// +// // 비밀번호 인코딩 검증 +// String encoded = userRepository.findById(response.userId()).get().getPassword(); +// assertThat(passwordEncoder.matches("P@ssw0rd!", encoded)).isTrue(); +// +// // UserProfile도 함께 생성되었는지 확인 +// assertThat(userProfileRepository.existsByNickname("홍길동")).isTrue(); +// } +// +// @Test +// @DisplayName("중복된 username이면 예외 발생") +// void register_duplicateUsername() { +// // given: 동일 username으로 첫 번째 가입 +// userService.register(new UserRegisterRequest( +// "dupuser", "dup@example.com", "P@ssw0rd!", "닉네임" +// )); +// +// // when & then: 같은 username으로 가입 시 예외 발생 +// assertThatThrownBy(() -> +// userService.register(new UserRegisterRequest( +// "dupuser", "other@example.com", "P@ssw0rd!", "다른닉네임" +// )) +// ).isInstanceOf(CustomException.class) +// .hasMessage(ErrorCode.USERNAME_DUPLICATED.getMessage()); +// } +// +// @Test +// @DisplayName("중복된 email이면 예외 발생") +// void register_duplicateEmail() { +// // given: 동일 email로 첫 번째 가입 +// userService.register(new UserRegisterRequest( +// "user1", "dup@example.com", "P@ssw0rd!", "닉네임" +// )); +// +// // when & then: 같은 email로 가입 시 예외 발생 +// assertThatThrownBy(() -> +// userService.register(new UserRegisterRequest( +// "user2", "dup@example.com", "P@ssw0rd!", "다른닉네임" +// )) +// ).isInstanceOf(CustomException.class) +// .hasMessage(ErrorCode.EMAIL_DUPLICATED.getMessage()); +// } +// +// @Test +// @DisplayName("중복된 nickname이면 예외 발생") +// void register_duplicateNickname() { +// // given: 동일 nickname으로 첫 번째 가입 +// userService.register(new UserRegisterRequest( +// "user1", "user1@example.com", "P@ssw0rd!", "dupnick" +// )); +// +// // when & then: 같은 nickname으로 가입 시 예외 발생 +// assertThatThrownBy(() -> +// userService.register(new UserRegisterRequest( +// "user2", "user2@example.com", "P@ssw0rd!", "dupnick" +// )) +// ).isInstanceOf(CustomException.class) +// .hasMessage(ErrorCode.NICKNAME_DUPLICATED.getMessage()); +// } +// +// @Test +// @DisplayName("비밀번호 정책 위반(숫자/특수문자 없음) → 예외 발생") +// void register_invalidPassword_noNumberOrSpecial() { +// // given: 숫자, 특수문자 없는 비밀번호 +// UserRegisterRequest request = new UserRegisterRequest( +// "user1", "user1@example.com", "abcdefgh", "닉네임" +// ); +// +// // when & then: 정책 위반으로 예외 발생 +// assertThatThrownBy(() -> userService.register(request)) +// .isInstanceOf(CustomException.class) +// .hasMessage(ErrorCode.INVALID_PASSWORD.getMessage()); +// } +// +// @Test +// @DisplayName("비밀번호 정책 위반(길이 7자) → 예외 발생") +// void register_invalidPassword_tooShort() { +// // given: 7자리 비밀번호 (정책상 8자 이상 필요) +// UserRegisterRequest request = new UserRegisterRequest( +// "user2", "user2@example.com", "Abc12!", "닉네임" +// ); +// +// // when & then: 정책 위반으로 예외 발생 +// assertThatThrownBy(() -> userService.register(request)) +// .isInstanceOf(CustomException.class) +// .hasMessage(ErrorCode.INVALID_PASSWORD.getMessage()); +// } +// +// @Test +// @DisplayName("비밀번호 정책 통과(정상 8자 이상, 숫자/특수문자 포함) → 성공") +// void register_validPassword() { +// // given: 정책을 만족하는 정상 비밀번호 +// UserRegisterRequest request = new UserRegisterRequest( +// "user3", "user3@example.com", "Abcd123!", "닉네임" +// ); +// +// // when: 회원가입 실행 +// UserResponse response = userService.register(request); +// +// // then: username과 비밀번호 인코딩 검증 +// assertThat(response.username()).isEqualTo("user3"); +// assertThat(passwordEncoder.matches("Abcd123!", +// userRepository.findById(response.userId()).get().getPassword())).isTrue(); +// } +// +// @Test +// @DisplayName("정상 로그인 성공") +// void login_success() { +// // given: 정상적인 사용자와 비밀번호 준비 +// String rawPassword = "P@ssw0rd!"; +// User user = setupUser("loginuser", "login@example.com", rawPassword, "닉네임", UserStatus.ACTIVE); +// MockHttpServletResponse response = new MockHttpServletResponse(); +// +// // when: 로그인 요청 실행 +// UserResponse userResponse = userService.login( +// new LoginRequest("loginuser", rawPassword), response); +// +// // then: 응답에 username과 토큰/쿠키가 포함됨 +// assertThat(userResponse.username()).isEqualTo("loginuser"); +// assertThat(response.getHeader("Authorization")).startsWith("Bearer "); +// Cookie refreshCookie = response.getCookie("refreshToken"); +// assertThat(refreshCookie).isNotNull(); +// assertThat(refreshCookie.isHttpOnly()).isTrue(); +// } +// +// @Test +// @DisplayName("잘못된 비밀번호 → INVALID_CREDENTIALS 예외 발생") +// void login_invalidPassword() { +// // given: 존재하는 사용자, 잘못된 비밀번호 입력 +// User user = setupUser("loginuser", "login@example.com", "P@ssw0rd!", "닉네임", UserStatus.ACTIVE); +// MockHttpServletResponse response = new MockHttpServletResponse(); +// +// // when & then: 로그인 시도 시 INVALID_CREDENTIALS 예외 발생 +// assertThatThrownBy(() -> userService.login( +// new LoginRequest("loginuser", "wrongPassword"), response +// )) +// .isInstanceOf(CustomException.class) +// .hasMessage(ErrorCode.INVALID_CREDENTIALS.getMessage()); +// } +// +// @Test +// @DisplayName("존재하지 않는 username → INVALID_CREDENTIALS 예외 발생") +// void login_userNotFound() { +// // given: 존재하지 않는 username 사용 +// MockHttpServletResponse response = new MockHttpServletResponse(); +// +// // when & then: 로그인 시도 시 INVALID_CREDENTIALS 예외 발생 +// assertThatThrownBy(() -> userService.login( +// new LoginRequest("nouser", "P@ssw0rd!"), response +// )) +// .isInstanceOf(CustomException.class) +// .hasMessage(ErrorCode.INVALID_CREDENTIALS.getMessage()); +// } +// +// @Test +// @DisplayName("상태가 PENDING → USER_EMAIL_NOT_VERIFIED 예외 발생") +// void login_pendingUser() { +// // given: 상태가 PENDING인 사용자 +// User user = setupUser("pendinguser", "pending@example.com", "P@ssw0rd!", "닉네임", UserStatus.PENDING); +// MockHttpServletResponse response = new MockHttpServletResponse(); +// +// // when & then: 로그인 시도 시 USER_EMAIL_NOT_VERIFIED 예외 발생 +// assertThatThrownBy(() -> userService.login( +// new LoginRequest(user.getUsername(), "P@ssw0rd!"), response +// )) +// .isInstanceOf(CustomException.class) +// .hasMessage(ErrorCode.USER_EMAIL_NOT_VERIFIED.getMessage()); +// } +// +// @Test +// @DisplayName("상태가 SUSPENDED → USER_SUSPENDED 예외 발생") +// void login_suspendedUser() { +// // given: 상태가 SUSPENDED인 사용자 +// User user = setupUser("suspended", "suspended@example.com", "P@ssw0rd!", "닉네임", UserStatus.SUSPENDED); +// MockHttpServletResponse response = new MockHttpServletResponse(); +// +// // when & then: 로그인 시도 시 USER_SUSPENDED 예외 발생 +// assertThatThrownBy(() -> userService.login( +// new LoginRequest(user.getUsername(), "P@ssw0rd!"), response +// )) +// .isInstanceOf(CustomException.class) +// .hasMessage(ErrorCode.USER_SUSPENDED.getMessage()); +// } +// +// @Test +// @DisplayName("상태가 DELETED → USER_DELETED 예외 발생") +// void login_deletedUser() { +// // given: 상태가 DELETED인 사용자 +// User user = setupUser("deleted", "deleted@example.com", "P@ssw0rd!", "닉네임", UserStatus.DELETED); +// MockHttpServletResponse response = new MockHttpServletResponse(); +// +// // when & then: 로그인 시도 시 USER_DELETED 예외 발생 +// assertThatThrownBy(() -> userService.login( +// new LoginRequest(user.getUsername(), "P@ssw0rd!"), response +// )) +// .isInstanceOf(CustomException.class) +// .hasMessage(ErrorCode.USER_DELETED.getMessage()); +// } +// @Test +// @DisplayName("정상 로그아웃 성공 → RefreshToken DB 삭제 + 쿠키 만료") +// void logout_success() { +// // given: 정상 로그인된 사용자 +// String rawPassword = "P@ssw0rd!"; +// User user = setupUser("logoutuser", "logout@example.com", rawPassword, "닉네임", UserStatus.ACTIVE); +// MockHttpServletResponse loginResponse = new MockHttpServletResponse(); +// +// userService.login(new LoginRequest("logoutuser", rawPassword), loginResponse); +// Cookie refreshCookie = loginResponse.getCookie("refreshToken"); +// assertThat(refreshCookie).isNotNull(); +// +// MockHttpServletResponse logoutResponse = new MockHttpServletResponse(); +// MockHttpServletRequest request = new MockHttpServletRequest(); +// request.setCookies(refreshCookie); // 쿠키를 요청에 실어줌 +// +// // when: 로그아웃 실행 +// userService.logout(request, logoutResponse); +// +// // then: DB에서 refreshToken 삭제됨 +// assertThat(userTokenRepository.findByRefreshToken(refreshCookie.getValue())).isEmpty(); +// +// // 응답 쿠키는 만료 처리됨 +// Cookie cleared = logoutResponse.getCookie("refreshToken"); +// assertThat(cleared).isNotNull(); +// assertThat(cleared.getMaxAge()).isZero(); +// assertThat(cleared.getValue()).isNull(); +// } +// +// @Test +// @DisplayName("RefreshToken 없으면 INVALID_TOKEN 예외 발생") +// void logout_noToken() { +// // given: 쿠키 없이 로그아웃 요청 +// MockHttpServletRequest request = new MockHttpServletRequest(); +// MockHttpServletResponse response = new MockHttpServletResponse(); +// +// // when & then +// assertThatThrownBy(() -> userService.logout(request, response)) +// .isInstanceOf(CustomException.class) +// .hasMessage(ErrorCode.BAD_REQUEST.getMessage()); +// } +// +// @Test +// @DisplayName("유효하지 않은 RefreshToken이면 INVALID_TOKEN 예외 발생") +// void logout_invalidToken() { +// // given: 잘못된 토큰 쿠키 세팅 +// MockHttpServletRequest request = new MockHttpServletRequest(); +// request.setCookies(new Cookie("refreshToken", "invalidToken")); +// MockHttpServletResponse response = new MockHttpServletResponse(); +// +// // when & then +// assertThatThrownBy(() -> userService.logout(request, response)) +// .isInstanceOf(CustomException.class) +// .hasMessage(ErrorCode.INVALID_TOKEN.getMessage()); +// } +// +// @Test +// @DisplayName("정상 토큰 재발급 성공 → 새로운 AccessToken 반환 및 헤더 설정") +// void refreshToken_success() throws InterruptedException { +// // given: 로그인된 사용자 준비 +// String rawPassword = "P@ssw0rd!"; +// User user = setupUser("refreshuser", "refresh@example.com", rawPassword, "닉네임", UserStatus.ACTIVE); +// MockHttpServletResponse loginResponse = new MockHttpServletResponse(); +// +// userService.login(new LoginRequest("refreshuser", rawPassword), loginResponse); +// String oldAccessToken = loginResponse.getHeader("Authorization").substring(7); +// Cookie refreshCookie = loginResponse.getCookie("refreshToken"); +// assertThat(refreshCookie).isNotNull(); +// +// // 요청/응답 객체 준비 +// MockHttpServletRequest request = new MockHttpServletRequest(); +// request.setCookies(refreshCookie); +// MockHttpServletResponse response = new MockHttpServletResponse(); +// +// // Issued At(발급 시간) 분리를 위해 1초 대기 +//// Thread.sleep(1000); +// +// // when: 토큰 재발급 실행 +// String newAccessToken = userService.refreshToken(request, response); +// +// // then: 반환값 및 응답 헤더 검증 +// assertThat(newAccessToken).isNotBlank(); +//// assertThat(newAccessToken).isNotEqualTo(oldAccessToken); +// assertThat(response.getHeader("Authorization")).isEqualTo("Bearer " + newAccessToken); +// } +// +// @Test +// @DisplayName("RefreshToken 없으면 BAD_REQUEST 예외 발생") +// void refreshToken_noToken() { +// // given: 쿠키 없는 요청 +// MockHttpServletRequest request = new MockHttpServletRequest(); +// MockHttpServletResponse response = new MockHttpServletResponse(); +// +// // when & then +// assertThatThrownBy(() -> userService.refreshToken(request, response)) +// .isInstanceOf(CustomException.class) +// .hasMessage(ErrorCode.BAD_REQUEST.getMessage()); +// } +// +// @Test +// @DisplayName("유효하지 않은 RefreshToken이면 INVALID_TOKEN 예외 발생") +// void refreshToken_invalidToken() { +// // given: 잘못된 Refresh Token +// MockHttpServletRequest request = new MockHttpServletRequest(); +// request.setCookies(new Cookie("refreshToken", "invalidToken")); +// MockHttpServletResponse response = new MockHttpServletResponse(); +// +// // when & then +// assertThatThrownBy(() -> userService.refreshToken(request, response)) +// .isInstanceOf(CustomException.class) +// .hasMessage(ErrorCode.INVALID_TOKEN.getMessage()); +// } +//} \ No newline at end of file From 5b2d0f073bf395afb233671bc72de5558c1ef998 Mon Sep 17 00:00:00 2001 From: joyewon0705 <77885098+joyewon0705@users.noreply.github.com> Date: Fri, 26 Sep 2025 11:47:40 +0900 Subject: [PATCH 2/3] =?UTF-8?q?Docs:=20Swagger=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/AuthControllerDocs.java | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/back/domain/user/controller/AuthControllerDocs.java b/src/main/java/com/back/domain/user/controller/AuthControllerDocs.java index e9826c21..ed441a5a 100644 --- a/src/main/java/com/back/domain/user/controller/AuthControllerDocs.java +++ b/src/main/java/com/back/domain/user/controller/AuthControllerDocs.java @@ -131,8 +131,7 @@ ResponseEntity> register( @Operation( summary = "로그인", - description = "username + password로 로그인합니다. " + - "로그인 성공 시 Access Token은 `Authorization` 헤더에, Refresh Token은 HttpOnly 쿠키로 발급됩니다." + description = "username + password로 로그인합니다. " ) @ApiResponses({ @ApiResponse( @@ -146,13 +145,16 @@ ResponseEntity> register( "code": "SUCCESS_200", "message": "로그인에 성공했습니다.", "data": { - "userId": 1, - "username": "testuser", - "email": "test@example.com", - "nickname": "홍길동", - "role": "USER", - "status": "ACTIVE", - "createdAt": "2025-09-19T15:00:00" + "accessToken": "{accessToken}", + "user": { + "userId": 1, + "username": "testuser", + "email": "test@example.com", + "nickname": "홍길동", + "role": "USER", + "status": "ACTIVE", + "createdAt": "2025-09-19T15:00:00" + } } } """) From 376cce52d3bed07ec2a5887d4139d29f1bdb7e25 Mon Sep 17 00:00:00 2001 From: joyewon0705 <77885098+joyewon0705@users.noreply.github.com> Date: Fri, 26 Sep 2025 12:04:27 +0900 Subject: [PATCH 3/3] =?UTF-8?q?Test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/AuthControllerTest.java | 1104 ++++++++--------- .../domain/user/service/UserServiceTest.java | 769 ++++++------ 2 files changed, 932 insertions(+), 941 deletions(-) diff --git a/src/test/java/com/back/domain/user/controller/AuthControllerTest.java b/src/test/java/com/back/domain/user/controller/AuthControllerTest.java index 4898c353..d75af6c9 100644 --- a/src/test/java/com/back/domain/user/controller/AuthControllerTest.java +++ b/src/test/java/com/back/domain/user/controller/AuthControllerTest.java @@ -1,558 +1,546 @@ -//package com.back.domain.user.controller; -// -//import com.back.domain.user.entity.User; -//import com.back.domain.user.entity.UserProfile; -//import com.back.domain.user.entity.UserStatus; -//import com.back.domain.user.repository.UserRepository; -//import com.back.fixture.TestJwtTokenProvider; -//import io.jsonwebtoken.Jwts; -//import io.jsonwebtoken.SignatureAlgorithm; -//import jakarta.servlet.http.Cookie; -//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.web.servlet.AutoConfigureMockMvc; -//import org.springframework.boot.test.context.SpringBootTest; -//import org.springframework.http.MediaType; -//import org.springframework.security.crypto.password.PasswordEncoder; -//import org.springframework.test.context.ActiveProfiles; -//import org.springframework.test.web.servlet.MockMvc; -//import org.springframework.test.web.servlet.ResultActions; -//import org.springframework.transaction.annotation.Transactional; -// -//import java.util.Date; -// -//import static org.assertj.core.api.Assertions.assertThat; -//import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -//import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -//import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; -// -//@SpringBootTest -//@AutoConfigureMockMvc -//@ActiveProfiles("test") -//@Transactional -//class AuthControllerTest { -// -// @Autowired -// private MockMvc mvc; -// -// @Autowired -// private UserRepository userRepository; -// -// @Autowired -// private TestJwtTokenProvider testJwtTokenProvider; -// -// @Test -// @DisplayName("정상 회원가입 → 201 Created") -// void register_success() throws Exception { -// // given: 정상적인 회원가입 요청 JSON -// String body = """ -// { -// "username": "testuser", -// "email": "test@example.com", -// "password": "P@ssw0rd!", -// "nickname": "홍길동" -// } -// """; -// -// // when: 회원가입 API 호출 -// ResultActions resultActions = mvc.perform( -// post("/api/auth/register") -// .contentType(MediaType.APPLICATION_JSON) -// .content(body) -// ).andDo(print()); -// -// // then: 응답 값과 DB 저장값 검증 -// resultActions -// .andExpect(status().isCreated()) -// .andExpect(jsonPath("$.success").value(true)) -// .andExpect(jsonPath("$.data.username").value("testuser")) -// .andExpect(jsonPath("$.data.email").value("test@example.com")) -// .andExpect(jsonPath("$.data.nickname").value("홍길동")); -// -// // DB에서 저장된 User 상태 검증 -// User saved = userRepository.findByUsername("testuser").orElseThrow(); -// // TODO: 이메일 인증 기능 개발 후 기본 상태를 PENDING으로 변경 -//// assertThat(saved.getUserStatus()).isEqualTo(UserStatus.PENDING); -// } -// -// @Test -// @DisplayName("중복 username → 409 Conflict") -// void register_duplicateUsername() throws Exception { -// // given: 이미 존재하는 username을 가진 User 저장 -// User existing = User.createUser("dupuser", "dup@example.com", "password123!"); -// existing.setUserProfile(new UserProfile(existing, "dupnick", null, null, null, 0)); -// userRepository.save(existing); -// -// // 동일 username으로 회원가입 요청 -// String body = """ -// { -// "username": "dupuser", -// "email": "other@example.com", -// "password": "P@ssw0rd!", -// "nickname": "다른닉네임" -// } -// """; -// -// // when & then: 409 Conflict 응답 및 에러 코드 확인 -// mvc.perform(post("/api/auth/register") -// .contentType(MediaType.APPLICATION_JSON) -// .content(body)) -// .andDo(print()) -// .andExpect(status().isConflict()) -// .andExpect(jsonPath("$.code").value("USER_002")); -// } -// -// @Test -// @DisplayName("중복 email → 409 Conflict") -// void register_duplicateEmail() throws Exception { -// // given: 이미 존재하는 email을 가진 User 저장 -// User existing = User.createUser("user1", "dup@example.com", "password123!"); -// existing.setUserProfile(new UserProfile(existing, "nick1", null, null, null, 0)); -// userRepository.save(existing); -// -// // 동일 email로 회원가입 요청 -// String body = """ -// { -// "username": "otheruser", -// "email": "dup@example.com", -// "password": "P@ssw0rd!", -// "nickname": "다른닉네임" -// } -// """; -// -// // when & then: 409 Conflict 응답 및 에러 코드 확인 -// mvc.perform(post("/api/auth/register") -// .contentType(MediaType.APPLICATION_JSON) -// .content(body)) -// .andDo(print()) -// .andExpect(status().isConflict()) -// .andExpect(jsonPath("$.code").value("USER_003")); -// } -// -// @Test -// @DisplayName("중복 nickname → 409 Conflict") -// void register_duplicateNickname() throws Exception { -// // given: 이미 존재하는 nickname을 가진 User 저장 -// User existing = User.createUser("user2", "user2@example.com", "password123!"); -// existing.setUserProfile(new UserProfile(existing, "dupnick", null, null, null, 0)); -// userRepository.save(existing); -// -// // 동일 nickname으로 회원가입 요청 -// String body = """ -// { -// "username": "newuser", -// "email": "new@example.com", -// "password": "P@ssw0rd!", -// "nickname": "dupnick" -// } -// """; -// -// // when & then: 409 Conflict 응답 및 에러 코드 확인 -// mvc.perform(post("/api/auth/register") -// .contentType(MediaType.APPLICATION_JSON) -// .content(body)) -// .andDo(print()) -// .andExpect(status().isConflict()) -// .andExpect(jsonPath("$.code").value("USER_004")); -// } -// -// @Test -// @DisplayName("비밀번호 정책 위반 → 400 Bad Request") -// void register_invalidPassword() throws Exception { -// // given: 숫자/특수문자 포함 안 된 약한 비밀번호 -// String body = """ -// { -// "username": "weakpw", -// "email": "weak@example.com", -// "password": "password", -// "nickname": "닉네임" -// } -// """; -// -// // when & then: 400 Bad Request 응답 및 에러 코드 확인 -// mvc.perform(post("/api/auth/register") -// .contentType(MediaType.APPLICATION_JSON) -// .content(body)) -// .andDo(print()) -// .andExpect(status().isBadRequest()) -// .andExpect(jsonPath("$.code").value("USER_005")); -// } -// -// @Test -// @DisplayName("잘못된 요청값 (필수 필드 누락) → 400 Bad Request") -// void register_invalidRequest_missingField() throws Exception { -// // given: 필수 값 누락 (username, password, nickname 비어있음 / email 형식 잘못됨) -// String body = """ -// { -// "username": "", -// "email": "invalid", -// "password": "", -// "nickname": "" -// } -// """; -// -// // when & then: 400 Bad Request 응답 및 공통 에러 코드 확인 -// mvc.perform(post("/api/auth/register") -// .contentType(MediaType.APPLICATION_JSON) -// .content(body)) -// .andDo(print()) -// .andExpect(status().isBadRequest()) -// .andExpect(jsonPath("$.code").value("COMMON_400")); -// } -// -// @Test -// @DisplayName("정상 로그인 → 200 OK + Authorization 헤더 + refreshToken 쿠키") -// void login_success() throws Exception { -// // given: 회원가입 요청으로 DB에 정상 유저 저장 -// String rawPassword = "P@ssw0rd!"; -// String registerBody = """ -// { -// "username": "loginuser", -// "email": "login@example.com", -// "password": "%s", -// "nickname": "홍길동" -// } -// """.formatted(rawPassword); -// -// mvc.perform(post("/api/auth/register") -// .contentType(MediaType.APPLICATION_JSON) -// .content(registerBody)) -// .andExpect(status().isCreated()); -// -// // when: 로그인 요청 -// String loginBody = """ -// { -// "username": "loginuser", -// "password": "%s" -// } -// """.formatted(rawPassword); -// -// ResultActions resultActions = mvc.perform(post("/api/auth/login") -// .contentType(MediaType.APPLICATION_JSON) -// .content(loginBody)) -// .andDo(print()); -// -// // then: 200 OK 응답 + username/Authorization 헤더/refreshToken 쿠키 확인 -// resultActions -// .andExpect(status().isOk()) -// .andExpect(jsonPath("$.success").value(true)) -// .andExpect(jsonPath("$.data.username").value("loginuser")) -// .andExpect(header().exists("Authorization")) -// .andExpect(cookie().exists("refreshToken")); -// } -// -// @Test -// @DisplayName("잘못된 비밀번호 → 401 Unauthorized") -// void login_invalidPassword() throws Exception { -// // given: 정상 유저를 회원가입으로 저장 -// String rawPassword = "P@ssw0rd!"; -// String registerBody = """ -// { -// "username": "badpwuser", -// "email": "badpw@example.com", -// "password": "%s", -// "nickname": "닉네임" -// } -// """.formatted(rawPassword); -// mvc.perform(post("/api/auth/register") -// .contentType(MediaType.APPLICATION_JSON) -// .content(registerBody)); -// -// // when: 틀린 비밀번호로 로그인 요청 -// String loginBody = """ -// { -// "username": "badpwuser", -// "password": "WrongPass!" -// } -// """; -// -// // then: 401 Unauthorized 응답 + 에러 코드 USER_006 확인 -// mvc.perform(post("/api/auth/login") -// .contentType(MediaType.APPLICATION_JSON) -// .content(loginBody)) -// .andDo(print()) -// .andExpect(status().isUnauthorized()) -// .andExpect(jsonPath("$.code").value("USER_006")); -// } -// -// @Test -// @DisplayName("존재하지 않는 username → 401 Unauthorized") -// void login_userNotFound() throws Exception { -// // given: 존재하지 않는 username 사용 -// String loginBody = """ -// { -// "username": "nouser", -// "password": "P@ssw0rd!" -// } -// """; -// -// // when & then: 401 Unauthorized 응답 + 에러 코드 USER_006 확인 -// mvc.perform(post("/api/auth/login") -// .contentType(MediaType.APPLICATION_JSON) -// .content(loginBody)) -// .andDo(print()) -// .andExpect(status().isUnauthorized()) -// .andExpect(jsonPath("$.code").value("USER_006")); -// } -// -// @Autowired -// private PasswordEncoder passwordEncoder; -// -// @Test -// @DisplayName("이메일 미인증(PENDING) 계정 로그인 → 403 Forbidden") -// void login_pendingUser() throws Exception { -// // given: 상태가 PENDING인 유저 저장 (비밀번호 인코딩 필수) -// User pending = User.createUser("pending", "pending@example.com", -// passwordEncoder.encode("P@ssw0rd!")); -// pending.setUserProfile(new UserProfile(pending, "닉네임", null, null, null, 0)); -// pending.setUserStatus(UserStatus.PENDING); -// userRepository.save(pending); -// -// String body = """ -// { -// "username": "pending", -// "password": "P@ssw0rd!" -// } -// """; -// -// // when & then: 403 Forbidden 응답 + 에러 코드 USER_007 확인 -// mvc.perform(post("/api/auth/login") -// .contentType(MediaType.APPLICATION_JSON) -// .content(body)) -// .andDo(print()) -// .andExpect(status().isForbidden()) -// .andExpect(jsonPath("$.code").value("USER_007")); -// } -// -// @Test -// @DisplayName("정지된 계정(SUSPENDED) 로그인 → 403 Forbidden") -// void login_suspendedUser() throws Exception { -// // given: 상태가 SUSPENDED인 유저 저장 -// User suspended = User.createUser("suspended", "suspended@example.com", -// passwordEncoder.encode("P@ssw0rd!")); -// suspended.setUserProfile(new UserProfile(suspended, "닉네임", null, null, null, 0)); -// suspended.setUserStatus(UserStatus.SUSPENDED); -// userRepository.save(suspended); -// -// String body = """ -// { -// "username": "suspended", -// "password": "P@ssw0rd!" -// } -// """; -// -// // when & then: 403 Forbidden 응답 + 에러 코드 USER_008 확인 -// mvc.perform(post("/api/auth/login") -// .contentType(MediaType.APPLICATION_JSON) -// .content(body)) -// .andDo(print()) -// .andExpect(status().isForbidden()) -// .andExpect(jsonPath("$.code").value("USER_008")); -// } -// -// @Test -// @DisplayName("탈퇴한 계정(DELETED) 로그인 → 410 Gone") -// void login_deletedUser() throws Exception { -// // given: 상태가 DELETED인 유저 저장 -// User deleted = User.createUser("deleted", "deleted@example.com", -// passwordEncoder.encode("P@ssw0rd!")); -// deleted.setUserProfile(new UserProfile(deleted, "닉네임", null, null, null, 0)); -// deleted.setUserStatus(UserStatus.DELETED); -// userRepository.save(deleted); -// -// String body = """ -// { -// "username": "deleted", -// "password": "P@ssw0rd!" -// } -// """; -// -// // when & then: 410 Gone 응답 + 에러 코드 USER_009 확인 -// mvc.perform(post("/api/auth/login") -// .contentType(MediaType.APPLICATION_JSON) -// .content(body)) -// .andDo(print()) -// .andExpect(status().isGone()) -// .andExpect(jsonPath("$.code").value("USER_009")); -// } -// -// @Test -// @DisplayName("정상 로그아웃 → 200 OK + RefreshToken 쿠키 만료") -// void logout_success() throws Exception { -// // given: 회원가입 + 로그인으로 refreshToken 쿠키 확보 -// String rawPassword = "P@ssw0rd!"; -// String registerBody = """ -// { -// "username": "logoutuser", -// "email": "logout@example.com", -// "password": "%s", -// "nickname": "홍길동" -// } -// """.formatted(rawPassword); -// -// mvc.perform(post("/api/auth/register") -// .contentType(MediaType.APPLICATION_JSON) -// .content(registerBody)) -// .andExpect(status().isCreated()); -// -// String loginBody = """ -// { -// "username": "logoutuser", -// "password": "%s" -// } -// """.formatted(rawPassword); -// -// ResultActions loginResult = mvc.perform(post("/api/auth/login") -// .contentType(MediaType.APPLICATION_JSON) -// .content(loginBody)) -// .andExpect(status().isOk()); -// -// // 로그인 응답에서 refreshToken 쿠키 추출 -// String refreshCookie = loginResult.andReturn() -// .getResponse() -// .getCookie("refreshToken") -// .getValue(); -// -// // when: 로그아웃 요청 (쿠키 포함) -// ResultActions logoutResult = mvc.perform(post("/api/auth/logout") -// .cookie(new Cookie("refreshToken", refreshCookie))) -// .andDo(print()); -// -// // then: 200 OK + 성공 메시지 + 쿠키 만료 -// logoutResult -// .andExpect(status().isOk()) -// .andExpect(jsonPath("$.success").value(true)) -// .andExpect(jsonPath("$.message").value("로그아웃 되었습니다.")) -// .andExpect(cookie().maxAge("refreshToken", 0)); -// } -// -// @Test -// @DisplayName("RefreshToken 누락 → 400 Bad Request") -// void logout_noToken() throws Exception { -// // when & then -// mvc.perform(post("/api/auth/logout")) -// .andDo(print()) -// .andExpect(status().isBadRequest()) -// .andExpect(jsonPath("$.code").value("COMMON_400")); -// } -// -// @Test -// @DisplayName("유효하지 않은 RefreshToken → 401 Unauthorized") -// void logout_invalidToken() throws Exception { -// // given: 잘못된 refreshToken 쿠키 -// Cookie invalid = new Cookie("refreshToken", "fake-token"); -// -// // when & then -// mvc.perform(post("/api/auth/logout").cookie(invalid)) -// .andDo(print()) -// .andExpect(status().isUnauthorized()) -// .andExpect(jsonPath("$.code").value("AUTH_401")); -// } -// -// @Test -// @DisplayName("정상 토큰 재발급 → 200 OK + 새로운 AccessToken 반환") -// void refreshToken_success() throws Exception { -// // given: 회원가입 + 로그인 → 기존 토큰 확보 -// String rawPassword = "P@ssw0rd!"; -// String registerBody = """ -// { -// "username": "refreshuser", -// "email": "refresh@example.com", -// "password": "%s", -// "nickname": "홍길동" -// } -// """.formatted(rawPassword); -// -// mvc.perform(post("/api/auth/register") -// .contentType(MediaType.APPLICATION_JSON) -// .content(registerBody)) -// .andExpect(status().isCreated()); -// -// String loginBody = """ -// { -// "username": "refreshuser", -// "password": "%s" -// } -// """.formatted(rawPassword); -// -// ResultActions loginResult = mvc.perform(post("/api/auth/login") -// .contentType(MediaType.APPLICATION_JSON) -// .content(loginBody)) -// .andExpect(status().isOk()); -// -// // 기존 AccessToken, RefreshToken 확보 -// String oldAccessToken = loginResult.andReturn() -// .getResponse() -// .getHeader("Authorization") -// .substring(7); // "Bearer " 제거 -// String refreshCookie = loginResult.andReturn() -// .getResponse() -// .getCookie("refreshToken") -// .getValue(); -// -// // Issued At(발급 시간) 분리를 위해 1초 대기 -//// Thread.sleep(1000); -// -// // when: 재발급 요청 (RefreshToken 쿠키 포함) -// ResultActions refreshResult = mvc.perform(post("/api/auth/refresh") -// .cookie(new Cookie("refreshToken", refreshCookie))) -// .andDo(print()); -// -// // then: 200 OK + 새로운 AccessToken 발급 -// refreshResult -// .andExpect(status().isOk()) -// .andExpect(jsonPath("$.success").value(true)) -// .andExpect(jsonPath("$.data.accessToken").exists()) -// .andExpect(header().exists("Authorization")); -// -// String newAccessToken = refreshResult.andReturn() -// .getResponse() -// .getHeader("Authorization") -// .substring(7); -// -// // 새 토큰은 기존 토큰과 달라야 함 -//// assertThat(newAccessToken).isNotEqualTo(oldAccessToken); -// } -// -// @Test -// @DisplayName("RefreshToken 누락 → 400 Bad Request") -// void refreshToken_noToken() throws Exception { -// mvc.perform(post("/api/auth/refresh")) -// .andDo(print()) -// .andExpect(status().isBadRequest()) -// .andExpect(jsonPath("$.code").value("COMMON_400")); -// } -// -// @Test -// @DisplayName("유효하지 않은 RefreshToken → 401 Unauthorized") -// void refreshToken_invalidToken() throws Exception { -// Cookie invalid = new Cookie("refreshToken", "fake-token"); -// -// mvc.perform(post("/api/auth/refresh").cookie(invalid)) -// .andDo(print()) -// .andExpect(status().isUnauthorized()) -// .andExpect(jsonPath("$.code").value("AUTH_401")); -// } -// -// @Test -// @DisplayName("만료된 RefreshToken → 401 Unauthorized") -// void refreshToken_expiredToken() throws Exception { -// // given: 이미 만료된 Refresh Token 발급 -// User user = User.createUser("expiredUser", "expired@example.com", passwordEncoder.encode("P@ssw0rd!")); -// user.setUserProfile(new UserProfile(user, "닉네임", null, null, null, 0)); -// userRepository.save(user); -// -// // JwtTokenProvider에 테스트용 메서드 추가 필요 -// String expiredRefreshToken = testJwtTokenProvider.createExpiredRefreshToken(user.getId()); -// -// Cookie expiredCookie = new Cookie("refreshToken", expiredRefreshToken); -// -// // when & then -// mvc.perform(post("/api/auth/refresh").cookie(expiredCookie)) -// .andDo(print()) -// .andExpect(status().isUnauthorized()) -// .andExpect(jsonPath("$.code").value("AUTH_401")) -// .andExpect(jsonPath("$.message").value("만료된 리프레시 토큰입니다.")); -// } -//} \ No newline at end of file +package com.back.domain.user.controller; + +import com.back.domain.user.entity.User; +import com.back.domain.user.entity.UserProfile; +import com.back.domain.user.entity.UserStatus; +import com.back.domain.user.repository.UserRepository; +import com.back.fixture.TestJwtTokenProvider; +import jakarta.servlet.http.Cookie; +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.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.transaction.annotation.Transactional; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +@Transactional +class AuthControllerTest { + + @Autowired + private MockMvc mvc; + + @Autowired + private UserRepository userRepository; + + @Autowired + private TestJwtTokenProvider testJwtTokenProvider; + + @Test + @DisplayName("정상 회원가입 → 201 Created") + void register_success() throws Exception { + // given: 정상적인 회원가입 요청 JSON + String body = """ + { + "username": "testuser", + "email": "test@example.com", + "password": "P@ssw0rd!", + "nickname": "홍길동" + } + """; + + // when: 회원가입 API 호출 + ResultActions resultActions = mvc.perform( + post("/api/auth/register") + .contentType(MediaType.APPLICATION_JSON) + .content(body) + ).andDo(print()); + + // then: 응답 값과 DB 저장값 검증 + resultActions + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.username").value("testuser")) + .andExpect(jsonPath("$.data.email").value("test@example.com")) + .andExpect(jsonPath("$.data.nickname").value("홍길동")); + + // DB에서 저장된 User 상태 검증 + User saved = userRepository.findByUsername("testuser").orElseThrow(); + // TODO: 이메일 인증 기능 개발 후 기본 상태를 PENDING으로 변경 +// assertThat(saved.getUserStatus()).isEqualTo(UserStatus.PENDING); + } + + @Test + @DisplayName("중복 username → 409 Conflict") + void register_duplicateUsername() throws Exception { + // given: 이미 존재하는 username을 가진 User 저장 + User existing = User.createUser("dupuser", "dup@example.com", "password123!"); + existing.setUserProfile(new UserProfile(existing, "dupnick", null, null, null, 0)); + userRepository.save(existing); + + // 동일 username으로 회원가입 요청 + String body = """ + { + "username": "dupuser", + "email": "other@example.com", + "password": "P@ssw0rd!", + "nickname": "다른닉네임" + } + """; + + // when & then: 409 Conflict 응답 및 에러 코드 확인 + mvc.perform(post("/api/auth/register") + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andDo(print()) + .andExpect(status().isConflict()) + .andExpect(jsonPath("$.code").value("USER_002")); + } + + @Test + @DisplayName("중복 email → 409 Conflict") + void register_duplicateEmail() throws Exception { + // given: 이미 존재하는 email을 가진 User 저장 + User existing = User.createUser("user1", "dup@example.com", "password123!"); + existing.setUserProfile(new UserProfile(existing, "nick1", null, null, null, 0)); + userRepository.save(existing); + + // 동일 email로 회원가입 요청 + String body = """ + { + "username": "otheruser", + "email": "dup@example.com", + "password": "P@ssw0rd!", + "nickname": "다른닉네임" + } + """; + + // when & then: 409 Conflict 응답 및 에러 코드 확인 + mvc.perform(post("/api/auth/register") + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andDo(print()) + .andExpect(status().isConflict()) + .andExpect(jsonPath("$.code").value("USER_003")); + } + + @Test + @DisplayName("중복 nickname → 409 Conflict") + void register_duplicateNickname() throws Exception { + // given: 이미 존재하는 nickname을 가진 User 저장 + User existing = User.createUser("user2", "user2@example.com", "password123!"); + existing.setUserProfile(new UserProfile(existing, "dupnick", null, null, null, 0)); + userRepository.save(existing); + + // 동일 nickname으로 회원가입 요청 + String body = """ + { + "username": "newuser", + "email": "new@example.com", + "password": "P@ssw0rd!", + "nickname": "dupnick" + } + """; + + // when & then: 409 Conflict 응답 및 에러 코드 확인 + mvc.perform(post("/api/auth/register") + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andDo(print()) + .andExpect(status().isConflict()) + .andExpect(jsonPath("$.code").value("USER_004")); + } + + @Test + @DisplayName("비밀번호 정책 위반 → 400 Bad Request") + void register_invalidPassword() throws Exception { + // given: 숫자/특수문자 포함 안 된 약한 비밀번호 + String body = """ + { + "username": "weakpw", + "email": "weak@example.com", + "password": "password", + "nickname": "닉네임" + } + """; + + // when & then: 400 Bad Request 응답 및 에러 코드 확인 + mvc.perform(post("/api/auth/register") + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("USER_005")); + } + + @Test + @DisplayName("잘못된 요청값 (필수 필드 누락) → 400 Bad Request") + void register_invalidRequest_missingField() throws Exception { + // given: 필수 값 누락 (username, password, nickname 비어있음 / email 형식 잘못됨) + String body = """ + { + "username": "", + "email": "invalid", + "password": "", + "nickname": "" + } + """; + + // when & then: 400 Bad Request 응답 및 공통 에러 코드 확인 + mvc.perform(post("/api/auth/register") + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("COMMON_400")); + } + + @Test + @DisplayName("정상 로그인 → 200 OK + accessToken + refreshToken 쿠키") + void login_success() throws Exception { + // given: 회원가입 요청으로 DB에 정상 유저 저장 + String rawPassword = "P@ssw0rd!"; + String registerBody = """ + { + "username": "loginuser", + "email": "login@example.com", + "password": "%s", + "nickname": "홍길동" + } + """.formatted(rawPassword); + + mvc.perform(post("/api/auth/register") + .contentType(MediaType.APPLICATION_JSON) + .content(registerBody)) + .andExpect(status().isCreated()); + + // when: 로그인 요청 + String loginBody = """ + { + "username": "loginuser", + "password": "%s" + } + """.formatted(rawPassword); + + ResultActions resultActions = mvc.perform(post("/api/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(loginBody)) + .andDo(print()); + + // then: 200 OK 응답 + username/accessToken/refreshToken 쿠키 확인 + resultActions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.user.username").value("loginuser")) + .andExpect(jsonPath("$.data.accessToken").exists()) + .andExpect(cookie().exists("refreshToken")); + } + + @Test + @DisplayName("잘못된 비밀번호 → 401 Unauthorized") + void login_invalidPassword() throws Exception { + // given: 정상 유저를 회원가입으로 저장 + String rawPassword = "P@ssw0rd!"; + String registerBody = """ + { + "username": "badpwuser", + "email": "badpw@example.com", + "password": "%s", + "nickname": "닉네임" + } + """.formatted(rawPassword); + mvc.perform(post("/api/auth/register") + .contentType(MediaType.APPLICATION_JSON) + .content(registerBody)); + + // when: 틀린 비밀번호로 로그인 요청 + String loginBody = """ + { + "username": "badpwuser", + "password": "WrongPass!" + } + """; + + // then: 401 Unauthorized 응답 + 에러 코드 USER_006 확인 + mvc.perform(post("/api/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(loginBody)) + .andDo(print()) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.code").value("USER_006")); + } + + @Test + @DisplayName("존재하지 않는 username → 401 Unauthorized") + void login_userNotFound() throws Exception { + // given: 존재하지 않는 username 사용 + String loginBody = """ + { + "username": "nouser", + "password": "P@ssw0rd!" + } + """; + + // when & then: 401 Unauthorized 응답 + 에러 코드 USER_006 확인 + mvc.perform(post("/api/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(loginBody)) + .andDo(print()) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.code").value("USER_006")); + } + + @Autowired + private PasswordEncoder passwordEncoder; + + @Test + @DisplayName("이메일 미인증(PENDING) 계정 로그인 → 403 Forbidden") + void login_pendingUser() throws Exception { + // given: 상태가 PENDING인 유저 저장 (비밀번호 인코딩 필수) + User pending = User.createUser("pending", "pending@example.com", + passwordEncoder.encode("P@ssw0rd!")); + pending.setUserProfile(new UserProfile(pending, "닉네임", null, null, null, 0)); + pending.setUserStatus(UserStatus.PENDING); + userRepository.save(pending); + + String body = """ + { + "username": "pending", + "password": "P@ssw0rd!" + } + """; + + // when & then: 403 Forbidden 응답 + 에러 코드 USER_007 확인 + mvc.perform(post("/api/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andDo(print()) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.code").value("USER_007")); + } + + @Test + @DisplayName("정지된 계정(SUSPENDED) 로그인 → 403 Forbidden") + void login_suspendedUser() throws Exception { + // given: 상태가 SUSPENDED인 유저 저장 + User suspended = User.createUser("suspended", "suspended@example.com", + passwordEncoder.encode("P@ssw0rd!")); + suspended.setUserProfile(new UserProfile(suspended, "닉네임", null, null, null, 0)); + suspended.setUserStatus(UserStatus.SUSPENDED); + userRepository.save(suspended); + + String body = """ + { + "username": "suspended", + "password": "P@ssw0rd!" + } + """; + + // when & then: 403 Forbidden 응답 + 에러 코드 USER_008 확인 + mvc.perform(post("/api/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andDo(print()) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.code").value("USER_008")); + } + + @Test + @DisplayName("탈퇴한 계정(DELETED) 로그인 → 410 Gone") + void login_deletedUser() throws Exception { + // given: 상태가 DELETED인 유저 저장 + User deleted = User.createUser("deleted", "deleted@example.com", + passwordEncoder.encode("P@ssw0rd!")); + deleted.setUserProfile(new UserProfile(deleted, "닉네임", null, null, null, 0)); + deleted.setUserStatus(UserStatus.DELETED); + userRepository.save(deleted); + + String body = """ + { + "username": "deleted", + "password": "P@ssw0rd!" + } + """; + + // when & then: 410 Gone 응답 + 에러 코드 USER_009 확인 + mvc.perform(post("/api/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andDo(print()) + .andExpect(status().isGone()) + .andExpect(jsonPath("$.code").value("USER_009")); + } + + @Test + @DisplayName("정상 로그아웃 → 200 OK + RefreshToken 쿠키 만료") + void logout_success() throws Exception { + // given: 회원가입 + 로그인으로 refreshToken 쿠키 확보 + String rawPassword = "P@ssw0rd!"; + String registerBody = """ + { + "username": "logoutuser", + "email": "logout@example.com", + "password": "%s", + "nickname": "홍길동" + } + """.formatted(rawPassword); + + mvc.perform(post("/api/auth/register") + .contentType(MediaType.APPLICATION_JSON) + .content(registerBody)) + .andExpect(status().isCreated()); + + String loginBody = """ + { + "username": "logoutuser", + "password": "%s" + } + """.formatted(rawPassword); + + ResultActions loginResult = mvc.perform(post("/api/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(loginBody)) + .andExpect(status().isOk()); + + // 로그인 응답에서 refreshToken 쿠키 추출 + String refreshCookie = loginResult.andReturn() + .getResponse() + .getCookie("refreshToken") + .getValue(); + + // when: 로그아웃 요청 (쿠키 포함) + ResultActions logoutResult = mvc.perform(post("/api/auth/logout") + .cookie(new Cookie("refreshToken", refreshCookie))) + .andDo(print()); + + // then: 200 OK + 성공 메시지 + 쿠키 만료 + logoutResult + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.message").value("로그아웃 되었습니다.")) + .andExpect(cookie().maxAge("refreshToken", 0)); + } + + @Test + @DisplayName("RefreshToken 누락 → 400 Bad Request") + void logout_noToken() throws Exception { + // when & then + mvc.perform(post("/api/auth/logout")) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("COMMON_400")); + } + + @Test + @DisplayName("유효하지 않은 RefreshToken → 401 Unauthorized") + void logout_invalidToken() throws Exception { + // given: 잘못된 refreshToken 쿠키 + Cookie invalid = new Cookie("refreshToken", "fake-token"); + + // when & then + mvc.perform(post("/api/auth/logout").cookie(invalid)) + .andDo(print()) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.code").value("AUTH_401")); + } + + @Test + @DisplayName("정상 토큰 재발급 → 200 OK + 새로운 AccessToken 반환") + void refreshToken_success() throws Exception { + // given: 회원가입 + 로그인 → 기존 토큰 확보 + String rawPassword = "P@ssw0rd!"; + String registerBody = """ + { + "username": "refreshuser", + "email": "refresh@example.com", + "password": "%s", + "nickname": "홍길동" + } + """.formatted(rawPassword); + + mvc.perform(post("/api/auth/register") + .contentType(MediaType.APPLICATION_JSON) + .content(registerBody)) + .andExpect(status().isCreated()); + + String loginBody = """ + { + "username": "refreshuser", + "password": "%s" + } + """.formatted(rawPassword); + + ResultActions loginResult = mvc.perform(post("/api/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(loginBody)) + .andExpect(status().isOk()); + + // 기존 AccessToken, RefreshToken 확보 + String oldAccessToken = loginResult.andReturn() + .getResponse() + .getContentAsString(); // body에서 accessToken 꺼낼 수 있도록 JSON 파싱 필요 + + String refreshCookie = loginResult.andReturn() + .getResponse() + .getCookie("refreshToken") + .getValue(); + + // when: 재발급 요청 (RefreshToken 쿠키 포함) + ResultActions refreshResult = mvc.perform(post("/api/auth/refresh") + .cookie(new Cookie("refreshToken", refreshCookie))) + .andDo(print()); + + // then: 200 OK + 새로운 AccessToken 발급 + refreshResult + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.accessToken").exists()); + + // 새 토큰은 기존 토큰과 달라야 함 (파싱 후 비교) +// String newAccessToken = JsonPath.read(refreshResult.andReturn() +// .getResponse().getContentAsString(), "$.data.accessToken"); +// assertThat(newAccessToken).isNotEqualTo(oldAccessToken); + } + + @Test + @DisplayName("RefreshToken 누락 → 400 Bad Request") + void refreshToken_noToken() throws Exception { + mvc.perform(post("/api/auth/refresh")) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("COMMON_400")); + } + + @Test + @DisplayName("유효하지 않은 RefreshToken → 401 Unauthorized") + void refreshToken_invalidToken() throws Exception { + Cookie invalid = new Cookie("refreshToken", "fake-token"); + + mvc.perform(post("/api/auth/refresh").cookie(invalid)) + .andDo(print()) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.code").value("AUTH_401")); + } + + @Test + @DisplayName("만료된 RefreshToken → 401 Unauthorized") + void refreshToken_expiredToken() throws Exception { + // given: 이미 만료된 Refresh Token 발급 + User user = User.createUser("expiredUser", "expired@example.com", passwordEncoder.encode("P@ssw0rd!")); + user.setUserProfile(new UserProfile(user, "닉네임", null, null, null, 0)); + userRepository.save(user); + + // JwtTokenProvider에 테스트용 메서드 추가 필요 + String expiredRefreshToken = testJwtTokenProvider.createExpiredRefreshToken(user.getId()); + + Cookie expiredCookie = new Cookie("refreshToken", expiredRefreshToken); + + // when & then + mvc.perform(post("/api/auth/refresh").cookie(expiredCookie)) + .andDo(print()) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.code").value("AUTH_401")) + .andExpect(jsonPath("$.message").value("만료된 리프레시 토큰입니다.")); + } +} \ No newline at end of file diff --git a/src/test/java/com/back/domain/user/service/UserServiceTest.java b/src/test/java/com/back/domain/user/service/UserServiceTest.java index a0a2cfda..a35ee38a 100644 --- a/src/test/java/com/back/domain/user/service/UserServiceTest.java +++ b/src/test/java/com/back/domain/user/service/UserServiceTest.java @@ -1,383 +1,386 @@ -//package com.back.domain.user.service; -// -//import com.back.domain.user.dto.LoginRequest; -//import com.back.domain.user.dto.UserRegisterRequest; -//import com.back.domain.user.dto.UserResponse; -//import com.back.domain.user.entity.User; -//import com.back.domain.user.entity.UserStatus; -//import com.back.domain.user.repository.UserProfileRepository; -//import com.back.domain.user.repository.UserRepository; -//import com.back.domain.user.repository.UserTokenRepository; -//import com.back.global.exception.CustomException; -//import com.back.global.exception.ErrorCode; -//import jakarta.servlet.http.Cookie; -//import org.junit.jupiter.api.DisplayName; -//import org.junit.jupiter.api.Test; -//import org.springframework.beans.factory.annotation.Autowired; -//import org.springframework.boot.test.context.SpringBootTest; -//import org.springframework.mock.web.MockHttpServletRequest; -//import org.springframework.mock.web.MockHttpServletResponse; -//import org.springframework.security.crypto.password.PasswordEncoder; -//import org.springframework.test.context.ActiveProfiles; -//import org.springframework.transaction.annotation.Transactional; -// -//import static org.assertj.core.api.Assertions.assertThat; -//import static org.assertj.core.api.Assertions.assertThatThrownBy; -// -//@SpringBootTest -//@Transactional -//@ActiveProfiles("test") -//class UserServiceTest { -// -// @Autowired -// private UserService userService; -// -// @Autowired -// private UserRepository userRepository; -// -// @Autowired -// private UserProfileRepository userProfileRepository; -// -// @Autowired -// private UserTokenRepository userTokenRepository; -// -// @Autowired -// private PasswordEncoder passwordEncoder; -// -// private User setupUser(String username, String email, String password, String nickname, UserStatus status) { -// UserRegisterRequest request = new UserRegisterRequest(username, email, password, nickname); -// UserResponse response = userService.register(request); -// -// User saved = userRepository.findById(response.userId()).orElseThrow(); -// saved.setUserStatus(status); // 상태 변경 (PENDING, SUSPENDED, DELETED) -// return saved; -// } -// -// @Test -// @DisplayName("정상 회원가입 성공") -// void register_success() { -// // given: 정상적인 회원가입 요청 생성 -// UserRegisterRequest request = new UserRegisterRequest( -// "testuser", "test@example.com", "P@ssw0rd!", "홍길동" -// ); -// -// // when: 회원가입 실행 -// UserResponse response = userService.register(request); -// -// // then: 반환된 값 검증 -// assertThat(response.username()).isEqualTo("testuser"); -// assertThat(response.email()).isEqualTo("test@example.com"); -// assertThat(response.nickname()).isEqualTo("홍길동"); -// // TODO: 이메일 인증 기능 개발 후 기본 상태를 PENDING으로 변경 -//// assertThat(response.status()).isEqualTo(UserStatus.PENDING); -// -// // 비밀번호 인코딩 검증 -// String encoded = userRepository.findById(response.userId()).get().getPassword(); -// assertThat(passwordEncoder.matches("P@ssw0rd!", encoded)).isTrue(); -// -// // UserProfile도 함께 생성되었는지 확인 -// assertThat(userProfileRepository.existsByNickname("홍길동")).isTrue(); -// } -// -// @Test -// @DisplayName("중복된 username이면 예외 발생") -// void register_duplicateUsername() { -// // given: 동일 username으로 첫 번째 가입 -// userService.register(new UserRegisterRequest( -// "dupuser", "dup@example.com", "P@ssw0rd!", "닉네임" -// )); -// -// // when & then: 같은 username으로 가입 시 예외 발생 -// assertThatThrownBy(() -> -// userService.register(new UserRegisterRequest( -// "dupuser", "other@example.com", "P@ssw0rd!", "다른닉네임" -// )) -// ).isInstanceOf(CustomException.class) -// .hasMessage(ErrorCode.USERNAME_DUPLICATED.getMessage()); -// } -// -// @Test -// @DisplayName("중복된 email이면 예외 발생") -// void register_duplicateEmail() { -// // given: 동일 email로 첫 번째 가입 -// userService.register(new UserRegisterRequest( -// "user1", "dup@example.com", "P@ssw0rd!", "닉네임" -// )); -// -// // when & then: 같은 email로 가입 시 예외 발생 -// assertThatThrownBy(() -> -// userService.register(new UserRegisterRequest( -// "user2", "dup@example.com", "P@ssw0rd!", "다른닉네임" -// )) -// ).isInstanceOf(CustomException.class) -// .hasMessage(ErrorCode.EMAIL_DUPLICATED.getMessage()); -// } -// -// @Test -// @DisplayName("중복된 nickname이면 예외 발생") -// void register_duplicateNickname() { -// // given: 동일 nickname으로 첫 번째 가입 -// userService.register(new UserRegisterRequest( -// "user1", "user1@example.com", "P@ssw0rd!", "dupnick" -// )); -// -// // when & then: 같은 nickname으로 가입 시 예외 발생 -// assertThatThrownBy(() -> -// userService.register(new UserRegisterRequest( -// "user2", "user2@example.com", "P@ssw0rd!", "dupnick" -// )) -// ).isInstanceOf(CustomException.class) -// .hasMessage(ErrorCode.NICKNAME_DUPLICATED.getMessage()); -// } -// -// @Test -// @DisplayName("비밀번호 정책 위반(숫자/특수문자 없음) → 예외 발생") -// void register_invalidPassword_noNumberOrSpecial() { -// // given: 숫자, 특수문자 없는 비밀번호 -// UserRegisterRequest request = new UserRegisterRequest( -// "user1", "user1@example.com", "abcdefgh", "닉네임" -// ); -// -// // when & then: 정책 위반으로 예외 발생 -// assertThatThrownBy(() -> userService.register(request)) -// .isInstanceOf(CustomException.class) -// .hasMessage(ErrorCode.INVALID_PASSWORD.getMessage()); -// } -// -// @Test -// @DisplayName("비밀번호 정책 위반(길이 7자) → 예외 발생") -// void register_invalidPassword_tooShort() { -// // given: 7자리 비밀번호 (정책상 8자 이상 필요) -// UserRegisterRequest request = new UserRegisterRequest( -// "user2", "user2@example.com", "Abc12!", "닉네임" -// ); -// -// // when & then: 정책 위반으로 예외 발생 -// assertThatThrownBy(() -> userService.register(request)) -// .isInstanceOf(CustomException.class) -// .hasMessage(ErrorCode.INVALID_PASSWORD.getMessage()); -// } -// -// @Test -// @DisplayName("비밀번호 정책 통과(정상 8자 이상, 숫자/특수문자 포함) → 성공") -// void register_validPassword() { -// // given: 정책을 만족하는 정상 비밀번호 -// UserRegisterRequest request = new UserRegisterRequest( -// "user3", "user3@example.com", "Abcd123!", "닉네임" -// ); -// -// // when: 회원가입 실행 -// UserResponse response = userService.register(request); -// -// // then: username과 비밀번호 인코딩 검증 -// assertThat(response.username()).isEqualTo("user3"); -// assertThat(passwordEncoder.matches("Abcd123!", -// userRepository.findById(response.userId()).get().getPassword())).isTrue(); -// } -// -// @Test -// @DisplayName("정상 로그인 성공") -// void login_success() { -// // given: 정상적인 사용자와 비밀번호 준비 -// String rawPassword = "P@ssw0rd!"; -// User user = setupUser("loginuser", "login@example.com", rawPassword, "닉네임", UserStatus.ACTIVE); -// MockHttpServletResponse response = new MockHttpServletResponse(); -// -// // when: 로그인 요청 실행 -// UserResponse userResponse = userService.login( -// new LoginRequest("loginuser", rawPassword), response); -// -// // then: 응답에 username과 토큰/쿠키가 포함됨 -// assertThat(userResponse.username()).isEqualTo("loginuser"); -// assertThat(response.getHeader("Authorization")).startsWith("Bearer "); -// Cookie refreshCookie = response.getCookie("refreshToken"); -// assertThat(refreshCookie).isNotNull(); -// assertThat(refreshCookie.isHttpOnly()).isTrue(); -// } -// -// @Test -// @DisplayName("잘못된 비밀번호 → INVALID_CREDENTIALS 예외 발생") -// void login_invalidPassword() { -// // given: 존재하는 사용자, 잘못된 비밀번호 입력 -// User user = setupUser("loginuser", "login@example.com", "P@ssw0rd!", "닉네임", UserStatus.ACTIVE); -// MockHttpServletResponse response = new MockHttpServletResponse(); -// -// // when & then: 로그인 시도 시 INVALID_CREDENTIALS 예외 발생 -// assertThatThrownBy(() -> userService.login( -// new LoginRequest("loginuser", "wrongPassword"), response -// )) -// .isInstanceOf(CustomException.class) -// .hasMessage(ErrorCode.INVALID_CREDENTIALS.getMessage()); -// } -// -// @Test -// @DisplayName("존재하지 않는 username → INVALID_CREDENTIALS 예외 발생") -// void login_userNotFound() { -// // given: 존재하지 않는 username 사용 -// MockHttpServletResponse response = new MockHttpServletResponse(); -// -// // when & then: 로그인 시도 시 INVALID_CREDENTIALS 예외 발생 -// assertThatThrownBy(() -> userService.login( -// new LoginRequest("nouser", "P@ssw0rd!"), response -// )) -// .isInstanceOf(CustomException.class) -// .hasMessage(ErrorCode.INVALID_CREDENTIALS.getMessage()); -// } -// -// @Test -// @DisplayName("상태가 PENDING → USER_EMAIL_NOT_VERIFIED 예외 발생") -// void login_pendingUser() { -// // given: 상태가 PENDING인 사용자 -// User user = setupUser("pendinguser", "pending@example.com", "P@ssw0rd!", "닉네임", UserStatus.PENDING); -// MockHttpServletResponse response = new MockHttpServletResponse(); -// -// // when & then: 로그인 시도 시 USER_EMAIL_NOT_VERIFIED 예외 발생 -// assertThatThrownBy(() -> userService.login( -// new LoginRequest(user.getUsername(), "P@ssw0rd!"), response -// )) -// .isInstanceOf(CustomException.class) -// .hasMessage(ErrorCode.USER_EMAIL_NOT_VERIFIED.getMessage()); -// } -// -// @Test -// @DisplayName("상태가 SUSPENDED → USER_SUSPENDED 예외 발생") -// void login_suspendedUser() { -// // given: 상태가 SUSPENDED인 사용자 -// User user = setupUser("suspended", "suspended@example.com", "P@ssw0rd!", "닉네임", UserStatus.SUSPENDED); -// MockHttpServletResponse response = new MockHttpServletResponse(); -// -// // when & then: 로그인 시도 시 USER_SUSPENDED 예외 발생 -// assertThatThrownBy(() -> userService.login( -// new LoginRequest(user.getUsername(), "P@ssw0rd!"), response -// )) -// .isInstanceOf(CustomException.class) -// .hasMessage(ErrorCode.USER_SUSPENDED.getMessage()); -// } -// -// @Test -// @DisplayName("상태가 DELETED → USER_DELETED 예외 발생") -// void login_deletedUser() { -// // given: 상태가 DELETED인 사용자 -// User user = setupUser("deleted", "deleted@example.com", "P@ssw0rd!", "닉네임", UserStatus.DELETED); -// MockHttpServletResponse response = new MockHttpServletResponse(); -// -// // when & then: 로그인 시도 시 USER_DELETED 예외 발생 -// assertThatThrownBy(() -> userService.login( -// new LoginRequest(user.getUsername(), "P@ssw0rd!"), response -// )) -// .isInstanceOf(CustomException.class) -// .hasMessage(ErrorCode.USER_DELETED.getMessage()); -// } -// @Test -// @DisplayName("정상 로그아웃 성공 → RefreshToken DB 삭제 + 쿠키 만료") -// void logout_success() { -// // given: 정상 로그인된 사용자 -// String rawPassword = "P@ssw0rd!"; -// User user = setupUser("logoutuser", "logout@example.com", rawPassword, "닉네임", UserStatus.ACTIVE); -// MockHttpServletResponse loginResponse = new MockHttpServletResponse(); -// -// userService.login(new LoginRequest("logoutuser", rawPassword), loginResponse); -// Cookie refreshCookie = loginResponse.getCookie("refreshToken"); -// assertThat(refreshCookie).isNotNull(); -// -// MockHttpServletResponse logoutResponse = new MockHttpServletResponse(); -// MockHttpServletRequest request = new MockHttpServletRequest(); -// request.setCookies(refreshCookie); // 쿠키를 요청에 실어줌 -// -// // when: 로그아웃 실행 -// userService.logout(request, logoutResponse); -// -// // then: DB에서 refreshToken 삭제됨 -// assertThat(userTokenRepository.findByRefreshToken(refreshCookie.getValue())).isEmpty(); -// -// // 응답 쿠키는 만료 처리됨 -// Cookie cleared = logoutResponse.getCookie("refreshToken"); -// assertThat(cleared).isNotNull(); -// assertThat(cleared.getMaxAge()).isZero(); -// assertThat(cleared.getValue()).isNull(); -// } -// -// @Test -// @DisplayName("RefreshToken 없으면 INVALID_TOKEN 예외 발생") -// void logout_noToken() { -// // given: 쿠키 없이 로그아웃 요청 -// MockHttpServletRequest request = new MockHttpServletRequest(); -// MockHttpServletResponse response = new MockHttpServletResponse(); -// -// // when & then -// assertThatThrownBy(() -> userService.logout(request, response)) -// .isInstanceOf(CustomException.class) -// .hasMessage(ErrorCode.BAD_REQUEST.getMessage()); -// } -// -// @Test -// @DisplayName("유효하지 않은 RefreshToken이면 INVALID_TOKEN 예외 발생") -// void logout_invalidToken() { -// // given: 잘못된 토큰 쿠키 세팅 -// MockHttpServletRequest request = new MockHttpServletRequest(); -// request.setCookies(new Cookie("refreshToken", "invalidToken")); -// MockHttpServletResponse response = new MockHttpServletResponse(); -// -// // when & then -// assertThatThrownBy(() -> userService.logout(request, response)) -// .isInstanceOf(CustomException.class) -// .hasMessage(ErrorCode.INVALID_TOKEN.getMessage()); -// } -// -// @Test -// @DisplayName("정상 토큰 재발급 성공 → 새로운 AccessToken 반환 및 헤더 설정") -// void refreshToken_success() throws InterruptedException { -// // given: 로그인된 사용자 준비 -// String rawPassword = "P@ssw0rd!"; -// User user = setupUser("refreshuser", "refresh@example.com", rawPassword, "닉네임", UserStatus.ACTIVE); -// MockHttpServletResponse loginResponse = new MockHttpServletResponse(); -// -// userService.login(new LoginRequest("refreshuser", rawPassword), loginResponse); -// String oldAccessToken = loginResponse.getHeader("Authorization").substring(7); -// Cookie refreshCookie = loginResponse.getCookie("refreshToken"); -// assertThat(refreshCookie).isNotNull(); -// -// // 요청/응답 객체 준비 -// MockHttpServletRequest request = new MockHttpServletRequest(); -// request.setCookies(refreshCookie); -// MockHttpServletResponse response = new MockHttpServletResponse(); -// -// // Issued At(발급 시간) 분리를 위해 1초 대기 -//// Thread.sleep(1000); -// -// // when: 토큰 재발급 실행 -// String newAccessToken = userService.refreshToken(request, response); -// -// // then: 반환값 및 응답 헤더 검증 -// assertThat(newAccessToken).isNotBlank(); -//// assertThat(newAccessToken).isNotEqualTo(oldAccessToken); -// assertThat(response.getHeader("Authorization")).isEqualTo("Bearer " + newAccessToken); -// } -// -// @Test -// @DisplayName("RefreshToken 없으면 BAD_REQUEST 예외 발생") -// void refreshToken_noToken() { -// // given: 쿠키 없는 요청 -// MockHttpServletRequest request = new MockHttpServletRequest(); -// MockHttpServletResponse response = new MockHttpServletResponse(); -// -// // when & then -// assertThatThrownBy(() -> userService.refreshToken(request, response)) -// .isInstanceOf(CustomException.class) -// .hasMessage(ErrorCode.BAD_REQUEST.getMessage()); -// } -// -// @Test -// @DisplayName("유효하지 않은 RefreshToken이면 INVALID_TOKEN 예외 발생") -// void refreshToken_invalidToken() { -// // given: 잘못된 Refresh Token -// MockHttpServletRequest request = new MockHttpServletRequest(); -// request.setCookies(new Cookie("refreshToken", "invalidToken")); -// MockHttpServletResponse response = new MockHttpServletResponse(); -// -// // when & then -// assertThatThrownBy(() -> userService.refreshToken(request, response)) -// .isInstanceOf(CustomException.class) -// .hasMessage(ErrorCode.INVALID_TOKEN.getMessage()); -// } -//} \ No newline at end of file +package com.back.domain.user.service; + +import com.back.domain.user.dto.LoginRequest; +import com.back.domain.user.dto.LoginResponse; +import com.back.domain.user.dto.UserRegisterRequest; +import com.back.domain.user.dto.UserResponse; +import com.back.domain.user.entity.User; +import com.back.domain.user.entity.UserStatus; +import com.back.domain.user.repository.UserProfileRepository; +import com.back.domain.user.repository.UserRepository; +import com.back.domain.user.repository.UserTokenRepository; +import com.back.global.exception.CustomException; +import com.back.global.exception.ErrorCode; +import jakarta.servlet.http.Cookie; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@SpringBootTest +@Transactional +@ActiveProfiles("test") +class UserServiceTest { + + @Autowired + private UserService userService; + + @Autowired + private UserRepository userRepository; + + @Autowired + private UserProfileRepository userProfileRepository; + + @Autowired + private UserTokenRepository userTokenRepository; + + @Autowired + private PasswordEncoder passwordEncoder; + + private User setupUser(String username, String email, String password, String nickname, UserStatus status) { + UserRegisterRequest request = new UserRegisterRequest(username, email, password, nickname); + UserResponse response = userService.register(request); + + User saved = userRepository.findById(response.userId()).orElseThrow(); + saved.setUserStatus(status); // 상태 변경 (PENDING, SUSPENDED, DELETED) + return saved; + } + + @Test + @DisplayName("정상 회원가입 성공") + void register_success() { + // given: 정상적인 회원가입 요청 생성 + UserRegisterRequest request = new UserRegisterRequest( + "testuser", "test@example.com", "P@ssw0rd!", "홍길동" + ); + + // when: 회원가입 실행 + UserResponse response = userService.register(request); + + // then: 반환된 값 검증 + assertThat(response.username()).isEqualTo("testuser"); + assertThat(response.email()).isEqualTo("test@example.com"); + assertThat(response.nickname()).isEqualTo("홍길동"); + // TODO: 이메일 인증 기능 개발 후 기본 상태를 PENDING으로 변경 +// assertThat(response.status()).isEqualTo(UserStatus.PENDING); + + // 비밀번호 인코딩 검증 + String encoded = userRepository.findById(response.userId()).get().getPassword(); + assertThat(passwordEncoder.matches("P@ssw0rd!", encoded)).isTrue(); + + // UserProfile도 함께 생성되었는지 확인 + assertThat(userProfileRepository.existsByNickname("홍길동")).isTrue(); + } + + @Test + @DisplayName("중복된 username이면 예외 발생") + void register_duplicateUsername() { + // given: 동일 username으로 첫 번째 가입 + userService.register(new UserRegisterRequest( + "dupuser", "dup@example.com", "P@ssw0rd!", "닉네임" + )); + + // when & then: 같은 username으로 가입 시 예외 발생 + assertThatThrownBy(() -> + userService.register(new UserRegisterRequest( + "dupuser", "other@example.com", "P@ssw0rd!", "다른닉네임" + )) + ).isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.USERNAME_DUPLICATED.getMessage()); + } + + @Test + @DisplayName("중복된 email이면 예외 발생") + void register_duplicateEmail() { + // given: 동일 email로 첫 번째 가입 + userService.register(new UserRegisterRequest( + "user1", "dup@example.com", "P@ssw0rd!", "닉네임" + )); + + // when & then: 같은 email로 가입 시 예외 발생 + assertThatThrownBy(() -> + userService.register(new UserRegisterRequest( + "user2", "dup@example.com", "P@ssw0rd!", "다른닉네임" + )) + ).isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.EMAIL_DUPLICATED.getMessage()); + } + + @Test + @DisplayName("중복된 nickname이면 예외 발생") + void register_duplicateNickname() { + // given: 동일 nickname으로 첫 번째 가입 + userService.register(new UserRegisterRequest( + "user1", "user1@example.com", "P@ssw0rd!", "dupnick" + )); + + // when & then: 같은 nickname으로 가입 시 예외 발생 + assertThatThrownBy(() -> + userService.register(new UserRegisterRequest( + "user2", "user2@example.com", "P@ssw0rd!", "dupnick" + )) + ).isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.NICKNAME_DUPLICATED.getMessage()); + } + + @Test + @DisplayName("비밀번호 정책 위반(숫자/특수문자 없음) → 예외 발생") + void register_invalidPassword_noNumberOrSpecial() { + // given: 숫자, 특수문자 없는 비밀번호 + UserRegisterRequest request = new UserRegisterRequest( + "user1", "user1@example.com", "abcdefgh", "닉네임" + ); + + // when & then: 정책 위반으로 예외 발생 + assertThatThrownBy(() -> userService.register(request)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.INVALID_PASSWORD.getMessage()); + } + + @Test + @DisplayName("비밀번호 정책 위반(길이 7자) → 예외 발생") + void register_invalidPassword_tooShort() { + // given: 7자리 비밀번호 (정책상 8자 이상 필요) + UserRegisterRequest request = new UserRegisterRequest( + "user2", "user2@example.com", "Abc12!", "닉네임" + ); + + // when & then: 정책 위반으로 예외 발생 + assertThatThrownBy(() -> userService.register(request)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.INVALID_PASSWORD.getMessage()); + } + + @Test + @DisplayName("비밀번호 정책 통과(정상 8자 이상, 숫자/특수문자 포함) → 성공") + void register_validPassword() { + // given: 정책을 만족하는 정상 비밀번호 + UserRegisterRequest request = new UserRegisterRequest( + "user3", "user3@example.com", "Abcd123!", "닉네임" + ); + + // when: 회원가입 실행 + UserResponse response = userService.register(request); + + // then: username과 비밀번호 인코딩 검증 + assertThat(response.username()).isEqualTo("user3"); + assertThat(passwordEncoder.matches("Abcd123!", + userRepository.findById(response.userId()).get().getPassword())).isTrue(); + } + + @Test + @DisplayName("정상 로그인 성공") + void login_success() { + // given: 정상적인 사용자와 비밀번호 준비 + String rawPassword = "P@ssw0rd!"; + User user = setupUser("loginuser", "login@example.com", rawPassword, "닉네임", UserStatus.ACTIVE); + MockHttpServletResponse response = new MockHttpServletResponse(); + + // when: 로그인 요청 실행 + LoginResponse loginResponse = userService.login( + new LoginRequest("loginuser", rawPassword), response); + + // then: 응답에 username과 토큰/쿠키가 포함됨 + assertThat(loginResponse.user().username()).isEqualTo("loginuser"); + assertThat(loginResponse.accessToken()).isNotBlank(); + + Cookie refreshCookie = response.getCookie("refreshToken"); + assertThat(refreshCookie).isNotNull(); + assertThat(refreshCookie.isHttpOnly()).isTrue(); + } + + + @Test + @DisplayName("잘못된 비밀번호 → INVALID_CREDENTIALS 예외 발생") + void login_invalidPassword() { + // given: 존재하는 사용자, 잘못된 비밀번호 입력 + User user = setupUser("loginuser", "login@example.com", "P@ssw0rd!", "닉네임", UserStatus.ACTIVE); + MockHttpServletResponse response = new MockHttpServletResponse(); + + // when & then: 로그인 시도 시 INVALID_CREDENTIALS 예외 발생 + assertThatThrownBy(() -> userService.login( + new LoginRequest("loginuser", "wrongPassword"), response + )) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.INVALID_CREDENTIALS.getMessage()); + } + + @Test + @DisplayName("존재하지 않는 username → INVALID_CREDENTIALS 예외 발생") + void login_userNotFound() { + // given: 존재하지 않는 username 사용 + MockHttpServletResponse response = new MockHttpServletResponse(); + + // when & then: 로그인 시도 시 INVALID_CREDENTIALS 예외 발생 + assertThatThrownBy(() -> userService.login( + new LoginRequest("nouser", "P@ssw0rd!"), response + )) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.INVALID_CREDENTIALS.getMessage()); + } + + @Test + @DisplayName("상태가 PENDING → USER_EMAIL_NOT_VERIFIED 예외 발생") + void login_pendingUser() { + // given: 상태가 PENDING인 사용자 + User user = setupUser("pendinguser", "pending@example.com", "P@ssw0rd!", "닉네임", UserStatus.PENDING); + MockHttpServletResponse response = new MockHttpServletResponse(); + + // when & then: 로그인 시도 시 USER_EMAIL_NOT_VERIFIED 예외 발생 + assertThatThrownBy(() -> userService.login( + new LoginRequest(user.getUsername(), "P@ssw0rd!"), response + )) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.USER_EMAIL_NOT_VERIFIED.getMessage()); + } + + @Test + @DisplayName("상태가 SUSPENDED → USER_SUSPENDED 예외 발생") + void login_suspendedUser() { + // given: 상태가 SUSPENDED인 사용자 + User user = setupUser("suspended", "suspended@example.com", "P@ssw0rd!", "닉네임", UserStatus.SUSPENDED); + MockHttpServletResponse response = new MockHttpServletResponse(); + + // when & then: 로그인 시도 시 USER_SUSPENDED 예외 발생 + assertThatThrownBy(() -> userService.login( + new LoginRequest(user.getUsername(), "P@ssw0rd!"), response + )) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.USER_SUSPENDED.getMessage()); + } + + @Test + @DisplayName("상태가 DELETED → USER_DELETED 예외 발생") + void login_deletedUser() { + // given: 상태가 DELETED인 사용자 + User user = setupUser("deleted", "deleted@example.com", "P@ssw0rd!", "닉네임", UserStatus.DELETED); + MockHttpServletResponse response = new MockHttpServletResponse(); + + // when & then: 로그인 시도 시 USER_DELETED 예외 발생 + assertThatThrownBy(() -> userService.login( + new LoginRequest(user.getUsername(), "P@ssw0rd!"), response + )) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.USER_DELETED.getMessage()); + } + @Test + @DisplayName("정상 로그아웃 성공 → RefreshToken DB 삭제 + 쿠키 만료") + void logout_success() { + // given: 정상 로그인된 사용자 + String rawPassword = "P@ssw0rd!"; + User user = setupUser("logoutuser", "logout@example.com", rawPassword, "닉네임", UserStatus.ACTIVE); + MockHttpServletResponse loginResponse = new MockHttpServletResponse(); + + userService.login(new LoginRequest("logoutuser", rawPassword), loginResponse); + Cookie refreshCookie = loginResponse.getCookie("refreshToken"); + assertThat(refreshCookie).isNotNull(); + + MockHttpServletResponse logoutResponse = new MockHttpServletResponse(); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setCookies(refreshCookie); // 쿠키를 요청에 실어줌 + + // when: 로그아웃 실행 + userService.logout(request, logoutResponse); + + // then: DB에서 refreshToken 삭제됨 + assertThat(userTokenRepository.findByRefreshToken(refreshCookie.getValue())).isEmpty(); + + // 응답 쿠키는 만료 처리됨 + Cookie cleared = logoutResponse.getCookie("refreshToken"); + assertThat(cleared).isNotNull(); + assertThat(cleared.getMaxAge()).isZero(); + assertThat(cleared.getValue()).isNull(); + } + + @Test + @DisplayName("RefreshToken 없으면 INVALID_TOKEN 예외 발생") + void logout_noToken() { + // given: 쿠키 없이 로그아웃 요청 + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + + // when & then + assertThatThrownBy(() -> userService.logout(request, response)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.BAD_REQUEST.getMessage()); + } + + @Test + @DisplayName("유효하지 않은 RefreshToken이면 INVALID_TOKEN 예외 발생") + void logout_invalidToken() { + // given: 잘못된 토큰 쿠키 세팅 + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setCookies(new Cookie("refreshToken", "invalidToken")); + MockHttpServletResponse response = new MockHttpServletResponse(); + + // when & then + assertThatThrownBy(() -> userService.logout(request, response)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.INVALID_TOKEN.getMessage()); + } + + @Test + @DisplayName("정상 토큰 재발급 성공 → 새로운 AccessToken 반환 및 헤더 설정") + void refreshToken_success() throws InterruptedException { + // given: 로그인된 사용자 준비 + String rawPassword = "P@ssw0rd!"; + User user = setupUser("refreshuser", "refresh@example.com", rawPassword, "닉네임", UserStatus.ACTIVE); + MockHttpServletResponse loginResponse = new MockHttpServletResponse(); + + LoginResponse loginRes = userService.login(new LoginRequest("refreshuser", rawPassword), loginResponse); + String oldAccessToken = loginRes.accessToken(); + Cookie refreshCookie = loginResponse.getCookie("refreshToken"); + assertThat(refreshCookie).isNotNull(); + + // 요청/응답 객체 준비 + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setCookies(refreshCookie); + MockHttpServletResponse response = new MockHttpServletResponse(); + + // Issued At(발급 시간) 분리를 위해 1초 대기 +// Thread.sleep(1000); + + // when: 토큰 재발급 실행 + String newAccessToken = userService.refreshToken(request, response); + + // then: 반환값 및 응답 헤더 검증 + assertThat(newAccessToken).isNotBlank(); +// assertThat(newAccessToken).isNotEqualTo(oldAccessToken); + assertThat(response.getHeader("Authorization")).isEqualTo("Bearer " + newAccessToken); + } + + @Test + @DisplayName("RefreshToken 없으면 BAD_REQUEST 예외 발생") + void refreshToken_noToken() { + // given: 쿠키 없는 요청 + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + + // when & then + assertThatThrownBy(() -> userService.refreshToken(request, response)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.BAD_REQUEST.getMessage()); + } + + @Test + @DisplayName("유효하지 않은 RefreshToken이면 INVALID_TOKEN 예외 발생") + void refreshToken_invalidToken() { + // given: 잘못된 Refresh Token + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setCookies(new Cookie("refreshToken", "invalidToken")); + MockHttpServletResponse response = new MockHttpServletResponse(); + + // when & then + assertThatThrownBy(() -> userService.refreshToken(request, response)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.INVALID_TOKEN.getMessage()); + } +} \ No newline at end of file