[#41] 예약 스키마 재정의 #42

Merged
pricelees merged 41 commits from refactor/#41 into main 2025-09-09 00:43:39 +00:00
2 changed files with 254 additions and 36 deletions
Showing only changes of commit c717e1cb5b - Show all commits

View File

@ -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")
}
}

View File

@ -2,22 +2,29 @@ package roomescape.util
import io.kotest.core.config.AbstractProjectConfig import io.kotest.core.config.AbstractProjectConfig
import io.kotest.core.spec.Spec 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.FunSpec
import io.kotest.core.spec.style.StringSpec
import io.kotest.extensions.spring.SpringExtension import io.kotest.extensions.spring.SpringExtension
import io.kotest.extensions.spring.SpringTestExtension import io.kotest.extensions.spring.SpringTestExtension
import io.restassured.RestAssured import io.restassured.RestAssured
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest 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.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.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 import roomescape.util.CleanerMode.AFTER_EACH_TEST
object KotestConfig : AbstractProjectConfig() { object KotestConfig : AbstractProjectConfig() {
override fun extensions(): List<SpringTestExtension> = listOf(SpringExtension) override fun extensions(): List<SpringTestExtension> = listOf(SpringExtension)
} }
@Import(TestConfig::class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
abstract class FunSpecSpringbootTest: FunSpec({ abstract class FunSpecSpringbootTest: FunSpec({
extension(DatabaseCleanerExtension(mode = AFTER_EACH_TEST)) extension(DatabaseCleanerExtension(mode = AFTER_EACH_TEST))
@ -25,23 +32,8 @@ abstract class FunSpecSpringbootTest : FunSpec({
@Autowired @Autowired
private lateinit var memberRepository: MemberRepository 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 StringSpecSpringbootTest : StringSpec({
extension(DatabaseCleanerExtension(mode = AFTER_EACH_TEST))
}) {
@Autowired @Autowired
private lateinit var memberRepository: MemberRepository lateinit var dummyInitializer: DummyInitializer
@LocalServerPort @LocalServerPort
var port: Int = 0 var port: Int = 0
@ -54,20 +46,22 @@ abstract class StringSpecSpringbootTest : StringSpec({
} }
} }
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @TestConfiguration
abstract class BehaviorSpecSpringbootTest : BehaviorSpec({ class TestConfig {
extension(DatabaseCleanerExtension(mode = AFTER_EACH_TEST)) @Bean
}) { fun dummyInitializer(
@Autowired themeRepository: ThemeRepository,
private lateinit var memberRepository: MemberRepository scheduleRepository: ScheduleRepository,
reservationRepository: ReservationRepository,
@LocalServerPort paymentWriter: PaymentWriter,
var port: Int = 0 paymentRepository: PaymentRepository
): DummyInitializer {
lateinit var loginUtil: LoginUtil return DummyInitializer(
themeRepository = themeRepository,
override suspend fun beforeSpec(spec: Spec) { scheduleRepository = scheduleRepository,
RestAssured.port = port reservationRepository = reservationRepository,
loginUtil = LoginUtil(memberRepository) paymentWriter = paymentWriter,
paymentRepository = paymentRepository
)
} }
} }