diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/src/main/java/until/the/eternity/das/auth/dto/response/SignUpResponse.java b/src/main/java/until/the/eternity/das/auth/dto/response/SignUpResponse.java index 177f380..1d7f10c 100644 --- a/src/main/java/until/the/eternity/das/auth/dto/response/SignUpResponse.java +++ b/src/main/java/until/the/eternity/das/auth/dto/response/SignUpResponse.java @@ -1,10 +1,10 @@ package until.the.eternity.das.auth.dto.response; -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; - import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + @Builder public record SignUpResponse( diff --git a/src/main/java/until/the/eternity/das/auth/presentation/AuthController.java b/src/main/java/until/the/eternity/das/auth/presentation/AuthController.java index b182fdc..30a93aa 100644 --- a/src/main/java/until/the/eternity/das/auth/presentation/AuthController.java +++ b/src/main/java/until/the/eternity/das/auth/presentation/AuthController.java @@ -11,13 +11,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import until.the.eternity.das.auth.application.AuthService; import until.the.eternity.das.auth.dto.request.LoginRequest; import until.the.eternity.das.auth.dto.request.SignUpRequest; @@ -26,9 +20,13 @@ import until.the.eternity.das.auth.dto.response.LoginResultResponse; import until.the.eternity.das.auth.dto.response.SignUpResponse; import until.the.eternity.das.common.constant.JwtConstant; +import until.the.eternity.das.common.exception.CustomException; +import until.the.eternity.das.common.exception.GlobalExceptionCode; import until.the.eternity.das.common.response.CommonResponse; import until.the.eternity.das.common.util.CookieUtil; +import until.the.eternity.das.common.util.JwtUtil; import until.the.eternity.das.oauth.service.SocialAuthService; +import until.the.eternity.das.token.application.TokenService; import static org.springframework.http.HttpStatus.CREATED; @@ -38,8 +36,10 @@ public class AuthController { private final AuthService authService; + private final TokenService tokenService; private final SocialAuthService socialAuthService; private final CookieUtil cookieUtil; + private final JwtUtil jwtUtil; private final JwtConstant jwtConstant; /** @@ -209,4 +209,38 @@ public ResponseEntity> login( return ResponseEntity.ok(CommonResponse.success(loginResponse)); } + + /** + * 토큰 갱신 API + * + * @param request 쿠키에서 refresh token을 추출하기 위한 HttpRequest + * @param response 새 토큰을 쿠키에 담기 위한 HttpResponse + * @return 갱신된 사용자 정보 + */ + @PostMapping("/refresh") + @Operation(summary = "토큰 갱신 API", description = """ + - Description : 리프레시 토큰을 사용하여 새로운 액세스 토큰과 리프레시 토큰을 발급합니다. + - 기존 리프레시 토큰은 revoke되고 새로운 토큰이 발급됩니다 (토큰 로테이션). + """) + @ApiResponse( + responseCode = "200", + content = @Content(schema = @Schema(implementation = LoginResponse.class))) + public ResponseEntity> refresh( + HttpServletRequest request, + HttpServletResponse response + ) { + String refreshToken = jwtUtil.extractRefreshToken(request); + if (refreshToken == null) { + throw new CustomException(GlobalExceptionCode.INVALID_REFRESH_TOKEN); + } + + LoginResultResponse loginResultResponse = tokenService.refresh(refreshToken); + + cookieUtil.createAccessTokenCookie(response, loginResultResponse.accessToken()); + cookieUtil.createRefreshTokenCookie(response, loginResultResponse.refreshToken()); + + LoginResponse loginResponse = LoginResponse.from(loginResultResponse.user()); + + return ResponseEntity.ok(CommonResponse.success(loginResponse)); + } } diff --git a/src/main/java/until/the/eternity/das/common/exception/GlobalExceptionCode.java b/src/main/java/until/the/eternity/das/common/exception/GlobalExceptionCode.java index ae9dc64..de2dc5a 100644 --- a/src/main/java/until/the/eternity/das/common/exception/GlobalExceptionCode.java +++ b/src/main/java/until/the/eternity/das/common/exception/GlobalExceptionCode.java @@ -15,6 +15,7 @@ public enum GlobalExceptionCode implements ExceptionCode { // JWT TOKEN INVALID_TOKEN(UNAUTHORIZED, "유효하지 않는 토큰입니다."), EXPIRED_TOKEN(UNAUTHORIZED, "만료된 토큰입니다."), + INVALID_REFRESH_TOKEN(UNAUTHORIZED, "유효하지 않은 리프레시 토큰입니다."), // AUTH EMAIL_ALREADY_EXISTS(HttpStatus.CONFLICT, "이미 가입되어 있는 이메일입니다."), diff --git a/src/main/java/until/the/eternity/das/common/util/JwtUtil.java b/src/main/java/until/the/eternity/das/common/util/JwtUtil.java index 085afa3..783696b 100644 --- a/src/main/java/until/the/eternity/das/common/util/JwtUtil.java +++ b/src/main/java/until/the/eternity/das/common/util/JwtUtil.java @@ -1,10 +1,6 @@ package until.the.eternity.das.common.util; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.ExpiredJwtException; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.MalformedJwtException; -import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.*; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; import jakarta.servlet.http.Cookie; diff --git a/src/main/java/until/the/eternity/das/login/entity/AccountLock.java b/src/main/java/until/the/eternity/das/login/entity/AccountLock.java index c0fbc19..6f9376c 100644 --- a/src/main/java/until/the/eternity/das/login/entity/AccountLock.java +++ b/src/main/java/until/the/eternity/das/login/entity/AccountLock.java @@ -1,19 +1,8 @@ package until.the.eternity.das.login.entity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.MapsId; -import jakarta.persistence.OneToOne; -import jakarta.persistence.Table; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import jakarta.persistence.*; +import lombok.*; import org.hibernate.annotations.Comment; import until.the.eternity.das.user.entity.User; diff --git a/src/main/java/until/the/eternity/das/role/entity/Role.java b/src/main/java/until/the/eternity/das/role/entity/Role.java index 821f683..6cc2793 100644 --- a/src/main/java/until/the/eternity/das/role/entity/Role.java +++ b/src/main/java/until/the/eternity/das/role/entity/Role.java @@ -1,19 +1,8 @@ package until.the.eternity.das.role.entity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.Table; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import jakarta.persistence.*; +import lombok.*; import until.the.eternity.das.role.entity.enums.Name; @Entity diff --git a/src/main/java/until/the/eternity/das/role/entity/RoleRepository.java b/src/main/java/until/the/eternity/das/role/entity/RoleRepository.java index e15754c..12c8244 100644 --- a/src/main/java/until/the/eternity/das/role/entity/RoleRepository.java +++ b/src/main/java/until/the/eternity/das/role/entity/RoleRepository.java @@ -1,8 +1,9 @@ package until.the.eternity.das.role.entity; -import java.util.Optional; import until.the.eternity.das.role.entity.enums.Name; +import java.util.Optional; + public interface RoleRepository { Optional findByName(Name name); diff --git a/src/main/java/until/the/eternity/das/role/infrastructure/JpaRoleRepository.java b/src/main/java/until/the/eternity/das/role/infrastructure/JpaRoleRepository.java index b7b378e..30ef135 100644 --- a/src/main/java/until/the/eternity/das/role/infrastructure/JpaRoleRepository.java +++ b/src/main/java/until/the/eternity/das/role/infrastructure/JpaRoleRepository.java @@ -1,10 +1,11 @@ package until.the.eternity.das.role.infrastructure; -import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import until.the.eternity.das.role.entity.Role; import until.the.eternity.das.role.entity.enums.Name; +import java.util.Optional; + public interface JpaRoleRepository extends JpaRepository { Optional findByName(Name name); diff --git a/src/main/java/until/the/eternity/das/role/infrastructure/RoleRepositoryImpl.java b/src/main/java/until/the/eternity/das/role/infrastructure/RoleRepositoryImpl.java index f6f2d2b..f3300e3 100644 --- a/src/main/java/until/the/eternity/das/role/infrastructure/RoleRepositoryImpl.java +++ b/src/main/java/until/the/eternity/das/role/infrastructure/RoleRepositoryImpl.java @@ -1,12 +1,13 @@ package until.the.eternity.das.role.infrastructure; -import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; import until.the.eternity.das.role.entity.Role; import until.the.eternity.das.role.entity.RoleRepository; import until.the.eternity.das.role.entity.enums.Name; +import java.util.Optional; + @Repository @RequiredArgsConstructor public class RoleRepositoryImpl implements RoleRepository { diff --git a/src/main/java/until/the/eternity/das/token/application/TokenService.java b/src/main/java/until/the/eternity/das/token/application/TokenService.java index 09b3555..2320a1e 100644 --- a/src/main/java/until/the/eternity/das/token/application/TokenService.java +++ b/src/main/java/until/the/eternity/das/token/application/TokenService.java @@ -4,6 +4,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import until.the.eternity.das.auth.dto.response.LoginResultResponse; import until.the.eternity.das.common.exception.CustomException; import until.the.eternity.das.common.exception.GlobalExceptionCode; import until.the.eternity.das.common.util.JwtUtil; @@ -11,6 +12,7 @@ import until.the.eternity.das.token.entity.RefreshTokenRepository; import until.the.eternity.das.user.entity.User; import until.the.eternity.das.user.entity.UserRepository; +import until.the.eternity.das.user.entity.enums.Status; import java.time.LocalDateTime; import java.util.List; @@ -46,6 +48,48 @@ public void saveNewRefreshToken(Long userId, String token) { refreshTokenRepository.save(newRefreshToken); } + @Transactional + public LoginResultResponse refresh(String refreshTokenStr) { + // 1. JWT 서명/만료 검증 + jwtUtil.validateToken(refreshTokenStr); + + // 2. Claims에서 type == "REFRESH" 확인 + var claims = jwtUtil.extractAllClaims(refreshTokenStr); + if (!"REFRESH".equals(claims.get("type", String.class))) { + throw new CustomException(GlobalExceptionCode.INVALID_REFRESH_TOKEN); + } + + // 3. DB에서 유효한(revoked=false) 토큰 조회 + RefreshToken storedToken = refreshTokenRepository.findByTokenAndRevokedFalse(refreshTokenStr) + .orElseThrow(() -> new CustomException(GlobalExceptionCode.INVALID_REFRESH_TOKEN)); + + // 4. 사용자 존재 + ACTIVE 상태 확인 + Long userId = claims.get("userId", Long.class); + User user = userRepository.findById(userId) + .orElseThrow(() -> new CustomException(GlobalExceptionCode.USER_NOT_EXISTS)); + + if (user.getStatus() != Status.ACTIVE) { + throw new CustomException(GlobalExceptionCode.USER_DISABLED); + } + + // 5. 기존 refresh token revoke + storedToken.revoke(); + + // 6. 새 access token + 새 refresh token 발급 (토큰 로테이션) + String newAccessToken = jwtUtil.generateAccessToken(user); + String newRefreshToken = jwtUtil.generateRefreshToken(user); + + // 7. 새 refresh token DB 저장 + saveNewRefreshToken(userId, newRefreshToken); + + // 8. LoginResultResponse 반환 + return LoginResultResponse.builder() + .user(user) + .accessToken(newAccessToken) + .refreshToken(newRefreshToken) + .build(); + } + @Transactional public void revokeAllUserTokens(Long userId) { User user = userRepository.findById(userId) diff --git a/src/main/java/until/the/eternity/das/token/entity/RefreshToken.java b/src/main/java/until/the/eternity/das/token/entity/RefreshToken.java index f7b45fd..f1bc56d 100644 --- a/src/main/java/until/the/eternity/das/token/entity/RefreshToken.java +++ b/src/main/java/until/the/eternity/das/token/entity/RefreshToken.java @@ -1,20 +1,8 @@ package until.the.eternity.das.token.entity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.Table; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import jakarta.persistence.*; +import lombok.*; import org.hibernate.annotations.Comment; import until.the.eternity.das.user.entity.User; diff --git a/src/main/java/until/the/eternity/das/token/entity/RefreshTokenRepository.java b/src/main/java/until/the/eternity/das/token/entity/RefreshTokenRepository.java index 4f13e99..8909039 100644 --- a/src/main/java/until/the/eternity/das/token/entity/RefreshTokenRepository.java +++ b/src/main/java/until/the/eternity/das/token/entity/RefreshTokenRepository.java @@ -17,4 +17,6 @@ public interface RefreshTokenRepository extends JpaRepository */ List findAllByUserAndRevokedFalse(User user); + + Optional findByTokenAndRevokedFalse(String token); } \ No newline at end of file diff --git a/src/main/java/until/the/eternity/das/user/entity/User.java b/src/main/java/until/the/eternity/das/user/entity/User.java index a193818..6570e6b 100644 --- a/src/main/java/until/the/eternity/das/user/entity/User.java +++ b/src/main/java/until/the/eternity/das/user/entity/User.java @@ -1,21 +1,7 @@ package until.the.eternity.das.user.entity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.Table; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import jakarta.persistence.*; +import lombok.*; import org.hibernate.annotations.Comment; import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; diff --git a/src/main/java/until/the/eternity/das/user/entity/enums/Status.java b/src/main/java/until/the/eternity/das/user/entity/enums/Status.java index 7b83ae9..001b427 100644 --- a/src/main/java/until/the/eternity/das/user/entity/enums/Status.java +++ b/src/main/java/until/the/eternity/das/user/entity/enums/Status.java @@ -1,6 +1,7 @@ package until.the.eternity.das.user.entity.enums; -import lombok.*; +import lombok.Getter; +import lombok.RequiredArgsConstructor; @Getter @RequiredArgsConstructor diff --git a/src/main/java/until/the/eternity/das/user/presentation/UserController.java b/src/main/java/until/the/eternity/das/user/presentation/UserController.java index e7b303f..01e23e5 100644 --- a/src/main/java/until/the/eternity/das/user/presentation/UserController.java +++ b/src/main/java/until/the/eternity/das/user/presentation/UserController.java @@ -7,12 +7,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PatchMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import until.the.eternity.das.common.response.CommonResponse; import until.the.eternity.das.user.application.UserService; import until.the.eternity.das.user.dto.request.UserInfoUpdateRequest; diff --git a/src/test/java/until/the/eternity/das/auth/application/AuthServiceTest.java b/src/test/java/until/the/eternity/das/auth/application/AuthServiceTest.java index 6598f55..cfce79f 100644 --- a/src/test/java/until/the/eternity/das/auth/application/AuthServiceTest.java +++ b/src/test/java/until/the/eternity/das/auth/application/AuthServiceTest.java @@ -18,6 +18,9 @@ import until.the.eternity.das.common.exception.CustomException; import until.the.eternity.das.common.exception.GlobalExceptionCode; import until.the.eternity.das.common.util.JwtUtil; +import until.the.eternity.das.login.entity.AccountLock; +import until.the.eternity.das.login.entity.AccountLockRepository; +import until.the.eternity.das.login.entity.LoginHistoryRepository; import until.the.eternity.das.role.entity.Role; import until.the.eternity.das.role.entity.RoleRepository; import until.the.eternity.das.role.entity.enums.Name; @@ -25,11 +28,10 @@ import until.the.eternity.das.user.entity.User; import until.the.eternity.das.user.entity.UserRepository; +import java.time.LocalDateTime; import java.util.Optional; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doNothing; @@ -54,7 +56,11 @@ class AuthServiceTest { @Mock private TokenService tokenService; @Mock - private S3Service s3Service; // S3Service Mock 추가 + private S3Service s3Service; + @Mock + private AccountLockRepository accountLockRepository; + @Mock + private LoginHistoryRepository loginHistoryRepository; private SignUpRequest signUpRequest; private LoginRequest loginRequest; @@ -156,7 +162,15 @@ void signUp_Fail_NicknameAlreadyExists() { @DisplayName("로그인 성공 테스트") void login_Success() { // given + AccountLock accountLock = AccountLock.builder() + .user(user) + .userId(user.getId()) + .failedAttempts(0) + .updatedAt(LocalDateTime.now()) + .build(); + when(userRepository.findByEmail(loginRequest.email())).thenReturn(Optional.of(user)); + when(accountLockRepository.findById(user.getId())).thenReturn(Optional.of(accountLock)); when(bCryptPasswordEncoder.matches(loginRequest.password(), user.getPasswordHash())).thenReturn(true); when(jwtUtil.generateAccessToken(user)).thenReturn("access-token"); when(jwtUtil.generateRefreshToken(user)).thenReturn("refresh-token"); @@ -164,7 +178,7 @@ void login_Success() { .saveNewRefreshToken(user.getId(), "refresh-token"); // when - LoginResultResponse response = authService.login(loginRequest); + LoginResultResponse response = authService.login(loginRequest, "127.0.0.1", "TestAgent"); // then assertNotNull(response); @@ -180,7 +194,8 @@ void login_Fail_UserNotFound() { when(userRepository.findByEmail(loginRequest.email())).thenReturn(Optional.empty()); // when & then - CustomException exception = assertThrows(CustomException.class, () -> authService.login(loginRequest)); + CustomException exception = assertThrows(CustomException.class, + () -> authService.login(loginRequest, "127.0.0.1", "TestAgent")); assertEquals(GlobalExceptionCode.USER_NOT_EXISTS, exception.getCode()); } @@ -188,11 +203,20 @@ void login_Fail_UserNotFound() { @DisplayName("로그인 실패 - 잘못된 비밀번호") void login_Fail_InvalidPassword() { // given + AccountLock accountLock = AccountLock.builder() + .user(user) + .userId(user.getId()) + .failedAttempts(0) + .updatedAt(LocalDateTime.now()) + .build(); + when(userRepository.findByEmail(loginRequest.email())).thenReturn(Optional.of(user)); + when(accountLockRepository.findById(user.getId())).thenReturn(Optional.of(accountLock)); when(bCryptPasswordEncoder.matches(loginRequest.password(), user.getPasswordHash())).thenReturn(false); // when & then - CustomException exception = assertThrows(CustomException.class, () -> authService.login(loginRequest)); + CustomException exception = assertThrows(CustomException.class, + () -> authService.login(loginRequest, "127.0.0.1", "TestAgent")); assertEquals(GlobalExceptionCode.INVALID_PASSWORD, exception.getCode()); } } \ No newline at end of file diff --git a/src/test/java/until/the/eternity/das/auth/presentation/AuthControllerTest.java b/src/test/java/until/the/eternity/das/auth/presentation/AuthControllerTest.java index e609a65..6a39819 100644 --- a/src/test/java/until/the/eternity/das/auth/presentation/AuthControllerTest.java +++ b/src/test/java/until/the/eternity/das/auth/presentation/AuthControllerTest.java @@ -18,15 +18,23 @@ import until.the.eternity.das.auth.dto.response.LoginResultResponse; import until.the.eternity.das.auth.dto.response.SignUpResponse; import until.the.eternity.das.common.config.TestSecurityConfig; +import until.the.eternity.das.common.constant.JwtConstant; +import until.the.eternity.das.common.exception.CustomException; +import until.the.eternity.das.common.exception.GlobalExceptionCode; import until.the.eternity.das.common.util.CookieUtil; +import until.the.eternity.das.common.util.JwtUtil; +import until.the.eternity.das.oauth.service.SocialAuthService; import until.the.eternity.das.role.entity.Role; import until.the.eternity.das.role.entity.enums.Name; +import until.the.eternity.das.token.application.TokenService; import until.the.eternity.das.user.entity.User; +import until.the.eternity.das.user.entity.UserRepository; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -44,9 +52,24 @@ class AuthControllerTest { @MockitoBean private AuthService authService; + @MockitoBean + private TokenService tokenService; + + @MockitoBean + private SocialAuthService socialAuthService; + @MockitoBean private CookieUtil cookieUtil; + @MockitoBean + private JwtUtil jwtUtil; + + @MockitoBean + private JwtConstant jwtConstant; + + @MockitoBean + private UserRepository userRepository; + private MockMultipartFile mockFile; private SignUpRequest signUpRequest; private LoginRequest loginRequest; @@ -83,7 +106,6 @@ void setUp() { @DisplayName("회원가입 API 성공 테스트") void signUpApi_Success() throws Exception { // given - SignUpRequest request = signUpRequest; SignUpResponse response = SignUpResponse.builder() .id(1L) .build(); @@ -91,22 +113,28 @@ void signUpApi_Success() throws Exception { when(authService.signUpUser(any(SignUpRequest.class))).thenReturn(response); // when & then - mockMvc.perform(post("/api/v1/auth/signup") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.data.email").value(request.email())); + mockMvc.perform(multipart("/api/auth/signup") + .file(mockFile) + .param("email", "test@test.com") + .param("password", "password123!") + .param("nickname", "testuser")) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.data.id").value(1L)); } @Test @DisplayName("회원가입 API 실패 - 잘못된 이메일 형식") void signUpApi_Fail_InvalidEmail() throws Exception { // given - SignUpRequest request = signUpRequest; + when(authService.signUpUser(any(SignUpRequest.class))) + .thenThrow(new CustomException(GlobalExceptionCode.INVALID_EMAIL_FORMAT)); + // when & then - mockMvc.perform(post("/api/v1/auth/signup") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) + mockMvc.perform(multipart("/api/auth/signup") + .file(mockFile) + .param("email", "invalid-email") + .param("password", "password123!") + .param("nickname", "testuser")) .andExpect(status().isBadRequest()); } @@ -131,7 +159,7 @@ void loginApi_Success() throws Exception { LoginResultResponse loginResult = new LoginResultResponse(user, accessToken, refreshToken); // 4. authService.login()이 호출되면, 위에서 만든 가짜 결과를 반환하도록 설정 - when(authService.login(any(LoginRequest.class))).thenReturn(loginResult); + when(authService.login(any(LoginRequest.class), any(), any())).thenReturn(loginResult); // --- WHEN (API를 호출했을 때) --- mockMvc.perform(post("/api/auth/login") // AuthController에 정의된 경로