Add classes to handle exceptions that occur in the Presentation layer

This commit is contained in:
Rebwon
2021-09-20 21:01:58 +09:00
committed by MaengSol
parent 8cb9d52d8b
commit f20b50601b
22 changed files with 260 additions and 73 deletions

View File

@@ -1,5 +1,6 @@
package com.yam.app.account.application;
import com.yam.app.account.domain.AccountNotFoundException;
import com.yam.app.account.domain.AccountReader;
import com.yam.app.account.domain.ConfirmRegisterAccountProcessor;
import com.yam.app.account.domain.LoginAccountProcessor;
@@ -58,7 +59,7 @@ public class AccountFacade {
@Transactional(readOnly = true)
public AccountResponse findInfo(String email) {
return translator.toResponse(accountReader.findByEmail(email)
.orElseThrow(IllegalArgumentException::new));
.orElseThrow(() -> new AccountNotFoundException(email)));
}
}

View File

@@ -0,0 +1,10 @@
package com.yam.app.account.domain;
import com.yam.app.common.EntityNotFoundException;
public final class AccountNotFoundException extends EntityNotFoundException {
public AccountNotFoundException(String email) {
super("Account could not be found, (email : %s)", email);
}
}

View File

@@ -16,7 +16,7 @@ public final class ConfirmRegisterAccountProcessor {
public void registerConfirm(String token, String email) {
tokenVerifier.verify(token, email);
var account = accountReader.findByEmail(email)
.orElseThrow(IllegalArgumentException::new);
.orElseThrow(() -> new AccountNotFoundException(email));
account.completeRegister();
accountRepository.update(account);
}

View File

@@ -1,5 +1,7 @@
package com.yam.app.account.domain;
import com.yam.app.common.DuplicateValueException;
public final class RegisterAccountProcessor {
private final AccountRepository accountRepository;
@@ -15,16 +17,16 @@ public final class RegisterAccountProcessor {
public Account process(String email, String nickname, String password) {
if (accountReader.existsByEmail(email)) {
throw new IllegalStateException();
throw new DuplicateValueException(email);
}
if (accountReader.existsByNickname(nickname)) {
throw new IllegalStateException();
throw new DuplicateValueException(nickname);
}
String encodedPassword = passwordEncrypter.encode(password);
accountRepository.save(Account.of(email, nickname, encodedPassword));
return accountReader.findByEmail(email)
.orElseThrow(IllegalArgumentException::new);
.orElseThrow(() -> new AccountNotFoundException(email));
}
}

View File

@@ -10,11 +10,10 @@ public final class TokenVerifier {
public void verify(String token, String email) {
var account = accountReader.findByEmail(email)
.orElseThrow(IllegalArgumentException::new);
.orElseThrow(() -> new AccountNotFoundException(email));
if (!account.isValidToken(token)) {
throw new IllegalStateException();
throw new IllegalStateException("Invalid token");
}
}
}

View File

@@ -1,5 +1,6 @@
package com.yam.app.account.infrastructure;
import com.yam.app.common.UnauthorizedRequestException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.springframework.core.MethodParameter;
@@ -22,11 +23,11 @@ public final class LoginAccountMethodArgumentResolver implements HandlerMethodAr
.getSession(false);
if (session == null) {
return null;
throw new UnauthorizedRequestException("Unauthorized request");
}
var sessionManager = new SessionManager(session);
return sessionManager.fetchPrincipal()
.orElseThrow(IllegalArgumentException::new);
.orElseThrow(() -> new UnauthorizedRequestException("Failed fetch principal"));
}
}

View File

