From 940c6b327c5747d4722921ed6623b32106a1e2f5 Mon Sep 17 00:00:00 2001 From: dongHyo Date: Sun, 22 May 2022 20:45:39 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=ED=83=88=ED=87=B4=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/AlreadyDeletedException.java | 9 +++ .../user/application/UserController.java | 13 ++++ .../request/UserDeleteRequest.java | 23 ++++++ .../domain/PasswordMismatchException.java | 11 +++ .../ticketing/server/user/domain/User.java | 13 +++- .../server/user/service/UserServiceImpl.java | 14 ++++ .../server/user/service/dto/DeleteUser.java | 39 ++++++++++ .../user/service/interfaces/UserService.java | 3 + .../server/user/domain/UserTest.java | 77 ++++++++++++++----- .../user/service/UserServiceImplTest.java | 31 +++++++- .../user/service/dto/DeleteUserTest.java | 37 +++++++++ 11 files changed, 246 insertions(+), 24 deletions(-) create mode 100644 server/src/main/java/com/ticketing/server/global/exception/AlreadyDeletedException.java create mode 100644 server/src/main/java/com/ticketing/server/user/application/request/UserDeleteRequest.java create mode 100644 server/src/main/java/com/ticketing/server/user/domain/PasswordMismatchException.java create mode 100644 server/src/main/java/com/ticketing/server/user/service/dto/DeleteUser.java create mode 100644 server/src/test/java/com/ticketing/server/user/service/dto/DeleteUserTest.java diff --git a/server/src/main/java/com/ticketing/server/global/exception/AlreadyDeletedException.java b/server/src/main/java/com/ticketing/server/global/exception/AlreadyDeletedException.java new file mode 100644 index 0000000..7c0e004 --- /dev/null +++ b/server/src/main/java/com/ticketing/server/global/exception/AlreadyDeletedException.java @@ -0,0 +1,9 @@ +package com.ticketing.server.global.exception; + +public class AlreadyDeletedException extends RuntimeException { + + public AlreadyDeletedException(String message) { + super(message); + } + +} diff --git a/server/src/main/java/com/ticketing/server/user/application/UserController.java b/server/src/main/java/com/ticketing/server/user/application/UserController.java index 2987deb..4a32b91 100644 --- a/server/src/main/java/com/ticketing/server/user/application/UserController.java +++ b/server/src/main/java/com/ticketing/server/user/application/UserController.java @@ -1,6 +1,7 @@ package com.ticketing.server.user.application; import com.ticketing.server.user.application.request.SignUpRequest; +import com.ticketing.server.user.application.request.UserDeleteRequest; import com.ticketing.server.user.domain.User; import com.ticketing.server.user.service.UserServiceImpl; import java.util.Optional; @@ -9,6 +10,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -33,4 +35,15 @@ public class UserController { return ResponseEntity.status(HttpStatus.CREATED).build(); } + @DeleteMapping + public ResponseEntity delete(@RequestBody @Valid UserDeleteRequest userDeleteRequest) { + Optional user = userService.delete(userDeleteRequest.toDeleteUser(passwordEncoder)); + + if (user.isEmpty()) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).build(); + } + + return ResponseEntity.status(HttpStatus.OK).build(); + } + } diff --git a/server/src/main/java/com/ticketing/server/user/application/request/UserDeleteRequest.java b/server/src/main/java/com/ticketing/server/user/application/request/UserDeleteRequest.java new file mode 100644 index 0000000..27b5320 --- /dev/null +++ b/server/src/main/java/com/ticketing/server/user/application/request/UserDeleteRequest.java @@ -0,0 +1,23 @@ +package com.ticketing.server.user.application.request; + +import com.ticketing.server.user.service.dto.DeleteUser; +import javax.validation.constraints.Email; +import javax.validation.constraints.NotEmpty; +import lombok.Getter; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Getter +public class UserDeleteRequest { + + @NotEmpty(message = "{validation.not.empty.email}") + @Email(message = "{validation.email}") + private String email; + + @NotEmpty(message = "{validation.not.empty.password}") + private String password; + + public DeleteUser toDeleteUser(PasswordEncoder passwordEncoder) { + return new DeleteUser(email, password, passwordEncoder); + } + +} diff --git a/server/src/main/java/com/ticketing/server/user/domain/PasswordMismatchException.java b/server/src/main/java/com/ticketing/server/user/domain/PasswordMismatchException.java new file mode 100644 index 0000000..3d32cc7 --- /dev/null +++ b/server/src/main/java/com/ticketing/server/user/domain/PasswordMismatchException.java @@ -0,0 +1,11 @@ +package com.ticketing.server.user.domain; + +public class PasswordMismatchException extends RuntimeException { + + private static final String MESSAGE = "패스워드가 일치하지 않습니다"; + + public PasswordMismatchException() { + super(MESSAGE); + } + +} diff --git a/server/src/main/java/com/ticketing/server/user/domain/User.java b/server/src/main/java/com/ticketing/server/user/domain/User.java index 30e420f..2ff5ec1 100644 --- a/server/src/main/java/com/ticketing/server/user/domain/User.java +++ b/server/src/main/java/com/ticketing/server/user/domain/User.java @@ -1,9 +1,10 @@ package com.ticketing.server.user.domain; import com.ticketing.server.global.dto.repository.AbstractEntity; +import com.ticketing.server.global.exception.AlreadyDeletedException; import com.ticketing.server.global.validator.constraints.Phone; +import com.ticketing.server.user.service.dto.DeleteUser; import java.time.LocalDateTime; -import java.util.Optional; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.EnumType; @@ -54,14 +55,18 @@ public class User extends AbstractEntity { private LocalDateTime deletedAt; - public Optional delete() { + public User delete(DeleteUser deleteUser) { if (isDeleted) { - return Optional.empty(); + throw new AlreadyDeletedException("이미 탈퇴된 회원 입니다."); + } + + if (!deleteUser.passwordEquals(password)) { + throw new PasswordMismatchException(); } isDeleted = true; deletedAt = LocalDateTime.now(); - return Optional.of(this); + return this; } } diff --git a/server/src/main/java/com/ticketing/server/user/service/UserServiceImpl.java b/server/src/main/java/com/ticketing/server/user/service/UserServiceImpl.java index 86fe3cc..4fbf9bb 100644 --- a/server/src/main/java/com/ticketing/server/user/service/UserServiceImpl.java +++ b/server/src/main/java/com/ticketing/server/user/service/UserServiceImpl.java @@ -2,6 +2,7 @@ package com.ticketing.server.user.service; import com.ticketing.server.user.domain.User; import com.ticketing.server.user.domain.repository.UserRepository; +import com.ticketing.server.user.service.dto.DeleteUser; import com.ticketing.server.user.service.dto.SignUp; import com.ticketing.server.user.service.interfaces.UserService; import java.util.Optional; @@ -34,4 +35,17 @@ public class UserServiceImpl implements UserService { return Optional.of(newUser); } + @Override + @Transactional + public Optional delete(@Valid DeleteUser deleteUser) { + Optional optionalUser = userRepository.findByEmail(deleteUser.getEmail()); + if (optionalUser.isEmpty()) { + log.error("존재하지 않는 이메일 입니다. :: {}", deleteUser); + return Optional.empty(); + } + + User user = optionalUser.get(); + return Optional.of(user.delete(deleteUser)); + } + } diff --git a/server/src/main/java/com/ticketing/server/user/service/dto/DeleteUser.java b/server/src/main/java/com/ticketing/server/user/service/dto/DeleteUser.java new file mode 100644 index 0000000..a1320d2 --- /dev/null +++ b/server/src/main/java/com/ticketing/server/user/service/dto/DeleteUser.java @@ -0,0 +1,39 @@ +package com.ticketing.server.user.service.dto; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotEmpty; +import org.springframework.security.crypto.password.PasswordEncoder; + +public class DeleteUser { + + public DeleteUser(String email, String inputPassword, PasswordEncoder passwordEncoder) { + this.email = email; + this.inputPassword = inputPassword; + this.passwordEncoder = passwordEncoder; + } + + @NotEmpty(message = "{validation.not.empty.email}") + @Email(message = "{validation.email}") + private String email; + + @NotEmpty(message = "{validation.not.empty.password}") + private String inputPassword; + + private PasswordEncoder passwordEncoder; + + public boolean passwordEquals(String password) { + return passwordEncoder.matches(this.inputPassword, password); + } + + public String getEmail() { + return email; + } + + @Override + public String toString() { + return "DeleteUser{" + + "email='" + email + '\'' + + '}'; + } + +} diff --git a/server/src/main/java/com/ticketing/server/user/service/interfaces/UserService.java b/server/src/main/java/com/ticketing/server/user/service/interfaces/UserService.java index 2fab6c8..4960dc9 100644 --- a/server/src/main/java/com/ticketing/server/user/service/interfaces/UserService.java +++ b/server/src/main/java/com/ticketing/server/user/service/interfaces/UserService.java @@ -1,6 +1,7 @@ package com.ticketing.server.user.service.interfaces; import com.ticketing.server.user.domain.User; +import com.ticketing.server.user.service.dto.DeleteUser; import com.ticketing.server.user.service.dto.SignUp; import java.util.Optional; import javax.validation.Valid; @@ -9,4 +10,6 @@ public interface UserService { Optional register(@Valid SignUp signUpDto); + Optional delete(@Valid DeleteUser deleteUser); + } diff --git a/server/src/test/java/com/ticketing/server/user/domain/UserTest.java b/server/src/test/java/com/ticketing/server/user/domain/UserTest.java index c7a0c5a..7ff71ea 100644 --- a/server/src/test/java/com/ticketing/server/user/domain/UserTest.java +++ b/server/src/test/java/com/ticketing/server/user/domain/UserTest.java @@ -1,9 +1,12 @@ package com.ticketing.server.user.domain; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; -import java.util.Optional; +import com.ticketing.server.global.exception.AlreadyDeletedException; +import com.ticketing.server.user.service.dto.DeleteUser; +import com.ticketing.server.user.service.dto.DeleteUserTest; import java.util.Set; import java.util.stream.Stream; import javax.validation.ConstraintViolation; @@ -14,6 +17,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; class UserTest { @@ -81,33 +85,68 @@ class UserTest { ); } - @ParameterizedTest - @MethodSource("provideCorrectUsers") - @DisplayName("이미 회원탈퇴 되어 있는 경우") - void deleteFail(User user) { - // given - user.delete(); + public static Stream provideDifferentPasswordDeleteUsers() { + return Stream.of( + Arguments.of(new User("유저1", "ticketing1@gmail.com", "123456", UserGrade.GUEST, "010-1234-5678") + , new DeleteUser("ticketing1@gmail.com", "1234561", DeleteUserTest.CUSTOM_PASSWORD_ENCODER)) + , Arguments.of(new User("유저2", "ticketing2@gmail.com", "qwe123", UserGrade.GUEST, "010-2234-5678") + , new DeleteUser("ticketing2@gmail.com", "qwe1231", DeleteUserTest.CUSTOM_PASSWORD_ENCODER)) + , Arguments.of(new User("유저3", "ticketing3@gmail.com", "ticketing", UserGrade.STAFF, "010-3234-5678") + , new DeleteUser("ticketing3@gmail.com", "ticketing1", DeleteUserTest.CUSTOM_PASSWORD_ENCODER)) + , Arguments.of(new User("유저4", "ticketing4@gmail.com", "ticketing123456", UserGrade.STAFF, "010-4234-5678") + , new DeleteUser("ticketing4@gmail.com", "ticketing1234561", DeleteUserTest.CUSTOM_PASSWORD_ENCODER)) + ); + } - // when - Optional deletedUser = user.delete(); - - // then - assertThat(deletedUser).isEmpty(); + public static Stream provideDeleteUsers() { + return Stream.of( + Arguments.of(new User("유저1", "ticketing1@gmail.com", "123456", UserGrade.GUEST, "010-1234-5678") + , new DeleteUser("ticketing1@gmail.com", "123456", DeleteUserTest.CUSTOM_PASSWORD_ENCODER)) + , Arguments.of(new User("유저2", "ticketing2@gmail.com", "qwe123", UserGrade.GUEST, "010-2234-5678") + , new DeleteUser("ticketing2@gmail.com", "qwe123", DeleteUserTest.CUSTOM_PASSWORD_ENCODER)) + , Arguments.of(new User("유저3", "ticketing3@gmail.com", "ticketing", UserGrade.STAFF, "010-3234-5678") + , new DeleteUser("ticketing3@gmail.com", "ticketing", DeleteUserTest.CUSTOM_PASSWORD_ENCODER)) + , Arguments.of(new User("유저4", "ticketing4@gmail.com", "ticketing123456", UserGrade.STAFF, "010-4234-5678") + , new DeleteUser("ticketing4@gmail.com", "ticketing123456", DeleteUserTest.CUSTOM_PASSWORD_ENCODER)) + ); } @ParameterizedTest - @MethodSource("provideCorrectUsers") - @DisplayName("회원탈퇴 성공") - void deleteSuccess(User user) { + @MethodSource("provideDifferentPasswordDeleteUsers") + @DisplayName("입력된 패스워드가 다를 경우") + void passwordMismatchException(User user, DeleteUser deleteUser) { // given // when - Optional deletedUser = user.delete(); + // then + assertThatThrownBy(() -> user.delete(deleteUser)) + .isInstanceOf(PasswordMismatchException.class); + } + + @ParameterizedTest + @MethodSource("provideDeleteUsers") + @DisplayName("이미 회원탈퇴 되어 있는 경우") + void alreadyDeletedException(User user, DeleteUser deleteUser) { + // given + // when + user.delete(deleteUser); + + // then + assertThatThrownBy(() -> user.delete(deleteUser)) + .isInstanceOf(AlreadyDeletedException.class); + } + + @ParameterizedTest + @MethodSource("provideDeleteUsers") + @DisplayName("회원탈퇴 성공") + void deleteSuccess(User user, DeleteUser deleteUser) { + // given + // when + User deletedUser = user.delete(deleteUser); // then - assertThat(deletedUser).isPresent(); assertAll( - () -> assertThat(deletedUser.get().getDeletedAt()).isNotNull() - , () -> assertThat(deletedUser.get().isDeleted()).isTrue() + () -> assertThat(deletedUser.getDeletedAt()).isNotNull() + , () -> assertThat(deletedUser.isDeleted()).isTrue() ); } diff --git a/server/src/test/java/com/ticketing/server/user/service/UserServiceImplTest.java b/server/src/test/java/com/ticketing/server/user/service/UserServiceImplTest.java index e7b17d8..8603c12 100644 --- a/server/src/test/java/com/ticketing/server/user/service/UserServiceImplTest.java +++ b/server/src/test/java/com/ticketing/server/user/service/UserServiceImplTest.java @@ -7,6 +7,8 @@ import static org.mockito.Mockito.when; import com.ticketing.server.user.domain.User; import com.ticketing.server.user.domain.UserGrade; import com.ticketing.server.user.domain.repository.UserRepository; +import com.ticketing.server.user.service.dto.DeleteUser; +import com.ticketing.server.user.service.dto.DeleteUserTest; import com.ticketing.server.user.service.dto.SignUp; import java.util.Optional; import org.junit.jupiter.api.BeforeEach; @@ -22,6 +24,7 @@ class UserServiceImplTest { User user; SignUp signUp; + DeleteUser deleteUser; @Mock UserRepository userRepository; @@ -31,9 +34,9 @@ class UserServiceImplTest { @BeforeEach void init() { - signUp = new SignUp("유저", "ticketing@gmail.com", "123456", "010-1234-5678"); user = new User("유저", "ticketing@gmail.com", "123456", UserGrade.GUEST, "010-1234-5678"); + deleteUser = new DeleteUser("ticketing@gmail.com", "123456", DeleteUserTest.CUSTOM_PASSWORD_ENCODER); } @Test @@ -63,4 +66,30 @@ class UserServiceImplTest { assertThat(user).isPresent(); } + @Test + @DisplayName("회원탈퇴 시 이메일이 존재하지 않을 경우") + void deleteFail() { + // given + when(userRepository.findByEmail("ticketing@gmail.com")).thenReturn(Optional.empty()); + + // when + Optional user = userService.delete(deleteUser); + + // then + assertThat(user).isEmpty(); + } + + @Test + @DisplayName("회원탈퇴 성공했을 경우") + void deleteSuccess() { + // given + when(userRepository.findByEmail("ticketing@gmail.com")).thenReturn(Optional.of(user)); + + // when + Optional user = userService.delete(deleteUser); + + // then + assertThat(user).isPresent(); + } + } diff --git a/server/src/test/java/com/ticketing/server/user/service/dto/DeleteUserTest.java b/server/src/test/java/com/ticketing/server/user/service/dto/DeleteUserTest.java new file mode 100644 index 0000000..e39eb36 --- /dev/null +++ b/server/src/test/java/com/ticketing/server/user/service/dto/DeleteUserTest.java @@ -0,0 +1,37 @@ +package com.ticketing.server.user.service.dto; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.security.crypto.password.PasswordEncoder; + +public class DeleteUserTest { + + public static PasswordEncoder CUSTOM_PASSWORD_ENCODER = new CustomPasswordEncoder(); + + public static class CustomPasswordEncoder implements PasswordEncoder { + + @Override + public String encode(CharSequence rawPassword) { + return null; + } + + @Override + public boolean matches(CharSequence rawPassword, String encodedPassword) { + return rawPassword.toString().equals(encodedPassword); + } + } + + @Test + @DisplayName("CustomPasswordEncoder matches 테스트") + void customPasswordEncoderMatches() { + // given + DeleteUser user = new DeleteUser("ticketing@gmail.com", "123456", CUSTOM_PASSWORD_ENCODER); + + // when + // then + assertThat(user.passwordEquals("123456")).isTrue(); + } + +}