diff --git a/src/main/java/camp/woowak/lab/common/exception/BadRequestException.java b/src/main/java/camp/woowak/lab/common/exception/BadRequestException.java index 3b4d4090..4fb65cd5 100644 --- a/src/main/java/camp/woowak/lab/common/exception/BadRequestException.java +++ b/src/main/java/camp/woowak/lab/common/exception/BadRequestException.java @@ -4,4 +4,8 @@ public class BadRequestException extends HttpStatusException { public BadRequestException(ErrorCode errorCode) { super(errorCode); } + + public BadRequestException(ErrorCode errorCode, String message) { + super(errorCode, message); + } } diff --git a/src/main/java/camp/woowak/lab/customer/domain/.gitkeep b/src/main/java/camp/woowak/lab/customer/domain/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/camp/woowak/lab/customer/domain/Customer.java b/src/main/java/camp/woowak/lab/customer/domain/Customer.java index dcdd0fc8..c18a476a 100644 --- a/src/main/java/camp/woowak/lab/customer/domain/Customer.java +++ b/src/main/java/camp/woowak/lab/customer/domain/Customer.java @@ -1,18 +1,45 @@ package camp.woowak.lab.customer.domain; +import camp.woowak.lab.customer.exception.InvalidCreationException; import camp.woowak.lab.payaccount.domain.PayAccount; +import camp.woowak.lab.web.authentication.PasswordEncoder; +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.OneToOne; +import lombok.Getter; @Entity +@Getter public class Customer { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(nullable = false, length = 50) + private String name; + @Column(unique = true, nullable = false, length = 100) + private String email; + @Column(nullable = false, length = 30) + private String password; + @Column(nullable = false, length = 30) + private String phone; @OneToOne(fetch = FetchType.LAZY) private PayAccount payAccount; + + public Customer() { + } + + public Customer(String name, String email, String password, String phone, PayAccount payAccount, + PasswordEncoder passwordEncoder) throws + InvalidCreationException { + CustomerValidator.validateCreation(name, email, password, phone, payAccount); + this.name = name; + this.email = email; + this.password = passwordEncoder.encode(password); + this.phone = phone; + this.payAccount = payAccount; + } } diff --git a/src/main/java/camp/woowak/lab/customer/domain/CustomerValidator.java b/src/main/java/camp/woowak/lab/customer/domain/CustomerValidator.java new file mode 100644 index 00000000..118692fb --- /dev/null +++ b/src/main/java/camp/woowak/lab/customer/domain/CustomerValidator.java @@ -0,0 +1,76 @@ +package camp.woowak.lab.customer.domain; + +import camp.woowak.lab.customer.exception.CustomerErrorCode; +import camp.woowak.lab.customer.exception.InvalidCreationException; +import camp.woowak.lab.payaccount.domain.PayAccount; + +public class CustomerValidator { + private static final int MAX_NAME_LENGTH = 50; + private static final int MAX_EMAIL_LENGTH = 100; + private static final int MIN_PASSWORD_LENGTH = 8; + private static final int MAX_PASSWORD_LENGTH = 30; + private static final int MAX_PHONE_LENGTH = 30; + + private CustomerValidator() { + } + + public static void validateCreation(String name, String email, String password, String phone, + PayAccount payAccount) throws InvalidCreationException { + validateName(name); + validateEmail(email); + validatePassword(password); + validatePhone(phone); + validatePayAccount(payAccount); + } + + public static void validateName(String name) throws InvalidCreationException { + if (name == null || name.isBlank()) { + throw new InvalidCreationException(CustomerErrorCode.INVALID_CREATION, "Customer name cannot be blank"); + } + if (name.length() > MAX_NAME_LENGTH) { + throw new InvalidCreationException(CustomerErrorCode.INVALID_CREATION, + "Customer name cannot be longer than 50 characters"); + } + } + + public static void validateEmail(String email) throws InvalidCreationException { + if (email == null || email.isBlank()) { + throw new InvalidCreationException(CustomerErrorCode.INVALID_CREATION, "Customer email cannot be blank"); + } + if (email.trim().length() > MAX_EMAIL_LENGTH) { + throw new InvalidCreationException(CustomerErrorCode.INVALID_CREATION, + "Customer email cannot be longer than 100 characters"); + } + } + + public static void validatePassword(String password) throws InvalidCreationException { + if (password == null || password.isBlank()) { + throw new InvalidCreationException(CustomerErrorCode.INVALID_CREATION, "Customer password cannot be blank"); + } + if (password.trim().length() < MIN_PASSWORD_LENGTH) { + throw new InvalidCreationException(CustomerErrorCode.INVALID_CREATION, + "Customer password cannot be shorter than 8 characters"); + } + if (password.trim().length() > MAX_PASSWORD_LENGTH) { + throw new InvalidCreationException(CustomerErrorCode.INVALID_CREATION, + "Customer password cannot be longer than 30 characters"); + } + } + + public static void validatePhone(String phone) throws InvalidCreationException { + if (phone == null || phone.isBlank()) { + throw new InvalidCreationException(CustomerErrorCode.INVALID_CREATION, "Customer phone cannot be blank"); + } + if (phone.trim().length() > MAX_PHONE_LENGTH) { + throw new InvalidCreationException(CustomerErrorCode.INVALID_CREATION, + "Customer phone cannot be longer than 30 characters"); + } + } + + public static void validatePayAccount(PayAccount payAccount) throws InvalidCreationException { + if (payAccount == null) { + throw new InvalidCreationException(CustomerErrorCode.INVALID_CREATION, + "Customer payAccount cannot be null"); + } + } +} diff --git a/src/main/java/camp/woowak/lab/customer/exception/CustomerErrorCode.java b/src/main/java/camp/woowak/lab/customer/exception/CustomerErrorCode.java new file mode 100644 index 00000000..7ad34f6c --- /dev/null +++ b/src/main/java/camp/woowak/lab/customer/exception/CustomerErrorCode.java @@ -0,0 +1,33 @@ +package camp.woowak.lab.customer.exception; + +import camp.woowak.lab.common.exception.ErrorCode; + +public enum CustomerErrorCode implements ErrorCode { + INVALID_CREATION(400, "C1", "잘못된 요청입니다."), + DUPLICATE_EMAIL(400, "C2", "이미 존재하는 이메일입니다."); + + private final int status; + private final String errorCode; + private final String message; + + CustomerErrorCode(int status, String errorCode, String message) { + this.status = status; + this.errorCode = errorCode; + this.message = message; + } + + @Override + public int getStatus() { + return status; + } + + @Override + public String getErrorCode() { + return errorCode; + } + + @Override + public String getMessage() { + return message; + } +} diff --git a/src/main/java/camp/woowak/lab/customer/exception/DuplicateEmailException.java b/src/main/java/camp/woowak/lab/customer/exception/DuplicateEmailException.java new file mode 100644 index 00000000..643729b0 --- /dev/null +++ b/src/main/java/camp/woowak/lab/customer/exception/DuplicateEmailException.java @@ -0,0 +1,9 @@ +package camp.woowak.lab.customer.exception; + +import camp.woowak.lab.common.exception.BadRequestException; + +public class DuplicateEmailException extends BadRequestException { + public DuplicateEmailException() { + super(CustomerErrorCode.DUPLICATE_EMAIL); + } +} diff --git a/src/main/java/camp/woowak/lab/customer/exception/InvalidCreationException.java b/src/main/java/camp/woowak/lab/customer/exception/InvalidCreationException.java new file mode 100644 index 00000000..26ae7d49 --- /dev/null +++ b/src/main/java/camp/woowak/lab/customer/exception/InvalidCreationException.java @@ -0,0 +1,10 @@ +package camp.woowak.lab.customer.exception; + +import camp.woowak.lab.common.exception.BadRequestException; +import camp.woowak.lab.common.exception.ErrorCode; + +public class InvalidCreationException extends BadRequestException { + public InvalidCreationException(ErrorCode errorCode, String message) { + super(errorCode, message); + } +} diff --git a/src/main/java/camp/woowak/lab/customer/repository/.gitkeep b/src/main/java/camp/woowak/lab/customer/repository/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/camp/woowak/lab/customer/repository/CustomerRepository.java b/src/main/java/camp/woowak/lab/customer/repository/CustomerRepository.java new file mode 100644 index 00000000..32b96ef2 --- /dev/null +++ b/src/main/java/camp/woowak/lab/customer/repository/CustomerRepository.java @@ -0,0 +1,8 @@ +package camp.woowak.lab.customer.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import camp.woowak.lab.customer.domain.Customer; + +public interface CustomerRepository extends JpaRepository { +} diff --git a/src/main/java/camp/woowak/lab/customer/service/.gitkeep b/src/main/java/camp/woowak/lab/customer/service/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/camp/woowak/lab/customer/service/SignUpCustomerService.java b/src/main/java/camp/woowak/lab/customer/service/SignUpCustomerService.java new file mode 100644 index 00000000..3aa0c421 --- /dev/null +++ b/src/main/java/camp/woowak/lab/customer/service/SignUpCustomerService.java @@ -0,0 +1,49 @@ +package camp.woowak.lab.customer.service; + +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.stereotype.Service; + +import camp.woowak.lab.customer.domain.Customer; +import camp.woowak.lab.customer.exception.DuplicateEmailException; +import camp.woowak.lab.customer.exception.InvalidCreationException; +import camp.woowak.lab.customer.repository.CustomerRepository; +import camp.woowak.lab.customer.service.command.SignUpCustomerCommand; +import camp.woowak.lab.payaccount.domain.PayAccount; +import camp.woowak.lab.payaccount.repository.PayAccountRepository; +import camp.woowak.lab.web.authentication.PasswordEncoder; +import jakarta.transaction.Transactional; + +@Service +public class SignUpCustomerService { + private final CustomerRepository customerRepository; + private final PayAccountRepository payAccountRepository; + private final PasswordEncoder passwordEncoder; + + public SignUpCustomerService(CustomerRepository customerRepository, PayAccountRepository payAccountRepository, + PasswordEncoder passwordEncoder) { + this.customerRepository = customerRepository; + this.payAccountRepository = payAccountRepository; + this.passwordEncoder = passwordEncoder; + } + + /** + * + * @throws InvalidCreationException 구매자 생성에 오류가 나는 경우 + * @throws DuplicateEmailException 이메일이 중복되는 경우 + */ + @Transactional + public Long signUp(SignUpCustomerCommand cmd) { + PayAccount payAccount = new PayAccount(); + payAccountRepository.save(payAccount); + + Customer newCustomer = new Customer(cmd.name(), cmd.email(), cmd.password(), cmd.phone(), payAccount, + passwordEncoder); + + try { + customerRepository.save(newCustomer); + } catch (DataIntegrityViolationException e) { + throw new DuplicateEmailException(); + } + return newCustomer.getId(); + } +} diff --git a/src/main/java/camp/woowak/lab/customer/service/command/SignUpCustomerCommand.java b/src/main/java/camp/woowak/lab/customer/service/command/SignUpCustomerCommand.java new file mode 100644 index 00000000..d59b8160 --- /dev/null +++ b/src/main/java/camp/woowak/lab/customer/service/command/SignUpCustomerCommand.java @@ -0,0 +1,4 @@ +package camp.woowak.lab.customer.service.command; + +public record SignUpCustomerCommand(String name, String email, String password, String phone) { +} diff --git a/src/main/java/camp/woowak/lab/web/api/customer/CustomerApiController.java b/src/main/java/camp/woowak/lab/web/api/customer/CustomerApiController.java new file mode 100644 index 00000000..17397fcd --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/api/customer/CustomerApiController.java @@ -0,0 +1,38 @@ +package camp.woowak.lab.web.api.customer; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import camp.woowak.lab.customer.service.SignUpCustomerService; +import camp.woowak.lab.customer.service.command.SignUpCustomerCommand; +import camp.woowak.lab.web.api.utils.APIResponse; +import camp.woowak.lab.web.api.utils.APIUtils; +import camp.woowak.lab.web.dto.request.customer.SignUpCustomerRequest; +import camp.woowak.lab.web.dto.response.customer.SignUpCustomerResponse; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; + +@RestController +public class CustomerApiController { + private final SignUpCustomerService signUpCustomerService; + + public CustomerApiController(SignUpCustomerService signUpCustomerService) { + this.signUpCustomerService = signUpCustomerService; + } + + @PostMapping("/customers") + public ResponseEntity> signUp(@Valid @RequestBody SignUpCustomerRequest request, + HttpServletResponse response) { + SignUpCustomerCommand command = + new SignUpCustomerCommand(request.name(), request.email(), request.password(), request.phone()); + + Long registeredId = signUpCustomerService.signUp(command); + + response.setHeader("Location", "/customers/" + registeredId); + + return APIUtils.of(HttpStatus.CREATED, new SignUpCustomerResponse(registeredId)); + } +} diff --git a/src/main/java/camp/woowak/lab/web/api/customer/CustomerExceptionHandler.java b/src/main/java/camp/woowak/lab/web/api/customer/CustomerExceptionHandler.java new file mode 100644 index 00000000..e7fb2482 --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/api/customer/CustomerExceptionHandler.java @@ -0,0 +1,43 @@ +package camp.woowak.lab.web.api.customer; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ProblemDetail; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import camp.woowak.lab.common.advice.DomainExceptionHandler; +import camp.woowak.lab.common.exception.BadRequestException; +import camp.woowak.lab.customer.exception.DuplicateEmailException; +import camp.woowak.lab.customer.exception.InvalidCreationException; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@DomainExceptionHandler +public class CustomerExceptionHandler extends ResponseEntityExceptionHandler { + /** + * + * BadRequestException.class 와 MethodArgumentNotValidException.class 를 처리한다. + */ + @ExceptionHandler({InvalidCreationException.class}) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ProblemDetail handleBadRequestException(BadRequestException e) { + ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, + e.errorCode().getErrorCode()); + problemDetail.setProperty("errorCode", e.errorCode().getErrorCode()); + return problemDetail; + } + + /** + * + * DuplicateEmailException.class 를 처리한다. + */ + @ExceptionHandler({DuplicateEmailException.class}) + @ResponseStatus(HttpStatus.CONFLICT) + public ProblemDetail handleDuplicateEmailException(DuplicateEmailException e) { + ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT, + e.errorCode().getMessage()); + problemDetail.setProperty("errorCode", e.errorCode().getErrorCode()); + return problemDetail; + } +} diff --git a/src/main/java/camp/woowak/lab/web/dto/request/SignUpVendorRequest.java b/src/main/java/camp/woowak/lab/web/dto/request/SignUpVendorRequest.java new file mode 100644 index 00000000..00cbe2ab --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/dto/request/SignUpVendorRequest.java @@ -0,0 +1,18 @@ +package camp.woowak.lab.web.dto.request; + +import org.hibernate.validator.constraints.Length; + +import camp.woowak.lab.web.validation.annotation.Phone; +import jakarta.validation.constraints.Email; + +public record SignUpVendorRequest( + @Length(min = 1, max = 50) + String name, + @Email + String email, + @Length(min = 1, max = 30) + String password, + @Phone + String phone +) { +} diff --git a/src/main/java/camp/woowak/lab/web/dto/request/customer/SignUpCustomerRequest.java b/src/main/java/camp/woowak/lab/web/dto/request/customer/SignUpCustomerRequest.java new file mode 100644 index 00000000..80dc9cbe --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/dto/request/customer/SignUpCustomerRequest.java @@ -0,0 +1,20 @@ +package camp.woowak.lab.web.dto.request.customer; + +import org.hibernate.validator.constraints.Length; + +import camp.woowak.lab.web.validation.annotation.Phone; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +public record SignUpCustomerRequest( + @Length(min = 1, max = 50, message = "이름은 1자 이상 50자 이하여야 합니다.") + String name, + @NotBlank(message = "이메일은 필수 입력값입니다.") + @Email(message = "이메일 형식이 올바르지 않습니다.") + String email, + @Length(min = 8, max = 20, message = "비밀번호는 8자 이상 20자 이하여야 합니다.") + String password, + @Phone(message = "전화번호 형식이 올바르지 않습니다.") + String phone +) { +} diff --git a/src/main/java/camp/woowak/lab/web/dto/response/customer/SignUpCustomerResponse.java b/src/main/java/camp/woowak/lab/web/dto/response/customer/SignUpCustomerResponse.java new file mode 100644 index 00000000..87b9a605 --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/dto/response/customer/SignUpCustomerResponse.java @@ -0,0 +1,4 @@ +package camp.woowak.lab.web.dto.response.customer; + +public record SignUpCustomerResponse(Long id) { +} diff --git a/src/test/java/camp/woowak/lab/customer/domain/CustomerTest.java b/src/test/java/camp/woowak/lab/customer/domain/CustomerTest.java new file mode 100644 index 00000000..28adbfd2 --- /dev/null +++ b/src/test/java/camp/woowak/lab/customer/domain/CustomerTest.java @@ -0,0 +1,200 @@ +package camp.woowak.lab.customer.domain; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import camp.woowak.lab.customer.exception.InvalidCreationException; +import camp.woowak.lab.payaccount.domain.PayAccount; +import camp.woowak.lab.payaccount.domain.TestPayAccount; +import camp.woowak.lab.web.authentication.NoOpPasswordEncoder; +import camp.woowak.lab.web.authentication.PasswordEncoder; + +class CustomerTest { + + private PayAccount payAccount; + private PasswordEncoder passwordEncoder; + + @BeforeEach + void setUp() { + payAccount = new TestPayAccount(1L); + passwordEncoder = new NoOpPasswordEncoder(); + } + + @Nested + @DisplayName("Customer 생성은") + class IsConstructed { + @Nested + @DisplayName("이름이") + class NameMust { + @Test + @DisplayName("[성공] 50자까지 허용한다.") + void successWith50() { + Assertions.assertDoesNotThrow( + () -> new Customer("aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeee", + "validEmail@validEmail.com", + "validPassword", "010-0000-0000", payAccount, passwordEncoder)); + } + + @Test + @DisplayName("[예외] null이면 예외가 발생한다.") + void failWithNull() { + Assertions.assertThrows(InvalidCreationException.class, + () -> new Customer(null, "validEmail@validEmail.com", "validPassword", "010-0000-0000", payAccount, + passwordEncoder)); + } + + @Test + @DisplayName("[예외] 공란이면 예외가 발생한다.") + void failWithBlank() { + Assertions.assertThrows(InvalidCreationException.class, + () -> new Customer(" ", "validEmail@validEmail.com", "validPassword", "010-0000-0000", payAccount, + passwordEncoder)); + } + + @Test + @DisplayName("[예외] 50자를 초과하면 예외가 발생한다.") + void failWith51() { + Assertions.assertThrows(InvalidCreationException.class, + () -> new Customer("aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeef", + "validEmail@validEmail.com", + "validPassword", "010-0000-0000", payAccount, passwordEncoder)); + } + } + + @Nested + @DisplayName("이메일이") + class EmailMust { + @Test + @DisplayName("[성공] 100자까지 허용한다.") + void successWith100() { + Assertions.assertDoesNotThrow( + () -> new Customer("aaaaaaaaaa", + "aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeaaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeee", + "validPassword", "010-0000-0000", payAccount, passwordEncoder)); + } + + @Test + @DisplayName("[예외] null이면 예외가 발생한다.") + void failWithNull() { + Assertions.assertThrows(InvalidCreationException.class, + () -> new Customer("aaaaaaaaaa", null, "validPassword", "010-0000-0000", payAccount, + passwordEncoder)); + } + + @Test + @DisplayName("[예외] 공란이면 예외가 발생한다.") + void failWithBlank() { + Assertions.assertThrows(InvalidCreationException.class, + () -> new Customer("aaaaaaaaaa", " ", "validPassword", "010-0000-0000", payAccount, + passwordEncoder)); + } + + @Test + @DisplayName("[예외] 100자를 초과하면 예외가 발생한다.") + void failWith101() { + Assertions.assertThrows(InvalidCreationException.class, + () -> new Customer("aaaaaaaaaa", + "aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeaaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeea", + "validPassword", "010-0000-0000", payAccount, passwordEncoder)); + } + } + + @Nested + @DisplayName("비밀번호가") + class PasswordMust { + @Test + @DisplayName("[성공] 30자까지 허용한다.") + void successWith30() { + Assertions.assertDoesNotThrow( + () -> new Customer("aaaaaaaaaa", "validEmail@validEmail.com", "thisstringsizeisthirtyalsnvien", + "010-0000-0000", payAccount, passwordEncoder)); + } + + @Test + @DisplayName("[예외] null이면 예외가 발생한다.") + void failWithNull() { + Assertions.assertThrows(InvalidCreationException.class, + () -> new Customer("aaaaaaaaaa", "validEmail@validEmail.com", null, "010-0000-0000", payAccount, + passwordEncoder)); + } + + @Test + @DisplayName("[예외] 공란이면 예외가 발생한다.") + void failWithBlank() { + Assertions.assertThrows(InvalidCreationException.class, + () -> new Customer("aaaaaaaaaa", "validEmail@validEmail.com", " ", "010-0000-0000", payAccount, + passwordEncoder)); + } + + @Test + @DisplayName("[예외] 30자를 초과하면 예외가 발생한다.") + void failWith31() { + Assertions.assertThrows(InvalidCreationException.class, + () -> new Customer("aaaaaaaaaa", "validEmail@validEmail.com", "thisstringsizeisthirtyonesnvien", + "010-0000-0000", payAccount, passwordEncoder)); + } + } + + @Nested + @DisplayName("전화번호가") + class PhoneMust { + @Test + @DisplayName("[성공] 30자까지 허용한다.") + void successWith30() { + Assertions.assertDoesNotThrow( + () -> new Customer("aaaaaaaaaa", "validEmail@validEmail.com", "validPassword", + "0000000000-0000000000-00000000", + payAccount, passwordEncoder)); + } + + @Test + @DisplayName("[예외] null이면 예외가 발생한다.") + void failWithNull() { + Assertions.assertThrows(InvalidCreationException.class, + () -> new Customer("aaaaaaaaaa", "validEmail@validEmail.com", "validPassword", null, + payAccount, passwordEncoder)); + } + + @Test + @DisplayName("[예외] 공란이면 예외가 발생한다.") + void failWithBlank() { + Assertions.assertThrows(InvalidCreationException.class, + () -> new Customer("aaaaaaaaaa", "validEmail@validEmail.com", "validPassword", " ", + payAccount, passwordEncoder)); + } + + @Test + @DisplayName("[예외] 30자를 초과하면 예외가 발생한다.") + void failWith31() { + Assertions.assertThrows(InvalidCreationException.class, + () -> new Customer("aaaaaaaaaa", "validEmail@validEmail.com", "validPassword", + "0000000000-0000000000-000000000", + payAccount, passwordEncoder)); + } + } + + @Nested + @DisplayName("페이계좌가") + class PayAccountMust { + @Test + @DisplayName("[성공] 있으면 성공한다.") + void successWithExist() { + Assertions.assertDoesNotThrow( + () -> new Customer("validName", "validEmail@validEmail.com", "validPassword", "010-0000-0000", + payAccount, passwordEncoder)); + } + + @Test + @DisplayName("[예외] null이면 예외가 발생한다.") + void failWithNull() { + Assertions.assertThrows(InvalidCreationException.class, + () -> new Customer("aaaaaaaaaa", "validEmail@validEmail.com", "validPassword", "010-0000-0000", + null, + passwordEncoder)); + } + } + } +} diff --git a/src/test/java/camp/woowak/lab/customer/service/SignUpCustomerServiceIntegrationTest.java b/src/test/java/camp/woowak/lab/customer/service/SignUpCustomerServiceIntegrationTest.java new file mode 100644 index 00000000..afb237b4 --- /dev/null +++ b/src/test/java/camp/woowak/lab/customer/service/SignUpCustomerServiceIntegrationTest.java @@ -0,0 +1,52 @@ +package camp.woowak.lab.customer.service; + +import static org.junit.jupiter.api.Assertions.*; + +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 camp.woowak.lab.customer.exception.DuplicateEmailException; +import camp.woowak.lab.customer.exception.InvalidCreationException; +import camp.woowak.lab.customer.repository.CustomerRepository; +import camp.woowak.lab.customer.service.command.SignUpCustomerCommand; +import camp.woowak.lab.payaccount.repository.PayAccountRepository; + +@SpringBootTest +class SignUpCustomerServiceIntegrationTest { + + @Autowired + private SignUpCustomerService service; + + @Autowired + private CustomerRepository customerRepository; + + @Autowired + private PayAccountRepository payAccountRepository; + + @Test + @DisplayName("이메일 중복 시 롤백 테스트") + void testRollbackOnDuplicateEmail() throws InvalidCreationException, DuplicateEmailException { + // given + SignUpCustomerCommand command1 = new SignUpCustomerCommand("name1", "email@example.com", "password", + "010-1234-5678"); + SignUpCustomerCommand command2 = new SignUpCustomerCommand("name2", "email@example.com", "password", + "010-8765-4321"); + + // when + service.signUp(command1); + + assertEquals(1, customerRepository.count()); + assertEquals(1, payAccountRepository.count()); + + // then + try { + service.signUp(command2); + fail("중복 이메일 예외가 발생해야 합니다."); + } catch (DuplicateEmailException e) { + assertEquals(1, customerRepository.count()); + assertEquals(1, payAccountRepository.count()); + } + } +} \ No newline at end of file diff --git a/src/test/java/camp/woowak/lab/customer/service/SignUpCustomerServiceTest.java b/src/test/java/camp/woowak/lab/customer/service/SignUpCustomerServiceTest.java new file mode 100644 index 00000000..f3bbb0d9 --- /dev/null +++ b/src/test/java/camp/woowak/lab/customer/service/SignUpCustomerServiceTest.java @@ -0,0 +1,78 @@ +package camp.woowak.lab.customer.service; + +import static org.mockito.BDDMockito.*; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.dao.DataIntegrityViolationException; + +import camp.woowak.lab.customer.domain.Customer; +import camp.woowak.lab.customer.exception.DuplicateEmailException; +import camp.woowak.lab.customer.exception.InvalidCreationException; +import camp.woowak.lab.customer.repository.CustomerRepository; +import camp.woowak.lab.customer.service.command.SignUpCustomerCommand; +import camp.woowak.lab.fixture.CustomerFixture; +import camp.woowak.lab.payaccount.domain.PayAccount; +import camp.woowak.lab.payaccount.repository.PayAccountRepository; +import camp.woowak.lab.web.authentication.NoOpPasswordEncoder; +import camp.woowak.lab.web.authentication.PasswordEncoder; + +@ExtendWith(MockitoExtension.class) +class SignUpCustomerServiceTest implements CustomerFixture { + + @InjectMocks + private SignUpCustomerService service; + + @Mock + private CustomerRepository customerRepository; + + @Mock + private PayAccountRepository payAccountRepository; + + @Mock + private PasswordEncoder passwordEncoder; + + @Test + @DisplayName("구매자 회원가입 테스트") + void testSignUp() throws InvalidCreationException, DuplicateEmailException { + // given + given(passwordEncoder.encode(Mockito.anyString())).willReturn("password"); + PayAccount payAccount = createPayAccount(); + Customer customer = createCustomer(payAccount, new NoOpPasswordEncoder()); + given(payAccountRepository.save(Mockito.any(PayAccount.class))).willReturn(payAccount); + given(customerRepository.save(Mockito.any(Customer.class))).willReturn(customer); + + // when + SignUpCustomerCommand command = + new SignUpCustomerCommand("name", "email@example.com", "password", "01012345678"); + service.signUp(command); + + // then + then(payAccountRepository).should().save(Mockito.any(PayAccount.class)); + then(customerRepository).should().save(Mockito.any(Customer.class)); + } + + @Test + @DisplayName("구매자 이메일 중복 회원가입 테스트") + void testSignUpWithExistingEmail() { + // given + given(passwordEncoder.encode(Mockito.anyString())).willReturn("password"); + given(payAccountRepository.save(Mockito.any(PayAccount.class))).willReturn(createPayAccount()); + when(customerRepository.save(Mockito.any(Customer.class))).thenThrow(DataIntegrityViolationException.class); + + // when + SignUpCustomerCommand command = + new SignUpCustomerCommand("name", "email@example.com", "password", "01012345678"); + + // then + Assertions.assertThrows(DuplicateEmailException.class, () -> service.signUp(command)); + then(payAccountRepository).should().save(Mockito.any(PayAccount.class)); + then(customerRepository).should().save(Mockito.any(Customer.class)); + } +} \ No newline at end of file diff --git a/src/test/java/camp/woowak/lab/fixture/CustomerFixture.java b/src/test/java/camp/woowak/lab/fixture/CustomerFixture.java new file mode 100644 index 00000000..75c1ac1b --- /dev/null +++ b/src/test/java/camp/woowak/lab/fixture/CustomerFixture.java @@ -0,0 +1,22 @@ +package camp.woowak.lab.fixture; + +import camp.woowak.lab.customer.domain.Customer; +import camp.woowak.lab.customer.exception.InvalidCreationException; +import camp.woowak.lab.payaccount.domain.PayAccount; +import camp.woowak.lab.web.authentication.PasswordEncoder; + +public interface CustomerFixture { + default PayAccount createPayAccount() { + return new PayAccount(); + } + + default Customer createCustomer(PayAccount payAccount, PasswordEncoder passwordEncoder) { + try { + return new Customer("vendorName", "vendorEmail@example.com", "vendorPassword", "010-0000-0000", payAccount, + passwordEncoder); + } catch (InvalidCreationException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/src/test/java/camp/woowak/lab/web/api/customer/CustomerApiControllerTest.java b/src/test/java/camp/woowak/lab/web/api/customer/CustomerApiControllerTest.java new file mode 100644 index 00000000..4d55fbe2 --- /dev/null +++ b/src/test/java/camp/woowak/lab/web/api/customer/CustomerApiControllerTest.java @@ -0,0 +1,215 @@ +package camp.woowak.lab.web.api.customer; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.BDDMockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +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.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import camp.woowak.lab.customer.exception.CustomerErrorCode; +import camp.woowak.lab.customer.exception.DuplicateEmailException; +import camp.woowak.lab.customer.service.SignUpCustomerService; +import camp.woowak.lab.web.dto.request.customer.SignUpCustomerRequest; + +@WebMvcTest(CustomerApiController.class) +@MockBean(JpaMetamodelMappingContext.class) +class CustomerApiControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private SignUpCustomerService signUpCustomerService; + + @Autowired + private ObjectMapper objectMapper; + + @Test + @DisplayName("구매자 회원가입 테스트 - 성공") + void testSignUpCustomer() throws Exception { + // given + SignUpCustomerRequest request = new SignUpCustomerRequest("name", "email@test.com", "password123", + "010-1234-5678"); + given(signUpCustomerService.signUp(any())).willReturn(1L); + + // when & then + mockMvc.perform(post("/customers") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isCreated()); + } + + @Test + @DisplayName("구매자 회원가입 테스트 - 이름이 없는 경우") + void testSignUpCustomerWithoutName() throws Exception { + // given + SignUpCustomerRequest request = new SignUpCustomerRequest("", "email@test.com", "password123", "010-1234-5678"); + + // when & then + mockMvc.perform(post("/customers") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("구매자 회원가입 테스트 - 이메일이 없는 경우") + void testSignUpCustomerWithoutEmail() throws Exception { + // given + SignUpCustomerRequest request = new SignUpCustomerRequest("name", "", "password123", "010-1234-5678"); + + // when & then + mockMvc.perform(post("/customers") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("구매자 회원가입 테스트 - 비밀번호가 없는 경우") + void testSignUpCustomerWithoutPassword() throws Exception { + // given + SignUpCustomerRequest request = new SignUpCustomerRequest("name", "email@test.com", "", "010-1234-5678"); + + // when & then + mockMvc.perform(post("/customers") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("구매자 회원가입 테스트 - 전화번호가 없는 경우") + void testSignUpCustomerWithoutPhone() throws Exception { + // given + SignUpCustomerRequest request = new SignUpCustomerRequest("name", "email@test.com", "password123", ""); + + // when & then + mockMvc.perform(post("/customers") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("구매자 회원가입 테스트 - 이름이 50자 초과인 경우") + void testSignUpCustomerWithLongName() throws Exception { + // given + SignUpCustomerRequest request = new SignUpCustomerRequest("n".repeat(51), "email@test.com", "password123", + "010-1234-5678"); + + // when & then + mockMvc.perform(post("/customers") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("구매자 회원가입 테스트 - 이메일이 100자 초과인 경우") + void testSignUpCustomerWithLongEmail() throws Exception { + // given + SignUpCustomerRequest request = new SignUpCustomerRequest("name", "e".repeat(90) + "@test.com", "password123", + "010-1234-5678"); + + // when & then + mockMvc.perform(post("/customers") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("구매자 회원가입 테스트 - 비밀번호가 20자 초과인 경우") + void testSignUpCustomerWithLongPassword() throws Exception { + SignUpCustomerRequest request = new SignUpCustomerRequest("name", "email@test.com", "p".repeat(21), + "010-1234-5678"); + + mockMvc.perform(post("/customers") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("구매자 회원가입 테스트 - 비밀번호가 8자 미만인 경우") + void testSignUpCustomerWithShortPassword() throws Exception { + // given + SignUpCustomerRequest request = new SignUpCustomerRequest("name", "email@test.com", "pass", "010-1234-5678"); + + // when & then + mockMvc.perform(post("/customers") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("구매자 회원가입 테스트 - 전화번호가 30자 초과인 경우") + void testSignUpCustomerWithLongPhone() throws Exception { + // given + SignUpCustomerRequest request = new SignUpCustomerRequest("name", "email@test.com", "password123", + "0".repeat(31)); + + // when & then + mockMvc.perform(post("/customers") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("구매자 회원가입 테스트 - 이메일 형식이 아닌 경우") + void testSignUpCustomerWithInvalidEmail() throws Exception { + // given + SignUpCustomerRequest request = new SignUpCustomerRequest("name", "invalid-email", "password123", + "010-1234-5678"); + + // when & then + mockMvc.perform(post("/customers") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("구매자 회원가입 테스트 - 전화번호 형식이 아닌 경우") + void testSignUpCustomerWithInvalidPhone() throws Exception { + // given + SignUpCustomerRequest request = new SignUpCustomerRequest("name", "email@test.com", "password123", + "invalid-phone"); + + // when & then + mockMvc.perform(post("/customers") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("구매자 회원가입 테스트 - 중복된 이메일인 경우") + void testSignUpCustomerWithDuplicateEmail() throws Exception { + // given + SignUpCustomerRequest request = new SignUpCustomerRequest("name", "duplicate@test.com", "password123", + "010-1234-5678"); + given(signUpCustomerService.signUp(any())).willThrow(new DuplicateEmailException()); + + // when & then + mockMvc.perform(post("/customers") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isConflict()) + .andExpect(jsonPath("$.errorCode").value(CustomerErrorCode.DUPLICATE_EMAIL.getErrorCode())) + .andExpect(jsonPath("$.detail").value(CustomerErrorCode.DUPLICATE_EMAIL.getMessage())); + } +}