test: 예약 API 테스트 추가

This commit is contained in:
이상진 2025-09-09 09:14:53 +09:00
parent b847e59d6f
commit 211edcaffd

View File

@ -0,0 +1,571 @@
package roomescape.reservation
import io.kotest.matchers.shouldBe
import org.hamcrest.CoreMatchers.equalTo
import org.springframework.data.repository.findByIdOrNull
import org.springframework.http.HttpStatus
import roomescape.common.exception.CommonErrorCode
import roomescape.member.infrastructure.persistence.Role
import roomescape.payment.exception.PaymentErrorCode
import roomescape.payment.infrastructure.common.BankCode
import roomescape.payment.infrastructure.common.CardIssuerCode
import roomescape.payment.infrastructure.common.EasyPayCompanyCode
import roomescape.payment.infrastructure.persistence.PaymentDetailRepository
import roomescape.reservation.exception.ReservationErrorCode
import roomescape.reservation.infrastructure.persistence.CanceledReservationRepository
import roomescape.reservation.infrastructure.persistence.ReservationEntity
import roomescape.reservation.infrastructure.persistence.ReservationRepository
import roomescape.reservation.infrastructure.persistence.ReservationStatus
import roomescape.reservation.web.ReservationCancelRequest
import roomescape.schedule.infrastructure.persistence.ScheduleEntity
import roomescape.schedule.infrastructure.persistence.ScheduleRepository
import roomescape.schedule.infrastructure.persistence.ScheduleStatus
import roomescape.theme.infrastructure.persistence.ThemeEntity
import roomescape.util.*
import java.time.LocalDate
import java.time.LocalTime
class ReservationApiTest(
private val reservationRepository: ReservationRepository,
private val canceledReservationRepository: CanceledReservationRepository,
private val scheduleRepository: ScheduleRepository,
private val paymentDetailRepository: PaymentDetailRepository,
) : FunSpecSpringbootTest() {
init {
context("결제 전 임시 예약을 생성한다.") {
val commonRequest = ReservationFixture.pendingCreateRequest
test("정상 생성") {
val schedule: ScheduleEntity = dummyInitializer.createSchedule(
adminToken = loginUtil.loginAsAdmin(),
request = ScheduleFixture.createRequest,
status = ScheduleStatus.HOLD
)
runTest(
token = loginUtil.loginAsUser(),
using = {
body(commonRequest.copy(scheduleId = schedule.id))
},
on = {
post("/reservations/pending")
},
expect = {
statusCode(HttpStatus.OK.value())
}
).also {
val reservation: ReservationEntity =
reservationRepository.findByIdOrNull(it.extract().path("data.id"))
?: throw AssertionError("Unexpected Exception Occurred.")
reservation.status shouldBe ReservationStatus.PENDING
reservation.scheduleId shouldBe schedule.id
reservation.reserverName shouldBe commonRequest.reserverName
}
}
test("예약을 생성할 때 해당 일정이 ${ScheduleStatus.HOLD} 상태가 아니면 실패한다.") {
val schedule: ScheduleEntity = dummyInitializer.createSchedule(
adminToken = loginUtil.loginAsAdmin(),
request = ScheduleFixture.createRequest,
status = ScheduleStatus.AVAILABLE
)
runTest(
token = loginUtil.loginAsUser(),
using = {
body(commonRequest.copy(scheduleId = schedule.id))
},
on = {
post("/reservations/pending")
},
expect = {
statusCode(HttpStatus.BAD_REQUEST.value())
body("code", equalTo(ReservationErrorCode.SCHEDULE_NOT_HOLD.errorCode))
}
)
}
test("예약 인원이 테마의 최소 인원보다 작거나 최대 인원보다 많으면 실패한다.") {
val adminToken = loginUtil.loginAsAdmin()
val theme: ThemeEntity = dummyInitializer.createTheme(
adminToken = adminToken,
request = ThemeFixture.createRequest
)
val schedule: ScheduleEntity = dummyInitializer.createSchedule(
adminToken = adminToken,
request = ScheduleFixture.createRequest.copy(themeId = theme.id),
status = ScheduleStatus.HOLD
)
runTest(
token = loginUtil.loginAsUser(),
using = {
body(
commonRequest.copy(
scheduleId = schedule.id,
participantCount = ((theme.minParticipants - 1).toShort())
)
)
},
on = {
post("/reservations/pending")
},
expect = {
statusCode(HttpStatus.BAD_REQUEST.value())
body("code", equalTo(ReservationErrorCode.INVALID_PARTICIPANT_COUNT.errorCode))
}
)
runTest(
token = loginUtil.loginAsUser(),
using = {
body(
commonRequest.copy(
scheduleId = schedule.id,
participantCount = ((theme.maxParticipants + 1).toShort())
)
)
},
on = {
post("/reservations/pending")
},
expect = {
statusCode(HttpStatus.BAD_REQUEST.value())
body("code", equalTo(ReservationErrorCode.INVALID_PARTICIPANT_COUNT.errorCode))
}
)
}
context("필수 입력값이 입력되지 않으면 실패한다.") {
test("예약자명") {
runTest(
token = loginUtil.loginAsUser(),
using = {
body(commonRequest.copy(reserverName = ""))
},
on = {
post("/reservations/pending")
},
expect = {
statusCode(HttpStatus.BAD_REQUEST.value())
body("code", equalTo(CommonErrorCode.INVALID_INPUT_VALUE.errorCode))
}
)
}
test("예약자 연락처") {
runTest(
token = loginUtil.loginAsUser(),
using = {
body(commonRequest.copy(reserverContact = ""))
},
on = {
post("/reservations/pending")
},
expect = {
statusCode(HttpStatus.BAD_REQUEST.value())
body("code", equalTo(CommonErrorCode.INVALID_INPUT_VALUE.errorCode))
}
)
}
}
}
context("예약을 확정한다.") {
test("정상 응답") {
val userToken = loginUtil.loginAsUser()
val reservation: ReservationEntity = dummyInitializer.createPendingReservation(
adminToken = loginUtil.loginAsAdmin(),
reserverToken = userToken,
)
runTest(
token = userToken,
on = {
post("/reservations/${reservation.id}/confirm")
},
expect = {
statusCode(HttpStatus.OK.value())
}
).also {
val updatedReservation = reservationRepository.findByIdOrNull(reservation.id)
?: throw AssertionError("Unexpected Exception Occurred.")
val updatedSchedule = scheduleRepository.findByIdOrNull(updatedReservation.scheduleId)
?: throw AssertionError("Unexpected Exception Occurred.")
updatedSchedule.status shouldBe ScheduleStatus.RESERVED
updatedReservation.status shouldBe ReservationStatus.CONFIRMED
}
}
test("예약이 없으면 실패한다.") {
runTest(
token = loginUtil.loginAsUser(),
on = {
post("/reservations/$INVALID_PK/confirm")
},
expect = {
statusCode(HttpStatus.NOT_FOUND.value())
body("code", equalTo(ReservationErrorCode.RESERVATION_NOT_FOUND.errorCode))
}
)
}
}
context("예약을 취소한다.") {
test("정상 응답") {
val userToken = loginUtil.loginAsUser()
val reservation: ReservationEntity = dummyInitializer.createConfirmReservation(
adminToken = loginUtil.loginAsAdmin(),
reserverToken = userToken,
)
runTest(
token = userToken,
using = {
body(ReservationCancelRequest(cancelReason = "test"))
},
on = {
post("/reservations/${reservation.id}/cancel")
},
expect = {
statusCode(HttpStatus.OK.value())
}
).also {
val updatedReservation = reservationRepository.findByIdOrNull(reservation.id)
?: throw AssertionError("Unexpected Exception Occurred.")
val updatedSchedule = scheduleRepository.findByIdOrNull(updatedReservation.scheduleId)
?: throw AssertionError("Unexpected Exception Occurred.")
updatedReservation.status shouldBe ReservationStatus.CANCELED
updatedSchedule.status shouldBe ScheduleStatus.AVAILABLE
canceledReservationRepository.findAll()[0].reservationId shouldBe updatedReservation.id
}
}
test("예약이 없으면 실패한다.") {
runTest(
token = loginUtil.loginAsUser(),
using = {
body(ReservationCancelRequest(cancelReason = "test"))
},
on = {
post("/reservations/$INVALID_PK/cancel")
},
expect = {
statusCode(HttpStatus.NOT_FOUND.value())
body("code", equalTo(ReservationErrorCode.RESERVATION_NOT_FOUND.errorCode))
}
)
}
test("관리자가 아닌 회원은 다른 회원의 예약을 취소할 수 없다.") {
val reservation: ReservationEntity = dummyInitializer.createConfirmReservation(
adminToken = loginUtil.loginAsAdmin(),
reserverToken = loginUtil.loginAsUser(),
)
val otherUserToken = loginUtil.login("other@example.com", "other", role = Role.MEMBER)
runTest(
token = otherUserToken,
using = {
body(ReservationCancelRequest(cancelReason = "test"))
},
on = {
post("/reservations/${reservation.id}/cancel")
},
expect = {
statusCode(HttpStatus.FORBIDDEN.value())
body("code", equalTo(ReservationErrorCode.NO_PERMISSION_TO_CANCEL_RESERVATION.errorCode))
}
)
}
test("관리자는 다른 회원의 예약을 취소할 수 있다.") {
val reservation: ReservationEntity = dummyInitializer.createConfirmReservation(
adminToken = loginUtil.loginAsAdmin(),
reserverToken = loginUtil.loginAsAdmin(),
)
val otherAdminToken = loginUtil.login("admin1@example.com", "admin1", role = Role.ADMIN)
runTest(
token = otherAdminToken,
using = {
body(ReservationCancelRequest(cancelReason = "test"))
},
on = {
post("/reservations/${reservation.id}/cancel")
},
expect = {
statusCode(HttpStatus.OK.value())
}
).also {
val updatedReservation = reservationRepository.findByIdOrNull(reservation.id)
?: throw AssertionError("Unexpected Exception Occurred.")
val updatedSchedule = scheduleRepository.findByIdOrNull(updatedReservation.scheduleId)
?: throw AssertionError("Unexpected Exception Occurred.")
updatedSchedule.status shouldBe ScheduleStatus.AVAILABLE
updatedReservation.status shouldBe ReservationStatus.CANCELED
canceledReservationRepository.findAll()[0].reservationId shouldBe updatedReservation.id
}
}
}
context("나의 예약 목록을 조회한다.") {
test("정상 응답") {
val userToken = loginUtil.loginAsUser()
val adminToken = loginUtil.loginAsAdmin()
for (i in 1..3) {
dummyInitializer.createConfirmReservation(
adminToken = adminToken,
reserverToken = userToken,
themeRequest = ThemeFixture.createRequest.copy(name = "theme-$i"),
scheduleRequest = ScheduleFixture.createRequest.copy(
date = LocalDate.now().plusDays(i.toLong()),
time = LocalTime.now().plusHours(i.toLong())
)
)
}
runTest(
token = userToken,
on = {
get("/reservations/summary")
},
expect = {
statusCode(HttpStatus.OK.value())
body("data.reservations.size()", equalTo(3))
assertProperties(
props = setOf("id", "themeName", "date", "startAt", "status"),
propsNameIfList = "reservations"
)
}
)
}
}
context("예약 상세 정보를 조회한다.") {
context("정상 응답") {
val commonPaymentRequest = PaymentFixture.confirmRequest
lateinit var reservation: ReservationEntity
beforeTest {
reservation = dummyInitializer.createConfirmReservation(
adminToken = loginUtil.loginAsAdmin(),
reserverToken = loginUtil.loginAsUser(),
)
}
test("카드 결제") {
dummyInitializer.createPayment(
reservationId = reservation.id,
request = commonPaymentRequest,
cardDetail = PaymentFixture.cardDetail(
amount = commonPaymentRequest.amount,
issuerCode = CardIssuerCode.SHINHAN
)
)
runDetailRetrieveTest(reservation).also {
it["method"] shouldBe "카드"
with(it.get("detail") as LinkedHashMap<*, *>) {
this["type"] shouldBe "CARD"
this["issuerCode"] shouldBe "신한"
}
}
}
test("카드 + 간편결제") {
dummyInitializer.createPayment(
reservationId = reservation.id,
request = commonPaymentRequest,
easyPayDetail = PaymentFixture.easypayDetail(
amount = 0,
provider = EasyPayCompanyCode.TOSSPAY,
),
cardDetail = PaymentFixture.cardDetail(
amount = commonPaymentRequest.amount,
)
)
runDetailRetrieveTest(reservation).also {
it["method"] shouldBe "간편결제"
with(it.get("detail") as LinkedHashMap<*, *>) {
this["type"] shouldBe "CARD"
this["easypayProviderName"] shouldBe "토스페이"
}
}
}
test("결제 취소 / 카드 + 간편결제 + 포인트 사용") {
val point = 5_000
dummyInitializer.createPayment(
reservationId = reservation.id,
request = commonPaymentRequest,
easyPayDetail = PaymentFixture.easypayDetail(
amount = 0,
provider = EasyPayCompanyCode.TOSSPAY,
discountAmount = point,
),
cardDetail = PaymentFixture.cardDetail(
amount = commonPaymentRequest.amount,
issuerCode = CardIssuerCode.SHINHAN
)
)
val cancelReason = "테스트입니다."
val memberId = loginUtil.getUser().id!!
dummyInitializer.cancelPayment(
memberId = memberId,
reservationId = reservation.id,
cancelReason = cancelReason,
)
runDetailRetrieveTest(reservation).also {
it["method"] shouldBe "간편결제"
with(it.get("detail") as LinkedHashMap<*, *>) {
this["type"] shouldBe "CARD"
this["issuerCode"] shouldBe "신한"
this["easypayProviderName"] shouldBe "토스페이"
this["easypayDiscountAmount"] shouldBe point
}
with((it.get("cancel") as LinkedHashMap<*, *>)) {
this["cancelReason"] shouldBe cancelReason
this["canceledBy"] shouldBe memberId
}
}
}
test("간편결제 선불충전금액 사용") {
dummyInitializer.createPayment(
reservationId = reservation.id,
request = commonPaymentRequest,
easyPayDetail = PaymentFixture.easypayDetail(
amount = commonPaymentRequest.amount,
provider = EasyPayCompanyCode.TOSSPAY,
),
)
runDetailRetrieveTest(reservation).also {
it["method"] shouldBe "간편결제"
with(it.get("detail") as LinkedHashMap<*, *>) {
this["type"] shouldBe "EASYPAY_PREPAID"
this["providerName"] shouldBe "토스페이"
}
}
}
test("계좌이체 사용") {
dummyInitializer.createPayment(
reservationId = reservation.id,
request = commonPaymentRequest,
transferDetail = PaymentFixture.transferDetail(
bankCode = BankCode.SHINHAN
)
)
runDetailRetrieveTest(reservation).also {
it["method"] shouldBe "계좌이체"
with(it.get("detail") as LinkedHashMap<*, *>) {
this["type"] shouldBe "BANK_TRANSFER"
this["bankName"] shouldBe "신한"
}
}
}
}
test("예약이 없으면 실패한다.") {
runTest(
token = loginUtil.loginAsUser(),
on = {
get("/reservations/$INVALID_PK/detail")
},
expect = {
statusCode(HttpStatus.NOT_FOUND.value())
body("code", equalTo(ReservationErrorCode.RESERVATION_NOT_FOUND.errorCode))
}
)
}
test("예약은 있지만, 결제 정보가 없으면 실패한다.") {
val reservation = dummyInitializer.createConfirmReservation(
adminToken = loginUtil.loginAsAdmin(),
reserverToken = loginUtil.loginAsUser(),
)
runTest(
token = loginUtil.loginAsUser(),
on = {
get("/reservations/${reservation.id}/detail")
},
expect = {
statusCode(HttpStatus.NOT_FOUND.value())
body("code", equalTo(PaymentErrorCode.PAYMENT_NOT_FOUND.errorCode))
}
)
}
test("예약과 결제는 있지만, 결제 세부 내역이 없으면 실패한다.") {
val reservation = dummyInitializer.createConfirmReservation(
adminToken = loginUtil.loginAsAdmin(),
reserverToken = loginUtil.loginAsUser(),
)
dummyInitializer.createPayment(
reservationId = reservation.id,
easyPayDetail = PaymentFixture.easypayDetail(amount = 100_000)
).also {
paymentDetailRepository.deleteAll()
}
runTest(
token = loginUtil.loginAsUser(),
on = {
get("/reservations/${reservation.id}/detail")
},
expect = {
statusCode(HttpStatus.NOT_FOUND.value())
body("code", equalTo(PaymentErrorCode.PAYMENT_DETAIL_NOT_FOUND.errorCode))
}
)
}
}
}
fun runDetailRetrieveTest(
reservation: ReservationEntity
): LinkedHashMap<String, Any> {
return runTest(
token = loginUtil.loginAsUser(),
on = {
get("/reservations/${reservation.id}/detail")
},
expect = {
statusCode(HttpStatus.OK.value())
assertProperties(props = setOf("id", "member", "applicationDateTime", "payment"))
}
).also {
it.extract().path<Long>("data.id") shouldBe reservation.id
it.extract().path<Long>("data.member.id") shouldBe reservation.memberId
}.extract().path("data.payment")
}
}