diff --git a/.github/DISCUSSION_TEMPLATE/general.yml b/.github/DISCUSSION_TEMPLATE/general.yml new file mode 100644 index 00000000..01e86dad --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/general.yml @@ -0,0 +1,29 @@ +title: "[๋…ผ์˜] " +labels: ["๐Ÿ™‹โ€โ™‚๏ธ Question"] +body: + - type: markdown + attributes: + value: | + This is text that will show up in the template! + - type: textarea + id: background + attributes: + label: ๋ฐฐ๊ฒฝ + description: "๋ฐœ์˜ํ•œ ๋ฐฐ๊ฒฝ์— ๋Œ€ํ•ด ๊ฐ„๋žตํžˆ ์„œ์ˆ ํ•ด์ฃผ์„ธ์š”" + value: | + ... + render: bash + validations: + required: true + - type: textarea + id: subject + attributes: + label: ์„ธ๋ถ€ ์ฃผ์ œ + description: "๋…ผ์˜ํ•˜๊ณ  ์‹ถ์€ ์„ธ๋ถ€ ์ฃผ์ œ๋ฅผ ์ž‘์„ฑํ•ด์ฃผ์„ธ์š”." + value: | + 1. + 2. + 3. + render: bash + validations: + required: true diff --git a/src/main/java/camp/woowak/lab/common/advice/DomainExceptionHandler.java b/src/main/java/camp/woowak/lab/common/advice/DomainExceptionHandler.java new file mode 100644 index 00000000..56a9c564 --- /dev/null +++ b/src/main/java/camp/woowak/lab/common/advice/DomainExceptionHandler.java @@ -0,0 +1,32 @@ +package camp.woowak.lab.common.advice; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.Ordered; +import org.springframework.core.annotation.AliasFor; +import org.springframework.core.annotation.Order; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@RestControllerAdvice +@Order(Ordered.LOWEST_PRECEDENCE - 1) +public @interface DomainExceptionHandler { + @AliasFor(annotation = RestControllerAdvice.class, attribute = "basePackages") + String[] basePackages() default {}; + + @AliasFor(annotation = RestControllerAdvice.class, attribute = "basePackageClasses") + Class[] basePackageClasses() default {}; + + @AliasFor(annotation = RestControllerAdvice.class, attribute = "assignableTypes") + Class[] assignableTypes() default {}; + + @AliasFor(annotation = RestControllerAdvice.class, attribute = "annotations") + Class[] annotations() default {}; +} diff --git a/src/main/java/camp/woowak/lab/common/advice/GlobalExceptionHandler.java b/src/main/java/camp/woowak/lab/common/advice/GlobalExceptionHandler.java new file mode 100644 index 00000000..5e0b39ab --- /dev/null +++ b/src/main/java/camp/woowak/lab/common/advice/GlobalExceptionHandler.java @@ -0,0 +1,26 @@ +package camp.woowak.lab.common.advice; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ProblemDetail; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { + + @ExceptionHandler(Exception.class) + public ProblemDetail handleAllUncaughtException(Exception e) { + ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, + e.getMessage()); + problemDetail.setProperty("errorCode", "9999"); + + log.error("[Unexpected Exception]", e); + // TODO: Notion Hook ๋“ฑ๋ก + + return problemDetail; + } +} diff --git a/src/main/java/camp/woowak/lab/common/exception/BadRequestException.java b/src/main/java/camp/woowak/lab/common/exception/BadRequestException.java new file mode 100644 index 00000000..4fb65cd5 --- /dev/null +++ b/src/main/java/camp/woowak/lab/common/exception/BadRequestException.java @@ -0,0 +1,11 @@ +package camp.woowak.lab.common.exception; + +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/common/exception/ConflictException.java b/src/main/java/camp/woowak/lab/common/exception/ConflictException.java new file mode 100644 index 00000000..e7b16219 --- /dev/null +++ b/src/main/java/camp/woowak/lab/common/exception/ConflictException.java @@ -0,0 +1,7 @@ +package camp.woowak.lab.common.exception; + +public class ConflictException extends HttpStatusException { + public ConflictException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/camp/woowak/lab/common/exception/ErrorCode.java b/src/main/java/camp/woowak/lab/common/exception/ErrorCode.java new file mode 100644 index 00000000..93bd938f --- /dev/null +++ b/src/main/java/camp/woowak/lab/common/exception/ErrorCode.java @@ -0,0 +1,9 @@ +package camp.woowak.lab.common.exception; + +public interface ErrorCode { + int getStatus(); + + String getErrorCode(); + + String getMessage(); +} diff --git a/src/main/java/camp/woowak/lab/common/exception/ForbiddenException.java b/src/main/java/camp/woowak/lab/common/exception/ForbiddenException.java new file mode 100644 index 00000000..05ab0863 --- /dev/null +++ b/src/main/java/camp/woowak/lab/common/exception/ForbiddenException.java @@ -0,0 +1,7 @@ +package camp.woowak.lab.common.exception; + +public class ForbiddenException extends HttpStatusException { + public ForbiddenException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/camp/woowak/lab/common/exception/HttpStatusException.java b/src/main/java/camp/woowak/lab/common/exception/HttpStatusException.java new file mode 100644 index 00000000..98deaf25 --- /dev/null +++ b/src/main/java/camp/woowak/lab/common/exception/HttpStatusException.java @@ -0,0 +1,20 @@ +package camp.woowak.lab.common.exception; + +public class HttpStatusException extends RuntimeException { + private final ErrorCode errorCode; + + @Deprecated + public HttpStatusException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } + + public HttpStatusException(ErrorCode errorCode, String message) { + super(message); + this.errorCode = errorCode; + } + + public ErrorCode errorCode() { + return errorCode; + } +} diff --git a/src/main/java/camp/woowak/lab/common/exception/MethodNotAllowedException.java b/src/main/java/camp/woowak/lab/common/exception/MethodNotAllowedException.java new file mode 100644 index 00000000..6704a8cd --- /dev/null +++ b/src/main/java/camp/woowak/lab/common/exception/MethodNotAllowedException.java @@ -0,0 +1,7 @@ +package camp.woowak.lab.common.exception; + +public class MethodNotAllowedException extends HttpStatusException { + public MethodNotAllowedException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/camp/woowak/lab/common/exception/NotFoundException.java b/src/main/java/camp/woowak/lab/common/exception/NotFoundException.java new file mode 100644 index 00000000..6928975e --- /dev/null +++ b/src/main/java/camp/woowak/lab/common/exception/NotFoundException.java @@ -0,0 +1,7 @@ +package camp.woowak.lab.common.exception; + +public class NotFoundException extends HttpStatusException { + public NotFoundException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/camp/woowak/lab/common/exception/UnauthorizedException.java b/src/main/java/camp/woowak/lab/common/exception/UnauthorizedException.java new file mode 100644 index 00000000..ba2e1237 --- /dev/null +++ b/src/main/java/camp/woowak/lab/common/exception/UnauthorizedException.java @@ -0,0 +1,7 @@ +package camp.woowak.lab.common.exception; + +public class UnauthorizedException extends HttpStatusException { + public UnauthorizedException(ErrorCode errorCode) { + super(errorCode); + } +} 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 d3fc6511..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,13 +1,45 @@ package camp.woowak.lab.customer.domain; +import camp.woowak.lab.customer.exception.InvalidCreationException; import camp.woowak.lab.payaccount.domain.PayAccount; -import jakarta.persistence.*; +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; - @OneToOne(fetch = FetchType.LAZY) - private PayAccount payAccount; + @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/menu/domain/Menu.java b/src/main/java/camp/woowak/lab/menu/domain/Menu.java index efaaac92..e5985670 100644 --- a/src/main/java/camp/woowak/lab/menu/domain/Menu.java +++ b/src/main/java/camp/woowak/lab/menu/domain/Menu.java @@ -1,13 +1,18 @@ package camp.woowak.lab.menu.domain; import camp.woowak.lab.store.domain.Store; -import jakarta.persistence.*; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; @Entity public class Menu { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - @ManyToOne(fetch = FetchType.LAZY) - private Store store; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @ManyToOne(fetch = FetchType.LAZY) + private Store store; } diff --git a/src/main/java/camp/woowak/lab/order/domain/Order.java b/src/main/java/camp/woowak/lab/order/domain/Order.java index bf7e8c42..206c0c4e 100644 --- a/src/main/java/camp/woowak/lab/order/domain/Order.java +++ b/src/main/java/camp/woowak/lab/order/domain/Order.java @@ -11,17 +11,13 @@ import jakarta.persistence.Table; @Entity -@Table(name = "Orders") +@Table(name = "ORDERS") public class Order { - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @ManyToOne(fetch = FetchType.LAZY) private Customer requester; - @ManyToOne(fetch = FetchType.LAZY) private Store store; - } diff --git a/src/main/java/camp/woowak/lab/payaccount/domain/PayAccount.java b/src/main/java/camp/woowak/lab/payaccount/domain/PayAccount.java index 3406a60f..cc5ff513 100644 --- a/src/main/java/camp/woowak/lab/payaccount/domain/PayAccount.java +++ b/src/main/java/camp/woowak/lab/payaccount/domain/PayAccount.java @@ -7,7 +7,7 @@ @Entity public class PayAccount { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; } diff --git a/src/main/java/camp/woowak/lab/payaccount/repository/PayAccountRepository.java b/src/main/java/camp/woowak/lab/payaccount/repository/PayAccountRepository.java new file mode 100644 index 00000000..1ca593af --- /dev/null +++ b/src/main/java/camp/woowak/lab/payaccount/repository/PayAccountRepository.java @@ -0,0 +1,8 @@ +package camp.woowak.lab.payaccount.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import camp.woowak.lab.payaccount.domain.PayAccount; + +public interface PayAccountRepository extends JpaRepository { +} diff --git a/src/main/java/camp/woowak/lab/payment/domain/PointPayment.java b/src/main/java/camp/woowak/lab/payment/domain/PointPayment.java index cee65e28..f0d46b2d 100644 --- a/src/main/java/camp/woowak/lab/payment/domain/PointPayment.java +++ b/src/main/java/camp/woowak/lab/payment/domain/PointPayment.java @@ -3,17 +3,22 @@ import camp.woowak.lab.customer.domain.Customer; import camp.woowak.lab.order.domain.Order; import camp.woowak.lab.vendor.domain.Vendor; -import jakarta.persistence.*; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; @Entity public class PointPayment { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - @ManyToOne(fetch = FetchType.LAZY) - private Order order; - @ManyToOne(fetch = FetchType.LAZY) - private Customer sender; - @ManyToOne(fetch = FetchType.LAZY) - private Vendor recipient; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @ManyToOne(fetch = FetchType.LAZY) + private Order order; + @ManyToOne(fetch = FetchType.LAZY) + private Customer sender; + @ManyToOne(fetch = FetchType.LAZY) + private Vendor recipient; } diff --git a/src/main/java/camp/woowak/lab/vendor/domain/Vendor.java b/src/main/java/camp/woowak/lab/vendor/domain/Vendor.java index 41c924ce..d6f35a9b 100644 --- a/src/main/java/camp/woowak/lab/vendor/domain/Vendor.java +++ b/src/main/java/camp/woowak/lab/vendor/domain/Vendor.java @@ -1,13 +1,49 @@ package camp.woowak.lab.vendor.domain; import camp.woowak.lab.payaccount.domain.PayAccount; -import jakarta.persistence.*; +import camp.woowak.lab.vendor.exception.InvalidVendorCreationException; +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; @Entity public class Vendor { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - @OneToOne(fetch = FetchType.LAZY) - private PayAccount payAccount; + @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; + + protected Vendor() { + } + + /** + * @throws InvalidVendorCreationException ๊ฒ€์ฆ์— ์‹คํŒจํ•˜๋ฉด + */ + public Vendor(String name, String email, String password, String phone, PayAccount payAccount, + PasswordEncoder passwordEncoder) { + VendorValidator.validate(name, email, password, phone, payAccount); + this.name = name; + this.email = email; + this.password = passwordEncoder.encode(password); + this.phone = phone; + this.payAccount = payAccount; + } + + public Long getId() { + return id; + } } diff --git a/src/main/java/camp/woowak/lab/vendor/domain/VendorValidator.java b/src/main/java/camp/woowak/lab/vendor/domain/VendorValidator.java new file mode 100644 index 00000000..6d8580cc --- /dev/null +++ b/src/main/java/camp/woowak/lab/vendor/domain/VendorValidator.java @@ -0,0 +1,64 @@ +package camp.woowak.lab.vendor.domain; + +import camp.woowak.lab.payaccount.domain.PayAccount; +import camp.woowak.lab.vendor.exception.InvalidVendorCreationException; +import camp.woowak.lab.vendor.exception.VendorErrorCode; + +public final class VendorValidator { + 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; + + public static void validate(final String name, final String email, final String password, final String phone, + final PayAccount payAccount) throws InvalidVendorCreationException { + checkName(name); + checkEmail(email); + checkPassword(password); + checkPhone(phone); + checkPayAccount(payAccount); + } + + private static void checkName(String name) throws InvalidVendorCreationException { + if (name == null || name.isBlank()) { + throw new InvalidVendorCreationException(VendorErrorCode.INVALID_NAME_EMPTY); + } + if (name.length() > MAX_NAME_LENGTH) { + throw new InvalidVendorCreationException(VendorErrorCode.INVALID_NAME_RANGE); + } + } + + private static void checkEmail(String email) throws InvalidVendorCreationException { + if (email == null || email.isBlank()) { + throw new InvalidVendorCreationException(VendorErrorCode.INVALID_EMAIL_EMPTY); + } + if (email.trim().length() > MAX_EMAIL_LENGTH) { + throw new InvalidVendorCreationException(VendorErrorCode.INVALID_EMAIL_RANGE); + } + } + + private static void checkPassword(String password) throws InvalidVendorCreationException { + if (password == null || password.isBlank()) { + throw new InvalidVendorCreationException(VendorErrorCode.INVALID_PASSWORD_EMPTY); + } + if (password.trim().length() < MIN_PASSWORD_LENGTH || password.trim().length() > MAX_PASSWORD_LENGTH) { + throw new InvalidVendorCreationException(VendorErrorCode.INVALID_PASSWORD_RANGE); + } + } + + private static void checkPhone(String phone) throws InvalidVendorCreationException { + if (phone == null || phone.isBlank()) { + throw new InvalidVendorCreationException(VendorErrorCode.INVALID_PHONE_EMPTY); + } + if (phone.trim().length() > MAX_PHONE_LENGTH) { + throw new InvalidVendorCreationException(VendorErrorCode.INVALID_PHONE_RANGE); + } + } + + private static void checkPayAccount(PayAccount payAccount) throws InvalidVendorCreationException { + if (payAccount == null) { + throw new InvalidVendorCreationException(VendorErrorCode.INVALID_PAY_ACCOUNT_EMPTY); + } + } +} diff --git a/src/main/java/camp/woowak/lab/vendor/exception/DuplicateEmailException.java b/src/main/java/camp/woowak/lab/vendor/exception/DuplicateEmailException.java new file mode 100644 index 00000000..d0867c72 --- /dev/null +++ b/src/main/java/camp/woowak/lab/vendor/exception/DuplicateEmailException.java @@ -0,0 +1,9 @@ +package camp.woowak.lab.vendor.exception; + +import camp.woowak.lab.common.exception.BadRequestException; + +public class DuplicateEmailException extends BadRequestException { + public DuplicateEmailException() { + super(VendorErrorCode.DUPLICATE_EMAIL); + } +} diff --git a/src/main/java/camp/woowak/lab/vendor/exception/InvalidVendorCreationException.java b/src/main/java/camp/woowak/lab/vendor/exception/InvalidVendorCreationException.java new file mode 100644 index 00000000..ded3d009 --- /dev/null +++ b/src/main/java/camp/woowak/lab/vendor/exception/InvalidVendorCreationException.java @@ -0,0 +1,10 @@ +package camp.woowak.lab.vendor.exception; + +import camp.woowak.lab.common.exception.BadRequestException; +import camp.woowak.lab.common.exception.ErrorCode; + +public class InvalidVendorCreationException extends BadRequestException { + public InvalidVendorCreationException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/camp/woowak/lab/vendor/exception/VendorErrorCode.java b/src/main/java/camp/woowak/lab/vendor/exception/VendorErrorCode.java new file mode 100644 index 00000000..8ff33311 --- /dev/null +++ b/src/main/java/camp/woowak/lab/vendor/exception/VendorErrorCode.java @@ -0,0 +1,43 @@ +package camp.woowak.lab.vendor.exception; + +import org.springframework.http.HttpStatus; + +import camp.woowak.lab.common.exception.ErrorCode; + +public enum VendorErrorCode implements ErrorCode { + INVALID_PHONE_EMPTY(HttpStatus.BAD_REQUEST, "v_1_1", "์ „ํ™”๋ฒˆํ˜ธ๊ฐ€ ์ž…๋ ฅ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."), + INVALID_PHONE_RANGE(HttpStatus.BAD_REQUEST, "v_1_2", "์ „ํ™”๋ฒˆํ˜ธ๋Š” 30์ž๋ฅผ ๋„˜์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."), + INVALID_PASSWORD_EMPTY(HttpStatus.BAD_REQUEST, "v1_3", "๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ž…๋ ฅ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."), + INVALID_PASSWORD_RANGE(HttpStatus.BAD_REQUEST, "v1_4", "๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” 8-30์ž ์ž…๋ ฅ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + INVALID_EMAIL_EMPTY(HttpStatus.BAD_REQUEST, "v1_5", "์ด๋ฉ”์ผ์ด ์ž…๋ ฅ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."), + INVALID_EMAIL_RANGE(HttpStatus.BAD_REQUEST, "v1_6", "์ด๋ฉ”์ผ์€ 100์ž๋ฅผ ๋„˜์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."), + INVALID_NAME_EMPTY(HttpStatus.BAD_REQUEST, "v1_7", "์ด๋ฆ„์ด ์ž…๋ ฅ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."), + INVALID_NAME_RANGE(HttpStatus.BAD_REQUEST, "v1_8", "์ด๋ฆ„์€ 50์ž๋ฅผ ๋„˜์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."), + INVALID_PAY_ACCOUNT_EMPTY(HttpStatus.BAD_REQUEST, "v_1_9", "ํฌ์ธํŠธ ๊ณ„์ขŒ๊ฐ€ ์ž…๋ ฅ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."), + DUPLICATE_EMAIL(HttpStatus.BAD_REQUEST, "v_2", "์ด๋ฏธ ๊ฐ€์ž…๋œ ์ด๋ฉ”์ผ์ž…๋‹ˆ๋‹ค."); + + private final int status; + private final String errorCode; + private final String message; + + VendorErrorCode(HttpStatus httpStatus, String errorCode, String message) { + this.status = httpStatus.value(); + 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/vendor/repository/.gitkeep b/src/main/java/camp/woowak/lab/vendor/repository/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/camp/woowak/lab/vendor/service/.gitkeep b/src/main/java/camp/woowak/lab/vendor/service/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/camp/woowak/lab/vendor/service/SignUpVendorService.java b/src/main/java/camp/woowak/lab/vendor/service/SignUpVendorService.java new file mode 100644 index 00000000..13b9e30a --- /dev/null +++ b/src/main/java/camp/woowak/lab/vendor/service/SignUpVendorService.java @@ -0,0 +1,41 @@ +package camp.woowak.lab.vendor.service; + +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import camp.woowak.lab.payaccount.domain.PayAccount; +import camp.woowak.lab.payaccount.repository.PayAccountRepository; +import camp.woowak.lab.vendor.domain.Vendor; +import camp.woowak.lab.vendor.exception.DuplicateEmailException; +import camp.woowak.lab.vendor.repository.VendorRepository; +import camp.woowak.lab.vendor.service.command.SignUpVendorCommand; +import camp.woowak.lab.web.authentication.PasswordEncoder; + +@Service +@Transactional +public class SignUpVendorService { + private final VendorRepository vendorRepository; + private final PayAccountRepository payAccountRepository; + private final PasswordEncoder passwordEncoder; + + public SignUpVendorService( + VendorRepository vendorRepository, PayAccountRepository payAccountRepository, PasswordEncoder passwordEncoder) { + this.vendorRepository = vendorRepository; + this.payAccountRepository = payAccountRepository; + this.passwordEncoder = passwordEncoder; + } + + public Long signUp(SignUpVendorCommand cmd) { + PayAccount newPayAccount = new PayAccount(); + payAccountRepository.save(newPayAccount); + Vendor newVendor = + new Vendor(cmd.name(), cmd.email(), cmd.password(), cmd.phone(), newPayAccount, passwordEncoder); + try { + vendorRepository.save(newVendor); + } catch (DataIntegrityViolationException e) { + throw new DuplicateEmailException(); + } + return newVendor.getId(); + } +} diff --git a/src/main/java/camp/woowak/lab/vendor/service/command/SignUpVendorCommand.java b/src/main/java/camp/woowak/lab/vendor/service/command/SignUpVendorCommand.java new file mode 100644 index 00000000..49e03095 --- /dev/null +++ b/src/main/java/camp/woowak/lab/vendor/service/command/SignUpVendorCommand.java @@ -0,0 +1,9 @@ +package camp.woowak.lab.vendor.service.command; + +public record SignUpVendorCommand( + String name, + String email, + String password, + String phone +) { +} diff --git a/src/main/java/camp/woowak/lab/web/api/.gitkeep b/src/main/java/camp/woowak/lab/web/api/.gitkeep deleted file mode 100644 index e69de29b..00000000 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/api/utils/APIResponse.java b/src/main/java/camp/woowak/lab/web/api/utils/APIResponse.java new file mode 100644 index 00000000..e41f44a8 --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/api/utils/APIResponse.java @@ -0,0 +1,25 @@ +package camp.woowak.lab.web.api.utils; + +import org.springframework.http.HttpStatus; + +/** + * APIResponse๋ฅผ Jackson์˜ ObjectMapper์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋ ค๋ฉด, + * Generic Type์˜ {@code data}์—๋Š” Getter ๋ฉ”์„œ๋“œ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. + */ +public class APIResponse { + private final T data; + private final int status; + + APIResponse(final HttpStatus status, final T data) { + this.data = data; + this.status = status.value(); + } + + public T getData() { + return data; + } + + public int getStatus() { + return status; + } +} diff --git a/src/main/java/camp/woowak/lab/web/api/utils/APIUtils.java b/src/main/java/camp/woowak/lab/web/api/utils/APIUtils.java new file mode 100644 index 00000000..9bdb3c68 --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/api/utils/APIUtils.java @@ -0,0 +1,19 @@ +package camp.woowak.lab.web.api.utils; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +/** + * + * API Utils + * of Method๋Š” status์™€ data๋ฅผ ์ด์šฉํ•ด APIResponse๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + * + */ +public final class APIUtils { + private APIUtils() { + } + + public static ResponseEntity> of(HttpStatus status, T data) { + return new ResponseEntity<>(new APIResponse<>(status, data), status); + } +} diff --git a/src/main/java/camp/woowak/lab/web/api/vendor/VendorApiController.java b/src/main/java/camp/woowak/lab/web/api/vendor/VendorApiController.java new file mode 100644 index 00000000..3d54139a --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/api/vendor/VendorApiController.java @@ -0,0 +1,35 @@ +package camp.woowak.lab.web.api.vendor; + +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.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import camp.woowak.lab.vendor.service.SignUpVendorService; +import camp.woowak.lab.vendor.service.command.SignUpVendorCommand; +import camp.woowak.lab.web.api.utils.APIResponse; +import camp.woowak.lab.web.api.utils.APIUtils; +import camp.woowak.lab.web.dto.request.vendor.SignUpVendorRequest; +import camp.woowak.lab.web.dto.response.vendor.SignUpVendorResponse; +import jakarta.validation.Valid; + +@RestController +public class VendorApiController { + private final SignUpVendorService signUpVendorService; + + public VendorApiController(SignUpVendorService signUpVendorService) { + this.signUpVendorService = signUpVendorService; + } + + @PostMapping("/vendors") + @ResponseStatus(HttpStatus.CREATED) + public ResponseEntity> signUpVendor( + @Valid @RequestBody SignUpVendorRequest request) { + SignUpVendorCommand command = + new SignUpVendorCommand(request.name(), request.email(), request.password(), request.phone()); + Long registeredId = signUpVendorService.signUp(command); + return APIUtils.of(HttpStatus.CREATED, new SignUpVendorResponse(registeredId)); + } +} diff --git a/src/main/java/camp/woowak/lab/web/api/vendor/VendorApiControllerAdvice.java b/src/main/java/camp/woowak/lab/web/api/vendor/VendorApiControllerAdvice.java new file mode 100644 index 00000000..1e328a3f --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/api/vendor/VendorApiControllerAdvice.java @@ -0,0 +1,25 @@ +package camp.woowak.lab.web.api.vendor; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ProblemDetail; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; + +import camp.woowak.lab.common.advice.DomainExceptionHandler; +import camp.woowak.lab.vendor.exception.DuplicateEmailException; +import camp.woowak.lab.vendor.exception.InvalidVendorCreationException; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@DomainExceptionHandler(basePackageClasses = VendorApiController.class) +public class VendorApiControllerAdvice { + @ExceptionHandler(InvalidVendorCreationException.class) + public ResponseEntity handleInvalidVendorCreationException(InvalidVendorCreationException ex) { + return ResponseEntity.of(ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, ex.getMessage())).build(); + } + + @ExceptionHandler(DuplicateEmailException.class) + public ResponseEntity handleDuplicateEmailException(DuplicateEmailException ex) { + return ResponseEntity.of(ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, ex.getMessage())).build(); + } +} diff --git a/src/main/java/camp/woowak/lab/web/authentication/.gitkeep b/src/main/java/camp/woowak/lab/web/authentication/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/camp/woowak/lab/web/authentication/AuthenticationErrorCode.java b/src/main/java/camp/woowak/lab/web/authentication/AuthenticationErrorCode.java new file mode 100644 index 00000000..9e635a74 --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/authentication/AuthenticationErrorCode.java @@ -0,0 +1,34 @@ +package camp.woowak.lab.web.authentication; + +import org.springframework.http.HttpStatus; + +import camp.woowak.lab.common.exception.ErrorCode; + +public enum AuthenticationErrorCode implements ErrorCode { + UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "a1", "๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค."); + + private final int status; + private final String errorCode; + private final String message; + + AuthenticationErrorCode(HttpStatus httpStatus, String errorCode, String message) { + this.status = httpStatus.value(); + 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/web/authentication/LoginCustomer.java b/src/main/java/camp/woowak/lab/web/authentication/LoginCustomer.java new file mode 100644 index 00000000..0a55c31d --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/authentication/LoginCustomer.java @@ -0,0 +1,14 @@ +package camp.woowak.lab.web.authentication; + +public class LoginCustomer implements LoginMember { + private final Long id; + + public LoginCustomer(Long id) { + this.id = id; + } + + @Override + public Long getId() { + return id; + } +} diff --git a/src/main/java/camp/woowak/lab/web/authentication/LoginMember.java b/src/main/java/camp/woowak/lab/web/authentication/LoginMember.java new file mode 100644 index 00000000..5079e9b8 --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/authentication/LoginMember.java @@ -0,0 +1,5 @@ +package camp.woowak.lab.web.authentication; + +public interface LoginMember { + Long getId(); +} diff --git a/src/main/java/camp/woowak/lab/web/authentication/LoginVendor.java b/src/main/java/camp/woowak/lab/web/authentication/LoginVendor.java new file mode 100644 index 00000000..4ac97539 --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/authentication/LoginVendor.java @@ -0,0 +1,14 @@ +package camp.woowak.lab.web.authentication; + +public class LoginVendor implements LoginMember { + private final Long id; + + public LoginVendor(Long id) { + this.id = id; + } + + @Override + public Long getId() { + return id; + } +} diff --git a/src/main/java/camp/woowak/lab/web/authentication/NoOpPasswordEncoder.java b/src/main/java/camp/woowak/lab/web/authentication/NoOpPasswordEncoder.java new file mode 100644 index 00000000..3ab44bce --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/authentication/NoOpPasswordEncoder.java @@ -0,0 +1,15 @@ +package camp.woowak.lab.web.authentication; + +import java.util.Objects; + +public class NoOpPasswordEncoder implements PasswordEncoder { + @Override + public String encode(String password) { + return password; + } + + @Override + public boolean matches(String password, String encodedPassword) { + return Objects.equals(password, encodedPassword); + } +} diff --git a/src/main/java/camp/woowak/lab/web/authentication/PasswordEncoder.java b/src/main/java/camp/woowak/lab/web/authentication/PasswordEncoder.java new file mode 100644 index 00000000..b2dcb931 --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/authentication/PasswordEncoder.java @@ -0,0 +1,7 @@ +package camp.woowak.lab.web.authentication; + +public interface PasswordEncoder { + String encode(String password); + + boolean matches(String password, String encodedPassword); +} diff --git a/src/main/java/camp/woowak/lab/web/authentication/annotation/AuthenticationPrincipal.java b/src/main/java/camp/woowak/lab/web/authentication/annotation/AuthenticationPrincipal.java new file mode 100644 index 00000000..4b6ae416 --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/authentication/annotation/AuthenticationPrincipal.java @@ -0,0 +1,12 @@ +package camp.woowak.lab.web.authentication.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface AuthenticationPrincipal { + boolean required() default true; +} diff --git a/src/main/java/camp/woowak/lab/web/authentication/config/AuthenticationConfig.java b/src/main/java/camp/woowak/lab/web/authentication/config/AuthenticationConfig.java new file mode 100644 index 00000000..28557cba --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/authentication/config/AuthenticationConfig.java @@ -0,0 +1,15 @@ +package camp.woowak.lab.web.authentication.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import camp.woowak.lab.web.authentication.NoOpPasswordEncoder; +import camp.woowak.lab.web.authentication.PasswordEncoder; + +@Configuration +public class AuthenticationConfig { + @Bean + public PasswordEncoder passwordEncoder() { + return new NoOpPasswordEncoder(); + } +} diff --git a/src/main/java/camp/woowak/lab/web/dto/request/.gitkeep b/src/main/java/camp/woowak/lab/web/dto/request/.gitkeep deleted file mode 100644 index e69de29b..00000000 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/request/vendor/SignUpVendorRequest.java b/src/main/java/camp/woowak/lab/web/dto/request/vendor/SignUpVendorRequest.java new file mode 100644 index 00000000..63cf946d --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/dto/request/vendor/SignUpVendorRequest.java @@ -0,0 +1,19 @@ +package camp.woowak.lab.web.dto.request.vendor; + +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 SignUpVendorRequest( + @NotBlank @Length(min = 1, max = 50) + String name, + @NotBlank @Email + String email, + @NotBlank @Length(min = 8, max = 30) + String password, + @Phone + String phone +) { +} diff --git a/src/main/java/camp/woowak/lab/web/dto/response/.gitkeep b/src/main/java/camp/woowak/lab/web/dto/response/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/camp/woowak/lab/web/dto/response/ApiResponse.java b/src/main/java/camp/woowak/lab/web/dto/response/ApiResponse.java new file mode 100644 index 00000000..7f0be70f --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/dto/response/ApiResponse.java @@ -0,0 +1,40 @@ +package camp.woowak.lab.web.dto.response; + +import camp.woowak.lab.web.error.ErrorCode; + +public class ApiResponse { + private String code; + private String message; + private T data; + + private ApiResponse(String code, String message) { + this.code = code; + this.message = message; + } + + private ApiResponse(String code, String message, T data) { + this.code = code; + this.message = message; + this.data = data; + } + + public static ApiResponse ok(T data) { + return new ApiResponse<>("OK", "success", data); + } + + public static ApiResponse error(ErrorCode errorCode) { + return new ApiResponse<>(errorCode.getCode(), errorCode.getMessage()); + } + + public String getCode() { + return code; + } + + public String getMessage() { + return message; + } + + public T getData() { + return data; + } +} 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/main/java/camp/woowak/lab/web/dto/response/vendor/SignUpVendorResponse.java b/src/main/java/camp/woowak/lab/web/dto/response/vendor/SignUpVendorResponse.java new file mode 100644 index 00000000..48216659 --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/dto/response/vendor/SignUpVendorResponse.java @@ -0,0 +1,6 @@ +package camp.woowak.lab.web.dto.response.vendor; + +public record SignUpVendorResponse( + Long id +) { +} diff --git a/src/main/java/camp/woowak/lab/web/error/ErrorCode.java b/src/main/java/camp/woowak/lab/web/error/ErrorCode.java new file mode 100644 index 00000000..53b0e82e --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/error/ErrorCode.java @@ -0,0 +1,22 @@ +package camp.woowak.lab.web.error; + +public enum ErrorCode { + AUTH_DUPLICATE_EMAIL("a1", "์ด๋ฏธ ๊ฐ€์ž…๋œ ์ด๋ฉ”์ผ ์ž…๋‹ˆ๋‹ค."), + SIGNUP_INVALID_REQUEST("s1", "์ž˜๋ชป๋œ ์š”์ฒญ์ž…๋‹ˆ๋‹ค."); + + private final String code; + private final String message; + + ErrorCode(String code, String message) { + this.code = code; + this.message = message; + } + + public String getCode() { + return code; + } + + public String getMessage() { + return message; + } +} diff --git a/src/main/java/camp/woowak/lab/web/resolver/session/LoginMemberArgumentResolver.java b/src/main/java/camp/woowak/lab/web/resolver/session/LoginMemberArgumentResolver.java new file mode 100644 index 00000000..435a0d54 --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/resolver/session/LoginMemberArgumentResolver.java @@ -0,0 +1,15 @@ +package camp.woowak.lab.web.resolver.session; + +import org.springframework.core.MethodParameter; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; + +import camp.woowak.lab.web.authentication.LoginMember; +import camp.woowak.lab.web.authentication.annotation.AuthenticationPrincipal; + +public abstract class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver { + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(AuthenticationPrincipal.class) + && LoginMember.class.isAssignableFrom(parameter.getParameterType()); + } +} diff --git a/src/main/java/camp/woowak/lab/web/resolver/session/SessionConst.java b/src/main/java/camp/woowak/lab/web/resolver/session/SessionConst.java new file mode 100644 index 00000000..a705bdb2 --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/resolver/session/SessionConst.java @@ -0,0 +1,9 @@ +package camp.woowak.lab.web.resolver.session; + +public final class SessionConst { + public static final String SESSION_VENDOR_KEY = "authentication_vendor"; + public static final String SESSION_CUSTOMER_KEY = "authentication_customer"; + + private SessionConst() { + } +} diff --git a/src/main/java/camp/woowak/lab/web/resolver/session/SessionCustomerArgumentResolver.java b/src/main/java/camp/woowak/lab/web/resolver/session/SessionCustomerArgumentResolver.java new file mode 100644 index 00000000..a172b231 --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/resolver/session/SessionCustomerArgumentResolver.java @@ -0,0 +1,37 @@ +package camp.woowak.lab.web.resolver.session; + +import org.springframework.core.MethodParameter; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.ModelAndViewContainer; + +import camp.woowak.lab.common.exception.UnauthorizedException; +import camp.woowak.lab.web.authentication.AuthenticationErrorCode; +import camp.woowak.lab.web.authentication.LoginCustomer; +import camp.woowak.lab.web.authentication.annotation.AuthenticationPrincipal; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; + +public class SessionCustomerArgumentResolver extends LoginMemberArgumentResolver { + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(AuthenticationPrincipal.class) + && LoginCustomer.class.isAssignableFrom(parameter.getParameterType()); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + AuthenticationPrincipal parameterAnnotation = parameter.getParameterAnnotation(AuthenticationPrincipal.class); + if (parameterAnnotation == null) { + return null; + } + HttpServletRequest request = (HttpServletRequest)webRequest.getNativeRequest(); + HttpSession session = request.getSession(false); + if (parameterAnnotation.required() && + (session == null || session.getAttribute(SessionConst.SESSION_CUSTOMER_KEY) == null)) { + throw new UnauthorizedException(AuthenticationErrorCode.UNAUTHORIZED); + } + return session == null ? null : session.getAttribute(SessionConst.SESSION_CUSTOMER_KEY); + } +} diff --git a/src/main/java/camp/woowak/lab/web/resolver/session/SessionVendorArgumentResolver.java b/src/main/java/camp/woowak/lab/web/resolver/session/SessionVendorArgumentResolver.java new file mode 100644 index 00000000..c6cafbfd --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/resolver/session/SessionVendorArgumentResolver.java @@ -0,0 +1,37 @@ +package camp.woowak.lab.web.resolver.session; + +import org.springframework.core.MethodParameter; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.ModelAndViewContainer; + +import camp.woowak.lab.common.exception.UnauthorizedException; +import camp.woowak.lab.web.authentication.AuthenticationErrorCode; +import camp.woowak.lab.web.authentication.LoginVendor; +import camp.woowak.lab.web.authentication.annotation.AuthenticationPrincipal; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; + +public class SessionVendorArgumentResolver extends LoginMemberArgumentResolver { + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(AuthenticationPrincipal.class) + && LoginVendor.class.isAssignableFrom(parameter.getParameterType()); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + AuthenticationPrincipal parameterAnnotation = parameter.getParameterAnnotation(AuthenticationPrincipal.class); + if (parameterAnnotation == null) { + return null; + } + HttpServletRequest request = (HttpServletRequest)webRequest.getNativeRequest(); + HttpSession session = request.getSession(false); + if (parameterAnnotation.required() && + (session == null || session.getAttribute(SessionConst.SESSION_VENDOR_KEY) == null)) { + throw new UnauthorizedException(AuthenticationErrorCode.UNAUTHORIZED); + } + return session == null ? null : session.getAttribute(SessionConst.SESSION_VENDOR_KEY); + } +} diff --git a/src/main/java/camp/woowak/lab/web/validation/annotation/Phone.java b/src/main/java/camp/woowak/lab/web/validation/annotation/Phone.java new file mode 100644 index 00000000..60deb9ec --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/validation/annotation/Phone.java @@ -0,0 +1,21 @@ +package camp.woowak.lab.web.validation.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import camp.woowak.lab.web.validation.validator.PhoneNumberValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +@Target({ElementType.FIELD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = PhoneNumberValidator.class) +public @interface Phone { + String message() default "Invalid phone number"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/src/main/java/camp/woowak/lab/web/validation/validator/PhoneNumberValidator.java b/src/main/java/camp/woowak/lab/web/validation/validator/PhoneNumberValidator.java new file mode 100644 index 00000000..788ec848 --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/validation/validator/PhoneNumberValidator.java @@ -0,0 +1,17 @@ +package camp.woowak.lab.web.validation.validator; + +import camp.woowak.lab.web.validation.annotation.Phone; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +public class PhoneNumberValidator implements ConstraintValidator { + private static final String PHONE_NUMBER_PATTERN = "^(01[0167]|02|0[3-6][1-4])-\\d{3,4}-\\d{4}$"; + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + if (value == null) { + return false; + } + return value.matches(PHONE_NUMBER_PATTERN); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 00000000..b4956434 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,4 @@ +spring.application.name=lab +spring.jpa.show-sql=true +spring.jpa.generate-ddl=true +spring.jpa.hibernate.ddl-auto=create-drop 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/fixture/VendorFixture.java b/src/test/java/camp/woowak/lab/fixture/VendorFixture.java new file mode 100644 index 00000000..ccf7063d --- /dev/null +++ b/src/test/java/camp/woowak/lab/fixture/VendorFixture.java @@ -0,0 +1,16 @@ +package camp.woowak.lab.fixture; + +import camp.woowak.lab.payaccount.domain.PayAccount; +import camp.woowak.lab.vendor.domain.Vendor; +import camp.woowak.lab.web.authentication.PasswordEncoder; + +public interface VendorFixture { + default PayAccount createPayAccount() { + return new PayAccount(); + } + + default Vendor createVendor(PayAccount payAccount, PasswordEncoder passwordEncoder) { + return new Vendor("vendorName", "vendorEmail@example.com", "vendorPassword", "010-0000-0000", payAccount, + passwordEncoder); + } +} diff --git a/src/test/java/camp/woowak/lab/payaccount/domain/TestPayAccount.java b/src/test/java/camp/woowak/lab/payaccount/domain/TestPayAccount.java new file mode 100644 index 00000000..7066928d --- /dev/null +++ b/src/test/java/camp/woowak/lab/payaccount/domain/TestPayAccount.java @@ -0,0 +1,14 @@ +package camp.woowak.lab.payaccount.domain; + +public class TestPayAccount extends PayAccount { + private Long id; + private PayAccount payAccount; + + public TestPayAccount(Long id) { + this.payAccount = payAccount; + } + + public Long getId() { + return id; + } +} diff --git a/src/test/java/camp/woowak/lab/vendor/domain/VendorTest.java b/src/test/java/camp/woowak/lab/vendor/domain/VendorTest.java new file mode 100644 index 00000000..ad89b05e --- /dev/null +++ b/src/test/java/camp/woowak/lab/vendor/domain/VendorTest.java @@ -0,0 +1,212 @@ +package camp.woowak.lab.vendor.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.payaccount.domain.PayAccount; +import camp.woowak.lab.payaccount.domain.TestPayAccount; +import camp.woowak.lab.vendor.exception.InvalidVendorCreationException; +import camp.woowak.lab.web.authentication.NoOpPasswordEncoder; +import camp.woowak.lab.web.authentication.PasswordEncoder; + +class VendorTest { + + private PayAccount payAccount; + private PasswordEncoder passwordEncoder; + + @BeforeEach + void setUp() { + payAccount = new TestPayAccount(1L); + passwordEncoder = new NoOpPasswordEncoder(); + } + + @Nested + @DisplayName("Vendor ์ƒ์„ฑ์€") + class IsConstructed { + @Nested + @DisplayName("์ด๋ฆ„์ด") + class NameMust { + @Test + @DisplayName("[์„ฑ๊ณต] 50์ž๊นŒ์ง€ ํ—ˆ์šฉํ•œ๋‹ค.") + void successWith50() { + Assertions.assertDoesNotThrow( + () -> new Vendor("aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeee", "validEmail@validEmail.com", + "validPassword", "010-0000-0000", payAccount, passwordEncoder)); + } + + @Test + @DisplayName("[์˜ˆ์™ธ] null์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + void failWithNull() { + Assertions.assertThrows(InvalidVendorCreationException.class, + () -> new Vendor(null, "validEmail@validEmail.com", "validPassword", "010-0000-0000", payAccount, + passwordEncoder)); + } + + @Test + @DisplayName("[์˜ˆ์™ธ] ๊ณต๋ž€์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + void failWithBlank() { + Assertions.assertThrows(InvalidVendorCreationException.class, + () -> new Vendor(" ", "validEmail@validEmail.com", "validPassword", "010-0000-0000", payAccount, + passwordEncoder)); + } + + @Test + @DisplayName("[์˜ˆ์™ธ] 50์ž๋ฅผ ์ดˆ๊ณผํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + void failWith51() { + Assertions.assertThrows(InvalidVendorCreationException.class, + () -> new Vendor("aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeef", "validEmail@validEmail.com", + "validPassword", "010-0000-0000", payAccount, passwordEncoder)); + } + } + + @Nested + @DisplayName("์ด๋ฉ”์ผ์ด") + class EmailMust { + @Test + @DisplayName("[์„ฑ๊ณต] 100์ž๊นŒ์ง€ ํ—ˆ์šฉํ•œ๋‹ค.") + void successWith100() { + Assertions.assertDoesNotThrow( + () -> new Vendor("aaaaaaaaaa", + "aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeaaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeee", + "validPassword", "010-0000-0000", payAccount, passwordEncoder)); + } + + @Test + @DisplayName("[์˜ˆ์™ธ] null์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + void failWithNull() { + Assertions.assertThrows(InvalidVendorCreationException.class, + () -> new Vendor("aaaaaaaaaa", null, "validPassword", "010-0000-0000", payAccount, + passwordEncoder)); + } + + @Test + @DisplayName("[์˜ˆ์™ธ] ๊ณต๋ž€์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + void failWithBlank() { + Assertions.assertThrows(InvalidVendorCreationException.class, + () -> new Vendor("aaaaaaaaaa", " ", "validPassword", "010-0000-0000", payAccount, passwordEncoder)); + } + + @Test + @DisplayName("[์˜ˆ์™ธ] 100์ž๋ฅผ ์ดˆ๊ณผํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + void failWith101() { + Assertions.assertThrows(InvalidVendorCreationException.class, + () -> new Vendor("aaaaaaaaaa", + "aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeaaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeea", + "validPassword", "010-0000-0000", payAccount, passwordEncoder)); + } + } + + @Nested + @DisplayName("๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€") + class PasswordMust { + @Test + @DisplayName("[์„ฑ๊ณต] 8์ž ์ด์ƒ๋ถ€ํ„ฐ ํ—ˆ์šฉํ•œ๋‹ค.") + void successWith8() { + Assertions.assertDoesNotThrow( + () -> new Vendor("aaaaaaaaaa", "validEmail@validEmail.com", "thisis8c", + "010-0000-0000", payAccount, passwordEncoder)); + } + + @Test + @DisplayName("[์„ฑ๊ณต] 30์ž๊นŒ์ง€ ํ—ˆ์šฉํ•œ๋‹ค.") + void successWith30() { + Assertions.assertDoesNotThrow( + () -> new Vendor("aaaaaaaaaa", "validEmail@validEmail.com", "thisstringsizeisthirtyalsnvien", + "010-0000-0000", payAccount, passwordEncoder)); + } + + @Test + @DisplayName("[์˜ˆ์™ธ] null์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + void failWithNull() { + Assertions.assertThrows(InvalidVendorCreationException.class, + () -> new Vendor("aaaaaaaaaa", "validEmail@validEmail.com", null, "010-0000-0000", payAccount, + passwordEncoder)); + } + + @Test + @DisplayName("[์˜ˆ์™ธ] ๊ณต๋ž€์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + void failWithBlank() { + Assertions.assertThrows(InvalidVendorCreationException.class, + () -> new Vendor("aaaaaaaaaa", "validEmail@validEmail.com", " ", "010-0000-0000", payAccount, + passwordEncoder)); + } + + @Test + @DisplayName("[์˜ˆ์™ธ] 8์ž ๋ฏธ๋งŒ์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + void failWith7() { + Assertions.assertThrows(InvalidVendorCreationException.class, + () -> new Vendor("aaaaaaaaaa", "validEmail@validEmail.com", "thisis7", + "010-0000-0000", payAccount, passwordEncoder)); + } + + @Test + @DisplayName("[์˜ˆ์™ธ] 30์ž๋ฅผ ์ดˆ๊ณผํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + void failWith31() { + Assertions.assertThrows(InvalidVendorCreationException.class, + () -> new Vendor("aaaaaaaaaa", "validEmail@validEmail.com", "thisstringsizeisthirtyonesnvien", + "010-0000-0000", payAccount, passwordEncoder)); + } + } + + @Nested + @DisplayName("์ „ํ™”๋ฒˆํ˜ธ๊ฐ€") + class PhoneMust { + @Test + @DisplayName("[์„ฑ๊ณต] 30์ž๊นŒ์ง€ ํ—ˆ์šฉํ•œ๋‹ค.") + void successWith30() { + Assertions.assertDoesNotThrow( + () -> new Vendor("aaaaaaaaaa", "validEmail@validEmail.com", "validPassword", + "0000000000-0000000000-00000000", + payAccount, passwordEncoder)); + } + + @Test + @DisplayName("[์˜ˆ์™ธ] null์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + void failWithNull() { + Assertions.assertThrows(InvalidVendorCreationException.class, + () -> new Vendor("aaaaaaaaaa", "validEmail@validEmail.com", "validPassword", null, + payAccount, passwordEncoder)); + } + + @Test + @DisplayName("[์˜ˆ์™ธ] ๊ณต๋ž€์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + void failWithBlank() { + Assertions.assertThrows(InvalidVendorCreationException.class, + () -> new Vendor("aaaaaaaaaa", "validEmail@validEmail.com", "validPassword", " ", + payAccount, passwordEncoder)); + } + + @Test + @DisplayName("[์˜ˆ์™ธ] 30์ž๋ฅผ ์ดˆ๊ณผํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + void failWith31() { + Assertions.assertThrows(InvalidVendorCreationException.class, + () -> new Vendor("aaaaaaaaaa", "validEmail@validEmail.com", "validPassword", + "0000000000-0000000000-000000000", + payAccount, passwordEncoder)); + } + } + + @Nested + @DisplayName("ํŽ˜์ด๊ณ„์ขŒ๊ฐ€") + class PayAccountMust { + @Test + @DisplayName("[์„ฑ๊ณต] ์žˆ์œผ๋ฉด ์„ฑ๊ณตํ•œ๋‹ค.") + void successWithExist() { + Assertions.assertDoesNotThrow( + () -> new Vendor("validName", "validEmail@validEmail.com", "validPassword", "010-0000-0000", + payAccount, passwordEncoder)); + } + + @Test + @DisplayName("[์˜ˆ์™ธ] null์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + void failWithNull() { + Assertions.assertThrows(InvalidVendorCreationException.class, + () -> new Vendor("aaaaaaaaaa", "validEmail@validEmail.com", "validPassword", "010-0000-0000", null, + passwordEncoder)); + } + } + } +} diff --git a/src/test/java/camp/woowak/lab/vendor/repository/VendorRepositoryTest.java b/src/test/java/camp/woowak/lab/vendor/repository/VendorRepositoryTest.java new file mode 100644 index 00000000..173fb485 --- /dev/null +++ b/src/test/java/camp/woowak/lab/vendor/repository/VendorRepositoryTest.java @@ -0,0 +1,82 @@ +package camp.woowak.lab.vendor.repository; + +import java.util.Optional; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.dao.DataIntegrityViolationException; + +import camp.woowak.lab.fixture.VendorFixture; +import camp.woowak.lab.payaccount.domain.PayAccount; +import camp.woowak.lab.payaccount.repository.PayAccountRepository; +import camp.woowak.lab.vendor.domain.Vendor; +import camp.woowak.lab.web.authentication.NoOpPasswordEncoder; +import camp.woowak.lab.web.authentication.PasswordEncoder; + +@DataJpaTest +class VendorRepositoryTest implements VendorFixture { + @Autowired + private VendorRepository vendorRepository; + @Autowired + private PayAccountRepository payAccountRepository; + @Autowired + private PasswordEncoder passwordEncoder; + + @TestConfiguration + static class TestContextConfiguration { + @Bean + public PasswordEncoder passwordEncoder() { + return new NoOpPasswordEncoder(); + } + } + + @AfterEach + void tearDown() { + vendorRepository.deleteAll(); + } + + @Nested + @DisplayName("Vendor ์ €์žฅ์€") + class IsSaved { + @Test + @DisplayName("[์„ฑ๊ณต] DB์— ์ €์žฅ๋œ๋‹ค.") + void success() { + // given + PayAccount payAccount = payAccountRepository.save(createPayAccount()); + + // when + Vendor vendor = createVendor(payAccount, passwordEncoder); + Vendor savedVendor = vendorRepository.save(vendor); + Long savedVendorId = savedVendor.getId(); + vendorRepository.flush(); + + // then + Optional findVendor = vendorRepository.findById(savedVendorId); + Assertions.assertTrue(findVendor.isPresent()); + Assertions.assertEquals(savedVendorId, findVendor.get().getId()); + } + + @Test + @DisplayName("[์˜ˆ์™ธ] ์ค‘๋ณต๋œ ์ด๋ฉ”์ผ์ด ์žˆ์œผ๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + void failWithDuplicateEmail() { + // given + PayAccount payAccount = payAccountRepository.save(createPayAccount()); + Vendor vendor = createVendor(payAccount, passwordEncoder); + vendorRepository.saveAndFlush(vendor); + + // when + PayAccount newPayAccount = payAccountRepository.save(createPayAccount()); + Vendor newVendor = createVendor(newPayAccount, passwordEncoder); + + // then + Assertions.assertThrows(DataIntegrityViolationException.class, () -> vendorRepository.save(newVendor)); + } + } +} diff --git a/src/test/java/camp/woowak/lab/vendor/service/SignUpVendorServiceTest.java b/src/test/java/camp/woowak/lab/vendor/service/SignUpVendorServiceTest.java new file mode 100644 index 00000000..daeb2398 --- /dev/null +++ b/src/test/java/camp/woowak/lab/vendor/service/SignUpVendorServiceTest.java @@ -0,0 +1,73 @@ +package camp.woowak.lab.vendor.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.fixture.VendorFixture; +import camp.woowak.lab.payaccount.domain.PayAccount; +import camp.woowak.lab.payaccount.repository.PayAccountRepository; +import camp.woowak.lab.vendor.domain.Vendor; +import camp.woowak.lab.vendor.exception.DuplicateEmailException; +import camp.woowak.lab.vendor.repository.VendorRepository; +import camp.woowak.lab.vendor.service.command.SignUpVendorCommand; +import camp.woowak.lab.web.authentication.NoOpPasswordEncoder; +import camp.woowak.lab.web.authentication.PasswordEncoder; + +@ExtendWith(MockitoExtension.class) +class SignUpVendorServiceTest implements VendorFixture { + @InjectMocks + private SignUpVendorService service; + @Mock + private VendorRepository vendorRepository; + @Mock + private PayAccountRepository payAccountRepository; + @Mock + private PasswordEncoder passwordEncoder; + + @Test + @DisplayName("[์„ฑ๊ณต] Vendor๊ฐ€ ์ €์žฅ๋œ๋‹ค.") + void success() throws DuplicateEmailException { + // given + given(passwordEncoder.encode(Mockito.anyString())).willReturn("password"); + PayAccount payAccount = createPayAccount(); + Vendor vendor = createVendor(payAccount, new NoOpPasswordEncoder()); + given(payAccountRepository.save(Mockito.any(PayAccount.class))).willReturn(payAccount); + given(vendorRepository.save(Mockito.any(Vendor.class))).willReturn(vendor); + + // when + SignUpVendorCommand command = + new SignUpVendorCommand("vendorName", "vendorEmail@example.com", "password", "010-0000-0000"); + service.signUp(command); + + // then + then(payAccountRepository).should().save(Mockito.any(PayAccount.class)); + then(vendorRepository).should().save(Mockito.any(Vendor.class)); + } + + @Test + @DisplayName("[์˜ˆ์™ธ] ๊ฐ€์ž…๋œ ์ด๋ฉ”์ผ์ธ ๊ฒฝ์šฐ ์˜ˆ์™ธ ๋ฐœ์ƒ") + void failWithDuplicateEmail() throws DuplicateEmailException { + // given + given(passwordEncoder.encode(Mockito.anyString())).willReturn("password"); + given(payAccountRepository.save(Mockito.any(PayAccount.class))).willReturn(createPayAccount()); + + // when + when(vendorRepository.save(Mockito.any(Vendor.class))).thenThrow(DataIntegrityViolationException.class); + SignUpVendorCommand command = + new SignUpVendorCommand("vendorName", "vendorEmail@example.com", "password", "010-0000-0000"); + + // then + Assertions.assertThrows(DuplicateEmailException.class, () -> service.signUp(command)); + then(payAccountRepository).should().save(Mockito.any(PayAccount.class)); + then(vendorRepository).should().save(Mockito.any(Vendor.class)); + } +} diff --git a/src/test/java/camp/woowak/lab/web/api/VendorApiControllerTest.java b/src/test/java/camp/woowak/lab/web/api/VendorApiControllerTest.java new file mode 100644 index 00000000..b6083049 --- /dev/null +++ b/src/test/java/camp/woowak/lab/web/api/VendorApiControllerTest.java @@ -0,0 +1,394 @@ +package camp.woowak.lab.web.api; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.util.Random; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.BDDMockito; +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.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import camp.woowak.lab.vendor.exception.DuplicateEmailException; +import camp.woowak.lab.vendor.service.SignUpVendorService; +import camp.woowak.lab.vendor.service.command.SignUpVendorCommand; +import camp.woowak.lab.web.api.vendor.VendorApiController; +import camp.woowak.lab.web.dto.request.vendor.SignUpVendorRequest; + +@WebMvcTest(controllers = VendorApiController.class) +@MockBean(JpaMetamodelMappingContext.class) +class VendorApiControllerTest { + @Autowired + private MockMvc mockMvc; + @MockBean + private SignUpVendorService signUpVendorService; + + @Nested + @DisplayName("ํŒ๋งค์ž ํšŒ์›๊ฐ€์ž…: POST /vendors") + class SignUpVendor { + @Test + @DisplayName("[์„ฑ๊ณต] 201") + void success() throws Exception { + long fakeVendorId = new Random().nextLong(1000L); + BDDMockito.given(signUpVendorService.signUp(BDDMockito.any(SignUpVendorCommand.class))) + .willReturn(fakeVendorId); + + // when + ResultActions actions = mockMvc.perform( + post("/vendors") + .content(new ObjectMapper().writeValueAsString( + new SignUpVendorRequest("validName", "validEmail@validEmail.com", "validPassword", + "010-0000-0000"))) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + ); + + // then + actions.andExpect(status().isCreated()) + .andExpect(jsonPath("$.status").value(HttpStatus.CREATED.value())) + .andExpect(jsonPath("$.data.id").value(fakeVendorId)) + .andDo(print()); + } + + @Nested + @DisplayName("[์‹คํŒจ] 400") + class FailWith400 { + @Nested + @DisplayName("์ด๋ฆ„์ด") + class NameMust { + @Test + @DisplayName("๋น„์–ด์žˆ๋Š” ๊ฒฝ์šฐ") + void failWithEmptyName() throws Exception { + long fakeVendorId = new Random().nextLong(1000L); + BDDMockito.given(signUpVendorService.signUp(BDDMockito.any(SignUpVendorCommand.class))) + .willReturn(fakeVendorId); + + // when + ResultActions actions = mockMvc.perform( + post("/vendors") + .content(new ObjectMapper().writeValueAsString( + new SignUpVendorRequest(null, "validEmail@validEmail.com", "validPassword", + "010-0000-0000"))) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + ); + + // then + actions.andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.type").value("about:blank")) + .andExpect(jsonPath("$.title").value("Bad Request")) + .andExpect(jsonPath("$.status").value(400)) + .andExpect(jsonPath("$.instance").value("/vendors")) + .andDo(print()); + } + + @Test + @DisplayName("๊ณต๋ž€์ธ ๊ฒฝ์šฐ") + void failWithBlankName() throws Exception { + long fakeVendorId = new Random().nextLong(1000L); + BDDMockito.given(signUpVendorService.signUp(BDDMockito.any(SignUpVendorCommand.class))) + .willReturn(fakeVendorId); + + // when + ResultActions actions = mockMvc.perform( + post("/vendors") + .content(new ObjectMapper().writeValueAsString( + new SignUpVendorRequest("", "validEmail@validEmail.com", "validPassword", + "010-0000-0000"))) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + ); + + // then + actions.andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.type").value("about:blank")) + .andExpect(jsonPath("$.title").value("Bad Request")) + .andExpect(jsonPath("$.status").value(400)) + .andExpect(jsonPath("$.instance").value("/vendors")) + .andDo(print()); + } + } + + @Nested + @DisplayName("์ด๋ฉ”์ผ์ด") + class EmailMust { + @Test + @DisplayName("๋น„์–ด์žˆ๋Š” ๊ฒฝ์šฐ") + void failWithEmptyEmail() throws Exception { + long fakeVendorId = new Random().nextLong(1000L); + BDDMockito.given(signUpVendorService.signUp(BDDMockito.any(SignUpVendorCommand.class))) + .willReturn(fakeVendorId); + + // when + ResultActions actions = mockMvc.perform( + post("/vendors") + .content(new ObjectMapper().writeValueAsString( + new SignUpVendorRequest("validName", null, "validPassword", "010-0000-0000"))) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + ); + + // then + actions.andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.type").value("about:blank")) + .andExpect(jsonPath("$.title").value("Bad Request")) + .andExpect(jsonPath("$.status").value(400)) + .andExpect(jsonPath("$.instance").value("/vendors")) + .andDo(print()); + } + + @Test + @DisplayName("๊ณต๋ž€์ธ ๊ฒฝ์šฐ") + void failWithBlankEmail() throws Exception { + long fakeVendorId = new Random().nextLong(1000L); + BDDMockito.given(signUpVendorService.signUp(BDDMockito.any(SignUpVendorCommand.class))) + .willReturn(fakeVendorId); + + // when + ResultActions actions = mockMvc.perform( + post("/vendors") + .content(new ObjectMapper().writeValueAsString( + new SignUpVendorRequest("validName", "", "validPassword", "010-0000-0000"))) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + ); + + // then + actions.andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.type").value("about:blank")) + .andExpect(jsonPath("$.title").value("Bad Request")) + .andExpect(jsonPath("$.status").value(400)) + .andExpect(jsonPath("$.instance").value("/vendors")) + .andDo(print()); + } + } + + @Nested + @DisplayName("๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€") + class PasswordMust { + @Test + @DisplayName("๋น„์–ด์žˆ๋Š” ๊ฒฝ์šฐ") + void failWithEmptyPassword() throws Exception { + long fakeVendorId = new Random().nextLong(1000L); + BDDMockito.given(signUpVendorService.signUp(BDDMockito.any(SignUpVendorCommand.class))) + .willReturn(fakeVendorId); + + // when + ResultActions actions = mockMvc.perform( + post("/vendors") + .content(new ObjectMapper().writeValueAsString( + new SignUpVendorRequest("validName", "validEmail@validEmail.com", null, + "010-0000-0000"))) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + ); + + // then + actions.andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.type").value("about:blank")) + .andExpect(jsonPath("$.title").value("Bad Request")) + .andExpect(jsonPath("$.status").value(400)) + .andExpect(jsonPath("$.instance").value("/vendors")) + .andDo(print()); + } + + @Test + @DisplayName("๊ณต๋ž€์ธ ๊ฒฝ์šฐ") + void failWithBlankPassword() throws Exception { + long fakeVendorId = new Random().nextLong(1000L); + BDDMockito.given(signUpVendorService.signUp(BDDMockito.any(SignUpVendorCommand.class))) + .willReturn(fakeVendorId); + + // when + ResultActions actions = mockMvc.perform( + post("/vendors") + .content(new ObjectMapper().writeValueAsString( + new SignUpVendorRequest("validName", "validEmail@validEmail.com", "", + "010-0000-0000"))) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + ); + + // then + actions.andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.type").value("about:blank")) + .andExpect(jsonPath("$.title").value("Bad Request")) + .andExpect(jsonPath("$.status").value(400)) + .andExpect(jsonPath("$.instance").value("/vendors")) + .andDo(print()); + } + + @Test + @DisplayName("8์ž ๋ฏธ๋งŒ์ธ ๊ฒฝ์šฐ") + void failWith7Password() throws Exception { + long fakeVendorId = new Random().nextLong(1000L); + BDDMockito.given(signUpVendorService.signUp(BDDMockito.any(SignUpVendorCommand.class))) + .willReturn(fakeVendorId); + + // when + ResultActions actions = mockMvc.perform( + post("/vendors") + .content(new ObjectMapper().writeValueAsString( + new SignUpVendorRequest("validName", "validEmail@validEmail.com", "abcdefg", + "010-0000-0000"))) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + ); + + // then + actions.andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.type").value("about:blank")) + .andExpect(jsonPath("$.title").value("Bad Request")) + .andExpect(jsonPath("$.status").value(400)) + .andExpect(jsonPath("$.instance").value("/vendors")) + .andDo(print()); + } + + @Test + @DisplayName("30์ž ์ดˆ๊ณผ์ธ ๊ฒฝ์šฐ") + void failWith31Password() throws Exception { + long fakeVendorId = new Random().nextLong(1000L); + BDDMockito.given(signUpVendorService.signUp(BDDMockito.any(SignUpVendorCommand.class))) + .willReturn(fakeVendorId); + + // when + ResultActions actions = mockMvc.perform( + post("/vendors") + .content(new ObjectMapper().writeValueAsString( + new SignUpVendorRequest("validName", "validEmail@validEmail.com", + "aaaaaaaaaabbbbbbbbbbccccccccccd", + "010-0000-0000"))) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + ); + + // then + actions.andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.type").value("about:blank")) + .andExpect(jsonPath("$.title").value("Bad Request")) + .andExpect(jsonPath("$.status").value(400)) + .andExpect(jsonPath("$.instance").value("/vendors")) + .andDo(print()); + } + } + + @Nested + @DisplayName("์ „ํ™”๋ฒˆํ˜ธ๊ฐ€") + class PhoneMust { + @Test + @DisplayName("๋น„์–ด์žˆ๋Š” ๊ฒฝ์šฐ") + void failWithEmptyPhone() throws Exception { + long fakeVendorId = new Random().nextLong(1000L); + BDDMockito.given(signUpVendorService.signUp(BDDMockito.any(SignUpVendorCommand.class))) + .willReturn(fakeVendorId); + + // when + ResultActions actions = mockMvc.perform( + post("/vendors") + .content(new ObjectMapper().writeValueAsString( + new SignUpVendorRequest("validName", "validEmail@validEmail.com", "validPassword", + null))) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + ); + + // then + actions.andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.type").value("about:blank")) + .andExpect(jsonPath("$.title").value("Bad Request")) + .andExpect(jsonPath("$.status").value(400)) + .andExpect(jsonPath("$.instance").value("/vendors")) + .andDo(print()); + } + + @Test + @DisplayName("๊ณต๋ž€์ธ ๊ฒฝ์šฐ") + void failWithBlankPhone() throws Exception { + long fakeVendorId = new Random().nextLong(1000L); + BDDMockito.given(signUpVendorService.signUp(BDDMockito.any(SignUpVendorCommand.class))) + .willReturn(fakeVendorId); + + // when + ResultActions actions = mockMvc.perform( + post("/vendors") + .content(new ObjectMapper().writeValueAsString( + new SignUpVendorRequest("validName", "validEmail@validEmail.com", "validPassword", ""))) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + ); + + // then + actions.andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.type").value("about:blank")) + .andExpect(jsonPath("$.title").value("Bad Request")) + .andExpect(jsonPath("$.status").value(400)) + .andExpect(jsonPath("$.instance").value("/vendors")) + .andDo(print()); + } + + @Test + @DisplayName("์ž˜๋ชป๋œ ํ˜•์‹์ธ ๊ฒฝ์šฐ") + void failWithInvalidPhone() throws Exception { + long fakeVendorId = new Random().nextLong(1000L); + BDDMockito.given(signUpVendorService.signUp(BDDMockito.any(SignUpVendorCommand.class))) + .willReturn(fakeVendorId); + + // when + ResultActions actions = mockMvc.perform( + post("/vendors") + .content(new ObjectMapper().writeValueAsString( + new SignUpVendorRequest("", "validEmail@validEmail.com", "validPassword", + "111-1111-0000"))) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + ); + + // then + actions.andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.type").value("about:blank")) + .andExpect(jsonPath("$.title").value("Bad Request")) + .andExpect(jsonPath("$.status").value(400)) + .andExpect(jsonPath("$.instance").value("/vendors")) + .andDo(print()); + } + } + } + + @Test + @DisplayName("[์‹คํŒจ] 400 : ์ด๋ฏธ ๊ฐ€์ž…๋œ ์ด๋ฉ”์ผ์ธ ๊ฒฝ์šฐ") + void failWithDuplicateEmail() throws Exception { + BDDMockito.given(signUpVendorService.signUp(BDDMockito.any(SignUpVendorCommand.class))) + .willThrow(DuplicateEmailException.class); + + // when + ResultActions actions = mockMvc.perform( + post("/vendors") + .content(new ObjectMapper().writeValueAsString( + new SignUpVendorRequest("validName", "validEmail@validEmail.com", "validPassword", + "010-0000-0000"))) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + ); + + // then + actions.andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.type").value("about:blank")) + .andExpect(jsonPath("$.title").value("Bad Request")) + .andExpect(jsonPath("$.status").value(400)) + .andExpect(jsonPath("$.instance").value("/vendors")) + .andDo(print()); + } + } +} 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())); + } +} diff --git a/src/test/java/camp/woowak/lab/web/api/utils/APIUtilsTest.java b/src/test/java/camp/woowak/lab/web/api/utils/APIUtilsTest.java new file mode 100644 index 00000000..a421a8fa --- /dev/null +++ b/src/test/java/camp/woowak/lab/web/api/utils/APIUtilsTest.java @@ -0,0 +1,73 @@ +package camp.woowak.lab.web.api.utils; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import com.fasterxml.jackson.core.JsonProcessingException; + +@DisplayName("APIUtils ํด๋ž˜์Šค") +class APIUtilsTest { + @Nested + @DisplayName("of๋ฉ”์„œ๋“œ์˜") + class OfTest { + @Nested + @DisplayName("HttpStatus ๊ฐ’๊ณผ Data๋ฅผ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋ฐ›๋Š” ๋ฉ”์„œ๋“œ๋Š”") + class ParamWithHttpStatusAndData { + @Test + @DisplayName("data์™€ status๋ฅผ ๊ฐ€์ง€๋Š” APIResponse๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.") + void APIResponseWithHttpStatusAndData() throws JsonProcessingException { + //given + HttpStatus status = HttpStatus.OK; + String message = "hello world"; + + //when + ResponseEntity> apiResponse = APIUtils.of(status, message); + + //then + assertThat(apiResponse.getStatusCode()).isEqualTo(status); + assertThat(apiResponse.getBody().getData()).isEqualTo(message); + assertThat(apiResponse.getBody().getStatus()).isEqualTo(status.value()); + } + + @Test + @DisplayName("data๊ฐ€ ๊ฐ์ฒด์ธ ๊ฒฝ์šฐ๋„ APIResponse๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.") + void APIResponseWithObjectData() throws JsonProcessingException { + //given + HttpStatus status = HttpStatus.OK; + Example example = new Example(27, "Hyeon-Uk"); + + //when + ResponseEntity> apiResponse = APIUtils.of(status, example); + + //then + assertThat(apiResponse.getStatusCode()).isEqualTo(status); + assertThat(apiResponse.getBody().getData()).isEqualTo(example); + assertThat(apiResponse.getBody().getStatus()).isEqualTo(status.value()); + } + + private class Example { + int age; + String name; + + public Example(int age, String name) { + this.age = age; + this.name = name; + } + + public int getAge() { + return age; + } + + public String getName() { + return name; + } + } + } + + } +} \ No newline at end of file diff --git a/src/test/java/camp/woowak/lab/web/resolver/session/SessionCustomerArgumentResolverTest.java b/src/test/java/camp/woowak/lab/web/resolver/session/SessionCustomerArgumentResolverTest.java new file mode 100644 index 00000000..2d9e1590 --- /dev/null +++ b/src/test/java/camp/woowak/lab/web/resolver/session/SessionCustomerArgumentResolverTest.java @@ -0,0 +1,135 @@ +package camp.woowak.lab.web.resolver.session; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.lang.annotation.Annotation; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.core.MethodParameter; +import org.springframework.web.context.request.NativeWebRequest; + +import camp.woowak.lab.common.exception.UnauthorizedException; +import camp.woowak.lab.web.authentication.LoginCustomer; +import camp.woowak.lab.web.authentication.LoginVendor; +import camp.woowak.lab.web.authentication.annotation.AuthenticationPrincipal; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; + +@ExtendWith(MockitoExtension.class) +public class SessionCustomerArgumentResolverTest { + @InjectMocks + private SessionCustomerArgumentResolver resolver; + @Mock + private MethodParameter methodParameter; + @Mock + private NativeWebRequest webRequest; + @Mock + private HttpServletRequest request; + @Mock + private HttpSession session; + @Mock + private LoginCustomer mockCustomer; + + @Nested + @DisplayName("supportsParameter") + class SupportsParameter { + @Test + @DisplayName("[True] ํŒŒ๋ผ๋ฏธํ„ฐ์— @AuthenticationPrincipal์ด ๋ถ™์€ ๊ฒฝ์šฐ") + public void supportsParameter_ReturnsTrue_WhenCorrectAnnotationAndType() { + when(methodParameter.hasParameterAnnotation(AuthenticationPrincipal.class)).thenReturn(true); + when(methodParameter.getParameterType()).thenReturn((Class)LoginCustomer.class); + + boolean supports = resolver.supportsParameter(methodParameter); + + assertThat(supports).isTrue(); + } + + @Test + @DisplayName("[False] ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ LoginCustomer๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ") + public void supportsParameter_ReturnsTrue_WhenIncorrectType() { + when(methodParameter.hasParameterAnnotation(AuthenticationPrincipal.class)).thenReturn(true); + when(methodParameter.getParameterType()).thenReturn((Class)LoginVendor.class); + + boolean supports = resolver.supportsParameter(methodParameter); + + assertThat(supports).isFalse(); + } + + @Test + @DisplayName("[False] ํŒŒ๋ผ๋ฏธํ„ฐ์— @AuthenticationPrincipal์ด ๋ถ™์ง€ ์•Š์€ ๊ฒฝ์šฐ") + public void supportsParameter_ReturnsFalse_WhenIncorrectAnnotationOrType() { + when(methodParameter.hasParameterAnnotation(AuthenticationPrincipal.class)).thenReturn(false); + + boolean supports = resolver.supportsParameter(methodParameter); + + assertThat(supports).isFalse(); + } + } + + @Nested + @DisplayName("resolveArgument") + class ResolveArgument { + @Test + @DisplayName("[LoginCustomer] ์„ธ์…˜์— LoginCustomer๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ") + public void resolveArgument_ReturnsCustomer_WhenSessionExists() throws Exception { + when(webRequest.getNativeRequest()).thenReturn(request); + when(request.getSession(false)).thenReturn(session); + when(session.getAttribute(SessionConst.SESSION_CUSTOMER_KEY)).thenReturn(mockCustomer); + when(methodParameter.getParameterAnnotation(AuthenticationPrincipal.class)).thenReturn( + new AuthenticationPrincipal() { + @Override + public Class annotationType() { + return null; + } + + @Override + public boolean required() { + return true; + } + } + ); + + Object result = resolver.resolveArgument(methodParameter, null, webRequest, null); + + assertThat(result).isEqualTo(mockCustomer); + } + + @Test + @DisplayName("[Null] ํŒŒ๋ผ๋ฏธํ„ฐ์— @AuthenticationPrincipal์ด ๋ถ™์ง€ ์•Š์€ ๊ฒฝ์šฐ") + public void resolveArgument_ReturnsNull_WhenSessionDoesNotExist() throws Exception { + Object result = resolver.resolveArgument(methodParameter, null, webRequest, null); + + assertThat(result).isNull(); + } + + @Test + @DisplayName("[UnauthorizedRequestException] @AuthenticationPrincipal(required=true)์ธ๋ฐ ์„ธ์…˜์ด LoginCustomer๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ") + public void resolveArgument_ThrowsException_WhenSessionRequiredButMissing() { + when(webRequest.getNativeRequest()).thenReturn(request); + when(methodParameter.getParameterAnnotation(AuthenticationPrincipal.class)).thenReturn( + new AuthenticationPrincipal() { + @Override + public boolean required() { + return true; + } + + @Override + public Class annotationType() { + return AuthenticationPrincipal.class; + } + }); + + assertThrows( + UnauthorizedException.class, + () -> resolver.resolveArgument(methodParameter, null, webRequest, null)); + } + } +} diff --git a/src/test/java/camp/woowak/lab/web/resolver/session/SessionVendorArgumentResolverTest.java b/src/test/java/camp/woowak/lab/web/resolver/session/SessionVendorArgumentResolverTest.java new file mode 100644 index 00000000..07ff28c0 --- /dev/null +++ b/src/test/java/camp/woowak/lab/web/resolver/session/SessionVendorArgumentResolverTest.java @@ -0,0 +1,135 @@ +package camp.woowak.lab.web.resolver.session; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.lang.annotation.Annotation; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.core.MethodParameter; +import org.springframework.web.context.request.NativeWebRequest; + +import camp.woowak.lab.common.exception.UnauthorizedException; +import camp.woowak.lab.web.authentication.LoginCustomer; +import camp.woowak.lab.web.authentication.LoginVendor; +import camp.woowak.lab.web.authentication.annotation.AuthenticationPrincipal; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; + +@ExtendWith(MockitoExtension.class) +class SessionVendorArgumentResolverTest { + @InjectMocks + private SessionVendorArgumentResolver resolver; + @Mock + private MethodParameter methodParameter; + @Mock + private NativeWebRequest webRequest; + @Mock + private HttpServletRequest request; + @Mock + private HttpSession session; + @Mock + private LoginVendor mockVendor; + + @Nested + @DisplayName("supportsParameter") + class SupportsParameter { + @Test + @DisplayName("[True] ํŒŒ๋ผ๋ฏธํ„ฐ์— @AuthenticationPrincipal์ด ๋ถ™์€ ๊ฒฝ์šฐ") + public void supportsParameter_ReturnsTrue_WhenCorrectAnnotationAndType() { + when(methodParameter.hasParameterAnnotation(AuthenticationPrincipal.class)).thenReturn(true); + when(methodParameter.getParameterType()).thenReturn((Class)LoginVendor.class); + + boolean supports = resolver.supportsParameter(methodParameter); + + assertThat(supports).isTrue(); + } + + @Test + @DisplayName("[False] ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ LoginVendor๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ") + public void supportsParameter_ReturnsTrue_WhenIncorrectType() { + when(methodParameter.hasParameterAnnotation(AuthenticationPrincipal.class)).thenReturn(true); + when(methodParameter.getParameterType()).thenReturn((Class)LoginCustomer.class); + + boolean supports = resolver.supportsParameter(methodParameter); + + assertThat(supports).isFalse(); + } + + @Test + @DisplayName("[False] ํŒŒ๋ผ๋ฏธํ„ฐ์— @AuthenticationPrincipal์ด ๋ถ™์ง€ ์•Š์€ ๊ฒฝ์šฐ") + public void supportsParameter_ReturnsFalse_WhenIncorrectAnnotationOrType() { + when(methodParameter.hasParameterAnnotation(AuthenticationPrincipal.class)).thenReturn(false); + + boolean supports = resolver.supportsParameter(methodParameter); + + assertThat(supports).isFalse(); + } + } + + @Nested + @DisplayName("resolveArgument") + class ResolveArgument { + @Test + @DisplayName("[LoginVendor] ์„ธ์…˜์— LoginVendor ์žˆ๋Š” ๊ฒฝ์šฐ") + public void resolveArgument_ReturnsVendor_WhenSessionExists() throws Exception { + when(webRequest.getNativeRequest()).thenReturn(request); + when(request.getSession(false)).thenReturn(session); + when(session.getAttribute(SessionConst.SESSION_VENDOR_KEY)).thenReturn(mockVendor); + when(methodParameter.getParameterAnnotation(AuthenticationPrincipal.class)).thenReturn( + new AuthenticationPrincipal() { + @Override + public Class annotationType() { + return null; + } + + @Override + public boolean required() { + return true; + } + } + ); + + Object result = resolver.resolveArgument(methodParameter, null, webRequest, null); + + assertThat(result).isEqualTo(mockVendor); + } + + @Test + @DisplayName("[Null] ํŒŒ๋ผ๋ฏธํ„ฐ์— @AuthenticationPrincipal์ด ๋ถ™์ง€ ์•Š์€ ๊ฒฝ์šฐ") + public void resolveArgument_ReturnsNull_WhenSessionDoesNotExist() throws Exception { + Object result = resolver.resolveArgument(methodParameter, null, webRequest, null); + + assertThat(result).isNull(); + } + + @Test + @DisplayName("[UnauthorizedRequestException] @AuthenticationPrincipal(required=true)์ธ๋ฐ ์„ธ์…˜์ด LoginVendor๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ") + public void resolveArgument_ThrowsException_WhenSessionRequiredButMissing() { + when(webRequest.getNativeRequest()).thenReturn(request); + when(methodParameter.getParameterAnnotation(AuthenticationPrincipal.class)).thenReturn( + new AuthenticationPrincipal() { + @Override + public boolean required() { + return true; + } + + @Override + public Class annotationType() { + return AuthenticationPrincipal.class; + } + }); + + assertThrows( + UnauthorizedException.class, + () -> resolver.resolveArgument(methodParameter, null, webRequest, null)); + } + } +}