From 0aeaddd86fa4ed99e867996eff63458083c1fb1e Mon Sep 17 00:00:00 2001 From: Park Na Rim Date: Sun, 9 Aug 2026 22:33:55 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/UserController.java | 12 ++++- .../daytodo/domain/user/dto/UserRequest.java | 16 ++++++- .../user/exception/code/UserErrorCode.java | 6 ++- .../domain/user/service/UserService.java | 18 ++++++- .../daytodo/global/config/SecurityConfig.java | 5 +- .../user/controller/UserControllerTest.java | 34 +++++++++---- .../domain/user/service/UserServiceTest.java | 48 ++++++++++++++++++- 7 files changed, 122 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/daytodo/domain/user/controller/UserController.java b/src/main/java/com/daytodo/domain/user/controller/UserController.java index 364a6f9..212a453 100644 --- a/src/main/java/com/daytodo/domain/user/controller/UserController.java +++ b/src/main/java/com/daytodo/domain/user/controller/UserController.java @@ -55,6 +55,16 @@ public UserResponse.Profile updateProfile( return profileService.updateProfile(userId, nickname, profileImage); } + @Operation(summary = "비밀번호 변경 (마이페이지)") + @PatchMapping("/password") + public ResponseEntity changePassword( + @AuthenticationPrincipal Long userId, + @Valid @RequestBody UserRequest.ChangePassword request + ) { + userService.changePassword(userId, request); + return ResponseEntity.noContent().build(); + } + @Operation(summary = "관심지역 조회") @GetMapping("/interest-region") public UserResponse.InterestRegions getInterestRegions( @@ -133,4 +143,4 @@ public ResponseEntity submitFeedback( public UserResponse.Policies getPolicies() { return policyService.getPolicies(); } -} +} \ No newline at end of file diff --git a/src/main/java/com/daytodo/domain/user/dto/UserRequest.java b/src/main/java/com/daytodo/domain/user/dto/UserRequest.java index 2117a1c..dcf81bc 100644 --- a/src/main/java/com/daytodo/domain/user/dto/UserRequest.java +++ b/src/main/java/com/daytodo/domain/user/dto/UserRequest.java @@ -3,6 +3,7 @@ import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; import com.daytodo.domain.user.enums.DevicePlatform; @@ -46,4 +47,17 @@ public record SubmitFeedback( String content ) { } -} + + public record ChangePassword( + @NotBlank(message = "현재 비밀번호는 필수입니다.") + String currentPassword, + + @NotBlank(message = "새 비밀번호는 필수입니다.") + @Pattern( + regexp = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d!@#$%^&*()_+]{8,}$", + message = "비밀번호는 영문+숫자 조합 8자 이상이어야 합니다." + ) + String newPassword + ) { + } +} \ No newline at end of file diff --git a/src/main/java/com/daytodo/domain/user/exception/code/UserErrorCode.java b/src/main/java/com/daytodo/domain/user/exception/code/UserErrorCode.java index 8856aca..ea61a96 100644 --- a/src/main/java/com/daytodo/domain/user/exception/code/UserErrorCode.java +++ b/src/main/java/com/daytodo/domain/user/exception/code/UserErrorCode.java @@ -17,9 +17,11 @@ public enum UserErrorCode implements BaseErrorCode { PROFILE_IMAGE_STORAGE_NOT_CONFIGURED(HttpStatus.SERVICE_UNAVAILABLE, "PROFILE_IMAGE_STORAGE_NOT_CONFIGURED", "프로필 이미지 저장소 설정이 완료되지 않았습니다."), PROFILE_IMAGE_UPLOAD_FAILED(HttpStatus.BAD_GATEWAY, "PROFILE_IMAGE_UPLOAD_FAILED", "프로필 이미지를 저장하지 못했습니다."), FEEDBACK_TOO_SHORT(HttpStatus.BAD_REQUEST, "FEEDBACK_TOO_SHORT", "의견은 공백을 제거한 후 100자 이상이어야 합니다."), - POLICY_NOT_AVAILABLE(HttpStatus.INTERNAL_SERVER_ERROR, "POLICY_NOT_AVAILABLE", "약관 및 정책을 불러오지 못했습니다."); + POLICY_NOT_AVAILABLE(HttpStatus.INTERNAL_SERVER_ERROR, "POLICY_NOT_AVAILABLE", "약관 및 정책을 불러오지 못했습니다."), + INVALID_CURRENT_PASSWORD(HttpStatus.UNAUTHORIZED, "INVALID_CURRENT_PASSWORD", "현재 비밀번호가 일치하지 않습니다."), + SOCIAL_ACCOUNT_PASSWORD_CHANGE_NOT_ALLOWED(HttpStatus.CONFLICT, "SOCIAL_ACCOUNT_PASSWORD_CHANGE_NOT_ALLOWED", "소셜 로그인 계정은 비밀번호 변경을 지원하지 않습니다."); private final HttpStatus status; private final String code; private final String message; -} +} \ No newline at end of file diff --git a/src/main/java/com/daytodo/domain/user/service/UserService.java b/src/main/java/com/daytodo/domain/user/service/UserService.java index 5cd3d81..6be0743 100644 --- a/src/main/java/com/daytodo/domain/user/service/UserService.java +++ b/src/main/java/com/daytodo/domain/user/service/UserService.java @@ -7,12 +7,14 @@ import com.daytodo.domain.user.dto.UserResponse; import com.daytodo.domain.user.entity.User; import com.daytodo.domain.user.entity.mapping.UserInterestRegion; +import com.daytodo.domain.user.enums.LoginType; import com.daytodo.domain.user.enums.UserStatus; import com.daytodo.domain.user.exception.code.UserErrorCode; import com.daytodo.domain.user.repository.UserInterestRegionRepository; import com.daytodo.domain.user.repository.UserRepository; import com.daytodo.global.apiPayload.exception.ProjectException; import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -30,6 +32,7 @@ public class UserService { private final UserRepository userRepository; private final UserInterestRegionRepository interestRegionRepository; private final RegionRepository regionRepository; + private final PasswordEncoder passwordEncoder; private final Clock clock; public UserResponse.Profile getProfile(Long userId) { @@ -85,6 +88,19 @@ public void withdraw(Long userId) { user.withdraw(LocalDateTime.now(clock)); } + @Transactional + public void changePassword(Long userId, UserRequest.ChangePassword request) { + User user = getActiveUser(userId); + // 네이버 전용 계정(password=null)은 현재 비밀번호가 없으므로 마이페이지에서 변경할 수 없다. + if (user.getLoginType() != LoginType.LOCAL) { + throw new ProjectException(UserErrorCode.SOCIAL_ACCOUNT_PASSWORD_CHANGE_NOT_ALLOWED); + } + if (!passwordEncoder.matches(request.currentPassword(), user.getPassword())) { + throw new ProjectException(UserErrorCode.INVALID_CURRENT_PASSWORD); + } + user.changePassword(passwordEncoder.encode(request.newPassword())); + } + public User getActiveUser(Long userId) { return userRepository.findByIdAndUserStatus(userId, UserStatus.ACTIVE) .orElseThrow(() -> new ProjectException(UserErrorCode.USER_NOT_FOUND)); @@ -113,4 +129,4 @@ private UserResponse.InterestRegion toInterestRegion(Region region) { region.getParent() == null ? null : region.getParent().getRegionName() ); } -} +} \ No newline at end of file diff --git a/src/main/java/com/daytodo/global/config/SecurityConfig.java b/src/main/java/com/daytodo/global/config/SecurityConfig.java index 44d0345..5808318 100644 --- a/src/main/java/com/daytodo/global/config/SecurityConfig.java +++ b/src/main/java/com/daytodo/global/config/SecurityConfig.java @@ -96,7 +96,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers(HttpMethod.PATCH, "/users/profile", "/users/interest-regions", - "/users/notifications" + "/users/notifications", + "/users/password" ).authenticated() .requestMatchers(HttpMethod.DELETE, "/users/me").authenticated() .requestMatchers(HttpMethod.DELETE, "/users/fcm-token").authenticated() @@ -154,4 +155,4 @@ private void writeSecurityError( response.setCharacterEncoding(java.nio.charset.StandardCharsets.UTF_8.name()); objectMapper.writeValue(response.getWriter(), ErrorResponse.of(errorCode)); } -} +} \ No newline at end of file diff --git a/src/test/java/com/daytodo/domain/user/controller/UserControllerTest.java b/src/test/java/com/daytodo/domain/user/controller/UserControllerTest.java index fc2c9d6..f98baa3 100644 --- a/src/test/java/com/daytodo/domain/user/controller/UserControllerTest.java +++ b/src/test/java/com/daytodo/domain/user/controller/UserControllerTest.java @@ -20,6 +20,7 @@ import org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -38,13 +39,13 @@ class UserControllerTest { @BeforeEach void setUp() { mockMvc = MockMvcBuilders.standaloneSetup(new UserController( - userService, - profileService, - notificationService, - feedbackService, - policyService, - fcmTokenService - )) + userService, + profileService, + notificationService, + feedbackService, + policyService, + fcmTokenService + )) .setCustomArgumentResolvers(new AuthenticationPrincipalArgumentResolver()) .build(); } @@ -84,6 +85,23 @@ void registersFcmTokenUsingAuthenticatedPrincipal() throws Exception { ); } + @Test + void changesPasswordUsingAuthenticatedPrincipal() throws Exception { + SecurityContextHolder.getContext().setAuthentication( + new UsernamePasswordAuthenticationToken(1L, null) + ); + + mockMvc.perform(patch("/users/password") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"currentPassword\":\"current1234\",\"newPassword\":\"newPassword1234\"}")) + .andExpect(status().isNoContent()); + + verify(userService).changePassword( + 1L, + new com.daytodo.domain.user.dto.UserRequest.ChangePassword("current1234", "newPassword1234") + ); + } + @Test void deletesFcmTokenUsingAuthenticatedPrincipal() throws Exception { SecurityContextHolder.getContext().setAuthentication( @@ -100,4 +118,4 @@ void deletesFcmTokenUsingAuthenticatedPrincipal() throws Exception { new com.daytodo.domain.user.dto.UserRequest.DeleteFcmToken("device-token") ); } -} +} \ No newline at end of file diff --git a/src/test/java/com/daytodo/domain/user/service/UserServiceTest.java b/src/test/java/com/daytodo/domain/user/service/UserServiceTest.java index 5a20d66..66bf797 100644 --- a/src/test/java/com/daytodo/domain/user/service/UserServiceTest.java +++ b/src/test/java/com/daytodo/domain/user/service/UserServiceTest.java @@ -18,6 +18,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.test.util.ReflectionTestUtils; import java.time.Clock; @@ -43,12 +44,15 @@ class UserServiceTest { @Mock UserRepository userRepository; @Mock UserInterestRegionRepository interestRegionRepository; @Mock RegionRepository regionRepository; + @Mock PasswordEncoder passwordEncoder; UserService userService; @BeforeEach void setUp() { - userService = new UserService(userRepository, interestRegionRepository, regionRepository, CLOCK); + userService = new UserService( + userRepository, interestRegionRepository, regionRepository, passwordEncoder, CLOCK + ); } @Test @@ -143,6 +147,46 @@ void rejectsRepeatedWithdrawal() { .isEqualTo(UserErrorCode.USER_ALREADY_WITHDRAWN); } + @Test + void changesPasswordWhenCurrentPasswordMatches() { + User user = user(1L, UserStatus.ACTIVE); + when(userRepository.findByIdAndUserStatus(1L, UserStatus.ACTIVE)).thenReturn(Optional.of(user)); + when(passwordEncoder.matches("current1234", "password")).thenReturn(true); + when(passwordEncoder.encode("newPassword1234")).thenReturn("encodedNewPassword"); + + userService.changePassword(1L, new UserRequest.ChangePassword("current1234", "newPassword1234")); + + assertThat(user.getPassword()).isEqualTo("encodedNewPassword"); + } + + @Test + void rejectsPasswordChangeWhenCurrentPasswordDoesNotMatch() { + User user = user(1L, UserStatus.ACTIVE); + when(userRepository.findByIdAndUserStatus(1L, UserStatus.ACTIVE)).thenReturn(Optional.of(user)); + when(passwordEncoder.matches("wrongPassword", "password")).thenReturn(false); + + assertThatThrownBy(() -> userService.changePassword( + 1L, new UserRequest.ChangePassword("wrongPassword", "newPassword1234") + )) + .isInstanceOf(ProjectException.class) + .extracting("errorCode") + .isEqualTo(UserErrorCode.INVALID_CURRENT_PASSWORD); + } + + @Test + void rejectsPasswordChangeForSocialOnlyAccount() { + User user = new User("user@example.com", null, "daytodo", null, LoginType.NAVER); + ReflectionTestUtils.setField(user, "id", 1L); + when(userRepository.findByIdAndUserStatus(1L, UserStatus.ACTIVE)).thenReturn(Optional.of(user)); + + assertThatThrownBy(() -> userService.changePassword( + 1L, new UserRequest.ChangePassword("current1234", "newPassword1234") + )) + .isInstanceOf(ProjectException.class) + .extracting("errorCode") + .isEqualTo(UserErrorCode.SOCIAL_ACCOUNT_PASSWORD_CHANGE_NOT_ALLOWED); + } + private User user(Long id, UserStatus status) { User user = new User("user@example.com", "password", "daytodo", "profile.png", LoginType.LOCAL); ReflectionTestUtils.setField(user, "id", id); @@ -155,4 +199,4 @@ private Region region(Long id, Region parent, String name, RegionLevel level) { ReflectionTestUtils.setField(region, "regionId", id); return region; } -} +} \ No newline at end of file From ec21cd317079bce03758741608b31fcd0f0ec06b Mon Sep 17 00:00:00 2001 From: Park Na Rim Date: Sun, 9 Aug 2026 22:45:10 +0900 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD=20=EC=8B=9C=20=EB=9D=BD=20=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=EC=A1=B0=ED=9A=8C=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20?= =?UTF-8?q?=EB=8F=99=EC=8B=9C=EC=84=B1=20=EC=9D=B4=EC=8A=88=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/user/service/UserService.java | 3 ++- .../domain/user/service/UserServiceTest.java | 18 +++++++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/daytodo/domain/user/service/UserService.java b/src/main/java/com/daytodo/domain/user/service/UserService.java index 6be0743..cc78ee5 100644 --- a/src/main/java/com/daytodo/domain/user/service/UserService.java +++ b/src/main/java/com/daytodo/domain/user/service/UserService.java @@ -90,7 +90,8 @@ public void withdraw(Long userId) { @Transactional public void changePassword(Long userId, UserRequest.ChangePassword request) { - User user = getActiveUser(userId); + User user = userRepository.findActiveUserForUpdate(userId, UserStatus.ACTIVE) + .orElseThrow(() -> new ProjectException(UserErrorCode.USER_NOT_FOUND)); // 네이버 전용 계정(password=null)은 현재 비밀번호가 없으므로 마이페이지에서 변경할 수 없다. if (user.getLoginType() != LoginType.LOCAL) { throw new ProjectException(UserErrorCode.SOCIAL_ACCOUNT_PASSWORD_CHANGE_NOT_ALLOWED); diff --git a/src/test/java/com/daytodo/domain/user/service/UserServiceTest.java b/src/test/java/com/daytodo/domain/user/service/UserServiceTest.java index 66bf797..7a8468d 100644 --- a/src/test/java/com/daytodo/domain/user/service/UserServiceTest.java +++ b/src/test/java/com/daytodo/domain/user/service/UserServiceTest.java @@ -150,7 +150,7 @@ void rejectsRepeatedWithdrawal() { @Test void changesPasswordWhenCurrentPasswordMatches() { User user = user(1L, UserStatus.ACTIVE); - when(userRepository.findByIdAndUserStatus(1L, UserStatus.ACTIVE)).thenReturn(Optional.of(user)); + when(userRepository.findActiveUserForUpdate(1L, UserStatus.ACTIVE)).thenReturn(Optional.of(user)); when(passwordEncoder.matches("current1234", "password")).thenReturn(true); when(passwordEncoder.encode("newPassword1234")).thenReturn("encodedNewPassword"); @@ -162,7 +162,7 @@ void changesPasswordWhenCurrentPasswordMatches() { @Test void rejectsPasswordChangeWhenCurrentPasswordDoesNotMatch() { User user = user(1L, UserStatus.ACTIVE); - when(userRepository.findByIdAndUserStatus(1L, UserStatus.ACTIVE)).thenReturn(Optional.of(user)); + when(userRepository.findActiveUserForUpdate(1L, UserStatus.ACTIVE)).thenReturn(Optional.of(user)); when(passwordEncoder.matches("wrongPassword", "password")).thenReturn(false); assertThatThrownBy(() -> userService.changePassword( @@ -177,7 +177,7 @@ void rejectsPasswordChangeWhenCurrentPasswordDoesNotMatch() { void rejectsPasswordChangeForSocialOnlyAccount() { User user = new User("user@example.com", null, "daytodo", null, LoginType.NAVER); ReflectionTestUtils.setField(user, "id", 1L); - when(userRepository.findByIdAndUserStatus(1L, UserStatus.ACTIVE)).thenReturn(Optional.of(user)); + when(userRepository.findActiveUserForUpdate(1L, UserStatus.ACTIVE)).thenReturn(Optional.of(user)); assertThatThrownBy(() -> userService.changePassword( 1L, new UserRequest.ChangePassword("current1234", "newPassword1234") @@ -187,6 +187,18 @@ void rejectsPasswordChangeForSocialOnlyAccount() { .isEqualTo(UserErrorCode.SOCIAL_ACCOUNT_PASSWORD_CHANGE_NOT_ALLOWED); } + @Test + void rejectsPasswordChangeWhenUserNotFound() { + when(userRepository.findActiveUserForUpdate(1L, UserStatus.ACTIVE)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> userService.changePassword( + 1L, new UserRequest.ChangePassword("current1234", "newPassword1234") + )) + .isInstanceOf(ProjectException.class) + .extracting("errorCode") + .isEqualTo(UserErrorCode.USER_NOT_FOUND); + } + private User user(Long id, UserStatus status) { User user = new User("user@example.com", "password", "daytodo", "profile.png", LoginType.LOCAL); ReflectionTestUtils.setField(user, "id", id); From 6e74a940c429f01e8d1a2ed50ad6ebd490eb296f Mon Sep 17 00:00:00 2001 From: Park Na Rim Date: Mon, 10 Aug 2026 00:06:22 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD=20=EC=8B=9C=20=EA=B8=B0=EC=A1=B4=20?= =?UTF-8?q?=EC=84=B8=EC=85=98=20=EB=B0=8F=20=EC=9E=AC=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EB=AC=B4=ED=9A=A8=ED=99=94,=20=EA=B8=B8?= =?UTF-8?q?=EC=9D=B4=20=EC=A0=9C=ED=95=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/daytodo/domain/user/dto/UserRequest.java | 2 ++ .../daytodo/domain/user/service/UserService.java | 8 ++++++++ .../domain/user/service/UserServiceTest.java | 15 ++++++++++++++- 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/daytodo/domain/user/dto/UserRequest.java b/src/main/java/com/daytodo/domain/user/dto/UserRequest.java index dcf81bc..3efb962 100644 --- a/src/main/java/com/daytodo/domain/user/dto/UserRequest.java +++ b/src/main/java/com/daytodo/domain/user/dto/UserRequest.java @@ -50,9 +50,11 @@ public record SubmitFeedback( public record ChangePassword( @NotBlank(message = "현재 비밀번호는 필수입니다.") + @Size(max = 72, message = "비밀번호는 72자 이하여야 합니다.") String currentPassword, @NotBlank(message = "새 비밀번호는 필수입니다.") + @Size(max = 72, message = "비밀번호는 72자 이하여야 합니다.") @Pattern( regexp = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d!@#$%^&*()_+]{8,}$", message = "비밀번호는 영문+숫자 조합 8자 이상이어야 합니다." diff --git a/src/main/java/com/daytodo/domain/user/service/UserService.java b/src/main/java/com/daytodo/domain/user/service/UserService.java index cc78ee5..0510c3f 100644 --- a/src/main/java/com/daytodo/domain/user/service/UserService.java +++ b/src/main/java/com/daytodo/domain/user/service/UserService.java @@ -1,5 +1,7 @@ package com.daytodo.domain.user.service; +import com.daytodo.domain.auth.repository.PasswordResetTokenRepository; +import com.daytodo.domain.auth.repository.RefreshTokenRepository; import com.daytodo.domain.region.entity.Region; import com.daytodo.domain.region.exception.code.RegionErrorCode; import com.daytodo.domain.region.repository.RegionRepository; @@ -33,6 +35,8 @@ public class UserService { private final UserInterestRegionRepository interestRegionRepository; private final RegionRepository regionRepository; private final PasswordEncoder passwordEncoder; + private final RefreshTokenRepository refreshTokenRepository; + private final PasswordResetTokenRepository passwordResetTokenRepository; private final Clock clock; public UserResponse.Profile getProfile(Long userId) { @@ -100,6 +104,10 @@ public void changePassword(Long userId, UserRequest.ChangePassword request) { throw new ProjectException(UserErrorCode.INVALID_CURRENT_PASSWORD); } user.changePassword(passwordEncoder.encode(request.newPassword())); + // 비밀번호 변경 시 기존에 발급된 refresh token(탈취 가능성 있는 세션)과 + // 대기 중이던 비밀번호 재설정 코드를 함께 무효화한다. + refreshTokenRepository.findById(userId).ifPresent(refreshTokenRepository::delete); + passwordResetTokenRepository.findById(userId).ifPresent(passwordResetTokenRepository::delete); } public User getActiveUser(Long userId) { diff --git a/src/test/java/com/daytodo/domain/user/service/UserServiceTest.java b/src/test/java/com/daytodo/domain/user/service/UserServiceTest.java index 7a8468d..e78e4e8 100644 --- a/src/test/java/com/daytodo/domain/user/service/UserServiceTest.java +++ b/src/test/java/com/daytodo/domain/user/service/UserServiceTest.java @@ -1,5 +1,9 @@ package com.daytodo.domain.user.service; +import com.daytodo.domain.auth.entity.PasswordResetToken; +import com.daytodo.domain.auth.entity.RefreshToken; +import com.daytodo.domain.auth.repository.PasswordResetTokenRepository; +import com.daytodo.domain.auth.repository.RefreshTokenRepository; import com.daytodo.domain.region.entity.Region; import com.daytodo.domain.region.enums.RegionLevel; import com.daytodo.domain.region.repository.RegionRepository; @@ -45,13 +49,16 @@ class UserServiceTest { @Mock UserInterestRegionRepository interestRegionRepository; @Mock RegionRepository regionRepository; @Mock PasswordEncoder passwordEncoder; + @Mock RefreshTokenRepository refreshTokenRepository; + @Mock PasswordResetTokenRepository passwordResetTokenRepository; UserService userService; @BeforeEach void setUp() { userService = new UserService( - userRepository, interestRegionRepository, regionRepository, passwordEncoder, CLOCK + userRepository, interestRegionRepository, regionRepository, passwordEncoder, + refreshTokenRepository, passwordResetTokenRepository, CLOCK ); } @@ -150,13 +157,19 @@ void rejectsRepeatedWithdrawal() { @Test void changesPasswordWhenCurrentPasswordMatches() { User user = user(1L, UserStatus.ACTIVE); + RefreshToken refreshToken = new RefreshToken(1L, "refresh-token", LocalDateTime.now(CLOCK).plusDays(1)); + PasswordResetToken resetToken = new PasswordResetToken(1L, "123456", LocalDateTime.now(CLOCK).plusMinutes(10)); when(userRepository.findActiveUserForUpdate(1L, UserStatus.ACTIVE)).thenReturn(Optional.of(user)); when(passwordEncoder.matches("current1234", "password")).thenReturn(true); when(passwordEncoder.encode("newPassword1234")).thenReturn("encodedNewPassword"); + when(refreshTokenRepository.findById(1L)).thenReturn(Optional.of(refreshToken)); + when(passwordResetTokenRepository.findById(1L)).thenReturn(Optional.of(resetToken)); userService.changePassword(1L, new UserRequest.ChangePassword("current1234", "newPassword1234")); assertThat(user.getPassword()).isEqualTo("encodedNewPassword"); + verify(refreshTokenRepository).delete(refreshToken); + verify(passwordResetTokenRepository).delete(resetToken); } @Test