@@ -1,5 +1,6 @@
package com.yam.app.account.infrastructure;
import com.yam.app.account.domain.AccountNotFoundException;
import com.yam.app.account.domain.AccountReader;
import com.yam.app.account.domain.LoginAccountProcessor;
import com.yam.app.account.domain.PasswordEncrypter;
@@ -21,14 +22,14 @@ public final class SessionBasedLoginAccountProcessor implements LoginAccountProc
@Override
public void login(String email, String password) {
var account = accountReader.findByEmail(email)
.orElseThrow(IllegalArgumentException::new);
.orElseThrow(() -> new AccountNotFoundException(email));
if (!account.isEmailVerified()) {
throw new IllegalStateException();
throw new IllegalStateException("Email not verified");
}
if (!passwordEncrypter.matches(password, account.getPassword())) {
throw new IllegalStateException();
throw new IllegalStateException("Password mismatched");
}
sessionManager.setPrincipal(new AccountPrincipal(email));

View File

@@ -42,11 +42,7 @@ public final class AccountCommandApi {
@GetMapping("/api/accounts/authorize")
public ResponseEntity<Void> registerConfirm(
@ModelAttribute @Valid ConfirmRegisterAccountCommand command) throws Exception {
try {
accountFacade.registerConfirm(command);
} catch (Exception e) {
return ResponseEntity.badRequest().build();
}
accountFacade.registerConfirm(command);
var uri = new URI("http://localhost:3000/login");
var header = new HttpHeaders();
@@ -57,12 +53,7 @@ public final class AccountCommandApi {
@PostMapping("/api/accounts/login")
public ResponseEntity<Void> login(
@RequestBody @Valid LoginAccountCommand request) {
try {
accountFacade.login(request);
} catch (IllegalStateException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
accountFacade.login(request);
return ResponseEntity.ok().build();
}
}

View File

@@ -3,7 +3,6 @@ package com.yam.app.account.presentation;
import com.yam.app.account.application.AccountFacade;
import com.yam.app.account.infrastructure.AccountPrincipal;
import com.yam.app.account.infrastructure.LoginAccount;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
@@ -26,16 +25,7 @@ public final class AccountQueryApi {
@GetMapping("/api/accounts/me")
public ResponseEntity<AccountResponse> findInfo(
@LoginAccount AccountPrincipal accountPrincipal) {
if (accountPrincipal == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
try {
return ResponseEntity.ok(accountFacade.findInfo(
accountPrincipal.getEmail()));
} catch (Exception e) {
return ResponseEntity.badRequest().build();
}
return ResponseEntity.ok(accountFacade.findInfo(accountPrincipal.getEmail()));
}
}

View File

@@ -14,6 +14,7 @@ public final class LoginAccountCommand {
@NotBlank
@Pattern(regexp = "^[A-Za-z1-9~!@#$%^&*()+|=]{8,12}$",
message = "비밀번호는 영어와 숫자, 특수문자로 8~12자리 이내로 입력해주세요.")
message = "Please enter the password in English, numbers, "
+ "and special characters within 8-12 digits.")
private String password;
}

View File

@@ -17,7 +17,8 @@ public final class RegisterAccountCommand {
@NotBlank
@Pattern(regexp = "^[A-Za-z1-9~!@#$%^&*()+|=]{8,12}$",
message = "비밀번호는 영어와 숫자, 특수문자로 8~12자리 이내로 입력해주세요.")
message = "Please enter the password in English, numbers, "
+ "and special characters within 8-12 digits.")
private String password;
}

View File

@@ -0,0 +1,25 @@
package com.yam.app.common;
import lombok.Getter;
@Getter
public final class ApiResult<T> {
private final boolean success;
private final T data;
private final String message;
private ApiResult(boolean success, T data, String message) {
this.success = success;
this.data = data;
this.message = message;
}
public static <T> ApiResult<T> success(T data) {
return new ApiResult<>(true, data, null);
}
public static ApiResult<?> error(String message) {
return new ApiResult<>(false, null, message);
}
}

View File

@@ -0,0 +1,15 @@
package com.yam.app.common;
import org.springframework.http.HttpStatus;
public final class DuplicateValueException extends SystemException {
public DuplicateValueException(String value) {
super("These are duplicated value, (value : %s)", value);
}
@Override
public HttpStatus getStatus() {
return HttpStatus.CONFLICT;
}
}

View File

@@ -0,0 +1,14 @@
package com.yam.app.common;
import org.springframework.http.HttpStatus;
public class EntityNotFoundException extends SystemException {
public EntityNotFoundException(String format, Object... args) {
super(format, args);
}
public HttpStatus getStatus() {
return HttpStatus.NOT_FOUND;
}
}

View File

@@ -0,0 +1,89 @@
package com.yam.app.common;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@Slf4j
@ControllerAdvice
public final class GlobalApiExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
private ResponseEntity<ApiResult<?>> handleMethod(MethodArgumentNotValidException e) {
log.error("MethodArgumentNotValidException", e);
return ResponseEntity
.badRequest()
.body(ApiResult.error("Invalid argument"));
}
@ExceptionHandler(BindException.class)
private ResponseEntity<ApiResult<?>> handleBind(BindException e) {
log.error("BindException", e);
return ResponseEntity
.badRequest()
.body(ApiResult.error("Invalid argument"));
}
@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
private ResponseEntity<ApiResult<?>> handleNotSupported(HttpMediaTypeNotSupportedException e) {
log.error("HttpMediaTypeNotSupportedException", e);
return ResponseEntity
.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
.body(ApiResult.error("Http media type not supported"));
}
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
private ResponseEntity<ApiResult<?>> handleNotSupported(
HttpRequestMethodNotSupportedException e) {
log.error("HttpRequestMethodNotSupportedException", e);
return ResponseEntity
.status(HttpStatus.METHOD_NOT_ALLOWED)
.body(ApiResult.error("Http request method not supported"));
}
@ExceptionHandler(Exception.class)
private ResponseEntity<ApiResult<?>> handleException(Exception e) {
log.error("Exception", e);
return ResponseEntity
.internalServerError()
.body(ApiResult.error("Internal server error"));
}
@ExceptionHandler(EntityNotFoundException.class)
private ResponseEntity<ApiResult<?>> handleNotFound(EntityNotFoundException e) {
log.error("EntityNotFoundException", e);
return ResponseEntity
.status(e.getStatus())
.body(ApiResult.error("Not found resources"));
}
@ExceptionHandler(DuplicateValueException.class)
private ResponseEntity<ApiResult<?>> handleDuplicate(DuplicateValueException e) {
log.error("DuplicateValueException", e);
return ResponseEntity
.status(e.getStatus())
.body(ApiResult.error("Duplicated value"));
}
@ExceptionHandler(IllegalStateException.class)
private ResponseEntity<ApiResult<?>> handleIllegalState(IllegalStateException e) {
log.error("IllegalStateException", e);
return ResponseEntity
.badRequest()
.body(ApiResult.error(e.getMessage()));
}
@ExceptionHandler(UnauthorizedRequestException.class)
private ResponseEntity<ApiResult<?>> handleUnauthorized(UnauthorizedRequestException e) {
log.error("UnauthorizedRequestException", e);
return ResponseEntity
.status(e.getStatus())
.body(ApiResult.error(e.getMessage()));
}
}

View File

@@ -0,0 +1,12 @@
package com.yam.app.common;
import org.springframework.http.HttpStatus;
public abstract class SystemException extends RuntimeException {
public SystemException(String format, Object... args) {
super(String.format(format, args));
}
public abstract HttpStatus getStatus();
}

View File

@@ -0,0 +1,15 @@
package com.yam.app.common;
import org.springframework.http.HttpStatus;
public final class UnauthorizedRequestException extends SystemException {
public UnauthorizedRequestException(String message) {
super(message);
}
@Override
public HttpStatus getStatus() {
return HttpStatus.UNAUTHORIZED;
}
}

View File

@@ -46,7 +46,7 @@ final class ConfirmRegisterAccountProcessorTest {
DynamicTest.dynamicTest("이메일 검증에 실패하여 예외를 리턴한다.",
() -> {
// Act & Assert
assertThatExceptionOfType(IllegalArgumentException.class)
assertThatExceptionOfType(AccountNotFoundException.class)
.isThrownBy(() -> confirmRegisterAccountProcessor.registerConfirm(
account.getEmailCheckToken(),
"HiIamNotExistEmail@naver.com"));

View File

@@ -3,6 +3,7 @@ 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.common.DuplicateValueException;
import java.util.Arrays;
import java.util.Collection;
import org.junit.jupiter.api.DisplayName;
@@ -30,12 +31,12 @@ final class RegisterAccountProcessorTest {
}),
DynamicTest.dynamicTest("이메일 검증에 실패하여 예외를 리턴한다.", () -> {
// Act & Assert
assertThatExceptionOfType(IllegalStateException.class)
assertThatExceptionOfType(DuplicateValueException.class)
.isThrownBy(() -> processor.process("rebwon@gmail.com", "rebwon", "password!"));
}),
DynamicTest.dynamicTest("닉네임 검증에 실패하여 예외를 리턴한다.", () -> {
// Act & Assert
assertThatExceptionOfType(IllegalStateException.class)
assertThatExceptionOfType(DuplicateValueException.class)
.isThrownBy(() -> processor.process("kitty@gmail.com", "rebwon", "password!"));
})
);

View File

@@ -5,6 +5,7 @@ import static org.assertj.core.api.Assertions.catchThrowable;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
import com.yam.app.account.domain.Account;
import com.yam.app.account.domain.AccountNotFoundException;
import com.yam.app.account.domain.FakeAccountRepository;
import com.yam.app.account.domain.PasswordEncrypterStub;
import java.util.Arrays;
@@ -53,7 +54,7 @@ final class SessionBasedLoginAccountProcessorTest {
);
// Assert
assertThat(throwable).isInstanceOf(IllegalArgumentException.class);
assertThat(throwable).isInstanceOf(AccountNotFoundException.class);
}),
dynamicTest("이메일은 유효하나 검증을 완료하지 않은 경우 예외를 리턴한다.", () -> {
// Act

View File

@@ -7,6 +7,7 @@ import static org.mockito.Mockito.doThrow;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -23,6 +24,7 @@ import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
@DisplayName("Account Command HTTP API")
@WebMvcTest(AccountCommandApi.class)
@@ -40,7 +42,7 @@ final class AccountCommandApiTests {
class LoginApi {
@Test
@DisplayName("이메일, 비밀번호 형식은 유효하나 로그인이 실패한 경우 401 에러를 반환한다.")
@DisplayName("이메일, 비밀번호 형식은 유효하나 로그인이 실패한 경우 400 에러를 반환한다.")
void login_fail() throws Exception {
// Arrange
var request = new LoginAccountCommand();
@@ -59,7 +61,7 @@ final class AccountCommandApiTests {
// Assert
actions
.andDo(print())
.andExpect(status().isUnauthorized());
.andExpect(status().isBadRequest());
}
@ParameterizedTest
@@ -79,9 +81,7 @@ final class AccountCommandApiTests {
);
// Assert
actions
.andDo(print())
.andExpect(status().isBadRequest());
assertThatInvalidArgumentError(actions);
}
@ParameterizedTest
@@ -101,9 +101,7 @@ final class AccountCommandApiTests {
);
// Assert
actions
.andDo(print())
.andExpect(status().isBadRequest());
assertThatInvalidArgumentError(actions);
}
@ParameterizedTest
@@ -123,9 +121,7 @@ final class AccountCommandApiTests {
);
//Assert
actions
.andDo(print())
.andExpect(status().isBadRequest());
assertThatInvalidArgumentError(actions);
}
}
@@ -155,9 +151,7 @@ final class AccountCommandApiTests {
);
// Assert
actions
.andDo(print())
.andExpect(status().isBadRequest());
assertThatInvalidArgumentError(actions);
}
@ParameterizedTest
@@ -170,8 +164,6 @@ final class AccountCommandApiTests {
command.setEmail(arg);
// Act
doThrow(IllegalStateException.class).when(accountFacade).registerConfirm(command);
final var actions = mockMvc.perform(get(EMAIL_CONFIRM)
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
@@ -180,9 +172,7 @@ final class AccountCommandApiTests {
);
// Assert
actions
.andDo(print())
.andExpect(status().isBadRequest());
assertThatInvalidArgumentError(actions);
}
}
@@ -208,7 +198,10 @@ final class AccountCommandApiTests {
// Assert
actions
.andExpect(status().isUnsupportedMediaType());
.andExpect(status().isUnsupportedMediaType())
.andExpect(jsonPath("$.success").value(false))
.andExpect(jsonPath("$.data").doesNotExist())
.andExpect(jsonPath("$.message").value("Http media type not supported"));
}
@ParameterizedTest
@@ -229,9 +222,7 @@ final class AccountCommandApiTests {
);
// Assert
actions
.andDo(print())
.andExpect(status().isBadRequest());
assertThatInvalidArgumentError(actions);
}
@ParameterizedTest
@@ -252,9 +243,7 @@ final class AccountCommandApiTests {
);
// Assert
actions
.andDo(print())
.andExpect(status().isBadRequest());
assertThatInvalidArgumentError(actions);
}
@ParameterizedTest
@@ -275,11 +264,17 @@ final class AccountCommandApiTests {
);
// Assert
actions
.andDo(print())
.andExpect(status().isBadRequest());
assertThatInvalidArgumentError(actions);
}
}
private void assertThatInvalidArgumentError(ResultActions actions) throws Exception {
actions
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.success").value(false))
.andExpect(jsonPath("$.data").doesNotExist())
.andExpect(jsonPath("$.message").value("Invalid argument"));
}
}

