From c717e1cb5b84d099358912f5d5a73a987c280193 Mon Sep 17 00:00:00 2001 From: pricelees Date: Tue, 9 Sep 2025 09:02:11 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=82=BD=EC=9E=85=20=EC=A0=84=EC=9A=A9=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../roomescape/util/DummyInitializer.kt | 224 ++++++++++++++++++ .../kotlin/roomescape/util/KotestConfig.kt | 66 +++--- 2 files changed, 254 insertions(+), 36 deletions(-) create mode 100644 src/test/kotlin/roomescape/util/DummyInitializer.kt diff --git a/src/test/kotlin/roomescape/util/DummyInitializer.kt b/src/test/kotlin/roomescape/util/DummyInitializer.kt new file mode 100644 index 00000000..cef0eab6 --- /dev/null +++ b/src/test/kotlin/roomescape/util/DummyInitializer.kt @@ -0,0 +1,224 @@ +package roomescape.util + +import io.restassured.module.kotlin.extensions.Extract +import io.restassured.module.kotlin.extensions.Given +import io.restassured.module.kotlin.extensions.When +import org.springframework.data.repository.findByIdOrNull +import org.springframework.http.MediaType +import roomescape.payment.business.PaymentWriter +import roomescape.payment.infrastructure.client.CardDetail +import roomescape.payment.infrastructure.client.EasyPayDetail +import roomescape.payment.infrastructure.client.TransferDetail +import roomescape.payment.infrastructure.common.PaymentMethod +import roomescape.payment.infrastructure.persistence.CanceledPaymentEntity +import roomescape.payment.infrastructure.persistence.PaymentEntity +import roomescape.payment.infrastructure.persistence.PaymentRepository +import roomescape.payment.web.PaymentConfirmRequest +import roomescape.payment.web.PaymentRetrieveResponse +import roomescape.payment.web.toPaymentDetailResponse +import roomescape.payment.web.toRetrieveResponse +import roomescape.reservation.infrastructure.persistence.ReservationEntity +import roomescape.reservation.infrastructure.persistence.ReservationRepository +import roomescape.reservation.web.PendingReservationCreateRequest +import roomescape.schedule.infrastructure.persistence.ScheduleEntity +import roomescape.schedule.infrastructure.persistence.ScheduleRepository +import roomescape.schedule.infrastructure.persistence.ScheduleStatus +import roomescape.schedule.web.ScheduleCreateRequest +import roomescape.schedule.web.ScheduleUpdateRequest +import roomescape.theme.infrastructure.persistence.ThemeEntity +import roomescape.theme.infrastructure.persistence.ThemeRepository +import roomescape.theme.web.ThemeCreateRequest +import java.time.LocalDateTime + +class DummyInitializer( + private val themeRepository: ThemeRepository, + private val scheduleRepository: ScheduleRepository, + private val reservationRepository: ReservationRepository, + private val paymentRepository: PaymentRepository, + private val paymentWriter: PaymentWriter +) { + fun createTheme(adminToken: String, request: ThemeCreateRequest): ThemeEntity { + val createdThemeId: Long = Given { + contentType(MediaType.APPLICATION_JSON_VALUE) + header("Authorization", "Bearer $adminToken") + body(request) + } When { + post("/admin/themes") + } Extract { + path("data.id") + } + + return themeRepository.findByIdOrNull(createdThemeId) + ?: throw RuntimeException("unexpected error occurred") + } + + fun createSchedule( + adminToken: String, + request: ScheduleCreateRequest, + status: ScheduleStatus = ScheduleStatus.AVAILABLE + ): ScheduleEntity { + val themeId: Long = if (request.themeId > 1L) { + request.themeId + } else { + createTheme( + adminToken = adminToken, + request = ThemeFixture.createRequest.copy(name = "theme-${System.currentTimeMillis()}") + ).id + } + val createdScheduleId: Long = Given { + contentType(MediaType.APPLICATION_JSON_VALUE) + header("Authorization", "Bearer $adminToken") + body(request.copy(themeId = themeId)) + } When { + post("/schedules") + } Extract { + path("data.id") + } + + Given { + contentType(MediaType.APPLICATION_JSON_VALUE) + header("Authorization", "Bearer $adminToken") + body(ScheduleUpdateRequest(status = status)) + } When { + patch("/schedules/$createdScheduleId") + } + + return scheduleRepository.findByIdOrNull(createdScheduleId) + ?: throw RuntimeException("unexpected error occurred") + } + + fun createPendingReservation( + adminToken: String, + reserverToken: String, + themeRequest: ThemeCreateRequest = ThemeFixture.createRequest, + scheduleRequest: ScheduleCreateRequest = ScheduleFixture.createRequest, + reservationRequest: PendingReservationCreateRequest = ReservationFixture.pendingCreateRequest, + ): ReservationEntity { + val themeId: Long = createTheme( + adminToken = adminToken, + request = themeRequest + ).id + + val scheduleId: Long = createSchedule( + adminToken = adminToken, + request = scheduleRequest.copy(themeId = themeId), + status = ScheduleStatus.HOLD + ).id + + return createPendingReservation( + reserverToken = reserverToken, + request = reservationRequest.copy(scheduleId = scheduleId) + ) + } + + fun createConfirmReservation( + adminToken: String, + reserverToken: String, + themeRequest: ThemeCreateRequest = ThemeFixture.createRequest, + scheduleRequest: ScheduleCreateRequest = ScheduleFixture.createRequest, + reservationRequest: PendingReservationCreateRequest = ReservationFixture.pendingCreateRequest, + ): ReservationEntity { + val themeId: Long = createTheme( + adminToken = adminToken, + request = themeRequest + ).id + + val schedule: ScheduleEntity = createSchedule( + adminToken = adminToken, + request = scheduleRequest.copy(themeId = themeId), + status = ScheduleStatus.HOLD + ) + + val reservation = createPendingReservation( + reserverToken = reserverToken, + request = reservationRequest.copy(scheduleId = schedule.id) + ) + + Given { + contentType(MediaType.APPLICATION_JSON_VALUE) + header("Authorization", "Bearer $reserverToken") + } When { + post("/reservations/${reservation.id}/confirm") + } + + return reservationRepository.findByIdOrNull(reservation.id) + ?: throw RuntimeException("unexpected error occurred") + } + + fun createPayment( + reservationId: Long, + request: PaymentConfirmRequest = PaymentFixture.confirmRequest, + cardDetail: CardDetail? = null, + easyPayDetail: EasyPayDetail? = null, + transferDetail: TransferDetail? = null, + ): PaymentRetrieveResponse { + val method = if (easyPayDetail != null) { + PaymentMethod.EASY_PAY + } else if (cardDetail != null) { + PaymentMethod.CARD + } else if (transferDetail != null) { + PaymentMethod.TRANSFER + } else { + throw AssertionError("결제타입 확인 필요.") + } + + val clientConfirmResponse = PaymentFixture.confirmResponse( + paymentKey = request.paymentKey, + amount = request.amount, + method = method, + cardDetail = cardDetail, + easyPayDetail = easyPayDetail, + transferDetail = transferDetail + ) + + val payment = paymentWriter.createPayment( + reservationId = reservationId, + orderId = request.orderId, + paymentType = request.paymentType, + paymentClientConfirmResponse = clientConfirmResponse + ) + + val detail = paymentWriter.createDetail(clientConfirmResponse, payment.id) + + return payment.toRetrieveResponse(detail = detail.toPaymentDetailResponse(), cancel = null) + } + + fun cancelPayment( + memberId: Long, + reservationId: Long, + cancelReason: String, + ): CanceledPaymentEntity { + val payment: PaymentEntity = paymentRepository.findByReservationId(reservationId) + ?: throw AssertionError("Unexpected Exception Occurred.") + + val clientCancelResponse = PaymentFixture.cancelResponse( + amount = payment.totalAmount, + cancelReason = cancelReason, + ) + + return paymentWriter.cancel( + memberId, + payment, + requestedAt = LocalDateTime.now(), + clientCancelResponse + ) + } + + private fun createPendingReservation( + reserverToken: String, + request: PendingReservationCreateRequest, + ): ReservationEntity { + val createdReservationId: Long = Given { + contentType(MediaType.APPLICATION_JSON_VALUE) + header("Authorization", "Bearer $reserverToken") + body(request.copy(scheduleId = request.scheduleId)) + } When { + post("/reservations/pending") + } Extract { + path("data.id") + } + + return reservationRepository.findByIdOrNull(createdReservationId) + ?: throw RuntimeException("unexpected error occurred") + } +} diff --git a/src/test/kotlin/roomescape/util/KotestConfig.kt b/src/test/kotlin/roomescape/util/KotestConfig.kt index a808def5..39abd06d 100644 --- a/src/test/kotlin/roomescape/util/KotestConfig.kt +++ b/src/test/kotlin/roomescape/util/KotestConfig.kt @@ -2,29 +2,39 @@ package roomescape.util import io.kotest.core.config.AbstractProjectConfig import io.kotest.core.spec.Spec -import io.kotest.core.spec.style.BehaviorSpec import io.kotest.core.spec.style.FunSpec -import io.kotest.core.spec.style.StringSpec import io.kotest.extensions.spring.SpringExtension import io.kotest.extensions.spring.SpringTestExtension import io.restassured.RestAssured import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.context.TestConfiguration import org.springframework.boot.test.web.server.LocalServerPort +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Import import roomescape.member.infrastructure.persistence.MemberRepository +import roomescape.payment.business.PaymentWriter +import roomescape.payment.infrastructure.persistence.PaymentRepository +import roomescape.reservation.infrastructure.persistence.ReservationRepository +import roomescape.schedule.infrastructure.persistence.ScheduleRepository +import roomescape.theme.infrastructure.persistence.ThemeRepository import roomescape.util.CleanerMode.AFTER_EACH_TEST object KotestConfig : AbstractProjectConfig() { override fun extensions(): List = listOf(SpringExtension) } +@Import(TestConfig::class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -abstract class FunSpecSpringbootTest : FunSpec({ +abstract class FunSpecSpringbootTest: FunSpec({ extension(DatabaseCleanerExtension(mode = AFTER_EACH_TEST)) }) { @Autowired private lateinit var memberRepository: MemberRepository + @Autowired + lateinit var dummyInitializer: DummyInitializer + @LocalServerPort var port: Int = 0 @@ -36,38 +46,22 @@ abstract class FunSpecSpringbootTest : FunSpec({ } } -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -abstract class StringSpecSpringbootTest : StringSpec({ - extension(DatabaseCleanerExtension(mode = AFTER_EACH_TEST)) -}) { - @Autowired - private lateinit var memberRepository: MemberRepository - - @LocalServerPort - var port: Int = 0 - - lateinit var loginUtil: LoginUtil - - override suspend fun beforeSpec(spec: Spec) { - RestAssured.port = port - loginUtil = LoginUtil(memberRepository) - } -} - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -abstract class BehaviorSpecSpringbootTest : BehaviorSpec({ - extension(DatabaseCleanerExtension(mode = AFTER_EACH_TEST)) -}) { - @Autowired - private lateinit var memberRepository: MemberRepository - - @LocalServerPort - var port: Int = 0 - - lateinit var loginUtil: LoginUtil - - override suspend fun beforeSpec(spec: Spec) { - RestAssured.port = port - loginUtil = LoginUtil(memberRepository) +@TestConfiguration +class TestConfig { + @Bean + fun dummyInitializer( + themeRepository: ThemeRepository, + scheduleRepository: ScheduleRepository, + reservationRepository: ReservationRepository, + paymentWriter: PaymentWriter, + paymentRepository: PaymentRepository + ): DummyInitializer { + return DummyInitializer( + themeRepository = themeRepository, + scheduleRepository = scheduleRepository, + reservationRepository = reservationRepository, + paymentWriter = paymentWriter, + paymentRepository = paymentRepository + ) } }