diff --git a/src/test/kotlin/roomescape/payment/implement/PaymentWriterTest.kt b/src/test/kotlin/roomescape/payment/implement/PaymentWriterTest.kt index 101b4fd2..ac003c06 100644 --- a/src/test/kotlin/roomescape/payment/implement/PaymentWriterTest.kt +++ b/src/test/kotlin/roomescape/payment/implement/PaymentWriterTest.kt @@ -61,6 +61,7 @@ class PaymentWriterTest : FunSpec({ val canceledAt = OffsetDateTime.now() afterTest { + clearMocks(paymentRepository) clearMocks(canceledPaymentRepository) } @@ -72,11 +73,14 @@ class PaymentWriterTest : FunSpec({ canceledPaymentRepository.save(capture(slot)) } returns mockk() + every { + paymentRepository.deleteByPaymentKey(paymentKey) + } returns Unit + paymentWriter.createCanceled(payment, cancelReason, canceledAt) - verify(exactly = 1) { - canceledPaymentRepository.save(any()) - } + verify(exactly = 1) { canceledPaymentRepository.save(any()) } + verify(exactly = 1) { paymentRepository.deleteByPaymentKey(any()) } slot.captured.also { it.paymentKey shouldBe payment.paymentKey @@ -92,6 +96,10 @@ class PaymentWriterTest : FunSpec({ canceledPaymentRepository.save(capture(slot)) } returns mockk() + every { + paymentRepository.deleteByPaymentKey(paymentKey) + } returns Unit + paymentWriter.createCanceled( cancelReason = cancelReason, cancelAmount = totalAmount, @@ -100,9 +108,8 @@ class PaymentWriterTest : FunSpec({ paymentKey = paymentKey ) - verify(exactly = 1) { - canceledPaymentRepository.save(any()) - } + verify(exactly = 1) { canceledPaymentRepository.save(any()) } + verify(exactly = 1) { paymentRepository.deleteByPaymentKey(any()) } slot.captured.also { it.paymentKey shouldBe paymentKey diff --git a/src/test/kotlin/roomescape/reservation/business/ReservationCommandServiceTest.kt b/src/test/kotlin/roomescape/reservation/business/ReservationCommandServiceTest.kt new file mode 100644 index 00000000..173dcd9d --- /dev/null +++ b/src/test/kotlin/roomescape/reservation/business/ReservationCommandServiceTest.kt @@ -0,0 +1,285 @@ +package roomescape.reservation.business + +import io.kotest.assertions.assertSoftly +import io.kotest.assertions.throwables.shouldNotThrow +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.mockk.* +import roomescape.reservation.exception.ReservationErrorCode +import roomescape.reservation.exception.ReservationException +import roomescape.reservation.implement.ReservationFinder +import roomescape.reservation.implement.ReservationWriter +import roomescape.reservation.infrastructure.persistence.ReservationEntity +import roomescape.reservation.infrastructure.persistence.ReservationStatus +import roomescape.util.MemberFixture +import roomescape.util.ReservationFixture +import roomescape.util.ThemeFixture +import roomescape.util.TimeFixture + +class ReservationCommandServiceTest : FunSpec({ + + val reservationFinder: ReservationFinder = mockk() + val reservationWriter: ReservationWriter = mockk() + val reservationCommandService = ReservationCommandService(reservationFinder, reservationWriter) + + context("createReservationWithPayment") { + val request = ReservationFixture.createRequest() + val memberId = 1L + + test("정상 응답") { + val createdReservation = ReservationFixture.create( + date = request.date, + time = TimeFixture.create(id = request.timeId), + theme = ThemeFixture.create(id = request.themeId), + member = MemberFixture.create(id = memberId), + status = ReservationStatus.CONFIRMED + ) + + every { + reservationWriter.create( + date = request.date, + timeId = request.timeId, + themeId = request.themeId, + status = ReservationStatus.CONFIRMED, + memberId = memberId, + requesterId = memberId + ) + } returns createdReservation + + val result = reservationCommandService.createReservationWithPayment(request, memberId) + + assertSoftly(result) { + this.date shouldBe request.date + this.time.id shouldBe request.timeId + this.theme.id shouldBe request.themeId + this.member.id shouldBe memberId + this.status shouldBe ReservationStatus.CONFIRMED + } + } + + test("예약 생성에 실패하면 예외 응답") { + every { + reservationWriter.create(any(), any(), any(), any(), any(), any()) + } throws ReservationException(ReservationErrorCode.RESERVATION_DUPLICATED) + + shouldThrow { + reservationCommandService.createReservationWithPayment(request, memberId) + }.also { + it.errorCode shouldBe ReservationErrorCode.RESERVATION_DUPLICATED + } + } + } + + context("createReservationByAdmin") { + val request = ReservationFixture.createAdminRequest() + val adminId = request.memberId + 1 + + test("정상 응답") { + val createdReservation = ReservationFixture.create( + date = request.date, + time = TimeFixture.create(id = request.timeId), + theme = ThemeFixture.create(id = request.themeId), + member = MemberFixture.create(id = request.memberId), + status = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED + ) + + every { + reservationWriter.create( + date = request.date, + timeId = request.timeId, + themeId = request.themeId, + status = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED, + memberId = request.memberId, + requesterId = adminId + ) + } returns createdReservation + + val response = reservationCommandService.createReservationByAdmin(request, adminId) + + assertSoftly(response) { + this.date shouldBe request.date + this.time.id shouldBe request.timeId + this.theme.id shouldBe request.themeId + this.member.id shouldBe request.memberId + this.status shouldBe ReservationStatus.CONFIRMED_PAYMENT_REQUIRED + } + } + } + + context("createWaiting") { + val request = ReservationFixture.createWaitingRequest() + val memberId = 1L + + test("정상 응답") { + val createdWaiting = ReservationFixture.create( + date = request.date, + time = TimeFixture.create(id = request.timeId), + theme = ThemeFixture.create(id = request.themeId), + member = MemberFixture.create(id = memberId), + status = ReservationStatus.WAITING + ) + + every { + reservationWriter.create( + date = request.date, + timeId = request.timeId, + themeId = request.themeId, + status = ReservationStatus.WAITING, + memberId = memberId, + requesterId = memberId + ) + } returns createdWaiting + + val response = reservationCommandService.createWaiting(request, memberId) + + assertSoftly(response) { + this.date shouldBe request.date + this.time.id shouldBe request.timeId + this.theme.id shouldBe request.themeId + this.member.id shouldBe memberId + this.status shouldBe ReservationStatus.WAITING + } + } + + test("이미 예약한 내역이 있으면 예외 응답") { + every { + reservationWriter.create(any(), any(), any(), any(), any(), any()) + } throws ReservationException(ReservationErrorCode.ALREADY_RESERVE) + + shouldThrow { + reservationCommandService.createWaiting(request, memberId) + }.also { + it.errorCode shouldBe ReservationErrorCode.ALREADY_RESERVE + } + } + } + + context("deleteReservation") { + val reservationId = 1L + val memberId = 1L + val reservation = ReservationFixture.create(id = reservationId, member = MemberFixture.create(id = memberId)) + + test("정상 응답") { + every { reservationFinder.findById(reservationId) } returns reservation + every { reservationWriter.deleteConfirmed(reservation, memberId) } just Runs + + shouldNotThrow { + reservationCommandService.deleteReservation(reservationId, memberId) + } + + verify(exactly = 1) { reservationWriter.deleteConfirmed(reservation, memberId) } + } + + test("예약을 찾을 수 없으면 예외 응답") { + every { + reservationFinder.findById(reservationId) + } throws ReservationException(ReservationErrorCode.RESERVATION_NOT_FOUND) + + shouldThrow { + reservationCommandService.deleteReservation(reservationId, memberId) + }.also { + it.errorCode shouldBe ReservationErrorCode.RESERVATION_NOT_FOUND + } + } + + test("삭제하려는 회원이 관리자가 아니고, 대기한 회원과 다르면 예외 응답") { + every { reservationFinder.findById(reservationId) } returns reservation + every { + reservationWriter.deleteConfirmed(reservation, memberId) + } throws ReservationException(ReservationErrorCode.NOT_RESERVATION_OWNER) + + shouldThrow { + reservationCommandService.deleteReservation(reservationId, memberId) + }.also { + it.errorCode shouldBe ReservationErrorCode.NOT_RESERVATION_OWNER + } + } + } + + context("confirmWaiting") { + val reservationId = 1L + val memberId = 99L // Admin + + test("정상 응답") { + every { reservationWriter.confirm(reservationId) } just Runs + + shouldNotThrow { + reservationCommandService.confirmWaiting(reservationId, memberId) + } + + verify(exactly = 1) { reservationWriter.confirm(reservationId) } + } + + test("이미 확정된 예약이 있으면 예외 응답") { + every { + reservationWriter.confirm(reservationId) + } throws ReservationException(ReservationErrorCode.CONFIRMED_RESERVATION_ALREADY_EXISTS) + + shouldThrow { + reservationCommandService.confirmWaiting(reservationId, memberId) + }.also { + it.errorCode shouldBe ReservationErrorCode.CONFIRMED_RESERVATION_ALREADY_EXISTS + } + } + } + + context("deleteWaiting") { + val reservationId = 1L + val memberId = 1L + val waitingReservation = ReservationFixture.create( + id = reservationId, + member = MemberFixture.create(id = memberId), + status = ReservationStatus.WAITING + ) + + test("정상 응답") { + every { reservationFinder.findById(reservationId) } returns waitingReservation + every { reservationWriter.deleteWaiting(waitingReservation, memberId) } just Runs + + shouldNotThrow { + reservationCommandService.deleteWaiting(reservationId, memberId) + } + + verify(exactly = 1) { reservationWriter.deleteWaiting(waitingReservation, memberId) } + } + + test("예약을 찾을 수 없으면 예외 응답") { + every { + reservationFinder.findById(reservationId) + } throws ReservationException(ReservationErrorCode.RESERVATION_NOT_FOUND) + + shouldThrow< ReservationException> { + reservationCommandService.deleteWaiting(reservationId, memberId) + }.also { + it.errorCode shouldBe ReservationErrorCode.RESERVATION_NOT_FOUND + } + } + + test("대기 상태가 아니면 예외 응답") { + every { reservationFinder.findById(reservationId) } returns waitingReservation + every { + reservationWriter.deleteWaiting(waitingReservation, memberId) + } throws ReservationException(ReservationErrorCode.ALREADY_CONFIRMED) + + shouldThrow { + reservationCommandService.deleteWaiting(reservationId, memberId) + }.also { + it.errorCode shouldBe ReservationErrorCode.ALREADY_CONFIRMED + } + } + + test("삭제하려는 회원이 관리자가 아니고, 대기한 회원과 다르면 예외 응답") { + every { reservationFinder.findById(reservationId) } returns waitingReservation + every { + reservationWriter.deleteWaiting(waitingReservation, memberId) + } throws ReservationException(ReservationErrorCode.NOT_RESERVATION_OWNER) + + shouldThrow { + reservationCommandService.deleteWaiting(reservationId, memberId) + }.also { + it.errorCode shouldBe ReservationErrorCode.NOT_RESERVATION_OWNER + } + } + } +}) diff --git a/src/test/kotlin/roomescape/reservation/business/ReservationQueryServiceTest.kt b/src/test/kotlin/roomescape/reservation/business/ReservationQueryServiceTest.kt new file mode 100644 index 00000000..0b909840 --- /dev/null +++ b/src/test/kotlin/roomescape/reservation/business/ReservationQueryServiceTest.kt @@ -0,0 +1,118 @@ +package roomescape.reservation.business + +import io.kotest.assertions.assertSoftly +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import roomescape.reservation.exception.ReservationErrorCode +import roomescape.reservation.exception.ReservationException +import roomescape.reservation.implement.ReservationFinder +import roomescape.reservation.infrastructure.persistence.ReservationStatus +import roomescape.reservation.web.MyReservationRetrieveResponse +import roomescape.util.MemberFixture +import roomescape.util.ReservationFixture +import roomescape.util.ThemeFixture +import java.time.LocalDate + +class ReservationQueryServiceTest : FunSpec({ + + val reservationFinder: ReservationFinder = mockk() + val reservationQueryService = ReservationQueryService(reservationFinder) + + context("findReservations") { + test("정상 응답") { + val confirmedReservations = listOf( + ReservationFixture.create(status = ReservationStatus.CONFIRMED), + ReservationFixture.create(status = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED) + ) + every { + reservationFinder.findAllByStatuses(*ReservationStatus.confirmedStatus()) + } returns confirmedReservations + + val response = reservationQueryService.findReservations() + + assertSoftly(response.reservations) { + this shouldHaveSize 2 + } + } + } + + context("findAllWaiting") { + test("정상 응답") { + val waitingReservations = listOf( + ReservationFixture.create(status = ReservationStatus.WAITING), + ReservationFixture.create(status = ReservationStatus.WAITING) + ) + every { + reservationFinder.findAllByStatuses(ReservationStatus.WAITING) + } returns waitingReservations + + val response = reservationQueryService.findAllWaiting() + + assertSoftly(response.reservations) { + this shouldHaveSize 2 + } + } + } + + context("findReservationsByMemberId") { + val memberId = 1L + test("정상 응답") { + val myReservations = listOf(mockk(), mockk()) + + every { + reservationFinder.findAllByMemberId(memberId) + } returns myReservations + + val response = reservationQueryService.findReservationsByMemberId(memberId) + + response.reservations shouldHaveSize 2 + } + } + + context("searchReservations") { + val themeId = 1L + val memberId = 1L + val startFrom = LocalDate.now() + val endAt = LocalDate.now().plusDays(1) + + test("정상 응답") { + val searchedReservations = listOf( + ReservationFixture.create( + theme = ThemeFixture.create(themeId), + member = MemberFixture.create(memberId), + date = startFrom + ) + ) + + every { + reservationFinder.searchReservations(themeId, memberId, startFrom, endAt) + } returns searchedReservations + + val response = reservationQueryService.searchReservations(themeId, memberId, startFrom, endAt) + + assertSoftly(response.reservations) { + this shouldHaveSize 1 + this[0].theme.id shouldBe themeId + this[0].member.id shouldBe memberId + this[0].date shouldBe startFrom + } + } + + test("종료 날짜가 시작 날짜 이전이면 예외 응답") { + val invalidEndAt = startFrom.minusDays(1) + every { + reservationFinder.searchReservations(themeId, memberId, startFrom, invalidEndAt) + } throws ReservationException(ReservationErrorCode.INVALID_SEARCH_DATE_RANGE) + + shouldThrow { + reservationQueryService.searchReservations(themeId, memberId, startFrom, invalidEndAt) + }.also { + it.errorCode shouldBe ReservationErrorCode.INVALID_SEARCH_DATE_RANGE + } + } + } +}) diff --git a/src/test/kotlin/roomescape/reservation/business/ReservationWithPaymentServiceTest.kt b/src/test/kotlin/roomescape/reservation/business/ReservationWithPaymentServiceTest.kt index c442f624..404b6bd4 100644 --- a/src/test/kotlin/roomescape/reservation/business/ReservationWithPaymentServiceTest.kt +++ b/src/test/kotlin/roomescape/reservation/business/ReservationWithPaymentServiceTest.kt @@ -18,11 +18,11 @@ import roomescape.reservation.web.ReservationCreateWithPaymentRequest import roomescape.util.* class ReservationWithPaymentServiceTest : FunSpec({ - val reservationService: ReservationService = mockk() + val reservationService: ReservationCommandService = mockk() val paymentService: PaymentService = mockk() val reservationWithPaymentService = ReservationWithPaymentService( - reservationService = reservationService, + reservationCommandService = reservationService, paymentService = paymentService ) @@ -48,7 +48,7 @@ class ReservationWithPaymentServiceTest : FunSpec({ context("addReservationWithPayment") { test("예약 및 결제 정보를 저장한다.") { every { - reservationService.createConfirmedReservation(reservationCreateWithPaymentRequest, memberId) + reservationService.createReservationWithPayment(reservationCreateWithPaymentRequest, memberId) } returns reservationEntity every { diff --git a/src/test/kotlin/roomescape/reservation/implement/ReservationFinderTest.kt b/src/test/kotlin/roomescape/reservation/implement/ReservationFinderTest.kt new file mode 100644 index 00000000..e11946d3 --- /dev/null +++ b/src/test/kotlin/roomescape/reservation/implement/ReservationFinderTest.kt @@ -0,0 +1,64 @@ +package roomescape.reservation.implement + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import org.springframework.data.jpa.domain.Specification +import org.springframework.data.repository.findByIdOrNull +import roomescape.reservation.exception.ReservationErrorCode +import roomescape.reservation.exception.ReservationException +import roomescape.reservation.infrastructure.persistence.ReservationEntity +import roomescape.reservation.infrastructure.persistence.ReservationRepository +import roomescape.reservation.infrastructure.persistence.ReservationSearchSpecification +import roomescape.reservation.infrastructure.persistence.ReservationStatus +import roomescape.time.exception.TimeErrorCode +import roomescape.time.exception.TimeException +import java.time.LocalDate + +class ReservationFinderTest : FunSpec({ + val reservationRepository: ReservationRepository = mockk() + val reservationValidator = ReservationValidator(reservationRepository) + + val reservationFinder = ReservationFinder(reservationRepository, reservationValidator) + + context("findById") { + val reservationId = 1L + test("동일한 ID인 시간을 찾아 응답한다.") { + every { + reservationRepository.findByIdOrNull(reservationId) + } returns mockk() + + reservationFinder.findById(reservationId) + + verify(exactly = 1) { + reservationRepository.findByIdOrNull(reservationId) + } + } + + test("동일한 ID인 시간이 없으면 실패한다.") { + every { + reservationRepository.findByIdOrNull(reservationId) + } returns null + + shouldThrow { + reservationFinder.findById(reservationId) + }.also { + it.errorCode shouldBe ReservationErrorCode.RESERVATION_NOT_FOUND + } + } + } + + context("searchReservations") { + test("시작 날짜가 종료 날짜 이전이면 실패한다.") { + shouldThrow { + reservationFinder.searchReservations(1L, 1L, LocalDate.now(), LocalDate.now().minusDays(1)) + }.also { + it.errorCode shouldBe ReservationErrorCode.INVALID_SEARCH_DATE_RANGE + } + } + } +}) diff --git a/src/test/kotlin/roomescape/reservation/implement/ReservationValidatorTest.kt b/src/test/kotlin/roomescape/reservation/implement/ReservationValidatorTest.kt new file mode 100644 index 00000000..878a5cc0 --- /dev/null +++ b/src/test/kotlin/roomescape/reservation/implement/ReservationValidatorTest.kt @@ -0,0 +1,170 @@ +package roomescape.reservation.implement + +import io.kotest.assertions.throwables.shouldNotThrow +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import org.springframework.data.jpa.domain.Specification +import roomescape.member.infrastructure.persistence.Role +import roomescape.reservation.exception.ReservationErrorCode +import roomescape.reservation.exception.ReservationException +import roomescape.reservation.infrastructure.persistence.ReservationEntity +import roomescape.reservation.infrastructure.persistence.ReservationRepository +import roomescape.reservation.infrastructure.persistence.ReservationStatus +import roomescape.util.MemberFixture +import roomescape.util.ReservationFixture +import roomescape.util.ThemeFixture +import roomescape.util.TimeFixture +import java.time.LocalDate +import java.time.LocalTime + +class ReservationValidatorTest : FunSpec({ + val reservationRepository: ReservationRepository = mockk() + + val reservationValidator = ReservationValidator(reservationRepository) + + context("validateIsNotPast") { + val today = LocalDate.now() + val now = LocalTime.now() + + test("입력된 날짜가 오늘 이전이면 예외를 던진다.") { + val requestDate = today.minusDays(1) + + shouldThrow { + reservationValidator.validateIsPast(requestDate, now) + }.also { + it.errorCode shouldBe ReservationErrorCode.PAST_REQUEST_DATETIME + } + } + + test("오늘 날짜라도 시간이 지났다면 예외를 던진다.") { + val requestTime = now.minusMinutes(1) + + shouldThrow { + reservationValidator.validateIsPast(today, requestTime) + }.also { + it.errorCode shouldBe ReservationErrorCode.PAST_REQUEST_DATETIME + } + } + } + + context("validateSearchDateRange") { + test("시작 날짜만 입력되면 종료한다.") { + shouldNotThrow { + reservationValidator.validateSearchDateRange(LocalDate.now(), null) + } + } + + test("종료 날짜만 입력되면 종료한다.") { + shouldNotThrow { + reservationValidator.validateSearchDateRange(null, LocalDate.now()) + } + } + + test("두 날짜가 같으면 종료한다.") { + shouldNotThrow { + reservationValidator.validateSearchDateRange(LocalDate.now(), LocalDate.now()) + } + } + + test("종료 날짜가 시작 날짜 이전이면 예외를 던진다.") { + shouldThrow { + reservationValidator.validateSearchDateRange(LocalDate.now(), LocalDate.now().minusDays(1)) + }.also { + it.errorCode shouldBe ReservationErrorCode.INVALID_SEARCH_DATE_RANGE + } + } + } + + context("validateIsAlreadyExists") { + test("동일한 날짜, 시간, 테마를 가지는 예약이 있으면 예외를 던진다.") { + every { + reservationRepository.exists(any>()) + } returns true + + shouldThrow { + reservationValidator.validateIsAlreadyExists( + LocalDate.now(), + TimeFixture.create(), + ThemeFixture.create() + ) + }.also { + it.errorCode shouldBe ReservationErrorCode.RESERVATION_DUPLICATED + } + } + } + + context("validateMemberAlreadyReserve") { + test("회원이 동일한 날짜, 시간, 테마인 예약(대기)를 이미 했다면 예외를 던진다.") { + every { + reservationRepository.exists(any>()) + } returns true + + shouldThrow { + reservationValidator.validateMemberAlreadyReserve(1L, 1L, LocalDate.now(), 1L) + }.also { + it.errorCode shouldBe ReservationErrorCode.ALREADY_RESERVE + } + } + } + + context("validateIsWaiting") { + test("예약 상태가 WAITING이 아니면 예외를 던진다.") { + ReservationStatus.confirmedStatus().forEach { status -> + shouldThrow { + val reservation = ReservationFixture.create(status = status) + reservationValidator.validateIsWaiting(reservation) + }.also { + it.errorCode shouldBe ReservationErrorCode.ALREADY_CONFIRMED + } + } + } + } + + context("validateCreateAuthority") { + test("관리자가 아니면 예외를 던진다.") { + shouldThrow { + reservationValidator.validateCreateAuthority(MemberFixture.user()) + }.also { + it.errorCode shouldBe ReservationErrorCode.NO_PERMISSION + } + } + } + + context("validateDeleteAuthority") { + test("입력된 회원이 관리자이면 종료한다.") { + shouldNotThrow { + reservationValidator.validateDeleteAuthority(mockk(), MemberFixture.admin()) + } + } + + test("입력된 회원이 관리자가 아니고, 예약한 회원과 다른 회원이면 예외를 던진다.") { + shouldThrow { + reservationValidator.validateDeleteAuthority( + ReservationFixture.create(member = MemberFixture.create(id = 1L)), + MemberFixture.create(id = 2L, role = Role.MEMBER) + ) + }.also { + it.errorCode shouldBe ReservationErrorCode.NOT_RESERVATION_OWNER + } + } + } + + context("validateAlreadyConfirmed") { + val reservationId = 1L + + test("입력된 ID의 예약과 동일한 날짜, 시간, 테마를 가지는 다른 확정 예약이 있으면 예외를 던진다.") { + every { + reservationRepository.isExistConfirmedReservation(reservationId) + } returns true + + shouldThrow { + reservationValidator.validateAlreadyConfirmed(reservationId) + }.also { + it.errorCode shouldBe ReservationErrorCode.CONFIRMED_RESERVATION_ALREADY_EXISTS + } + } + } +}) diff --git a/src/test/kotlin/roomescape/reservation/implement/ReservationWriterTest.kt b/src/test/kotlin/roomescape/reservation/implement/ReservationWriterTest.kt new file mode 100644 index 00000000..2fe07bce --- /dev/null +++ b/src/test/kotlin/roomescape/reservation/implement/ReservationWriterTest.kt @@ -0,0 +1,246 @@ +package roomescape.reservation.implement + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import roomescape.member.exception.MemberErrorCode +import roomescape.member.exception.MemberException +import roomescape.member.implement.MemberFinder +import roomescape.reservation.exception.ReservationErrorCode +import roomescape.reservation.exception.ReservationException +import roomescape.reservation.infrastructure.persistence.ReservationEntity +import roomescape.reservation.infrastructure.persistence.ReservationRepository +import roomescape.reservation.infrastructure.persistence.ReservationStatus +import roomescape.theme.exception.ThemeErrorCode +import roomescape.theme.exception.ThemeException +import roomescape.theme.implement.ThemeFinder +import roomescape.time.exception.TimeErrorCode +import roomescape.time.exception.TimeException +import roomescape.time.implement.TimeFinder +import roomescape.util.MemberFixture +import roomescape.util.ThemeFixture +import roomescape.util.TimeFixture +import roomescape.util.TsidFactory +import java.time.LocalDate +import java.time.LocalTime + +class ReservationWriterTest : FunSpec({ + + val reservationValidator: ReservationValidator = mockk() + val reservationRepository: ReservationRepository = mockk() + val memberFinder: MemberFinder = mockk() + val timeFinder: TimeFinder = mockk() + val themeFinder: ThemeFinder = mockk() + + val reservationWriter = ReservationWriter( + reservationValidator, reservationRepository, memberFinder, timeFinder, themeFinder, TsidFactory + ) + + context("create") { + val today = LocalDate.now() + val timeId = 1L + val themeId = 1L + val memberId = 1L + val status = ReservationStatus.CONFIRMED + val requesterId = 1L + + test("시간을 찾을 수 없으면 실패한다.") { + every { + timeFinder.findById(any()) + } throws TimeException(TimeErrorCode.TIME_NOT_FOUND) + + shouldThrow { + reservationWriter.create(today, timeId, themeId, memberId, status, requesterId) + }.also { + it.errorCode shouldBe TimeErrorCode.TIME_NOT_FOUND + } + } + + test("이전 날짜이면 실패한다.") { + every { + timeFinder.findById(timeId) + } returns TimeFixture.create(id = timeId, startAt = LocalTime.now().plusHours(1)) + + every { + reservationValidator.validateIsPast(any(), any()) + } throws ReservationException(ReservationErrorCode.PAST_REQUEST_DATETIME) + + shouldThrow { + reservationWriter.create(today.minusDays(1), timeId, themeId, memberId, status, requesterId) + }.also { + it.errorCode shouldBe ReservationErrorCode.PAST_REQUEST_DATETIME + } + } + + test("테마를 찾을 수 없으면 실패한다.") { + every { timeFinder.findById(timeId) } returns TimeFixture.create(id = timeId) + every { reservationValidator.validateIsPast(any(), any()) } returns Unit + + every { + themeFinder.findById(themeId) + } throws ThemeException(ThemeErrorCode.THEME_NOT_FOUND) + + shouldThrow { + reservationWriter.create(today.plusDays(1), timeId, themeId, memberId, status, requesterId) + }.also { + it.errorCode shouldBe ThemeErrorCode.THEME_NOT_FOUND + } + } + + test("회원을 찾을 수 없으면 실패한다.") { + every { timeFinder.findById(timeId) } returns TimeFixture.create(id = timeId) + every { reservationValidator.validateIsPast(any(), any()) } returns Unit + every { themeFinder.findById(themeId) } returns ThemeFixture.create(id = themeId) + + every { + memberFinder.findById(memberId) + } throws MemberException(MemberErrorCode.MEMBER_NOT_FOUND) + + shouldThrow { + reservationWriter.create(today.plusDays(1), timeId, themeId, memberId, status, requesterId) + }.also { + it.errorCode shouldBe MemberErrorCode.MEMBER_NOT_FOUND + } + } + + test("이미 예약이 있는 회원이 대기를 추가하면 실패한다.") { + every { timeFinder.findById(timeId) } returns TimeFixture.create(id = timeId) + every { reservationValidator.validateIsPast(any(), any()) } returns Unit + every { themeFinder.findById(themeId) } returns ThemeFixture.create(id = themeId) + every { memberFinder.findById(memberId) } returns MemberFixture.create(id = memberId) + + every { + reservationValidator.validateMemberAlreadyReserve(themeId, timeId, today, memberId) + } throws ReservationException(ReservationErrorCode.ALREADY_RESERVE) + + shouldThrow { + reservationWriter.create(today, timeId, themeId, memberId, status = ReservationStatus.WAITING, memberId) + }.also { + it.errorCode shouldBe ReservationErrorCode.ALREADY_RESERVE + } + } + + test("동일한 날짜, 시간, 테마인 예약이 이미 있으면 실패한다.") { + every { timeFinder.findById(timeId) } returns TimeFixture.create(id = timeId) + every { reservationValidator.validateIsPast(any(), any()) } returns Unit + every { themeFinder.findById(themeId) } returns ThemeFixture.create(id = themeId) + every { memberFinder.findById(memberId) } returns MemberFixture.create(id = memberId) + + every { + reservationValidator.validateIsAlreadyExists(today, any(), any()) + } throws ReservationException(ReservationErrorCode.RESERVATION_DUPLICATED) + + shouldThrow { + reservationWriter.create(today, timeId, themeId, memberId, status, memberId) + }.also { + it.errorCode shouldBe ReservationErrorCode.RESERVATION_DUPLICATED + } + } + + test("예약하려는 회원과 신청한 회원이 다를 때, 신청한 회원을 찾을 수 없으면 실패한다.") { + every { timeFinder.findById(timeId) } returns TimeFixture.create(id = timeId) + every { reservationValidator.validateIsPast(any(), any()) } returns Unit + every { themeFinder.findById(themeId) } returns ThemeFixture.create(id = themeId) + every { memberFinder.findById(memberId) } returns MemberFixture.create(id = memberId) + every { reservationValidator.validateIsAlreadyExists(today, any(), any()) } returns Unit + + every { + memberFinder.findById(memberId + 1) + } throws MemberException(MemberErrorCode.MEMBER_NOT_FOUND) + + shouldThrow { + reservationWriter.create(today, timeId, themeId, memberId = memberId, status, requesterId = (memberId + 1)) + }.also { + it.errorCode shouldBe MemberErrorCode.MEMBER_NOT_FOUND + } + } + + test("예약하려는 회원과 신청한 회원이 다를 때, 신청한 회원이 관리자가 아니면 실패한다.") { + every { timeFinder.findById(timeId) } returns TimeFixture.create(id = timeId) + every { reservationValidator.validateIsPast(any(), any()) } returns Unit + every { themeFinder.findById(themeId) } returns ThemeFixture.create(id = themeId) + every { memberFinder.findById(memberId) } returns MemberFixture.create(id = memberId) + every { reservationValidator.validateIsAlreadyExists(today, any(), any()) } returns Unit + + every { + memberFinder.findById(memberId + 1) + } returns MemberFixture.create(id = memberId + 1) + + every { + reservationValidator.validateCreateAuthority(any()) + } throws ReservationException(ReservationErrorCode.NO_PERMISSION) + + shouldThrow { + reservationWriter.create(today, timeId, themeId, memberId = memberId, status, requesterId = (memberId + 1)) + }.also { + it.errorCode shouldBe ReservationErrorCode.NO_PERMISSION + } + } + } + + context("deleteWaiting") { + val reservation: ReservationEntity = mockk() + val requesterId = 1L + + test("대기 상태가 아니면 실패한다.") { + every { + reservationValidator.validateIsWaiting(any()) + } throws ReservationException(ReservationErrorCode.ALREADY_CONFIRMED) + + shouldThrow { + reservationWriter.deleteWaiting(reservation, requesterId) + }.also { + it.errorCode shouldBe ReservationErrorCode.ALREADY_CONFIRMED + } + } + + test("삭제하려는 회원이 관리자가 아니고, 예약한 회원과 다르면 실패한다.") { + every { reservationValidator.validateIsWaiting(any()) } returns Unit + every { + reservationValidator.validateDeleteAuthority(any(), any()) + } throws ReservationException(ReservationErrorCode.NOT_RESERVATION_OWNER) + + shouldThrow { + reservationWriter.deleteWaiting(reservation, requesterId) + }.also { + it.errorCode shouldBe ReservationErrorCode.NOT_RESERVATION_OWNER + } + } + } + + context("deleteConfirm") { + val reservation: ReservationEntity = mockk() + val requesterId = 1L + + test("삭제하려는 회원이 관리자가 아니고, 예약한 회원과 다르면 실패한다.") { + every { reservationValidator.validateIsWaiting(any()) } returns Unit + every { + reservationValidator.validateDeleteAuthority(any(), any()) + } throws ReservationException(ReservationErrorCode.NOT_RESERVATION_OWNER) + + shouldThrow { + reservationWriter.deleteConfirmed(reservation, requesterId) + }.also { + it.errorCode shouldBe ReservationErrorCode.NOT_RESERVATION_OWNER + } + } + } + + context("confirm") { + val reservationId = 1L + + test("승인하려는 대기와 같은 날짜,시간,테마를 가진 확정 예약이 있으면 실패한다.") { + every { + reservationValidator.validateAlreadyConfirmed(reservationId) + } throws ReservationException(ReservationErrorCode.CONFIRMED_RESERVATION_ALREADY_EXISTS) + + shouldThrow { + reservationWriter.confirm(reservationId) + }.also { + it.errorCode shouldBe ReservationErrorCode.CONFIRMED_RESERVATION_ALREADY_EXISTS + } + } + } +}) diff --git a/src/test/kotlin/roomescape/util/Fixtures.kt b/src/test/kotlin/roomescape/util/Fixtures.kt index 526e3d16..8c74a3da 100644 --- a/src/test/kotlin/roomescape/util/Fixtures.kt +++ b/src/test/kotlin/roomescape/util/Fixtures.kt @@ -14,6 +14,7 @@ import roomescape.payment.web.PaymentCancelRequest import roomescape.payment.web.PaymentCancelResponse import roomescape.reservation.infrastructure.persistence.ReservationEntity import roomescape.reservation.infrastructure.persistence.ReservationStatus +import roomescape.reservation.web.AdminReservationCreateRequest import roomescape.reservation.web.ReservationCreateWithPaymentRequest import roomescape.reservation.web.WaitingCreateRequest import roomescape.theme.infrastructure.persistence.ThemeEntity @@ -22,7 +23,6 @@ import java.time.LocalDate import java.time.LocalTime import java.time.OffsetDateTime - val TsidFactory: TsidFactory = TsidFactory(0) object MemberFixture { @@ -103,6 +103,18 @@ object ReservationFixture { paymentType = paymentType ) + fun createAdminRequest( + date: LocalDate = LocalDate.now().plusWeeks(1), + themeId: Long = 1L, + timeId: Long = 1L, + memberId: Long = 1L + ): AdminReservationCreateRequest = AdminReservationCreateRequest( + date = date, + timeId = timeId, + themeId = themeId, + memberId = memberId + ) + fun createWaitingRequest( date: LocalDate = LocalDate.now().plusWeeks(1), themeId: Long = 1L,