From 8ca39dedb0707a3b251d6f048dbf1b81280e8b16 Mon Sep 17 00:00:00 2001 From: Kim Hyunsu <38347891+kimhyun5u@users.noreply.github.com> Date: Thu, 15 Aug 2024 15:30:50 +0900 Subject: [PATCH] =?UTF-8?q?[feat]=20=EA=B5=AC=EB=A7=A4=EC=9E=90=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20(#64)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [chore] CustomerFixture 주석 추가 - CustomerFixture 의 기능과 목적을 설명하는 주석 명시 * [test] SignInCustomerServiceTest 추가 - 로그인 성공, 로그인 실패 - 이메일 없음, 패스워드 불일치 추가 * [feat] SignInCustomerService 구현 - SignInCustomerService.signIn 구현 - 이메일 없음과 패스워드 불일치를 같이 처리하면서 보안 강화 * [feat] SignInCustomerCommand 구현 - 구매자 로그인에 필요한 email, password 필드 추가 * [feat] CustomerRepository.findByEmail 추가 - 구매자 로그인 시 구매자 검색에 필요한 findByEmail 구현 * [feat] AuthenticationException - 구매자 로그인 실패 시 인증 오류 구현 * [test] CustomerControllerTest.signIn 추가 - CustomerControllerTest.testSignInCustomer: 구매자 로그인 테스트 성공 - CustomerControllerTest.testSignInCustomerFail: 구매자 로그인 테스트 실패 * [feat] CustomerController.signIn 구현 - 구매자 로그인 성공 시 OK 반환 - 구매자 로그인 실패 시 BadRequest 반환 * [feat] ErrorCode.AUTH_INVALID_CREDENTIALS - 로그인 실패 시 ErrorCode 추가 * [feat] SignInCustomerRequest - 구매자 로그인을 위한 SignInCustomerRequest 이메일, 패스워드 필드 추가 * [feat] CustomerAuthenticationException 구현 - 구매자 로그인 실패 시 예외 추가 * [feat] CustomerErrorCode.AUTHENTICATION_FAILED 구현 - 로그인 실패 시 에러코드 구현 * [feat] SignInCustomerService.signIn 에러 분기 - 이메일이 존재하지 않는 지 비밀번호가 존재하지 않는 지 서버 시점에서 명확히 구분할 수 있도록 수정 * [feat] UnauthorizedException 생성자 추가 - UnauthorizedException(ErroCode, String) 생성자 추가 * [fix] AuthenticationException 수정에 따른 반영 * [docs] SignInCustomerService 예외처리 명시 * [feat] SignInCustomerService.signIn 반환 값 변경 - 로그인 된 구매자의 UUID 를 전달하도록 수정 * [refactor] SignInCustomerRequest 패키지 변경 * [feat] CustomerRepository.findByEmail 반환값 변경 * [feat] Customer 로그인 인증 주체 변경 * [test] CustomerApiController.login 테스트 추가 - CustomerApiControllerTest.testLoginCustomer: 로그인 성공 시 테스트 - CustomerApiControllerTest.testLoginFail: 로그인 실패 시 테스트 * [feat] CustomerApiController.login 추가 - 로그인 인증 후 session 에 LoginCustomer 저장 * [feat] CustomerExceptionHandler.handleCustomerAuthenticationException 추가 - 로그인 실패 시 발생하는 예외 처리 * [feat] SignInCustomerResponse 구현 - CustomerApiController.login 반환값 추가 * [refactor] CustomerErrorCode Status 처리 - 기존 400 로 관리하던 Status 를 HttpStatus 를 이용해 관리하도록 수정 * [fix] SignInCustomerService.signIn 로직 수정 - 비밀번호가 일치하지 않을 때 예외 던지도록 수정 * [fix] CustomerRepository.findByEmail 반환값 변경에 따른 수정 - 반환값을 Optional 로 랩핑 * [refactor] 중복 코드 추출 - ProblemDetail 를 생성하는 코드가 겹쳐서 private 메소드로 추출 --- .../exception/UnauthorizedException.java | 4 + .../woowak/lab/customer/domain/Customer.java | 4 + .../CustomerAuthenticationException.java | 9 ++ .../customer/exception/CustomerErrorCode.java | 13 +-- .../repository/CustomerRepository.java | 3 + .../service/SignInCustomerService.java | 35 ++++++++ .../command/SignInCustomerCommand.java | 7 ++ .../api/customer/CustomerApiController.java | 26 +++++- .../customer/CustomerExceptionHandler.java | 20 +++-- .../customer/SignInCustomerRequest.java | 10 +++ .../customer/SignInCustomerResponse.java | 4 + .../camp/woowak/lab/web/error/ErrorCode.java | 4 +- .../service/SignInCustomerServiceTest.java | 83 +++++++++++++++++++ .../woowak/lab/fixture/CustomerFixture.java | 6 +- .../customer/CustomerApiControllerTest.java | 59 +++++++++++++ 15 files changed, 272 insertions(+), 15 deletions(-) create mode 100644 src/main/java/camp/woowak/lab/customer/exception/CustomerAuthenticationException.java create mode 100644 src/main/java/camp/woowak/lab/customer/service/SignInCustomerService.java create mode 100644 src/main/java/camp/woowak/lab/customer/service/command/SignInCustomerCommand.java create mode 100644 src/main/java/camp/woowak/lab/web/dto/request/customer/SignInCustomerRequest.java create mode 100644 src/main/java/camp/woowak/lab/web/dto/response/customer/SignInCustomerResponse.java create mode 100644 src/test/java/camp/woowak/lab/customer/service/SignInCustomerServiceTest.java diff --git a/src/main/java/camp/woowak/lab/common/exception/UnauthorizedException.java b/src/main/java/camp/woowak/lab/common/exception/UnauthorizedException.java index ba2e1237..b68755c7 100644 --- a/src/main/java/camp/woowak/lab/common/exception/UnauthorizedException.java +++ b/src/main/java/camp/woowak/lab/common/exception/UnauthorizedException.java @@ -4,4 +4,8 @@ public class UnauthorizedException extends HttpStatusException { public UnauthorizedException(ErrorCode errorCode) { super(errorCode); } + + public UnauthorizedException(ErrorCode errorCode, String message) { + super(errorCode, message); + } } 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 4bf75642..0dc706e0 100644 --- a/src/main/java/camp/woowak/lab/customer/domain/Customer.java +++ b/src/main/java/camp/woowak/lab/customer/domain/Customer.java @@ -46,4 +46,8 @@ public Customer(String name, String email, String password, String phone, PayAcc this.phone = phone; this.payAccount = payAccount; } + + public boolean validatePassword(String password, PasswordEncoder passwordEncoder) { + return passwordEncoder.matches(password, this.password); + } } diff --git a/src/main/java/camp/woowak/lab/customer/exception/CustomerAuthenticationException.java b/src/main/java/camp/woowak/lab/customer/exception/CustomerAuthenticationException.java new file mode 100644 index 00000000..9d6edb8c --- /dev/null +++ b/src/main/java/camp/woowak/lab/customer/exception/CustomerAuthenticationException.java @@ -0,0 +1,9 @@ +package camp.woowak.lab.customer.exception; + +import camp.woowak.lab.common.exception.UnauthorizedException; + +public class CustomerAuthenticationException extends UnauthorizedException { + public CustomerAuthenticationException(String message) { + super(CustomerErrorCode.AUTHENTICATION_FAILED, message); + } +} diff --git a/src/main/java/camp/woowak/lab/customer/exception/CustomerErrorCode.java b/src/main/java/camp/woowak/lab/customer/exception/CustomerErrorCode.java index 7ad34f6c..8b575767 100644 --- a/src/main/java/camp/woowak/lab/customer/exception/CustomerErrorCode.java +++ b/src/main/java/camp/woowak/lab/customer/exception/CustomerErrorCode.java @@ -1,16 +1,19 @@ package camp.woowak.lab.customer.exception; +import org.springframework.http.HttpStatus; + import camp.woowak.lab.common.exception.ErrorCode; public enum CustomerErrorCode implements ErrorCode { - INVALID_CREATION(400, "C1", "잘못된 요청입니다."), - DUPLICATE_EMAIL(400, "C2", "이미 존재하는 이메일입니다."); + INVALID_CREATION(HttpStatus.BAD_REQUEST, "C1", "잘못된 요청입니다."), + DUPLICATE_EMAIL(HttpStatus.BAD_REQUEST, "C2", "이미 존재하는 이메일입니다."), + AUTHENTICATION_FAILED(HttpStatus.UNAUTHORIZED, "C3", "이메일 또는 비밀번호가 올바르지 않습니다."); - private final int status; + private final HttpStatus status; private final String errorCode; private final String message; - CustomerErrorCode(int status, String errorCode, String message) { + CustomerErrorCode(HttpStatus status, String errorCode, String message) { this.status = status; this.errorCode = errorCode; this.message = message; @@ -18,7 +21,7 @@ public enum CustomerErrorCode implements ErrorCode { @Override public int getStatus() { - return status; + return status.value(); } @Override diff --git a/src/main/java/camp/woowak/lab/customer/repository/CustomerRepository.java b/src/main/java/camp/woowak/lab/customer/repository/CustomerRepository.java index 32b96ef2..615781b0 100644 --- a/src/main/java/camp/woowak/lab/customer/repository/CustomerRepository.java +++ b/src/main/java/camp/woowak/lab/customer/repository/CustomerRepository.java @@ -1,8 +1,11 @@ package camp.woowak.lab.customer.repository; +import java.util.Optional; + import org.springframework.data.jpa.repository.JpaRepository; import camp.woowak.lab.customer.domain.Customer; public interface CustomerRepository extends JpaRepository { + Optional findByEmail(String email); } diff --git a/src/main/java/camp/woowak/lab/customer/service/SignInCustomerService.java b/src/main/java/camp/woowak/lab/customer/service/SignInCustomerService.java new file mode 100644 index 00000000..82b6f85f --- /dev/null +++ b/src/main/java/camp/woowak/lab/customer/service/SignInCustomerService.java @@ -0,0 +1,35 @@ +package camp.woowak.lab.customer.service; + +import java.util.UUID; + +import org.springframework.stereotype.Service; + +import camp.woowak.lab.customer.domain.Customer; +import camp.woowak.lab.customer.exception.CustomerAuthenticationException; +import camp.woowak.lab.customer.repository.CustomerRepository; +import camp.woowak.lab.customer.service.command.SignInCustomerCommand; +import camp.woowak.lab.web.authentication.PasswordEncoder; + +@Service +public class SignInCustomerService { + private final CustomerRepository customerRepository; + private final PasswordEncoder passwordEncoder; + + public SignInCustomerService(CustomerRepository customerRepository, PasswordEncoder passwordEncoder) { + this.customerRepository = customerRepository; + this.passwordEncoder = passwordEncoder; + } + + /** + * @throws CustomerAuthenticationException 이메일이 존재하지 않거나 비밀번호가 일치하지 않으면 + */ + public UUID signIn(SignInCustomerCommand cmd) { + Customer byEmail = customerRepository.findByEmail(cmd.email()) + .orElseThrow(() -> new CustomerAuthenticationException("email not found")); + if (!byEmail.validatePassword(cmd.password(), passwordEncoder)) { + throw new CustomerAuthenticationException("password not matched"); + } + + return byEmail.getId(); + } +} diff --git a/src/main/java/camp/woowak/lab/customer/service/command/SignInCustomerCommand.java b/src/main/java/camp/woowak/lab/customer/service/command/SignInCustomerCommand.java new file mode 100644 index 00000000..31dde74c --- /dev/null +++ b/src/main/java/camp/woowak/lab/customer/service/command/SignInCustomerCommand.java @@ -0,0 +1,7 @@ +package camp.woowak.lab.customer.service.command; + +public record SignInCustomerCommand( + String email, + String password +) { +} 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 index 4097e2f5..58a9f51b 100644 --- a/src/main/java/camp/woowak/lab/web/api/customer/CustomerApiController.java +++ b/src/main/java/camp/woowak/lab/web/api/customer/CustomerApiController.java @@ -1,24 +1,36 @@ package camp.woowak.lab.web.api.customer; +import java.util.UUID; + import org.springframework.http.HttpStatus; 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.customer.service.SignInCustomerService; import camp.woowak.lab.customer.service.SignUpCustomerService; +import camp.woowak.lab.customer.service.command.SignInCustomerCommand; import camp.woowak.lab.customer.service.command.SignUpCustomerCommand; +import camp.woowak.lab.web.authentication.LoginCustomer; +import camp.woowak.lab.web.dto.request.customer.SignInCustomerRequest; import camp.woowak.lab.web.dto.request.customer.SignUpCustomerRequest; +import camp.woowak.lab.web.dto.response.customer.SignInCustomerResponse; import camp.woowak.lab.web.dto.response.customer.SignUpCustomerResponse; +import camp.woowak.lab.web.resolver.session.SessionConst; import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; import jakarta.validation.Valid; @RestController public class CustomerApiController { private final SignUpCustomerService signUpCustomerService; + private final SignInCustomerService signInCustomerService; - public CustomerApiController(SignUpCustomerService signUpCustomerService) { + public CustomerApiController(SignUpCustomerService signUpCustomerService, + SignInCustomerService signInCustomerService) { this.signUpCustomerService = signUpCustomerService; + this.signInCustomerService = signInCustomerService; } @PostMapping("/customers") @@ -34,4 +46,16 @@ public SignUpCustomerResponse signUp(@Valid @RequestBody SignUpCustomerRequest r return new SignUpCustomerResponse(registeredId); } + + @PostMapping("/customers/login") + @ResponseStatus(HttpStatus.NO_CONTENT) + public SignInCustomerResponse login(@RequestBody SignInCustomerRequest request, HttpSession session) { + SignInCustomerCommand command = new SignInCustomerCommand(request.email(), request.password()); + + UUID customerId = signInCustomerService.signIn(command); + + session.setAttribute(SessionConst.SESSION_CUSTOMER_KEY, new LoginCustomer(customerId)); + + return new SignInCustomerResponse(); + } } 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 index e7fb2482..ecf63b99 100644 --- a/src/main/java/camp/woowak/lab/web/api/customer/CustomerExceptionHandler.java +++ b/src/main/java/camp/woowak/lab/web/api/customer/CustomerExceptionHandler.java @@ -8,6 +8,8 @@ import camp.woowak.lab.common.advice.DomainExceptionHandler; import camp.woowak.lab.common.exception.BadRequestException; +import camp.woowak.lab.common.exception.HttpStatusException; +import camp.woowak.lab.customer.exception.CustomerAuthenticationException; import camp.woowak.lab.customer.exception.DuplicateEmailException; import camp.woowak.lab.customer.exception.InvalidCreationException; import lombok.extern.slf4j.Slf4j; @@ -22,10 +24,7 @@ public class CustomerExceptionHandler extends ResponseEntityExceptionHandler { @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; + return getProblemDetail(e, HttpStatus.BAD_REQUEST); } /** @@ -35,8 +34,17 @@ public ProblemDetail handleBadRequestException(BadRequestException e) { @ExceptionHandler({DuplicateEmailException.class}) @ResponseStatus(HttpStatus.CONFLICT) public ProblemDetail handleDuplicateEmailException(DuplicateEmailException e) { - ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT, - e.errorCode().getMessage()); + return getProblemDetail(e, HttpStatus.CONFLICT); + } + + @ExceptionHandler({CustomerAuthenticationException.class}) + @ResponseStatus(HttpStatus.UNAUTHORIZED) + public ProblemDetail handleCustomerAuthenticationException(CustomerAuthenticationException e) { + return getProblemDetail(e, HttpStatus.UNAUTHORIZED); + } + + private ProblemDetail getProblemDetail(HttpStatusException e, HttpStatus status) { + ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(status, e.errorCode().getMessage()); problemDetail.setProperty("errorCode", e.errorCode().getErrorCode()); return problemDetail; } diff --git a/src/main/java/camp/woowak/lab/web/dto/request/customer/SignInCustomerRequest.java b/src/main/java/camp/woowak/lab/web/dto/request/customer/SignInCustomerRequest.java new file mode 100644 index 00000000..cc12e435 --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/dto/request/customer/SignInCustomerRequest.java @@ -0,0 +1,10 @@ +package camp.woowak.lab.web.dto.request.customer; + +/** + * 이메일 비밀번호 조건을 알 수 없도록 모든 요청을 받을 수 있도록 구현 + */ +public record SignInCustomerRequest( + String email, + String password +) { +} diff --git a/src/main/java/camp/woowak/lab/web/dto/response/customer/SignInCustomerResponse.java b/src/main/java/camp/woowak/lab/web/dto/response/customer/SignInCustomerResponse.java new file mode 100644 index 00000000..934fc442 --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/dto/response/customer/SignInCustomerResponse.java @@ -0,0 +1,4 @@ +package camp.woowak.lab.web.dto.response.customer; + +public record SignInCustomerResponse() { +} diff --git a/src/main/java/camp/woowak/lab/web/error/ErrorCode.java b/src/main/java/camp/woowak/lab/web/error/ErrorCode.java index 53b0e82e..0913ef92 100644 --- a/src/main/java/camp/woowak/lab/web/error/ErrorCode.java +++ b/src/main/java/camp/woowak/lab/web/error/ErrorCode.java @@ -2,8 +2,8 @@ public enum ErrorCode { AUTH_DUPLICATE_EMAIL("a1", "이미 가입된 이메일 입니다."), - SIGNUP_INVALID_REQUEST("s1", "잘못된 요청입니다."); - + SIGNUP_INVALID_REQUEST("s1", "잘못된 요청입니다."), + AUTH_INVALID_CREDENTIALS("a2", "이메일 또는 비밀번호가 올바르지 않습니다."); private final String code; private final String message; diff --git a/src/test/java/camp/woowak/lab/customer/service/SignInCustomerServiceTest.java b/src/test/java/camp/woowak/lab/customer/service/SignInCustomerServiceTest.java new file mode 100644 index 00000000..80e33a26 --- /dev/null +++ b/src/test/java/camp/woowak/lab/customer/service/SignInCustomerServiceTest.java @@ -0,0 +1,83 @@ +package camp.woowak.lab.customer.service; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import java.util.Optional; + +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.junit.jupiter.MockitoExtension; + +import camp.woowak.lab.customer.domain.Customer; +import camp.woowak.lab.customer.exception.CustomerAuthenticationException; +import camp.woowak.lab.customer.repository.CustomerRepository; +import camp.woowak.lab.customer.service.command.SignInCustomerCommand; +import camp.woowak.lab.fixture.CustomerFixture; +import camp.woowak.lab.payaccount.domain.PayAccount; +import camp.woowak.lab.payaccount.domain.TestPayAccount; +import camp.woowak.lab.web.authentication.PasswordEncoder; + +/** + * + */ +@ExtendWith(MockitoExtension.class) +public class SignInCustomerServiceTest implements CustomerFixture { + @InjectMocks + private SignInCustomerService signInCustomerService; + + @Mock + private CustomerRepository customerRepository; + + @Mock + private PasswordEncoder passwordEncoder; + + @Test + @DisplayName("로그인 성공") + void testSignIn() { + // given + PayAccount newPayAccount = new TestPayAccount(1L); + Customer customer = createCustomer(newPayAccount, passwordEncoder); + SignInCustomerCommand cmd = new SignInCustomerCommand(customer.getEmail(), customer.getPassword()); + given(customerRepository.findByEmail(customer.getEmail())).willReturn(Optional.of(customer)); + given(passwordEncoder.matches(cmd.password(), customer.getPassword())).willReturn(true); + + // when & then + assertDoesNotThrow(() -> signInCustomerService.signIn(cmd)); + verify(customerRepository).findByEmail(customer.getEmail()); + verify(passwordEncoder).matches(cmd.password(), customer.getPassword()); + } + + @Test + @DisplayName("로그인 실패 - 이메일 없음") + void testSignInFailEmailNotFound() { + // given + PayAccount newPayAccount = new TestPayAccount(1L); + Customer customer = createCustomer(newPayAccount, passwordEncoder); + SignInCustomerCommand cmd = new SignInCustomerCommand("InvalidCustomer@email.com", customer.getPassword()); + given(customerRepository.findByEmail(cmd.email())).willReturn(Optional.empty()); + + // when & then + assertThrows(CustomerAuthenticationException.class, () -> signInCustomerService.signIn(cmd)); + verify(customerRepository).findByEmail(cmd.email()); + } + + @Test + @DisplayName("로그인 실패 - 패스워드 불일치") + void testSignInFail() { + // given + PayAccount newPayAccount = new TestPayAccount(1L); + Customer customer = createCustomer(newPayAccount, passwordEncoder); + SignInCustomerCommand cmd = new SignInCustomerCommand(customer.getEmail(), customer.getPassword()); + given(customerRepository.findByEmail(customer.getEmail())).willReturn(Optional.of(customer)); + given(passwordEncoder.matches(cmd.password(), customer.getPassword())).willReturn(false); + + // when & then + assertThrows(CustomerAuthenticationException.class, () -> signInCustomerService.signIn(cmd)); + verify(customerRepository).findByEmail(customer.getEmail()); + verify(passwordEncoder).matches(cmd.password(), customer.getPassword()); + } +} diff --git a/src/test/java/camp/woowak/lab/fixture/CustomerFixture.java b/src/test/java/camp/woowak/lab/fixture/CustomerFixture.java index 75c1ac1b..d30b2ea2 100644 --- a/src/test/java/camp/woowak/lab/fixture/CustomerFixture.java +++ b/src/test/java/camp/woowak/lab/fixture/CustomerFixture.java @@ -5,6 +5,9 @@ import camp.woowak.lab.payaccount.domain.PayAccount; import camp.woowak.lab.web.authentication.PasswordEncoder; +/** + * CustomerFixture는 Customer와 관련된 테스트에서 공통적으로 사용되는 객체를 생성하는 인터페이스입니다. + */ public interface CustomerFixture { default PayAccount createPayAccount() { return new PayAccount(); @@ -12,7 +15,8 @@ default PayAccount createPayAccount() { default Customer createCustomer(PayAccount payAccount, PasswordEncoder passwordEncoder) { try { - return new Customer("vendorName", "vendorEmail@example.com", "vendorPassword", "010-0000-0000", payAccount, + return new Customer("customerName", "customerEmail@example.com", "customerPassword", "010-0000-0000", + payAccount, passwordEncoder); } catch (InvalidCreationException e) { throw new RuntimeException(e); diff --git a/src/test/java/camp/woowak/lab/web/api/customer/CustomerApiControllerTest.java b/src/test/java/camp/woowak/lab/web/api/customer/CustomerApiControllerTest.java index d303ac00..f7113717 100644 --- a/src/test/java/camp/woowak/lab/web/api/customer/CustomerApiControllerTest.java +++ b/src/test/java/camp/woowak/lab/web/api/customer/CustomerApiControllerTest.java @@ -3,25 +3,37 @@ 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.MockMvcResultHandlers.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import java.util.UUID; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; 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.customer.exception.CustomerAuthenticationException; import camp.woowak.lab.customer.exception.CustomerErrorCode; import camp.woowak.lab.customer.exception.DuplicateEmailException; +import camp.woowak.lab.customer.service.SignInCustomerService; import camp.woowak.lab.customer.service.SignUpCustomerService; +import camp.woowak.lab.customer.service.command.SignInCustomerCommand; +import camp.woowak.lab.web.authentication.LoginCustomer; +import camp.woowak.lab.web.dto.request.customer.SignInCustomerRequest; import camp.woowak.lab.web.dto.request.customer.SignUpCustomerRequest; +import camp.woowak.lab.web.resolver.session.SessionConst; +import jakarta.servlet.http.HttpSession; @WebMvcTest(CustomerApiController.class) @MockBean(JpaMetamodelMappingContext.class) @@ -33,6 +45,9 @@ class CustomerApiControllerTest { @MockBean private SignUpCustomerService signUpCustomerService; + @MockBean + private SignInCustomerService signInCustomerService; + @Autowired private ObjectMapper objectMapper; @@ -215,4 +230,48 @@ void testSignUpCustomerWithDuplicateEmail() throws Exception { .andExpect(jsonPath("$.errorCode").value(CustomerErrorCode.DUPLICATE_EMAIL.getErrorCode())) .andExpect(jsonPath("$.detail").value(CustomerErrorCode.DUPLICATE_EMAIL.getMessage())); } + + @Test + @DisplayName("구매자 로그인 테스트") + void testLoginCustomer() throws Exception { + UUID fakeCustomerId = UUID.randomUUID(); + BDDMockito.given(signInCustomerService.signIn(BDDMockito.any(SignInCustomerCommand.class))) + .willReturn(fakeCustomerId); + + // when + ResultActions actions = mockMvc.perform( + post("/customers/login") + .content(new ObjectMapper().writeValueAsString( + new SignInCustomerRequest("customer@email.com", "validPassword"))) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + ); + + // then + actions.andExpect(status().isNoContent()) + .andExpect(jsonPath("$.status").value(HttpStatus.NO_CONTENT.value())) + .andExpect(result -> { + HttpSession session = result.getRequest().getSession(); + LoginCustomer loginCustomer = (LoginCustomer)session.getAttribute(SessionConst.SESSION_CUSTOMER_KEY); + Assertions.assertNotNull(loginCustomer); + Assertions.assertEquals(loginCustomer.getId(), fakeCustomerId); + }) + .andDo(print()); + } + + @Test + @DisplayName("구매자 로그인 테스트 - 로그인 실패") + void testLoginFail() throws Exception { + // given + SignInCustomerCommand command = new SignInCustomerCommand("email", "password"); + given(signInCustomerService.signIn(command)).willThrow(new CustomerAuthenticationException("invalid email")); + + // when & then + mockMvc.perform(post("/customers/login") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new SignInCustomerRequest("email", "password")))) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.errorCode").value(CustomerErrorCode.AUTHENTICATION_FAILED.getErrorCode())); + } + }