Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat-be: 이메일 인증 후, 회원가입 이메일의 인증 여부 확인 #882

Merged
merged 4 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions backend/src/docs/asciidoc/member.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Original file line number Diff line number Diff line change
@@ -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);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -72,5 +72,6 @@ public void verifyCode(VerifyCodeRequest request) {
String storedVerificationCode = emailRedisClient.getVerificationCode(email);

VerificationCodeUtil.verify(storedVerificationCode, inputVerificationCode);
emailRedisClient.saveVerifiedEmail(email);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<String, String> 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();
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<MemberCreateRequest> InvalidMemberSignUpRequest() {
String validName = "크루루";
String validMail = "mail@mail.com";
Expand All @@ -40,6 +48,7 @@ private static Stream<MemberCreateRequest> 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()
Expand 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)
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 {
Expand All @@ -21,6 +27,9 @@ class MemberFacadeTest extends ServiceTest {
@Autowired
private MemberRepository memberRepository;

@MockBean
private EmailRedisClient emailRedisClient;

@DisplayName("사용자를 생성하면 ID를 반환한다.")
@Test
void create() {
Expand All @@ -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);
Expand All @@ -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);
}
}