diff --git a/backend/src/docs/asciidoc/member.adoc b/backend/src/docs/asciidoc/member.adoc index fbd907607..ba0393b3f 100644 --- a/backend/src/docs/asciidoc/member.adoc +++ b/backend/src/docs/asciidoc/member.adoc @@ -9,3 +9,7 @@ operation::member/signup[snippets="http-request,request-fields,http-response"] ==== 실패: 유효하지 않은 요청 operation::member/signup-fail/invalid-request[snippets="http-request,request-fields,http-response"] + +==== 실패: 인증되지 않은 이메일 + +operation::member/signup-fail/not-verified-email[snippets="http-request,request-fields,http-response"] diff --git a/backend/src/main/java/com/cruru/email/exception/NotVerifiedEmailException.java b/backend/src/main/java/com/cruru/email/exception/NotVerifiedEmailException.java new file mode 100644 index 000000000..bad5e1b96 --- /dev/null +++ b/backend/src/main/java/com/cruru/email/exception/NotVerifiedEmailException.java @@ -0,0 +1,13 @@ +package com.cruru.email.exception; + +import com.cruru.advice.UnauthorizedException; + +public class NotVerifiedEmailException extends UnauthorizedException { + + private static final String MESSAGE = "이메일 인증이 필요합니다."; + + public NotVerifiedEmailException() { + super(MESSAGE); + } +} + diff --git a/backend/src/main/java/com/cruru/email/facade/EmailFacade.java b/backend/src/main/java/com/cruru/email/facade/EmailFacade.java index e51cdb4d0..3c00bfbee 100644 --- a/backend/src/main/java/com/cruru/email/facade/EmailFacade.java +++ b/backend/src/main/java/com/cruru/email/facade/EmailFacade.java @@ -69,5 +69,6 @@ public void verifyCode(VerifyCodeRequest request) { String storedVerificationCode = emailRedisClient.getVerificationCode(email); VerificationCodeUtil.verify(storedVerificationCode, inputVerificationCode); + emailRedisClient.saveVerifiedEmail(email); } } diff --git a/backend/src/main/java/com/cruru/email/service/EmailRedisClient.java b/backend/src/main/java/com/cruru/email/service/EmailRedisClient.java index 70115bdf1..b0eb2a7fd 100644 --- a/backend/src/main/java/com/cruru/email/service/EmailRedisClient.java +++ b/backend/src/main/java/com/cruru/email/service/EmailRedisClient.java @@ -1,5 +1,6 @@ package com.cruru.email.service; +import com.cruru.email.exception.NotVerifiedEmailException; import java.util.concurrent.TimeUnit; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.RedisTemplate; @@ -9,17 +10,46 @@ @RequiredArgsConstructor public class EmailRedisClient { - private static final String REDIS_PREFIX = "email_verification:"; - private static final long VERIFICATION_CODE_EXPIRATION = 10; + private static final String VERIFICATION_CODE_PREFIX = "email_verification_code:"; + private static final String VERIFIED_EMAIL_PREFIX = "email_verified:"; + private static final int VERIFICATION_CODE_EXPIRATION = 10; + private static final int VERIFIED_EMAIL_EXPIRATION = 10; private final RedisTemplate redisTemplate; public void saveVerificationCode(String email, String verificationCode) { redisTemplate.opsForValue() - .set(REDIS_PREFIX + email, verificationCode, VERIFICATION_CODE_EXPIRATION, TimeUnit.MINUTES); + .set( + VERIFICATION_CODE_PREFIX + email, + verificationCode, + VERIFICATION_CODE_EXPIRATION, + TimeUnit.MINUTES + ); } public String getVerificationCode(String email) { - return redisTemplate.opsForValue().get(REDIS_PREFIX + email); + return redisTemplate.opsForValue().get(VERIFICATION_CODE_PREFIX + email); + } + + public void saveVerifiedEmail(String email) { + redisTemplate.opsForValue() + .set( + VERIFIED_EMAIL_PREFIX + email, + VerificationStatus.VERIFIED.getValue(), + VERIFIED_EMAIL_EXPIRATION, + TimeUnit.MINUTES + ); + } + + public void verifyEmail(String email) { + String verifiedStatus = redisTemplate.opsForValue().get(VERIFIED_EMAIL_PREFIX + email); + VerificationStatus status = VerificationStatus.fromValue(verifiedStatus); + checkVerification(status); + } + + private void checkVerification(VerificationStatus status) { + if (!status.isVerified()) { + throw new NotVerifiedEmailException(); + } } } diff --git a/backend/src/main/java/com/cruru/email/service/VerificationStatus.java b/backend/src/main/java/com/cruru/email/service/VerificationStatus.java new file mode 100644 index 000000000..73fcec8f1 --- /dev/null +++ b/backend/src/main/java/com/cruru/email/service/VerificationStatus.java @@ -0,0 +1,29 @@ +package com.cruru.email.service; + +import java.util.Arrays; +import lombok.Getter; + +@Getter +public enum VerificationStatus { + + VERIFIED("verified"), + NOT_VERIFIED("not_verified"), + ; + + private final String value; + + VerificationStatus(String value) { + this.value = value; + } + + public static VerificationStatus fromValue(String value) { + return Arrays.stream(VerificationStatus.values()) + .filter(status -> status.getValue().equalsIgnoreCase(value)) + .findFirst() + .orElse(NOT_VERIFIED); + } + + public boolean isVerified() { + return this == VERIFIED; + } +} diff --git a/backend/src/main/java/com/cruru/member/facade/MemberFacade.java b/backend/src/main/java/com/cruru/member/facade/MemberFacade.java index e0f02a956..248fa416a 100644 --- a/backend/src/main/java/com/cruru/member/facade/MemberFacade.java +++ b/backend/src/main/java/com/cruru/member/facade/MemberFacade.java @@ -1,6 +1,7 @@ package com.cruru.member.facade; import com.cruru.club.service.ClubService; +import com.cruru.email.service.EmailRedisClient; import com.cruru.member.controller.request.MemberCreateRequest; import com.cruru.member.domain.Member; import com.cruru.member.service.MemberService; @@ -15,9 +16,11 @@ public class MemberFacade { private final MemberService memberService; private final ClubService clubService; + private final EmailRedisClient emailRedisClient; @Transactional public long create(MemberCreateRequest request) { + emailRedisClient.verifyEmail(request.email()); Member savedMember = memberService.create(request); clubService.create(request.clubName(), savedMember); return savedMember.getId(); diff --git a/backend/src/test/java/com/cruru/member/controller/MemberControllerTest.java b/backend/src/test/java/com/cruru/member/controller/MemberControllerTest.java index 712190a00..60627962a 100644 --- a/backend/src/test/java/com/cruru/member/controller/MemberControllerTest.java +++ b/backend/src/test/java/com/cruru/member/controller/MemberControllerTest.java @@ -1,9 +1,13 @@ package com.cruru.member.controller; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; +import com.cruru.email.exception.NotVerifiedEmailException; +import com.cruru.email.service.EmailRedisClient; import com.cruru.member.controller.request.MemberCreateRequest; import com.cruru.util.ControllerTest; import io.restassured.RestAssured; @@ -13,10 +17,14 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.boot.test.mock.mockito.MockBean; @DisplayName("사용자 컨트롤러 테스트") class MemberControllerTest extends ControllerTest { + @MockBean + private EmailRedisClient emailRedisClient; + private static Stream InvalidMemberSignUpRequest() { String validName = "크루루"; String validMail = "mail@mail.com"; @@ -40,6 +48,7 @@ private static Stream InvalidMemberSignUpRequest() { void create() { // given MemberCreateRequest request = new MemberCreateRequest("크루루", "mail@mail.com", "newPassword214!", "01012341234"); + doNothing().when(emailRedisClient).verifyEmail(request.email()); // when&then RestAssured.given(spec).log().all() @@ -61,7 +70,10 @@ void create() { @ParameterizedTest @MethodSource("InvalidMemberSignUpRequest") void create_invalidEmail(MemberCreateRequest request) { - // given&when&then + // given + doNothing().when(emailRedisClient).verifyEmail(request.email()); + + //when&then RestAssured.given(spec).log().all() .contentType(ContentType.JSON) .body(request) @@ -76,4 +88,27 @@ void create_invalidEmail(MemberCreateRequest request) { .when().post("/v1/members/signup") .then().log().all().statusCode(400); } + + @DisplayName("인증되지 않은 사용자가 회원가입할 경우, 401을 반환한다.") + @Test + void create_notVerifiedEmail() { + // given + MemberCreateRequest request = new MemberCreateRequest("크루루", "mail@mail.com", "newPassword214!", "01012341234"); + doThrow(NotVerifiedEmailException.class).when(emailRedisClient).verifyEmail(request.email()); + + // when&then + RestAssured.given(spec).log().all() + .contentType(ContentType.JSON) + .body(request) + .filter(document("member/signup-fail/not-verified-email", + requestFields( + fieldWithPath("clubName").description("동아리명"), + fieldWithPath("email").description("인증되지 않은 사용자 이메일"), + fieldWithPath("password").description("사용자 패스워드"), + fieldWithPath("phone").description("사용자 전화번호") + ) + )) + .when().post("/v1/members/signup") + .then().log().all().statusCode(401); + } } diff --git a/backend/src/test/java/com/cruru/member/facade/MemberFacadeTest.java b/backend/src/test/java/com/cruru/member/facade/MemberFacadeTest.java index aa00e698d..b472db0df 100644 --- a/backend/src/test/java/com/cruru/member/facade/MemberFacadeTest.java +++ b/backend/src/test/java/com/cruru/member/facade/MemberFacadeTest.java @@ -1,8 +1,13 @@ package com.cruru.member.facade; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import com.cruru.email.exception.NotVerifiedEmailException; +import com.cruru.email.service.EmailRedisClient; import com.cruru.member.controller.request.MemberCreateRequest; import com.cruru.member.domain.Member; import com.cruru.member.domain.repository.MemberRepository; @@ -11,6 +16,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; @DisplayName("회원 파사드 서비스 테스트") class MemberFacadeTest extends ServiceTest { @@ -21,6 +27,9 @@ class MemberFacadeTest extends ServiceTest { @Autowired private MemberRepository memberRepository; + @MockBean + private EmailRedisClient emailRedisClient; + @DisplayName("사용자를 생성하면 ID를 반환한다.") @Test void create() { @@ -30,6 +39,7 @@ void create() { String password = "newPassword214!"; String phone = "01012341234"; MemberCreateRequest request = new MemberCreateRequest(clubName, email, password, phone); + doNothing().when(emailRedisClient).verifyEmail(email); // when long memberId = memberFacade.create(request); @@ -42,4 +52,20 @@ void create() { () -> assertThat(member.get().getPhone()).isEqualTo(phone) ); } + + @DisplayName("인증되지 않은 사용자로 사용자를 생성하면, 예외가 발생한다.") + @Test + void create_notVerifiedEmail() { + // given + String clubName = "크루루"; + String email = "new@mail.com"; + String password = "newPassword214!"; + String phone = "01012341234"; + MemberCreateRequest request = new MemberCreateRequest(clubName, email, password, phone); + doThrow(NotVerifiedEmailException.class).when(emailRedisClient).verifyEmail(email); + + // when&then + assertThatThrownBy(() -> memberFacade.create(request)) + .isInstanceOf(NotVerifiedEmailException.class); + } }