diff --git a/src/main/java/camp/woowak/lab/common/advice/GlobalExceptionHandler.java b/src/main/java/camp/woowak/lab/common/advice/GlobalExceptionHandler.java index 69c86318..78dede28 100644 --- a/src/main/java/camp/woowak/lab/common/advice/GlobalExceptionHandler.java +++ b/src/main/java/camp/woowak/lab/common/advice/GlobalExceptionHandler.java @@ -5,12 +5,13 @@ import org.springframework.http.ResponseEntity; 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 { +public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler(Exception.class) public ResponseEntity handleAllUncaughtException(Exception e) { 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..dcdd0fc8 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,18 @@ package camp.woowak.lab.customer.domain; import camp.woowak.lab.payaccount.domain.PayAccount; -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.OneToOne; @Entity 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; + @OneToOne(fetch = FetchType.LAZY) + private PayAccount payAccount; } 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 2f0a6bd7..206c0c4e 100644 --- a/src/main/java/camp/woowak/lab/order/domain/Order.java +++ b/src/main/java/camp/woowak/lab/order/domain/Order.java @@ -2,15 +2,22 @@ import camp.woowak.lab.customer.domain.Customer; 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; +import jakarta.persistence.Table; @Entity +@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; + @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/store/domain/Store.java b/src/main/java/camp/woowak/lab/store/domain/Store.java index 9626c773..d98dfb79 100644 --- a/src/main/java/camp/woowak/lab/store/domain/Store.java +++ b/src/main/java/camp/woowak/lab/store/domain/Store.java @@ -1,13 +1,18 @@ package camp.woowak.lab.store.domain; 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 Store { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - @ManyToOne(fetch = FetchType.LAZY) - private Vendor owner; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @ManyToOne(fetch = FetchType.LAZY) + private Vendor owner; } 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/repository/VendorRepository.java b/src/main/java/camp/woowak/lab/vendor/repository/VendorRepository.java new file mode 100644 index 00000000..8cbf9a34 --- /dev/null +++ b/src/main/java/camp/woowak/lab/vendor/repository/VendorRepository.java @@ -0,0 +1,8 @@ +package camp.woowak.lab.vendor.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import camp.woowak.lab.vendor.domain.Vendor; + +public interface VendorRepository extends JpaRepository { +} 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/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/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/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/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/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/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 index cc1ad9a6..b4956434 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +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/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()); + } + } +}