Refactor ConfirmRegisterAccountProcessor, API

- 기존 TokenVerifier에 토큰 검증과 회원 인증책임을 분리
- ConfirmRegisterAccountProcessor를 추가
- verify 메서드가 void를 반환하도록 변경
- 테스트에서 MailTokenVerifierStub 제거
This commit is contained in:
JiwonDev
2021-09-05 03:37:23 +09:00
parent 7cdc352d84
commit 207c54120c
15 changed files with 188 additions and 130 deletions

View File

@@ -1,9 +1,10 @@
package com.yam.app.account.application;
import com.yam.app.account.domain.ConfirmRegisterAccountProcessor;
import com.yam.app.account.domain.RegisterAccountEvent;
import com.yam.app.account.domain.RegisterAccountProcessor;
import com.yam.app.account.domain.TokenVerifier;
import com.yam.app.account.presentation.AccountResponse;
import com.yam.app.account.presentation.ConfirmRegisterAccountRequest;
import com.yam.app.account.presentation.RegisterAccountRequest;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
@@ -15,15 +16,15 @@ public class AccountFacade {
private final RegisterAccountProcessor processor;
private final AccountTranslator translator;
private final ApplicationEventPublisher publisher;
private final TokenVerifier tokenVerifier;
private final ConfirmRegisterAccountProcessor confirmRegisterProcessor;
public AccountFacade(RegisterAccountProcessor processor,
AccountTranslator translator, ApplicationEventPublisher publisher,
TokenVerifier tokenVerifier) {
ConfirmRegisterAccountProcessor confirmRegisterProcessor) {
this.processor = processor;
this.translator = translator;
this.publisher = publisher;
this.tokenVerifier = tokenVerifier;
this.confirmRegisterProcessor = confirmRegisterProcessor;
}
@Transactional
@@ -33,7 +34,7 @@ public class AccountFacade {
return translator.toResponse(entity);
}
public boolean verify(String token, String email) {
return tokenVerifier.verify(token, email);
public void registerConfirm(ConfirmRegisterAccountRequest request) {
confirmRegisterProcessor.registerConfirm(translator.toCommand(request));
}
}

View File

@@ -2,6 +2,7 @@ package com.yam.app.account.application;
import com.yam.app.account.domain.Account;
import com.yam.app.account.presentation.AccountResponse;
import com.yam.app.account.presentation.ConfirmRegisterAccountRequest;
import com.yam.app.account.presentation.RegisterAccountRequest;
import org.springframework.stereotype.Component;
@@ -13,6 +14,10 @@ final class AccountTranslator {
request.getPassword());
}
public ConfirmRegisterAccountCommand toCommand(ConfirmRegisterAccountRequest request) {
return new ConfirmRegisterAccountCommand(request.getToken(), request.getEmail());
}
public AccountResponse toResponse(Account entity) {
return new AccountResponse(entity.getId(), entity.getEmail(),
entity.getNickname());

View File

@@ -0,0 +1,15 @@
package com.yam.app.account.application;
import lombok.Getter;
@Getter
public class ConfirmRegisterAccountCommand {
private final String token;
private final String email;
public ConfirmRegisterAccountCommand(String token, String email) {
this.token = token;
this.email = email;
}
}

View File

@@ -0,0 +1,24 @@
package com.yam.app.account.domain;
import com.yam.app.account.application.ConfirmRegisterAccountCommand;
public final class ConfirmRegisterAccountProcessor {
private final AccountReader accountReader;
private final AccountRepository accountRepository;
private final TokenVerifier tokenVerifier;
public ConfirmRegisterAccountProcessor(AccountReader accountReader,
AccountRepository accountRepository, TokenVerifier tokenVerifier) {
this.accountReader = accountReader;
this.accountRepository = accountRepository;
this.tokenVerifier = tokenVerifier;
}
public void registerConfirm(ConfirmRegisterAccountCommand command) {
tokenVerifier.verify(command.getToken(), command.getEmail());
Account account = accountReader.findByEmail(command.getEmail());
account.completeRegister();
accountRepository.update(account);
}
}

View File

@@ -1,23 +1,14 @@
package com.yam.app.account.domain;
import com.yam.app.common.StringUtils;
public final class TokenVerifier {
private final AccountReader accountReader;
private final AccountRepository accountRepository;
public TokenVerifier(AccountReader accountReader,
AccountRepository accountRepository) {
public TokenVerifier(AccountReader accountReader) {
this.accountReader = accountReader;
this.accountRepository = accountRepository;
}
public boolean verify(String token, String email) {
if (StringUtils.isBlank(token) || StringUtils.isBlank(email)) {
throw new IllegalArgumentException();
}
public void verify(String token, String email) {
var account = accountReader.findByEmail(email);
if (account == null) {
@@ -28,9 +19,5 @@ public final class TokenVerifier {
throw new IllegalStateException();
}
account.completeRegister();
accountRepository.update(account);
return true;
}
}

View File

@@ -2,6 +2,7 @@ package com.yam.app.account.infrastructure;
import com.yam.app.account.domain.AccountReader;
import com.yam.app.account.domain.AccountRepository;
import com.yam.app.account.domain.ConfirmRegisterAccountProcessor;
import com.yam.app.account.domain.PasswordEncrypter;
import com.yam.app.account.domain.RegisterAccountProcessor;
import com.yam.app.account.domain.TokenVerifier;
@@ -49,8 +50,14 @@ public class AppConfiguration {
}
@Bean
public TokenVerifier tokenVerifier(AccountReader accountReader,
AccountRepository accountRepository) {
return new TokenVerifier(accountReader, accountRepository);
public TokenVerifier tokenVerifier(AccountReader accountReader) {
return new TokenVerifier(accountReader);
}
@Bean
public ConfirmRegisterAccountProcessor confirmRegisterAccountProcessor(
AccountReader accountReader, AccountRepository accountRepository,
TokenVerifier tokenVerifier) {
return new ConfirmRegisterAccountProcessor(accountReader, accountRepository, tokenVerifier);
}
}

View File

@@ -8,10 +8,10 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@@ -35,16 +35,18 @@ public final class AccountCommandApi {
/**
* 회원가입 이메일 검증 컨트롤러
* 임시로 "http://localhost:3000/login"로 리다이렉트 되도록 설정.
* - 임시로 "http://localhost:3000/login"로 리다이렉트 되도록 설정.
* - 임시로 검증에 실패해서 예외가 발생하면 400 HTTP 을 반환하도록 설정.
*/
@GetMapping("/api/accounts/authorize")
public ResponseEntity<Void> verify(
@RequestParam String token,
@RequestParam String email) throws Exception {
var result = accountFacade.verify(token, email);
if (!result) {
public ResponseEntity<Void> registerConfirm(
@ModelAttribute ConfirmRegisterAccountRequest request) throws Exception {
try {
accountFacade.registerConfirm(request);
} catch (Exception e) {
return ResponseEntity.badRequest().build();
}
var uri = new URI("http://localhost:3000/login");
var header = new HttpHeaders();
header.setLocation(uri);

View File

@@ -0,0 +1,16 @@
package com.yam.app.account.presentation;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import lombok.Data;
@Data
public final class ConfirmRegisterAccountRequest {
@NotBlank
private String token;
@Email
@NotBlank
private String email;
}

View File

@@ -1,17 +0,0 @@
package com.yam.app.common;
public final class StringUtils {
private StringUtils() {
}
/**
* 입력값이 null 또는 공백이라면 true 반환.
*/
public static boolean isBlank(String value) {
if (value == null || value.isEmpty()) {
return true;
}
return value.trim().isEmpty();
}
}

View File

@@ -12,7 +12,6 @@
password = #{password},
withdraw = #{withdraw}
WHERE id = #{id}
LIMIT 1
</update>
<select id="existsByEmail" parameterType="String" resultType="int">

View File

@@ -0,0 +1,72 @@
package com.yam.app.account.domain;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import com.yam.app.account.application.ConfirmRegisterAccountCommand;
import java.util.Arrays;
import java.util.Collection;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
@DisplayName("회원 검증 도메인 서비스")
class ConfirmRegisterAccountProcessorTest {
@TestFactory
@DisplayName("이메일 검증 시나리오")
Collection<DynamicTest> verify_account_scenarios() {
var accountRepository = new FakeAccountRepository();
var accountReader = accountRepository;
var tokenVerifier = new TokenVerifier(accountReader);
var confirmRegisterAccountProcessor = new ConfirmRegisterAccountProcessor(accountReader,
accountRepository, tokenVerifier);
var account = accountRepository.save(
Account.of("jiwonDev@gmail.com", "jiwon", "password!"));
return Arrays.asList(
DynamicTest.dynamicTest("회원 이메일 토큰검증에 성공한다.", () -> {
//Arrange
var command = new ConfirmRegisterAccountCommand(account.getEmailCheckToken(),
account.getEmail());
// Act
confirmRegisterAccountProcessor.registerConfirm(command);
Account updatedAccount = accountRepository.findByEmail(account.getEmail());
// Assert
assertThat(updatedAccount.isEmailVerified()).isTrue();
}),
DynamicTest.dynamicTest("이메일이나 토큰의 값이 null인 경우 예외를 리턴한다.",
() -> {
//Arrange
var command = new ConfirmRegisterAccountCommand(null, null);
// Act & Assert
assertThatExceptionOfType(NullPointerException.class)
.isThrownBy(() -> confirmRegisterAccountProcessor.registerConfirm(command));
}),
DynamicTest.dynamicTest("이메일 검증에 실패하여 예외를 리턴한다.",
() -> {
//Arrange
var command = new ConfirmRegisterAccountCommand(account.getEmailCheckToken(),
"HiIamNotExistEmail@naver.com");
// Act & Assert
assertThatExceptionOfType(IllegalStateException.class)
.isThrownBy(() -> confirmRegisterAccountProcessor.registerConfirm(command));
}),
DynamicTest.dynamicTest("토큰 검증에 실패하여 예외를 리턴한다.",
() -> {
//Arrange
var command = new ConfirmRegisterAccountCommand("안녕난가짜토큰이라고해",
account.getEmail());
// Act & Assert
assertThatExceptionOfType(IllegalStateException.class)
.isThrownBy(() -> confirmRegisterAccountProcessor.registerConfirm(command));
})
);
}
}

View File

@@ -3,9 +3,7 @@ package com.yam.app.account.domain;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.stereotype.Component;
@Component
public final class FakeAccountRepository implements AccountRepository, AccountReader {
private final Map<Long, Account> data = new ConcurrentHashMap<>();

View File

@@ -1,77 +1,21 @@
package com.yam.app.account.domain;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.NullAndEmptySource;
@DisplayName("검증 도메인 서비스")
@DisplayName("토큰 검증 도메인 테스트")
class TokenVerifierTest {
@Test
@DisplayName("이메일과 토큰값을 입력받고 검증에 성공하여 회원의 이메일 검증 상태를 갱신하고, true를 반환한다.")
void verify_success() {
@DisplayName("입력된 이메일과 토큰값을 예외를 발생시키지 않고 올바르게 검증하는지 테스트한다.")
void verify_email_and_token_correctly() throws Exception {
//Arrange
var accountRepository = new FakeAccountRepository();
var accountReader = accountRepository;
var tokenVerifier = new TokenVerifier(accountReader, accountRepository);
var tokenVerifier = new TokenVerifier(accountRepository);
var account = accountRepository.save(
Account.of("jiwonDev@gmail.com", "jiwon", "password!"));
Account.of("jijiwon@gmail.com", "jiwon", "password!"));
//Act
boolean result = tokenVerifier.verify(account.getEmailCheckToken(),
account.getEmail());
var updatedAccount = accountReader.findByEmail(account.getEmail());
//Assert
assertThat(result).isTrue();
assertThat(updatedAccount.isEmailVerified()).isTrue();
}
@ParameterizedTest
@NullAndEmptySource
@DisplayName("이메일과 토큰의 값이 비어있거나 null인 경우 IllegalArgumentException이 발생한다.")
void param_is_empty_or_null(String arg) {
// Arrange
var accountRepository = new FakeAccountRepository();
var accountReader = new FakeAccountRepository();
var tokenVerifier = new TokenVerifier(accountReader, accountRepository);
// Act & Assert
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> tokenVerifier.verify(arg, arg));
}
@Test
@DisplayName("유효하지 않은 이메일의 경우 IllegalStateException이 발생한다.")
void email_is_not_valid() {
// Arrange
var accountRepository = new FakeAccountRepository();
var accountReader = new FakeAccountRepository();
var tokenVerifier = new TokenVerifier(accountReader, accountRepository);
// Act & Assert
assertThatExceptionOfType(IllegalStateException.class)
.isThrownBy(() -> tokenVerifier.verify("abcdefgh", "abcdefgh@naver.com"));
}
@Test
@DisplayName("유효하지 않은 토큰인 경우 IllegalStateException이 발생한다.")
void token_is_not_valid() {
// Arrange
var accountRepository = new FakeAccountRepository();
var accountReader = new FakeAccountRepository();
var tokenVerifier = new TokenVerifier(accountReader, accountRepository);
accountRepository.save(Account.of("jiwonDev@gmail.com", "jiwon", "password!"));
// Act & Assert
assertThatExceptionOfType(IllegalStateException.class)
.isThrownBy(() -> tokenVerifier.verify("asdhji", "jiwondev@naver.com"));
//Act & Assert
tokenVerifier.verify(account.getEmailCheckToken(), account.getEmail());
}
}

View File

@@ -60,7 +60,7 @@ class MybatisAccountRepositoryTest {
Account account = accountRepository.save(
Account.of("jiwon22@gmail.com", "jiwon2", "password!"));
account.completeRegister(); // boolean emailVerified = true
account.completeRegister();
Account updatedAccount = accountRepository.update(account);
assertThat(updatedAccount.isEmailVerified()).isTrue();

View File

@@ -1,6 +1,6 @@
package com.yam.app.account.presentation;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@@ -35,21 +35,23 @@ class AccountCommandApiTests {
@Nested
@DisplayName("이메일 검증 HTTP API")
class EmailVerifiedApi {
class RegisterConfirmApi {
@ParameterizedTest
@NullAndEmptySource
@DisplayName("비어있거나 null인 토큰과 이메일 정보로 검증 요청을 보낸 경우 400 HTTP Code 리턴한다.")
@DisplayName("HTTP 파라메타가 비었거나 null인 검증요청을 보낸 경우 400 HTTP Code 리턴한다.")
void http_param_is_empty_or_null(String args) throws Exception {
// Arrange
// Act
when(accountFacade.verify(args, args)).thenReturn(false);
var request = new ConfirmRegisterAccountRequest();
request.setToken(args);
request.setEmail(args);
doThrow(IllegalStateException.class).when(accountFacade).registerConfirm(request);
final var actions = mockMvc.perform(get("/api/accounts/authorize")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.param("token", "emailTOken")
.param("email", "jiwonDev@gmail.com")
.param("token", args)
.param("email", args)
);
// Assert
@@ -59,17 +61,19 @@ class AccountCommandApiTests {
}
@Test
@DisplayName("유효하지 않은 토큰과 이메일 정보로 검증 요청을 보낸 경우 400 HTTP Code 리턴한다.")
@DisplayName("HTTP 파라메타가 유효하지 않은 값으로 검증요청을 보낸 경우 400 HTTP Code 리턴한다.")
void http_param_is_not_valid() throws Exception {
// Arrange
// Act
when(accountFacade.verify(anyString(), anyString())).thenReturn(false);
var request = new ConfirmRegisterAccountRequest();
request.setToken("QWEIUHQWDU");
request.setEmail("QWEIOWQJE@naver.com");
doThrow(IllegalStateException.class).when(accountFacade).registerConfirm(request);
final var actions = mockMvc.perform(get("/api/accounts/authorize")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.param("token", "emailTOken")
.param("email", "jiwonDev@gmail.com")
.param("token", "QWEIUHQWDU")
.param("email", "QWEIOWQJE@naver.com")
);
// Assert
@@ -79,11 +83,12 @@ class AccountCommandApiTests {
}
@Test
@DisplayName("토큰과 이메일 정보로 검증 요청을 보낸 경우 303 HTTP Code 리턴한다.")
@DisplayName("토큰과 이메일 정보로 검증요청을 보낸 경우 303 HTTP Code 리턴한다.")
void valid_success() throws Exception {
// Arrange
// Act
when(accountFacade.verify(anyString(), anyString())).thenReturn(true);
var request = new ConfirmRegisterAccountRequest();
request.setToken("emailTOken");
request.setEmail("jiwonDev@gmail.com");
final var actions = mockMvc.perform(get("/api/accounts/authorize")
.accept(MediaType.APPLICATION_JSON)