Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ public enum AuthErrorCode implements ErrorCode {
NOT_ISSUED_VERIFICATION_CODE(HttpStatus.BAD_REQUEST, "AUTH_003", "발급된 인증번호가 없습니다. 인증번호를 먼저 요청해 주세요."),
INVALID_VERIFICATION_CODE(HttpStatus.FORBIDDEN, "AUTH_004", "잘못된 인증번호입니다. 입력한 인증번호를 확인해 주세요."),
EXISTING_EMAIL(HttpStatus.CONFLICT, "AUTH_005", "이미 가입된 이메일입니다. 다른 이메일을 사용해 주세요."),
INVALID_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH_006", "인증 정보가 유효하지 않습니다.");
INVALID_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH_006", "인증 정보가 유효하지 않습니다."),
UNVERIFIED_EMAIL(HttpStatus.FORBIDDEN, "AUTH_007", "인증되지 않은 이메일입니다. 이메일 인증을 완료해 주세요.");

private final HttpStatus httpStatus;
private final String code;
Expand Down
12 changes: 6 additions & 6 deletions src/main/java/project/flipnote/auth/service/AuthService.java
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ public TokenPair refreshToken(String refreshToken) {
return jwtComponent.generateTokenPair(userAuth);
}

public void validatePasswordMatch(String rawPassword, String encodedPassword) {
if (!passwordEncoder.matches(rawPassword, encodedPassword)) {
throw new BizException(AuthErrorCode.INVALID_CREDENTIALS);
}
}

private String generateVerificationCode(int length) {
int origin = (int)Math.pow(10, length - 1);
int bound = (int)Math.pow(10, length);
Expand All @@ -96,12 +102,6 @@ private User findActiveUserByEmail(String email) {
.orElseThrow(() -> new BizException(AuthErrorCode.INVALID_CREDENTIALS));
}

private void validatePasswordMatch(String rawPassword, String encodedPassword) {
if (!passwordEncoder.matches(rawPassword, encodedPassword)) {
throw new BizException(AuthErrorCode.INVALID_CREDENTIALS);
}
}

private void validateEmailIsAvailable(String email) {
if (userRepository.existsByEmail(email)) {
throw new BizException(AuthErrorCode.EXISTING_EMAIL);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package project.flipnote.auth.service;

import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;
import project.flipnote.auth.exception.AuthErrorCode;
import project.flipnote.auth.repository.EmailVerificationRedisRepository;
import project.flipnote.common.exception.BizException;

@RequiredArgsConstructor
@Service
public class EmailVerificationService {

private final EmailVerificationRedisRepository emailVerificationRedisRepository;

public void validateVerified(String email) {
if (!emailVerificationRedisRepository.isVerified(email)) {
throw new BizException(AuthErrorCode.UNVERIFIED_EMAIL);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,9 @@ public Optional<Long> findTokenVersion(long userId) {
return dbTokenVersion;
});
}

public void incrementTokenVersion(long userId) {
userRepository.incrementTokenVersion(userId);
tokenVersionRedisRepository.deleteTokenVersion(userId);
}
}
11 changes: 11 additions & 0 deletions src/main/java/project/flipnote/user/controller/UserController.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
Expand All @@ -16,6 +17,7 @@
import lombok.RequiredArgsConstructor;
import project.flipnote.common.security.dto.UserAuth;
import project.flipnote.user.model.MyInfoResponse;
import project.flipnote.user.model.ChangePasswordRequest;
import project.flipnote.user.model.UserInfoResponse;
import project.flipnote.user.model.UserRegisterRequest;
import project.flipnote.user.model.UserRegisterResponse;
Expand Down Expand Up @@ -66,4 +68,13 @@ public ResponseEntity<UserInfoResponse> getUserInfo(
UserInfoResponse res = userService.getUserInfo(userId);
return ResponseEntity.ok(res);
}

@PatchMapping("/me/password")
public ResponseEntity<Void> updatePassword(
@AuthenticationPrincipal UserAuth userAuth,
@Valid @RequestBody ChangePasswordRequest req
) {
userService.changePassword(userAuth.userId(), req);
return ResponseEntity.noContent().build();
}
}
4 changes: 4 additions & 0 deletions src/main/java/project/flipnote/user/entity/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,8 @@ public void update(String nickname, String phone, boolean smsAgree, String profi
this.smsAgree = smsAgree;
this.profileImageUrl = profileImageUrl;
}

public void changePassword(String password) {
this.password = password;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ public enum UserErrorCode implements ErrorCode {

DUPLICATE_EMAIL(HttpStatus.CONFLICT, "USER_001", "이미 사용 중인 이메일입니다."),
DUPLICATE_PHONE(HttpStatus.CONFLICT, "USER_002", "이미 사용 중인 휴대전화 번호입니다."),
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER_003", "회원이 존재하지 않습니다."),
UNVERIFIED_EMAIL(HttpStatus.FORBIDDEN, "USER_004", "인증되지 않은 이메일입니다. 이메일 인증을 완료해 주세요.");

USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER_003", "회원이 존재하지 않습니다.");
Comment on lines 13 to +15
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

기존 코드에서 UNVERIFIED_EMAIL 사용 여부를 확인해 주세요.

UserErrorCode.UNVERIFIED_EMAIL을 사용하던 기존 코드가 모두 AuthErrorCode.UNVERIFIED_EMAIL로 업데이트되었는지 확인이 필요합니다.

다음 스크립트로 기존 사용처를 확인해 주세요:


🏁 Script executed:

#!/bin/bash
# Description: UserErrorCode.UNVERIFIED_EMAIL 사용처 확인

# UserErrorCode.UNVERIFIED_EMAIL 사용처 검색
rg -A 3 -B 3 "UserErrorCode\.UNVERIFIED_EMAIL"

# AuthErrorCode.UNVERIFIED_EMAIL로 변경된 부분 확인
rg -A 3 -B 3 "AuthErrorCode\.UNVERIFIED_EMAIL"

Length of output: 1574


테스트 코드에서 UserErrorCode.UNVERIFIED_EMAIL 참조 수정 필요

UserServiceTest에서 여전히 UserErrorCode.UNVERIFIED_EMAIL을 사용하고 있으므로, AuthErrorCode.UNVERIFIED_EMAIL로 변경해 주세요.

  • src/test/java/project/flipnote/user/service/UserServiceTest.java
    - assertThat(exception.getErrorCode()).isEqualTo(UserErrorCode.UNVERIFIED_EMAIL);
    + assertThat(exception.getErrorCode()).isEqualTo(AuthErrorCode.UNVERIFIED_EMAIL);
  • 필요한 경우 import 구문도 함께 수정하세요.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/test/java/project/flipnote/user/service/UserServiceTest.java, update all
references of UserErrorCode.UNVERIFIED_EMAIL to AuthErrorCode.UNVERIFIED_EMAIL
to reflect the correct error code usage. Also, modify the import statements
accordingly to import AuthErrorCode instead of UserErrorCode where necessary.

private final HttpStatus httpStatus;
private final String code;
private final String message;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package project.flipnote.user.model;

import project.flipnote.common.validation.annotation.ValidPassword;

public record ChangePasswordRequest(

@ValidPassword
String currentPassword,

@ValidPassword
String newPassword
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

Expand All @@ -22,4 +23,7 @@ public interface UserRepository extends JpaRepository<User, Long> {
@Query("SELECT u.tokenVersion FROM User u WHERE u.id = :userId")
Optional<Long> findTokenVersionById(@Param("userId") Long userId);

@Modifying
@Query("UPDATE User u SET u.tokenVersion = u.tokenVersion + 1 WHERE u.id = :id")
void incrementTokenVersion(@Param("id") Long userId);
}
31 changes: 21 additions & 10 deletions src/main/java/project/flipnote/user/service/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@

import lombok.RequiredArgsConstructor;
import project.flipnote.auth.repository.EmailVerificationRedisRepository;
import project.flipnote.auth.repository.TokenVersionRedisRepository;
import project.flipnote.auth.service.AuthService;
import project.flipnote.auth.service.EmailVerificationService;
import project.flipnote.auth.service.TokenVersionService;
import project.flipnote.common.exception.BizException;
import project.flipnote.user.entity.User;
import project.flipnote.user.entity.UserStatus;
import project.flipnote.user.exception.UserErrorCode;
import project.flipnote.user.model.MyInfoResponse;
import project.flipnote.user.model.ChangePasswordRequest;
import project.flipnote.user.model.UserInfoResponse;
import project.flipnote.user.model.UserRegisterRequest;
import project.flipnote.user.model.UserRegisterResponse;
Expand All @@ -28,8 +31,9 @@ public class UserService {

private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final TokenVersionRedisRepository tokenVersionRedisRepository;
private final EmailVerificationRedisRepository emailVerificationRedisRepository;
private final AuthService authService;
private final TokenVersionService tokenVersionService;
private final EmailVerificationService emailVerificationService;

@Transactional
public UserRegisterResponse register(UserRegisterRequest req) {
Expand All @@ -38,10 +42,7 @@ public UserRegisterResponse register(UserRegisterRequest req) {

validateEmailDuplicate(email);
validatePhoneDuplicate(phone);

if (!emailVerificationRedisRepository.isVerified(email)) {
throw new BizException(UserErrorCode.UNVERIFIED_EMAIL);
}
emailVerificationService.validateVerified(email);

User user = User.builder()
.email(email)
Expand All @@ -54,8 +55,6 @@ public UserRegisterResponse register(UserRegisterRequest req) {
.build();
User savedUser = userRepository.save(user);

emailVerificationRedisRepository.deleteVerified(email);

return UserRegisterResponse.from(savedUser.getId());
}

Expand All @@ -64,7 +63,8 @@ public void unregister(Long userId) {
User user = findActiveUserById(userId);

user.unregister();
tokenVersionRedisRepository.deleteTokenVersion(userId);

tokenVersionService.incrementTokenVersion(userId);
}

@Transactional
Expand Down Expand Up @@ -93,6 +93,17 @@ public UserInfoResponse getUserInfo(Long userId) {
return UserInfoResponse.from(user);
}

@Transactional
public void changePassword(Long userId, ChangePasswordRequest req) {
User user = findActiveUserById(userId);

authService.validatePasswordMatch(req.currentPassword(), user.getPassword());

user.changePassword(passwordEncoder.encode(req.newPassword()));

tokenVersionService.incrementTokenVersion(userId);
}

private User findActiveUserById(Long userId) {
return userRepository.findByIdAndStatus(userId, UserStatus.ACTIVE)
.orElseThrow(() -> new BizException(UserErrorCode.USER_NOT_FOUND));
Expand Down
95 changes: 82 additions & 13 deletions src/test/java/project/flipnote/user/service/UserServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,21 @@
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.crypto.password.PasswordEncoder;

import project.flipnote.auth.exception.AuthErrorCode;
import project.flipnote.auth.repository.EmailVerificationRedisRepository;
import project.flipnote.auth.repository.TokenVersionRedisRepository;
import project.flipnote.auth.service.AuthService;
import project.flipnote.auth.service.EmailVerificationService;
import project.flipnote.auth.service.TokenVersionService;
import project.flipnote.common.exception.BizException;
import project.flipnote.fixture.UserFixture;
import project.flipnote.user.entity.User;
import project.flipnote.user.entity.UserStatus;
import project.flipnote.user.exception.UserErrorCode;
import project.flipnote.user.model.UserRegisterRequest;
import project.flipnote.user.model.UserInfoResponse;
import project.flipnote.user.model.MyInfoResponse;
import project.flipnote.user.model.ChangePasswordRequest;
import project.flipnote.user.model.UserInfoResponse;
import project.flipnote.user.model.UserRegisterRequest;
import project.flipnote.user.model.UserRegisterResponse;
import project.flipnote.user.model.UserUpdateRequest;
import project.flipnote.user.model.UserUpdateResponse;
Expand All @@ -49,6 +54,15 @@ class UserServiceTest {
@Mock
EmailVerificationRedisRepository emailVerificationRedisRepository;

@Mock
AuthService authService;

@Mock
TokenVersionService tokenVersionService;

@Mock
EmailVerificationService emailVerificationService;

@DisplayName("회원가입 테스트")
@Nested
class Register {
Expand All @@ -63,11 +77,9 @@ void success() {

given(userRepository.existsByEmail(any(String.class))).willReturn(false);
given(userRepository.existsByPhone(any(String.class))).willReturn(false);
given(emailVerificationRedisRepository.isVerified(anyString())).willReturn(true);
given(passwordEncoder.encode(any(String.class))).willReturn("encodedPass");
given(userRepository.save(any(User.class))).willReturn(user);


UserRegisterResponse res = userService.register(req);

assertThat(res.userId()).isEqualTo(user.getId());
Expand All @@ -82,7 +94,6 @@ void success_ifPhoneIsNull() {
);

given(userRepository.existsByEmail(any(String.class))).willReturn(false);
given(emailVerificationRedisRepository.isVerified(anyString())).willReturn(true);
given(passwordEncoder.encode(any(String.class))).willReturn("encodedPass");
given(userRepository.save(any(User.class))).willReturn(user);

Expand Down Expand Up @@ -132,10 +143,12 @@ void fail_unverifiedEmail() {

given(userRepository.existsByEmail(anyString())).willReturn(false);
given(userRepository.existsByPhone(anyString())).willReturn(false);
given(emailVerificationRedisRepository.isVerified(anyString())).willReturn(false);
doThrow(new BizException(AuthErrorCode.UNVERIFIED_EMAIL))
.when(emailVerificationService)
.validateVerified(anyString());

BizException exception = assertThrows(BizException.class, () -> userService.register(req));
assertThat(exception.getErrorCode()).isEqualTo(UserErrorCode.UNVERIFIED_EMAIL);
assertThat(exception.getErrorCode()).isEqualTo(AuthErrorCode.UNVERIFIED_EMAIL);

verify(userRepository, never()).save(any(User.class));
}
Expand All @@ -154,11 +167,8 @@ void success() {

userService.unregister(user.getId());

assertThat(user.getStatus()).isEqualTo(UserStatus.INACTIVE);
assertThat(user.getDeletedAt()).isNotNull();

verify(user, times(1)).softDelete();
verify(tokenVersionRedisRepository, times(1)).deleteTokenVersion(anyLong());
verify(user, times(1)).unregister();
verify(tokenVersionService, times(1)).incrementTokenVersion(user.getId());
}

@DisplayName("회원 id가 존재하지 않는 경우 예외 발생")
Expand Down Expand Up @@ -317,4 +327,63 @@ void fail_userNotFound() {
assertThat(exception.getErrorCode()).isEqualTo(UserErrorCode.USER_NOT_FOUND);
}
}
}

@DisplayName("비밀번호 변경 테스트")
@Nested
class ChangePassword {

@DisplayName("성공")
@Test
void success() {
User user = spy(UserFixture.createActiveUser());
ChangePasswordRequest req = new ChangePasswordRequest("currentPassword123!", "newPassword123!");
String encodedNewPassword = "encodedNewPassword";

given(userRepository.findByIdAndStatus(user.getId(), UserStatus.ACTIVE)).willReturn(Optional.of(user));
given(passwordEncoder.encode(req.newPassword())).willReturn(encodedNewPassword);

userService.changePassword(user.getId(), req);

verify(user, times(1)).changePassword(encodedNewPassword);
verify(tokenVersionService, times(1)).incrementTokenVersion(user.getId());
}

@DisplayName("존재하지 않는 회원의 비밀번호 변경 시 예외 발생")
@Test
void fail_userNotFound() {
ChangePasswordRequest req = new ChangePasswordRequest("currentPassword123!", "newPassword123!");
Long nonExistentUserId = 99L;

given(userRepository.findByIdAndStatus(nonExistentUserId, UserStatus.ACTIVE)).willReturn(Optional.empty());

BizException exception = assertThrows(BizException.class,
() -> userService.changePassword(nonExistentUserId, req));

assertThat(exception.getErrorCode()).isEqualTo(UserErrorCode.USER_NOT_FOUND);

verify(passwordEncoder, never()).matches(anyString(), anyString());
verify(passwordEncoder, never()).encode(anyString());
verify(tokenVersionRedisRepository, never()).deleteTokenVersion(anyLong());
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

토큰 버전 서비스 사용으로 업데이트가 필요합니다.

테스트에서 tokenVersionRedisRepository.deleteTokenVersion() 대신 tokenVersionService.incrementTokenVersion()을 검증해야 합니다. 현재 코드가 서비스 계층을 사용하도록 리팩토링되었기 때문입니다.

다음과 같이 수정하세요:

-			verify(tokenVersionRedisRepository, never()).deleteTokenVersion(anyLong());
+			verify(tokenVersionService, never()).incrementTokenVersion(anyLong());

Also applies to: 386-386

🤖 Prompt for AI Agents
In src/test/java/project/flipnote/user/service/UserServiceTest.java at lines 366
and 386, the test incorrectly verifies
tokenVersionRedisRepository.deleteTokenVersion() calls, but the code now uses
tokenVersionService.incrementTokenVersion(). Update the verify statements to
check that tokenVersionService.incrementTokenVersion() is called or not called
as appropriate, replacing all instances of deleteTokenVersion verification with
incrementTokenVersion verification to reflect the service layer refactor.

}

@DisplayName("현재 비밀번호가 일치하지 않을 경우 예외 발생")
@Test
void fail_incorrectCurrentPassword() {
User user = UserFixture.createActiveUser();
ChangePasswordRequest req = new ChangePasswordRequest("wrongPassword", "newPassword123!");

given(userRepository.findByIdAndStatus(user.getId(), UserStatus.ACTIVE)).willReturn(Optional.of(user));
doThrow(new BizException(AuthErrorCode.INVALID_CREDENTIALS))
.when(authService)
.validatePasswordMatch(req.currentPassword(), user.getPassword());

BizException exception = assertThrows(BizException.class,
() -> userService.changePassword(user.getId(), req));

assertThat(exception.getErrorCode()).isEqualTo(AuthErrorCode.INVALID_CREDENTIALS);
verify(authService, times(1)).validatePasswordMatch(req.currentPassword(), user.getPassword());
verify(passwordEncoder, never()).encode(anyString());
verify(tokenVersionRedisRepository, never()).deleteTokenVersion(anyLong());
}
}
}
Loading