From bafb94105b477b1dac68b30d5cb14743bd04ecdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B3=BD=EC=84=B1=EC=A4=80?= <70939232+sjk4618@users.noreply.github.com> Date: Sun, 8 Feb 2026 07:28:39 +0900 Subject: [PATCH 1/7] readme update --- README.md | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 81636d0..c887aac 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,6 @@ ## ๐Ÿ“‹ ๋ชฉ์ฐจ - [๊ธฐ์ˆ  ์Šคํƒ](#-๊ธฐ์ˆ -์Šคํƒ) -- [์‹œ์Šคํ…œ ์•„ํ‚คํ…์ฒ˜](#-์‹œ์Šคํ…œ-์•„ํ‚คํ…์ฒ˜) - [์ฃผ์š” ๊ธฐ๋Šฅ](#-์ฃผ์š”-๊ธฐ๋Šฅ) - [ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ](#-ํ”„๋กœ์ ํŠธ-๊ตฌ์กฐ) - [CI/CD](#cicd) @@ -48,22 +47,10 @@ --- -## ๐Ÿ— ์‹œ์Šคํ…œ ์•„ํ‚คํ…์ฒ˜ -``` -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ Client โ”‚โ”€โ”€โ”€โ”€โ–ถโ”‚ Nginx โ”‚โ”€โ”€โ”€โ”€โ–ถโ”‚ Spring Boot โ”‚ -โ”‚ (Frontend) โ”‚ โ”‚ (Reverse โ”‚ โ”‚ Server โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ Proxy) โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ - โ”‚ - โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ - โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ -โ”Œโ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ MySQL โ”‚ โ”‚ Redis โ”‚ โ”‚ AWS S3 โ”‚ โ”‚ Toss Payments โ”‚ โ”‚ Google โ”‚ โ”‚ Kakao โ”‚ -โ”‚ DB โ”‚ โ”‚ Cache โ”‚ โ”‚ (Images) โ”‚ โ”‚ API โ”‚ โ”‚ OAuth โ”‚ โ”‚ OAuth โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -``` + + + --- From 7a94e9472a439cb93451fcbd760e399009c5a03e Mon Sep 17 00:00:00 2001 From: Kwak Seong Joon Date: Thu, 26 Feb 2026 14:57:29 +0900 Subject: [PATCH 2/7] =?UTF-8?q?test:=20=EA=B8=B0=EB=B3=B8=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EB=93=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + .../auth/jwt/JwtGeneratorCacheTest.java | 51 +-- .../permitserver/domain/SimpleEnumTest.java | 144 ++++++ .../auth/api/service/AuthServiceTest.java | 307 +++++++++++++ .../coupon/api/service/CouponServiceTest.java | 53 +++ .../event/api/service/EventServiceTest.java | 153 +++++++ .../core/component/EventRetrieverTest.java | 143 ++++++ .../event/core/domain/EventEntityTest.java | 176 ++++++++ .../event/core/domain/EventTypeTest.java | 47 ++ .../api/service/TimetableServiceTest.java | 283 ++++++++++++ .../api/service/TimetableLikeServiceTest.java | 105 +++++ .../guest/api/service/GuestServiceTest.java | 173 +++++++ .../api/service/PaymentServiceTest.java | 285 ++++++++++++ .../core/component/PaymentRetrieverTest.java | 138 ++++++ .../core/domain/PaymentEntityTest.java | 102 +++++ .../api/service/ReservationServiceTest.java | 358 +++++++++++++++ .../component/ReservationRetrieverTest.java | 197 ++++++++ .../core/domain/ReservationEntityTest.java | 184 ++++++++ .../core/domain/ReservationStatusTest.java | 106 +++++ .../service/EventSiteMapImageServiceTest.java | 80 ++++ .../ticket/api/service/TicketServiceTest.java | 424 ++++++++++++++++++ .../core/component/TicketGeneratorTest.java | 169 +++++++ .../core/component/TicketRetrieverTest.java | 197 ++++++++ .../ticket/core/domain/TicketEntityTest.java | 139 ++++++ .../user/api/service/UserServiceTest.java | 149 ++++++ .../core/component/UserRetrieverTest.java | 245 ++++++++++ .../user/core/domain/UserEntityTest.java | 165 +++++++ .../TicketOrCouponCodeGeneratorTest.java | 89 ++++ .../util/LocalDateTimeFormatterUtilTest.java | 384 ++++++++++++++++ .../global/util/PriceFormatterUtilTest.java | 163 +++++++ .../global/util/SecureUrlUtilTest.java | 149 ++++++ .../global/util/TimeFormatterUtilTest.java | 117 +++++ 32 files changed, 5445 insertions(+), 31 deletions(-) create mode 100644 src/test/java/com/permitseoul/permitserver/domain/SimpleEnumTest.java create mode 100644 src/test/java/com/permitseoul/permitserver/domain/auth/api/service/AuthServiceTest.java create mode 100644 src/test/java/com/permitseoul/permitserver/domain/coupon/api/service/CouponServiceTest.java create mode 100644 src/test/java/com/permitseoul/permitserver/domain/event/api/service/EventServiceTest.java create mode 100644 src/test/java/com/permitseoul/permitserver/domain/event/core/component/EventRetrieverTest.java create mode 100644 src/test/java/com/permitseoul/permitserver/domain/event/core/domain/EventEntityTest.java create mode 100644 src/test/java/com/permitseoul/permitserver/domain/event/core/domain/EventTypeTest.java create mode 100644 src/test/java/com/permitseoul/permitserver/domain/eventtimetable/timetable/api/service/TimetableServiceTest.java create mode 100644 src/test/java/com/permitseoul/permitserver/domain/eventtimetable/userlike/api/service/TimetableLikeServiceTest.java create mode 100644 src/test/java/com/permitseoul/permitserver/domain/guest/api/service/GuestServiceTest.java create mode 100644 src/test/java/com/permitseoul/permitserver/domain/payment/api/service/PaymentServiceTest.java create mode 100644 src/test/java/com/permitseoul/permitserver/domain/payment/core/component/PaymentRetrieverTest.java create mode 100644 src/test/java/com/permitseoul/permitserver/domain/payment/core/domain/PaymentEntityTest.java create mode 100644 src/test/java/com/permitseoul/permitserver/domain/reservation/api/service/ReservationServiceTest.java create mode 100644 src/test/java/com/permitseoul/permitserver/domain/reservation/core/component/ReservationRetrieverTest.java create mode 100644 src/test/java/com/permitseoul/permitserver/domain/reservation/core/domain/ReservationEntityTest.java create mode 100644 src/test/java/com/permitseoul/permitserver/domain/reservation/core/domain/ReservationStatusTest.java create mode 100644 src/test/java/com/permitseoul/permitserver/domain/sitemapimage/api/service/EventSiteMapImageServiceTest.java create mode 100644 src/test/java/com/permitseoul/permitserver/domain/ticket/api/service/TicketServiceTest.java create mode 100644 src/test/java/com/permitseoul/permitserver/domain/ticket/core/component/TicketGeneratorTest.java create mode 100644 src/test/java/com/permitseoul/permitserver/domain/ticket/core/component/TicketRetrieverTest.java create mode 100644 src/test/java/com/permitseoul/permitserver/domain/ticket/core/domain/TicketEntityTest.java create mode 100644 src/test/java/com/permitseoul/permitserver/domain/user/api/service/UserServiceTest.java create mode 100644 src/test/java/com/permitseoul/permitserver/domain/user/core/component/UserRetrieverTest.java create mode 100644 src/test/java/com/permitseoul/permitserver/domain/user/core/domain/UserEntityTest.java create mode 100644 src/test/java/com/permitseoul/permitserver/global/TicketOrCouponCodeGeneratorTest.java create mode 100644 src/test/java/com/permitseoul/permitserver/global/util/LocalDateTimeFormatterUtilTest.java create mode 100644 src/test/java/com/permitseoul/permitserver/global/util/PriceFormatterUtilTest.java create mode 100644 src/test/java/com/permitseoul/permitserver/global/util/SecureUrlUtilTest.java create mode 100644 src/test/java/com/permitseoul/permitserver/global/util/TimeFormatterUtilTest.java diff --git a/.gitignore b/.gitignore index d6bdfc6..94fca7c 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ out/ ### Mac ### .DS_Store +.agent src/main/resources/application.yml src/main/resources/application-dev.yml diff --git a/src/test/java/com/permitseoul/permitserver/auth/jwt/JwtGeneratorCacheTest.java b/src/test/java/com/permitseoul/permitserver/auth/jwt/JwtGeneratorCacheTest.java index 671076f..8ff12b1 100644 --- a/src/test/java/com/permitseoul/permitserver/auth/jwt/JwtGeneratorCacheTest.java +++ b/src/test/java/com/permitseoul/permitserver/auth/jwt/JwtGeneratorCacheTest.java @@ -15,6 +15,7 @@ import static org.assertj.core.api.Assertions.assertThat; +// TODO: RTCacheManager ํด๋ž˜์Šค๊ฐ€ ์‚ญ์ œ๋˜์–ด ๊ด€๋ จ ํ…Œ์ŠคํŠธ๊ฐ€ ๋น„ํ™œ์„ฑํ™”๋จ. ์บ์‹œ ๋งค๋‹ˆ์ € ๋ณ€๊ฒฝ ํ›„ ํ…Œ์ŠคํŠธ ๋ณต์› ํ•„์š”. @SpringBootTest class JwtGeneratorCacheTest { @@ -27,42 +28,30 @@ class JwtGeneratorCacheTest { @Autowired private JwtProvider jwtProvider; - @Autowired - private RTCacheManager rtCacheManager; + // @Autowired + // private RTCacheManager rtCacheManager; // TODO: RTCacheManager ์‚ญ์ œ๋จ - ๋Œ€์ฒด ๊ตฌํ˜„ ํ›„ + // ๋ณต์› - //ํ…Œ์ŠคํŠธ ํ›„ ์บ์‹œ ์‚ญ์ œ + // ํ…Œ์ŠคํŠธ ํ›„ ์บ์‹œ ์‚ญ์ œ @AfterEach void tearDown() { Objects.requireNonNull(cacheManager.getCache(Constants.REFRESH_TOKEN)).clear(); } - @Test - void ๋ฆฌํ”„๋ ˆ์‹œ_ํ† ํฐ_์บ์‹œ์—_์ •์ƒ_์ €์žฅ๋จ() { - // given - long userId = 1L; - - // when - String token = jwtGenerator.generateRefreshToken(userId, UserRole.USER); - - // then - String cachedToken = rtCacheManager.getRefreshTokenFromCache(userId); - - assertThat(cachedToken).isEqualTo(token); - } - - @Test - void ์บ์‹œ์—_userId_๊ฐ’์ด_์—†์œผ๋ฉด_null_๋ฐ˜ํ™˜() { - // given - long nonExistUserId = 999L; - - // when - String token = rtCacheManager.getRefreshTokenFromCache(nonExistUserId); - - // then - Assertions.assertNull(token); - } - - - + // TODO: RTCacheManager ๋Œ€์ฒด ์ดํ›„ ๋ณต์› + // @Test + // void ๋ฆฌํ”„๋ ˆ์‹œ_ํ† ํฐ_์บ์‹œ์—_์ •์ƒ_์ €์žฅ๋จ() { + // long userId = 1L; + // String token = jwtGenerator.generateRefreshToken(userId, UserRole.USER); + // String cachedToken = rtCacheManager.getRefreshTokenFromCache(userId); + // assertThat(cachedToken).isEqualTo(token); + // } + + // @Test + // void ์บ์‹œ์—_userId_๊ฐ’์ด_์—†์œผ๋ฉด_null_๋ฐ˜ํ™˜() { + // long nonExistUserId = 999L; + // String token = rtCacheManager.getRefreshTokenFromCache(nonExistUserId); + // Assertions.assertNull(token); + // } } \ No newline at end of file diff --git a/src/test/java/com/permitseoul/permitserver/domain/SimpleEnumTest.java b/src/test/java/com/permitseoul/permitserver/domain/SimpleEnumTest.java new file mode 100644 index 0000000..9cac488 --- /dev/null +++ b/src/test/java/com/permitseoul/permitserver/domain/SimpleEnumTest.java @@ -0,0 +1,144 @@ +package com.permitseoul.permitserver.domain; + +import com.permitseoul.permitserver.domain.admin.base.core.domain.MediaType; +import com.permitseoul.permitserver.domain.payment.core.domain.Currency; +import com.permitseoul.permitserver.domain.payment.core.domain.PaymentType; +import com.permitseoul.permitserver.domain.ticket.core.domain.TicketStatus; +import com.permitseoul.permitserver.domain.ticket.core.domain.TicketUsability; +import com.permitseoul.permitserver.domain.user.core.domain.Gender; +import com.permitseoul.permitserver.domain.user.core.domain.SocialType; +import com.permitseoul.permitserver.domain.user.core.domain.UserRole; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayName("๋‹จ์ˆœ Enum ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ") +class SimpleEnumTest { + + @Nested + @DisplayName("Currency") + class CurrencyTest { + + @Test + @DisplayName("์—ด๊ฑฐ๊ฐ’์€ 3๊ฐœ์ด๋‹ค (KRW, USD, JPY)") + void hasThreeValues() { + assertThat(Currency.values()).hasSize(3); + assertThat(Currency.values()).containsExactly(Currency.KRW, Currency.USD, Currency.JPY); + } + + @Test + @DisplayName("valueOf๋กœ KRW๋ฅผ ์กฐํšŒํ•  ์ˆ˜ ์žˆ๋‹ค") + void valueOfKRW() { + assertThat(Currency.valueOf("KRW")).isEqualTo(Currency.KRW); + } + + @Test + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ฐ’์€ IllegalArgumentException์„ ๋˜์ง„๋‹ค") + void throwsExceptionForInvalidValue() { + assertThatThrownBy(() -> Currency.valueOf("EUR")) + .isInstanceOf(IllegalArgumentException.class); + } + } + + @Nested + @DisplayName("PaymentType") + class PaymentTypeTest { + + @Test + @DisplayName("์—ด๊ฑฐ๊ฐ’์€ 8๊ฐœ์ด๋‹ค") + void hasEightValues() { + assertThat(PaymentType.values()).hasSize(8); + } + + @Test + @DisplayName("CARD์™€ EASY_PAY๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ๋‹ค") + void containsCardAndEasyPay() { + assertThat(PaymentType.values()) + .contains(PaymentType.CARD, PaymentType.EASY_PAY); + } + + @Test + @DisplayName("valueOf๋กœ CARD๋ฅผ ์กฐํšŒํ•  ์ˆ˜ ์žˆ๋‹ค") + void valueOfCard() { + assertThat(PaymentType.valueOf("CARD")).isEqualTo(PaymentType.CARD); + } + } + + @Nested + @DisplayName("TicketStatus") + class TicketStatusTest { + + @Test + @DisplayName("์—ด๊ฑฐ๊ฐ’์€ 3๊ฐœ์ด๋‹ค (RESERVED, USED, CANCELED)") + void hasThreeValues() { + assertThat(TicketStatus.values()).hasSize(3); + assertThat(TicketStatus.values()).containsExactly( + TicketStatus.RESERVED, TicketStatus.USED, TicketStatus.CANCELED); + } + } + + @Nested + @DisplayName("TicketUsability") + class TicketUsabilityTest { + + @Test + @DisplayName("์—ด๊ฑฐ๊ฐ’์€ 2๊ฐœ์ด๋‹ค (USABLE, UNUSABLE)") + void hasTwoValues() { + assertThat(TicketUsability.values()).hasSize(2); + assertThat(TicketUsability.values()).containsExactly( + TicketUsability.USABLE, TicketUsability.UNUSABLE); + } + } + + @Nested + @DisplayName("UserRole") + class UserRoleTest { + + @Test + @DisplayName("์—ด๊ฑฐ๊ฐ’์€ 3๊ฐœ์ด๋‹ค (USER, ADMIN, STAFF)") + void hasThreeValues() { + assertThat(UserRole.values()).hasSize(3); + assertThat(UserRole.values()).containsExactly( + UserRole.USER, UserRole.ADMIN, UserRole.STAFF); + } + } + + @Nested + @DisplayName("Gender") + class GenderTest { + + @Test + @DisplayName("์—ด๊ฑฐ๊ฐ’์€ 2๊ฐœ์ด๋‹ค (MALE, FEMALE)") + void hasTwoValues() { + assertThat(Gender.values()).hasSize(2); + assertThat(Gender.values()).containsExactly(Gender.MALE, Gender.FEMALE); + } + } + + @Nested + @DisplayName("SocialType") + class SocialTypeTest { + + @Test + @DisplayName("์—ด๊ฑฐ๊ฐ’์€ 2๊ฐœ์ด๋‹ค (KAKAO, GOOGLE)") + void hasTwoValues() { + assertThat(SocialType.values()).hasSize(2); + assertThat(SocialType.values()).containsExactly(SocialType.KAKAO, SocialType.GOOGLE); + } + } + + @Nested + @DisplayName("MediaType") + class MediaTypeTest { + + @Test + @DisplayName("์—ด๊ฑฐ๊ฐ’์€ 2๊ฐœ์ด๋‹ค (IMAGE, VIDEO)") + void hasTwoValues() { + assertThat(MediaType.values()).hasSize(2); + assertThat(MediaType.values()).containsExactly(MediaType.IMAGE, MediaType.VIDEO); + } + } +} diff --git a/src/test/java/com/permitseoul/permitserver/domain/auth/api/service/AuthServiceTest.java b/src/test/java/com/permitseoul/permitserver/domain/auth/api/service/AuthServiceTest.java new file mode 100644 index 0000000..c11c1a3 --- /dev/null +++ b/src/test/java/com/permitseoul/permitserver/domain/auth/api/service/AuthServiceTest.java @@ -0,0 +1,307 @@ +package com.permitseoul.permitserver.domain.auth.api.service; + +import com.permitseoul.permitserver.domain.auth.api.dto.TokenDto; +import com.permitseoul.permitserver.domain.auth.api.exception.AuthRedisException; +import com.permitseoul.permitserver.domain.auth.api.exception.AuthSocialNotFoundApiException; +import com.permitseoul.permitserver.domain.auth.api.exception.AuthUnAuthorizedException; +import com.permitseoul.permitserver.domain.auth.api.exception.AuthUnAuthorizedFeignException; +import com.permitseoul.permitserver.domain.auth.core.domain.Token; +import com.permitseoul.permitserver.domain.auth.core.dto.UserSocialInfoDto; +import com.permitseoul.permitserver.domain.auth.core.exception.*; +import com.permitseoul.permitserver.domain.auth.core.jwt.JwtProperties; +import com.permitseoul.permitserver.domain.auth.core.jwt.JwtProvider; +import com.permitseoul.permitserver.domain.auth.core.jwt.RefreshTokenManager; +import com.permitseoul.permitserver.domain.auth.core.strategy.LoginStrategy; +import com.permitseoul.permitserver.domain.auth.core.strategy.LoginStrategyManager; +import com.permitseoul.permitserver.domain.user.core.component.UserRetriever; +import com.permitseoul.permitserver.domain.user.core.component.UserSaver; +import com.permitseoul.permitserver.domain.user.core.domain.Gender; +import com.permitseoul.permitserver.domain.user.core.domain.SocialType; +import com.permitseoul.permitserver.domain.user.core.domain.User; +import com.permitseoul.permitserver.domain.user.core.domain.UserRole; +import com.permitseoul.permitserver.domain.user.core.domain.entity.UserEntity; +import com.permitseoul.permitserver.domain.user.core.exception.UserDuplicateException; +import com.permitseoul.permitserver.domain.user.core.exception.UserNotFoundException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.dao.DataAccessException; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@DisplayName("AuthService ํ…Œ์ŠคํŠธ") +class AuthServiceTest { + + @Mock + private LoginStrategyManager loginStrategyManager; + @Mock + private UserSaver userSaver; + @Mock + private JwtProvider jwtProvider; + @Mock + private UserRetriever userRetriever; + @Mock + private RefreshTokenManager refreshTokenManager; + @Mock + private JwtProperties jwtProperties; + @InjectMocks + private AuthService authService; + + private static final long USER_ID = 1L; + private static final String ACCESS_TOKEN = "access-token"; + private static final String REFRESH_TOKEN = "refresh-token"; + private static final String SOCIAL_ACCESS_TOKEN = "social-access-token"; + private static final String SOCIAL_ID = "social-123"; + private static final String AUTH_CODE = "auth-code"; + private static final String REDIRECT_URL = "https://redirect.com"; + + private Token createToken() { + return Token.of(ACCESS_TOKEN, REFRESH_TOKEN); + } + + private User createUser() { + return new User(USER_ID, "ํ™๊ธธ๋™", Gender.MALE, 25, "test@email.com", SOCIAL_ID, SocialType.KAKAO, UserRole.USER); + } + + private LoginStrategy mockLoginStrategy() { + final LoginStrategy strategy = mock(LoginStrategy.class); + when(loginStrategyManager.getStrategy(SocialType.KAKAO)).thenReturn(strategy); + return strategy; + } + + @Nested + @DisplayName("signUp") + class SignUpTest { + + @Test + @DisplayName("์ •์ƒ: ํšŒ์›๊ฐ€์ž… โ†’ ํ† ํฐ ๋ฐ˜ํ™˜") + void success() { + final LoginStrategy strategy = mockLoginStrategy(); + when(strategy.getUserSocialId(SOCIAL_ACCESS_TOKEN)).thenReturn(SOCIAL_ID); + doNothing().when(userRetriever).validDuplicatedUserBySocial(SocialType.KAKAO, SOCIAL_ID); + + final UserEntity savedEntity = UserEntity.create("ํ™๊ธธ๋™", Gender.MALE, 25, "test@email.com", SOCIAL_ID, + SocialType.KAKAO, UserRole.USER); + ReflectionTestUtils.setField(savedEntity, "userId", USER_ID); + when(userSaver.saveUser(any(UserEntity.class))).thenReturn(savedEntity); + when(jwtProvider.issueToken(USER_ID, UserRole.USER)).thenReturn(createToken()); + when(jwtProperties.refreshTokenExpirationTime()).thenReturn(604800000L); + + final TokenDto result = authService.signUp("ํ™๊ธธ๋™", 25, Gender.MALE, "test@email.com", SocialType.KAKAO, + SOCIAL_ACCESS_TOKEN); + + assertThat(result.accessToken()).isEqualTo(ACCESS_TOKEN); + assertThat(result.refreshToken()).isEqualTo(REFRESH_TOKEN); + verify(refreshTokenManager).saveRefreshTokenInRedis(eq(USER_ID), eq(REFRESH_TOKEN), anyLong()); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์†Œ์…œ API ์‹คํŒจ โ†’ AuthUnAuthorizedFeignException") + void throwsWhenFeignFails() { + final LoginStrategy strategy = mockLoginStrategy(); + when(strategy.getUserSocialId(SOCIAL_ACCESS_TOKEN)) + .thenThrow(new AuthPlatformFeignException("KAKAO_ERROR")); + + assertThatThrownBy(() -> authService.signUp("ํ™๊ธธ๋™", 25, Gender.MALE, "test@email.com", SocialType.KAKAO, + SOCIAL_ACCESS_TOKEN)) + .isInstanceOf(AuthUnAuthorizedFeignException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์ค‘๋ณต ์‚ฌ์šฉ์ž โ†’ AuthUnAuthorizedException") + void throwsWhenDuplicate() { + final LoginStrategy strategy = mockLoginStrategy(); + when(strategy.getUserSocialId(SOCIAL_ACCESS_TOKEN)).thenReturn(SOCIAL_ID); + doThrow(new UserDuplicateException()).when(userRetriever).validDuplicatedUserBySocial(SocialType.KAKAO, + SOCIAL_ID); + + assertThatThrownBy(() -> authService.signUp("ํ™๊ธธ๋™", 25, Gender.MALE, "test@email.com", SocialType.KAKAO, + SOCIAL_ACCESS_TOKEN)) + .isInstanceOf(AuthUnAuthorizedException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: Redis ์ €์žฅ ์‹คํŒจ โ†’ AuthRedisException") + void throwsWhenRedisFails() { + final LoginStrategy strategy = mockLoginStrategy(); + when(strategy.getUserSocialId(SOCIAL_ACCESS_TOKEN)).thenReturn(SOCIAL_ID); + doNothing().when(userRetriever).validDuplicatedUserBySocial(SocialType.KAKAO, SOCIAL_ID); + + final UserEntity savedEntity = UserEntity.create("ํ™๊ธธ๋™", Gender.MALE, 25, "test@email.com", SOCIAL_ID, + SocialType.KAKAO, UserRole.USER); + ReflectionTestUtils.setField(savedEntity, "userId", USER_ID); + when(userSaver.saveUser(any(UserEntity.class))).thenReturn(savedEntity); + when(jwtProvider.issueToken(USER_ID, UserRole.USER)).thenReturn(createToken()); + when(jwtProperties.refreshTokenExpirationTime()).thenReturn(604800000L); + doThrow(new AuthRTException()).when(refreshTokenManager).saveRefreshTokenInRedis(eq(USER_ID), + eq(REFRESH_TOKEN), anyLong()); + + assertThatThrownBy(() -> authService.signUp("ํ™๊ธธ๋™", 25, Gender.MALE, "test@email.com", SocialType.KAKAO, + SOCIAL_ACCESS_TOKEN)) + .isInstanceOf(AuthRedisException.class); + } + } + + @Nested + @DisplayName("login") + class LoginTest { + + @Test + @DisplayName("์ •์ƒ: ๋กœ๊ทธ์ธ โ†’ ํ† ํฐ ๋ฐ˜ํ™˜") + void success() { + final LoginStrategy strategy = mockLoginStrategy(); + final UserSocialInfoDto socialInfo = UserSocialInfoDto.of(SocialType.KAKAO, SOCIAL_ID, SOCIAL_ACCESS_TOKEN); + when(strategy.getUserSocialInfo(AUTH_CODE, REDIRECT_URL)).thenReturn(socialInfo); + when(userRetriever.getUserBySocialInfo(SocialType.KAKAO, SOCIAL_ID)).thenReturn(createUser()); + when(jwtProvider.issueToken(USER_ID, UserRole.USER)).thenReturn(createToken()); + when(jwtProperties.refreshTokenExpirationTime()).thenReturn(604800000L); + + final TokenDto result = authService.login(SocialType.KAKAO, AUTH_CODE, REDIRECT_URL); + + assertThat(result.accessToken()).isEqualTo(ACCESS_TOKEN); + assertThat(result.refreshToken()).isEqualTo(REFRESH_TOKEN); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์†Œ์…œ API ์‹คํŒจ โ†’ AuthUnAuthorizedFeignException") + void throwsWhenFeignFails() { + final LoginStrategy strategy = mockLoginStrategy(); + when(strategy.getUserSocialInfo(AUTH_CODE, REDIRECT_URL)) + .thenThrow(new AuthPlatformFeignException("KAKAO_ERROR")); + + assertThatThrownBy(() -> authService.login(SocialType.KAKAO, AUTH_CODE, REDIRECT_URL)) + .isInstanceOf(AuthUnAuthorizedFeignException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์‚ฌ์šฉ์ž ๋ฏธ์กด์žฌ โ†’ AuthSocialNotFoundApiException") + void throwsWhenUserNotFound() { + final LoginStrategy strategy = mockLoginStrategy(); + final UserSocialInfoDto socialInfo = UserSocialInfoDto.of(SocialType.KAKAO, SOCIAL_ID, SOCIAL_ACCESS_TOKEN); + when(strategy.getUserSocialInfo(AUTH_CODE, REDIRECT_URL)).thenReturn(socialInfo); + when(userRetriever.getUserBySocialInfo(SocialType.KAKAO, SOCIAL_ID)).thenThrow(new UserNotFoundException()); + + assertThatThrownBy(() -> authService.login(SocialType.KAKAO, AUTH_CODE, REDIRECT_URL)) + .isInstanceOf(AuthSocialNotFoundApiException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ๋นˆ ์†Œ์…œ ํ† ํฐ โ†’ AuthUnAuthorizedException") + void throwsWhenEmptySocialToken() { + final LoginStrategy strategy = mockLoginStrategy(); + final UserSocialInfoDto socialInfo = UserSocialInfoDto.of(SocialType.KAKAO, SOCIAL_ID, ""); + when(strategy.getUserSocialInfo(AUTH_CODE, REDIRECT_URL)).thenReturn(socialInfo); + + assertThatThrownBy(() -> authService.login(SocialType.KAKAO, AUTH_CODE, REDIRECT_URL)) + .isInstanceOf(AuthUnAuthorizedException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: Redis ์ €์žฅ ์‹คํŒจ โ†’ AuthRedisException") + void throwsWhenRedisFails() { + final LoginStrategy strategy = mockLoginStrategy(); + final UserSocialInfoDto socialInfo = UserSocialInfoDto.of(SocialType.KAKAO, SOCIAL_ID, SOCIAL_ACCESS_TOKEN); + when(strategy.getUserSocialInfo(AUTH_CODE, REDIRECT_URL)).thenReturn(socialInfo); + when(userRetriever.getUserBySocialInfo(SocialType.KAKAO, SOCIAL_ID)).thenReturn(createUser()); + when(jwtProvider.issueToken(USER_ID, UserRole.USER)).thenReturn(createToken()); + when(jwtProperties.refreshTokenExpirationTime()).thenReturn(604800000L); + doThrow(new AuthRTException()).when(refreshTokenManager).saveRefreshTokenInRedis(eq(USER_ID), + eq(REFRESH_TOKEN), anyLong()); + + assertThatThrownBy(() -> authService.login(SocialType.KAKAO, AUTH_CODE, REDIRECT_URL)) + .isInstanceOf(AuthRedisException.class); + } + } + + @Nested + @DisplayName("reissue") + class ReissueTest { + + @Test + @DisplayName("์ •์ƒ: ํ† ํฐ ์žฌ๋ฐœ๊ธ‰") + void success() { + when(jwtProvider.extractUserIdFromToken(REFRESH_TOKEN)).thenReturn(USER_ID); + when(userRetriever.findUserById(USER_ID)).thenReturn(createUser()); + when(jwtProvider.issueToken(USER_ID, UserRole.USER)).thenReturn(Token.of("new-access", "new-refresh")); + when(jwtProperties.refreshTokenExpirationTime()).thenReturn(604800000L); + + final TokenDto result = authService.reissue(REFRESH_TOKEN); + + assertThat(result.accessToken()).isEqualTo("new-access"); + assertThat(result.refreshToken()).isEqualTo("new-refresh"); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์ž˜๋ชป๋œ RF ํ† ํฐ โ†’ AuthUnAuthorizedException") + void throwsWhenWrongToken() { + when(jwtProvider.extractUserIdFromToken(REFRESH_TOKEN)).thenThrow(new AuthWrongJwtException()); + + assertThatThrownBy(() -> authService.reissue(REFRESH_TOKEN)) + .isInstanceOf(AuthUnAuthorizedException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ๋งŒ๋ฃŒ๋œ RF ํ† ํฐ โ†’ AuthUnAuthorizedException") + void throwsWhenExpired() { + when(jwtProvider.extractUserIdFromToken(REFRESH_TOKEN)).thenThrow(new AuthExpiredJwtException()); + + assertThatThrownBy(() -> authService.reissue(REFRESH_TOKEN)) + .isInstanceOf(AuthUnAuthorizedException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: Redis ์˜ค๋ฅ˜ โ†’ AuthUnAuthorizedException") + void throwsWhenRedisError() { + when(jwtProvider.extractUserIdFromToken(REFRESH_TOKEN)).thenReturn(USER_ID); + doThrow(new AuthRTException()).when(refreshTokenManager).validateSameWithOriginalRefreshToken(USER_ID, + REFRESH_TOKEN); + + assertThatThrownBy(() -> authService.reissue(REFRESH_TOKEN)) + .isInstanceOf(AuthUnAuthorizedException.class); + } + } + + @Nested + @DisplayName("logout") + class LogoutTest { + + @Test + @DisplayName("์ •์ƒ: ๋กœ๊ทธ์•„์›ƒ โ†’ RF ํ† ํฐ ์‚ญ์ œ") + void success() { + doNothing().when(refreshTokenManager).validateSameWithOriginalRefreshToken(USER_ID, REFRESH_TOKEN); + + authService.logout(USER_ID, REFRESH_TOKEN); + + verify(refreshTokenManager).deleteRefreshToken(USER_ID); + } + + @Test + @DisplayName("์˜ˆ์™ธ: RT ๋ฏธ์กด์žฌ โ†’ AuthUnAuthorizedException") + void throwsWhenRTNotFound() { + doThrow(new AuthRTNotFoundException()).when(refreshTokenManager) + .validateSameWithOriginalRefreshToken(USER_ID, REFRESH_TOKEN); + + assertThatThrownBy(() -> authService.logout(USER_ID, REFRESH_TOKEN)) + .isInstanceOf(AuthUnAuthorizedException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: Redis ์˜ค๋ฅ˜ โ†’ AuthRedisException") + void throwsWhenRedisError() { + doNothing().when(refreshTokenManager).validateSameWithOriginalRefreshToken(USER_ID, REFRESH_TOKEN); + doThrow(mock(DataAccessException.class)).when(refreshTokenManager).deleteRefreshToken(USER_ID); + + assertThatThrownBy(() -> authService.logout(USER_ID, REFRESH_TOKEN)) + .isInstanceOf(AuthRedisException.class); + } + } +} diff --git a/src/test/java/com/permitseoul/permitserver/domain/coupon/api/service/CouponServiceTest.java b/src/test/java/com/permitseoul/permitserver/domain/coupon/api/service/CouponServiceTest.java new file mode 100644 index 0000000..ed13f11 --- /dev/null +++ b/src/test/java/com/permitseoul/permitserver/domain/coupon/api/service/CouponServiceTest.java @@ -0,0 +1,53 @@ +package com.permitseoul.permitserver.domain.coupon.api.service; + +import com.permitseoul.permitserver.domain.coupon.api.dto.CouponValidateResponse; +import com.permitseoul.permitserver.domain.coupon.api.exception.NotFoundCouponException; +import com.permitseoul.permitserver.domain.coupon.core.component.CouponRetriever; +import com.permitseoul.permitserver.domain.coupon.core.domain.Coupon; +import com.permitseoul.permitserver.domain.coupon.core.exception.CouponNotfoundException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@DisplayName("CouponService ํ…Œ์ŠคํŠธ") +class CouponServiceTest { + + @Mock + private CouponRetriever couponRetriever; + @InjectMocks + private CouponService couponService; + + private static final String COUPON_CODE = "COUPON-2026"; + private static final long EVENT_ID = 100L; + + @Test + @DisplayName("์ •์ƒ: ์œ ํšจํ•œ ์ฟ ํฐ ์ฝ”๋“œ ๊ฒ€์ฆ โ†’ ํ• ์ธ์œจ ๋ฐ˜ํ™˜") + void validateCouponSuccess() { + final Coupon coupon = new Coupon(1L, EVENT_ID, COUPON_CODE, 10, "ํ…Œ์ŠคํŠธ ์ฟ ํฐ", false, null, LocalDateTime.now()); + when(couponRetriever.findValidCouponByCodeAndEvent(COUPON_CODE, EVENT_ID)).thenReturn(coupon); + + final CouponValidateResponse result = couponService.validateCoupon(COUPON_CODE, EVENT_ID); + + assertThat(result.discountRate()).isEqualTo(10); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์ฟ ํฐ ์ฝ”๋“œ ๋ฏธ์กด์žฌ โ†’ NotFoundCouponException") + void throwsWhenCouponNotFound() { + when(couponRetriever.findValidCouponByCodeAndEvent(COUPON_CODE, EVENT_ID)) + .thenThrow(new CouponNotfoundException()); + + assertThatThrownBy(() -> couponService.validateCoupon(COUPON_CODE, EVENT_ID)) + .isInstanceOf(NotFoundCouponException.class); + } +} diff --git a/src/test/java/com/permitseoul/permitserver/domain/event/api/service/EventServiceTest.java b/src/test/java/com/permitseoul/permitserver/domain/event/api/service/EventServiceTest.java new file mode 100644 index 0000000..08a5fa6 --- /dev/null +++ b/src/test/java/com/permitseoul/permitserver/domain/event/api/service/EventServiceTest.java @@ -0,0 +1,153 @@ +package com.permitseoul.permitserver.domain.event.api.service; + +import com.permitseoul.permitserver.domain.event.api.dto.EventAllResponse; +import com.permitseoul.permitserver.domain.event.api.dto.EventDetailResponse; +import com.permitseoul.permitserver.domain.event.api.exception.NotFoundEventException; +import com.permitseoul.permitserver.domain.event.core.component.EventRetriever; +import com.permitseoul.permitserver.domain.event.core.domain.Event; +import com.permitseoul.permitserver.domain.event.core.domain.EventType; +import com.permitseoul.permitserver.domain.event.core.exception.EventNotfoundException; +import com.permitseoul.permitserver.domain.eventimage.core.component.EventImageRetriever; +import com.permitseoul.permitserver.domain.eventimage.core.domain.EventImage; +import com.permitseoul.permitserver.domain.eventimage.core.exception.EventImageNotFoundException; +import com.permitseoul.permitserver.global.util.SecureUrlUtil; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@DisplayName("EventService ํ…Œ์ŠคํŠธ") +class EventServiceTest { + + @Mock + private EventRetriever eventRetriever; + @Mock + private EventImageRetriever eventImageRetriever; + @Mock + private SecureUrlUtil secureUrlUtil; + @InjectMocks + private EventService eventService; + + private static final long EVENT_ID = 100L; + private static final LocalDateTime NOW = LocalDateTime.of(2026, 2, 18, 14, 0); + + private Event createEvent(long id, String name, EventType type) { + return new Event(id, name, type, NOW.minusDays(1), NOW.plusDays(1), + "์„œ์šธ", "[POP] ์•„ํ‹ฐ์ŠคํŠธA, ์•„ํ‹ฐ์ŠคํŠธB", "์ƒ์„ธ", 0, NOW.minusDays(7), NOW.plusDays(7), "CHECK-CODE"); + } + + @Nested + @DisplayName("getAllVisibleEvents") + class GetAllVisibleEventsTest { + + @Test + @DisplayName("์ •์ƒ: ์ด๋ฒคํŠธ ๋ชฉ๋ก ์กฐํšŒ โ†’ ํƒ€์ž…๋ณ„ ๋ถ„๋ฅ˜") + void success() { + final List eventList = List.of( + createEvent(1L, "ํผ๋ฐ‹ ์ด๋ฒคํŠธ", EventType.PERMIT), + createEvent(2L, "์ฒœ์žฅ ์ด๋ฒคํŠธ", EventType.CEILING)); + final Map thumbnailMap = Map.of( + 1L, new EventImage(10L, 1L, "https://img.com/1.png", 1), + 2L, new EventImage(11L, 2L, "https://img.com/2.png", 1)); + when(eventRetriever.findAllVisibleEvents(any(LocalDateTime.class))).thenReturn(eventList); + when(eventImageRetriever.findAllThumbnailsByEventIds(anyList())).thenReturn(thumbnailMap); + when(secureUrlUtil.encode(1L)).thenReturn("encoded-1"); + when(secureUrlUtil.encode(2L)).thenReturn("encoded-2"); + + final EventAllResponse result = eventService.getAllVisibleEvents(); + + assertThat(result.permit()).hasSize(1); + assertThat(result.permit().get(0).eventName()).isEqualTo("ํผ๋ฐ‹ ์ด๋ฒคํŠธ"); + assertThat(result.ceilingService()).hasSize(1); + assertThat(result.ceilingService().get(0).eventName()).isEqualTo("์ฒœ์žฅ ์ด๋ฒคํŠธ"); + assertThat(result.festival()).isEmpty(); + } + + @Test + @DisplayName("์ •์ƒ: ์ด๋ฒคํŠธ ๋นˆ ๋ชฉ๋ก โ†’ ๋นˆ ์‘๋‹ต") + void emptyList() { + when(eventRetriever.findAllVisibleEvents(any(LocalDateTime.class))).thenReturn(List.of()); + + final EventAllResponse result = eventService.getAllVisibleEvents(); + + assertThat(result.permit()).isEmpty(); + assertThat(result.ceilingService()).isEmpty(); + assertThat(result.festival()).isEmpty(); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์ธ๋„ค์ผ ์ด๋ฏธ์ง€ ๋ฏธ์กด์žฌ โ†’ NotFoundEventException") + void throwsWhenImageNotFound() { + final List eventList = List.of(createEvent(1L, "์ด๋ฒคํŠธ", EventType.PERMIT)); + when(eventRetriever.findAllVisibleEvents(any(LocalDateTime.class))).thenReturn(eventList); + when(eventImageRetriever.findAllThumbnailsByEventIds(anyList())) + .thenThrow(new EventImageNotFoundException()); + + assertThatThrownBy(() -> eventService.getAllVisibleEvents()) + .isInstanceOf(NotFoundEventException.class); + } + } + + @Nested + @DisplayName("getEventDetail") + class GetEventDetailTest { + + @Test + @DisplayName("์ •์ƒ: ์ด๋ฒคํŠธ ์ƒ์„ธ ์กฐํšŒ โ†’ ๋ผ์ธ์—… ํŒŒ์‹ฑ ํฌํ•จ") + void success() { + final Event event = createEvent(EVENT_ID, "ํ…Œ์ŠคํŠธ ์ด๋ฒคํŠธ", EventType.PERMIT); + final List images = List.of( + new EventImage(10L, EVENT_ID, "https://img.com/detail1.png", 1), + new EventImage(11L, EVENT_ID, "https://img.com/detail2.png", 2)); + when(eventRetriever.findEventById(EVENT_ID)).thenReturn(event); + when(eventImageRetriever.findAllEventImagesByEventId(EVENT_ID)).thenReturn(images); + + final EventDetailResponse result = eventService.getEventDetail(EVENT_ID); + + assertThat(result.eventName()).isEqualTo("ํ…Œ์ŠคํŠธ ์ด๋ฒคํŠธ"); + assertThat(result.venue()).isEqualTo("์„œ์šธ"); + assertThat(result.minAge()).isZero(); + assertThat(result.details()).isEqualTo("์ƒ์„ธ"); + assertThat(result.images()).hasSize(2); + // ๋ผ์ธ์—… ํŒŒ์‹ฑ ๊ฒ€์ฆ + assertThat(result.lineup()).hasSize(1); + assertThat(result.lineup().get(0).category()).isEqualTo("[POP]"); + assertThat(result.lineup().get(0).artists()).hasSize(2); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์ด๋ฒคํŠธ ๋ฏธ์กด์žฌ โ†’ NotFoundEventException") + void throwsWhenEventNotFound() { + when(eventRetriever.findEventById(EVENT_ID)).thenThrow(new EventNotfoundException()); + + assertThatThrownBy(() -> eventService.getEventDetail(EVENT_ID)) + .isInstanceOf(NotFoundEventException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์ด๋ฒคํŠธ ์ด๋ฏธ์ง€ ๋ฏธ์กด์žฌ โ†’ NotFoundEventException") + void throwsWhenImageNotFound() { + final Event event = createEvent(EVENT_ID, "ํ…Œ์ŠคํŠธ ์ด๋ฒคํŠธ", EventType.PERMIT); + when(eventRetriever.findEventById(EVENT_ID)).thenReturn(event); + when(eventImageRetriever.findAllEventImagesByEventId(EVENT_ID)) + .thenThrow(new EventImageNotFoundException()); + + assertThatThrownBy(() -> eventService.getEventDetail(EVENT_ID)) + .isInstanceOf(NotFoundEventException.class); + } + } +} diff --git a/src/test/java/com/permitseoul/permitserver/domain/event/core/component/EventRetrieverTest.java b/src/test/java/com/permitseoul/permitserver/domain/event/core/component/EventRetrieverTest.java new file mode 100644 index 0000000..5b73ff8 --- /dev/null +++ b/src/test/java/com/permitseoul/permitserver/domain/event/core/component/EventRetrieverTest.java @@ -0,0 +1,143 @@ +package com.permitseoul.permitserver.domain.event.core.component; + +import com.permitseoul.permitserver.domain.event.core.domain.Event; +import com.permitseoul.permitserver.domain.event.core.domain.EventType; +import com.permitseoul.permitserver.domain.event.core.domain.entity.EventEntity; +import com.permitseoul.permitserver.domain.event.core.exception.EventNotfoundException; +import com.permitseoul.permitserver.domain.event.core.repository.EventRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.BDDMockito.given; + +@DisplayName("EventRetriever ํ…Œ์ŠคํŠธ") +@ExtendWith(MockitoExtension.class) +class EventRetrieverTest { + + @Mock + private EventRepository eventRepository; + + @InjectMocks + private EventRetriever eventRetriever; + + private EventEntity createTestEntity() { + final EventEntity entity = EventEntity.create( + "2026 ์‹ ๋…„ ์ฝ˜์„œํŠธ", EventType.PERMIT, + LocalDateTime.of(2026, 1, 19, 17, 0), + LocalDateTime.of(2026, 1, 19, 21, 0), + "์„œ์šธ ์˜ฌ๋ฆผํ”ฝ๊ณต์›", "์•„ํ‹ฐ์ŠคํŠธA", "์ƒ์„ธ ์„ค๋ช…", 15, + LocalDateTime.of(2026, 1, 1, 0, 0), + LocalDateTime.of(2026, 1, 19, 17, 0), + "CHECK-2026"); + ReflectionTestUtils.setField(entity, "eventId", 100L); + return entity; + } + + @Nested + @DisplayName("findEventById ๋ฉ”์„œ๋“œ") + class FindEventById { + + @Test + @DisplayName("์กด์žฌํ•˜๋ฉด Event๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void returnsEventWhenFound() { + // given + given(eventRepository.findById(100L)).willReturn(Optional.of(createTestEntity())); + + // when + final Event result = eventRetriever.findEventById(100L); + + // then + assertThat(result.getEventId()).isEqualTo(100L); + assertThat(result.getName()).isEqualTo("2026 ์‹ ๋…„ ์ฝ˜์„œํŠธ"); + } + + @Test + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š์œผ๋ฉด EventNotfoundException์„ ๋˜์ง„๋‹ค") + void throwsExceptionWhenNotFound() { + // given + given(eventRepository.findById(999L)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> eventRetriever.findEventById(999L)) + .isInstanceOf(EventNotfoundException.class); + } + } + + @Nested + @DisplayName("findAllEventsById ๋ฉ”์„œ๋“œ") + class FindAllEventsById { + + @Test + @DisplayName("ID ๋ชฉ๋ก์œผ๋กœ ์กฐํšŒํ•˜๋ฉด Event ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void returnsEventListWhenFound() { + // given + given(eventRepository.findAllById(List.of(100L))).willReturn(List.of(createTestEntity())); + + // when + final List result = eventRetriever.findAllEventsById(List.of(100L)); + + // then + assertThat(result).hasSize(1); + assertThat(result.get(0).getEventId()).isEqualTo(100L); + } + + @Test + @DisplayName("๋นˆ ๋ฆฌ์ŠคํŠธ๋ฉด ๋นˆ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void returnsEmptyListWhenNoEvents() { + // given + given(eventRepository.findAllById(List.of(999L))).willReturn(Collections.emptyList()); + + // when + final List result = eventRetriever.findAllEventsById(List.of(999L)); + + // then + assertThat(result).isEmpty(); + } + } + + @Nested + @DisplayName("findAllVisibleEvents ๋ฉ”์„œ๋“œ") + class FindAllVisibleEvents { + + @Test + @DisplayName("ํ˜„์žฌ ์‹œ๊ฐ„์œผ๋กœ ์กฐํšŒํ•˜๋ฉด ๋ณด์ด๋Š” ์ด๋ฒคํŠธ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void returnsVisibleEventsWhenFound() { + // given + final LocalDateTime now = LocalDateTime.of(2026, 1, 10, 12, 0); + given(eventRepository.findVisibleEvents(now)).willReturn(List.of(createTestEntity())); + + // when + final List result = eventRetriever.findAllVisibleEvents(now); + + // then + assertThat(result).hasSize(1); + } + + @Test + @DisplayName("๋ณด์ด๋Š” ์ด๋ฒคํŠธ๊ฐ€ ์—†์œผ๋ฉด ๋นˆ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void returnsEmptyListWhenNoVisibleEvents() { + // given + final LocalDateTime now = LocalDateTime.of(2027, 1, 1, 0, 0); + given(eventRepository.findVisibleEvents(now)).willReturn(Collections.emptyList()); + + // when + final List result = eventRetriever.findAllVisibleEvents(now); + + // then + assertThat(result).isEmpty(); + } + } +} diff --git a/src/test/java/com/permitseoul/permitserver/domain/event/core/domain/EventEntityTest.java b/src/test/java/com/permitseoul/permitserver/domain/event/core/domain/EventEntityTest.java new file mode 100644 index 0000000..8adb52d --- /dev/null +++ b/src/test/java/com/permitseoul/permitserver/domain/event/core/domain/EventEntityTest.java @@ -0,0 +1,176 @@ +package com.permitseoul.permitserver.domain.event.core.domain; + +import com.permitseoul.permitserver.domain.event.core.domain.entity.EventEntity; +import com.permitseoul.permitserver.domain.event.core.exception.EventIllegalArgumentException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayName("Event & EventEntity ํ…Œ์ŠคํŠธ") +class EventEntityTest { + + private static final String NAME = "2026 ์‹ ๋…„ ์ฝ˜์„œํŠธ"; + private static final EventType EVENT_TYPE = EventType.PERMIT; + private static final LocalDateTime START_AT = LocalDateTime.of(2026, 1, 19, 17, 0); + private static final LocalDateTime END_AT = LocalDateTime.of(2026, 1, 19, 21, 0); + private static final String VENUE = "์„œ์šธ ์˜ฌ๋ฆผํ”ฝ๊ณต์›"; + private static final String LINE_UP = "์•„ํ‹ฐ์ŠคํŠธA, ์•„ํ‹ฐ์ŠคํŠธB"; + private static final String DETAILS = "2026๋…„ ์‹ ๋…„ ํŠน๋ณ„ ๊ณต์—ฐ"; + private static final int MIN_AGE = 15; + private static final LocalDateTime VISIBLE_START_AT = LocalDateTime.of(2026, 1, 1, 0, 0); + private static final LocalDateTime VISIBLE_END_AT = LocalDateTime.of(2026, 1, 19, 17, 0); + private static final String TICKET_CHECK_CODE = "CHECK-2026"; + + private EventEntity createTestEntity() { + return EventEntity.create(NAME, EVENT_TYPE, START_AT, END_AT, VENUE, + LINE_UP, DETAILS, MIN_AGE, VISIBLE_START_AT, VISIBLE_END_AT, TICKET_CHECK_CODE); + } + + @Nested + @DisplayName("EventEntity.create ๋ฉ”์„œ๋“œ") + class Create { + + @Test + @DisplayName("์ •์ƒ์ ์ธ ๊ฐ’์œผ๋กœ EventEntity๋ฅผ ์ƒ์„ฑํ•œ๋‹ค") + void createsEventEntitySuccessfully() { + // when + final EventEntity entity = createTestEntity(); + + // then + assertThat(entity.getName()).isEqualTo(NAME); + assertThat(entity.getEventType()).isEqualTo(EVENT_TYPE); + assertThat(entity.getStartAt()).isEqualTo(START_AT); + assertThat(entity.getEndAt()).isEqualTo(END_AT); + assertThat(entity.getVenue()).isEqualTo(VENUE); + assertThat(entity.getLineUp()).isEqualTo(LINE_UP); + assertThat(entity.getDetails()).isEqualTo(DETAILS); + assertThat(entity.getMinAge()).isEqualTo(MIN_AGE); + assertThat(entity.getVisibleStartAt()).isEqualTo(VISIBLE_START_AT); + assertThat(entity.getVisibleEndAt()).isEqualTo(VISIBLE_END_AT); + assertThat(entity.getTicketCheckCode()).isEqualTo(TICKET_CHECK_CODE); + } + + @Test + @DisplayName("์ƒ์„ฑ ์งํ›„ eventId๋Š” null์ด๋‹ค (@GeneratedValue)") + void eventIdIsNullAfterCreate() { + // when + final EventEntity entity = createTestEntity(); + + // then + assertThat(entity.getEventId()).isNull(); + } + } + + @Nested + @DisplayName("updateEvent ๋ฉ”์„œ๋“œ") + class UpdateEvent { + + @Test + @DisplayName("์ •์ƒ์ ์ธ ๊ฐ’์œผ๋กœ ์ด๋ฒคํŠธ๋ฅผ ์—…๋ฐ์ดํŠธํ•œ๋‹ค") + void updatesEventSuccessfully() { + // given + final EventEntity entity = createTestEntity(); + final String newName = "์ˆ˜์ •๋œ ์ฝ˜์„œํŠธ"; + final LocalDateTime newStart = LocalDateTime.of(2026, 2, 1, 18, 0); + final LocalDateTime newEnd = LocalDateTime.of(2026, 2, 1, 22, 0); + final LocalDateTime newVisibleStart = LocalDateTime.of(2026, 1, 15, 0, 0); + final LocalDateTime newVisibleEnd = LocalDateTime.of(2026, 2, 1, 18, 0); + + // when + entity.updateEvent(newName, EventType.CEILING, newStart, newEnd, + "์ƒˆ๋กœ์šด ์žฅ์†Œ", "์ƒˆ ๋ผ์ธ์—…", "์ƒˆ ์ƒ์„ธ", 18, + newVisibleStart, newVisibleEnd, "NEW-CHECK"); + + // then + assertThat(entity.getName()).isEqualTo(newName); + assertThat(entity.getEventType()).isEqualTo(EventType.CEILING); + assertThat(entity.getStartAt()).isEqualTo(newStart); + assertThat(entity.getEndAt()).isEqualTo(newEnd); + assertThat(entity.getMinAge()).isEqualTo(18); + assertThat(entity.getTicketCheckCode()).isEqualTo("NEW-CHECK"); + } + + @Test + @DisplayName("startAt์ด endAt๋ณด๋‹ค ์ดํ›„์ด๋ฉด EventIllegalArgumentException์„ ๋˜์ง„๋‹ค") + void throwsExceptionWhenStartAfterEnd() { + // given + final EventEntity entity = createTestEntity(); + final LocalDateTime invalidStart = LocalDateTime.of(2026, 2, 2, 0, 0); + final LocalDateTime invalidEnd = LocalDateTime.of(2026, 2, 1, 0, 0); + + // when & then + assertThatThrownBy(() -> entity.updateEvent(NAME, EVENT_TYPE, + invalidStart, invalidEnd, VENUE, LINE_UP, DETAILS, MIN_AGE, + VISIBLE_START_AT, VISIBLE_END_AT, TICKET_CHECK_CODE)) + .isInstanceOf(EventIllegalArgumentException.class); + } + + @Test + @DisplayName("visibleStartAt์ด visibleEndAt๋ณด๋‹ค ์ดํ›„์ด๋ฉด EventIllegalArgumentException์„ ๋˜์ง„๋‹ค") + void throwsExceptionWhenVisibleStartAfterVisibleEnd() { + // given + final EventEntity entity = createTestEntity(); + final LocalDateTime invalidVisibleStart = LocalDateTime.of(2026, 2, 2, 0, 0); + final LocalDateTime invalidVisibleEnd = LocalDateTime.of(2026, 1, 1, 0, 0); + + // when & then + assertThatThrownBy(() -> entity.updateEvent(NAME, EVENT_TYPE, + START_AT, END_AT, VENUE, LINE_UP, DETAILS, MIN_AGE, + invalidVisibleStart, invalidVisibleEnd, TICKET_CHECK_CODE)) + .isInstanceOf(EventIllegalArgumentException.class); + } + + @Test + @DisplayName("startAt๊ณผ endAt์ด ๊ฐ™์œผ๋ฉด ์ •์ƒ ๋™์ž‘ํ•œ๋‹ค (๊ฒฝ๊ณ„๊ฐ’)") + void allowsSameStartAndEnd() { + // given + final EventEntity entity = createTestEntity(); + final LocalDateTime sameTime = LocalDateTime.of(2026, 2, 1, 18, 0); + + // when + entity.updateEvent(NAME, EVENT_TYPE, sameTime, sameTime, + VENUE, LINE_UP, DETAILS, MIN_AGE, + VISIBLE_START_AT, VISIBLE_END_AT, TICKET_CHECK_CODE); + + // then + assertThat(entity.getStartAt()).isEqualTo(sameTime); + assertThat(entity.getEndAt()).isEqualTo(sameTime); + } + } + + @Nested + @DisplayName("Event.fromEntity ๋ฉ”์„œ๋“œ") + class FromEntity { + + @Test + @DisplayName("Entity์˜ ๋ชจ๋“  ํ•„๋“œ๊ฐ€ Domain ๊ฐ์ฒด๋กœ ์ •ํ™•ํžˆ ๋งคํ•‘๋œ๋‹ค") + void mapsAllFieldsCorrectly() { + // given + final EventEntity entity = createTestEntity(); + ReflectionTestUtils.setField(entity, "eventId", 100L); + + // when + final Event event = Event.fromEntity(entity); + + // then + assertThat(event.getEventId()).isEqualTo(100L); + assertThat(event.getName()).isEqualTo(NAME); + assertThat(event.getEventType()).isEqualTo(EVENT_TYPE); + assertThat(event.getStartAt()).isEqualTo(START_AT); + assertThat(event.getEndAt()).isEqualTo(END_AT); + assertThat(event.getVenue()).isEqualTo(VENUE); + assertThat(event.getLineUp()).isEqualTo(LINE_UP); + assertThat(event.getDetails()).isEqualTo(DETAILS); + assertThat(event.getMinAge()).isEqualTo(MIN_AGE); + assertThat(event.getVisibleStartAt()).isEqualTo(VISIBLE_START_AT); + assertThat(event.getVisibleEndAt()).isEqualTo(VISIBLE_END_AT); + assertThat(event.getTicketCheckCode()).isEqualTo(TICKET_CHECK_CODE); + } + } +} diff --git a/src/test/java/com/permitseoul/permitserver/domain/event/core/domain/EventTypeTest.java b/src/test/java/com/permitseoul/permitserver/domain/event/core/domain/EventTypeTest.java new file mode 100644 index 0000000..3a33444 --- /dev/null +++ b/src/test/java/com/permitseoul/permitserver/domain/event/core/domain/EventTypeTest.java @@ -0,0 +1,47 @@ +package com.permitseoul.permitserver.domain.event.core.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("EventType ํ…Œ์ŠคํŠธ") +class EventTypeTest { + + @Nested + @DisplayName("์—ด๊ฑฐ๊ฐ’ ๊ธฐ๋ณธ ๊ฒ€์ฆ") + class EnumBasics { + + @Test + @DisplayName("์—ด๊ฑฐ๊ฐ’์€ 3๊ฐœ์ด๋‹ค") + void hasThreeValues() { + assertThat(EventType.values()).hasSize(3); + } + } + + @Nested + @DisplayName("displayName ํ•„๋“œ ๊ฒ€์ฆ") + class DisplayNameField { + + @ParameterizedTest(name = "{0} โ†’ \"{1}\"") + @CsvSource({ + "PERMIT, PERMIT", + "CEILING, ceiling service", + "OLYMPAN, Olympan" + }) + @DisplayName("๊ฐ ์ด๋ฒคํŠธ ํƒ€์ž…์˜ displayName์ด ์˜ฌ๋ฐ”๋ฅด๋‹ค") + void hasCorrectDisplayName(final String enumName, final String expectedDisplayName) { + // given + final EventType eventType = EventType.valueOf(enumName); + + // when + final String displayName = eventType.getDisplayName(); + + // then + assertThat(displayName).isEqualTo(expectedDisplayName); + } + } +} diff --git a/src/test/java/com/permitseoul/permitserver/domain/eventtimetable/timetable/api/service/TimetableServiceTest.java b/src/test/java/com/permitseoul/permitserver/domain/eventtimetable/timetable/api/service/TimetableServiceTest.java new file mode 100644 index 0000000..8ec50f1 --- /dev/null +++ b/src/test/java/com/permitseoul/permitserver/domain/eventtimetable/timetable/api/service/TimetableServiceTest.java @@ -0,0 +1,283 @@ +package com.permitseoul.permitserver.domain.eventtimetable.timetable.api.service; + +import com.permitseoul.permitserver.domain.event.core.component.EventRetriever; +import com.permitseoul.permitserver.domain.event.core.domain.Event; +import com.permitseoul.permitserver.domain.event.core.domain.EventType; +import com.permitseoul.permitserver.domain.event.core.exception.EventNotfoundException; +import com.permitseoul.permitserver.domain.eventtimetable.block.core.component.AdminTimetableBlockRetriever; +import com.permitseoul.permitserver.domain.eventtimetable.block.core.domain.TimetableBlock; +import com.permitseoul.permitserver.domain.eventtimetable.block.core.exception.TimetableBlockNotfoundException; +import com.permitseoul.permitserver.domain.eventtimetable.blockmedia.component.TimetableBlockMediaRetriever; +import com.permitseoul.permitserver.domain.eventtimetable.blockmedia.domain.TimetableBlockMedia; +import com.permitseoul.permitserver.domain.eventtimetable.category.core.component.TimetableCategoryRetriever; +import com.permitseoul.permitserver.domain.eventtimetable.category.core.domain.TimetableCategory; +import com.permitseoul.permitserver.domain.eventtimetable.category.core.exception.TimetableCategoryNotfoundException; +import com.permitseoul.permitserver.domain.eventtimetable.stage.core.component.TimetableStageRetriever; +import com.permitseoul.permitserver.domain.eventtimetable.stage.core.domain.TimetableStage; +import com.permitseoul.permitserver.domain.eventtimetable.stage.core.exception.TimetableStageNotFoundException; +import com.permitseoul.permitserver.domain.eventtimetable.timetable.api.dto.TimetableDetailResponse; +import com.permitseoul.permitserver.domain.eventtimetable.timetable.api.dto.TimetableResponse; +import com.permitseoul.permitserver.domain.eventtimetable.timetable.api.exception.NotfoundTimetableException; +import com.permitseoul.permitserver.domain.eventtimetable.timetable.core.component.TimetableRetriever; +import com.permitseoul.permitserver.domain.eventtimetable.timetable.core.domain.Timetable; +import com.permitseoul.permitserver.domain.eventtimetable.timetable.core.exception.TimetableNotFoundException; +import com.permitseoul.permitserver.domain.eventtimetable.userlike.core.component.TimetableUserLikeRetriever; +import com.permitseoul.permitserver.global.util.SecureUrlUtil; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@DisplayName("TimetableService ํ…Œ์ŠคํŠธ") +class TimetableServiceTest { + + @Mock + private TimetableRetriever timetableRetriever; + @Mock + private TimetableStageRetriever timetableStageRetriever; + @Mock + private TimetableCategoryRetriever timetableCategoryRetriever; + @Mock + private AdminTimetableBlockRetriever adminTimetableBlockRetriever; + @Mock + private TimetableBlockMediaRetriever timetableBlockMediaRetriever; + @Mock + private TimetableUserLikeRetriever timetableUserLikeRetriever; + @Mock + private EventRetriever eventRetriever; + @Mock + private SecureUrlUtil secureUrlUtil; + @InjectMocks + private TimetableService timetableService; + + private static final long EVENT_ID = 100L; + private static final long TIMETABLE_ID = 200L; + private static final long BLOCK_ID = 300L; + private static final long USER_ID = 1L; + private static final LocalDateTime NOW = LocalDateTime.of(2026, 2, 18, 14, 0); + + private Event createEvent() { + return new Event(EVENT_ID, "ํ…Œ์ŠคํŠธ ์ด๋ฒคํŠธ", EventType.PERMIT, NOW.minusDays(1), NOW.plusDays(1), + "์„œ์šธ", "", "์ƒ์„ธ", 0, NOW.minusDays(7), NOW.plusDays(7), "CHECK-CODE"); + } + + private Timetable createTimetable() { + return new Timetable(TIMETABLE_ID, EVENT_ID, NOW.minusDays(1), NOW.plusDays(1), "notion-tt", "notion-stage", + "notion-cat"); + } + + private TimetableStage createStage() { + return new TimetableStage(1L, TIMETABLE_ID, "๋ฉ”์ธ ์Šคํ…Œ์ด์ง€", 1, "stage-notion-1"); + } + + private TimetableCategory createCategory() { + return new TimetableCategory(1L, TIMETABLE_ID, "POP", "#FF0000", "#CC0000", "cat-notion-1"); + } + + private TimetableBlock createBlock() { + return new TimetableBlock(BLOCK_ID, TIMETABLE_ID, "cat-notion-1", "stage-notion-1", + NOW, NOW.plusHours(1), "๊ณต์—ฐ A", "์•„ํ‹ฐ์ŠคํŠธ", "๊ณต์—ฐ ์ •๋ณด", "https://link.com", "block-notion-1"); + } + + @Nested + @DisplayName("getEventTimetable") + class GetEventTimetableTest { + + @Test + @DisplayName("์ •์ƒ: userId ์žˆ์Œ โ†’ ์ข‹์•„์š” ํฌํ•จ ํƒ€์ž„ํ…Œ์ด๋ธ” ์กฐํšŒ") + void successWithUserId() { + when(eventRetriever.findEventById(EVENT_ID)).thenReturn(createEvent()); + when(timetableRetriever.getTimetableByEventId(EVENT_ID)).thenReturn(createTimetable()); + when(timetableStageRetriever.findTimetableStageListByTimetableId(TIMETABLE_ID)) + .thenReturn(List.of(createStage())); + when(timetableCategoryRetriever.findAllTimetableCategory(TIMETABLE_ID)) + .thenReturn(List.of(createCategory())); + when(adminTimetableBlockRetriever.findAllTimetableBlockByTimetableId(TIMETABLE_ID)) + .thenReturn(List.of(createBlock())); + when(timetableUserLikeRetriever.findLikedBlockIdsIn(eq(USER_ID), anyList())).thenReturn(List.of(BLOCK_ID)); + when(secureUrlUtil.encode(BLOCK_ID)).thenReturn("encoded-300"); + + final TimetableResponse result = timetableService.getEventTimetable(EVENT_ID, USER_ID); + + assertThat(result.eventName()).isEqualTo("ํ…Œ์ŠคํŠธ ์ด๋ฒคํŠธ"); + assertThat(result.stages()).hasSize(1); + assertThat(result.blocks()).hasSize(1); + assertThat(result.blocks().get(0).isUserLiked()).isTrue(); + } + + @Test + @DisplayName("์ •์ƒ: userId null โ†’ ์ข‹์•„์š” ์—†์ด ์กฐํšŒ") + void successWithoutUserId() { + when(eventRetriever.findEventById(EVENT_ID)).thenReturn(createEvent()); + when(timetableRetriever.getTimetableByEventId(EVENT_ID)).thenReturn(createTimetable()); + when(timetableStageRetriever.findTimetableStageListByTimetableId(TIMETABLE_ID)) + .thenReturn(List.of(createStage())); + when(timetableCategoryRetriever.findAllTimetableCategory(TIMETABLE_ID)) + .thenReturn(List.of(createCategory())); + when(adminTimetableBlockRetriever.findAllTimetableBlockByTimetableId(TIMETABLE_ID)) + .thenReturn(List.of(createBlock())); + when(secureUrlUtil.encode(BLOCK_ID)).thenReturn("encoded-300"); + + final TimetableResponse result = timetableService.getEventTimetable(EVENT_ID, null); + + assertThat(result.blocks().get(0).isUserLiked()).isFalse(); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์ด๋ฒคํŠธ ๋ฏธ์กด์žฌ โ†’ NotfoundTimetableException") + void throwsWhenEventNotFound() { + when(eventRetriever.findEventById(EVENT_ID)).thenThrow(new EventNotfoundException()); + + assertThatThrownBy(() -> timetableService.getEventTimetable(EVENT_ID, USER_ID)) + .isInstanceOf(NotfoundTimetableException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ํƒ€์ž„ํ…Œ์ด๋ธ” ๋ฏธ์กด์žฌ โ†’ NotfoundTimetableException") + void throwsWhenTimetableNotFound() { + when(eventRetriever.findEventById(EVENT_ID)).thenReturn(createEvent()); + when(timetableRetriever.getTimetableByEventId(EVENT_ID)).thenThrow(new TimetableNotFoundException()); + + assertThatThrownBy(() -> timetableService.getEventTimetable(EVENT_ID, USER_ID)) + .isInstanceOf(NotfoundTimetableException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์Šคํ…Œ์ด์ง€ ๋ฏธ์กด์žฌ โ†’ NotfoundTimetableException") + void throwsWhenStageNotFound() { + when(eventRetriever.findEventById(EVENT_ID)).thenReturn(createEvent()); + when(timetableRetriever.getTimetableByEventId(EVENT_ID)).thenReturn(createTimetable()); + when(timetableStageRetriever.findTimetableStageListByTimetableId(TIMETABLE_ID)) + .thenThrow(new TimetableStageNotFoundException()); + + assertThatThrownBy(() -> timetableService.getEventTimetable(EVENT_ID, USER_ID)) + .isInstanceOf(NotfoundTimetableException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์นดํ…Œ๊ณ ๋ฆฌ ๋ฏธ์กด์žฌ โ†’ NotfoundTimetableException") + void throwsWhenCategoryNotFound() { + when(eventRetriever.findEventById(EVENT_ID)).thenReturn(createEvent()); + when(timetableRetriever.getTimetableByEventId(EVENT_ID)).thenReturn(createTimetable()); + when(timetableStageRetriever.findTimetableStageListByTimetableId(TIMETABLE_ID)) + .thenReturn(List.of(createStage())); + when(timetableCategoryRetriever.findAllTimetableCategory(TIMETABLE_ID)) + .thenThrow(new TimetableCategoryNotfoundException()); + + assertThatThrownBy(() -> timetableService.getEventTimetable(EVENT_ID, USER_ID)) + .isInstanceOf(NotfoundTimetableException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ๋ธ”๋ก ๋ฏธ์กด์žฌ โ†’ NotfoundTimetableException") + void throwsWhenBlockNotFound() { + when(eventRetriever.findEventById(EVENT_ID)).thenReturn(createEvent()); + when(timetableRetriever.getTimetableByEventId(EVENT_ID)).thenReturn(createTimetable()); + when(timetableStageRetriever.findTimetableStageListByTimetableId(TIMETABLE_ID)) + .thenReturn(List.of(createStage())); + when(timetableCategoryRetriever.findAllTimetableCategory(TIMETABLE_ID)) + .thenReturn(List.of(createCategory())); + when(adminTimetableBlockRetriever.findAllTimetableBlockByTimetableId(TIMETABLE_ID)) + .thenThrow(new TimetableBlockNotfoundException()); + + assertThatThrownBy(() -> timetableService.getEventTimetable(EVENT_ID, USER_ID)) + .isInstanceOf(NotfoundTimetableException.class); + } + } + + @Nested + @DisplayName("getEventTimetableDetail") + class GetEventTimetableDetailTest { + + @Test + @DisplayName("์ •์ƒ: userId ์žˆ์Œ โ†’ ์ข‹์•„์š” ํฌํ•จ ์ƒ์„ธ ์กฐํšŒ") + void successWithUserId() { + final TimetableBlock block = createBlock(); + final List mediaList = List.of( + new TimetableBlockMedia(1L, BLOCK_ID, 1, "https://media.com/img1.png"), + new TimetableBlockMedia(2L, BLOCK_ID, 2, "https://media.com/img2.png")); + when(adminTimetableBlockRetriever.findTimetableBlockById(BLOCK_ID)).thenReturn(block); + when(timetableBlockMediaRetriever.getAllTimetableBlockMediaByBlockId(BLOCK_ID)).thenReturn(mediaList); + when(timetableCategoryRetriever.findTimetableCategoryByCategoryNotionRowId("cat-notion-1")) + .thenReturn(createCategory()); + when(timetableStageRetriever.findTimetableStageByStageNotionRowId("stage-notion-1")) + .thenReturn(createStage()); + when(timetableUserLikeRetriever.isExistUserLikeByUserIdAndBlockId(USER_ID, BLOCK_ID)).thenReturn(true); + + final TimetableDetailResponse result = timetableService.getEventTimetableDetail(BLOCK_ID, USER_ID); + + assertThat(result.blockName()).isEqualTo("๊ณต์—ฐ A"); + assertThat(result.blockCategory()).isEqualTo("POP"); + assertThat(result.stage()).isEqualTo("๋ฉ”์ธ ์Šคํ…Œ์ด์ง€"); + assertThat(result.isLiked()).isTrue(); + assertThat(result.media()).hasSize(2); + } + + @Test + @DisplayName("์ •์ƒ: userId null โ†’ ์ข‹์•„์š” false") + void successWithoutUserId() { + final TimetableBlock block = createBlock(); + when(adminTimetableBlockRetriever.findTimetableBlockById(BLOCK_ID)).thenReturn(block); + when(timetableBlockMediaRetriever.getAllTimetableBlockMediaByBlockId(BLOCK_ID)).thenReturn(List.of()); + when(timetableCategoryRetriever.findTimetableCategoryByCategoryNotionRowId("cat-notion-1")) + .thenReturn(createCategory()); + when(timetableStageRetriever.findTimetableStageByStageNotionRowId("stage-notion-1")) + .thenReturn(createStage()); + + final TimetableDetailResponse result = timetableService.getEventTimetableDetail(BLOCK_ID, null); + + assertThat(result.isLiked()).isFalse(); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ๋ธ”๋ก ๋ฏธ์กด์žฌ โ†’ NotfoundTimetableException") + void throwsWhenBlockNotFound() { + when(adminTimetableBlockRetriever.findTimetableBlockById(BLOCK_ID)) + .thenThrow(new TimetableBlockNotfoundException()); + + assertThatThrownBy(() -> timetableService.getEventTimetableDetail(BLOCK_ID, USER_ID)) + .isInstanceOf(NotfoundTimetableException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์นดํ…Œ๊ณ ๋ฆฌ ๋ฏธ์กด์žฌ โ†’ NotfoundTimetableException") + void throwsWhenCategoryNotFound() { + final TimetableBlock block = createBlock(); + when(adminTimetableBlockRetriever.findTimetableBlockById(BLOCK_ID)).thenReturn(block); + when(timetableBlockMediaRetriever.getAllTimetableBlockMediaByBlockId(BLOCK_ID)).thenReturn(List.of()); + when(timetableCategoryRetriever.findTimetableCategoryByCategoryNotionRowId("cat-notion-1")) + .thenThrow(new TimetableCategoryNotfoundException()); + + assertThatThrownBy(() -> timetableService.getEventTimetableDetail(BLOCK_ID, USER_ID)) + .isInstanceOf(NotfoundTimetableException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์Šคํ…Œ์ด์ง€ ๋ฏธ์กด์žฌ โ†’ NotfoundTimetableException") + void throwsWhenStageNotFound() { + final TimetableBlock block = createBlock(); + when(adminTimetableBlockRetriever.findTimetableBlockById(BLOCK_ID)).thenReturn(block); + when(timetableBlockMediaRetriever.getAllTimetableBlockMediaByBlockId(BLOCK_ID)).thenReturn(List.of()); + when(timetableCategoryRetriever.findTimetableCategoryByCategoryNotionRowId("cat-notion-1")) + .thenReturn(createCategory()); + when(timetableStageRetriever.findTimetableStageByStageNotionRowId("stage-notion-1")) + .thenThrow(new TimetableStageNotFoundException()); + + assertThatThrownBy(() -> timetableService.getEventTimetableDetail(BLOCK_ID, USER_ID)) + .isInstanceOf(NotfoundTimetableException.class); + } + } +} diff --git a/src/test/java/com/permitseoul/permitserver/domain/eventtimetable/userlike/api/service/TimetableLikeServiceTest.java b/src/test/java/com/permitseoul/permitserver/domain/eventtimetable/userlike/api/service/TimetableLikeServiceTest.java new file mode 100644 index 0000000..976efa1 --- /dev/null +++ b/src/test/java/com/permitseoul/permitserver/domain/eventtimetable/userlike/api/service/TimetableLikeServiceTest.java @@ -0,0 +1,105 @@ +package com.permitseoul.permitserver.domain.eventtimetable.userlike.api.service; + +import com.permitseoul.permitserver.domain.eventtimetable.block.core.component.AdminTimetableBlockRetriever; +import com.permitseoul.permitserver.domain.eventtimetable.block.core.exception.TimetableBlockNotfoundException; +import com.permitseoul.permitserver.domain.eventtimetable.timetable.api.exception.NotfoundTimetableException; +import com.permitseoul.permitserver.domain.eventtimetable.userlike.core.component.TimetableUserLikeRemover; +import com.permitseoul.permitserver.domain.eventtimetable.userlike.core.component.TimetableUserLikeRetriever; +import com.permitseoul.permitserver.domain.eventtimetable.userlike.core.component.TimetableUserLikeSaver; +import com.permitseoul.permitserver.domain.eventtimetable.userlike.core.domain.entity.TimetableUserLikeEntity; +import com.permitseoul.permitserver.domain.eventtimetable.userlike.core.exception.TimetableUserLikeNotfoundException; +import com.permitseoul.permitserver.domain.user.core.component.UserRetriever; +import com.permitseoul.permitserver.domain.user.core.exception.UserNotFoundException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@DisplayName("TimetableLikeService ํ…Œ์ŠคํŠธ") +class TimetableLikeServiceTest { + + @Mock + private UserRetriever userRetriever; + @Mock + private AdminTimetableBlockRetriever adminTimetableBlockRetriever; + @Mock + private TimetableUserLikeSaver timetableUserLikeSaver; + @Mock + private TimetableUserLikeRetriever timetableUserLikeRetriever; + @Mock + private TimetableUserLikeRemover timetableUserLikeRemover; + @InjectMocks + private TimetableLikeService timetableLikeService; + + private static final long USER_ID = 1L; + private static final long BLOCK_ID = 100L; + + @Nested + @DisplayName("likeBlock") + class LikeBlockTest { + + @Test + @DisplayName("์ •์ƒ: ๋ธ”๋ก ์ข‹์•„์š”") + void success() { + doNothing().when(userRetriever).validExistUserById(USER_ID); + doNothing().when(adminTimetableBlockRetriever).validExistTimetableBlock(BLOCK_ID); + + timetableLikeService.likeBlock(USER_ID, BLOCK_ID); + + verify(timetableUserLikeSaver).saveTimetableBlockLike(USER_ID, BLOCK_ID); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์‚ฌ์šฉ์ž ๋ฏธ์กด์žฌ โ†’ NotfoundTimetableException") + void throwsWhenUserNotFound() { + doThrow(new UserNotFoundException()).when(userRetriever).validExistUserById(USER_ID); + + assertThatThrownBy(() -> timetableLikeService.likeBlock(USER_ID, BLOCK_ID)) + .isInstanceOf(NotfoundTimetableException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ๋ธ”๋ก ๋ฏธ์กด์žฌ โ†’ NotfoundTimetableException") + void throwsWhenBlockNotFound() { + doNothing().when(userRetriever).validExistUserById(USER_ID); + doThrow(new TimetableBlockNotfoundException()).when(adminTimetableBlockRetriever) + .validExistTimetableBlock(BLOCK_ID); + + assertThatThrownBy(() -> timetableLikeService.likeBlock(USER_ID, BLOCK_ID)) + .isInstanceOf(NotfoundTimetableException.class); + } + } + + @Nested + @DisplayName("disLikeBlock") + class DisLikeBlockTest { + + @Test + @DisplayName("์ •์ƒ: ๋ธ”๋ก ์ข‹์•„์š” ์ทจ์†Œ") + void success() { + final TimetableUserLikeEntity entity = mock(TimetableUserLikeEntity.class); + when(timetableUserLikeRetriever.findByUserIdAndBlockId(USER_ID, BLOCK_ID)).thenReturn(entity); + + timetableLikeService.disLikeBlock(USER_ID, BLOCK_ID); + + verify(timetableUserLikeRemover).dislikeUserLike(entity); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์ข‹์•„์š” ๋‚ด์—ญ ๋ฏธ์กด์žฌ โ†’ NotfoundTimetableException") + void throwsWhenLikeNotFound() { + when(timetableUserLikeRetriever.findByUserIdAndBlockId(USER_ID, BLOCK_ID)) + .thenThrow(new TimetableUserLikeNotfoundException()); + + assertThatThrownBy(() -> timetableLikeService.disLikeBlock(USER_ID, BLOCK_ID)) + .isInstanceOf(NotfoundTimetableException.class); + } + } +} diff --git a/src/test/java/com/permitseoul/permitserver/domain/guest/api/service/GuestServiceTest.java b/src/test/java/com/permitseoul/permitserver/domain/guest/api/service/GuestServiceTest.java new file mode 100644 index 0000000..a39fda4 --- /dev/null +++ b/src/test/java/com/permitseoul/permitserver/domain/guest/api/service/GuestServiceTest.java @@ -0,0 +1,173 @@ +package com.permitseoul.permitserver.domain.guest.api.service; + +import com.permitseoul.permitserver.domain.admin.guestticket.core.domain.GuestTicketStatus; +import com.permitseoul.permitserver.domain.admin.guestticket.core.domain.entity.GuestTicketEntity; +import com.permitseoul.permitserver.domain.admin.guestticket.core.exception.GuestTicketNotFoundException; +import com.permitseoul.permitserver.domain.event.core.component.EventRetriever; +import com.permitseoul.permitserver.domain.event.core.domain.Event; +import com.permitseoul.permitserver.domain.event.core.domain.EventType; +import com.permitseoul.permitserver.domain.event.core.exception.EventNotfoundException; +import com.permitseoul.permitserver.domain.guest.api.dto.res.GuestTicketValidateResponse; +import com.permitseoul.permitserver.domain.guest.api.exception.GuestNotFoundException; +import com.permitseoul.permitserver.domain.guest.api.exception.GuestTicketIllegalException; +import com.permitseoul.permitserver.domain.guest.core.component.GuestRetriever; +import com.permitseoul.permitserver.domain.guest.core.component.GuestUpdater; +import com.permitseoul.permitserver.domain.guest.core.domain.GuestTicket; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@DisplayName("GuestService ํ…Œ์ŠคํŠธ") +class GuestServiceTest { + + @Mock + private GuestRetriever guestRetriever; + @Mock + private EventRetriever eventRetriever; + @Mock + private GuestUpdater guestUpdater; + @InjectMocks + private GuestService guestService; + + private static final long EVENT_ID = 100L; + private static final String TICKET_CODE = "GUEST-TICKET-001"; + private static final String CHECK_CODE = "EVENT-CHECK-CODE"; + private static final LocalDateTime NOW = LocalDateTime.of(2026, 2, 18, 14, 0); + + private Event createEvent() { + return new Event(EVENT_ID, "ํ…Œ์ŠคํŠธ ์ด๋ฒคํŠธ", EventType.PERMIT, NOW.minusDays(1), NOW.plusDays(1), + "์„œ์šธ", "๋ผ์ธ์—…", "์ƒ์„ธ", 0, NOW.minusDays(7), NOW.plusDays(7), CHECK_CODE); + } + + private GuestTicket createGuestTicket(GuestTicketStatus status) { + return new GuestTicket(1L, EVENT_ID, 10L, TICKET_CODE, status, null); + } + + private GuestTicketEntity createGuestTicketEntity(GuestTicketStatus status) { + final GuestTicketEntity entity = GuestTicketEntity.create(EVENT_ID, 10L, TICKET_CODE); + ReflectionTestUtils.setField(entity, "status", status); + return entity; + } + + @Nested + @DisplayName("validateGuestTicket") + class ValidateGuestTicketTest { + + @Test + @DisplayName("์ •์ƒ: ์œ ํšจํ•œ ๊ฒŒ์ŠคํŠธ ํ‹ฐ์ผ“ ๊ฒ€์ฆ โ†’ ์ด๋ฒคํŠธ ์ด๋ฆ„ ๋ฐ˜ํ™˜") + void success() { + when(guestRetriever.findGuestTicketByTicketCode(TICKET_CODE)) + .thenReturn(createGuestTicket(GuestTicketStatus.ISSUED)); + when(eventRetriever.findEventById(EVENT_ID)).thenReturn(createEvent()); + + final GuestTicketValidateResponse result = guestService.validateGuestTicket(TICKET_CODE); + + assertThat(result.eventName()).isEqualTo("ํ…Œ์ŠคํŠธ ์ด๋ฒคํŠธ"); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ๊ฒŒ์ŠคํŠธ ํ‹ฐ์ผ“ ๋ฏธ์กด์žฌ โ†’ GuestNotFoundException") + void throwsWhenNotFound() { + when(guestRetriever.findGuestTicketByTicketCode(TICKET_CODE)).thenThrow(new GuestTicketNotFoundException()); + + assertThatThrownBy(() -> guestService.validateGuestTicket(TICKET_CODE)) + .isInstanceOf(GuestNotFoundException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์ด๋ฏธ ์‚ฌ์šฉ๋œ ํ‹ฐ์ผ“ โ†’ GuestTicketIllegalException") + void throwsWhenAlreadyUsed() { + when(guestRetriever.findGuestTicketByTicketCode(TICKET_CODE)) + .thenReturn(createGuestTicket(GuestTicketStatus.USED)); + + assertThatThrownBy(() -> guestService.validateGuestTicket(TICKET_CODE)) + .isInstanceOf(GuestTicketIllegalException.class); + } + } + + @Nested + @DisplayName("confirmGuestTicketByStaffCheckCode") + class ConfirmByCheckCodeTest { + + @Test + @DisplayName("์ •์ƒ: ์ฒดํฌ์ฝ”๋“œ๋กœ ๊ฒŒ์ŠคํŠธ ํ‹ฐ์ผ“ ํ™•์ธ โ†’ USED ์ƒํƒœ๋กœ ๋ณ€๊ฒฝ") + void success() { + final GuestTicketEntity entity = createGuestTicketEntity(GuestTicketStatus.ISSUED); + when(guestRetriever.findGuestTicketEntityByTicketCode(TICKET_CODE)).thenReturn(entity); + when(eventRetriever.findEventById(EVENT_ID)).thenReturn(createEvent()); + + guestService.confirmGuestTicketByStaffCheckCode(TICKET_CODE, CHECK_CODE); + + verify(guestUpdater).updateGuestTicketStatus(entity, GuestTicketStatus.USED); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ๊ฒŒ์ŠคํŠธ ํ‹ฐ์ผ“ ๋ฏธ์กด์žฌ โ†’ GuestNotFoundException") + void throwsWhenNotFound() { + when(guestRetriever.findGuestTicketEntityByTicketCode(TICKET_CODE)) + .thenThrow(new GuestTicketNotFoundException()); + + assertThatThrownBy(() -> guestService.confirmGuestTicketByStaffCheckCode(TICKET_CODE, CHECK_CODE)) + .isInstanceOf(GuestNotFoundException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์ด๋ฏธ ์‚ฌ์šฉ๋œ ํ‹ฐ์ผ“ โ†’ GuestTicketIllegalException") + void throwsWhenAlreadyUsed() { + final GuestTicketEntity entity = createGuestTicketEntity(GuestTicketStatus.USED); + when(guestRetriever.findGuestTicketEntityByTicketCode(TICKET_CODE)).thenReturn(entity); + + assertThatThrownBy(() -> guestService.confirmGuestTicketByStaffCheckCode(TICKET_CODE, CHECK_CODE)) + .isInstanceOf(GuestTicketIllegalException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์ฒดํฌ์ฝ”๋“œ ๋ถˆ์ผ์น˜ โ†’ GuestTicketIllegalException") + void throwsWhenCheckCodeMismatch() { + final GuestTicketEntity entity = createGuestTicketEntity(GuestTicketStatus.ISSUED); + when(guestRetriever.findGuestTicketEntityByTicketCode(TICKET_CODE)).thenReturn(entity); + when(eventRetriever.findEventById(EVENT_ID)).thenReturn(createEvent()); + + assertThatThrownBy(() -> guestService.confirmGuestTicketByStaffCheckCode(TICKET_CODE, "WRONG-CODE")) + .isInstanceOf(GuestTicketIllegalException.class); + } + } + + @Nested + @DisplayName("confirmGuestTicketByStaffCamera") + class ConfirmByCameraTest { + + @Test + @DisplayName("์ •์ƒ: ์นด๋ฉ”๋ผ๋กœ ๊ฒŒ์ŠคํŠธ ํ‹ฐ์ผ“ ํ™•์ธ โ†’ USED ์ƒํƒœ๋กœ ๋ณ€๊ฒฝ") + void success() { + final GuestTicketEntity entity = createGuestTicketEntity(GuestTicketStatus.ISSUED); + when(guestRetriever.findGuestTicketEntityByTicketCode(TICKET_CODE)).thenReturn(entity); + + guestService.confirmGuestTicketByStaffCamera(TICKET_CODE); + + verify(guestUpdater).updateGuestTicketStatus(entity, GuestTicketStatus.USED); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ๊ฒŒ์ŠคํŠธ ํ‹ฐ์ผ“ ๋ฏธ์กด์žฌ โ†’ GuestNotFoundException") + void throwsWhenNotFound() { + when(guestRetriever.findGuestTicketEntityByTicketCode(TICKET_CODE)) + .thenThrow(new GuestTicketNotFoundException()); + + assertThatThrownBy(() -> guestService.confirmGuestTicketByStaffCamera(TICKET_CODE)) + .isInstanceOf(GuestNotFoundException.class); + } + } +} diff --git a/src/test/java/com/permitseoul/permitserver/domain/payment/api/service/PaymentServiceTest.java b/src/test/java/com/permitseoul/permitserver/domain/payment/api/service/PaymentServiceTest.java new file mode 100644 index 0000000..ed2693f --- /dev/null +++ b/src/test/java/com/permitseoul/permitserver/domain/payment/api/service/PaymentServiceTest.java @@ -0,0 +1,285 @@ +package com.permitseoul.permitserver.domain.payment.api.service; + +import com.permitseoul.permitserver.domain.event.core.component.EventRetriever; +import com.permitseoul.permitserver.domain.event.core.domain.Event; +import com.permitseoul.permitserver.domain.event.core.domain.EventType; +import com.permitseoul.permitserver.domain.event.core.exception.EventNotfoundException; +import com.permitseoul.permitserver.domain.payment.api.client.TossPaymentClient; +import com.permitseoul.permitserver.domain.payment.api.exception.NotFoundPaymentException; +import com.permitseoul.permitserver.domain.payment.api.exception.PaymentBadRequestException; +import com.permitseoul.permitserver.domain.payment.core.component.PaymentRetriever; +import com.permitseoul.permitserver.domain.payment.core.domain.Currency; +import com.permitseoul.permitserver.domain.payment.core.domain.Payment; +import com.permitseoul.permitserver.domain.payment.core.exception.PaymentNotFoundException; +import com.permitseoul.permitserver.domain.reservation.api.TossProperties; +import com.permitseoul.permitserver.domain.reservation.core.component.ReservationRetriever; +import com.permitseoul.permitserver.domain.reservation.core.domain.Reservation; +import com.permitseoul.permitserver.domain.reservation.core.domain.ReservationStatus; +import com.permitseoul.permitserver.domain.reservationsession.core.component.ReservationSessionRemover; +import com.permitseoul.permitserver.domain.reservationsession.core.component.ReservationSessionRetriever; +import com.permitseoul.permitserver.domain.reservationticket.core.component.ReservationTicketRetriever; +import com.permitseoul.permitserver.domain.reservationticket.core.domain.ReservationTicket; +import com.permitseoul.permitserver.domain.ticket.core.component.TicketRetriever; +import com.permitseoul.permitserver.domain.ticket.core.domain.Ticket; +import com.permitseoul.permitserver.domain.ticket.core.domain.TicketStatus; +import com.permitseoul.permitserver.domain.ticket.core.exception.TicketNotFoundException; +import com.permitseoul.permitserver.domain.tickettype.core.component.TicketTypeRetriever; +import com.permitseoul.permitserver.global.redis.RedisManager; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@DisplayName("PaymentService ํ…Œ์ŠคํŠธ") +class PaymentServiceTest { + + @Mock + private ReservationTicketRetriever reservationTicketRetriever; + @Mock + private EventRetriever eventRetriever; + @Mock + private TossPaymentClient tossPaymentClient; + @Mock + private ReservationRetriever reservationRetriever; + @Mock + private TicketTypeRetriever ticketTypeRetriever; + @Mock + private PaymentRetriever paymentRetriever; + @Mock + private TicketRetriever ticketRetriever; + @Mock + private TicketReservationPaymentFacade ticketReservationPaymentFacade; + @Mock + private ReservationSessionRetriever reservationSessionRetriever; + @Mock + private RedisManager redisManager; + @Mock + private ReservationSessionRemover reservationSessionRemover; + + private PaymentService paymentService; + + // โ”€โ”€ ๊ณตํ†ต ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ โ”€โ”€ + private static final long USER_ID = 1L; + private static final long EVENT_ID = 100L; + private static final String ORDER_ID = "ORDER-20260213-001"; + private static final String PAYMENT_KEY = "toss_pk_test_abc123"; + private static final LocalDateTime NOW = LocalDateTime.of(2026, 2, 13, 14, 0); + + @BeforeEach + void setUp() { + final TossProperties tossProperties = new TossProperties("test_secret_key"); + paymentService = new PaymentService( + reservationTicketRetriever, + eventRetriever, + tossPaymentClient, + reservationRetriever, + tossProperties, + ticketTypeRetriever, + paymentRetriever, + ticketRetriever, + ticketReservationPaymentFacade, + reservationSessionRetriever, + redisManager, + reservationSessionRemover); + } + + private Payment createPayment() { + return new Payment(1L, 1L, ORDER_ID, EVENT_ID, PAYMENT_KEY, + new BigDecimal("60000"), Currency.KRW, NOW, NOW); + } + + private Event createEventWithStartAt(final LocalDateTime startAt) { + return new Event(EVENT_ID, "ํ…Œ์ŠคํŠธ ์ด๋ฒคํŠธ", EventType.PERMIT, startAt, startAt.plusDays(1), + "์„œ์šธ", "๋ผ์ธ์—…", "์ƒ์„ธ", 0, NOW.minusDays(30), NOW.plusDays(30), "CHECK-CODE"); + } + + private Ticket createTicket(final TicketStatus status) { + return Ticket.builder() + .ticketId(1L) + .userId(USER_ID) + .orderId(ORDER_ID) + .ticketTypeId(10L) + .eventId(EVENT_ID) + .ticketCode("TKT-001") + .status(status) + .createdAt(NOW) + .ticketPrice(new BigDecimal("60000")) + .build(); + } + + private Reservation createReservation() { + return new Reservation(1L, "[ํ…Œ์ŠคํŠธ ์ด๋ฒคํŠธ] 1์ผ๊ถŒx1", USER_ID, EVENT_ID, ORDER_ID, + new BigDecimal("60000"), null, ReservationStatus.PAYMENT_SUCCESS, null); + } + + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // cancelPayment + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + @Nested + @DisplayName("cancelPayment") + class CancelPaymentTest { + + @Test + @DisplayName("์˜ˆ์™ธ: ๊ฒฐ์ œ ๋‚ด์—ญ ๋ฏธ์กด์žฌ") + void throwsWhenPaymentNotFound() { + when(paymentRetriever.findPaymentByOrderId(ORDER_ID)) + .thenThrow(new PaymentNotFoundException()); + + assertThatThrownBy(() -> paymentService.cancelPayment(USER_ID, ORDER_ID)) + .isInstanceOf(NotFoundPaymentException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์ด๋ฒคํŠธ ๋ฏธ์กด์žฌ") + void throwsWhenEventNotFound() { + final Payment payment = createPayment(); + when(paymentRetriever.findPaymentByOrderId(ORDER_ID)).thenReturn(payment); + when(eventRetriever.findEventById(EVENT_ID)).thenThrow(new EventNotfoundException()); + + assertThatThrownBy(() -> paymentService.cancelPayment(USER_ID, ORDER_ID)) + .isInstanceOf(NotFoundPaymentException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์ทจ์†Œ ๊ธฐ๊ฐ„ ์ดˆ๊ณผ (์ด๋ฒคํŠธ 3์ผ ์ด๋‚ด)") + void throwsWhenCancelPeriodExpired() { + final Payment payment = createPayment(); + // ์ด๋ฒคํŠธ๊ฐ€ ๋‚ด์ผ์ด๋ผ๋ฉด daysUntilEvent=1 โ†’ 3 ๋ฏธ๋งŒ โ†’ ์ทจ์†Œ ๋ถˆ๊ฐ€ + final Event event = createEventWithStartAt(LocalDateTime.now().plusDays(1)); + when(paymentRetriever.findPaymentByOrderId(ORDER_ID)).thenReturn(payment); + when(eventRetriever.findEventById(EVENT_ID)).thenReturn(event); + + assertThatThrownBy(() -> paymentService.cancelPayment(USER_ID, ORDER_ID)) + .isInstanceOf(PaymentBadRequestException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์ด๋ฏธ ์‚ฌ์šฉ๋œ ํ‹ฐ์ผ“") + void throwsWhenTicketUsed() { + final Payment payment = createPayment(); + final Event event = createEventWithStartAt(LocalDateTime.now().plusDays(30)); + final Ticket usedTicket = createTicket(TicketStatus.USED); + + when(paymentRetriever.findPaymentByOrderId(ORDER_ID)).thenReturn(payment); + when(eventRetriever.findEventById(EVENT_ID)).thenReturn(event); + when(ticketRetriever.findAllTicketsByOrderIdAndUserId(ORDER_ID, USER_ID)) + .thenReturn(List.of(usedTicket)); + + assertThatThrownBy(() -> paymentService.cancelPayment(USER_ID, ORDER_ID)) + .isInstanceOf(PaymentBadRequestException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์ด๋ฏธ ์ทจ์†Œ๋œ ํ‹ฐ์ผ“") + void throwsWhenTicketCanceled() { + final Payment payment = createPayment(); + final Event event = createEventWithStartAt(LocalDateTime.now().plusDays(30)); + final Ticket canceledTicket = createTicket(TicketStatus.CANCELED); + + when(paymentRetriever.findPaymentByOrderId(ORDER_ID)).thenReturn(payment); + when(eventRetriever.findEventById(EVENT_ID)).thenReturn(event); + when(ticketRetriever.findAllTicketsByOrderIdAndUserId(ORDER_ID, USER_ID)) + .thenReturn(List.of(canceledTicket)); + + assertThatThrownBy(() -> paymentService.cancelPayment(USER_ID, ORDER_ID)) + .isInstanceOf(PaymentBadRequestException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ํ‹ฐ์ผ“ ๋ฏธ์กด์žฌ") + void throwsWhenTicketNotFound() { + final Payment payment = createPayment(); + final Event event = createEventWithStartAt(LocalDateTime.now().plusDays(30)); + + when(paymentRetriever.findPaymentByOrderId(ORDER_ID)).thenReturn(payment); + when(eventRetriever.findEventById(EVENT_ID)).thenReturn(event); + when(ticketRetriever.findAllTicketsByOrderIdAndUserId(ORDER_ID, USER_ID)) + .thenThrow(new TicketNotFoundException()); + + assertThatThrownBy(() -> paymentService.cancelPayment(USER_ID, ORDER_ID)) + .isInstanceOf(NotFoundPaymentException.class); + } + } + + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // getPaymentConfirm (์ฃผ์š” ์˜ˆ์™ธ ์ผ€์ด์Šค๋งŒ) + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + @Nested + @DisplayName("getPaymentConfirm") + class GetPaymentConfirmTest { + + @Test + @DisplayName("์˜ˆ์™ธ: ์˜ˆ์•ฝ ์„ธ์…˜ ๋ฏธ์กด์žฌ") + void throwsWhenSessionNotFound() { + when(reservationSessionRetriever.getValidatedReservationSession(eq(USER_ID), anyString(), any())) + .thenThrow( + new com.permitseoul.permitserver.domain.reservationsession.core.exception.ReservationSessionNotFoundException()); + + assertThatThrownBy(() -> paymentService.getPaymentConfirm( + USER_ID, ORDER_ID, PAYMENT_KEY, new BigDecimal("60000"), "session-key")) + .isInstanceOf(NotFoundPaymentException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์˜ˆ์•ฝ ๋ฏธ์กด์žฌ ์‹œ Redis ๋กค๋ฐฑ ๋ฐœ์ƒ") + void throwsWhenReservationNotFoundWithRollback() { + final com.permitseoul.permitserver.domain.reservationsession.core.domain.ReservationSession session = new com.permitseoul.permitserver.domain.reservationsession.core.domain.ReservationSession( + 1L, USER_ID, ORDER_ID, "session-key", false); + final ReservationTicket reservationTicket = new ReservationTicket(1L, 10L, ORDER_ID, 1); + + when(reservationSessionRetriever.getValidatedReservationSession(eq(USER_ID), eq("session-key"), any())) + .thenReturn(session); + when(reservationTicketRetriever.findAllByOrderId(ORDER_ID)) + .thenReturn(List.of(reservationTicket)); + when(reservationRetriever.findReservationByOrderIdAndAmountAndUserId(ORDER_ID, new BigDecimal("60000"), + USER_ID)) + .thenThrow( + new com.permitseoul.permitserver.domain.reservation.core.exception.ReservationNotFoundException()); + + assertThatThrownBy(() -> paymentService.getPaymentConfirm( + USER_ID, ORDER_ID, PAYMENT_KEY, new BigDecimal("60000"), "session-key")) + .isInstanceOf(NotFoundPaymentException.class); + + // Redis ๋กค๋ฐฑ์ด ์‹คํ–‰๋˜์—ˆ๋Š”์ง€ ๊ฒ€์ฆ + verify(redisManager).increment(anyString(), anyLong()); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์ด๋ฒคํŠธ ๋ฏธ์กด์žฌ ์‹œ Redis ๋กค๋ฐฑ ๋ฐœ์ƒ") + void throwsWhenEventNotFoundWithRollback() { + final com.permitseoul.permitserver.domain.reservationsession.core.domain.ReservationSession session = new com.permitseoul.permitserver.domain.reservationsession.core.domain.ReservationSession( + 1L, USER_ID, ORDER_ID, "session-key", false); + final ReservationTicket reservationTicket = new ReservationTicket(1L, 10L, ORDER_ID, 1); + final Reservation reservation = createReservation(); + + when(reservationSessionRetriever.getValidatedReservationSession(eq(USER_ID), eq("session-key"), any())) + .thenReturn(session); + when(reservationTicketRetriever.findAllByOrderId(ORDER_ID)) + .thenReturn(List.of(reservationTicket)); + when(reservationRetriever.findReservationByOrderIdAndAmountAndUserId(ORDER_ID, new BigDecimal("60000"), + USER_ID)) + .thenReturn(reservation); + when(eventRetriever.findEventById(EVENT_ID)) + .thenThrow(new EventNotfoundException()); + + assertThatThrownBy(() -> paymentService.getPaymentConfirm( + USER_ID, ORDER_ID, PAYMENT_KEY, new BigDecimal("60000"), "session-key")) + .isInstanceOf(NotFoundPaymentException.class); + + // Redis ๋กค๋ฐฑ์ด ์‹คํ–‰๋˜์—ˆ๋Š”์ง€ ๊ฒ€์ฆ + verify(redisManager).increment(anyString(), anyLong()); + } + } +} diff --git a/src/test/java/com/permitseoul/permitserver/domain/payment/core/component/PaymentRetrieverTest.java b/src/test/java/com/permitseoul/permitserver/domain/payment/core/component/PaymentRetrieverTest.java new file mode 100644 index 0000000..eca2a11 --- /dev/null +++ b/src/test/java/com/permitseoul/permitserver/domain/payment/core/component/PaymentRetrieverTest.java @@ -0,0 +1,138 @@ +package com.permitseoul.permitserver.domain.payment.core.component; + +import com.permitseoul.permitserver.domain.payment.core.domain.Currency; +import com.permitseoul.permitserver.domain.payment.core.domain.Payment; +import com.permitseoul.permitserver.domain.payment.core.domain.entity.PaymentEntity; +import com.permitseoul.permitserver.domain.payment.core.exception.PaymentNotFoundException; +import com.permitseoul.permitserver.domain.payment.core.repository.PaymentRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.BDDMockito.given; + +@DisplayName("PaymentRetriever ํ…Œ์ŠคํŠธ") +@ExtendWith(MockitoExtension.class) +class PaymentRetrieverTest { + + @Mock + private PaymentRepository paymentRepository; + + @InjectMocks + private PaymentRetriever paymentRetriever; + + private PaymentEntity createTestEntity() { + final PaymentEntity entity = PaymentEntity.create( + 1L, "ORDER-001", 10L, "pay_key_123", + new BigDecimal("60000"), Currency.KRW, + LocalDateTime.of(2026, 1, 19, 17, 0), + LocalDateTime.of(2026, 1, 19, 17, 1)); + ReflectionTestUtils.setField(entity, "paymentId", 100L); + return entity; + } + + @Nested + @DisplayName("findPaymentByOrderId ๋ฉ”์„œ๋“œ") + class FindPaymentByOrderId { + + @Test + @DisplayName("์กด์žฌํ•˜๋Š” orderId๋กœ ์กฐํšŒํ•˜๋ฉด Payment๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void returnsPaymentWhenFound() { + // given + given(paymentRepository.findByOrderId("ORDER-001")).willReturn(Optional.of(createTestEntity())); + + // when + final Payment result = paymentRetriever.findPaymentByOrderId("ORDER-001"); + + // then + assertThat(result.getPaymentId()).isEqualTo(100L); + assertThat(result.getOrderId()).isEqualTo("ORDER-001"); + } + + @Test + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” orderId๋กœ ์กฐํšŒํ•˜๋ฉด PaymentNotFoundException์„ ๋˜์ง„๋‹ค") + void throwsExceptionWhenNotFound() { + // given + given(paymentRepository.findByOrderId("INVALID")).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> paymentRetriever.findPaymentByOrderId("INVALID")) + .isInstanceOf(PaymentNotFoundException.class); + } + } + + @Nested + @DisplayName("findPaymentEntityByOrderId ๋ฉ”์„œ๋“œ") + class FindPaymentEntityByOrderId { + + @Test + @DisplayName("์กด์žฌํ•˜๋Š” orderId๋กœ ์กฐํšŒํ•˜๋ฉด PaymentEntity๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void returnsEntityWhenFound() { + // given + given(paymentRepository.findByOrderId("ORDER-001")).willReturn(Optional.of(createTestEntity())); + + // when + final PaymentEntity result = paymentRetriever.findPaymentEntityByOrderId("ORDER-001"); + + // then + assertThat(result.getPaymentId()).isEqualTo(100L); + } + + @Test + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” orderId๋กœ ์กฐํšŒํ•˜๋ฉด PaymentNotFoundException์„ ๋˜์ง„๋‹ค") + void throwsExceptionWhenNotFound() { + // given + given(paymentRepository.findByOrderId("INVALID")).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> paymentRetriever.findPaymentEntityByOrderId("INVALID")) + .isInstanceOf(PaymentNotFoundException.class); + } + } + + @Nested + @DisplayName("findPaymentByOrderIdIn ๋ฉ”์„œ๋“œ") + class FindPaymentByOrderIdIn { + + @Test + @DisplayName("์กด์žฌํ•˜๋Š” orderIds๋กœ ์กฐํšŒํ•˜๋ฉด Payment ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void returnsPaymentListWhenFound() { + // given + given(paymentRepository.findByOrderIdIn(Set.of("ORDER-001"))) + .willReturn(List.of(createTestEntity())); + + // when + final List result = paymentRetriever.findPaymentByOrderIdIn(Set.of("ORDER-001")); + + // then + assertThat(result).hasSize(1); + assertThat(result.get(0).getOrderId()).isEqualTo("ORDER-001"); + } + + @Test + @DisplayName("๋นˆ ๋ฆฌ์ŠคํŠธ๋ฉด PaymentNotFoundException์„ ๋˜์ง„๋‹ค") + void throwsExceptionWhenEmpty() { + // given + given(paymentRepository.findByOrderIdIn(Set.of("INVALID"))) + .willReturn(Collections.emptyList()); + + // when & then + assertThatThrownBy(() -> paymentRetriever.findPaymentByOrderIdIn(Set.of("INVALID"))) + .isInstanceOf(PaymentNotFoundException.class); + } + } +} diff --git a/src/test/java/com/permitseoul/permitserver/domain/payment/core/domain/PaymentEntityTest.java b/src/test/java/com/permitseoul/permitserver/domain/payment/core/domain/PaymentEntityTest.java new file mode 100644 index 0000000..15f2639 --- /dev/null +++ b/src/test/java/com/permitseoul/permitserver/domain/payment/core/domain/PaymentEntityTest.java @@ -0,0 +1,102 @@ +package com.permitseoul.permitserver.domain.payment.core.domain; + +import com.permitseoul.permitserver.domain.payment.core.domain.entity.PaymentEntity; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("Payment & PaymentEntity ํ…Œ์ŠคํŠธ") +class PaymentEntityTest { + + private static final long RESERVATION_ID = 1L; + private static final String ORDER_ID = "ORDER-20260119-001"; + private static final long EVENT_ID = 10L; + private static final String PAYMENT_KEY = "toss_pay_key_123"; + private static final BigDecimal TOTAL_AMOUNT = new BigDecimal("60000.00"); + private static final Currency CURRENCY = Currency.KRW; + private static final LocalDateTime REQUESTED_AT = LocalDateTime.of(2026, 1, 19, 17, 0); + private static final LocalDateTime APPROVED_AT = LocalDateTime.of(2026, 1, 19, 17, 1); + + @Nested + @DisplayName("PaymentEntity.create ๋ฉ”์„œ๋“œ") + class Create { + + @Test + @DisplayName("์ •์ƒ์ ์ธ ๊ฐ’์œผ๋กœ PaymentEntity๋ฅผ ์ƒ์„ฑํ•œ๋‹ค") + void createsPaymentEntitySuccessfully() { + // when + final PaymentEntity entity = PaymentEntity.create( + RESERVATION_ID, ORDER_ID, EVENT_ID, PAYMENT_KEY, + TOTAL_AMOUNT, CURRENCY, REQUESTED_AT, APPROVED_AT); + + // then + assertThat(entity.getReservationId()).isEqualTo(RESERVATION_ID); + assertThat(entity.getOrderId()).isEqualTo(ORDER_ID); + assertThat(entity.getEventId()).isEqualTo(EVENT_ID); + assertThat(entity.getPaymentKey()).isEqualTo(PAYMENT_KEY); + assertThat(entity.getTotalAmount()).isEqualByComparingTo(TOTAL_AMOUNT); + assertThat(entity.getCurrency()).isEqualTo(CURRENCY); + assertThat(entity.getRequestedAt()).isEqualTo(REQUESTED_AT); + assertThat(entity.getApprovedAt()).isEqualTo(APPROVED_AT); + } + + @Test + @DisplayName("์ƒ์„ฑ ์งํ›„ paymentId๋Š” null์ด๋‹ค (@GeneratedValue)") + void paymentIdIsNullAfterCreate() { + // when + final PaymentEntity entity = PaymentEntity.create( + RESERVATION_ID, ORDER_ID, EVENT_ID, PAYMENT_KEY, + TOTAL_AMOUNT, CURRENCY, REQUESTED_AT, APPROVED_AT); + + // then + assertThat(entity.getPaymentId()).isNull(); + } + + @Test + @DisplayName("approvedAt์ด null์ด์–ด๋„ ์ƒ์„ฑ ๊ฐ€๋Šฅํ•˜๋‹ค") + void createsWithNullApprovedAt() { + // when + final PaymentEntity entity = PaymentEntity.create( + RESERVATION_ID, ORDER_ID, EVENT_ID, PAYMENT_KEY, + TOTAL_AMOUNT, CURRENCY, REQUESTED_AT, null); + + // then + assertThat(entity.getApprovedAt()).isNull(); + } + } + + @Nested + @DisplayName("Payment.fromEntity ๋ฉ”์„œ๋“œ") + class FromEntity { + + @Test + @DisplayName("Entity์˜ ๋ชจ๋“  ํ•„๋“œ๊ฐ€ Domain ๊ฐ์ฒด๋กœ ์ •ํ™•ํžˆ ๋งคํ•‘๋œ๋‹ค") + void mapsAllFieldsCorrectly() { + // given + final PaymentEntity entity = PaymentEntity.create( + RESERVATION_ID, ORDER_ID, EVENT_ID, PAYMENT_KEY, + TOTAL_AMOUNT, CURRENCY, REQUESTED_AT, APPROVED_AT); + ReflectionTestUtils.setField(entity, "paymentId", 100L); + + // when + final Payment payment = Payment.fromEntity(entity); + + // then + assertThat(payment.getPaymentId()).isEqualTo(100L); + assertThat(payment.getReservationId()).isEqualTo(RESERVATION_ID); + assertThat(payment.getOrderId()).isEqualTo(ORDER_ID); + assertThat(payment.getEventId()).isEqualTo(EVENT_ID); + assertThat(payment.getPaymentKey()).isEqualTo(PAYMENT_KEY); + assertThat(payment.getTotalAmount()).isEqualByComparingTo(TOTAL_AMOUNT); + assertThat(payment.getCurrency()).isEqualTo(CURRENCY); + assertThat(payment.getRequestedAt()).isEqualTo(REQUESTED_AT); + assertThat(payment.getApprovedAt()).isEqualTo(APPROVED_AT); + } + } +} diff --git a/src/test/java/com/permitseoul/permitserver/domain/reservation/api/service/ReservationServiceTest.java b/src/test/java/com/permitseoul/permitserver/domain/reservation/api/service/ReservationServiceTest.java new file mode 100644 index 0000000..1a7492e --- /dev/null +++ b/src/test/java/com/permitseoul/permitserver/domain/reservation/api/service/ReservationServiceTest.java @@ -0,0 +1,358 @@ +package com.permitseoul.permitserver.domain.reservation.api.service; + +import com.permitseoul.permitserver.domain.coupon.core.component.CouponRetriever; +import com.permitseoul.permitserver.domain.coupon.core.domain.Coupon; +import com.permitseoul.permitserver.domain.coupon.core.exception.CouponConflictException; +import com.permitseoul.permitserver.domain.coupon.core.exception.CouponNotfoundException; +import com.permitseoul.permitserver.domain.event.core.component.EventRetriever; +import com.permitseoul.permitserver.domain.event.core.domain.Event; +import com.permitseoul.permitserver.domain.event.core.domain.EventType; +import com.permitseoul.permitserver.domain.event.core.exception.EventNotfoundException; +import com.permitseoul.permitserver.domain.reservation.api.dto.ReservationInfoRequest; +import com.permitseoul.permitserver.domain.reservation.api.dto.ReservationInfoResponse; +import com.permitseoul.permitserver.domain.reservation.api.exception.*; +import com.permitseoul.permitserver.domain.reservation.core.component.ReservationAndReservationTicketFacade; +import com.permitseoul.permitserver.domain.reservation.core.component.ReservationRetriever; +import com.permitseoul.permitserver.domain.reservation.core.domain.Reservation; +import com.permitseoul.permitserver.domain.reservation.core.domain.ReservationStatus; +import com.permitseoul.permitserver.domain.reservation.core.exception.ReservationNotFoundException; +import com.permitseoul.permitserver.domain.reservationsession.core.component.ReservationSessionRetriever; +import com.permitseoul.permitserver.domain.reservationsession.core.domain.ReservationSession; +import com.permitseoul.permitserver.domain.reservationsession.core.exception.ReservationSessionNotFoundException; +import com.permitseoul.permitserver.domain.ticketround.core.component.TicketRoundRetriever; +import com.permitseoul.permitserver.domain.ticketround.core.domain.entity.TicketRoundEntity; +import com.permitseoul.permitserver.domain.tickettype.core.component.TicketTypeRetriever; +import com.permitseoul.permitserver.domain.tickettype.core.domain.entity.TicketTypeEntity; +import com.permitseoul.permitserver.domain.tickettype.core.exception.TicketTypeInsufficientCountException; +import com.permitseoul.permitserver.domain.user.core.component.UserRetriever; +import com.permitseoul.permitserver.domain.user.core.domain.*; +import com.permitseoul.permitserver.domain.user.core.exception.UserNotFoundException; +import com.permitseoul.permitserver.global.Constants; +import com.permitseoul.permitserver.global.redis.RedisManager; +import org.springframework.test.util.ReflectionTestUtils; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@DisplayName("ReservationService ํ…Œ์ŠคํŠธ") +class ReservationServiceTest { + + @Mock + private EventRetriever eventRetriever; + @Mock + private UserRetriever userRetriever; + @Mock + private TicketTypeRetriever ticketTypeRetriever; + @Mock + private CouponRetriever couponRetriever; + @Mock + private ReservationRetriever reservationRetriever; + @Mock + private TicketRoundRetriever ticketRoundRetriever; + @Mock + private RedisManager redisManager; + @Mock + private ReservationAndReservationTicketFacade reservationAndReservationTicketFacade; + @Mock + private ReservationSessionRetriever reservationSessionRetriever; + + @InjectMocks + private ReservationService reservationService; + + // โ”€โ”€ ๊ณตํ†ต ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ โ”€โ”€ + private static final long USER_ID = 1L; + private static final long EVENT_ID = 100L; + private static final String ORDER_ID = "ORDER-20260213-001"; + private static final String SESSION_KEY = "session-uuid-key"; + private static final String COUPON_CODE = "COUPON-2026"; + private static final LocalDateTime NOW = LocalDateTime.of(2026, 2, 13, 14, 0); + + private Event createEvent() { + return new Event(EVENT_ID, "ํ…Œ์ŠคํŠธ ์ด๋ฒคํŠธ", EventType.PERMIT, NOW.minusDays(1), NOW.plusDays(1), + "์„œ์šธ", "๋ผ์ธ์—…", "์ƒ์„ธ", 0, NOW.minusDays(7), NOW.plusDays(7), "CHECK-CODE"); + } + + private User createUser() { + return new User(USER_ID, "ํ™๊ธธ๋™", Gender.MALE, 25, "test@email.com", "social123", SocialType.KAKAO, + UserRole.USER); + } + + private TicketTypeEntity createTicketTypeEntity(final long ticketTypeId, final long ticketRoundId) { + final TicketTypeEntity entity = TicketTypeEntity.create(ticketRoundId, "1์ผ๊ถŒ", new BigDecimal("60000"), 100, + NOW.minusDays(1), NOW.plusDays(1)); + ReflectionTestUtils.setField(entity, "ticketTypeId", ticketTypeId); + return entity; + } + + private TicketRoundEntity createTicketRoundEntity() { + final TicketRoundEntity entity = TicketRoundEntity.create(EVENT_ID, "1์ฐจ", NOW.minusDays(1), NOW.plusDays(1)); + ReflectionTestUtils.setField(entity, "ticketRoundId", 1L); + return entity; + } + + private Coupon createCoupon() { + return new Coupon(1L, EVENT_ID, COUPON_CODE, 10, "ํ…Œ์ŠคํŠธ ์ฟ ํฐ", false, null, NOW.minusDays(7)); + } + + private Reservation createReservation() { + return new Reservation(1L, "[ํ…Œ์ŠคํŠธ ์ด๋ฒคํŠธ] 1์ผ๊ถŒx1", USER_ID, EVENT_ID, ORDER_ID, + new BigDecimal("60000"), null, ReservationStatus.RESERVED, null); + } + + private ReservationSession createReservationSession() { + return new ReservationSession(1L, USER_ID, ORDER_ID, SESSION_KEY, false); + } + + private List createTicketTypeInfos() { + return List.of(new ReservationInfoRequest.TicketTypeInfo(10L, 1)); + } + + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // getReservationInfo + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + @Nested + @DisplayName("getReservationInfo") + class GetReservationInfoTest { + + @Test + @DisplayName("์ •์ƒ: ์˜ˆ์•ฝ ์ •๋ณด ์กฐํšŒ ์„ฑ๊ณต") + void success() { + final ReservationSession session = createReservationSession(); + final User user = createUser(); + final Reservation reservation = createReservation(); + + when(reservationSessionRetriever.getValidatedReservationSession(eq(USER_ID), eq(SESSION_KEY), any())) + .thenReturn(session); + when(userRetriever.findUserById(USER_ID)).thenReturn(user); + when(reservationRetriever.findReservationByOrderIdAndUserId(ORDER_ID, USER_ID)) + .thenReturn(reservation); + + final ReservationInfoResponse result = reservationService.getReservationInfo(USER_ID, SESSION_KEY); + + assertThat(result.orderName()).isEqualTo("[ํ…Œ์ŠคํŠธ ์ด๋ฒคํŠธ] 1์ผ๊ถŒx1"); + assertThat(result.orderId()).isEqualTo(ORDER_ID); + assertThat(result.userName()).isEqualTo("ํ™๊ธธ๋™"); + assertThat(result.userEmail()).isEqualTo("test@email.com"); + assertThat(result.totalAmount()).isEqualByComparingTo(new BigDecimal("60000")); + assertThat(result.customerKey()).isEqualTo("social123"); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์œ ํšจํ•˜์ง€ ์•Š์€ ์„ธ์…˜") + void throwsWhenSessionNotFound() { + when(reservationSessionRetriever.getValidatedReservationSession(eq(USER_ID), eq(SESSION_KEY), any())) + .thenThrow(new ReservationSessionNotFoundException()); + + assertThatThrownBy(() -> reservationService.getReservationInfo(USER_ID, SESSION_KEY)) + .isInstanceOf(NotfoundReservationException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์‚ฌ์šฉ์ž ๋ฏธ์กด์žฌ") + void throwsWhenUserNotFound() { + final ReservationSession session = createReservationSession(); + when(reservationSessionRetriever.getValidatedReservationSession(eq(USER_ID), eq(SESSION_KEY), any())) + .thenReturn(session); + when(userRetriever.findUserById(USER_ID)).thenThrow(new UserNotFoundException()); + + assertThatThrownBy(() -> reservationService.getReservationInfo(USER_ID, SESSION_KEY)) + .isInstanceOf(NotfoundReservationException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์˜ˆ์•ฝ ๋ฏธ์กด์žฌ") + void throwsWhenReservationNotFound() { + final ReservationSession session = createReservationSession(); + when(reservationSessionRetriever.getValidatedReservationSession(eq(USER_ID), eq(SESSION_KEY), any())) + .thenReturn(session); + when(userRetriever.findUserById(USER_ID)).thenReturn(createUser()); + when(reservationRetriever.findReservationByOrderIdAndUserId(ORDER_ID, USER_ID)) + .thenThrow(new ReservationNotFoundException()); + + assertThatThrownBy(() -> reservationService.getReservationInfo(USER_ID, SESSION_KEY)) + .isInstanceOf(NotfoundReservationException.class); + } + } + + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // saveReservation + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + @Nested + @DisplayName("saveReservation") + class SaveReservationTest { + + @Test + @DisplayName("์ •์ƒ: ์ฟ ํฐ ์—†์ด ์˜ˆ์•ฝ ์ €์žฅ ์„ฑ๊ณต") + void successWithoutCoupon() { + final List ticketTypeInfos = createTicketTypeInfos(); + final TicketTypeEntity ticketTypeEntity = createTicketTypeEntity(10L, 1L); + final TicketRoundEntity ticketRoundEntity = createTicketRoundEntity(); + final Event event = createEvent(); + + when(ticketTypeRetriever.findAllTicketTypeEntityByIds(List.of(10L))) + .thenReturn(List.of(ticketTypeEntity)); + doNothing().when(userRetriever).validExistUserById(USER_ID); + when(eventRetriever.findEventById(EVENT_ID)).thenReturn(event); + when(ticketRoundRetriever.findTicketRoundEntityById(anyLong())).thenReturn(ticketRoundEntity); + when(redisManager.decrement(anyString(), anyLong())).thenReturn(49L); + when(reservationAndReservationTicketFacade.saveReservationWithTicketAndSession( + anyString(), eq(USER_ID), eq(EVENT_ID), eq(ORDER_ID), + any(BigDecimal.class), isNull(), eq(ticketTypeInfos))) + .thenReturn(SESSION_KEY); + + final String result = reservationService.saveReservation( + USER_ID, EVENT_ID, null, new BigDecimal("60000"), ORDER_ID, ticketTypeInfos, NOW); + + assertThat(result).isEqualTo(SESSION_KEY); + verify(reservationAndReservationTicketFacade).saveReservationWithTicketAndSession( + anyString(), eq(USER_ID), eq(EVENT_ID), eq(ORDER_ID), + any(BigDecimal.class), isNull(), eq(ticketTypeInfos)); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์ด๋ฒคํŠธ ๋ฏธ์กด์žฌ") + void throwsWhenEventNotFound() { + final List ticketTypeInfos = createTicketTypeInfos(); + final TicketTypeEntity ticketTypeEntity = createTicketTypeEntity(10L, 1L); + when(ticketTypeRetriever.findAllTicketTypeEntityByIds(List.of(10L))) + .thenReturn(List.of(ticketTypeEntity)); + doNothing().when(userRetriever).validExistUserById(USER_ID); + when(eventRetriever.findEventById(EVENT_ID)).thenThrow(new EventNotfoundException()); + + assertThatThrownBy(() -> reservationService.saveReservation( + USER_ID, EVENT_ID, null, new BigDecimal("60000"), ORDER_ID, ticketTypeInfos, NOW)) + .isInstanceOf(NotfoundReservationException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์‚ฌ์šฉ์ž ๋ฏธ์กด์žฌ") + void throwsWhenUserNotFound() { + final List ticketTypeInfos = createTicketTypeInfos(); + final TicketTypeEntity ticketTypeEntity = createTicketTypeEntity(10L, 1L); + when(ticketTypeRetriever.findAllTicketTypeEntityByIds(List.of(10L))) + .thenReturn(List.of(ticketTypeEntity)); + doThrow(new UserNotFoundException()).when(userRetriever).validExistUserById(USER_ID); + + assertThatThrownBy(() -> reservationService.saveReservation( + USER_ID, EVENT_ID, null, new BigDecimal("60000"), ORDER_ID, ticketTypeInfos, NOW)) + .isInstanceOf(NotfoundReservationException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์ฟ ํฐ ์ฝ”๋“œ ๋ฏธ์กด์žฌ") + void throwsWhenCouponNotFound() { + final List ticketTypeInfos = createTicketTypeInfos(); + final TicketTypeEntity ticketTypeEntity = createTicketTypeEntity(10L, 1L); + final Event event = createEvent(); + + when(ticketTypeRetriever.findAllTicketTypeEntityByIds(List.of(10L))) + .thenReturn(List.of(ticketTypeEntity)); + doNothing().when(userRetriever).validExistUserById(USER_ID); + when(eventRetriever.findEventById(EVENT_ID)).thenReturn(event); + when(couponRetriever.findValidCouponByCodeAndEvent(COUPON_CODE, EVENT_ID)) + .thenThrow(new CouponNotfoundException()); + + assertThatThrownBy(() -> reservationService.saveReservation( + USER_ID, EVENT_ID, COUPON_CODE, new BigDecimal("54000"), ORDER_ID, ticketTypeInfos, NOW)) + .isInstanceOf(NotfoundReservationException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์ด๋ฏธ ์‚ฌ์šฉ๋œ ์ฟ ํฐ") + void throwsWhenCouponAlreadyUsed() { + final List ticketTypeInfos = createTicketTypeInfos(); + final TicketTypeEntity ticketTypeEntity = createTicketTypeEntity(10L, 1L); + final Event event = createEvent(); + + when(ticketTypeRetriever.findAllTicketTypeEntityByIds(List.of(10L))) + .thenReturn(List.of(ticketTypeEntity)); + doNothing().when(userRetriever).validExistUserById(USER_ID); + when(eventRetriever.findEventById(EVENT_ID)).thenReturn(event); + when(couponRetriever.findValidCouponByCodeAndEvent(COUPON_CODE, EVENT_ID)) + .thenThrow(new CouponConflictException()); + + assertThatThrownBy(() -> reservationService.saveReservation( + USER_ID, EVENT_ID, COUPON_CODE, new BigDecimal("54000"), ORDER_ID, ticketTypeInfos, NOW)) + .isInstanceOf(ConflictReservationException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ํŒ๋งค ๊ธฐ๊ฐ„ ๋งŒ๋ฃŒ ๋ผ์šด๋“œ") + void throwsWhenTicketRoundExpired() { + final List ticketTypeInfos = createTicketTypeInfos(); + // ๋ฏธ๋ž˜ ๋‚ ์งœ์˜ ๋ผ์šด๋“œ (์•„์ง ํŒ๋งค ์‹œ์ž‘ ์ „) + final TicketRoundEntity expiredRound = TicketRoundEntity.create(EVENT_ID, "1์ฐจ", + NOW.minusDays(10), NOW.minusDays(5)); + final TicketTypeEntity ticketTypeEntity = createTicketTypeEntity(10L, 1L); + final Event event = createEvent(); + + when(ticketTypeRetriever.findAllTicketTypeEntityByIds(List.of(10L))) + .thenReturn(List.of(ticketTypeEntity)); + doNothing().when(userRetriever).validExistUserById(USER_ID); + when(eventRetriever.findEventById(EVENT_ID)).thenReturn(event); + when(ticketRoundRetriever.findTicketRoundEntityById(anyLong())).thenReturn(expiredRound); + + assertThatThrownBy(() -> reservationService.saveReservation( + USER_ID, EVENT_ID, null, new BigDecimal("60000"), ORDER_ID, ticketTypeInfos, NOW)) + .isInstanceOf(ExpiredReservationException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: Redis ์žฌ๊ณ  ๋ถ€์กฑ") + void throwsWhenRedisInsufficientTicket() { + final List ticketTypeInfos = createTicketTypeInfos(); + final TicketTypeEntity ticketTypeEntity = createTicketTypeEntity(10L, 1L); + final TicketRoundEntity ticketRoundEntity = createTicketRoundEntity(); + final Event event = createEvent(); + + when(ticketTypeRetriever.findAllTicketTypeEntityByIds(List.of(10L))) + .thenReturn(List.of(ticketTypeEntity)); + doNothing().when(userRetriever).validExistUserById(USER_ID); + when(eventRetriever.findEventById(EVENT_ID)).thenReturn(event); + when(ticketRoundRetriever.findTicketRoundEntityById(anyLong())).thenReturn(ticketRoundEntity); + // Redis ์žฌ๊ณ ๊ฐ€ -1 ๋ฐ˜ํ™˜ โ†’ ๋ถ€์กฑ + when(redisManager.decrement(anyString(), anyLong())).thenReturn(-1L); + + assertThatThrownBy(() -> reservationService.saveReservation( + USER_ID, EVENT_ID, null, new BigDecimal("60000"), ORDER_ID, ticketTypeInfos, NOW)) + .isInstanceOf(InSufficientReservationException.class); + + // Redis ๋กค๋ฐฑ ๊ฒ€์ฆ + verify(redisManager).increment(anyString(), anyLong()); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ๊ธˆ์•ก ๋ถˆ์ผ์น˜") + void throwsWhenAmountMismatch() { + final List ticketTypeInfos = createTicketTypeInfos(); + final TicketTypeEntity ticketTypeEntity = createTicketTypeEntity(10L, 1L); + final TicketRoundEntity ticketRoundEntity = createTicketRoundEntity(); + final Event event = createEvent(); + + when(ticketTypeRetriever.findAllTicketTypeEntityByIds(List.of(10L))) + .thenReturn(List.of(ticketTypeEntity)); + doNothing().when(userRetriever).validExistUserById(USER_ID); + when(eventRetriever.findEventById(EVENT_ID)).thenReturn(event); + when(ticketRoundRetriever.findTicketRoundEntityById(anyLong())).thenReturn(ticketRoundEntity); + + // ๊ธˆ์•ก ๋ถˆ์ผ์น˜: ์‹ค์ œ 60000 but ์š”์ฒญ 99999 + assertThatThrownBy(() -> reservationService.saveReservation( + USER_ID, EVENT_ID, null, new BigDecimal("99999"), ORDER_ID, ticketTypeInfos, NOW)) + .isInstanceOf(ReservationBadRequestException.class); + } + } +} diff --git a/src/test/java/com/permitseoul/permitserver/domain/reservation/core/component/ReservationRetrieverTest.java b/src/test/java/com/permitseoul/permitserver/domain/reservation/core/component/ReservationRetrieverTest.java new file mode 100644 index 0000000..46d1708 --- /dev/null +++ b/src/test/java/com/permitseoul/permitserver/domain/reservation/core/component/ReservationRetrieverTest.java @@ -0,0 +1,197 @@ +package com.permitseoul.permitserver.domain.reservation.core.component; + +import com.permitseoul.permitserver.domain.reservation.core.domain.Reservation; +import com.permitseoul.permitserver.domain.reservation.core.domain.entity.ReservationEntity; +import com.permitseoul.permitserver.domain.reservation.core.exception.ReservationNotFoundException; +import com.permitseoul.permitserver.domain.reservation.core.repository.ReservationRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +import java.math.BigDecimal; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.BDDMockito.given; + +@DisplayName("ReservationRetriever ํ…Œ์ŠคํŠธ") +@ExtendWith(MockitoExtension.class) +class ReservationRetrieverTest { + + @Mock + private ReservationRepository reservationRepository; + + @InjectMocks + private ReservationRetriever reservationRetriever; + + private static final String ORDER_ID = "ORDER-001"; + private static final BigDecimal TOTAL_AMOUNT = new BigDecimal("60000.00"); + private static final long USER_ID = 1L; + + private ReservationEntity createTestEntity() { + final ReservationEntity entity = ReservationEntity.create( + "ํ…Œ์ŠคํŠธ ์˜ˆ์•ฝ", USER_ID, 10L, ORDER_ID, TOTAL_AMOUNT, null); + ReflectionTestUtils.setField(entity, "reservationId", 100L); + return entity; + } + + @Nested + @DisplayName("findReservationByOrderIdAndAmountAndUserId ๋ฉ”์„œ๋“œ") + class FindByOrderIdAndAmountAndUserId { + + @Test + @DisplayName("์กด์žฌํ•˜๋ฉด Reservation์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void returnsReservationWhenFound() { + // given + given(reservationRepository.findByOrderIdAndTotalAmountAndUserId(ORDER_ID, TOTAL_AMOUNT, USER_ID)) + .willReturn(Optional.of(createTestEntity())); + + // when + final Reservation result = reservationRetriever.findReservationByOrderIdAndAmountAndUserId(ORDER_ID, + TOTAL_AMOUNT, USER_ID); + + // then + assertThat(result.getReservationId()).isEqualTo(100L); + assertThat(result.getOrderId()).isEqualTo(ORDER_ID); + } + + @Test + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š์œผ๋ฉด ReservationNotFoundException์„ ๋˜์ง„๋‹ค") + void throwsExceptionWhenNotFound() { + // given + given(reservationRepository.findByOrderIdAndTotalAmountAndUserId(ORDER_ID, TOTAL_AMOUNT, USER_ID)) + .willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> reservationRetriever.findReservationByOrderIdAndAmountAndUserId(ORDER_ID, + TOTAL_AMOUNT, USER_ID)) + .isInstanceOf(ReservationNotFoundException.class); + } + } + + @Nested + @DisplayName("findReservationById ๋ฉ”์„œ๋“œ") + class FindReservationById { + + @Test + @DisplayName("์กด์žฌํ•˜๋ฉด Reservation์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void returnsReservationWhenFound() { + // given + given(reservationRepository.findById(100L)).willReturn(Optional.of(createTestEntity())); + + // when + final Reservation result = reservationRetriever.findReservationById(100L); + + // then + assertThat(result.getReservationId()).isEqualTo(100L); + } + + @Test + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š์œผ๋ฉด ReservationNotFoundException์„ ๋˜์ง„๋‹ค") + void throwsExceptionWhenNotFound() { + // given + given(reservationRepository.findById(999L)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> reservationRetriever.findReservationById(999L)) + .isInstanceOf(ReservationNotFoundException.class); + } + } + + @Nested + @DisplayName("findReservationEntityById ๋ฉ”์„œ๋“œ") + class FindReservationEntityById { + + @Test + @DisplayName("์กด์žฌํ•˜๋ฉด ReservationEntity๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void returnsEntityWhenFound() { + // given + given(reservationRepository.findById(100L)).willReturn(Optional.of(createTestEntity())); + + // when + final ReservationEntity result = reservationRetriever.findReservationEntityById(100L); + + // then + assertThat(result.getReservationId()).isEqualTo(100L); + } + + @Test + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š์œผ๋ฉด ReservationNotFoundException์„ ๋˜์ง„๋‹ค") + void throwsExceptionWhenNotFound() { + // given + given(reservationRepository.findById(999L)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> reservationRetriever.findReservationEntityById(999L)) + .isInstanceOf(ReservationNotFoundException.class); + } + } + + @Nested + @DisplayName("findReservationByIdAndUserId ๋ฉ”์„œ๋“œ") + class FindReservationByIdAndUserId { + + @Test + @DisplayName("์กด์žฌํ•˜๋ฉด Reservation์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void returnsReservationWhenFound() { + // given + given(reservationRepository.findByReservationIdAndUserId(100L, USER_ID)) + .willReturn(Optional.of(createTestEntity())); + + // when + final Reservation result = reservationRetriever.findReservationByIdAndUserId(100L, USER_ID); + + // then + assertThat(result.getReservationId()).isEqualTo(100L); + } + + @Test + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š์œผ๋ฉด ReservationNotFoundException์„ ๋˜์ง„๋‹ค") + void throwsExceptionWhenNotFound() { + // given + given(reservationRepository.findByReservationIdAndUserId(999L, USER_ID)) + .willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> reservationRetriever.findReservationByIdAndUserId(999L, USER_ID)) + .isInstanceOf(ReservationNotFoundException.class); + } + } + + @Nested + @DisplayName("findReservationByOrderIdAndUserId ๋ฉ”์„œ๋“œ") + class FindReservationByOrderIdAndUserId { + + @Test + @DisplayName("์กด์žฌํ•˜๋ฉด Reservation์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void returnsReservationWhenFound() { + // given + given(reservationRepository.findByOrderIdAndUserId(ORDER_ID, USER_ID)) + .willReturn(Optional.of(createTestEntity())); + + // when + final Reservation result = reservationRetriever.findReservationByOrderIdAndUserId(ORDER_ID, USER_ID); + + // then + assertThat(result.getOrderId()).isEqualTo(ORDER_ID); + } + + @Test + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š์œผ๋ฉด ReservationNotFoundException์„ ๋˜์ง„๋‹ค") + void throwsExceptionWhenNotFound() { + // given + given(reservationRepository.findByOrderIdAndUserId("INVALID", USER_ID)) + .willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> reservationRetriever.findReservationByOrderIdAndUserId("INVALID", USER_ID)) + .isInstanceOf(ReservationNotFoundException.class); + } + } +} diff --git a/src/test/java/com/permitseoul/permitserver/domain/reservation/core/domain/ReservationEntityTest.java b/src/test/java/com/permitseoul/permitserver/domain/reservation/core/domain/ReservationEntityTest.java new file mode 100644 index 0000000..ecc4cce --- /dev/null +++ b/src/test/java/com/permitseoul/permitserver/domain/reservation/core/domain/ReservationEntityTest.java @@ -0,0 +1,184 @@ +package com.permitseoul.permitserver.domain.reservation.core.domain; + +import com.permitseoul.permitserver.domain.reservation.core.domain.entity.ReservationEntity; +import com.permitseoul.permitserver.global.exception.IllegalEnumTransitionException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayName("Reservation & ReservationEntity ํ…Œ์ŠคํŠธ") +class ReservationEntityTest { + + private static final String RESERVATION_NAME = "ํ…Œ์ŠคํŠธ ์˜ˆ์•ฝ"; + private static final long USER_ID = 1L; + private static final long EVENT_ID = 10L; + private static final String ORDER_ID = "ORDER-20260119-001"; + private static final BigDecimal TOTAL_AMOUNT = new BigDecimal("60000.00"); + private static final String COUPON_CODE = "COUPON-001"; + + private ReservationEntity createTestEntity() { + return ReservationEntity.create(RESERVATION_NAME, USER_ID, EVENT_ID, ORDER_ID, TOTAL_AMOUNT, COUPON_CODE); + } + + @Nested + @DisplayName("ReservationEntity.create ๋ฉ”์„œ๋“œ") + class Create { + + @Test + @DisplayName("์ •์ƒ์ ์ธ ๊ฐ’์œผ๋กœ ReservationEntity๋ฅผ ์ƒ์„ฑํ•œ๋‹ค") + void createsReservationEntitySuccessfully() { + // when + final ReservationEntity entity = createTestEntity(); + + // then + assertThat(entity.getReservationName()).isEqualTo(RESERVATION_NAME); + assertThat(entity.getUserId()).isEqualTo(USER_ID); + assertThat(entity.getEventId()).isEqualTo(EVENT_ID); + assertThat(entity.getOrderId()).isEqualTo(ORDER_ID); + assertThat(entity.getTotalAmount()).isEqualByComparingTo(TOTAL_AMOUNT); + assertThat(entity.getCouponCode()).isEqualTo(COUPON_CODE); + } + + @Test + @DisplayName("์ดˆ๊ธฐ status๋Š” RESERVED์ด๋‹ค") + void initialStatusIsReserved() { + // when + final ReservationEntity entity = createTestEntity(); + + // then + assertThat(entity.getStatus()).isEqualTo(ReservationStatus.RESERVED); + } + + @Test + @DisplayName("์ดˆ๊ธฐ tossPaymentResponseAt์€ null์ด๋‹ค") + void initialTossPaymentResponseAtIsNull() { + // when + final ReservationEntity entity = createTestEntity(); + + // then + assertThat(entity.getTossPaymentResponseAt()).isNull(); + } + } + + @Nested + @DisplayName("updateReservationStatus ๋ฉ”์„œ๋“œ") + class UpdateReservationStatus { + + @Test + @DisplayName("RESERVED โ†’ PAYMENT_SUCCESS ์ „์ด ์„ฑ๊ณต") + void transitionsFromReservedToPaymentSuccess() { + // given + final ReservationEntity entity = createTestEntity(); + + // when + entity.updateReservationStatus(ReservationStatus.PAYMENT_SUCCESS); + + // then + assertThat(entity.getStatus()).isEqualTo(ReservationStatus.PAYMENT_SUCCESS); + } + + @Test + @DisplayName("RESERVED โ†’ PAYMENT_FAILED ์ „์ด ์„ฑ๊ณต") + void transitionsFromReservedToPaymentFailed() { + // given + final ReservationEntity entity = createTestEntity(); + + // when + entity.updateReservationStatus(ReservationStatus.PAYMENT_FAILED); + + // then + assertThat(entity.getStatus()).isEqualTo(ReservationStatus.PAYMENT_FAILED); + } + + @Test + @DisplayName("RESERVED โ†’ TICKET_ISSUED ์ „์ด ๋ถˆ๊ฐ€ โ†’ IllegalEnumTransitionException") + void throwsExceptionForInvalidTransition() { + // given + final ReservationEntity entity = createTestEntity(); + + // when & then + assertThatThrownBy(() -> entity.updateReservationStatus(ReservationStatus.TICKET_ISSUED)) + .isInstanceOf(IllegalEnumTransitionException.class); + } + + @Test + @DisplayName("PAYMENT_FAILED ์ƒํƒœ์—์„œ๋Š” ์–ด๋–ค ์ „์ด๋„ ๋ถˆ๊ฐ€ํ•˜๋‹ค") + void paymentFailedCannotTransition() { + // given + final ReservationEntity entity = createTestEntity(); + entity.updateReservationStatus(ReservationStatus.PAYMENT_FAILED); + + // when & then + assertThatThrownBy(() -> entity.updateReservationStatus(ReservationStatus.RESERVED)) + .isInstanceOf(IllegalEnumTransitionException.class); + } + + @Test + @DisplayName("์ •์ƒ ํ๋ฆ„: RESERVED โ†’ PAYMENT_SUCCESS โ†’ TICKET_ISSUED โ†’ PAYMENT_CANCELED") + void fullLifecycleTransition() { + // given + final ReservationEntity entity = createTestEntity(); + + // when + entity.updateReservationStatus(ReservationStatus.PAYMENT_SUCCESS); + entity.updateReservationStatus(ReservationStatus.TICKET_ISSUED); + entity.updateReservationStatus(ReservationStatus.PAYMENT_CANCELED); + + // then + assertThat(entity.getStatus()).isEqualTo(ReservationStatus.PAYMENT_CANCELED); + } + } + + @Nested + @DisplayName("updateTossPaymentResponseTime ๋ฉ”์„œ๋“œ") + class UpdateTossPaymentResponseTime { + + @Test + @DisplayName("tossPaymentResponseAt์„ ์ •์ƒ ์—…๋ฐ์ดํŠธํ•œ๋‹ค") + void updatesTossPaymentResponseTime() { + // given + final ReservationEntity entity = createTestEntity(); + final LocalDateTime responseTime = LocalDateTime.of(2026, 1, 19, 17, 5); + + // when + entity.updateTossPaymentResponseTime(responseTime); + + // then + assertThat(entity.getTossPaymentResponseAt()).isEqualTo(responseTime); + } + } + + @Nested + @DisplayName("Reservation.fromEntity ๋ฉ”์„œ๋“œ") + class FromEntity { + + @Test + @DisplayName("Entity์˜ ๋ชจ๋“  ํ•„๋“œ๊ฐ€ Domain ๊ฐ์ฒด๋กœ ์ •ํ™•ํžˆ ๋งคํ•‘๋œ๋‹ค") + void mapsAllFieldsCorrectly() { + // given + final ReservationEntity entity = createTestEntity(); + ReflectionTestUtils.setField(entity, "reservationId", 100L); + + // when + final Reservation reservation = Reservation.fromEntity(entity); + + // then + assertThat(reservation.getReservationId()).isEqualTo(100L); + assertThat(reservation.getReservationName()).isEqualTo(RESERVATION_NAME); + assertThat(reservation.getUserId()).isEqualTo(USER_ID); + assertThat(reservation.getEventId()).isEqualTo(EVENT_ID); + assertThat(reservation.getOrderId()).isEqualTo(ORDER_ID); + assertThat(reservation.getTotalAmount()).isEqualByComparingTo(TOTAL_AMOUNT); + assertThat(reservation.getCouponCode()).isEqualTo(COUPON_CODE); + assertThat(reservation.getStatus()).isEqualTo(ReservationStatus.RESERVED); + assertThat(reservation.getTossPaymentResponseAt()).isNull(); + } + } +} diff --git a/src/test/java/com/permitseoul/permitserver/domain/reservation/core/domain/ReservationStatusTest.java b/src/test/java/com/permitseoul/permitserver/domain/reservation/core/domain/ReservationStatusTest.java new file mode 100644 index 0000000..87b89c8 --- /dev/null +++ b/src/test/java/com/permitseoul/permitserver/domain/reservation/core/domain/ReservationStatusTest.java @@ -0,0 +1,106 @@ +package com.permitseoul.permitserver.domain.reservation.core.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("ReservationStatus ํ…Œ์ŠคํŠธ") +class ReservationStatusTest { + + @Nested + @DisplayName("์—ด๊ฑฐ๊ฐ’ ๊ธฐ๋ณธ ๊ฒ€์ฆ") + class EnumBasics { + + @Test + @DisplayName("์—ด๊ฑฐ๊ฐ’์€ 5๊ฐœ์ด๋‹ค") + void hasFiveValues() { + assertThat(ReservationStatus.values()).hasSize(5); + } + + @ParameterizedTest(name = "valueOf(\"{0}\")์œผ๋กœ ๋ณ€ํ™˜ ๊ฐ€๋Šฅํ•˜๋‹ค") + @EnumSource(ReservationStatus.class) + @DisplayName("๋ชจ๋“  ์—ด๊ฑฐ๊ฐ’์ด valueOf๋กœ ๋ณ€ํ™˜ ๊ฐ€๋Šฅํ•˜๋‹ค") + void valueOfWorksForAll(final ReservationStatus status) { + assertThat(ReservationStatus.valueOf(status.name())).isEqualTo(status); + } + } + + @Nested + @DisplayName("canTransitionTo - ํ—ˆ์šฉ๋˜๋Š” ์ „์ด") + class AllowedTransitions { + + @Test + @DisplayName("RESERVED โ†’ PAYMENT_SUCCESS ์ „์ด ๊ฐ€๋Šฅ") + void reservedToPaymentSuccess() { + assertThat(ReservationStatus.RESERVED.canTransitionTo(ReservationStatus.PAYMENT_SUCCESS)).isTrue(); + } + + @Test + @DisplayName("RESERVED โ†’ PAYMENT_FAILED ์ „์ด ๊ฐ€๋Šฅ") + void reservedToPaymentFailed() { + assertThat(ReservationStatus.RESERVED.canTransitionTo(ReservationStatus.PAYMENT_FAILED)).isTrue(); + } + + @Test + @DisplayName("PAYMENT_SUCCESS โ†’ TICKET_ISSUED ์ „์ด ๊ฐ€๋Šฅ") + void paymentSuccessToTicketIssued() { + assertThat(ReservationStatus.PAYMENT_SUCCESS.canTransitionTo(ReservationStatus.TICKET_ISSUED)).isTrue(); + } + + @Test + @DisplayName("TICKET_ISSUED โ†’ PAYMENT_CANCELED ์ „์ด ๊ฐ€๋Šฅ") + void ticketIssuedToPaymentCanceled() { + assertThat(ReservationStatus.TICKET_ISSUED.canTransitionTo(ReservationStatus.PAYMENT_CANCELED)).isTrue(); + } + } + + @Nested + @DisplayName("canTransitionTo - ๋ถˆํ—ˆ๋˜๋Š” ์ „์ด") + class DisallowedTransitions { + + private static Stream disallowedTransitions() { + return Stream.of( + // RESERVED์—์„œ ๋ถˆ๊ฐ€ + Arguments.of(ReservationStatus.RESERVED, ReservationStatus.RESERVED), + Arguments.of(ReservationStatus.RESERVED, ReservationStatus.TICKET_ISSUED), + Arguments.of(ReservationStatus.RESERVED, ReservationStatus.PAYMENT_CANCELED), + // PAYMENT_SUCCESS์—์„œ ๋ถˆ๊ฐ€ + Arguments.of(ReservationStatus.PAYMENT_SUCCESS, ReservationStatus.RESERVED), + Arguments.of(ReservationStatus.PAYMENT_SUCCESS, ReservationStatus.PAYMENT_SUCCESS), + Arguments.of(ReservationStatus.PAYMENT_SUCCESS, ReservationStatus.PAYMENT_FAILED), + Arguments.of(ReservationStatus.PAYMENT_SUCCESS, ReservationStatus.PAYMENT_CANCELED), + // TICKET_ISSUED์—์„œ ๋ถˆ๊ฐ€ + Arguments.of(ReservationStatus.TICKET_ISSUED, ReservationStatus.RESERVED), + Arguments.of(ReservationStatus.TICKET_ISSUED, ReservationStatus.PAYMENT_SUCCESS), + Arguments.of(ReservationStatus.TICKET_ISSUED, ReservationStatus.PAYMENT_FAILED), + Arguments.of(ReservationStatus.TICKET_ISSUED, ReservationStatus.TICKET_ISSUED), + // PAYMENT_FAILED์—์„œ ๋ชจ๋‘ ๋ถˆ๊ฐ€ + Arguments.of(ReservationStatus.PAYMENT_FAILED, ReservationStatus.RESERVED), + Arguments.of(ReservationStatus.PAYMENT_FAILED, ReservationStatus.PAYMENT_SUCCESS), + Arguments.of(ReservationStatus.PAYMENT_FAILED, ReservationStatus.PAYMENT_FAILED), + Arguments.of(ReservationStatus.PAYMENT_FAILED, ReservationStatus.TICKET_ISSUED), + Arguments.of(ReservationStatus.PAYMENT_FAILED, ReservationStatus.PAYMENT_CANCELED), + // PAYMENT_CANCELED์—์„œ ๋ชจ๋‘ ๋ถˆ๊ฐ€ + Arguments.of(ReservationStatus.PAYMENT_CANCELED, ReservationStatus.RESERVED), + Arguments.of(ReservationStatus.PAYMENT_CANCELED, ReservationStatus.PAYMENT_SUCCESS), + Arguments.of(ReservationStatus.PAYMENT_CANCELED, ReservationStatus.PAYMENT_FAILED), + Arguments.of(ReservationStatus.PAYMENT_CANCELED, ReservationStatus.TICKET_ISSUED), + Arguments.of(ReservationStatus.PAYMENT_CANCELED, ReservationStatus.PAYMENT_CANCELED)); + } + + @ParameterizedTest(name = "{0} โ†’ {1} ์ „์ด ๋ถˆ๊ฐ€") + @MethodSource("disallowedTransitions") + @DisplayName("๋ถˆํ—ˆ๋˜๋Š” ์ƒํƒœ ์ „์ด๋Š” false๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void disallowedTransitionReturnsFalse(final ReservationStatus from, final ReservationStatus to) { + assertThat(from.canTransitionTo(to)).isFalse(); + } + } +} diff --git a/src/test/java/com/permitseoul/permitserver/domain/sitemapimage/api/service/EventSiteMapImageServiceTest.java b/src/test/java/com/permitseoul/permitserver/domain/sitemapimage/api/service/EventSiteMapImageServiceTest.java new file mode 100644 index 0000000..22e3b4a --- /dev/null +++ b/src/test/java/com/permitseoul/permitserver/domain/sitemapimage/api/service/EventSiteMapImageServiceTest.java @@ -0,0 +1,80 @@ +package com.permitseoul.permitserver.domain.sitemapimage.api.service; + +import com.permitseoul.permitserver.domain.event.core.component.EventRetriever; +import com.permitseoul.permitserver.domain.event.core.domain.Event; +import com.permitseoul.permitserver.domain.event.core.domain.EventType; +import com.permitseoul.permitserver.domain.event.core.exception.EventNotfoundException; +import com.permitseoul.permitserver.domain.sitemapimage.api.dto.res.EventSiteMapGetResponse; +import com.permitseoul.permitserver.domain.sitemapimage.api.exception.SiteMapImageApiException; +import com.permitseoul.permitserver.domain.sitemapimage.core.component.SiteMapImageRetriever; +import com.permitseoul.permitserver.domain.sitemapimage.core.domain.EventSiteMapImage; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@DisplayName("EventSiteMapImageService ํ…Œ์ŠคํŠธ") +class EventSiteMapImageServiceTest { + + @Mock + private SiteMapImageRetriever siteMapImageRetriever; + @Mock + private EventRetriever eventRetriever; + @InjectMocks + private EventSiteMapImageService eventSiteMapImageService; + + private static final long EVENT_ID = 100L; + private static final LocalDateTime NOW = LocalDateTime.of(2026, 2, 13, 14, 0); + + private Event createEvent() { + return new Event(EVENT_ID, "ํ…Œ์ŠคํŠธ ์ด๋ฒคํŠธ", EventType.PERMIT, NOW.minusDays(1), NOW.plusDays(1), + "์„œ์šธ", "๋ผ์ธ์—…", "์ƒ์„ธ", 0, NOW.minusDays(7), NOW.plusDays(7), "CHECK-CODE"); + } + + @Test + @DisplayName("์ •์ƒ: ์‚ฌ์ดํŠธ๋งต ์ด๋ฏธ์ง€ ๋ชฉ๋ก ์กฐํšŒ") + void success() { + final Event event = createEvent(); + final List images = List.of( + new EventSiteMapImage(1L, 1, "https://example.com/map1.png", EVENT_ID), + new EventSiteMapImage(2L, 2, "https://example.com/map2.png", EVENT_ID)); + when(eventRetriever.findEventById(EVENT_ID)).thenReturn(event); + when(siteMapImageRetriever.findAllEventSiteMapImagesByEventId(EVENT_ID)).thenReturn(images); + + final EventSiteMapGetResponse result = eventSiteMapImageService.getEventSiteMapImages(EVENT_ID); + + assertThat(result.eventName()).isEqualTo("ํ…Œ์ŠคํŠธ ์ด๋ฒคํŠธ"); + assertThat(result.siteMapImages()).hasSize(2); + assertThat(result.siteMapImages().get(0).imageUrl()).isEqualTo("https://example.com/map1.png"); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์ด๋ฒคํŠธ ๋ฏธ์กด์žฌ โ†’ SiteMapImageApiException") + void throwsWhenEventNotFound() { + when(eventRetriever.findEventById(EVENT_ID)).thenThrow(new EventNotfoundException()); + + assertThatThrownBy(() -> eventSiteMapImageService.getEventSiteMapImages(EVENT_ID)) + .isInstanceOf(SiteMapImageApiException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์‚ฌ์ดํŠธ๋งต ์ด๋ฏธ์ง€ ์—†์Œ โ†’ SiteMapImageApiException") + void throwsWhenNoImages() { + final Event event = createEvent(); + when(eventRetriever.findEventById(EVENT_ID)).thenReturn(event); + when(siteMapImageRetriever.findAllEventSiteMapImagesByEventId(EVENT_ID)).thenReturn(List.of()); + + assertThatThrownBy(() -> eventSiteMapImageService.getEventSiteMapImages(EVENT_ID)) + .isInstanceOf(SiteMapImageApiException.class); + } +} diff --git a/src/test/java/com/permitseoul/permitserver/domain/ticket/api/service/TicketServiceTest.java b/src/test/java/com/permitseoul/permitserver/domain/ticket/api/service/TicketServiceTest.java new file mode 100644 index 0000000..979fdfa --- /dev/null +++ b/src/test/java/com/permitseoul/permitserver/domain/ticket/api/service/TicketServiceTest.java @@ -0,0 +1,424 @@ +package com.permitseoul.permitserver.domain.ticket.api.service; + +import com.permitseoul.permitserver.domain.event.core.component.EventRetriever; +import com.permitseoul.permitserver.domain.event.core.domain.Event; +import com.permitseoul.permitserver.domain.event.core.domain.EventType; +import com.permitseoul.permitserver.domain.event.core.exception.EventNotfoundException; +import com.permitseoul.permitserver.domain.payment.core.component.PaymentRetriever; +import com.permitseoul.permitserver.domain.ticket.api.dto.res.DoorValidateUserTicket; +import com.permitseoul.permitserver.domain.ticket.api.dto.res.EventTicketInfoResponse; +import com.permitseoul.permitserver.domain.ticket.api.dto.res.UserBuyTicketInfoResponse; +import com.permitseoul.permitserver.domain.ticket.api.exception.ConflictTicketException; +import com.permitseoul.permitserver.domain.ticket.api.exception.DateTicketException; +import com.permitseoul.permitserver.domain.ticket.api.exception.IllegalTicketException; +import com.permitseoul.permitserver.domain.ticket.api.exception.NotFoundTicketException; +import com.permitseoul.permitserver.domain.ticket.core.component.TicketRetriever; +import com.permitseoul.permitserver.domain.ticket.core.component.TicketUpdater; +import com.permitseoul.permitserver.domain.ticket.core.domain.Ticket; +import com.permitseoul.permitserver.domain.ticket.core.domain.TicketStatus; +import com.permitseoul.permitserver.domain.ticket.core.domain.entity.TicketEntity; +import com.permitseoul.permitserver.domain.ticket.core.exception.TicketNotFoundException; +import com.permitseoul.permitserver.domain.ticketround.core.component.TicketRoundRetriever; +import com.permitseoul.permitserver.domain.ticketround.core.domain.TicketRound; +import com.permitseoul.permitserver.domain.tickettype.core.component.TicketTypeRetriever; +import com.permitseoul.permitserver.domain.tickettype.core.domain.TicketType; +import com.permitseoul.permitserver.domain.tickettype.core.exception.TicketTypeNotfoundException; +import com.permitseoul.permitserver.global.redis.RedisManager; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@DisplayName("TicketService ํ…Œ์ŠคํŠธ") +class TicketServiceTest { + + @Mock + private TicketRoundRetriever ticketRoundRetriever; + @Mock + private TicketTypeRetriever ticketTypeRetriever; + @Mock + private TicketRetriever ticketRetriever; + @Mock + private EventRetriever eventRetriever; + @Mock + private PaymentRetriever paymentRetriever; + @Mock + private RedisManager redisManager; + @Mock + private TicketUpdater ticketUpdater; + + @InjectMocks + private TicketService ticketService; + + // โ”€โ”€ ๊ณตํ†ต ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ โ”€โ”€ + private static final String TICKET_CODE = "TKT-20260213-ABC123"; + private static final String CHECK_CODE = "EVENT-CHECK-001"; + private static final long TICKET_TYPE_ID = 10L; + private static final long EVENT_ID = 100L; + private static final LocalDateTime NOW = LocalDateTime.of(2026, 2, 13, 14, 0); + private static final LocalDateTime TICKET_START = NOW.minusHours(1); + private static final LocalDateTime TICKET_END = NOW.plusHours(5); + + private TicketEntity createTicketEntity(final TicketStatus status) { + final TicketEntity entity = TicketEntity.create(1L, "ORDER-001", TICKET_TYPE_ID, EVENT_ID, TICKET_CODE, + new BigDecimal("60000")); + if (status != TicketStatus.RESERVED) { + entity.updateTicketStatus(status); + } + return entity; + } + + private Ticket createTicket(final TicketStatus status) { + return Ticket.builder() + .ticketId(1L) + .userId(1L) + .orderId("ORDER-001") + .ticketTypeId(TICKET_TYPE_ID) + .eventId(EVENT_ID) + .ticketCode(TICKET_CODE) + .status(status) + .createdAt(NOW) + .ticketPrice(new BigDecimal("60000")) + .build(); + } + + private TicketType createTicketType(final LocalDateTime startAt, final LocalDateTime endAt) { + return new TicketType(TICKET_TYPE_ID, 1L, "1์ผ๊ถŒ", new BigDecimal("60000"), 100, 50, startAt, endAt); + } + + private Event createEvent() { + return new Event(EVENT_ID, "ํ…Œ์ŠคํŠธ ์ด๋ฒคํŠธ", EventType.PERMIT, NOW.minusDays(1), NOW.plusDays(1), + "์„œ์šธ", "๋ผ์ธ์—…", "์ƒ์„ธ", 0, NOW.minusDays(7), NOW.plusDays(7), CHECK_CODE); + } + + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // confirmTicketByStaffCode + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + @Nested + @DisplayName("confirmTicketByStaffCode") + class ConfirmTicketByStaffCodeTest { + + @Test + @DisplayName("์ •์ƒ: ์œ ํšจํ•œ ํ‹ฐ์ผ“ ์ฝ”๋“œ์™€ ์ฒดํฌ์ฝ”๋“œ๋กœ ์ž…์žฅ ํ™•์ธ ์„ฑ๊ณต") + void success() { + final TicketEntity ticketEntity = createTicketEntity(TicketStatus.RESERVED); + final TicketType ticketType = createTicketType(TICKET_START, TICKET_END); + final Event event = createEvent(); + + when(ticketRetriever.findTicketEntityByTicketCode(TICKET_CODE)).thenReturn(ticketEntity); + when(ticketTypeRetriever.findTicketTypeById(TICKET_TYPE_ID)).thenReturn(ticketType); + when(eventRetriever.findEventById(EVENT_ID)).thenReturn(event); + + ticketService.confirmTicketByStaffCode(TICKET_CODE, CHECK_CODE); + + verify(ticketUpdater).updateTicketStatus(ticketEntity, TicketStatus.USED); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์กด์žฌํ•˜์ง€ ์•Š๋Š” ํ‹ฐ์ผ“ ์ฝ”๋“œ") + void throwsWhenTicketNotFound() { + when(ticketRetriever.findTicketEntityByTicketCode(TICKET_CODE)) + .thenThrow(new TicketNotFoundException()); + + assertThatThrownBy(() -> ticketService.confirmTicketByStaffCode(TICKET_CODE, CHECK_CODE)) + .isInstanceOf(NotFoundTicketException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์กด์žฌํ•˜์ง€ ์•Š๋Š” ํ‹ฐ์ผ“ ํƒ€์ž…") + void throwsWhenTicketTypeNotFound() { + final TicketEntity ticketEntity = createTicketEntity(TicketStatus.RESERVED); + when(ticketRetriever.findTicketEntityByTicketCode(TICKET_CODE)).thenReturn(ticketEntity); + when(ticketTypeRetriever.findTicketTypeById(TICKET_TYPE_ID)) + .thenThrow(new TicketTypeNotfoundException()); + + assertThatThrownBy(() -> ticketService.confirmTicketByStaffCode(TICKET_CODE, CHECK_CODE)) + .isInstanceOf(NotFoundTicketException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ด๋ฒคํŠธ") + void throwsWhenEventNotFound() { + final TicketEntity ticketEntity = createTicketEntity(TicketStatus.RESERVED); + final TicketType ticketType = createTicketType(TICKET_START, TICKET_END); + when(ticketRetriever.findTicketEntityByTicketCode(TICKET_CODE)).thenReturn(ticketEntity); + when(ticketTypeRetriever.findTicketTypeById(TICKET_TYPE_ID)).thenReturn(ticketType); + when(eventRetriever.findEventById(EVENT_ID)).thenThrow(new EventNotfoundException()); + + assertThatThrownBy(() -> ticketService.confirmTicketByStaffCode(TICKET_CODE, CHECK_CODE)) + .isInstanceOf(NotFoundTicketException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์ด๋ฏธ ์‚ฌ์šฉ๋œ ํ‹ฐ์ผ“") + void throwsWhenTicketUsed() { + final TicketEntity ticketEntity = createTicketEntity(TicketStatus.USED); + when(ticketRetriever.findTicketEntityByTicketCode(TICKET_CODE)).thenReturn(ticketEntity); + + assertThatThrownBy(() -> ticketService.confirmTicketByStaffCode(TICKET_CODE, CHECK_CODE)) + .isInstanceOf(ConflictTicketException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์ทจ์†Œ๋œ ํ‹ฐ์ผ“") + void throwsWhenTicketCanceled() { + final TicketEntity ticketEntity = createTicketEntity(TicketStatus.CANCELED); + when(ticketRetriever.findTicketEntityByTicketCode(TICKET_CODE)).thenReturn(ticketEntity); + + assertThatThrownBy(() -> ticketService.confirmTicketByStaffCode(TICKET_CODE, CHECK_CODE)) + .isInstanceOf(IllegalTicketException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์ฒดํฌ์ฝ”๋“œ ๋ถˆ์ผ์น˜") + void throwsWhenCheckCodeMismatch() { + final TicketEntity ticketEntity = createTicketEntity(TicketStatus.RESERVED); + final TicketType ticketType = createTicketType(TICKET_START, TICKET_END); + final Event event = createEvent(); + when(ticketRetriever.findTicketEntityByTicketCode(TICKET_CODE)).thenReturn(ticketEntity); + when(ticketTypeRetriever.findTicketTypeById(TICKET_TYPE_ID)).thenReturn(ticketType); + when(eventRetriever.findEventById(EVENT_ID)).thenReturn(event); + + assertThatThrownBy(() -> ticketService.confirmTicketByStaffCode(TICKET_CODE, "WRONG-CODE")) + .isInstanceOf(IllegalTicketException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ํ‹ฐ์ผ“ ์ด์šฉ ๊ธฐ๊ฐ„ ์™ธ") + void throwsWhenOutOfDateRange() { + final TicketEntity ticketEntity = createTicketEntity(TicketStatus.RESERVED); + // ๋ฏธ๋ž˜ ๋‚ ์งœ๋กœ ์„ค์ •ํ•˜์—ฌ ํ˜„์žฌ ์‹œ๊ฐ„์ด ticketStartAt ์ด์ „์ด ๋˜๋„๋ก ํ•จ + final TicketType ticketType = createTicketType( + LocalDateTime.now().plusDays(10), + LocalDateTime.now().plusDays(11)); + when(ticketRetriever.findTicketEntityByTicketCode(TICKET_CODE)).thenReturn(ticketEntity); + when(ticketTypeRetriever.findTicketTypeById(TICKET_TYPE_ID)).thenReturn(ticketType); + + assertThatThrownBy(() -> ticketService.confirmTicketByStaffCode(TICKET_CODE, CHECK_CODE)) + .isInstanceOf(DateTicketException.class); + } + } + + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // confirmTicketByStaffCamera + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + @Nested + @DisplayName("confirmTicketByStaffCamera") + class ConfirmTicketByStaffCameraTest { + + @Test + @DisplayName("์ •์ƒ: ์นด๋ฉ”๋ผ๋กœ ํ‹ฐ์ผ“ ํ™•์ธ ์„ฑ๊ณต") + void success() { + final TicketEntity ticketEntity = createTicketEntity(TicketStatus.RESERVED); + final TicketType ticketType = createTicketType(TICKET_START, TICKET_END); + when(ticketRetriever.findTicketEntityByTicketCode(TICKET_CODE)).thenReturn(ticketEntity); + when(ticketTypeRetriever.findTicketTypeById(TICKET_TYPE_ID)).thenReturn(ticketType); + + ticketService.confirmTicketByStaffCamera(TICKET_CODE); + + verify(ticketUpdater).updateTicketStatus(ticketEntity, TicketStatus.USED); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์กด์žฌํ•˜์ง€ ์•Š๋Š” ํ‹ฐ์ผ“") + void throwsWhenTicketNotFound() { + when(ticketRetriever.findTicketEntityByTicketCode(TICKET_CODE)) + .thenThrow(new TicketNotFoundException()); + + assertThatThrownBy(() -> ticketService.confirmTicketByStaffCamera(TICKET_CODE)) + .isInstanceOf(NotFoundTicketException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์กด์žฌํ•˜์ง€ ์•Š๋Š” ํ‹ฐ์ผ“ ํƒ€์ž…") + void throwsWhenTicketTypeNotFound() { + final TicketEntity ticketEntity = createTicketEntity(TicketStatus.RESERVED); + when(ticketRetriever.findTicketEntityByTicketCode(TICKET_CODE)).thenReturn(ticketEntity); + when(ticketTypeRetriever.findTicketTypeById(TICKET_TYPE_ID)) + .thenThrow(new TicketTypeNotfoundException()); + + assertThatThrownBy(() -> ticketService.confirmTicketByStaffCamera(TICKET_CODE)) + .isInstanceOf(NotFoundTicketException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์ด๋ฏธ ์‚ฌ์šฉ๋œ ํ‹ฐ์ผ“") + void throwsWhenTicketUsed() { + final TicketEntity ticketEntity = createTicketEntity(TicketStatus.USED); + when(ticketRetriever.findTicketEntityByTicketCode(TICKET_CODE)).thenReturn(ticketEntity); + + assertThatThrownBy(() -> ticketService.confirmTicketByStaffCamera(TICKET_CODE)) + .isInstanceOf(ConflictTicketException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ํ‹ฐ์ผ“ ์ด์šฉ ๊ธฐ๊ฐ„ ์™ธ") + void throwsWhenOutOfDateRange() { + final TicketEntity ticketEntity = createTicketEntity(TicketStatus.RESERVED); + final TicketType ticketType = createTicketType( + LocalDateTime.now().plusDays(10), + LocalDateTime.now().plusDays(11)); + when(ticketRetriever.findTicketEntityByTicketCode(TICKET_CODE)).thenReturn(ticketEntity); + when(ticketTypeRetriever.findTicketTypeById(TICKET_TYPE_ID)).thenReturn(ticketType); + + assertThatThrownBy(() -> ticketService.confirmTicketByStaffCamera(TICKET_CODE)) + .isInstanceOf(DateTicketException.class); + } + } + + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // validateUserTicket + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + @Nested + @DisplayName("validateUserTicket") + class ValidateUserTicketTest { + + @Test + @DisplayName("์ •์ƒ: ์œ ํšจํ•œ ํ‹ฐ์ผ“ ๊ฒ€์ฆ ํ›„ DoorValidateUserTicket ๋ฐ˜ํ™˜") + void success() { + final Ticket ticket = createTicket(TicketStatus.RESERVED); + final TicketType ticketType = createTicketType(TICKET_START, TICKET_END); + final Event event = createEvent(); + when(ticketRetriever.findTicketByTicketCode(TICKET_CODE)).thenReturn(ticket); + when(ticketTypeRetriever.findTicketTypeById(TICKET_TYPE_ID)).thenReturn(ticketType); + when(eventRetriever.findEventById(EVENT_ID)).thenReturn(event); + + final DoorValidateUserTicket result = ticketService.validateUserTicket(TICKET_CODE); + + assertThat(result.eventName()).isEqualTo("ํ…Œ์ŠคํŠธ ์ด๋ฒคํŠธ"); + assertThat(result.ticketName()).isEqualTo("1์ผ๊ถŒ"); + assertThat(result.ticketStartDate()).isEqualTo(TICKET_START); + assertThat(result.ticketEndDate()).isEqualTo(TICKET_END); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์กด์žฌํ•˜์ง€ ์•Š๋Š” ํ‹ฐ์ผ“") + void throwsWhenTicketNotFound() { + when(ticketRetriever.findTicketByTicketCode(TICKET_CODE)) + .thenThrow(new TicketNotFoundException()); + + assertThatThrownBy(() -> ticketService.validateUserTicket(TICKET_CODE)) + .isInstanceOf(NotFoundTicketException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์กด์žฌํ•˜์ง€ ์•Š๋Š” ํ‹ฐ์ผ“ ํƒ€์ž…") + void throwsWhenTicketTypeNotFound() { + final Ticket ticket = createTicket(TicketStatus.RESERVED); + when(ticketRetriever.findTicketByTicketCode(TICKET_CODE)).thenReturn(ticket); + when(ticketTypeRetriever.findTicketTypeById(TICKET_TYPE_ID)) + .thenThrow(new TicketTypeNotfoundException()); + + assertThatThrownBy(() -> ticketService.validateUserTicket(TICKET_CODE)) + .isInstanceOf(NotFoundTicketException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ด๋ฒคํŠธ") + void throwsWhenEventNotFound() { + final Ticket ticket = createTicket(TicketStatus.RESERVED); + final TicketType ticketType = createTicketType(TICKET_START, TICKET_END); + when(ticketRetriever.findTicketByTicketCode(TICKET_CODE)).thenReturn(ticket); + when(ticketTypeRetriever.findTicketTypeById(TICKET_TYPE_ID)).thenReturn(ticketType); + when(eventRetriever.findEventById(EVENT_ID)).thenThrow(new EventNotfoundException()); + + assertThatThrownBy(() -> ticketService.validateUserTicket(TICKET_CODE)) + .isInstanceOf(NotFoundTicketException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์ด๋ฏธ ์‚ฌ์šฉ๋œ ํ‹ฐ์ผ“") + void throwsWhenTicketUsed() { + final Ticket ticket = createTicket(TicketStatus.USED); + when(ticketRetriever.findTicketByTicketCode(TICKET_CODE)).thenReturn(ticket); + + assertThatThrownBy(() -> ticketService.validateUserTicket(TICKET_CODE)) + .isInstanceOf(ConflictTicketException.class); + } + } + + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // getUserBuyTicketInfo + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + @Nested + @DisplayName("getUserBuyTicketInfo") + class GetUserBuyTicketInfoTest { + + @Test + @DisplayName("userId๊ฐ€ null์ด๋ฉด ๋นˆ ๋ฆฌ์ŠคํŠธ ๋ฐ˜ํ™˜") + void returnsEmptyWhenUserIdNull() { + final UserBuyTicketInfoResponse result = ticketService.getUserBuyTicketInfo(null); + + assertThat(result.orders()).isEmpty(); + verifyNoInteractions(ticketRetriever); + } + + @Test + @DisplayName("ํ‹ฐ์ผ“์ด ์—†์œผ๋ฉด ๋นˆ ๋ฆฌ์ŠคํŠธ ๋ฐ˜ํ™˜") + void returnsEmptyWhenNoTickets() { + when(ticketRetriever.findAllTicketsByUserId(1L)).thenReturn(List.of()); + + final UserBuyTicketInfoResponse result = ticketService.getUserBuyTicketInfo(1L); + + assertThat(result.orders()).isEmpty(); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ํ‹ฐ์ผ“ ํƒ€์ž… ๋ฏธ์กด์žฌ") + void throwsWhenTicketTypeNotFound() { + final Ticket ticket = createTicket(TicketStatus.RESERVED); + when(ticketRetriever.findAllTicketsByUserId(1L)).thenReturn(List.of(ticket)); + when(ticketTypeRetriever.findAllTicketTypeById(List.of(TICKET_TYPE_ID))) + .thenThrow(new TicketTypeNotfoundException()); + + assertThatThrownBy(() -> ticketService.getUserBuyTicketInfo(1L)) + .isInstanceOf(NotFoundTicketException.class); + } + } + + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // getEventTicketInfo + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + @Nested + @DisplayName("getEventTicketInfo") + class GetEventTicketInfoTest { + + @Test + @DisplayName("ํŒ๋งค ๊ฐ€๋Šฅํ•œ ๋ผ์šด๋“œ๊ฐ€ ์—†์œผ๋ฉด ๋นˆ ๋ฆฌ์ŠคํŠธ ๋ฐ˜ํ™˜") + void returnsEmptyWhenNoRounds() { + when(ticketRoundRetriever.findSalesOrSalesEndTicketRoundByEventId(eq(EVENT_ID), any())) + .thenReturn(List.of()); + + final EventTicketInfoResponse result = ticketService.getEventTicketInfo(EVENT_ID, NOW); + + assertThat(result.rounds()).isEmpty(); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ๋ผ์šด๋“œ์— ํ•ด๋‹นํ•˜๋Š” ํ‹ฐ์ผ“ ํƒ€์ž…์ด ์—†์Œ") + void throwsWhenTicketTypeNotFound() { + final TicketRound round = new TicketRound(1L, EVENT_ID, "1์ฐจ", NOW.minusDays(1), NOW.plusDays(1)); + when(ticketRoundRetriever.findSalesOrSalesEndTicketRoundByEventId(eq(EVENT_ID), any())) + .thenReturn(List.of(round)); + when(ticketTypeRetriever.findTicketTypeListByRoundIdList(List.of(1L))) + .thenThrow(new TicketTypeNotfoundException()); + + assertThatThrownBy(() -> ticketService.getEventTicketInfo(EVENT_ID, NOW)) + .isInstanceOf(NotFoundTicketException.class); + } + } +} diff --git a/src/test/java/com/permitseoul/permitserver/domain/ticket/core/component/TicketGeneratorTest.java b/src/test/java/com/permitseoul/permitserver/domain/ticket/core/component/TicketGeneratorTest.java new file mode 100644 index 0000000..8b4b932 --- /dev/null +++ b/src/test/java/com/permitseoul/permitserver/domain/ticket/core/component/TicketGeneratorTest.java @@ -0,0 +1,169 @@ +package com.permitseoul.permitserver.domain.ticket.core.component; + +import com.permitseoul.permitserver.domain.reservation.core.domain.Reservation; +import com.permitseoul.permitserver.domain.reservation.core.domain.ReservationStatus; +import com.permitseoul.permitserver.domain.reservationticket.core.domain.ReservationTicket; +import com.permitseoul.permitserver.domain.ticket.core.domain.Ticket; +import com.permitseoul.permitserver.domain.ticket.core.domain.TicketStatus; +import com.permitseoul.permitserver.domain.tickettype.core.domain.entity.TicketTypeEntity; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayName("TicketGenerator ํ…Œ์ŠคํŠธ") +class TicketGeneratorTest { + + private static final long USER_ID = 1L; + private static final long EVENT_ID = 10L; + private static final String ORDER_ID = "ORDER-20260119-001"; + private static final BigDecimal TICKET_TYPE_PRICE = new BigDecimal("60000"); + private static final BigDecimal COUPON_TOTAL_AMOUNT = new BigDecimal("50000"); + + private Reservation createReservation(final String couponCode) { + return new Reservation( + 100L, "ํ…Œ์ŠคํŠธ ์˜ˆ์•ฝ", USER_ID, EVENT_ID, ORDER_ID, + COUPON_TOTAL_AMOUNT, couponCode, ReservationStatus.RESERVED, null); + } + + private ReservationTicket createReservationTicket(final long ticketTypeId, final int count) { + return new ReservationTicket(1L, ticketTypeId, ORDER_ID, count); + } + + private TicketTypeEntity createTicketTypeEntity(final long ticketTypeId) { + final TicketTypeEntity entity = TicketTypeEntity.create( + 1L, "VIP์„", TICKET_TYPE_PRICE, 100, + LocalDateTime.of(2026, 1, 19, 17, 0), + LocalDateTime.of(2026, 1, 19, 21, 0)); + ReflectionTestUtils.setField(entity, "ticketTypeId", ticketTypeId); + return entity; + } + + @Nested + @DisplayName("generatePublicTickets ๋ฉ”์„œ๋“œ") + class GeneratePublicTickets { + + @Test + @DisplayName("์ฟ ํฐ ์—†์„ ๋•Œ ticketTypeEntity์˜ ๊ฐ€๊ฒฉ์„ ์‚ฌ์šฉํ•œ๋‹ค") + void usesTicketTypePriceWithoutCoupon() { + // given + final Reservation reservation = createReservation(null); + final List reservationTickets = List.of( + createReservationTicket(5L, 2)); + final List ticketTypes = List.of(createTicketTypeEntity(5L)); + + // when + final List tickets = TicketGenerator.generatePublicTickets( + reservationTickets, USER_ID, reservation, ticketTypes); + + // then + assertThat(tickets).hasSize(2); + tickets.forEach(ticket -> assertThat(ticket.getTicketPrice()) + .isEqualByComparingTo(TICKET_TYPE_PRICE)); + } + + @Test + @DisplayName("์ฟ ํฐ ์žˆ์„ ๋•Œ reservation์˜ totalAmount๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค") + void usesTotalAmountWithCoupon() { + // given + final Reservation reservation = createReservation("COUPON-001"); + final List reservationTickets = List.of( + createReservationTicket(5L, 1)); + final List ticketTypes = List.of(createTicketTypeEntity(5L)); + + // when + final List tickets = TicketGenerator.generatePublicTickets( + reservationTickets, USER_ID, reservation, ticketTypes); + + // then + assertThat(tickets).hasSize(1); + assertThat(tickets.get(0).getTicketPrice()).isEqualByComparingTo(COUPON_TOTAL_AMOUNT); + } + + @Test + @DisplayName("์ƒ์„ฑ๋œ Ticket์˜ ๊ธฐ๋ณธ ํ•„๋“œ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์„ค์ •๋œ๋‹ค") + void setsTicketFieldsCorrectly() { + // given + final Reservation reservation = createReservation(null); + final List reservationTickets = List.of( + createReservationTicket(5L, 1)); + final List ticketTypes = List.of(createTicketTypeEntity(5L)); + + // when + final List tickets = TicketGenerator.generatePublicTickets( + reservationTickets, USER_ID, reservation, ticketTypes); + + // then + final Ticket ticket = tickets.get(0); + assertThat(ticket.getUserId()).isEqualTo(USER_ID); + assertThat(ticket.getOrderId()).isEqualTo(ORDER_ID); + assertThat(ticket.getTicketTypeId()).isEqualTo(5L); + assertThat(ticket.getEventId()).isEqualTo(EVENT_ID); + assertThat(ticket.getStatus()).isEqualTo(TicketStatus.RESERVED); + assertThat(ticket.getTicketCode()).matches("[0-9A-F]{10}"); + } + + @Test + @DisplayName("์—ฌ๋Ÿฌ ReservationTicket์˜ count ํ•ฉ๋งŒํผ Ticket์ด ์ƒ์„ฑ๋œ๋‹ค") + void generatesCorrectNumberOfTickets() { + // given + final Reservation reservation = createReservation(null); + final List reservationTickets = List.of( + createReservationTicket(5L, 3), + createReservationTicket(6L, 2)); + final List ticketTypes = List.of( + createTicketTypeEntity(5L), + createTicketTypeEntity(6L)); + + // when + final List tickets = TicketGenerator.generatePublicTickets( + reservationTickets, USER_ID, reservation, ticketTypes); + + // then + assertThat(tickets).hasSize(5); + } + + @Test + @DisplayName("ticketTypeId์— ๋งค์นญ๋˜๋Š” TicketTypeEntity๊ฐ€ ์—†์œผ๋ฉด IllegalArgumentException์„ ๋˜์ง„๋‹ค") + void throwsExceptionWhenTicketTypeNotFound() { + // given + final Reservation reservation = createReservation(null); + final List reservationTickets = List.of( + createReservationTicket(999L, 1)); + final List ticketTypes = List.of(createTicketTypeEntity(5L)); + + // when & then + assertThatThrownBy(() -> TicketGenerator.generatePublicTickets( + reservationTickets, USER_ID, reservation, ticketTypes)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("๊ฐ Ticket์˜ ticketCode๊ฐ€ ์„œ๋กœ ๊ณ ์œ ํ•˜๋‹ค") + void generatesUniqueTicketCodes() { + // given + final Reservation reservation = createReservation(null); + final List reservationTickets = List.of( + createReservationTicket(5L, 10)); + final List ticketTypes = List.of(createTicketTypeEntity(5L)); + + // when + final List tickets = TicketGenerator.generatePublicTickets( + reservationTickets, USER_ID, reservation, ticketTypes); + + // then + final long uniqueCodeCount = tickets.stream() + .map(Ticket::getTicketCode) + .distinct() + .count(); + assertThat(uniqueCodeCount).isEqualTo(10); + } + } +} diff --git a/src/test/java/com/permitseoul/permitserver/domain/ticket/core/component/TicketRetrieverTest.java b/src/test/java/com/permitseoul/permitserver/domain/ticket/core/component/TicketRetrieverTest.java new file mode 100644 index 0000000..a9cf25f --- /dev/null +++ b/src/test/java/com/permitseoul/permitserver/domain/ticket/core/component/TicketRetrieverTest.java @@ -0,0 +1,197 @@ +package com.permitseoul.permitserver.domain.ticket.core.component; + +import com.permitseoul.permitserver.domain.ticket.core.domain.Ticket; +import com.permitseoul.permitserver.domain.ticket.core.domain.entity.TicketEntity; +import com.permitseoul.permitserver.domain.ticket.core.exception.TicketNotFoundException; +import com.permitseoul.permitserver.domain.ticket.core.repository.TicketRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +import java.math.BigDecimal; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.BDDMockito.given; + +@DisplayName("TicketRetriever ํ…Œ์ŠคํŠธ") +@ExtendWith(MockitoExtension.class) +class TicketRetrieverTest { + + @Mock + private TicketRepository ticketRepository; + + @InjectMocks + private TicketRetriever ticketRetriever; + + private TicketEntity createTestEntity() { + final TicketEntity entity = TicketEntity.create(1L, "ORDER-001", 5L, 10L, "ABC1234567", + new BigDecimal("60000")); + ReflectionTestUtils.setField(entity, "ticketId", 100L); + return entity; + } + + @Nested + @DisplayName("findTicketEntityByTicketCode ๋ฉ”์„œ๋“œ") + class FindTicketEntityByTicketCode { + + @Test + @DisplayName("์กด์žฌํ•˜๋Š” ํ‹ฐ์ผ“ ์ฝ”๋“œ๋กœ ์กฐํšŒํ•˜๋ฉด TicketEntity๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void returnsEntityWhenFound() { + // given + final TicketEntity entity = createTestEntity(); + given(ticketRepository.findByTicketCode("ABC1234567")).willReturn(Optional.of(entity)); + + // when + final TicketEntity result = ticketRetriever.findTicketEntityByTicketCode("ABC1234567"); + + // then + assertThat(result.getTicketId()).isEqualTo(100L); + assertThat(result.getTicketCode()).isEqualTo("ABC1234567"); + } + + @Test + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ํ‹ฐ์ผ“ ์ฝ”๋“œ๋กœ ์กฐํšŒํ•˜๋ฉด TicketNotFoundException์„ ๋˜์ง„๋‹ค") + void throwsExceptionWhenNotFound() { + // given + given(ticketRepository.findByTicketCode("INVALID")).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> ticketRetriever.findTicketEntityByTicketCode("INVALID")) + .isInstanceOf(TicketNotFoundException.class); + } + } + + @Nested + @DisplayName("findAllTicketsByOrderIdAndUserId ๋ฉ”์„œ๋“œ") + class FindAllTicketsByOrderIdAndUserId { + + @Test + @DisplayName("ํ‹ฐ์ผ“์ด ์กด์žฌํ•˜๋ฉด Ticket ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void returnsTicketListWhenFound() { + // given + final TicketEntity entity = createTestEntity(); + given(ticketRepository.findAllByOrderIdAndUserId("ORDER-001", 1L)) + .willReturn(List.of(entity)); + + // when + final List result = ticketRetriever.findAllTicketsByOrderIdAndUserId("ORDER-001", 1L); + + // then + assertThat(result).hasSize(1); + assertThat(result.get(0).getTicketId()).isEqualTo(100L); + } + + @Test + @DisplayName("๋นˆ ๋ฆฌ์ŠคํŠธ๋ฉด TicketNotFoundException์„ ๋˜์ง„๋‹ค") + void throwsExceptionWhenEmpty() { + // given + given(ticketRepository.findAllByOrderIdAndUserId("ORDER-001", 1L)) + .willReturn(Collections.emptyList()); + + // when & then + assertThatThrownBy(() -> ticketRetriever.findAllTicketsByOrderIdAndUserId("ORDER-001", 1L)) + .isInstanceOf(TicketNotFoundException.class); + } + } + + @Nested + @DisplayName("findAllTicketEntitiesById ๋ฉ”์„œ๋“œ") + class FindAllTicketEntitiesById { + + @Test + @DisplayName("ID ๋ชฉ๋ก์œผ๋กœ ์กฐํšŒํ•˜๋ฉด TicketEntity ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void returnsEntityListWhenFound() { + // given + final TicketEntity entity = createTestEntity(); + given(ticketRepository.findAllById(List.of(100L))).willReturn(List.of(entity)); + + // when + final List result = ticketRetriever.findAllTicketEntitiesById(List.of(100L)); + + // then + assertThat(result).hasSize(1); + } + + @Test + @DisplayName("๋นˆ ๋ฆฌ์ŠคํŠธ๋ฉด TicketNotFoundException์„ ๋˜์ง„๋‹ค") + void throwsExceptionWhenEmpty() { + // given + given(ticketRepository.findAllById(List.of(999L))).willReturn(Collections.emptyList()); + + // when & then + assertThatThrownBy(() -> ticketRetriever.findAllTicketEntitiesById(List.of(999L))) + .isInstanceOf(TicketNotFoundException.class); + } + } + + @Nested + @DisplayName("findAllTicketsByUserId ๋ฉ”์„œ๋“œ") + class FindAllTicketsByUserId { + + @Test + @DisplayName("ํ‹ฐ์ผ“์ด ์กด์žฌํ•˜๋ฉด Ticket ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void returnsTicketListWhenFound() { + // given + final TicketEntity entity = createTestEntity(); + given(ticketRepository.findAllByUserId(1L)).willReturn(List.of(entity)); + + // when + final List result = ticketRetriever.findAllTicketsByUserId(1L); + + // then + assertThat(result).hasSize(1); + } + + @Test + @DisplayName("๋นˆ ๋ฆฌ์ŠคํŠธ๋ฉด ๋นˆ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค (์˜ˆ์™ธ ์—†์Œ)") + void returnsEmptyListWhenNoTickets() { + // given + given(ticketRepository.findAllByUserId(1L)).willReturn(Collections.emptyList()); + + // when + final List result = ticketRetriever.findAllTicketsByUserId(1L); + + // then + assertThat(result).isEmpty(); + } + } + + @Nested + @DisplayName("findTicketByTicketCode ๋ฉ”์„œ๋“œ") + class FindTicketByTicketCode { + + @Test + @DisplayName("์กด์žฌํ•˜๋Š” ์ฝ”๋“œ๋กœ ์กฐํšŒํ•˜๋ฉด Ticket์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void returnsTicketWhenFound() { + // given + final TicketEntity entity = createTestEntity(); + given(ticketRepository.findByTicketCode("ABC1234567")).willReturn(Optional.of(entity)); + + // when + final Ticket result = ticketRetriever.findTicketByTicketCode("ABC1234567"); + + // then + assertThat(result.getTicketCode()).isEqualTo("ABC1234567"); + } + + @Test + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ฝ”๋“œ๋กœ ์กฐํšŒํ•˜๋ฉด TicketNotFoundException์„ ๋˜์ง„๋‹ค") + void throwsExceptionWhenNotFound() { + // given + given(ticketRepository.findByTicketCode("INVALID")).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> ticketRetriever.findTicketByTicketCode("INVALID")) + .isInstanceOf(TicketNotFoundException.class); + } + } +} diff --git a/src/test/java/com/permitseoul/permitserver/domain/ticket/core/domain/TicketEntityTest.java b/src/test/java/com/permitseoul/permitserver/domain/ticket/core/domain/TicketEntityTest.java new file mode 100644 index 0000000..eaeb918 --- /dev/null +++ b/src/test/java/com/permitseoul/permitserver/domain/ticket/core/domain/TicketEntityTest.java @@ -0,0 +1,139 @@ +package com.permitseoul.permitserver.domain.ticket.core.domain; + +import com.permitseoul.permitserver.domain.ticket.core.domain.entity.TicketEntity; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import java.math.BigDecimal; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("Ticket & TicketEntity ํ…Œ์ŠคํŠธ") +class TicketEntityTest { + + private static final long USER_ID = 1L; + private static final String ORDER_ID = "ORDER-20260119-001"; + private static final long TICKET_TYPE_ID = 5L; + private static final long EVENT_ID = 10L; + private static final String TICKET_CODE = "ABC1234567"; + private static final BigDecimal TICKET_PRICE = new BigDecimal("60000"); + + private TicketEntity createTestEntity() { + return TicketEntity.create(USER_ID, ORDER_ID, TICKET_TYPE_ID, EVENT_ID, TICKET_CODE, TICKET_PRICE); + } + + @Nested + @DisplayName("TicketEntity.create ๋ฉ”์„œ๋“œ") + class Create { + + @Test + @DisplayName("์ •์ƒ์ ์ธ ๊ฐ’์œผ๋กœ TicketEntity๋ฅผ ์ƒ์„ฑํ•œ๋‹ค") + void createsTicketEntitySuccessfully() { + // when + final TicketEntity entity = createTestEntity(); + + // then + assertThat(entity.getUserId()).isEqualTo(USER_ID); + assertThat(entity.getOrderId()).isEqualTo(ORDER_ID); + assertThat(entity.getTicketTypeId()).isEqualTo(TICKET_TYPE_ID); + assertThat(entity.getEventId()).isEqualTo(EVENT_ID); + assertThat(entity.getTicketCode()).isEqualTo(TICKET_CODE); + assertThat(entity.getTicketPrice()).isEqualByComparingTo(TICKET_PRICE); + } + + @Test + @DisplayName("์ดˆ๊ธฐ status๋Š” RESERVED์ด๋‹ค") + void initialStatusIsReserved() { + // when + final TicketEntity entity = createTestEntity(); + + // then + assertThat(entity.getStatus()).isEqualTo(TicketStatus.RESERVED); + } + + @Test + @DisplayName("์ƒ์„ฑ ์งํ›„ ticketId๋Š” null์ด๋‹ค (@GeneratedValue)") + void ticketIdIsNullAfterCreate() { + // when + final TicketEntity entity = createTestEntity(); + + // then + assertThat(entity.getTicketId()).isNull(); + } + } + + @Nested + @DisplayName("updateTicketStatus ๋ฉ”์„œ๋“œ") + class UpdateTicketStatus { + + @Test + @DisplayName("USED๋กœ ๋ณ€๊ฒฝํ•˜๋ฉด usedTime์ด ์„ค์ •๋œ๋‹ค") + void setsUsedTimeWhenStatusIsUsed() { + // given + final TicketEntity entity = createTestEntity(); + + // when + entity.updateTicketStatus(TicketStatus.USED); + + // then + assertThat(entity.getStatus()).isEqualTo(TicketStatus.USED); + assertThat(entity.getUsedTime()).isNotNull(); + } + + @Test + @DisplayName("CANCELED๋กœ ๋ณ€๊ฒฝํ•˜๋ฉด usedTime์€ ์„ค์ •๋˜์ง€ ์•Š๋Š”๋‹ค") + void doesNotSetUsedTimeWhenStatusIsCanceled() { + // given + final TicketEntity entity = createTestEntity(); + + // when + entity.updateTicketStatus(TicketStatus.CANCELED); + + // then + assertThat(entity.getStatus()).isEqualTo(TicketStatus.CANCELED); + assertThat(entity.getUsedTime()).isNull(); + } + + @Test + @DisplayName("RESERVED์—์„œ USED๋กœ ๋ณ€๊ฒฝ ํ›„ ์ƒํƒœ๊ฐ’์ด ์˜ฌ๋ฐ”๋ฅด๋‹ค") + void transitionsFromReservedToUsed() { + // given + final TicketEntity entity = createTestEntity(); + assertThat(entity.getStatus()).isEqualTo(TicketStatus.RESERVED); + + // when + entity.updateTicketStatus(TicketStatus.USED); + + // then + assertThat(entity.getStatus()).isEqualTo(TicketStatus.USED); + } + } + + @Nested + @DisplayName("Ticket.fromEntity ๋ฉ”์„œ๋“œ") + class FromEntity { + + @Test + @DisplayName("Entity์˜ ๋ชจ๋“  ํ•„๋“œ๊ฐ€ Domain ๊ฐ์ฒด๋กœ ์ •ํ™•ํžˆ ๋งคํ•‘๋œ๋‹ค") + void mapsAllFieldsCorrectly() { + // given + final TicketEntity entity = createTestEntity(); + ReflectionTestUtils.setField(entity, "ticketId", 100L); + + // when + final Ticket ticket = Ticket.fromEntity(entity); + + // then + assertThat(ticket.getTicketId()).isEqualTo(100L); + assertThat(ticket.getUserId()).isEqualTo(USER_ID); + assertThat(ticket.getOrderId()).isEqualTo(ORDER_ID); + assertThat(ticket.getTicketTypeId()).isEqualTo(TICKET_TYPE_ID); + assertThat(ticket.getEventId()).isEqualTo(EVENT_ID); + assertThat(ticket.getTicketCode()).isEqualTo(TICKET_CODE); + assertThat(ticket.getStatus()).isEqualTo(TicketStatus.RESERVED); + assertThat(ticket.getTicketPrice()).isEqualByComparingTo(TICKET_PRICE); + } + } +} diff --git a/src/test/java/com/permitseoul/permitserver/domain/user/api/service/UserServiceTest.java b/src/test/java/com/permitseoul/permitserver/domain/user/api/service/UserServiceTest.java new file mode 100644 index 0000000..4c58b1c --- /dev/null +++ b/src/test/java/com/permitseoul/permitserver/domain/user/api/service/UserServiceTest.java @@ -0,0 +1,149 @@ +package com.permitseoul.permitserver.domain.user.api.service; + +import com.permitseoul.permitserver.domain.user.api.dto.UserInfoResponse; +import com.permitseoul.permitserver.domain.user.api.exception.ConflictUserException; +import com.permitseoul.permitserver.domain.user.api.exception.NotfoundUserException; +import com.permitseoul.permitserver.domain.user.core.component.UserRetriever; +import com.permitseoul.permitserver.domain.user.core.component.UserUpdater; +import com.permitseoul.permitserver.domain.user.core.domain.Gender; +import com.permitseoul.permitserver.domain.user.core.domain.SocialType; +import com.permitseoul.permitserver.domain.user.core.domain.User; +import com.permitseoul.permitserver.domain.user.core.domain.UserRole; +import com.permitseoul.permitserver.domain.user.core.domain.entity.UserEntity; +import com.permitseoul.permitserver.domain.user.core.exception.UserDuplicateException; +import com.permitseoul.permitserver.domain.user.core.exception.UserNotFoundException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@DisplayName("UserService ํ…Œ์ŠคํŠธ") +class UserServiceTest { + + @Mock + private UserRetriever userRetriever; + @Mock + private UserUpdater userUpdater; + @InjectMocks + private UserService userService; + + private static final long USER_ID = 1L; + + private User createUser() { + return new User(USER_ID, "ํ™๊ธธ๋™", Gender.MALE, 25, "test@email.com", "social123", SocialType.KAKAO, + UserRole.USER); + } + + @Nested + @DisplayName("checkEmailDuplicated") + class CheckEmailDuplicatedTest { + + @Test + @DisplayName("์ •์ƒ: ์ค‘๋ณต๋˜์ง€ ์•Š์€ ์ด๋ฉ”์ผ") + void success() { + doNothing().when(userRetriever).validEmailDuplicated("new@email.com"); + + userService.checkEmailDuplicated("new@email.com"); + + verify(userRetriever).validEmailDuplicated("new@email.com"); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์ด๋ฉ”์ผ ์ค‘๋ณต โ†’ ConflictUserException") + void throwsWhenDuplicated() { + doThrow(new UserDuplicateException()).when(userRetriever).validEmailDuplicated("dup@email.com"); + + assertThatThrownBy(() -> userService.checkEmailDuplicated("dup@email.com")) + .isInstanceOf(ConflictUserException.class); + } + } + + @Nested + @DisplayName("getUserInfo") + class GetUserInfoTest { + + @Test + @DisplayName("์ •์ƒ: ์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ") + void success() { + when(userRetriever.findUserById(USER_ID)).thenReturn(createUser()); + + final UserInfoResponse result = userService.getUserInfo(USER_ID); + + assertThat(result.name()).isEqualTo("ํ™๊ธธ๋™"); + assertThat(result.age()).isEqualTo(25); + assertThat(result.gender()).isEqualTo(Gender.MALE); + assertThat(result.email()).isEqualTo("test@email.com"); + assertThat(result.role()).isEqualTo(UserRole.USER); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์‚ฌ์šฉ์ž ๋ฏธ์กด์žฌ โ†’ NotfoundUserException") + void throwsWhenNotFound() { + when(userRetriever.findUserById(USER_ID)).thenThrow(new UserNotFoundException()); + + assertThatThrownBy(() -> userService.getUserInfo(USER_ID)) + .isInstanceOf(NotfoundUserException.class); + } + } + + @Nested + @DisplayName("updateUserInfo") + class UpdateUserInfoTest { + + @Test + @DisplayName("์ •์ƒ: ์ด๋ฉ”์ผ ์—†์ด ์‚ฌ์šฉ์ž ์ •๋ณด ์ˆ˜์ •") + void successWithoutEmail() { + final UserEntity userEntity = UserEntity.create("ํ™๊ธธ๋™", Gender.MALE, 25, "old@email.com", "social123", + SocialType.KAKAO, UserRole.USER); + when(userRetriever.findUserEntityById(USER_ID)).thenReturn(userEntity); + + userService.updateUserInfo(USER_ID, "๊น€๊ธธ๋™", Gender.FEMALE, null); + + verify(userUpdater).updateUserInfo(userEntity, "๊น€๊ธธ๋™", Gender.FEMALE, null); + verify(userRetriever, never()).validEmailDuplicated(any()); + } + + @Test + @DisplayName("์ •์ƒ: ์ด๋ฉ”์ผ ํฌํ•จ ์‚ฌ์šฉ์ž ์ •๋ณด ์ˆ˜์ •") + void successWithEmail() { + final UserEntity userEntity = UserEntity.create("ํ™๊ธธ๋™", Gender.MALE, 25, "old@email.com", "social123", + SocialType.KAKAO, UserRole.USER); + when(userRetriever.findUserEntityById(USER_ID)).thenReturn(userEntity); + doNothing().when(userRetriever).validEmailDuplicated("new@email.com"); + + userService.updateUserInfo(USER_ID, "๊น€๊ธธ๋™", Gender.FEMALE, "new@email.com"); + + verify(userRetriever).validEmailDuplicated("new@email.com"); + verify(userUpdater).updateUserInfo(userEntity, "๊น€๊ธธ๋™", Gender.FEMALE, "new@email.com"); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์‚ฌ์šฉ์ž ๋ฏธ์กด์žฌ โ†’ NotfoundUserException") + void throwsWhenUserNotFound() { + when(userRetriever.findUserEntityById(USER_ID)).thenThrow(new UserNotFoundException()); + + assertThatThrownBy(() -> userService.updateUserInfo(USER_ID, "๊น€๊ธธ๋™", Gender.MALE, null)) + .isInstanceOf(NotfoundUserException.class); + } + + @Test + @DisplayName("์˜ˆ์™ธ: ์ด๋ฉ”์ผ ์ค‘๋ณต โ†’ ConflictUserException") + void throwsWhenEmailDuplicated() { + final UserEntity userEntity = UserEntity.create("ํ™๊ธธ๋™", Gender.MALE, 25, "old@email.com", "social123", + SocialType.KAKAO, UserRole.USER); + when(userRetriever.findUserEntityById(USER_ID)).thenReturn(userEntity); + doThrow(new UserDuplicateException()).when(userRetriever).validEmailDuplicated("dup@email.com"); + + assertThatThrownBy(() -> userService.updateUserInfo(USER_ID, "๊น€๊ธธ๋™", Gender.MALE, "dup@email.com")) + .isInstanceOf(ConflictUserException.class); + } + } +} diff --git a/src/test/java/com/permitseoul/permitserver/domain/user/core/component/UserRetrieverTest.java b/src/test/java/com/permitseoul/permitserver/domain/user/core/component/UserRetrieverTest.java new file mode 100644 index 0000000..69f79af --- /dev/null +++ b/src/test/java/com/permitseoul/permitserver/domain/user/core/component/UserRetrieverTest.java @@ -0,0 +1,245 @@ +package com.permitseoul.permitserver.domain.user.core.component; + +import com.permitseoul.permitserver.domain.user.core.domain.Gender; +import com.permitseoul.permitserver.domain.user.core.domain.SocialType; +import com.permitseoul.permitserver.domain.user.core.domain.User; +import com.permitseoul.permitserver.domain.user.core.domain.UserRole; +import com.permitseoul.permitserver.domain.user.core.domain.entity.UserEntity; +import com.permitseoul.permitserver.domain.user.core.exception.UserDuplicateException; +import com.permitseoul.permitserver.domain.user.core.exception.UserNotFoundException; +import com.permitseoul.permitserver.domain.user.core.repository.UserRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.BDDMockito.given; + +@DisplayName("UserRetriever ํ…Œ์ŠคํŠธ") +@ExtendWith(MockitoExtension.class) +class UserRetrieverTest { + + @Mock + private UserRepository userRepository; + + @InjectMocks + private UserRetriever userRetriever; + + private UserEntity createTestEntity() { + final UserEntity entity = UserEntity.create("ํ™๊ธธ๋™", Gender.MALE, 25, "test@example.com", "kakao_123", + SocialType.KAKAO, UserRole.USER); + ReflectionTestUtils.setField(entity, "userId", 100L); + return entity; + } + + @Nested + @DisplayName("getUserBySocialInfo ๋ฉ”์„œ๋“œ") + class GetUserBySocialInfo { + + @Test + @DisplayName("์กด์žฌํ•˜๋ฉด User๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void returnsUserWhenFound() { + // given + given(userRepository.findUserBySocialTypeAndSocialId(SocialType.KAKAO, "kakao_123")) + .willReturn(Optional.of(createTestEntity())); + + // when + final User result = userRetriever.getUserBySocialInfo(SocialType.KAKAO, "kakao_123"); + + // then + assertThat(result.getUserId()).isEqualTo(100L); + assertThat(result.getSocialType()).isEqualTo(SocialType.KAKAO); + } + + @Test + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š์œผ๋ฉด UserNotFoundException์„ ๋˜์ง„๋‹ค") + void throwsExceptionWhenNotFound() { + // given + given(userRepository.findUserBySocialTypeAndSocialId(SocialType.KAKAO, "invalid")) + .willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> userRetriever.getUserBySocialInfo(SocialType.KAKAO, "invalid")) + .isInstanceOf(UserNotFoundException.class); + } + } + + @Nested + @DisplayName("validDuplicatedUserBySocial ๋ฉ”์„œ๋“œ") + class ValidDuplicatedUserBySocial { + + @Test + @DisplayName("์ค‘๋ณต์ด ์•„๋‹ˆ๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๋Š”๋‹ค") + void doesNotThrowWhenNotDuplicated() { + // given + given(userRepository.existsBySocialTypeAndSocialId(SocialType.KAKAO, "new_user")) + .willReturn(false); + + // when & then + assertThatCode(() -> userRetriever.validDuplicatedUserBySocial(SocialType.KAKAO, "new_user")) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("์ค‘๋ณต์ด๋ฉด UserDuplicateException์„ ๋˜์ง„๋‹ค") + void throwsExceptionWhenDuplicated() { + // given + given(userRepository.existsBySocialTypeAndSocialId(SocialType.KAKAO, "kakao_123")) + .willReturn(true); + + // when & then + assertThatThrownBy(() -> userRetriever.validDuplicatedUserBySocial(SocialType.KAKAO, "kakao_123")) + .isInstanceOf(UserDuplicateException.class); + } + } + + @Nested + @DisplayName("validExistUserById ๋ฉ”์„œ๋“œ") + class ValidExistUserById { + + @Test + @DisplayName("์กด์žฌํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๋Š”๋‹ค") + void doesNotThrowWhenExists() { + // given + given(userRepository.existsById(100L)).willReturn(true); + + // when & then + assertThatCode(() -> userRetriever.validExistUserById(100L)) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š์œผ๋ฉด UserNotFoundException์„ ๋˜์ง„๋‹ค") + void throwsExceptionWhenNotExists() { + // given + given(userRepository.existsById(999L)).willReturn(false); + + // when & then + assertThatThrownBy(() -> userRetriever.validExistUserById(999L)) + .isInstanceOf(UserNotFoundException.class); + } + } + + @Nested + @DisplayName("findUserById ๋ฉ”์„œ๋“œ") + class FindUserById { + + @Test + @DisplayName("์กด์žฌํ•˜๋ฉด User๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void returnsUserWhenFound() { + // given + given(userRepository.findById(100L)).willReturn(Optional.of(createTestEntity())); + + // when + final User result = userRetriever.findUserById(100L); + + // then + assertThat(result.getUserId()).isEqualTo(100L); + } + + @Test + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š์œผ๋ฉด UserNotFoundException์„ ๋˜์ง„๋‹ค") + void throwsExceptionWhenNotFound() { + // given + given(userRepository.findById(999L)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> userRetriever.findUserById(999L)) + .isInstanceOf(UserNotFoundException.class); + } + } + + @Nested + @DisplayName("findUserEntityById ๋ฉ”์„œ๋“œ") + class FindUserEntityById { + + @Test + @DisplayName("์กด์žฌํ•˜๋ฉด UserEntity๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void returnsEntityWhenFound() { + // given + given(userRepository.findById(100L)).willReturn(Optional.of(createTestEntity())); + + // when + final UserEntity result = userRetriever.findUserEntityById(100L); + + // then + assertThat(result.getUserId()).isEqualTo(100L); + } + + @Test + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š์œผ๋ฉด UserNotFoundException์„ ๋˜์ง„๋‹ค") + void throwsExceptionWhenNotFound() { + // given + given(userRepository.findById(999L)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> userRetriever.findUserEntityById(999L)) + .isInstanceOf(UserNotFoundException.class); + } + } + + @Nested + @DisplayName("validEmailDuplicated ๋ฉ”์„œ๋“œ") + class ValidEmailDuplicated { + + @Test + @DisplayName("์ค‘๋ณต์ด ์•„๋‹ˆ๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๋Š”๋‹ค") + void doesNotThrowWhenNotDuplicated() { + // given + given(userRepository.existsByEmail("new@example.com")).willReturn(false); + + // when & then + assertThatCode(() -> userRetriever.validEmailDuplicated("new@example.com")) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("์ค‘๋ณต์ด๋ฉด UserDuplicateException์„ ๋˜์ง„๋‹ค") + void throwsExceptionWhenDuplicated() { + // given + given(userRepository.existsByEmail("test@example.com")).willReturn(true); + + // when & then + assertThatThrownBy(() -> userRetriever.validEmailDuplicated("test@example.com")) + .isInstanceOf(UserDuplicateException.class); + } + } + + @Nested + @DisplayName("findUserByEmail ๋ฉ”์„œ๋“œ") + class FindUserByEmail { + + @Test + @DisplayName("์กด์žฌํ•˜๋ฉด User๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void returnsUserWhenFound() { + // given + given(userRepository.findByEmail("test@example.com")).willReturn(Optional.of(createTestEntity())); + + // when + final User result = userRetriever.findUserByEmail("test@example.com"); + + // then + assertThat(result.getEmail()).isEqualTo("test@example.com"); + } + + @Test + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š์œผ๋ฉด UserNotFoundException์„ ๋˜์ง„๋‹ค") + void throwsExceptionWhenNotFound() { + // given + given(userRepository.findByEmail("invalid@example.com")).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> userRetriever.findUserByEmail("invalid@example.com")) + .isInstanceOf(UserNotFoundException.class); + } + } +} diff --git a/src/test/java/com/permitseoul/permitserver/domain/user/core/domain/UserEntityTest.java b/src/test/java/com/permitseoul/permitserver/domain/user/core/domain/UserEntityTest.java new file mode 100644 index 0000000..2b77e02 --- /dev/null +++ b/src/test/java/com/permitseoul/permitserver/domain/user/core/domain/UserEntityTest.java @@ -0,0 +1,165 @@ +package com.permitseoul.permitserver.domain.user.core.domain; + +import com.permitseoul.permitserver.domain.user.core.domain.entity.UserEntity; +import com.permitseoul.permitserver.domain.user.core.exception.UserIllegalArgumentException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayName("User & UserEntity ํ…Œ์ŠคํŠธ") +class UserEntityTest { + + private static final String NAME = "ํ™๊ธธ๋™"; + private static final Gender GENDER = Gender.MALE; + private static final int AGE = 25; + private static final String EMAIL = "test@example.com"; + private static final String SOCIAL_ID = "kakao_12345"; + private static final SocialType SOCIAL_TYPE = SocialType.KAKAO; + private static final UserRole USER_ROLE = UserRole.USER; + + private UserEntity createTestEntity() { + return UserEntity.create(NAME, GENDER, AGE, EMAIL, SOCIAL_ID, SOCIAL_TYPE, USER_ROLE); + } + + @Nested + @DisplayName("UserEntity.create ๋ฉ”์„œ๋“œ") + class Create { + + @Test + @DisplayName("์ •์ƒ์ ์ธ ๊ฐ’์œผ๋กœ UserEntity๋ฅผ ์ƒ์„ฑํ•œ๋‹ค") + void createsUserEntitySuccessfully() { + // when + final UserEntity entity = createTestEntity(); + + // then + assertThat(entity.getName()).isEqualTo(NAME); + assertThat(entity.getGender()).isEqualTo(GENDER); + assertThat(entity.getAge()).isEqualTo(AGE); + assertThat(entity.getEmail()).isEqualTo(EMAIL); + assertThat(entity.getSocialId()).isEqualTo(SOCIAL_ID); + assertThat(entity.getSocialType()).isEqualTo(SOCIAL_TYPE); + assertThat(entity.getUserRole()).isEqualTo(USER_ROLE); + } + + @Test + @DisplayName("์ƒ์„ฑ ์งํ›„ userId๋Š” null์ด๋‹ค (@GeneratedValue)") + void userIdIsNullAfterCreate() { + // when + final UserEntity entity = createTestEntity(); + + // then + assertThat(entity.getUserId()).isNull(); + } + } + + @Nested + @DisplayName("updateUserInfo ๋ฉ”์„œ๋“œ") + class UpdateUserInfo { + + @Test + @DisplayName("name, gender, email์„ ์ •์ƒ ์—…๋ฐ์ดํŠธํ•œ๋‹ค") + void updatesUserInfoSuccessfully() { + // given + final UserEntity entity = createTestEntity(); + final String newName = "๊น€์ฒ ์ˆ˜"; + final Gender newGender = Gender.MALE; + final String newEmail = "new@example.com"; + + // when + entity.updateUserInfo(newName, newGender, newEmail); + + // then + assertThat(entity.getName()).isEqualTo(newName); + assertThat(entity.getGender()).isEqualTo(newGender); + assertThat(entity.getEmail()).isEqualTo(newEmail); + } + + @Test + @DisplayName("email์ด null์ด๋ฉด ๊ธฐ์กด email์„ ์œ ์ง€ํ•œ๋‹ค") + void keepsOriginalEmailWhenNewEmailIsNull() { + // given + final UserEntity entity = createTestEntity(); + final String originalEmail = entity.getEmail(); + + // when + entity.updateUserInfo("์ƒˆ์ด๋ฆ„", Gender.FEMALE, null); + + // then + assertThat(entity.getEmail()).isEqualTo(originalEmail); + assertThat(entity.getName()).isEqualTo("์ƒˆ์ด๋ฆ„"); + assertThat(entity.getGender()).isEqualTo(Gender.FEMALE); + } + } + + @Nested + @DisplayName("updateUserRole ๋ฉ”์„œ๋“œ") + class UpdateUserRole { + + @Test + @DisplayName("USER โ†’ ADMIN์œผ๋กœ ์—ญํ• ์„ ๋ณ€๊ฒฝํ•œ๋‹ค") + void updatesRoleFromUserToAdmin() { + // given + final UserEntity entity = createTestEntity(); + + // when + entity.updateUserRole(UserRole.ADMIN); + + // then + assertThat(entity.getUserRole()).isEqualTo(UserRole.ADMIN); + } + + @Test + @DisplayName("USER โ†’ STAFF๋กœ ์—ญํ• ์„ ๋ณ€๊ฒฝํ•œ๋‹ค") + void updatesRoleFromUserToStaff() { + // given + final UserEntity entity = createTestEntity(); + + // when + entity.updateUserRole(UserRole.STAFF); + + // then + assertThat(entity.getUserRole()).isEqualTo(UserRole.STAFF); + } + + @Test + @DisplayName("null์„ ์ „๋‹ฌํ•˜๋ฉด UserIllegalArgumentException์„ ๋˜์ง„๋‹ค") + void throwsExceptionWhenRoleIsNull() { + // given + final UserEntity entity = createTestEntity(); + + // when & then + assertThatThrownBy(() -> entity.updateUserRole(null)) + .isInstanceOf(UserIllegalArgumentException.class); + } + } + + @Nested + @DisplayName("User.fromEntity ๋ฉ”์„œ๋“œ") + class FromEntity { + + @Test + @DisplayName("Entity์˜ ๋ชจ๋“  ํ•„๋“œ๊ฐ€ Domain ๊ฐ์ฒด๋กœ ์ •ํ™•ํžˆ ๋งคํ•‘๋œ๋‹ค") + void mapsAllFieldsCorrectly() { + // given + final UserEntity entity = createTestEntity(); + ReflectionTestUtils.setField(entity, "userId", 100L); + + // when + final User user = User.fromEntity(entity); + + // then + assertThat(user.getUserId()).isEqualTo(100L); + assertThat(user.getName()).isEqualTo(NAME); + assertThat(user.getGender()).isEqualTo(GENDER); + assertThat(user.getAge()).isEqualTo(AGE); + assertThat(user.getEmail()).isEqualTo(EMAIL); + assertThat(user.getSocialId()).isEqualTo(SOCIAL_ID); + assertThat(user.getSocialType()).isEqualTo(SOCIAL_TYPE); + assertThat(user.getUserRole()).isEqualTo(USER_ROLE); + } + } +} diff --git a/src/test/java/com/permitseoul/permitserver/global/TicketOrCouponCodeGeneratorTest.java b/src/test/java/com/permitseoul/permitserver/global/TicketOrCouponCodeGeneratorTest.java new file mode 100644 index 0000000..62a222e --- /dev/null +++ b/src/test/java/com/permitseoul/permitserver/global/TicketOrCouponCodeGeneratorTest.java @@ -0,0 +1,89 @@ +package com.permitseoul.permitserver.global; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.Test; + +import java.util.HashSet; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("TicketOrCouponCodeGenerator ํ…Œ์ŠคํŠธ") +class TicketOrCouponCodeGeneratorTest { + + @Nested + @DisplayName("generateCode ๋ฉ”์„œ๋“œ") + class GenerateCode { + + @Test + @DisplayName("์ƒ์„ฑ๋œ ์ฝ”๋“œ๋Š” null์ด ์•„๋‹ˆ๋‹ค") + void generatesNonNullCode() { + // when + final String code = TicketOrCouponCodeGenerator.generateCode(); + + // then + assertThat(code).isNotNull(); + } + + @Test + @DisplayName("์ƒ์„ฑ๋œ ์ฝ”๋“œ์˜ ๊ธธ์ด๋Š” 10์ž์ด๋‹ค (SHA-256์˜ ์•ž 5๋ฐ”์ดํŠธ = 10 hex chars)") + void generatesCodeWithCorrectLength() { + // when + final String code = TicketOrCouponCodeGenerator.generateCode(); + + // then + assertThat(code).hasSize(10); + } + + @Test + @DisplayName("์ƒ์„ฑ๋œ ์ฝ”๋“œ๋Š” ๋Œ€๋ฌธ์ž 16์ง„์ˆ˜ ๋ฌธ์ž๋กœ๋งŒ ๊ตฌ์„ฑ๋œ๋‹ค") + void generatesCodeWithUppercaseHexCharacters() { + // when + final String code = TicketOrCouponCodeGenerator.generateCode(); + + // then + assertThat(code).matches("[0-9A-F]{10}"); + } + + @RepeatedTest(10) + @DisplayName("๋ฐ˜๋ณต ์ƒ์„ฑ ์‹œ์—๋„ ํ•ญ์ƒ ์˜ฌ๋ฐ”๋ฅธ ํ˜•์‹์„ ์œ ์ง€ํ•œ๋‹ค") + void maintainsFormatOnRepeatedGeneration() { + // when + final String code = TicketOrCouponCodeGenerator.generateCode(); + + // then + assertThat(code) + .isNotNull() + .hasSize(10) + .matches("[0-9A-F]{10}"); + } + + @Test + @DisplayName("100๊ฐœ์˜ ์ฝ”๋“œ๋ฅผ ์ƒ์„ฑํ•˜๋ฉด ๋ชจ๋‘ ๊ณ ์œ ํ•˜๋‹ค") + void generatesUniqueCodesInBatch() { + // given + final Set codes = new HashSet<>(); + final int batchSize = 100; + + // when + for (int i = 0; i < batchSize; i++) { + codes.add(TicketOrCouponCodeGenerator.generateCode()); + } + + // then + assertThat(codes).hasSize(batchSize); + } + + @Test + @DisplayName("์†Œ๋ฌธ์ž๋ฅผ ํฌํ•จํ•˜์ง€ ์•Š๋Š”๋‹ค") + void doesNotContainLowercaseLetters() { + // when + final String code = TicketOrCouponCodeGenerator.generateCode(); + + // then + assertThat(code).isEqualTo(code.toUpperCase()); + } + } +} diff --git a/src/test/java/com/permitseoul/permitserver/global/util/LocalDateTimeFormatterUtilTest.java b/src/test/java/com/permitseoul/permitserver/global/util/LocalDateTimeFormatterUtilTest.java new file mode 100644 index 0000000..a773919 --- /dev/null +++ b/src/test/java/com/permitseoul/permitserver/global/util/LocalDateTimeFormatterUtilTest.java @@ -0,0 +1,384 @@ +package com.permitseoul.permitserver.global.util; + +import com.permitseoul.permitserver.domain.payment.api.dto.PaymentCancelResponse; +import com.permitseoul.permitserver.global.exception.DateFormatException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayName("LocalDateTimeFormatterUtil ํ…Œ์ŠคํŠธ") +class LocalDateTimeFormatterUtilTest { + + @Nested + @DisplayName("formatStartEndDate ๋ฉ”์„œ๋“œ") + class FormatStartEndDate { + + @Test + @DisplayName("์‹œ์ž‘์ผ๊ณผ ์ข…๋ฃŒ์ผ์ด ๊ฐ™์œผ๋ฉด 'Jan 19 (Mon), 2026' ํ˜•์‹์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void formatsSameDateCorrectly() { + // given + final LocalDateTime start = LocalDateTime.of(2026, 1, 19, 17, 0); + final LocalDateTime end = LocalDateTime.of(2026, 1, 19, 19, 0); + + // when + final String result = LocalDateTimeFormatterUtil.formatStartEndDate(start, end); + + // then + assertThat(result).isEqualTo("Jan 19 (Mon), 2026"); + } + + @Test + @DisplayName("๊ฐ™์€ ์—ฐ๋„ ๋‚ด ๋‹ค๋ฅธ ๋‚ ์งœ๋ฉด 'Jan 26 (Mon) โ€“ Jan 29 (Thu), 2026' ํ˜•์‹์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void formatsSameYearDifferentDates() { + // given + final LocalDateTime start = LocalDateTime.of(2026, 1, 26, 17, 0); + final LocalDateTime end = LocalDateTime.of(2026, 1, 29, 19, 0); + + // when + final String result = LocalDateTimeFormatterUtil.formatStartEndDate(start, end); + + // then + assertThat(result).isEqualTo("Jan 26 (Mon) โ€“ Jan 29 (Thu), 2026"); + } + + @Test + @DisplayName("๊ฐ™์€ ์—ฐ๋„ ๋‚ด ๋‹ค๋ฅธ ์›”์ด๋ฉด 'Jan 26 (Mon) โ€“ Feb 10 (Tue), 2026' ํ˜•์‹์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void formatsSameYearDifferentMonths() { + // given + final LocalDateTime start = LocalDateTime.of(2026, 1, 26, 17, 0); + final LocalDateTime end = LocalDateTime.of(2026, 2, 10, 19, 0); + + // when + final String result = LocalDateTimeFormatterUtil.formatStartEndDate(start, end); + + // then + assertThat(result).isEqualTo("Jan 26 (Mon) โ€“ Feb 10 (Tue), 2026"); + } + + @Test + @DisplayName("์—ฐ๋„๊ฐ€ ๋‹ค๋ฅด๋ฉด 'Dec 25 (Thu), 2025 โ€“ Jan 5 (Mon), 2026' ํ˜•์‹์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void formatsDifferentYears() { + // given + final LocalDateTime start = LocalDateTime.of(2025, 12, 25, 17, 0); + final LocalDateTime end = LocalDateTime.of(2026, 1, 5, 19, 0); + + // when + final String result = LocalDateTimeFormatterUtil.formatStartEndDate(start, end); + + // then + assertThat(result).isEqualTo("Dec 25 (Thu), 2025 โ€“ Jan 5 (Mon), 2026"); + } + + @Test + @DisplayName("startDate๊ฐ€ null์ด๋ฉด IllegalArgumentException์„ ๋˜์ง„๋‹ค") + void throwsExceptionWhenStartDateIsNull() { + // given + final LocalDateTime end = LocalDateTime.of(2026, 1, 19, 19, 0); + + // when & then + assertThatThrownBy(() -> LocalDateTimeFormatterUtil.formatStartEndDate(null, end)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("startDate๊ฐ€ null"); + } + + @Test + @DisplayName("endDate๊ฐ€ null์ด๋ฉด IllegalArgumentException์„ ๋˜์ง„๋‹ค") + void throwsExceptionWhenEndDateIsNull() { + // given + final LocalDateTime start = LocalDateTime.of(2026, 1, 19, 17, 0); + + // when & then + assertThatThrownBy(() -> LocalDateTimeFormatterUtil.formatStartEndDate(start, null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("endDate๊ฐ€ null"); + } + } + + @Nested + @DisplayName("formatDayWithDate ๋ฉ”์„œ๋“œ") + class FormatDayWithDate { + + @Test + @DisplayName("2026-01-04(์ผ) โ†’ 'Sun, 04' ํ˜•์‹์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void formatsDateCorrectly() { + // given + final LocalDateTime dateTime = LocalDateTime.of(2026, 1, 4, 10, 0); + + // when + final String result = LocalDateTimeFormatterUtil.formatDayWithDate(dateTime); + + // then + assertThat(result).isEqualTo("Sun, 04"); + } + } + + @Nested + @DisplayName("formatYearMonth ๋ฉ”์„œ๋“œ") + class FormatYearMonth { + + @Test + @DisplayName("2025๋…„ 8์›” โ†’ '2025.08' ํ˜•์‹์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void formatsYearMonthCorrectly() { + // given + final LocalDateTime dateTime = LocalDateTime.of(2025, 8, 15, 10, 0); + + // when + final String result = LocalDateTimeFormatterUtil.formatYearMonth(dateTime); + + // then + assertThat(result).isEqualTo("2025.08"); + } + } + + @Nested + @DisplayName("formatyyyyMMdd ๋ฉ”์„œ๋“œ") + class FormatYyyyMMdd { + + @Test + @DisplayName("'2025-08-15' ํ˜•์‹์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void formatsDateCorrectly() { + // given + final LocalDateTime dateTime = LocalDateTime.of(2025, 8, 15, 10, 0); + + // when + final String result = LocalDateTimeFormatterUtil.formatyyyyMMdd(dateTime); + + // then + assertThat(result).isEqualTo("2025-08-15"); + } + } + + @Nested + @DisplayName("formatHHmm ๋ฉ”์„œ๋“œ") + class FormatHHmm { + + @Test + @DisplayName("'17:30' ํ˜•์‹์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void formatsTimeCorrectly() { + // given + final LocalDateTime dateTime = LocalDateTime.of(2026, 1, 19, 17, 30); + + // when + final String result = LocalDateTimeFormatterUtil.formatHHmm(dateTime); + + // then + assertThat(result).isEqualTo("17:30"); + } + } + + @Nested + @DisplayName("combineDateAndTime ๋ฉ”์„œ๋“œ") + class CombineDateAndTime { + + @Test + @DisplayName("๋‚ ์งœ์™€ ์‹œ๊ฐ„์„ ๊ฒฐํ•ฉํ•˜์—ฌ LocalDateTime์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void combinesDateAndTime() { + // given + final LocalDate date = LocalDate.of(2026, 1, 19); + final LocalTime time = LocalTime.of(17, 30); + + // when + final LocalDateTime result = LocalDateTimeFormatterUtil.combineDateAndTime(date, time); + + // then + assertThat(result).isEqualTo(LocalDateTime.of(2026, 1, 19, 17, 30)); + } + + @Test + @DisplayName("date๊ฐ€ null์ด๋ฉด NullPointerException์„ ๋˜์ง„๋‹ค") + void throwsExceptionWhenDateIsNull() { + // given + final LocalTime time = LocalTime.of(17, 30); + + // when & then + assertThatThrownBy(() -> LocalDateTimeFormatterUtil.combineDateAndTime(null, time)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("date๊ฐ€ null"); + } + + @Test + @DisplayName("time์ด null์ด๋ฉด NullPointerException์„ ๋˜์ง„๋‹ค") + void throwsExceptionWhenTimeIsNull() { + // given + final LocalDate date = LocalDate.of(2026, 1, 19); + + // when & then + assertThatThrownBy(() -> LocalDateTimeFormatterUtil.combineDateAndTime(date, null)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("time์ด null"); + } + } + + @Nested + @DisplayName("combineDateAndTimeForUpdate ๋ฉ”์„œ๋“œ") + class CombineDateAndTimeForUpdate { + + @Test + @DisplayName("date์™€ time ๋ชจ๋‘ null์ด๋ฉด originalDateTime์„ ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void returnsOriginalWhenBothNull() { + // given + final LocalDateTime original = LocalDateTime.of(2026, 1, 19, 17, 30); + + // when + final LocalDateTime result = LocalDateTimeFormatterUtil.combineDateAndTimeForUpdate(null, null, original); + + // then + assertThat(result).isEqualTo(original); + } + + @Test + @DisplayName("date๋งŒ ์ œ๊ณต๋˜๋ฉด originalDateTime์˜ ์‹œ๊ฐ„๊ณผ ์ƒˆ ๋‚ ์งœ๋ฅผ ๊ฒฐํ•ฉํ•œ๋‹ค") + void usesNewDateWithOriginalTime() { + // given + final LocalDate newDate = LocalDate.of(2026, 2, 1); + final LocalDateTime original = LocalDateTime.of(2026, 1, 19, 17, 30); + + // when + final LocalDateTime result = LocalDateTimeFormatterUtil.combineDateAndTimeForUpdate(newDate, null, + original); + + // then + assertThat(result).isEqualTo(LocalDateTime.of(2026, 2, 1, 17, 30)); + } + + @Test + @DisplayName("time๋งŒ ์ œ๊ณต๋˜๋ฉด originalDateTime์˜ ๋‚ ์งœ์™€ ์ƒˆ ์‹œ๊ฐ„์„ ๊ฒฐํ•ฉํ•œ๋‹ค") + void usesOriginalDateWithNewTime() { + // given + final LocalTime newTime = LocalTime.of(20, 0); + final LocalDateTime original = LocalDateTime.of(2026, 1, 19, 17, 30); + + // when + final LocalDateTime result = LocalDateTimeFormatterUtil.combineDateAndTimeForUpdate(null, newTime, + original); + + // then + assertThat(result).isEqualTo(LocalDateTime.of(2026, 1, 19, 20, 0)); + } + + @Test + @DisplayName("date์™€ time ๋ชจ๋‘ ์ œ๊ณต๋˜๋ฉด ์ƒˆ ๋‚ ์งœ์™€ ์ƒˆ ์‹œ๊ฐ„์„ ๊ฒฐํ•ฉํ•œ๋‹ค") + void usesBothNewDateAndTime() { + // given + final LocalDate newDate = LocalDate.of(2026, 3, 1); + final LocalTime newTime = LocalTime.of(9, 0); + final LocalDateTime original = LocalDateTime.of(2026, 1, 19, 17, 30); + + // when + final LocalDateTime result = LocalDateTimeFormatterUtil.combineDateAndTimeForUpdate(newDate, newTime, + original); + + // then + assertThat(result).isEqualTo(LocalDateTime.of(2026, 3, 1, 9, 0)); + } + + @Test + @DisplayName("originalDateTime์ด null์ด๋ฉด DateFormatException์„ ๋˜์ง„๋‹ค") + void throwsExceptionWhenOriginalIsNull() { + assertThatThrownBy(() -> LocalDateTimeFormatterUtil.combineDateAndTimeForUpdate( + LocalDate.of(2026, 1, 19), LocalTime.of(17, 0), null)) + .isInstanceOf(DateFormatException.class); + } + } + + @Nested + @DisplayName("parseISO8601DateToLocalDateTime ๋ฉ”์„œ๋“œ") + class ParseISO8601DateToLocalDateTime { + + @Test + @DisplayName("ISO 8601 ํ˜•์‹์˜ ๋ฌธ์ž์—ด์„ LocalDateTime์œผ๋กœ ๋ณ€ํ™˜ํ•œ๋‹ค") + void parsesValidISODate() { + // given + final String isoDate = "2026-01-19T17:30:00+09:00"; + + // when + final LocalDateTime result = LocalDateTimeFormatterUtil.parseISO8601DateToLocalDateTime(isoDate); + + // then + assertThat(result).isEqualTo(LocalDateTime.of(2026, 1, 19, 17, 30, 0)); + } + + @Test + @DisplayName("null์ด ์ž…๋ ฅ๋˜๋ฉด DateFormatException์„ ๋˜์ง„๋‹ค") + void throwsExceptionWhenNull() { + assertThatThrownBy(() -> LocalDateTimeFormatterUtil.parseISO8601DateToLocalDateTime(null)) + .isInstanceOf(DateFormatException.class); + } + + @Test + @DisplayName("๋นˆ ๋ฌธ์ž์—ด์ด ์ž…๋ ฅ๋˜๋ฉด DateFormatException์„ ๋˜์ง„๋‹ค") + void throwsExceptionWhenEmpty() { + assertThatThrownBy(() -> LocalDateTimeFormatterUtil.parseISO8601DateToLocalDateTime("")) + .isInstanceOf(DateFormatException.class); + } + + @Test + @DisplayName("๊ณต๋ฐฑ ๋ฌธ์ž์—ด์ด ์ž…๋ ฅ๋˜๋ฉด DateFormatException์„ ๋˜์ง„๋‹ค") + void throwsExceptionWhenBlank() { + assertThatThrownBy(() -> LocalDateTimeFormatterUtil.parseISO8601DateToLocalDateTime(" ")) + .isInstanceOf(DateFormatException.class); + } + } + + @Nested + @DisplayName("getLatestCancelPaymentByDate ๋ฉ”์„œ๋“œ") + class GetLatestCancelPaymentByDate { + + @Test + @DisplayName("๊ฐ€์žฅ ์ตœ๊ทผ ์ทจ์†Œ ๋‚ด์—ญ์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void returnsLatestCancelDetail() { + // given + final PaymentCancelResponse.CancelDetail older = new PaymentCancelResponse.CancelDetail( + "์‚ฌ์šฉ์ž ์š”์ฒญ", new java.math.BigDecimal("30000"), "2026-01-10T10:00:00+09:00", "txKey1"); + final PaymentCancelResponse.CancelDetail newer = new PaymentCancelResponse.CancelDetail( + "์‚ฌ์šฉ์ž ์š”์ฒญ", new java.math.BigDecimal("50000"), "2026-01-15T10:00:00+09:00", "txKey2"); + + // when + final Optional result = LocalDateTimeFormatterUtil + .getLatestCancelPaymentByDate(List.of(older, newer)); + + // then + assertThat(result).isPresent(); + assertThat(result.get().transactionKey()).isEqualTo("txKey2"); + } + + @Test + @DisplayName("canceledAt์ด null์ธ ํ•ญ๋ชฉ์€ ํ•„ํ„ฐ๋งํ•œ๋‹ค") + void filtersOutNullCanceledAt() { + // given + final PaymentCancelResponse.CancelDetail withDate = new PaymentCancelResponse.CancelDetail( + "์‚ฌ์šฉ์ž ์š”์ฒญ", new java.math.BigDecimal("30000"), "2026-01-10T10:00:00+09:00", "txKey1"); + final PaymentCancelResponse.CancelDetail withoutDate = new PaymentCancelResponse.CancelDetail( + "์‚ฌ์šฉ์ž ์š”์ฒญ", new java.math.BigDecimal("50000"), null, "txKey2"); + + // when + final Optional result = LocalDateTimeFormatterUtil + .getLatestCancelPaymentByDate(List.of(withDate, withoutDate)); + + // then + assertThat(result).isPresent(); + assertThat(result.get().transactionKey()).isEqualTo("txKey1"); + } + + @Test + @DisplayName("๋นˆ ๋ฆฌ์ŠคํŠธ์ด๋ฉด ๋นˆ Optional์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void returnsEmptyOptionalWhenListIsEmpty() { + // when + final Optional result = LocalDateTimeFormatterUtil + .getLatestCancelPaymentByDate(Collections.emptyList()); + + // then + assertThat(result).isEmpty(); + } + } +} diff --git a/src/test/java/com/permitseoul/permitserver/global/util/PriceFormatterUtilTest.java b/src/test/java/com/permitseoul/permitserver/global/util/PriceFormatterUtilTest.java new file mode 100644 index 0000000..c076aff --- /dev/null +++ b/src/test/java/com/permitseoul/permitserver/global/util/PriceFormatterUtilTest.java @@ -0,0 +1,163 @@ +package com.permitseoul.permitserver.global.util; + +import com.permitseoul.permitserver.global.exception.PriceFormatException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.math.BigDecimal; +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayName("PriceFormatterUtil ํ…Œ์ŠคํŠธ") +class PriceFormatterUtilTest { + + @Nested + @DisplayName("formatPrice ๋ฉ”์„œ๋“œ") + class FormatPrice { + + @ParameterizedTest(name = "{0} โ†’ {1}") + @CsvSource({ + "60000, '60,000'", + "0, '0'", + "1000000, '1,000,000'", + "999, '999'", + "1000, '1,000'", + "123456789, '123,456,789'" + }) + @DisplayName("๋‹ค์–‘ํ•œ ๊ฐ€๊ฒฉ์„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ํฌ๋งทํŒ…ํ•œ๋‹ค") + void formatsVariousPrices(final String input, final String expected) { + // given + final BigDecimal price = new BigDecimal(input); + + // when + final String result = PriceFormatterUtil.formatPrice(price); + + // then + assertThat(result).isEqualTo(expected); + } + + @Test + @DisplayName("null์ด ์ž…๋ ฅ๋˜๋ฉด '-'์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void returnsDashWhenPriceIsNull() { + // given & when + final String result = PriceFormatterUtil.formatPrice(null); + + // then + assertThat(result).isEqualTo("-"); + } + + @Test + @DisplayName("์†Œ์ˆ˜์ ์ด ์žˆ๋Š” ๊ฐ€๊ฒฉ์€ ์ •์ˆ˜๋ถ€๋งŒ ์ฝค๋งˆ๋ฅผ ํฌํ•จํ•œ๋‹ค") + void formatsDecimalPrice() { + // given + final BigDecimal price = new BigDecimal("60000.50"); + + // when + final String result = PriceFormatterUtil.formatPrice(price); + + // then + assertThat(result).contains("60,000"); + } + } + + @Nested + @DisplayName("formatRoundPrice ๋ฉ”์„œ๋“œ") + class FormatRoundPrice { + + @Test + @DisplayName("๊ฐ€๊ฒฉ์ด ํ•˜๋‚˜๋ฟ์ด๋ฉด ๋‹จ์ผ ๊ฐ€๊ฒฉ์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void returnsSinglePriceWhenOnlyOnePrice() { + // given + final List prices = List.of(new BigDecimal("50000")); + + // when + final String result = PriceFormatterUtil.formatRoundPrice(prices); + + // then + assertThat(result).isEqualTo("50,000"); + } + + @Test + @DisplayName("๋ชจ๋“  ๊ฐ€๊ฒฉ์ด ๋™์ผํ•˜๋ฉด ๋‹จ์ผ ๊ฐ€๊ฒฉ์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void returnsSinglePriceWhenAllPricesAreSame() { + // given + final List prices = List.of( + new BigDecimal("30000"), + new BigDecimal("30000"), + new BigDecimal("30000")); + + // when + final String result = PriceFormatterUtil.formatRoundPrice(prices); + + // then + assertThat(result).isEqualTo("30,000"); + } + + @Test + @DisplayName("๊ฐ€๊ฒฉ์ด ๋‹ค๋ฅด๋ฉด '์ตœ์ €๊ฐ€ ~ ์ตœ๊ณ ๊ฐ€' ํ˜•์‹์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void returnsRangeWhenPricesDiffer() { + // given + final List prices = List.of( + new BigDecimal("30000"), + new BigDecimal("50000"), + new BigDecimal("80000")); + + // when + final String result = PriceFormatterUtil.formatRoundPrice(prices); + + // then + assertThat(result).isEqualTo("30,000 ~ 80,000"); + } + + @Test + @DisplayName("๋‘ ๊ฐœ์˜ ๋‹ค๋ฅธ ๊ฐ€๊ฒฉ์ด๋ฉด '์ตœ์ €๊ฐ€ ~ ์ตœ๊ณ ๊ฐ€' ํ˜•์‹์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void returnsRangeWithTwoDifferentPrices() { + // given + final List prices = List.of( + new BigDecimal("10000"), + new BigDecimal("50000")); + + // when + final String result = PriceFormatterUtil.formatRoundPrice(prices); + + // then + assertThat(result).isEqualTo("10,000 ~ 50,000"); + } + + @Test + @DisplayName("์ •๋ ฌ๋˜์ง€ ์•Š์€ ๊ฐ€๊ฒฉ ๋ฆฌ์ŠคํŠธ๋„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ •๋ ฌํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void sortsUnsortedPricesCorrectly() { + // given + final List prices = List.of( + new BigDecimal("80000"), + new BigDecimal("10000"), + new BigDecimal("50000")); + + // when + final String result = PriceFormatterUtil.formatRoundPrice(prices); + + // then + assertThat(result).isEqualTo("10,000 ~ 80,000"); + } + + @Test + @DisplayName("prices๊ฐ€ null์ด๋ฉด PriceFormatException์„ ๋˜์ง„๋‹ค") + void throwsExceptionWhenPricesIsNull() { + assertThatThrownBy(() -> PriceFormatterUtil.formatRoundPrice(null)) + .isInstanceOf(PriceFormatException.class); + } + + @Test + @DisplayName("prices๊ฐ€ ๋นˆ ๋ฆฌ์ŠคํŠธ์ด๋ฉด PriceFormatException์„ ๋˜์ง„๋‹ค") + void throwsExceptionWhenPricesIsEmpty() { + assertThatThrownBy(() -> PriceFormatterUtil.formatRoundPrice(Collections.emptyList())) + .isInstanceOf(PriceFormatException.class); + } + } +} diff --git a/src/test/java/com/permitseoul/permitserver/global/util/SecureUrlUtilTest.java b/src/test/java/com/permitseoul/permitserver/global/util/SecureUrlUtilTest.java new file mode 100644 index 0000000..172f554 --- /dev/null +++ b/src/test/java/com/permitseoul/permitserver/global/util/SecureUrlUtilTest.java @@ -0,0 +1,149 @@ +package com.permitseoul.permitserver.global.util; + +import com.permitseoul.permitserver.global.HashIdProperties; +import com.permitseoul.permitserver.global.exception.UrlSecureException; +import com.permitseoul.permitserver.global.response.code.ErrorCode; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayName("SecureUrlUtil ํ…Œ์ŠคํŠธ") +class SecureUrlUtilTest { + + private SecureUrlUtil secureUrlUtil; + + @BeforeEach + void setUp() { + final HashIdProperties properties = new HashIdProperties("test-salt", 8); + secureUrlUtil = new SecureUrlUtil(properties); + } + + @Nested + @DisplayName("encode ๋ฉ”์„œ๋“œ") + class Encode { + + @Test + @DisplayName("์œ ํšจํ•œ ID๋ฅผ ์ธ์ฝ”๋”ฉํ•˜๋ฉด null์ด ์•„๋‹Œ ๋ฌธ์ž์—ด์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void encodesValidId() { + // when + final String encoded = secureUrlUtil.encode(1L); + + // then + assertThat(encoded).isNotNull().isNotEmpty(); + } + + @Test + @DisplayName("์ธ์ฝ”๋”ฉ๋œ ๊ฐ’์€ ์ตœ์†Œ ์ง€์ • ๊ธธ์ด ์ด์ƒ์ด๋‹ค") + void encodedValueHasMinimumLength() { + // when + final String encoded = secureUrlUtil.encode(1L); + + // then + assertThat(encoded.length()).isGreaterThanOrEqualTo(8); + } + + @Test + @DisplayName("๊ฐ™์€ ID๋ฅผ ์ธ์ฝ”๋”ฉํ•˜๋ฉด ํ•ญ์ƒ ๊ฐ™์€ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void encodingSameIdReturnsConsistentResult() { + // when + final String first = secureUrlUtil.encode(100L); + final String second = secureUrlUtil.encode(100L); + + // then + assertThat(first).isEqualTo(second); + } + + @Test + @DisplayName("๋‹ค๋ฅธ ID๋ฅผ ์ธ์ฝ”๋”ฉํ•˜๋ฉด ๋‹ค๋ฅธ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void encodingDifferentIdsReturnsDifferentResults() { + // when + final String first = secureUrlUtil.encode(1L); + final String second = secureUrlUtil.encode(2L); + + // then + assertThat(first).isNotEqualTo(second); + } + + @Test + @DisplayName("null ID๋ฅผ ์ธ์ฝ”๋”ฉํ•˜๋ฉด UrlSecureException์„ ๋˜์ง€๊ณ  ErrorCode๋Š” INTERNAL_ID_ENCODE_ERROR์ด๋‹ค") + void throwsExceptionWhenIdIsNull() { + assertThatThrownBy(() -> secureUrlUtil.encode(null)) + .isInstanceOf(UrlSecureException.class) + .satisfies(exception -> { + final UrlSecureException ex = (UrlSecureException) exception; + assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.INTERNAL_ID_ENCODE_ERROR); + }); + } + + @Test + @DisplayName("0์„ ์ธ์ฝ”๋”ฉํ•ด๋„ ์ •์ƒ ๋™์ž‘ํ•œ๋‹ค") + void encodesZeroSuccessfully() { + // when + final String encoded = secureUrlUtil.encode(0L); + + // then + assertThat(encoded).isNotNull().isNotEmpty(); + } + } + + @Nested + @DisplayName("decode ๋ฉ”์„œ๋“œ") + class Decode { + + @Test + @DisplayName("์ธ์ฝ”๋”ฉ๋œ ๊ฐ’์„ ๋””์ฝ”๋”ฉํ•˜๋ฉด ์›๋ž˜ ID๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void decodesEncodedValueToOriginalId() { + // given + final long originalId = 42L; + final String encoded = secureUrlUtil.encode(originalId); + + // when + final long decoded = secureUrlUtil.decode(encoded); + + // then + assertThat(decoded).isEqualTo(originalId); + } + + @Test + @DisplayName("encode โ†’ decode ๋ผ์šด๋“œํŠธ๋ฆฝ์ด ์ •์ƒ ๋™์ž‘ํ•œ๋‹ค") + void roundTripWorksCorrectly() { + // given + final long[] testIds = { 0L, 1L, 100L, 999L, 123456L }; + + for (final long id : testIds) { + // when + final String encoded = secureUrlUtil.encode(id); + final long decoded = secureUrlUtil.decode(encoded); + + // then + assertThat(decoded).as("ID %d์˜ encode-decode ๋ผ์šด๋“œํŠธ๋ฆฝ", id).isEqualTo(id); + } + } + + @Test + @DisplayName("์ž˜๋ชป๋œ ํ•ด์‹œ๋ฅผ ๋””์ฝ”๋”ฉํ•˜๋ฉด UrlSecureException์„ ๋˜์ง€๊ณ  ErrorCode๋Š” BAD_REQUEST_ID_DECODE_ERROR์ด๋‹ค") + void throwsExceptionWhenHashIsInvalid() { + assertThatThrownBy(() -> secureUrlUtil.decode("invalid-hash-!@#$%")) + .isInstanceOf(UrlSecureException.class) + .satisfies(exception -> { + final UrlSecureException ex = (UrlSecureException) exception; + assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.BAD_REQUEST_ID_DECODE_ERROR); + }); + } + + @Test + @DisplayName("๋นˆ ๋ฌธ์ž์—ด์„ ๋””์ฝ”๋”ฉํ•˜๋ฉด UrlSecureException์„ ๋˜์ง€๊ณ  ErrorCode๋Š” BAD_REQUEST_ID_DECODE_ERROR์ด๋‹ค") + void throwsExceptionWhenHashIsEmpty() { + assertThatThrownBy(() -> secureUrlUtil.decode("")) + .isInstanceOf(UrlSecureException.class) + .satisfies(exception -> { + final UrlSecureException ex = (UrlSecureException) exception; + assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.BAD_REQUEST_ID_DECODE_ERROR); + }); + } + } +} diff --git a/src/test/java/com/permitseoul/permitserver/global/util/TimeFormatterUtilTest.java b/src/test/java/com/permitseoul/permitserver/global/util/TimeFormatterUtilTest.java new file mode 100644 index 0000000..609a48c --- /dev/null +++ b/src/test/java/com/permitseoul/permitserver/global/util/TimeFormatterUtilTest.java @@ -0,0 +1,117 @@ +package com.permitseoul.permitserver.global.util; + +import com.permitseoul.permitserver.global.exception.TimeFormatException; +import com.permitseoul.permitserver.global.response.code.ErrorCode; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayName("TimeFormatterUtil ํ…Œ์ŠคํŠธ") +class TimeFormatterUtilTest { + + @Nested + @DisplayName("formatEventTime ๋ฉ”์„œ๋“œ") + class FormatEventTime { + + @Test + @DisplayName("์ •์ƒ์ ์ธ ์‹œ์ž‘/์ข…๋ฃŒ ์‹œ๊ฐ„์„ '์‹œ์ž‘-์ข…๋ฃŒ' ํ˜•์‹์œผ๋กœ ํฌ๋งทํŒ…ํ•œ๋‹ค") + void formatsNormalStartAndEndTime() { + // given + final LocalDateTime start = LocalDateTime.of(2026, 1, 19, 17, 0); + final LocalDateTime end = LocalDateTime.of(2026, 1, 19, 19, 0); + + // when + final String result = TimeFormatterUtil.formatEventTime(start, end); + + // then + assertThat(result).isEqualTo("17:00-19:00"); + } + + @Test + @DisplayName("์ž์ •์„ ํฌํ•จํ•˜๋Š” ์‹œ๊ฐ„์„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ํฌ๋งทํŒ…ํ•œ๋‹ค") + void formatsTimesAroundMidnight() { + // given + final LocalDateTime start = LocalDateTime.of(2026, 1, 19, 23, 30); + final LocalDateTime end = LocalDateTime.of(2026, 1, 20, 0, 30); + + // when + final String result = TimeFormatterUtil.formatEventTime(start, end); + + // then + assertThat(result).isEqualTo("23:30-00:30"); + } + + @Test + @DisplayName("๊ฐ™์€ ์‹œ๊ฐ„์ด๋ฉด ๋™์ผํ•œ ์‹œ๊ฐ„ ๋‘ ๋ฒˆ์„ ํฌ๋งทํŒ…ํ•œ๋‹ค") + void formatsSameStartAndEndTime() { + // given + final LocalDateTime sameTime = LocalDateTime.of(2026, 1, 19, 14, 0); + + // when + final String result = TimeFormatterUtil.formatEventTime(sameTime, sameTime); + + // then + assertThat(result).isEqualTo("14:00-14:00"); + } + + @Test + @DisplayName("๋ถ„์ด ํ•œ ์ž๋ฆฌ์ˆ˜์—ฌ๋„ ๋‘ ์ž๋ฆฌ๋กœ ํ‘œ์‹œํ•œ๋‹ค") + void formatsSingleDigitMinutes() { + // given + final LocalDateTime start = LocalDateTime.of(2026, 1, 19, 9, 5); + final LocalDateTime end = LocalDateTime.of(2026, 1, 19, 10, 0); + + // when + final String result = TimeFormatterUtil.formatEventTime(start, end); + + // then + assertThat(result).isEqualTo("09:05-10:00"); + } + + @Test + @DisplayName("startDateTime์ด null์ด๋ฉด TimeFormatException์„ ๋˜์ง€๊ณ  ErrorCode๋Š” INTERNAL_TIME_FORMAT_ERROR์ด๋‹ค") + void throwsExceptionWhenStartDateTimeIsNull() { + // given + final LocalDateTime end = LocalDateTime.of(2026, 1, 19, 19, 0); + + // when & then + assertThatThrownBy(() -> TimeFormatterUtil.formatEventTime(null, end)) + .isInstanceOf(TimeFormatException.class) + .satisfies(exception -> { + final TimeFormatException ex = (TimeFormatException) exception; + assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.INTERNAL_TIME_FORMAT_ERROR); + }); + } + + @Test + @DisplayName("endDateTime์ด null์ด๋ฉด TimeFormatException์„ ๋˜์ง€๊ณ  ErrorCode๋Š” INTERNAL_TIME_FORMAT_ERROR์ด๋‹ค") + void throwsExceptionWhenEndDateTimeIsNull() { + // given + final LocalDateTime start = LocalDateTime.of(2026, 1, 19, 17, 0); + + // when & then + assertThatThrownBy(() -> TimeFormatterUtil.formatEventTime(start, null)) + .isInstanceOf(TimeFormatException.class) + .satisfies(exception -> { + final TimeFormatException ex = (TimeFormatException) exception; + assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.INTERNAL_TIME_FORMAT_ERROR); + }); + } + + @Test + @DisplayName("startDateTime๊ณผ endDateTime ๋ชจ๋‘ null์ด๋ฉด TimeFormatException์„ ๋˜์ง€๊ณ  ErrorCode๋Š” INTERNAL_TIME_FORMAT_ERROR์ด๋‹ค") + void throwsExceptionWhenBothAreNull() { + assertThatThrownBy(() -> TimeFormatterUtil.formatEventTime(null, null)) + .isInstanceOf(TimeFormatException.class) + .satisfies(exception -> { + final TimeFormatException ex = (TimeFormatException) exception; + assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.INTERNAL_TIME_FORMAT_ERROR); + }); + } + } +} From f3e4f949817435345b933ae6028db503c1f7fae1 Mon Sep 17 00:00:00 2001 From: Kwak Seong Joon Date: Fri, 27 Feb 2026 00:55:39 +0900 Subject: [PATCH 3/7] =?UTF-8?q?feat:=20reissue=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20-=20#249?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../permitserver/domain/auth/api/service/AuthService.java | 6 +++--- .../permitserver/domain/auth/core/domain/RefreshToken.java | 3 --- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/permitseoul/permitserver/domain/auth/api/service/AuthService.java b/src/main/java/com/permitseoul/permitserver/domain/auth/api/service/AuthService.java index 7caac4c..9cb4c0d 100644 --- a/src/main/java/com/permitseoul/permitserver/domain/auth/api/service/AuthService.java +++ b/src/main/java/com/permitseoul/permitserver/domain/auth/api/service/AuthService.java @@ -98,12 +98,12 @@ public TokenDto login(final SocialType socialType, final String authorizationCod public TokenDto reissue(final String refreshToken) { try { final long userId = jwtProvider.extractUserIdFromToken(refreshToken); + final UserRole userRole = UserRole.valueOf(jwtProvider.extractUserRoleFromToken(refreshToken)); checkIsSameRefreshToken(userId, refreshToken); - final User user = userRetriever.findUserById(userId); - final Token newToken = getLoginOrReissueJwtToken(user.getUserId(), user.getUserRole()); + final Token newToken = getLoginOrReissueJwtToken(userId, userRole); - saveRefreshTokenInRedis(user.getUserId(), newToken.getRefreshToken()); + saveRefreshTokenInRedis(userId, newToken.getRefreshToken()); return TokenDto.of(newToken.getAccessToken(), newToken.getRefreshToken()); } catch (AuthWrongJwtException | AuthRTNotFoundException e) { diff --git a/src/main/java/com/permitseoul/permitserver/domain/auth/core/domain/RefreshToken.java b/src/main/java/com/permitseoul/permitserver/domain/auth/core/domain/RefreshToken.java index ff5838a..b666807 100644 --- a/src/main/java/com/permitseoul/permitserver/domain/auth/core/domain/RefreshToken.java +++ b/src/main/java/com/permitseoul/permitserver/domain/auth/core/domain/RefreshToken.java @@ -12,14 +12,11 @@ @RedisHash("refreshToken") public class RefreshToken { - /** ์‚ฌ์šฉ์ž ์‹๋ณ„์ž(ํ‚ค). ์‚ฌ์šฉ์ž๋‹น 1๊ฐœ ์„ธ์…˜๋งŒ ํ—ˆ์šฉ ๋ชจ๋ธ */ @Id private Long userId; - /** ํ† ํฐ ํ•ด์‹œ(SHA-256 ๋“ฑ). ํ‰๋ฌธ ์ €์žฅ ๊ธˆ์ง€ */ private String refreshToken; - /** ๋งŒ๋ฃŒ ์‹œ๊ฐ„(์ดˆ ๋‹จ์œ„). @RedisHash ํด๋ž˜์Šค ๋‹จ์œ„ TTL ๋Œ€์‹  ์ธ์Šคํ„ด์Šค๋ณ„ TTL ์ถ”์ฒœ */ @TimeToLive private long ttlSeconds; From b8765839e83d27bde1d73dfe85f0519763ebccb3 Mon Sep 17 00:00:00 2001 From: Kwak Seong Joon Date: Fri, 27 Feb 2026 00:55:49 +0900 Subject: [PATCH 4/7] =?UTF-8?q?feat:=20server=20duration=20time=20?= =?UTF-8?q?=EC=B8=A1=EC=A0=95=20-=20#249?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...r.java => RequestObservabilityFilter.java} | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) rename src/main/java/com/permitseoul/permitserver/global/filter/{MDCLoggingFilter.java => RequestObservabilityFilter.java} (70%) diff --git a/src/main/java/com/permitseoul/permitserver/global/filter/MDCLoggingFilter.java b/src/main/java/com/permitseoul/permitserver/global/filter/RequestObservabilityFilter.java similarity index 70% rename from src/main/java/com/permitseoul/permitserver/global/filter/MDCLoggingFilter.java rename to src/main/java/com/permitseoul/permitserver/global/filter/RequestObservabilityFilter.java index 5d99ab7..afe00a8 100644 --- a/src/main/java/com/permitseoul/permitserver/global/filter/MDCLoggingFilter.java +++ b/src/main/java/com/permitseoul/permitserver/global/filter/RequestObservabilityFilter.java @@ -18,9 +18,9 @@ @Component @Slf4j -@Profile("!local") +//@Profile("!local") @Order(Ordered.HIGHEST_PRECEDENCE) -class MDCLoggingFilter extends OncePerRequestFilter { +class RequestObservabilityFilter extends OncePerRequestFilter { private static final String NGINX_REQUEST_ID = "X-Request-ID"; private static final String TRACE_ID = "trace_id"; @@ -28,16 +28,20 @@ class MDCLoggingFilter extends OncePerRequestFilter { private static final String METHOD = "method"; private static final String STATUS = "status"; + private static final long SLOW_REQUEST_THRESHOLD_MS = 1000L; + @Override protected void doFilterInternal(@NonNull final HttpServletRequest request, - @NonNull final HttpServletResponse response, - @NonNull final FilterChain filterChain) throws ServletException, IOException { + @NonNull final HttpServletResponse response, + @NonNull final FilterChain filterChain) throws ServletException, IOException { final String uri = request.getRequestURI(); // ํ—ฌ์Šค์ฒดํฌ๋Š” ํŒจ์Šค if (uri != null && uri.contains(Constants.HEALTH_CHECK_URL)) { filterChain.doFilter(request, response); return; } + + final long start = System.currentTimeMillis(); try { String traceId = request.getHeader(NGINX_REQUEST_ID); if (traceId == null || traceId.isBlank()) { @@ -48,9 +52,17 @@ protected void doFilterInternal(@NonNull final HttpServletRequest request, MDC.put(METHOD, request.getMethod()); filterChain.doFilter(request, response); - - MDC.put(STATUS, String.valueOf(response.getStatus())); } finally { + final long duration = System.currentTimeMillis() - start; + final int status = response.getStatus(); + final String method = request.getMethod(); + + if (duration >= SLOW_REQUEST_THRESHOLD_MS) { + log.warn("[SLOW] {} {} โ†’ {} ({}ms)", method, uri, status, duration); + } else { + log.info("{} {} โ†’ {} ({}ms)", method, uri, status, duration); + } + MDC.remove(STATUS); MDC.remove(METHOD); MDC.remove(URI); From 818b098bcd5cd23c7fa5e8ab9ccfe23a2d3d622b Mon Sep 17 00:00:00 2001 From: Kwak Seong Joon Date: Fri, 27 Feb 2026 01:19:19 +0900 Subject: [PATCH 5/7] =?UTF-8?q?feat:=20user=20=EA=B6=8C=ED=95=9C=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EC=8B=9C,=20rt=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20-=20#249?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/admin/base/api/service/AdminService.java | 3 +++ .../permitserver/domain/auth/api/service/AuthService.java | 7 +++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/permitseoul/permitserver/domain/admin/base/api/service/AdminService.java b/src/main/java/com/permitseoul/permitserver/domain/admin/base/api/service/AdminService.java index e5eed20..15aec4e 100644 --- a/src/main/java/com/permitseoul/permitserver/domain/admin/base/api/service/AdminService.java +++ b/src/main/java/com/permitseoul/permitserver/domain/admin/base/api/service/AdminService.java @@ -3,6 +3,7 @@ import com.permitseoul.permitserver.domain.admin.base.api.AdminProperties; import com.permitseoul.permitserver.domain.admin.base.api.dto.res.UserAuthorityGetResponse; import com.permitseoul.permitserver.domain.admin.base.api.exception.AdminAuthorizationException; +import com.permitseoul.permitserver.domain.auth.core.jwt.RefreshTokenManager; import com.permitseoul.permitserver.domain.user.core.component.UserRetriever; import com.permitseoul.permitserver.domain.user.core.component.UserUpdater; import com.permitseoul.permitserver.domain.user.core.domain.User; @@ -20,6 +21,7 @@ public class AdminService { private final AdminProperties adminProperties; private final UserRetriever userRetriever; private final UserUpdater userUpdater; + private final RefreshTokenManager refreshTokenManager; public void validateAdminCode(final String adminCode) { if(!(adminProperties.accessCode().equals(adminCode))){ @@ -44,6 +46,7 @@ public void updateUserAuthority(final long userId, final UserRole userRole) { try { userEntity = userRetriever.findUserEntityById(userId); userUpdater.updateUserRole(userEntity, userRole); + refreshTokenManager.deleteRefreshToken(userEntity.getUserId()); } catch (UserNotFoundException e) { throw new AdminAuthorizationException(ErrorCode.NOT_FOUND_USER); } diff --git a/src/main/java/com/permitseoul/permitserver/domain/auth/api/service/AuthService.java b/src/main/java/com/permitseoul/permitserver/domain/auth/api/service/AuthService.java index 9cb4c0d..302d151 100644 --- a/src/main/java/com/permitseoul/permitserver/domain/auth/api/service/AuthService.java +++ b/src/main/java/com/permitseoul/permitserver/domain/auth/api/service/AuthService.java @@ -99,16 +99,16 @@ public TokenDto reissue(final String refreshToken) { try { final long userId = jwtProvider.extractUserIdFromToken(refreshToken); final UserRole userRole = UserRole.valueOf(jwtProvider.extractUserRoleFromToken(refreshToken)); - checkIsSameRefreshToken(userId, refreshToken); + checkIsSameRefreshToken(userId, refreshToken); final Token newToken = getLoginOrReissueJwtToken(userId, userRole); saveRefreshTokenInRedis(userId, newToken.getRefreshToken()); return TokenDto.of(newToken.getAccessToken(), newToken.getRefreshToken()); - } catch (AuthWrongJwtException | AuthRTNotFoundException e) { + } catch (AuthWrongJwtException e) { throw new AuthUnAuthorizedException(ErrorCode.UNAUTHORIZED_WRONG_RT); - } catch (AuthExpiredJwtException e) { + } catch (AuthExpiredJwtException |AuthRTNotFoundException e) { throw new AuthUnAuthorizedException(ErrorCode.UNAUTHORIZED_RT_EXPIRED); } catch (AuthRTException e) { throw new AuthUnAuthorizedException(ErrorCode.INTERNAL_RT_REDIS_ERROR); @@ -125,7 +125,6 @@ public void logout(final long userId, final String refreshTokenFromCookie) { } catch (DataAccessException e) { throw new AuthRedisException(ErrorCode.INTERNAL_RT_REDIS_ERROR); } - } private void checkIsSameRefreshToken(final long userId, final String refreshToken) { From d7ee586e0c2449bc821a020979771a86ccb65d0a Mon Sep 17 00:00:00 2001 From: Kwak Seong Joon Date: Fri, 27 Feb 2026 01:31:39 +0900 Subject: [PATCH 6/7] =?UTF-8?q?feat:=20nullable=20=EA=B2=80=EC=A6=9D=20-?= =?UTF-8?q?=20#249?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../permitserver/domain/auth/core/jwt/JwtProvider.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/permitseoul/permitserver/domain/auth/core/jwt/JwtProvider.java b/src/main/java/com/permitseoul/permitserver/domain/auth/core/jwt/JwtProvider.java index 9823bbe..c8c2fe6 100644 --- a/src/main/java/com/permitseoul/permitserver/domain/auth/core/jwt/JwtProvider.java +++ b/src/main/java/com/permitseoul/permitserver/domain/auth/core/jwt/JwtProvider.java @@ -48,7 +48,11 @@ public long extractUserIdFromToken(final String token) { public String extractUserRoleFromToken(final String token) { final Jws claims = parseToken(token); - return claims.getBody().get(Constants.USER_ROLE, String.class); + final String role = claims.getBody().get(Constants.USER_ROLE, String.class); + if(role == null) { + throw new AuthWrongJwtException(); + } + return role; } //ํ† ํฐ ํŒŒ์‹ฑ From 75948738cfb782674813004c611037b99f9e4446 Mon Sep 17 00:00:00 2001 From: Kwak Seong Joon Date: Fri, 27 Feb 2026 02:18:35 +0900 Subject: [PATCH 7/7] =?UTF-8?q?fix:=20filter=20exception=20info=20log=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20-=20#251?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../permitserver/global/filter/ExceptionHandlerFilter.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/java/com/permitseoul/permitserver/global/filter/ExceptionHandlerFilter.java b/src/main/java/com/permitseoul/permitserver/global/filter/ExceptionHandlerFilter.java index 4f40ec5..7d49340 100644 --- a/src/main/java/com/permitseoul/permitserver/global/filter/ExceptionHandlerFilter.java +++ b/src/main/java/com/permitseoul/permitserver/global/filter/ExceptionHandlerFilter.java @@ -32,10 +32,6 @@ protected void doFilterInternal(@NonNull final HttpServletRequest request, try { filterChain.doFilter(request, response); } catch (FilterException e) { - log.warn("[FilterException] code={}, ua={}", - e.getErrorCode().name(), - request.getHeader("User-Agent") - ); handleUnauthorizedException(response, e); } catch (Exception e) {