allowedAuthorities, PassportHandler passportHandler) {
+ this.targetAnnotationType = targetAnnotationType;
+ this.allowedAuthorities = allowedAuthorities;
+ this.passportHandler = passportHandler;
+ }
+
+ /**
+ * 요청 전처리 핸들러입니다.
+ *
+ * 다음 순서로 검증을 수행합니다:
+ *
+ * - 핸들러가 컨트롤러 메서드인지 확인
+ * - 대상 어노테이션이 메서드에 존재하는지 확인
+ * - 사용자의 권한이 허용 목록에 포함되는지 검증
+ *
+ *
+ *
+ * @param request HTTP 요청
+ * @param response HTTP 응답
+ * @param handler 핸들러 객체
+ * @return 다음 인터셉터 체인 진행 여부 (true: 진행, false: 중단)
+ * @throws Exception 예외 발생 시
+ */
+ @Override
+ public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
+ Object handler) throws Exception {
+
+ // 1. 핸들러가 컨트롤러 메서드인지 확인
+ HandlerMethod handlerMethod = extractHandlerMethod(handler);
+ if (handlerMethod == null) {
+ return true;
+ }
+
+ // 2. 대상 어노테이션이 메서드에 존재하는지 확인
+ A annotation = findAnnotationOnMethod(handlerMethod);
+ if (annotation == null) {
+ return true;
+ }
+
+ // 3. 사용자 권한 검증
+ validateUserAuthority(request);
+ return true;
+ }
+
+ /**
+ * 핸들러 객체가 HandlerMethod인 경우 추출합니다.
+ *
+ * @param handler 핸들러 객체
+ * @return HandlerMethod 또는 null (HandlerMethod가 아닌 경우)
+ */
+ private HandlerMethod extractHandlerMethod(Object handler) {
+ if (!(handler instanceof HandlerMethod handlerMethod)) {
+ return null;
+ }
+ return handlerMethod;
+ }
+
+ /**
+ * 메서드에 대상 어노테이션이 존재하는지 확인합니다.
+ *
+ * @param handlerMethod 핸들러 메서드
+ * @return 어노테이션 객체 또는 null (어노테이션이 없는 경우)
+ */
+ private A findAnnotationOnMethod(HandlerMethod handlerMethod) {
+ return handlerMethod.getMethodAnnotation(targetAnnotationType);
+ }
+
+ /**
+ * 사용자의 권한을 검증합니다.
+ *
+ * 현재 사용자의 권한이 허용된 권한 목록({@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)의 권한 정보를
+ * 확인하여 접근을 제어합니다.
+ *
+ *
+ * 허용 권한
+ *
+ * - {@link CatsnapAuthority#ADMIN} - 관리자
+ *
+ *
+ * 사용 예시
+ * {@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 {
+
+ public AdminInterceptor(PassportHandler passportHandler) {
+ super(Admin.class, List.of(CatsnapAuthority.ADMIN), passportHandler);
+ }
+}
\ No newline at end of file
diff --git a/reservation/src/main/java/net/catsnap/CatsnapReservation/shared/presentation/web/interceptor/AnyUserInterceptor.java b/reservation/src/main/java/net/catsnap/CatsnapReservation/shared/presentation/web/interceptor/AnyUserInterceptor.java
new file mode 100644
index 00000000..ea537374
--- /dev/null
+++ b/reservation/src/main/java/net/catsnap/CatsnapReservation/shared/presentation/web/interceptor/AnyUserInterceptor.java
@@ -0,0 +1,51 @@
+package net.catsnap.CatsnapReservation.shared.presentation.web.interceptor;
+
+import java.util.List;
+import net.catsnap.shared.auth.AnyUser;
+import net.catsnap.shared.auth.CatsnapAuthority;
+import net.catsnap.shared.passport.domain.PassportHandler;
+import org.springframework.stereotype.Component;
+
+/**
+ * 모든 사용자 접근 허용 인터셉터입니다.
+ *
+ * {@link AnyUser} 어노테이션이 붙은 컨트롤러 메서드에 대해 모든 권한의 접근을 허용합니다. 익명 사용자를 포함한 모든 사용자가 접근할 수 있지만, 반드시 인증
+ * 헤더는 필요합니다.
+ *
+ *
+ * 허용 권한
+ *
+ * - {@link CatsnapAuthority#ANONYMOUS} - 익명 사용자
+ * - {@link CatsnapAuthority#MODEL} - 모델
+ * - {@link CatsnapAuthority#PHOTOGRAPHER} - 사진작가
+ * - {@link CatsnapAuthority#ADMIN} - 관리자
+ *
+ *
+ * 사용 예시
+ * {@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 {
+
+ public AnyUserInterceptor(PassportHandler passportHandler) {
+ super(AnyUser.class, List.of(
+ CatsnapAuthority.ANONYMOUS,
+ CatsnapAuthority.MODEL,
+ CatsnapAuthority.PHOTOGRAPHER,
+ CatsnapAuthority.ADMIN
+ ), passportHandler);
+ }
+}
diff --git a/reservation/src/main/java/net/catsnap/CatsnapReservation/shared/presentation/web/interceptor/LoginModelInterceptor.java b/reservation/src/main/java/net/catsnap/CatsnapReservation/shared/presentation/web/interceptor/LoginModelInterceptor.java
new file mode 100644
index 00000000..6728999f
--- /dev/null
+++ b/reservation/src/main/java/net/catsnap/CatsnapReservation/shared/presentation/web/interceptor/LoginModelInterceptor.java
@@ -0,0 +1,52 @@
+package net.catsnap.CatsnapReservation.shared.presentation.web.interceptor;
+
+import java.util.List;
+import net.catsnap.shared.auth.CatsnapAuthority;
+import net.catsnap.shared.auth.LoginModel;
+import net.catsnap.shared.passport.domain.PassportHandler;
+import org.springframework.stereotype.Component;
+
+/**
+ * 모델 권한 검증 인터셉터입니다.
+ *
+ * {@link LoginModel} 어노테이션이 붙은 컨트롤러 메서드에 대해 모델 권한을 검증합니다. 모델(MODEL)과 관리자(ADMIN)만 접근할 수 있습니다.
+ *
+ *
+ * 허용 권한
+ *
+ * - {@link CatsnapAuthority#MODEL} - 모델
+ * - {@link CatsnapAuthority#ADMIN} - 관리자
+ *
+ *
+ * 거부 권한
+ *
+ * - {@link CatsnapAuthority#PHOTOGRAPHER} - 사진작가
+ * - {@link CatsnapAuthority#ANONYMOUS} - 익명 사용자
+ *
+ *
+ * 사용 예시
+ * {@code
+ * @RestController
+ * public class BookingController {
+ *
+ * @LoginModel // 모델과 관리자만 접근 가능
+ * @GetMapping("/bookings/my")
+ * public List getMyBookings() {
+ * // 모델이 자신의 촬영 예약 목록 조회
+ * }
+ * }
+ * }
+ *
+ * @see LoginModel
+ * @see AbstractAuthInterceptor
+ */
+@Component
+public class LoginModelInterceptor extends AbstractAuthInterceptor {
+
+ public LoginModelInterceptor(PassportHandler passportHandler) {
+ super(LoginModel.class, List.of(
+ CatsnapAuthority.MODEL,
+ CatsnapAuthority.ADMIN
+ ), passportHandler);
+ }
+}
\ No newline at end of file
diff --git a/reservation/src/main/java/net/catsnap/CatsnapReservation/shared/presentation/web/interceptor/LoginPhotographerInterceptor.java b/reservation/src/main/java/net/catsnap/CatsnapReservation/shared/presentation/web/interceptor/LoginPhotographerInterceptor.java
new file mode 100644
index 00000000..c6d8ab74
--- /dev/null
+++ b/reservation/src/main/java/net/catsnap/CatsnapReservation/shared/presentation/web/interceptor/LoginPhotographerInterceptor.java
@@ -0,0 +1,54 @@
+package net.catsnap.CatsnapReservation.shared.presentation.web.interceptor;
+
+import java.util.List;
+import net.catsnap.shared.auth.CatsnapAuthority;
+import net.catsnap.shared.auth.LoginPhotographer;
+import net.catsnap.shared.passport.domain.PassportHandler;
+import org.springframework.stereotype.Component;
+
+/**
+ * 사진작가 권한 검증 인터셉터입니다.
+ *
+ * {@link LoginPhotographer} 어노테이션이 붙은 컨트롤러 메서드에 대해 사진작가 권한을 검증합니다. 사진작가(PHOTOGRAPHER)와 관리자(ADMIN)만
+ * 접근할 수 있습니다.
+ *
+ *
+ * 허용 권한
+ *
+ * - {@link CatsnapAuthority#PHOTOGRAPHER} - 사진작가
+ * - {@link CatsnapAuthority#ADMIN} - 관리자
+ *
+ *
+ * 거부 권한
+ *
+ * - {@link CatsnapAuthority#MODEL} - 모델
+ * - {@link CatsnapAuthority#ANONYMOUS} - 익명 사용자
+ *
+ *
+ * 사용 예시
+ * {@code
+ * @RestController
+ * public class PortfolioController {
+ *
+ * @LoginPhotographer // 사진작가와 관리자만 접근 가능
+ * @PostMapping("/portfolios")
+ * public PortfolioResponse createPortfolio(@RequestBody PortfolioRequest request) {
+ * // 사진작가가 포트폴리오 생성
+ * }
+ * }
+ * }
+ *
+ * @see LoginPhotographer
+ * @see AbstractAuthInterceptor
+ */
+@Component
+public class LoginPhotographerInterceptor
+ extends AbstractAuthInterceptor {
+
+ public LoginPhotographerInterceptor(PassportHandler passportHandler) {
+ super(LoginPhotographer.class, List.of(
+ CatsnapAuthority.PHOTOGRAPHER,
+ CatsnapAuthority.ADMIN
+ ), passportHandler);
+ }
+}
diff --git a/reservation/src/main/java/net/catsnap/CatsnapReservation/shared/presentation/web/interceptor/LoginUserInterceptor.java b/reservation/src/main/java/net/catsnap/CatsnapReservation/shared/presentation/web/interceptor/LoginUserInterceptor.java
new file mode 100644
index 00000000..d029a480
--- /dev/null
+++ b/reservation/src/main/java/net/catsnap/CatsnapReservation/shared/presentation/web/interceptor/LoginUserInterceptor.java
@@ -0,0 +1,54 @@
+package net.catsnap.CatsnapReservation.shared.presentation.web.interceptor;
+
+import java.util.List;
+import net.catsnap.shared.auth.CatsnapAuthority;
+import net.catsnap.shared.auth.LoginUser;
+import net.catsnap.shared.passport.domain.PassportHandler;
+import org.springframework.stereotype.Component;
+
+/**
+ * 로그인한 사용자 권한 검증 인터셉터입니다.
+ *
+ * {@link LoginUser} 어노테이션이 붙은 컨트롤러 메서드에 대해 로그인한 사용자만 접근을 허용합니다. 익명(ANONYMOUS) 사용자는 접근할 수 없으며, 실제
+ * 계정을 가진 사용자만 접근 가능합니다.
+ *
+ *
+ * 허용 권한
+ *
+ * - {@link CatsnapAuthority#MODEL} - 모델
+ * - {@link CatsnapAuthority#PHOTOGRAPHER} - 사진작가
+ * - {@link CatsnapAuthority#ADMIN} - 관리자
+ *
+ *
+ * 거부 권한
+ *
+ * - {@link CatsnapAuthority#ANONYMOUS} - 익명 사용자
+ *
+ *
+ * 사용 예시
+ * {@code
+ * @RestController
+ * public class ProfileController {
+ *
+ * @LoginUser // 로그인한 사용자만 접근 가능
+ * @GetMapping("/profile")
+ * public ProfileResponse getMyProfile() {
+ * // 모델, 사진작가, 관리자 모두 자신의 프로필 조회 가능
+ * }
+ * }
+ * }
+ *
+ * @see LoginUser
+ * @see AbstractAuthInterceptor
+ */
+@Component
+public class LoginUserInterceptor extends AbstractAuthInterceptor {
+
+ public LoginUserInterceptor(PassportHandler passportHandler) {
+ super(LoginUser.class, List.of(
+ CatsnapAuthority.MODEL,
+ CatsnapAuthority.PHOTOGRAPHER,
+ CatsnapAuthority.ADMIN
+ ), passportHandler);
+ }
+}
diff --git a/reservation/src/test/java/net/catsnap/CatsnapReservation/shared/domain/error/DomainErrorCodeTest.java b/reservation/src/test/java/net/catsnap/CatsnapReservation/shared/domain/error/DomainErrorCodeTest.java
new file mode 100644
index 00000000..b85e8bd8
--- /dev/null
+++ b/reservation/src/test/java/net/catsnap/CatsnapReservation/shared/domain/error/DomainErrorCodeTest.java
@@ -0,0 +1,38 @@
+package net.catsnap.CatsnapReservation.shared.domain.error;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+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;
+
+@DisplayName("DomainErrorCode 테스트")
+@DisplayNameGeneration(ReplaceUnderscores.class)
+@SuppressWarnings("NonAsciiCharacters")
+class DomainErrorCodeTest {
+
+ @Test
+ void 모든_DomainErrorCode는_ED로_시작하는_코드를_가진다() {
+ // when & then
+ for (DomainErrorCode errorCode : DomainErrorCode.values()) {
+ assertThat(errorCode.getCode()).startsWith("ED");
+ }
+ }
+
+ @Test
+ void 모든_DomainErrorCode는_HttpStatus를_가진다() {
+ // when & then
+ for (DomainErrorCode errorCode : DomainErrorCode.values()) {
+ assertThat(errorCode.getHttpStatus()).isNotNull();
+ }
+ }
+
+ @Test
+ void 모든_DomainErrorCode는_메시지를_가진다() {
+ // when & then
+ for (DomainErrorCode errorCode : DomainErrorCode.values()) {
+ assertThat(errorCode.getMessage()).isNotBlank();
+ }
+ }
+}
diff --git a/reservation/src/test/java/net/catsnap/CatsnapReservation/shared/domain/error/DomainExceptionTest.java b/reservation/src/test/java/net/catsnap/CatsnapReservation/shared/domain/error/DomainExceptionTest.java
new file mode 100644
index 00000000..fd2595aa
--- /dev/null
+++ b/reservation/src/test/java/net/catsnap/CatsnapReservation/shared/domain/error/DomainExceptionTest.java
@@ -0,0 +1,58 @@
+package net.catsnap.CatsnapReservation.shared.domain.error;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import net.catsnap.CatsnapReservation.shared.ResultCode;
+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;
+
+@DisplayName("DomainException 테스트")
+@DisplayNameGeneration(ReplaceUnderscores.class)
+@SuppressWarnings("NonAsciiCharacters")
+class DomainExceptionTest {
+
+ @Test
+ void ResultCode로_예외를_생성한다() {
+ // given
+ ResultCode resultCode = DomainErrorCode.DOMAIN_CONSTRAINT_VIOLATION;
+
+ // when
+ DomainException exception = new DomainException(resultCode);
+
+ // then
+ assertThat(exception.getResultCode()).isEqualTo(resultCode);
+ assertThat(exception.getMessage()).isEqualTo("해당 값이 유효하지 않습니다.");
+ }
+
+ @Test
+ void ResultCode와_원인_예외로_예외를_생성한다() {
+ // given
+ ResultCode resultCode = DomainErrorCode.DOMAIN_CONSTRAINT_VIOLATION;
+ Throwable cause = new RuntimeException("원인 예외");
+
+ // when
+ DomainException exception = new DomainException(resultCode, cause);
+
+ // then
+ assertThat(exception.getResultCode()).isEqualTo(resultCode);
+ assertThat(exception.getMessage()).isEqualTo("해당 값이 유효하지 않습니다.");
+ assertThat(exception.getCause()).isEqualTo(cause);
+ }
+
+ @Test
+ void ResultCode와_추가_메시지로_예외를_생성한다() {
+ // given
+ ResultCode resultCode = DomainErrorCode.DOMAIN_CONSTRAINT_VIOLATION;
+ String additionalMessage = "상세 정보";
+
+ // when
+ DomainException exception = new DomainException(resultCode, additionalMessage);
+
+ // then
+ assertThat(exception.getResultCode()).isEqualTo(resultCode);
+ assertThat(exception.getMessage()).contains("해당 값이 유효하지 않습니다.");
+ assertThat(exception.getMessage()).contains("상세 정보");
+ }
+}
diff --git a/reservation/src/test/java/net/catsnap/CatsnapReservation/shared/fixture/PassportTestHelper.java b/reservation/src/test/java/net/catsnap/CatsnapReservation/shared/fixture/PassportTestHelper.java
new file mode 100644
index 00000000..a34a9fd7
--- /dev/null
+++ b/reservation/src/test/java/net/catsnap/CatsnapReservation/shared/fixture/PassportTestHelper.java
@@ -0,0 +1,124 @@
+package net.catsnap.CatsnapReservation.shared.fixture;
+
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import net.catsnap.shared.auth.CatsnapAuthority;
+import net.catsnap.shared.passport.domain.Passport;
+import net.catsnap.shared.passport.domain.PassportHandler;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.TestComponent;
+import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
+
+/**
+ * 테스트용 Passport 헤더 설정 헬퍼 클래스
+ *
+ * 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/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> response = handler.handleNoHandlerFoundException(e);
+
+ // then
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
+ assertThat(response.getBody().getCode()).isEqualTo(PresentationErrorCode.NOT_FOUND_API.getCode());
+ }
+
+ @Test
+ void MissingServletRequestParameterException_처리시_MISSING_REQUEST_PARAMETER_응답을_반환한다() {
+ // given
+ MissingServletRequestParameterException e =
+ new MissingServletRequestParameterException("param", "String");
+
+ // when
+ ResponseEntity> response =
+ handler.handleMissingServletRequestParameterException(e);
+
+ // then
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(response.getBody().getCode())
+ .isEqualTo(PresentationErrorCode.MISSING_REQUEST_PARAMETER.getCode());
+ }
+
+ @Test
+ void HttpMessageNotReadableException_처리시_INVALID_REQUEST_BODY_응답을_반환한다() {
+ // given
+ HttpMessageNotReadableException e =
+ new HttpMessageNotReadableException("Invalid JSON", (org.springframework.http.HttpInputMessage) null);
+
+ // when
+ ResponseEntity> response =
+ handler.handleHttpMessageNotReadableException(e);
+
+ // then
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(response.getBody().getCode())
+ .isEqualTo(PresentationErrorCode.INVALID_REQUEST_BODY.getCode());
+ }
+
+ @Test
+ void HttpRequestMethodNotSupportedException_처리시_METHOD_NOT_ALLOWED_응답을_반환한다() {
+ // given
+ HttpRequestMethodNotSupportedException e =
+ new HttpRequestMethodNotSupportedException("POST");
+
+ // when
+ ResponseEntity> response =
+ handler.handleHttpRequestMethodNotSupportedException(e);
+
+ // then
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.METHOD_NOT_ALLOWED);
+ assertThat(response.getBody().getCode())
+ .isEqualTo(PresentationErrorCode.METHOD_NOT_ALLOWED.getCode());
+ }
+
+ @Test
+ void HttpMediaTypeNotSupportedException_처리시_UNSUPPORTED_MEDIA_TYPE_응답을_반환한다() {
+ // given
+ HttpMediaTypeNotSupportedException e =
+ new HttpMediaTypeNotSupportedException("Unsupported media type");
+
+ // when
+ ResponseEntity> response =
+ handler.handleHttpMediaTypeNotSupportedException(e);
+
+ // then
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNSUPPORTED_MEDIA_TYPE);
+ assertThat(response.getBody().getCode())
+ .isEqualTo(PresentationErrorCode.UNSUPPORTED_MEDIA_TYPE.getCode());
+ }
+
+ @Test
+ void PresentationException_처리시_해당_ErrorCode_응답을_반환한다() {
+ // given
+ PresentationException e = new PresentationException(PresentationErrorCode.UNAUTHORIZED);
+
+ // when
+ ResponseEntity> response = handler.handlePresentationException(e);
+
+ // then
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
+ assertThat(response.getBody().getCode())
+ .isEqualTo(PresentationErrorCode.UNAUTHORIZED.getCode());
+ }
+
+ @Test
+ void DomainException_처리시_해당_ErrorCode_응답을_반환한다() {
+ // given
+ DomainException e = new DomainException(DomainErrorCode.DOMAIN_CONSTRAINT_VIOLATION);
+
+ // when
+ ResponseEntity> response = handler.handleDomainException(e);
+
+ // then
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(response.getBody().getCode())
+ .isEqualTo(DomainErrorCode.DOMAIN_CONSTRAINT_VIOLATION.getCode());
+ }
+
+ @Test
+ void 기타_Exception_처리시_INTERNAL_SERVER_ERROR_응답을_반환한다() {
+ // given
+ Exception e = new RuntimeException("Unexpected error");
+
+ // when
+ ResponseEntity> response = handler.handleException(e);
+
+ // then
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
+ assertThat(response.getBody().getCode())
+ .isEqualTo(PresentationErrorCode.INTERNAL_SERVER_ERROR.getCode());
+ }
+}
diff --git a/reservation/src/test/java/net/catsnap/CatsnapReservation/shared/presentation/error/PresentationErrorCodeTest.java b/reservation/src/test/java/net/catsnap/CatsnapReservation/shared/presentation/error/PresentationErrorCodeTest.java
new file mode 100644
index 00000000..0fcc93af
--- /dev/null
+++ b/reservation/src/test/java/net/catsnap/CatsnapReservation/shared/presentation/error/PresentationErrorCodeTest.java
@@ -0,0 +1,60 @@
+package net.catsnap.CatsnapReservation.shared.presentation.error;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+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;
+
+@DisplayName("PresentationErrorCode 테스트")
+@DisplayNameGeneration(ReplaceUnderscores.class)
+@SuppressWarnings("NonAsciiCharacters")
+class PresentationErrorCodeTest {
+
+ @Test
+ void 모든_PresentationErrorCode는_EA로_시작하는_코드를_가진다() {
+ // when & then
+ for (PresentationErrorCode errorCode : PresentationErrorCode.values()) {
+ assertThat(errorCode.getCode()).startsWith("EA");
+ }
+ }
+
+ @Test
+ void 모든_PresentationErrorCode는_HttpStatus를_가진다() {
+ // when & then
+ for (PresentationErrorCode errorCode : PresentationErrorCode.values()) {
+ assertThat(errorCode.getHttpStatus()).isNotNull();
+ }
+ }
+
+ @Test
+ void 모든_PresentationErrorCode는_메시지를_가진다() {
+ // when & then
+ for (PresentationErrorCode errorCode : PresentationErrorCode.values()) {
+ assertThat(errorCode.getMessage()).isNotBlank();
+ }
+ }
+
+ @Test
+ void API_요청_에러는_EA0XX_코드를_가진다() {
+ // when & then
+ assertThat(PresentationErrorCode.NOT_FOUND_API.getCode()).startsWith("EA0");
+ assertThat(PresentationErrorCode.MISSING_REQUEST_PARAMETER.getCode()).startsWith("EA0");
+ assertThat(PresentationErrorCode.INVALID_REQUEST_BODY.getCode()).startsWith("EA0");
+ assertThat(PresentationErrorCode.INTERNAL_SERVER_ERROR.getCode()).startsWith("EA0");
+ assertThat(PresentationErrorCode.INVALID_REQUEST_FORMAT.getCode()).startsWith("EA0");
+ assertThat(PresentationErrorCode.METHOD_NOT_ALLOWED.getCode()).startsWith("EA0");
+ assertThat(PresentationErrorCode.UNSUPPORTED_MEDIA_TYPE.getCode()).startsWith("EA0");
+ }
+
+ @Test
+ void 인증_인가_에러는_EA1XX_코드를_가진다() {
+ // when & then
+ assertThat(PresentationErrorCode.UNAUTHORIZED.getCode()).startsWith("EA1");
+ assertThat(PresentationErrorCode.INVALID_AUTHORITY.getCode()).startsWith("EA1");
+ assertThat(PresentationErrorCode.FORBIDDEN.getCode()).startsWith("EA1");
+ assertThat(PresentationErrorCode.INVALID_PASSPORT.getCode()).startsWith("EA1");
+ assertThat(PresentationErrorCode.EXPIRED_PASSPORT.getCode()).startsWith("EA1");
+ }
+}
diff --git a/reservation/src/test/java/net/catsnap/CatsnapReservation/shared/presentation/error/PresentationExceptionTest.java b/reservation/src/test/java/net/catsnap/CatsnapReservation/shared/presentation/error/PresentationExceptionTest.java
new file mode 100644
index 00000000..4e55f300
--- /dev/null
+++ b/reservation/src/test/java/net/catsnap/CatsnapReservation/shared/presentation/error/PresentationExceptionTest.java
@@ -0,0 +1,84 @@
+package net.catsnap.CatsnapReservation.shared.presentation.error;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import net.catsnap.CatsnapReservation.shared.ResultCode;
+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;
+
+@DisplayName("PresentationException 테스트")
+@DisplayNameGeneration(ReplaceUnderscores.class)
+@SuppressWarnings("NonAsciiCharacters")
+class PresentationExceptionTest {
+
+ @Test
+ void ResultCode로_예외를_생성한다() {
+ // given
+ ResultCode resultCode = PresentationErrorCode.INTERNAL_SERVER_ERROR;
+
+ // when
+ PresentationException exception = new PresentationException(resultCode);
+
+ // then
+ assertThat(exception.getResultCode()).isEqualTo(resultCode);
+ assertThat(exception.getMessage()).isEqualTo("서버 내부 오류가 발생했습니다.");
+ }
+
+ @Test
+ void ResultCode와_원인_예외로_예외를_생성한다() {
+ // given
+ ResultCode resultCode = PresentationErrorCode.INTERNAL_SERVER_ERROR;
+ Throwable cause = new RuntimeException("원인 예외");
+
+ // when
+ PresentationException exception = new PresentationException(resultCode, cause);
+
+ // then
+ assertThat(exception.getResultCode()).isEqualTo(resultCode);
+ assertThat(exception.getMessage()).isEqualTo("서버 내부 오류가 발생했습니다.");
+ assertThat(exception.getCause()).isEqualTo(cause);
+ }
+
+ @Test
+ void ResultCode와_추가_메시지로_예외를_생성한다() {
+ // given
+ ResultCode resultCode = PresentationErrorCode.INTERNAL_SERVER_ERROR;
+ String additionalMessage = "상세 정보";
+
+ // when
+ PresentationException exception = new PresentationException(resultCode, additionalMessage);
+
+ // then
+ assertThat(exception.getResultCode()).isEqualTo(resultCode);
+ assertThat(exception.getMessage()).contains("서버 내부 오류가 발생했습니다.");
+ assertThat(exception.getMessage()).contains("상세 정보");
+ }
+
+ @Test
+ void 인증_에러코드로_예외를_생성한다() {
+ // given
+ ResultCode resultCode = PresentationErrorCode.UNAUTHORIZED;
+
+ // when
+ PresentationException exception = new PresentationException(resultCode);
+
+ // then
+ assertThat(exception.getResultCode()).isEqualTo(resultCode);
+ assertThat(exception.getMessage()).isEqualTo("인증 정보가 없습니다.");
+ }
+
+ @Test
+ void 인가_에러코드로_예외를_생성한다() {
+ // given
+ ResultCode resultCode = PresentationErrorCode.FORBIDDEN;
+
+ // when
+ PresentationException exception = new PresentationException(resultCode);
+
+ // then
+ assertThat(exception.getResultCode()).isEqualTo(resultCode);
+ assertThat(exception.getMessage()).isEqualTo("접근 권한이 없습니다.");
+ }
+}
diff --git a/reservation/src/test/java/net/catsnap/CatsnapReservation/shared/presentation/response/ResultResponseTest.java b/reservation/src/test/java/net/catsnap/CatsnapReservation/shared/presentation/response/ResultResponseTest.java
new file mode 100644
index 00000000..a4634552
--- /dev/null
+++ b/reservation/src/test/java/net/catsnap/CatsnapReservation/shared/presentation/response/ResultResponseTest.java
@@ -0,0 +1,70 @@
+package net.catsnap.CatsnapReservation.shared.presentation.response;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import net.catsnap.CatsnapReservation.shared.ResultCode;
+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.HttpStatusCode;
+import org.springframework.http.ResponseEntity;
+
+@DisplayName("ResultResponse 테스트")
+@DisplayNameGeneration(ReplaceUnderscores.class)
+@SuppressWarnings("NonAsciiCharacters")
+class ResultResponseTest {
+
+ @Test
+ void 데이터와_함께_응답을_생성한다() {
+ // given
+ TestResultCode resultCode = new TestResultCode();
+ String testData = "test data";
+
+ // when
+ ResponseEntity> response = ResultResponse.of(resultCode, testData);
+
+ // then
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(response.getBody()).isNotNull();
+ assertThat(response.getBody().getCode()).isEqualTo("TEST001");
+ assertThat(response.getBody().getMessage()).isEqualTo("테스트 성공");
+ assertThat(response.getBody().getData()).isEqualTo(testData);
+ }
+
+ @Test
+ void 데이터없이_응답을_생성한다() {
+ // given
+ TestResultCode resultCode = new TestResultCode();
+
+ // when
+ ResponseEntity> response = ResultResponse.of(resultCode);
+
+ // then
+ assertThat(response.getStatusCode()).isEqualTo(resultCode.getHttpStatus());
+ assertThat(response.getBody()).isNotNull();
+ assertThat(response.getBody().getCode()).isEqualTo("TEST001");
+ assertThat(response.getBody().getMessage()).isEqualTo("테스트 성공");
+ assertThat(response.getBody().getData()).isNull();
+ }
+
+ // 테스트용 ResultCode 구현체
+ private static class TestResultCode implements ResultCode {
+
+ @Override
+ public HttpStatusCode getHttpStatus() {
+ return HttpStatus.OK;
+ }
+
+ @Override
+ public String getCode() {
+ return "TEST001";
+ }
+
+ @Override
+ public String getMessage() {
+ return "테스트 성공";
+ }
+ }
+}
diff --git a/reservation/src/test/java/net/catsnap/CatsnapReservation/shared/presentation/success/PresentationSuccessCodeTest.java b/reservation/src/test/java/net/catsnap/CatsnapReservation/shared/presentation/success/PresentationSuccessCodeTest.java
new file mode 100644
index 00000000..510ada31
--- /dev/null
+++ b/reservation/src/test/java/net/catsnap/CatsnapReservation/shared/presentation/success/PresentationSuccessCodeTest.java
@@ -0,0 +1,38 @@
+package net.catsnap.CatsnapReservation.shared.presentation.success;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+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;
+
+@DisplayName("PresentationSuccessCode 테스트")
+@DisplayNameGeneration(ReplaceUnderscores.class)
+@SuppressWarnings("NonAsciiCharacters")
+class PresentationSuccessCodeTest {
+
+ @Test
+ void 모든_SuccessCode는_S로_시작하는_코드를_가진다() {
+ // when & then
+ for (PresentationSuccessCode successCode : PresentationSuccessCode.values()) {
+ assertThat(successCode.getCode()).startsWith("S");
+ }
+ }
+
+ @Test
+ void 모든_SuccessCode는_HttpStatus를_가진다() {
+ // when & then
+ for (PresentationSuccessCode successCode : PresentationSuccessCode.values()) {
+ assertThat(successCode.getHttpStatus()).isNotNull();
+ }
+ }
+
+ @Test
+ void 모든_SuccessCode는_메시지를_가진다() {
+ // when & then
+ for (PresentationSuccessCode successCode : PresentationSuccessCode.values()) {
+ assertThat(successCode.getMessage()).isNotBlank();
+ }
+ }
+}
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() {
+ }
+ }
+}
diff --git a/reservation/src/test/resources/application.yml b/reservation/src/test/resources/application.yml
index e169b7d3..00b1d36f 100644
--- a/reservation/src/test/resources/application.yml
+++ b/reservation/src/test/resources/application.yml
@@ -20,3 +20,6 @@ spring:
h2:
console:
enabled: false
+
+passport:
+ secret-key: itstestsecreykey123123123itstestsecreykey123123123
\ No newline at end of file