diff --git a/src/main/java/com/ampersand/groom/domain/auth/application/port/EmailVerificationPort.java b/src/main/java/com/ampersand/groom/domain/auth/application/port/EmailVerificationPort.java new file mode 100644 index 0000000..a21058a --- /dev/null +++ b/src/main/java/com/ampersand/groom/domain/auth/application/port/EmailVerificationPort.java @@ -0,0 +1,21 @@ +package com.ampersand.groom.domain.auth.application.port; + +import com.ampersand.groom.domain.auth.persistence.EmailVerification; + +import java.time.LocalDateTime; +import java.util.Optional; + +public interface EmailVerificationPort { + + // 인증 정보 저장 + EmailVerification save(EmailVerification emailVerification); + + // 인증 코드로 이메일 조회 + Optional findByCode(String code); + + // 이메일로 인증 정보 조회 + Optional findByEmail(String email); + + // 만료된 인증 정보 삭제 + void deleteAllExpired(LocalDateTime now); +} \ No newline at end of file diff --git a/src/main/java/com/ampersand/groom/domain/auth/application/service/EmailSchedulingService.java b/src/main/java/com/ampersand/groom/domain/auth/application/service/EmailSchedulingService.java new file mode 100644 index 0000000..ceba45a --- /dev/null +++ b/src/main/java/com/ampersand/groom/domain/auth/application/service/EmailSchedulingService.java @@ -0,0 +1,23 @@ +package com.ampersand.groom.domain.auth.application.service; + +import com.ampersand.groom.domain.auth.application.port.EmailVerificationPort; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; + +@Service +@RequiredArgsConstructor +public class EmailSchedulingService { + + private final EmailVerificationPort emailVerificationPort; + + // 만료된 인증 정보 삭제(1시간) + @Scheduled(fixedRate = 3600000) + @Transactional + public void deleteExpiredVerifications() { + emailVerificationPort.deleteAllExpired(LocalDateTime.now()); + } +} diff --git a/src/main/java/com/ampersand/groom/domain/auth/application/service/EmailVerificationService.java b/src/main/java/com/ampersand/groom/domain/auth/application/service/EmailVerificationService.java new file mode 100644 index 0000000..1b6eb73 --- /dev/null +++ b/src/main/java/com/ampersand/groom/domain/auth/application/service/EmailVerificationService.java @@ -0,0 +1,85 @@ +package com.ampersand.groom.domain.auth.application.service; + +import com.ampersand.groom.domain.auth.application.port.EmailVerificationPort; +import com.ampersand.groom.domain.auth.expection.EmailFormatInvalidException; +import com.ampersand.groom.domain.auth.expection.VerificationCodeFormatInvalidException; +import com.ampersand.groom.domain.auth.expection.VerificationCodeExpiredOrInvalidException; +import com.ampersand.groom.domain.auth.persistence.EmailVerification; +import lombok.RequiredArgsConstructor; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.stereotype.Service; + +import java.util.Random; + +@Service +@RequiredArgsConstructor +public class EmailVerificationService { + + private final EmailVerificationPort emailVerificationPort; + private final JavaMailSender javaMailSender; + + private static final int MAX_EMAIL_LENGTH = 16; + private static final int CODE_LENGTH = 8; + + + //8자리 숫자 인증 코드 생성 + private String generateVerificationCode() { + Random random = new Random(); + int code = 10000000 + random.nextInt(90000000); + return String.valueOf(code); + } + + // 이메일 전송 메서드 + private void sendEmail(String to, String subject, String text) { + SimpleMailMessage message = new SimpleMailMessage(); + message.setTo(to); + message.setSubject(subject); + message.setText(text); + javaMailSender.send(message); + } + + // 회원가입 인증 이메일 전송 + public void sendSignupVerificationEmail(String email) { + verifyEmail(email); + String code = generateVerificationCode(); + sendEmail(email, "회원가입 인증", "귀하의 인증 코드는: " + code); + + EmailVerification emailVerification = new EmailVerification(email, code); + + emailVerificationPort.save(emailVerification); + } + + // 비밀번호 변경을 위한 인증 이메일 전송 + public void sendPasswordResetEmail(String email) { + verifyEmail(email); + String code = generateVerificationCode(); + sendEmail(email, "비밀번호 변경 인증", "귀하의 인증 코드는: " + code); + + EmailVerification emailVerification = new EmailVerification(email, code); + + emailVerificationPort.save(emailVerification); + } + + // 인증 코드 검증 + public void verifyCode(String code) { + if(code == null || code.length() != CODE_LENGTH) { + throw new VerificationCodeFormatInvalidException(); + } + + EmailVerification emailVerification = emailVerificationPort.findByCode(code) + .orElseThrow(VerificationCodeExpiredOrInvalidException::new); + + + emailVerification.setIsVerified(true); + emailVerificationPort.save(emailVerification); + } + + // 이메일 검증 + public void verifyEmail(String email) { + if(email == null || email.length() != MAX_EMAIL_LENGTH) { + throw new EmailFormatInvalidException(); + } + + } +} diff --git a/src/main/java/com/ampersand/groom/domain/auth/application/usecase/EmailVerificationUseCase.java b/src/main/java/com/ampersand/groom/domain/auth/application/usecase/EmailVerificationUseCase.java new file mode 100644 index 0000000..4f07dad --- /dev/null +++ b/src/main/java/com/ampersand/groom/domain/auth/application/usecase/EmailVerificationUseCase.java @@ -0,0 +1,29 @@ +package com.ampersand.groom.domain.auth.application.usecase; + +import com.ampersand.groom.domain.auth.application.service.EmailVerificationService; +import com.ampersand.groom.global.annotation.usecase.UseCaseWithTransaction; +import lombok.RequiredArgsConstructor; + +@UseCaseWithTransaction +@RequiredArgsConstructor +public class EmailVerificationUseCase { + + private final EmailVerificationService emailVerificationService; + + + // 회원가입 인증 이메일 전송 + public void executeSendSignupVerificationEmail(String email) { + emailVerificationService.sendSignupVerificationEmail(email); + } + + // 비밀번호 변경 인증 이메일 전송 + public void executeSendPasswordResetEmail(String email) { + emailVerificationService.sendPasswordResetEmail(email); + } + + // 인증 코드 검증 + public void executeVerifyCode(String code) { + emailVerificationService.verifyCode(code); + } + +} diff --git a/src/main/java/com/ampersand/groom/domain/auth/expection/EmailFormatInvalidException.java b/src/main/java/com/ampersand/groom/domain/auth/expection/EmailFormatInvalidException.java new file mode 100644 index 0000000..5f21f37 --- /dev/null +++ b/src/main/java/com/ampersand/groom/domain/auth/expection/EmailFormatInvalidException.java @@ -0,0 +1,10 @@ +package com.ampersand.groom.domain.auth.expection; + +import com.ampersand.groom.global.error.ErrorCode; +import com.ampersand.groom.global.error.exception.GroomException; + +public class EmailFormatInvalidException extends GroomException { + public EmailFormatInvalidException() { + super(ErrorCode.EMAIL_FORMAT_INVALID); + } +} diff --git a/src/main/java/com/ampersand/groom/domain/auth/expection/VerificationCodeExpiredOrInvalidException.java b/src/main/java/com/ampersand/groom/domain/auth/expection/VerificationCodeExpiredOrInvalidException.java new file mode 100644 index 0000000..31f8e7c --- /dev/null +++ b/src/main/java/com/ampersand/groom/domain/auth/expection/VerificationCodeExpiredOrInvalidException.java @@ -0,0 +1,10 @@ +package com.ampersand.groom.domain.auth.expection; + +import com.ampersand.groom.global.error.ErrorCode; +import com.ampersand.groom.global.error.exception.GroomException; + +public class VerificationCodeExpiredOrInvalidException extends GroomException { + public VerificationCodeExpiredOrInvalidException() { + super(ErrorCode.VERIFICATION_CODE_EXPIRED_OR_INVALID); + } +} diff --git a/src/main/java/com/ampersand/groom/domain/auth/expection/VerificationCodeFormatInvalidException.java b/src/main/java/com/ampersand/groom/domain/auth/expection/VerificationCodeFormatInvalidException.java new file mode 100644 index 0000000..1dd0dc4 --- /dev/null +++ b/src/main/java/com/ampersand/groom/domain/auth/expection/VerificationCodeFormatInvalidException.java @@ -0,0 +1,10 @@ +package com.ampersand.groom.domain.auth.expection; + +import com.ampersand.groom.global.error.ErrorCode; +import com.ampersand.groom.global.error.exception.GroomException; + +public class VerificationCodeFormatInvalidException extends GroomException { + public VerificationCodeFormatInvalidException() { + super(ErrorCode.VERIFICATION_CODE_FORMAT_INVALID); + } +} diff --git a/src/main/java/com/ampersand/groom/domain/auth/persistence/EmailVerification.java b/src/main/java/com/ampersand/groom/domain/auth/persistence/EmailVerification.java new file mode 100644 index 0000000..5c565df --- /dev/null +++ b/src/main/java/com/ampersand/groom/domain/auth/persistence/EmailVerification.java @@ -0,0 +1,48 @@ +package com.ampersand.groom.domain.auth.persistence; + +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; + +@Entity +@Getter +@NoArgsConstructor +@Table(name = "email") +@ToString +public class EmailVerification { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @Column(nullable = false) + private String email; + @Column(nullable = false) + private String code; + @Column(nullable = false) + private boolean isVerified; + @Column(nullable = false) + private LocalDateTime verificationDate; + + public EmailVerification(String email, String code) { + this.email = email; + this.code = code; + this.isVerified = false; + this.verificationDate = LocalDateTime.now().plusMinutes(5); + } + + + @Builder + public EmailVerification(Long id, String email, String code, boolean isVerified, LocalDateTime verificationDate) { + this.id = id; + this.email = email; + this.code = code; + this.isVerified = isVerified; + this.verificationDate = verificationDate; + } + + + public void setIsVerified(boolean isVerified) { + this.isVerified = isVerified; + } +} diff --git a/src/main/java/com/ampersand/groom/domain/auth/persistence/adapter/email/EmailVerificationAdapter.java b/src/main/java/com/ampersand/groom/domain/auth/persistence/adapter/email/EmailVerificationAdapter.java new file mode 100644 index 0000000..47a7376 --- /dev/null +++ b/src/main/java/com/ampersand/groom/domain/auth/persistence/adapter/email/EmailVerificationAdapter.java @@ -0,0 +1,38 @@ +package com.ampersand.groom.domain.auth.persistence.adapter.email; + +import com.ampersand.groom.domain.auth.application.port.EmailVerificationPort; + +import com.ampersand.groom.domain.auth.persistence.EmailVerification; +import com.ampersand.groom.domain.auth.persistence.repository.JpaEmailVerificationRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.Optional; + +@Component +@RequiredArgsConstructor +public class EmailVerificationAdapter implements EmailVerificationPort { + + private final JpaEmailVerificationRepository jpaEmailVerificationRepository; + + @Override + public EmailVerification save(EmailVerification emailVerification) { + return jpaEmailVerificationRepository.save(emailVerification); + } + + @Override + public Optional findByCode(String code) { + return jpaEmailVerificationRepository.findByCode(code); + } + + @Override + public Optional findByEmail(String email) { + return jpaEmailVerificationRepository.findByEmail(email); + } + + @Override + public void deleteAllExpired(LocalDateTime now) { + jpaEmailVerificationRepository.deleteAllExpired(now); + } +} diff --git a/src/main/java/com/ampersand/groom/domain/auth/persistence/repository/JpaEmailVerificationRepository.java b/src/main/java/com/ampersand/groom/domain/auth/persistence/repository/JpaEmailVerificationRepository.java new file mode 100644 index 0000000..46a6c90 --- /dev/null +++ b/src/main/java/com/ampersand/groom/domain/auth/persistence/repository/JpaEmailVerificationRepository.java @@ -0,0 +1,33 @@ +package com.ampersand.groom.domain.auth.persistence.repository; + +import com.ampersand.groom.domain.auth.persistence.EmailVerification; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.util.Optional; + +@Repository +@RequiredArgsConstructor +public class JpaEmailVerificationRepository { + + private final SpringDataEmailVerificationRepository repository; + + + public EmailVerification save(EmailVerification emailVerification) { + return repository.save(emailVerification); + } + + public Optional findByCode(String code) { + return repository.findByCode(code); + } + + public Optional findByEmail(String email) { + return repository.findByEmail(email); + } + + public void deleteAllExpired(LocalDateTime now) { + repository.deleteAllExpired(now); + } + +} \ No newline at end of file diff --git a/src/main/java/com/ampersand/groom/domain/auth/persistence/repository/SpringDataEmailVerificationRepository.java b/src/main/java/com/ampersand/groom/domain/auth/persistence/repository/SpringDataEmailVerificationRepository.java new file mode 100644 index 0000000..499fbe7 --- /dev/null +++ b/src/main/java/com/ampersand/groom/domain/auth/persistence/repository/SpringDataEmailVerificationRepository.java @@ -0,0 +1,23 @@ +package com.ampersand.groom.domain.auth.persistence.repository; + +import com.ampersand.groom.domain.auth.persistence.EmailVerification; +import io.lettuce.core.dynamic.annotation.Param; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.util.Optional; + +@Repository +public interface SpringDataEmailVerificationRepository extends JpaRepository { + + Optional findByCode(String code); + + Optional findByEmail(String email); + + @Modifying + @Query("DELETE FROM EmailVerification e WHERE e.verificationDate < :now") + void deleteAllExpired(@Param("now") LocalDateTime now); +} diff --git a/src/main/java/com/ampersand/groom/domain/auth/presentation/controller/AuthController.java b/src/main/java/com/ampersand/groom/domain/auth/presentation/controller/AuthController.java new file mode 100644 index 0000000..0d82131 --- /dev/null +++ b/src/main/java/com/ampersand/groom/domain/auth/presentation/controller/AuthController.java @@ -0,0 +1,40 @@ +package com.ampersand.groom.domain.auth.presentation.controller; + +import com.ampersand.groom.domain.auth.application.usecase.EmailVerificationUseCase; +import com.ampersand.groom.domain.auth.presentation.dto.EmailRequest; +import com.ampersand.groom.domain.auth.presentation.dto.VerificationCodeRequest; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +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.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + + +@RestController +@RequestMapping("/auth") +@RequiredArgsConstructor +public class AuthController { + + private final EmailVerificationUseCase emailVerificationUseCase; + + @PostMapping("/verify-email") + public ResponseEntity verifyEmail(@RequestBody @Valid VerificationCodeRequest request) { + emailVerificationUseCase.executeVerifyCode(request.getCode()); + return ResponseEntity.status(HttpStatus.RESET_CONTENT).body("Verification successful."); + } + + @PostMapping("/signup/email") + public ResponseEntity signup(@RequestBody @Valid EmailRequest request) { + emailVerificationUseCase.executeSendSignupVerificationEmail(request.getEmail()); + return ResponseEntity.status(HttpStatus.RESET_CONTENT).body("Verification email sent"); + } + + @PostMapping("/password-change/email") + public ResponseEntity refresh(@RequestBody @Valid EmailRequest request) { + emailVerificationUseCase.executeSendPasswordResetEmail(request.getEmail()); + return ResponseEntity.status(HttpStatus.RESET_CONTENT).body("Verification email sent"); + } +} \ No newline at end of file diff --git a/src/main/java/com/ampersand/groom/domain/auth/presentation/dto/EmailRequest.java b/src/main/java/com/ampersand/groom/domain/auth/presentation/dto/EmailRequest.java new file mode 100644 index 0000000..eaaf4e7 --- /dev/null +++ b/src/main/java/com/ampersand/groom/domain/auth/presentation/dto/EmailRequest.java @@ -0,0 +1,15 @@ +package com.ampersand.groom.domain.auth.presentation.dto; + +import jakarta.validation.constraints.Email; +import lombok.Getter; + +@Getter +public class EmailRequest { + + @Email(message = "Invalid format") + private final String email; + + public EmailRequest(String email) { + this.email = email; + } +} diff --git a/src/main/java/com/ampersand/groom/domain/auth/presentation/dto/VerificationCodeRequest.java b/src/main/java/com/ampersand/groom/domain/auth/presentation/dto/VerificationCodeRequest.java new file mode 100644 index 0000000..5f4e41e --- /dev/null +++ b/src/main/java/com/ampersand/groom/domain/auth/presentation/dto/VerificationCodeRequest.java @@ -0,0 +1,15 @@ +package com.ampersand.groom.domain.auth.presentation.dto; + +import jakarta.validation.constraints.Size; +import lombok.Getter; + +@Getter +public class VerificationCodeRequest { + + @Size(min = 8, max = 8, message = "Invalid format") + private final String code; + + public VerificationCodeRequest(String code) { + this.code = code; + } +} diff --git a/src/main/java/com/ampersand/groom/global/error/ErrorCode.java b/src/main/java/com/ampersand/groom/global/error/ErrorCode.java index d740343..cc6d639 100644 --- a/src/main/java/com/ampersand/groom/global/error/ErrorCode.java +++ b/src/main/java/com/ampersand/groom/global/error/ErrorCode.java @@ -8,7 +8,10 @@ public enum ErrorCode { MEMBER_ALREADY_EXISTS("Member already exists", 409), - MEMBER_NOT_FOUND("Member not found", 404); + MEMBER_NOT_FOUND("Member not found", 404), + VERIFICATION_CODE_EXPIRED_OR_INVALID("Verification code expired or invalid", 401), + EMAIL_FORMAT_INVALID("Email format invalid", 400), + VERIFICATION_CODE_FORMAT_INVALID("Verification code format invalid", 400); private final String message; private final int httpStatus; diff --git a/src/main/java/com/ampersand/groom/global/thirdParty/EmailConfig.java b/src/main/java/com/ampersand/groom/global/thirdParty/EmailConfig.java new file mode 100644 index 0000000..90ca511 --- /dev/null +++ b/src/main/java/com/ampersand/groom/global/thirdParty/EmailConfig.java @@ -0,0 +1,41 @@ +package com.ampersand.groom.global.thirdParty; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.JavaMailSenderImpl; + +import java.util.Properties; + +@Configuration +public class EmailConfig { + + @Value("${email.host}") + private String host; + + @Value("${email.port}") + private int port; + + @Value("${email.username}") + private String username; + + @Value("${email.password}") + private String password; + + @Bean + public JavaMailSender emailSender() { + JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); + mailSender.setHost(host); + mailSender.setPort(port); + mailSender.setUsername(username); + mailSender.setPassword(password); + + Properties properties = new Properties(); + properties.put("mail.smtp.auth", "true"); + properties.put("mail.smtp.starttls.enable", "true"); + mailSender.setJavaMailProperties(properties); + + return mailSender; + } +} \ No newline at end of file diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml index c6426b6..f6affc9 100644 --- a/src/main/resources/application-test.yml +++ b/src/main/resources/application-test.yml @@ -12,4 +12,15 @@ spring: data: redis: host: localhost - port: 6379 \ No newline at end of file + port: 6379 +email: + host: smtp.example.com + port: 587 + username: test-email@example.com + password: test-password + properties: + mail: + smtp: + auth: true + starttls: + enable: true \ No newline at end of file diff --git a/src/test/java/com/ampersand/groom/domain/auth/application/usecase/EmailVerificationUseCaseTest.java b/src/test/java/com/ampersand/groom/domain/auth/application/usecase/EmailVerificationUseCaseTest.java new file mode 100644 index 0000000..1950ee2 --- /dev/null +++ b/src/test/java/com/ampersand/groom/domain/auth/application/usecase/EmailVerificationUseCaseTest.java @@ -0,0 +1,173 @@ +package com.ampersand.groom.domain.auth.application.usecase; + +import com.ampersand.groom.domain.auth.application.service.EmailVerificationService; +import com.ampersand.groom.domain.auth.expection.EmailFormatInvalidException; +import com.ampersand.groom.domain.auth.expection.VerificationCodeFormatInvalidException; +import com.ampersand.groom.domain.auth.expection.VerificationCodeExpiredOrInvalidException; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@DisplayName("EmailVerificationUseCase 클래스의") +class EmailVerificationUseCaseTest { + + @Mock + private EmailVerificationService emailVerificationService; + + @InjectMocks + private EmailVerificationUseCase emailVerificationUseCase; + + @DisplayName("executeSendSignupVerificationEmail 메서드는") + @Nested + class Describe_executeSendSignupVerificationEmail { + + @Nested + @DisplayName("유효한 이메일을 입력했을 때") + class Context_with_valid_email { + + @Test + @DisplayName("회원가입 인증 이메일을 전송한다.") + void it_sends_signup_verification_email() { + // given + String email = "test@example.com"; + + // when + emailVerificationUseCase.executeSendSignupVerificationEmail(email); + + // then + verify(emailVerificationService).sendSignupVerificationEmail(email); // 메서드 호출 검증 + } + } + + @Nested + @DisplayName("유효하지 않은 이메일을 입력했을 때") + class Context_with_invalid_email { + + @Test + @DisplayName("InvalidFormatException을 발생시킨다.") + void it_throws_invalid_format_exception() { + // given + String invalidEmail = "email"; // 잘못된 이메일 주소 + + + // when + doThrow(new EmailFormatInvalidException()) + .when(emailVerificationService).sendSignupVerificationEmail(invalidEmail); + + + EmailFormatInvalidException exception = assertThrows(EmailFormatInvalidException.class, () -> { + emailVerificationUseCase.executeSendSignupVerificationEmail(invalidEmail); // 이메일 전송 시 예외 발생해야 함 + }); + + // then + assertEquals("Email format invalid", exception.getErrorCode().getMessage()); // 예외 메시지 확인 + assertEquals(400, exception.getErrorCode().getHttpStatus()); // HTTP 상태 코드 확인 + } + + } + + @DisplayName("executeSendPasswordResetEmail 메서드는") + @Nested + class Describe_executeSendPasswordResetEmail { + + @Nested + @DisplayName("유효한 이메일을 입력했을 때") + class Context_with_valid_email { + + @Test + @DisplayName("비밀번호 변경 인증 이메일을 전송한다.") + void it_sends_password_reset_email() { + // given + String email = "s24010@gsm.hs.kr"; + + // when + emailVerificationUseCase.executeSendPasswordResetEmail(email); + + // then + verify(emailVerificationService).sendPasswordResetEmail(email); // 메서드 호출 검증 + } + } + + @Nested + @DisplayName("유효하지 않은 이메일을 입력했을 때") + class Context_with_invalid_email { + + @Test + @DisplayName("InvalidFormatException을 발생시킨다.") + void it_throws_invalid_format_exception() { + // given + String invalidEmail = "email"; // 잘못된 이메일 주소 + + // when + doThrow(new EmailFormatInvalidException()) + .when(emailVerificationService).sendPasswordResetEmail(invalidEmail); + + EmailFormatInvalidException exception = assertThrows(EmailFormatInvalidException.class, () -> { + emailVerificationUseCase.executeSendPasswordResetEmail(invalidEmail); // 이메일 전송 시 예외 발생해야 함 + }); + + // then + assertEquals("Email format invalid", exception.getErrorCode().getMessage()); + assertEquals(400, exception.getErrorCode().getHttpStatus()); + } + + } + } + + @DisplayName("executeVerifyCode 메서드는") + @Nested + class Describe_executeVerifyCode { + + @Nested + @DisplayName("유효하지 않거나 만료된 인증 코드를 입력했을 때") + class Context_with_invalid_or_expired_code { + + @Test + @DisplayName("InvalidOrExpiredCodeException을 발생시킨다.") + void it_throws_invalid_or_expired_code_exception() { + // given + String invalidCode = "12345678"; + + // when + doThrow(new VerificationCodeExpiredOrInvalidException()) + .when(emailVerificationService).verifyCode(invalidCode); // doThrow 사용 + + // then + VerificationCodeExpiredOrInvalidException exception = assertThrows(VerificationCodeExpiredOrInvalidException.class, () -> { + emailVerificationUseCase.executeVerifyCode(invalidCode); + }); + + assertEquals("Verification code expired or invalid", exception.getErrorCode().getMessage()); + assertEquals(401, exception.getErrorCode().getHttpStatus()); + } + } + + @Nested + @DisplayName("올바르지 않은 형식의 코드를 입력했을 때") + class Context_with_invalid_code_format { + + @Test + @DisplayName("InvalidFormatException을 발생시킨다.") + void it_throws_invalid_format_exception() { + String invalidCode = "123456"; + + doThrow(new VerificationCodeFormatInvalidException()) + .when(emailVerificationService).verifyCode(invalidCode); + + VerificationCodeFormatInvalidException exception = assertThrows(VerificationCodeFormatInvalidException.class, () -> { + emailVerificationUseCase.executeVerifyCode(invalidCode); + }); + + assertEquals("Verification code format invalid", exception.getErrorCode().getMessage()); + assertEquals(400, exception.getErrorCode().getHttpStatus()); + } + } + } + } +}