From f4f75762152bb9428dd6a5615726a8563afdfde1 Mon Sep 17 00:00:00 2001
From: chokyungho
+ * 비즈니스 규칙 위반 시 발생하는 에러를 정의합니다.
+ * 예약 중복, 불가능한 시간대, 이미 취소된 예약 등 도메인 로직 관련 에러를 담당합니다.
+ *
+ * 도메인 로직 상 허용되지 않는 값이나 상태인 경우
+ *
+ * 도메인 레이어에서 비즈니스 규칙 위반 시 발생하는 모든 예외의 기본 클래스입니다.
+ * ResultCode를 포함하여 일관된 응답 형식을 제공합니다.
+ *
+ * API 요청/응답 처리 과정에서 발생하는 에러를 정의합니다.
+ * 잘못된 요청 형식, 존재하지 않는 엔드포인트, 인증/인가 실패 등 HTTP 레벨의 에러를 담당합니다.
+ *
+ * 요청에 인증 헤더가 없거나 비어있는 경우
+ *
+ * 인증 헤더의 권한 값이 유효하지 않은 경우
+ *
+ * 인증은 되었으나 해당 리소스에 접근할 권한이 없는 경우
+ *
+ * Passport 서명 검증에 실패했거나 파싱할 수 없는 경우
+ *
+ * Passport의 유효기간이 만료된 경우
+ *
+ * 프레젠테이션 레이어에서 발생하는 모든 예외의 기본 클래스입니다.
+ * API 요청 형식 오류, 인증/인가 실패 등의 예외를 포함합니다.
+ * ResultCode를 포함하여 일관된 응답 형식을 제공합니다.
+ *
+ * 모든 레이어에서 공통으로 사용하는 결과 코드 인터페이스입니다.
+ * 도메인 에러와 프레젠테이션 에러 모두 이 인터페이스를 구현합니다.
+ *
+ * 결과 코드 형식:
+ *
+ *
+ *
+ * ResponseEntity의 상태 코드로 사용됩니다. + *
+ */ + HttpStatusCode getHttpStatus(); + + /** + * 비즈니스 상태 코드를 반환합니다. + *+ * 응답 body에 포함되어 더 세밀한 상태를 나타냅니다. + *
+ */ + String getCode(); + + /** + * 상태에 대한 설명 메시지를 반환합니다. + */ + String getMessage(); +} From b7ba3ca0d5d0f303ec0216480bbf57092b27a503 Mon Sep 17 00:00:00 2001 From: chokyungho+ * 특정 어노테이션이 붙은 컨트롤러 메서드에 대해 권한을 검증합니다. 하위 클래스는 검증할 어노테이션 타입과 허용할 권한 목록을 지정하여 구체적인 인터셉터를 구현합니다. + *
+ * + * @param 검증 대상 어노테이션 타입 + */ +public abstract class AbstractAuthInterceptor implements + HandlerInterceptor { + + /** + * 검증할 어노테이션의 타입 + */ + private final Class targetAnnotationType; + + /** + * 접근을 허용할 권한 목록 + */ + private final List+ * 다음 순서로 검증을 수행합니다: + *
+ * 현재 사용자의 권한이 허용된 권한 목록({@link #allowedAuthorities})에 포함되는지 확인합니다. 게이트웨이에서 발급한 서명된 Passport + * 헤더(X-Passport)를 파싱하여 사용자 권한을 확인합니다. + *
+ * + * @param request HTTP 요청 객체 + * @throws PresentationException 인증 정보가 없거나 유효하지 않은 경우 (401), 접근 권한이 없는 경우 (403) + */ + protected void validateUserAuthority(HttpServletRequest request) { + String signedPassport = request.getHeader(PassportHandler.PASSPORT_KEY); + + // 1. Passport 헤더가 없는 경우 예외 발생 + if (signedPassport == null || signedPassport.isBlank()) { + throw new PresentationException(PresentationErrorCode.UNAUTHORIZED); + } + + // 2. 서명된 Passport를 파싱 및 검증 + // 3. 사용자 권한이 허용 목록에 포함되는지 확인 + try { + Passport passport = passportHandler.parse(signedPassport); + + if (!allowedAuthorities.contains(passport.authority())) { + throw new PresentationException(PresentationErrorCode.FORBIDDEN); + } + } catch (PassportParsingException | InvalidPassportException e) { + throw new PresentationException(PresentationErrorCode.INVALID_PASSPORT); + } catch (ExpiredPassportException e) { + throw new PresentationException(PresentationErrorCode.EXPIRED_PASSPORT); + } + } +} diff --git a/reservation/src/main/java/net/catsnap/CatsnapReservation/shared/presentation/web/interceptor/AdminInterceptor.java b/reservation/src/main/java/net/catsnap/CatsnapReservation/shared/presentation/web/interceptor/AdminInterceptor.java new file mode 100644 index 00000000..ffbfe8a1 --- /dev/null +++ b/reservation/src/main/java/net/catsnap/CatsnapReservation/shared/presentation/web/interceptor/AdminInterceptor.java @@ -0,0 +1,44 @@ +package net.catsnap.CatsnapReservation.shared.presentation.web.interceptor; + +import java.util.List; +import net.catsnap.CatsnapReservation.shared.presentation.web.interceptor.AbstractAuthInterceptor; +import net.catsnap.shared.auth.Admin; +import net.catsnap.shared.auth.CatsnapAuthority; +import net.catsnap.shared.passport.domain.PassportHandler; +import org.springframework.stereotype.Component; + +/** + * 관리자 권한 검증 인터셉터입니다. + *+ * {@link Admin} 어노테이션이 붙은 컨트롤러 메서드에 대해 ADMIN 권한을 검증합니다. 게이트웨이에서 발급한 Passport 헤더(X-Passport)의 권한 정보를 + * 확인하여 접근을 제어합니다. + *
+ * + *{@code
+ * @RestController
+ * public class UserController {
+ *
+ * @Admin // ADMIN 권한만 접근 가능
+ * @DeleteMapping("/users/{id}")
+ * public void deleteUser(@PathVariable Long id) {
+ * // 관리자만 사용자 삭제 가능
+ * }
+ * }
+ * }
+ *
+ * @see Admin
+ * @see AbstractAuthInterceptor
+ */
+@Component
+public class AdminInterceptor extends AbstractAuthInterceptor+ * {@link AnyUser} 어노테이션이 붙은 컨트롤러 메서드에 대해 모든 권한의 접근을 허용합니다. 익명 사용자를 포함한 모든 사용자가 접근할 수 있지만, 반드시 인증 + * 헤더는 필요합니다. + *
+ * + *{@code
+ * @RestController
+ * public class PhotoController {
+ *
+ * @AnyUser // 모든 인증된 사용자 접근 가능
+ * @GetMapping("/photos/{id}")
+ * public PhotoResponse getPhoto(@PathVariable Long id) {
+ * // 익명 사용자 포함 모든 사용자가 사진 조회 가능
+ * }
+ * }
+ * }
+ *
+ * @see AnyUser
+ * @see AbstractAuthInterceptor
+ */
+@Component
+public class AnyUserInterceptor extends AbstractAuthInterceptor+ * {@link LoginModel} 어노테이션이 붙은 컨트롤러 메서드에 대해 모델 권한을 검증합니다. 모델(MODEL)과 관리자(ADMIN)만 접근할 수 있습니다. + *
+ * + *{@code
+ * @RestController
+ * public class BookingController {
+ *
+ * @LoginModel // 모델과 관리자만 접근 가능
+ * @GetMapping("/bookings/my")
+ * public List getMyBookings() {
+ * // 모델이 자신의 촬영 예약 목록 조회
+ * }
+ * }
+ * }
+ *
+ * @see LoginModel
+ * @see AbstractAuthInterceptor
+ */
+@Component
+public class LoginModelInterceptor extends AbstractAuthInterceptor+ * {@link LoginPhotographer} 어노테이션이 붙은 컨트롤러 메서드에 대해 사진작가 권한을 검증합니다. 사진작가(PHOTOGRAPHER)와 관리자(ADMIN)만 + * 접근할 수 있습니다. + *
+ * + *{@code
+ * @RestController
+ * public class PortfolioController {
+ *
+ * @LoginPhotographer // 사진작가와 관리자만 접근 가능
+ * @PostMapping("/portfolios")
+ * public PortfolioResponse createPortfolio(@RequestBody PortfolioRequest request) {
+ * // 사진작가가 포트폴리오 생성
+ * }
+ * }
+ * }
+ *
+ * @see LoginPhotographer
+ * @see AbstractAuthInterceptor
+ */
+@Component
+public class LoginPhotographerInterceptor
+ extends AbstractAuthInterceptor+ * {@link LoginUser} 어노테이션이 붙은 컨트롤러 메서드에 대해 로그인한 사용자만 접근을 허용합니다. 익명(ANONYMOUS) 사용자는 접근할 수 없으며, 실제 + * 계정을 가진 사용자만 접근 가능합니다. + *
+ * + *{@code
+ * @RestController
+ * public class ProfileController {
+ *
+ * @LoginUser // 로그인한 사용자만 접근 가능
+ * @GetMapping("/profile")
+ * public ProfileResponse getMyProfile() {
+ * // 모델, 사진작가, 관리자 모두 자신의 프로필 조회 가능
+ * }
+ * }
+ * }
+ *
+ * @see LoginUser
+ * @see AbstractAuthInterceptor
+ */
+@Component
+public class LoginUserInterceptor extends AbstractAuthInterceptor+ * MockMvc 테스트에서 서명된 Passport 헤더를 편리하게 설정할 수 있도록 도와줍니다. + * 게이트웨이에서 발급하는 서명된 Passport 헤더(X-Passport)를 시뮬레이션합니다. + *
+ * + *{@code
+ * @Autowired
+ * private PassportTestHelper passportTestHelper;
+ *
+ * // 관리자 권한으로 요청
+ * mockMvc.perform(passportTestHelper.withAdmin(get("/api/users")))
+ * .andExpect(status().isOk());
+ *
+ * // 사진작가 권한으로 요청
+ * mockMvc.perform(passportTestHelper.withPhotographer(post("/api/portfolios"), 100L)
+ * .content(requestBody))
+ * .andExpect(status().isCreated());
+ *
+ * // 커스텀 권한으로 요청
+ * mockMvc.perform(passportTestHelper.withAuthority(get("/api/profile"), 1L, CatsnapAuthority.MODEL))
+ * .andExpect(status().isOk());
+ * }
+ */
+@TestComponent
+public class PassportTestHelper {
+
+ private final PassportHandler passportHandler;
+
+ private static final String PASSPORT_HEADER = PassportHandler.PASSPORT_KEY;
+ private static final Long DEFAULT_ADMIN_USER_ID = 1L;
+ private static final Long DEFAULT_ANONYMOUS_USER_ID = -1L;
+ private static final int DEFAULT_EXPIRATION_MINUTES = 30;
+ private static final byte DEFAULT_PASSPORT_VERSION = 1;
+
+ @Autowired
+ public PassportTestHelper(PassportHandler passportHandler) {
+ this.passportHandler = passportHandler;
+ }
+
+ /**
+ * 관리자(ADMIN) 권한으로 요청을 설정합니다.
+ */
+ public MockHttpServletRequestBuilder withAdmin(MockHttpServletRequestBuilder builder) {
+ return withAuthority(builder, DEFAULT_ADMIN_USER_ID, CatsnapAuthority.ADMIN);
+ }
+
+ /**
+ * 관리자(ADMIN) 권한으로 요청을 설정합니다.
+ */
+ public MockHttpServletRequestBuilder withAdmin(MockHttpServletRequestBuilder builder,
+ Long userId) {
+ return withAuthority(builder, userId, CatsnapAuthority.ADMIN);
+ }
+
+ /**
+ * 사진작가(PHOTOGRAPHER) 권한으로 요청을 설정합니다.
+ */
+ public MockHttpServletRequestBuilder withPhotographer(
+ MockHttpServletRequestBuilder builder, Long userId) {
+ return withAuthority(builder, userId, CatsnapAuthority.PHOTOGRAPHER);
+ }
+
+ /**
+ * 모델(MODEL) 권한으로 요청을 설정합니다.
+ */
+ public MockHttpServletRequestBuilder withModel(MockHttpServletRequestBuilder builder,
+ Long userId) {
+ return withAuthority(builder, userId, CatsnapAuthority.MODEL);
+ }
+
+ /**
+ * 익명(ANONYMOUS) 사용자로 요청을 설정합니다.
+ */
+ public MockHttpServletRequestBuilder withAnonymous(
+ MockHttpServletRequestBuilder builder) {
+ return withAuthority(builder, DEFAULT_ANONYMOUS_USER_ID, CatsnapAuthority.ANONYMOUS);
+ }
+
+ /**
+ * 지정된 권한으로 요청을 설정합니다.
+ */
+ public MockHttpServletRequestBuilder withAuthority(
+ MockHttpServletRequestBuilder builder,
+ Long userId,
+ CatsnapAuthority authority) {
+ Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS);
+ Instant exp = now.plus(DEFAULT_EXPIRATION_MINUTES, ChronoUnit.MINUTES);
+
+ Passport passport = new Passport(DEFAULT_PASSPORT_VERSION, userId, authority, now, exp);
+ String signedPassport = passportHandler.sign(passport);
+
+ return builder.header(PASSPORT_HEADER, signedPassport);
+ }
+
+ /**
+ * 유효하지 않은 Passport로 요청을 설정합니다.
+ */
+ public MockHttpServletRequestBuilder withInvalidPassport(
+ MockHttpServletRequestBuilder builder) {
+ return builder.header(PASSPORT_HEADER, "invalid-passport-string");
+ }
+
+ /**
+ * 인증 헤더 없이 요청을 설정합니다.
+ */
+ public MockHttpServletRequestBuilder withoutAuth(
+ MockHttpServletRequestBuilder builder) {
+ return builder;
+ }
+}
diff --git a/reservation/src/test/java/net/catsnap/CatsnapReservation/shared/presentation/web/interceptor/AdminInterceptorTest.java b/reservation/src/test/java/net/catsnap/CatsnapReservation/shared/presentation/web/interceptor/AdminInterceptorTest.java
new file mode 100644
index 00000000..d4df0db4
--- /dev/null
+++ b/reservation/src/test/java/net/catsnap/CatsnapReservation/shared/presentation/web/interceptor/AdminInterceptorTest.java
@@ -0,0 +1,146 @@
+package net.catsnap.CatsnapReservation.shared.presentation.web.interceptor;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.lang.reflect.Method;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import net.catsnap.CatsnapReservation.shared.presentation.error.PresentationErrorCode;
+import net.catsnap.CatsnapReservation.shared.presentation.error.PresentationException;
+import net.catsnap.shared.auth.Admin;
+import net.catsnap.shared.auth.CatsnapAuthority;
+import net.catsnap.shared.passport.domain.Passport;
+import net.catsnap.shared.passport.domain.PassportHandler;
+import net.catsnap.shared.passport.domain.exception.InvalidPassportException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.DisplayNameGeneration;
+import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores;
+import org.junit.jupiter.api.Test;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.method.HandlerMethod;
+
+@DisplayName("AdminInterceptor 테스트")
+@DisplayNameGeneration(ReplaceUnderscores.class)
+@SuppressWarnings("NonAsciiCharacters")
+class AdminInterceptorTest {
+
+ private AdminInterceptor adminInterceptor;
+ private PassportHandler passportHandler;
+ private MockHttpServletRequest request;
+ private MockHttpServletResponse response;
+
+ @BeforeEach
+ void setUp() {
+ passportHandler = mock(PassportHandler.class);
+ adminInterceptor = new AdminInterceptor(passportHandler);
+ request = new MockHttpServletRequest();
+ response = new MockHttpServletResponse();
+ }
+
+ @Test
+ void Admin_어노테이션을_붙이지_않으면_통과한다() throws Exception {
+ // given
+ HandlerMethod handler = createHandlerMethod("methodWithoutAnnotation");
+
+ // when & then
+ assertTrue(adminInterceptor.preHandle(request, response, handler));
+ }
+
+ @Test
+ void ADMIN_권한이_있으면_통과한다() throws Exception {
+ // given
+ Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS);
+ Instant exp = now.plus(30, ChronoUnit.MINUTES);
+ Passport adminPassport = new Passport((byte) 1, 1L, CatsnapAuthority.ADMIN, now, exp);
+
+ when(passportHandler.parse(anyString())).thenReturn(adminPassport);
+
+ request.addHeader("X-Passport", "signed-passport-string");
+ HandlerMethod handler = createHandlerMethod("adminMethod");
+
+ // when & then
+ assertTrue(adminInterceptor.preHandle(request, response, handler));
+ }
+
+ @Test
+ void 인증_헤더가_없으면_UNAUTHORIZED_예외가_발생한다() throws Exception {
+ // given
+ HandlerMethod handler = createHandlerMethod("adminMethod");
+
+ // when & then
+ assertThatThrownBy(() -> adminInterceptor.preHandle(request, response, handler))
+ .isInstanceOf(PresentationException.class)
+ .satisfies(e -> {
+ PresentationException ex = (PresentationException) e;
+ assertThat(ex.getResultCode()).isEqualTo(PresentationErrorCode.UNAUTHORIZED);
+ });
+ }
+
+ @Test
+ void ADMIN이_아닌_권한이면_FORBIDDEN_예외가_발생한다() throws Exception {
+ // given
+ Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS);
+ Instant exp = now.plus(30, ChronoUnit.MINUTES);
+ Passport photographerPassport = new Passport((byte) 1, 2L, CatsnapAuthority.PHOTOGRAPHER,
+ now, exp);
+
+ when(passportHandler.parse(anyString())).thenReturn(photographerPassport);
+
+ request.addHeader("X-Passport", "signed-passport-string");
+ HandlerMethod handler = createHandlerMethod("adminMethod");
+
+ // when & then
+ assertThatThrownBy(() -> adminInterceptor.preHandle(request, response, handler))
+ .isInstanceOf(PresentationException.class)
+ .satisfies(e -> {
+ PresentationException ex = (PresentationException) e;
+ assertThat(ex.getResultCode()).isEqualTo(PresentationErrorCode.FORBIDDEN);
+ });
+ }
+
+ @Test
+ void 유효하지_않은_Passport면_INVALID_PASSPORT_예외가_발생한다() throws Exception {
+ // given
+ when(passportHandler.parse(anyString())).thenThrow(
+ new InvalidPassportException("Invalid passport"));
+
+ request.addHeader("X-Passport", "invalid-passport");
+ HandlerMethod handler = createHandlerMethod("adminMethod");
+
+ // when & then
+ assertThatThrownBy(() -> adminInterceptor.preHandle(request, response, handler))
+ .isInstanceOf(PresentationException.class)
+ .satisfies(e -> {
+ PresentationException ex = (PresentationException) e;
+ assertThat(ex.getResultCode()).isEqualTo(PresentationErrorCode.INVALID_PASSPORT);
+ });
+ }
+
+ private HandlerMethod createHandlerMethod(String methodName) throws NoSuchMethodException {
+ TestController controller = new TestController();
+ Method method = controller.getClass().getMethod(methodName);
+ return new HandlerMethod(controller, method);
+ }
+
+ @RestController
+ static class TestController {
+
+ @GetMapping("/admin")
+ @Admin
+ public void adminMethod() {
+ }
+
+ @GetMapping("/public")
+ public void methodWithoutAnnotation() {
+ }
+ }
+}
diff --git a/reservation/src/test/java/net/catsnap/CatsnapReservation/shared/presentation/web/interceptor/AnyUserInterceptorTest.java b/reservation/src/test/java/net/catsnap/CatsnapReservation/shared/presentation/web/interceptor/AnyUserInterceptorTest.java
new file mode 100644
index 00000000..3da4b0a6
--- /dev/null
+++ b/reservation/src/test/java/net/catsnap/CatsnapReservation/shared/presentation/web/interceptor/AnyUserInterceptorTest.java
@@ -0,0 +1,153 @@
+package net.catsnap.CatsnapReservation.shared.presentation.web.interceptor;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.lang.reflect.Method;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import net.catsnap.CatsnapReservation.shared.presentation.error.PresentationErrorCode;
+import net.catsnap.CatsnapReservation.shared.presentation.error.PresentationException;
+import net.catsnap.shared.auth.AnyUser;
+import net.catsnap.shared.auth.CatsnapAuthority;
+import net.catsnap.shared.passport.domain.Passport;
+import net.catsnap.shared.passport.domain.PassportHandler;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.DisplayNameGeneration;
+import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores;
+import org.junit.jupiter.api.Test;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.method.HandlerMethod;
+
+@DisplayName("AnyUserInterceptor 테스트")
+@DisplayNameGeneration(ReplaceUnderscores.class)
+@SuppressWarnings("NonAsciiCharacters")
+class AnyUserInterceptorTest {
+
+ private AnyUserInterceptor anyUserInterceptor;
+ private PassportHandler passportHandler;
+ private MockHttpServletRequest request;
+ private MockHttpServletResponse response;
+
+ @BeforeEach
+ void setUp() {
+ passportHandler = mock(PassportHandler.class);
+ anyUserInterceptor = new AnyUserInterceptor(passportHandler);
+ request = new MockHttpServletRequest();
+ response = new MockHttpServletResponse();
+ }
+
+ @Test
+ void AnyUser_어노테이션을_붙이지_않으면_통과한다() throws Exception {
+ // given
+ HandlerMethod handler = createHandlerMethod("methodWithoutAnnotation");
+
+ // when & then
+ assertTrue(anyUserInterceptor.preHandle(request, response, handler));
+ }
+
+ @Test
+ void ANONYMOUS_권한이_있으면_통과한다() throws Exception {
+ // given
+ Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS);
+ Instant exp = now.plus(30, ChronoUnit.MINUTES);
+ Passport passport = new Passport((byte) 1, -1L, CatsnapAuthority.ANONYMOUS, now, exp);
+
+ when(passportHandler.parse(anyString())).thenReturn(passport);
+
+ request.addHeader("X-Passport", "signed-passport-string");
+ HandlerMethod handler = createHandlerMethod("anyUserMethod");
+
+ // when & then
+ assertTrue(anyUserInterceptor.preHandle(request, response, handler));
+ }
+
+ @Test
+ void MODEL_권한이_있으면_통과한다() throws Exception {
+ // given
+ Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS);
+ Instant exp = now.plus(30, ChronoUnit.MINUTES);
+ Passport passport = new Passport((byte) 1, 1L, CatsnapAuthority.MODEL, now, exp);
+
+ when(passportHandler.parse(anyString())).thenReturn(passport);
+
+ request.addHeader("X-Passport", "signed-passport-string");
+ HandlerMethod handler = createHandlerMethod("anyUserMethod");
+
+ // when & then
+ assertTrue(anyUserInterceptor.preHandle(request, response, handler));
+ }
+
+ @Test
+ void PHOTOGRAPHER_권한이_있으면_통과한다() throws Exception {
+ // given
+ Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS);
+ Instant exp = now.plus(30, ChronoUnit.MINUTES);
+ Passport passport = new Passport((byte) 1, 2L, CatsnapAuthority.PHOTOGRAPHER, now, exp);
+
+ when(passportHandler.parse(anyString())).thenReturn(passport);
+
+ request.addHeader("X-Passport", "signed-passport-string");
+ HandlerMethod handler = createHandlerMethod("anyUserMethod");
+
+ // when & then
+ assertTrue(anyUserInterceptor.preHandle(request, response, handler));
+ }
+
+ @Test
+ void ADMIN_권한이_있으면_통과한다() throws Exception {
+ // given
+ Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS);
+ Instant exp = now.plus(30, ChronoUnit.MINUTES);
+ Passport passport = new Passport((byte) 1, 3L, CatsnapAuthority.ADMIN, now, exp);
+
+ when(passportHandler.parse(anyString())).thenReturn(passport);
+
+ request.addHeader("X-Passport", "signed-passport-string");
+ HandlerMethod handler = createHandlerMethod("anyUserMethod");
+
+ // when & then
+ assertTrue(anyUserInterceptor.preHandle(request, response, handler));
+ }
+
+ @Test
+ void 인증_헤더가_없으면_UNAUTHORIZED_예외가_발생한다() throws Exception {
+ // given
+ HandlerMethod handler = createHandlerMethod("anyUserMethod");
+
+ // when & then
+ assertThatThrownBy(() -> anyUserInterceptor.preHandle(request, response, handler))
+ .isInstanceOf(PresentationException.class)
+ .satisfies(e -> {
+ PresentationException ex = (PresentationException) e;
+ assertThat(ex.getResultCode()).isEqualTo(PresentationErrorCode.UNAUTHORIZED);
+ });
+ }
+
+ private HandlerMethod createHandlerMethod(String methodName) throws NoSuchMethodException {
+ TestController controller = new TestController();
+ Method method = controller.getClass().getMethod(methodName);
+ return new HandlerMethod(controller, method);
+ }
+
+ @RestController
+ static class TestController {
+
+ @GetMapping("/any")
+ @AnyUser
+ public void anyUserMethod() {
+ }
+
+ @GetMapping("/public")
+ public void methodWithoutAnnotation() {
+ }
+ }
+}
diff --git a/reservation/src/test/java/net/catsnap/CatsnapReservation/shared/presentation/web/interceptor/LoginModelInterceptorTest.java b/reservation/src/test/java/net/catsnap/CatsnapReservation/shared/presentation/web/interceptor/LoginModelInterceptorTest.java
new file mode 100644
index 00000000..14ac3099
--- /dev/null
+++ b/reservation/src/test/java/net/catsnap/CatsnapReservation/shared/presentation/web/interceptor/LoginModelInterceptorTest.java
@@ -0,0 +1,142 @@
+package net.catsnap.CatsnapReservation.shared.presentation.web.interceptor;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.lang.reflect.Method;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import net.catsnap.CatsnapReservation.shared.presentation.error.PresentationErrorCode;
+import net.catsnap.CatsnapReservation.shared.presentation.error.PresentationException;
+import net.catsnap.shared.auth.CatsnapAuthority;
+import net.catsnap.shared.auth.LoginModel;
+import net.catsnap.shared.passport.domain.Passport;
+import net.catsnap.shared.passport.domain.PassportHandler;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.DisplayNameGeneration;
+import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores;
+import org.junit.jupiter.api.Test;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.method.HandlerMethod;
+
+@DisplayName("LoginModelInterceptor 테스트")
+@DisplayNameGeneration(ReplaceUnderscores.class)
+@SuppressWarnings("NonAsciiCharacters")
+class LoginModelInterceptorTest {
+
+ private LoginModelInterceptor loginModelInterceptor;
+ private PassportHandler passportHandler;
+ private MockHttpServletRequest request;
+ private MockHttpServletResponse response;
+
+ @BeforeEach
+ void setUp() {
+ passportHandler = mock(PassportHandler.class);
+ loginModelInterceptor = new LoginModelInterceptor(passportHandler);
+ request = new MockHttpServletRequest();
+ response = new MockHttpServletResponse();
+ }
+
+ @Test
+ void LoginModel_어노테이션을_붙이지_않으면_통과한다() throws Exception {
+ // given
+ HandlerMethod handler = createHandlerMethod("methodWithoutAnnotation");
+
+ // when & then
+ assertTrue(loginModelInterceptor.preHandle(request, response, handler));
+ }
+
+ @Test
+ void MODEL_권한이_있으면_통과한다() throws Exception {
+ // given
+ Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS);
+ Instant exp = now.plus(30, ChronoUnit.MINUTES);
+ Passport passport = new Passport((byte) 1, 1L, CatsnapAuthority.MODEL, now, exp);
+
+ when(passportHandler.parse(anyString())).thenReturn(passport);
+
+ request.addHeader("X-Passport", "signed-passport-string");
+ HandlerMethod handler = createHandlerMethod("modelMethod");
+
+ // when & then
+ assertTrue(loginModelInterceptor.preHandle(request, response, handler));
+ }
+
+ @Test
+ void ADMIN_권한이_있으면_통과한다() throws Exception {
+ // given
+ Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS);
+ Instant exp = now.plus(30, ChronoUnit.MINUTES);
+ Passport passport = new Passport((byte) 1, 2L, CatsnapAuthority.ADMIN, now, exp);
+
+ when(passportHandler.parse(anyString())).thenReturn(passport);
+
+ request.addHeader("X-Passport", "signed-passport-string");
+ HandlerMethod handler = createHandlerMethod("modelMethod");
+
+ // when & then
+ assertTrue(loginModelInterceptor.preHandle(request, response, handler));
+ }
+
+ @Test
+ void PHOTOGRAPHER_권한이면_FORBIDDEN_예외가_발생한다() throws Exception {
+ // given
+ Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS);
+ Instant exp = now.plus(30, ChronoUnit.MINUTES);
+ Passport passport = new Passport((byte) 1, 3L, CatsnapAuthority.PHOTOGRAPHER, now, exp);
+
+ when(passportHandler.parse(anyString())).thenReturn(passport);
+
+ request.addHeader("X-Passport", "signed-passport-string");
+ HandlerMethod handler = createHandlerMethod("modelMethod");
+
+ // when & then
+ assertThatThrownBy(() -> loginModelInterceptor.preHandle(request, response, handler))
+ .isInstanceOf(PresentationException.class)
+ .satisfies(e -> {
+ PresentationException ex = (PresentationException) e;
+ assertThat(ex.getResultCode()).isEqualTo(PresentationErrorCode.FORBIDDEN);
+ });
+ }
+
+ @Test
+ void 인증_헤더가_없으면_UNAUTHORIZED_예외가_발생한다() throws Exception {
+ // given
+ HandlerMethod handler = createHandlerMethod("modelMethod");
+
+ // when & then
+ assertThatThrownBy(() -> loginModelInterceptor.preHandle(request, response, handler))
+ .isInstanceOf(PresentationException.class)
+ .satisfies(e -> {
+ PresentationException ex = (PresentationException) e;
+ assertThat(ex.getResultCode()).isEqualTo(PresentationErrorCode.UNAUTHORIZED);
+ });
+ }
+
+ private HandlerMethod createHandlerMethod(String methodName) throws NoSuchMethodException {
+ TestController controller = new TestController();
+ Method method = controller.getClass().getMethod(methodName);
+ return new HandlerMethod(controller, method);
+ }
+
+ @RestController
+ static class TestController {
+
+ @GetMapping("/model")
+ @LoginModel
+ public void modelMethod() {
+ }
+
+ @GetMapping("/public")
+ public void methodWithoutAnnotation() {
+ }
+ }
+}
diff --git a/reservation/src/test/java/net/catsnap/CatsnapReservation/shared/presentation/web/interceptor/LoginPhotographerInterceptorTest.java b/reservation/src/test/java/net/catsnap/CatsnapReservation/shared/presentation/web/interceptor/LoginPhotographerInterceptorTest.java
new file mode 100644
index 00000000..b809e840
--- /dev/null
+++ b/reservation/src/test/java/net/catsnap/CatsnapReservation/shared/presentation/web/interceptor/LoginPhotographerInterceptorTest.java
@@ -0,0 +1,142 @@
+package net.catsnap.CatsnapReservation.shared.presentation.web.interceptor;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.lang.reflect.Method;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import net.catsnap.CatsnapReservation.shared.presentation.error.PresentationErrorCode;
+import net.catsnap.CatsnapReservation.shared.presentation.error.PresentationException;
+import net.catsnap.shared.auth.CatsnapAuthority;
+import net.catsnap.shared.auth.LoginPhotographer;
+import net.catsnap.shared.passport.domain.Passport;
+import net.catsnap.shared.passport.domain.PassportHandler;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.DisplayNameGeneration;
+import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores;
+import org.junit.jupiter.api.Test;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.method.HandlerMethod;
+
+@DisplayName("LoginPhotographerInterceptor 테스트")
+@DisplayNameGeneration(ReplaceUnderscores.class)
+@SuppressWarnings("NonAsciiCharacters")
+class LoginPhotographerInterceptorTest {
+
+ private LoginPhotographerInterceptor loginPhotographerInterceptor;
+ private PassportHandler passportHandler;
+ private MockHttpServletRequest request;
+ private MockHttpServletResponse response;
+
+ @BeforeEach
+ void setUp() {
+ passportHandler = mock(PassportHandler.class);
+ loginPhotographerInterceptor = new LoginPhotographerInterceptor(passportHandler);
+ request = new MockHttpServletRequest();
+ response = new MockHttpServletResponse();
+ }
+
+ @Test
+ void LoginPhotographer_어노테이션을_붙이지_않으면_통과한다() throws Exception {
+ // given
+ HandlerMethod handler = createHandlerMethod("methodWithoutAnnotation");
+
+ // when & then
+ assertTrue(loginPhotographerInterceptor.preHandle(request, response, handler));
+ }
+
+ @Test
+ void PHOTOGRAPHER_권한이_있으면_통과한다() throws Exception {
+ // given
+ Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS);
+ Instant exp = now.plus(30, ChronoUnit.MINUTES);
+ Passport passport = new Passport((byte) 1, 1L, CatsnapAuthority.PHOTOGRAPHER, now, exp);
+
+ when(passportHandler.parse(anyString())).thenReturn(passport);
+
+ request.addHeader("X-Passport", "signed-passport-string");
+ HandlerMethod handler = createHandlerMethod("photographerMethod");
+
+ // when & then
+ assertTrue(loginPhotographerInterceptor.preHandle(request, response, handler));
+ }
+
+ @Test
+ void ADMIN_권한이_있으면_통과한다() throws Exception {
+ // given
+ Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS);
+ Instant exp = now.plus(30, ChronoUnit.MINUTES);
+ Passport passport = new Passport((byte) 1, 2L, CatsnapAuthority.ADMIN, now, exp);
+
+ when(passportHandler.parse(anyString())).thenReturn(passport);
+
+ request.addHeader("X-Passport", "signed-passport-string");
+ HandlerMethod handler = createHandlerMethod("photographerMethod");
+
+ // when & then
+ assertTrue(loginPhotographerInterceptor.preHandle(request, response, handler));
+ }
+
+ @Test
+ void MODEL_권한이면_FORBIDDEN_예외가_발생한다() throws Exception {
+ // given
+ Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS);
+ Instant exp = now.plus(30, ChronoUnit.MINUTES);
+ Passport passport = new Passport((byte) 1, 3L, CatsnapAuthority.MODEL, now, exp);
+
+ when(passportHandler.parse(anyString())).thenReturn(passport);
+
+ request.addHeader("X-Passport", "signed-passport-string");
+ HandlerMethod handler = createHandlerMethod("photographerMethod");
+
+ // when & then
+ assertThatThrownBy(() -> loginPhotographerInterceptor.preHandle(request, response, handler))
+ .isInstanceOf(PresentationException.class)
+ .satisfies(e -> {
+ PresentationException ex = (PresentationException) e;
+ assertThat(ex.getResultCode()).isEqualTo(PresentationErrorCode.FORBIDDEN);
+ });
+ }
+
+ @Test
+ void 인증_헤더가_없으면_UNAUTHORIZED_예외가_발생한다() throws Exception {
+ // given
+ HandlerMethod handler = createHandlerMethod("photographerMethod");
+
+ // when & then
+ assertThatThrownBy(() -> loginPhotographerInterceptor.preHandle(request, response, handler))
+ .isInstanceOf(PresentationException.class)
+ .satisfies(e -> {
+ PresentationException ex = (PresentationException) e;
+ assertThat(ex.getResultCode()).isEqualTo(PresentationErrorCode.UNAUTHORIZED);
+ });
+ }
+
+ private HandlerMethod createHandlerMethod(String methodName) throws NoSuchMethodException {
+ TestController controller = new TestController();
+ Method method = controller.getClass().getMethod(methodName);
+ return new HandlerMethod(controller, method);
+ }
+
+ @RestController
+ static class TestController {
+
+ @GetMapping("/photographer")
+ @LoginPhotographer
+ public void photographerMethod() {
+ }
+
+ @GetMapping("/public")
+ public void methodWithoutAnnotation() {
+ }
+ }
+}
diff --git a/reservation/src/test/java/net/catsnap/CatsnapReservation/shared/presentation/web/interceptor/LoginUserInterceptorTest.java b/reservation/src/test/java/net/catsnap/CatsnapReservation/shared/presentation/web/interceptor/LoginUserInterceptorTest.java
new file mode 100644
index 00000000..476610c3
--- /dev/null
+++ b/reservation/src/test/java/net/catsnap/CatsnapReservation/shared/presentation/web/interceptor/LoginUserInterceptorTest.java
@@ -0,0 +1,158 @@
+package net.catsnap.CatsnapReservation.shared.presentation.web.interceptor;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.lang.reflect.Method;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import net.catsnap.CatsnapReservation.shared.presentation.error.PresentationErrorCode;
+import net.catsnap.CatsnapReservation.shared.presentation.error.PresentationException;
+import net.catsnap.shared.auth.CatsnapAuthority;
+import net.catsnap.shared.auth.LoginUser;
+import net.catsnap.shared.passport.domain.Passport;
+import net.catsnap.shared.passport.domain.PassportHandler;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.DisplayNameGeneration;
+import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores;
+import org.junit.jupiter.api.Test;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.method.HandlerMethod;
+
+@DisplayName("LoginUserInterceptor 테스트")
+@DisplayNameGeneration(ReplaceUnderscores.class)
+@SuppressWarnings("NonAsciiCharacters")
+class LoginUserInterceptorTest {
+
+ private LoginUserInterceptor loginUserInterceptor;
+ private PassportHandler passportHandler;
+ private MockHttpServletRequest request;
+ private MockHttpServletResponse response;
+
+ @BeforeEach
+ void setUp() {
+ passportHandler = mock(PassportHandler.class);
+ loginUserInterceptor = new LoginUserInterceptor(passportHandler);
+ request = new MockHttpServletRequest();
+ response = new MockHttpServletResponse();
+ }
+
+ @Test
+ void LoginUser_어노테이션을_붙이지_않으면_통과한다() throws Exception {
+ // given
+ HandlerMethod handler = createHandlerMethod("methodWithoutAnnotation");
+
+ // when & then
+ assertTrue(loginUserInterceptor.preHandle(request, response, handler));
+ }
+
+ @Test
+ void MODEL_권한이_있으면_통과한다() throws Exception {
+ // given
+ Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS);
+ Instant exp = now.plus(30, ChronoUnit.MINUTES);
+ Passport passport = new Passport((byte) 1, 1L, CatsnapAuthority.MODEL, now, exp);
+
+ when(passportHandler.parse(anyString())).thenReturn(passport);
+
+ request.addHeader("X-Passport", "signed-passport-string");
+ HandlerMethod handler = createHandlerMethod("loginUserMethod");
+
+ // when & then
+ assertTrue(loginUserInterceptor.preHandle(request, response, handler));
+ }
+
+ @Test
+ void PHOTOGRAPHER_권한이_있으면_통과한다() throws Exception {
+ // given
+ Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS);
+ Instant exp = now.plus(30, ChronoUnit.MINUTES);
+ Passport passport = new Passport((byte) 1, 2L, CatsnapAuthority.PHOTOGRAPHER, now, exp);
+
+ when(passportHandler.parse(anyString())).thenReturn(passport);
+
+ request.addHeader("X-Passport", "signed-passport-string");
+ HandlerMethod handler = createHandlerMethod("loginUserMethod");
+
+ // when & then
+ assertTrue(loginUserInterceptor.preHandle(request, response, handler));
+ }
+
+ @Test
+ void ADMIN_권한이_있으면_통과한다() throws Exception {
+ // given
+ Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS);
+ Instant exp = now.plus(30, ChronoUnit.MINUTES);
+ Passport passport = new Passport((byte) 1, 3L, CatsnapAuthority.ADMIN, now, exp);
+
+ when(passportHandler.parse(anyString())).thenReturn(passport);
+
+ request.addHeader("X-Passport", "signed-passport-string");
+ HandlerMethod handler = createHandlerMethod("loginUserMethod");
+
+ // when & then
+ assertTrue(loginUserInterceptor.preHandle(request, response, handler));
+ }
+
+ @Test
+ void ANONYMOUS_권한이면_FORBIDDEN_예외가_발생한다() throws Exception {
+ // given
+ Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS);
+ Instant exp = now.plus(30, ChronoUnit.MINUTES);
+ Passport passport = new Passport((byte) 1, -1L, CatsnapAuthority.ANONYMOUS, now, exp);
+
+ when(passportHandler.parse(anyString())).thenReturn(passport);
+
+ request.addHeader("X-Passport", "signed-passport-string");
+ HandlerMethod handler = createHandlerMethod("loginUserMethod");
+
+ // when & then
+ assertThatThrownBy(() -> loginUserInterceptor.preHandle(request, response, handler))
+ .isInstanceOf(PresentationException.class)
+ .satisfies(e -> {
+ PresentationException ex = (PresentationException) e;
+ assertThat(ex.getResultCode()).isEqualTo(PresentationErrorCode.FORBIDDEN);
+ });
+ }
+
+ @Test
+ void 인증_헤더가_없으면_UNAUTHORIZED_예외가_발생한다() throws Exception {
+ // given
+ HandlerMethod handler = createHandlerMethod("loginUserMethod");
+
+ // when & then
+ assertThatThrownBy(() -> loginUserInterceptor.preHandle(request, response, handler))
+ .isInstanceOf(PresentationException.class)
+ .satisfies(e -> {
+ PresentationException ex = (PresentationException) e;
+ assertThat(ex.getResultCode()).isEqualTo(PresentationErrorCode.UNAUTHORIZED);
+ });
+ }
+
+ private HandlerMethod createHandlerMethod(String methodName) throws NoSuchMethodException {
+ TestController controller = new TestController();
+ Method method = controller.getClass().getMethod(methodName);
+ return new HandlerMethod(controller, method);
+ }
+
+ @RestController
+ static class TestController {
+
+ @GetMapping("/user")
+ @LoginUser
+ public void loginUserMethod() {
+ }
+
+ @GetMapping("/public")
+ public void methodWithoutAnnotation() {
+ }
+ }
+}
From feafcd8df0798ca9732cd922ca623956b1b32147 Mon Sep 17 00:00:00 2001
From: chokyungho + * 애플리케이션에서 발생하는 예외를 일관된 응답 형식으로 변환합니다. + *
+ */ +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + /** + * 존재하지 않는 API 엔드포인트 요청 처리 + */ + @ExceptionHandler(NoHandlerFoundException.class) + public ResponseEntity+ * 인증 헤더가 없거나 유효하지 않은 경우, 권한이 없는 경우 등 + * 프레젠테이션 레이어에서 발생하는 예외를 처리합니다. + *
+ */ + @ExceptionHandler(PresentationException.class) + public ResponseEntity+ * DomainException에 포함된 ResultCode를 사용하여 응답을 생성합니다. + *
+ */ + @ExceptionHandler(DomainException.class) + public ResponseEntity+ * HTTP 상태 코드는 ResponseEntity의 헤더에 설정되며, + * 응답 body에는 비즈니스 상태 코드(code)와 메시지, 데이터만 포함됩니다. + *
+ */ +@Builder +@Getter +public class ResultResponse+ * API 요청 처리 성공 시 사용하는 공통 응답 코드를 정의합니다. + *
+ */ +@Getter +@RequiredArgsConstructor +public enum PresentationSuccessCode implements ResultCode { + + /** + * 데이터 조회 성공 + */ + READ(HttpStatus.OK, "SA000", "성공적으로 데이터를 조회했습니다."), + + /** + * 데이터 생성 성공 + */ + CREATE(HttpStatus.CREATED, "SA001", "성공적으로 데이터를 생성했습니다."), + + /** + * 데이터 수정 성공 + */ + UPDATE(HttpStatus.OK, "SA002", "성공적으로 데이터를 수정했습니다."), + + /** + * 데이터 삭제 성공 + */ + DELETE(HttpStatus.OK, "SA003", "성공적으로 데이터를 삭제했습니다."), + + /** + * 작업 완료 (데이터 없음) + */ + NO_CONTENT(HttpStatus.NO_CONTENT, "SA004", "요청이 성공적으로 처리되었습니다."); + + private final HttpStatusCode httpStatus; + private final String code; + private final String message; +} diff --git a/reservation/src/test/java/net/catsnap/CatsnapReservation/shared/presentation/GlobalExceptionHandlerTest.java b/reservation/src/test/java/net/catsnap/CatsnapReservation/shared/presentation/GlobalExceptionHandlerTest.java new file mode 100644 index 00000000..c671c5ef --- /dev/null +++ b/reservation/src/test/java/net/catsnap/CatsnapReservation/shared/presentation/GlobalExceptionHandlerTest.java @@ -0,0 +1,153 @@ +package net.catsnap.CatsnapReservation.shared.presentation; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.catsnap.CatsnapReservation.shared.domain.error.DomainErrorCode; +import net.catsnap.CatsnapReservation.shared.domain.error.DomainException; +import net.catsnap.CatsnapReservation.shared.presentation.error.PresentationErrorCode; +import net.catsnap.CatsnapReservation.shared.presentation.error.PresentationException; +import net.catsnap.CatsnapReservation.shared.presentation.response.ResultResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.HttpMediaTypeNotSupportedException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.servlet.NoHandlerFoundException; + +@DisplayName("GlobalExceptionHandler 테스트") +@DisplayNameGeneration(ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class GlobalExceptionHandlerTest { + + private GlobalExceptionHandler handler; + + @BeforeEach + void setUp() { + handler = new GlobalExceptionHandler(); + } + + @Test + void NoHandlerFoundException_처리시_NOT_FOUND_API_응답을_반환한다() { + // given + NoHandlerFoundException e = new NoHandlerFoundException("GET", "/api/not-found", null); + + // when + ResponseEntity