[#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,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<SpringTestExtension> = 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
)
}
}