View File

@@ -2,7 +2,7 @@ package com.yam.app.account.presentation;
import static com.yam.app.account.presentation.AccountApiUri.FIND_INFO;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import com.yam.app.account.application.AccountFacade;
@@ -13,6 +13,7 @@ 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.http.MediaType;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.test.web.servlet.MockMvc;
@DisplayName("Account Query HTTP API")
@@ -38,8 +39,30 @@ class AccountQueryApiTest {
//Assert
actions
.andDo(print())
.andExpect(status().isUnauthorized());
.andExpect(status().isUnauthorized())
.andExpect(jsonPath("$.success").value(false))
.andExpect(jsonPath("$.data").doesNotExist())
.andExpect(jsonPath("$.message").value("Unauthorized request"));
}
@Test
@DisplayName("잘못된 인증 정보로 요청을 보낸 경우 401 응답을 반환한다.")
void failed_fetch_session_principal() throws Exception {
//Act
var session = new MockHttpSession();
final var actions = mockMvc.perform(get(FIND_INFO)
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.session(session)
);
//Assert
actions
.andExpect(status().isUnauthorized())
.andExpect(jsonPath("$.success").value(false))
.andExpect(jsonPath("$.data").doesNotExist())
.andExpect(jsonPath("$.message").value("Failed fetch principal"));
}
}
}