[#28] 쿠버네티스 환경 배포 #29

Merged
pricelees merged 25 commits from infra/#28 into main 2025-08-04 05:59:38 +00:00
17 changed files with 259 additions and 289 deletions
Showing only changes of commit 612bbfbddc - Show all commits

View File

@ -15,10 +15,11 @@ import roomescape.member.infrastructure.persistence.MemberEntity
import roomescape.member.infrastructure.persistence.MemberRepository
import roomescape.util.JwtFixture
import roomescape.util.MemberFixture
import roomescape.util.TsidFactory
class AuthServiceTest : BehaviorSpec({
val memberRepository: MemberRepository = mockk()
val memberService: MemberService = MemberService(memberRepository)
val memberService = MemberService(TsidFactory, memberRepository)
val jwtHandler: JwtHandler = JwtFixture.create()
val authService = AuthService(memberService, jwtHandler)

View File

@ -14,13 +14,14 @@ import roomescape.payment.infrastructure.persistence.CanceledPaymentRepository
import roomescape.payment.infrastructure.persistence.PaymentRepository
import roomescape.payment.web.PaymentCancelRequest
import roomescape.util.PaymentFixture
import roomescape.util.TsidFactory
import java.time.OffsetDateTime
class PaymentServiceTest : FunSpec({
val paymentRepository: PaymentRepository = mockk()
val canceledPaymentRepository: CanceledPaymentRepository = mockk()
val paymentService = PaymentService(paymentRepository, canceledPaymentRepository)
val paymentService = PaymentService(TsidFactory, paymentRepository, canceledPaymentRepository)
context("createCanceledPaymentByReservationId") {
val reservationId = 1L

View File

@ -1,11 +1,13 @@
package roomescape.payment.infrastructure.client
import com.ninjasquad.springmockk.MockkBean
import io.kotest.assertions.assertSoftly
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.client.RestClientTest
import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext
import org.springframework.http.HttpMethod
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
@ -20,6 +22,7 @@ import roomescape.payment.web.PaymentCancelRequest
import roomescape.payment.web.PaymentCancelResponse
@RestClientTest(TossPaymentClient::class)
@MockkBean(JpaMetamodelMappingContext::class)
class TossPaymentClientTest(
@Autowired val client: TossPaymentClient,
@Autowired val mockServer: MockRestServiceServer

View File

@ -5,10 +5,12 @@ import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import roomescape.common.config.next
import roomescape.util.PaymentFixture
import roomescape.util.TsidFactory
import java.util.*
@DataJpaTest
@DataJpaTest(showSql = false)
class CanceledPaymentRepositoryTest(
@Autowired val canceledPaymentRepository: CanceledPaymentRepository,
) : FunSpec() {
@ -16,7 +18,7 @@ class CanceledPaymentRepositoryTest(
context("paymentKey로 CanceledPaymentEntity 조회") {
val paymentKey = "test-payment-key"
beforeTest {
PaymentFixture.createCanceled(paymentKey = paymentKey)
PaymentFixture.createCanceled(id = TsidFactory.next(), paymentKey = paymentKey)
.also { canceledPaymentRepository.save(it) }
}

View File

@ -6,11 +6,13 @@ import io.kotest.matchers.shouldBe
import jakarta.persistence.EntityManager
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import roomescape.common.config.next
import roomescape.reservation.infrastructure.persistence.ReservationEntity
import roomescape.util.PaymentFixture
import roomescape.util.ReservationFixture
import roomescape.util.TsidFactory
@DataJpaTest
@DataJpaTest(showSql = false)
class PaymentRepositoryTest(
@Autowired val paymentRepository: PaymentRepository,
@Autowired val entityManager: EntityManager
@ -91,7 +93,9 @@ class PaymentRepositoryTest(
}
private fun setupReservation(): ReservationEntity {
return ReservationFixture.create().also {
return ReservationFixture.create(
id = TsidFactory.next()
).also {
entityManager.persist(it.member)
entityManager.persist(it.theme)
entityManager.persist(it.time)

View File

@ -16,6 +16,7 @@ import roomescape.theme.business.ThemeService
import roomescape.time.business.TimeService
import roomescape.util.MemberFixture
import roomescape.util.ReservationFixture
import roomescape.util.TsidFactory
import roomescape.util.TimeFixture
import java.time.LocalDate
import java.time.LocalTime
@ -27,6 +28,7 @@ class ReservationServiceTest : FunSpec({
val memberService: MemberService = mockk()
val themeService: ThemeService = mockk()
val reservationService = ReservationService(
TsidFactory,
reservationRepository,
timeService,
memberService,

View File

@ -15,7 +15,7 @@ import roomescape.util.ReservationFixture
import roomescape.util.ThemeFixture
import roomescape.util.TimeFixture
@DataJpaTest
@DataJpaTest(showSql = false)
class ReservationRepositoryTest(
val entityManager: EntityManager,
val reservationRepository: ReservationRepository,

View File

@ -15,7 +15,7 @@ import roomescape.util.ThemeFixture
import roomescape.util.TimeFixture
import java.time.LocalDate
@DataJpaTest
@DataJpaTest(showSql = false)
class ReservationSearchSpecificationTest(
val entityManager: EntityManager,
val reservationRepository: ReservationRepository

View File

@ -39,7 +39,7 @@ import java.time.LocalTime
class ReservationControllerTest(
@LocalServerPort val port: Int,
val entityManager: EntityManager,
val transactionTemplate: TransactionTemplate
val transactionTemplate: TransactionTemplate,
) : FunSpec({
extension(DatabaseCleanerExtension(mode = CleanerMode.AFTER_EACH_TEST))
}) {
@ -55,24 +55,34 @@ class ReservationControllerTest(
@MockkBean
lateinit var jwtHandler: JwtHandler
lateinit var testDataHelper: TestDataHelper
fun login(member: MemberEntity) {
every { jwtHandler.getMemberIdFromToken(any()) } returns member.id!!
every { memberService.findById(member.id!!) } returns member
every { memberIdResolver.resolveArgument(any(), any(), any(), any()) } returns member.id!!
}
init {
beforeSpec {
testDataHelper = TestDataHelper(entityManager, transactionTemplate)
}
context("POST /reservations") {
lateinit var member: MemberEntity
beforeTest {
member = login(MemberFixture.create(role = Role.MEMBER))
val member = testDataHelper.createMember(role = Role.MEMBER)
login(member)
}
test("정상 응답") {
val reservationRequest = createRequest()
val reservationRequest = testDataHelper.createReservationRequest()
val paymentApproveResponse = PaymentFixture.createApproveResponse().copy(
paymentKey = reservationRequest.paymentKey,
orderId = reservationRequest.orderId,
totalAmount = reservationRequest.amount,
)
every {
paymentClient.confirm(any())
} returns paymentApproveResponse
every { paymentClient.confirm(any()) } returns paymentApproveResponse
Given {
port(port)
@ -88,12 +98,10 @@ class ReservationControllerTest(
}
test("결제 과정에서 발생하는 에러는 그대로 응답") {
val reservationRequest = createRequest()
val reservationRequest = testDataHelper.createReservationRequest()
val paymentException = PaymentException(PaymentErrorCode.PAYMENT_PROVIDER_ERROR)
every {
paymentClient.confirm(any())
} throws paymentException
every { paymentClient.confirm(any()) } throws paymentException
Given {
port(port)
@ -108,24 +116,20 @@ class ReservationControllerTest(
}
test("결제 완료 후 예약 / 결제 정보 저장 과정에서 에러 발생시 결제 취소 후 에러 응답을 받는다.") {
val reservationRequest = createRequest()
val reservationRequest = testDataHelper.createReservationRequest()
val paymentApproveResponse = PaymentFixture.createApproveResponse().copy(
paymentKey = reservationRequest.paymentKey,
orderId = reservationRequest.orderId,
totalAmount = reservationRequest.amount,
)
every {
paymentClient.confirm(any())
} returns paymentApproveResponse
every { paymentClient.confirm(any()) } returns paymentApproveResponse
// 예약 저장 과정에서 테마가 없는 예외
val invalidRequest = reservationRequest.copy(themeId = reservationRequest.themeId + 1)
val expectedException = ThemeErrorCode.THEME_NOT_FOUND
every {
paymentClient.cancel(any())
} returns PaymentFixture.createCancelResponse()
every { paymentClient.cancel(any()) } returns PaymentFixture.createCancelResponse()
val canceledPaymentSizeBeforeApiCall: Long = entityManager.createQuery(
"SELECT COUNT(c) FROM CanceledPaymentEntity c",
@ -153,13 +157,13 @@ class ReservationControllerTest(
}
context("GET /reservations") {
lateinit var reservations: MutableMap<MemberEntity, MutableList<ReservationEntity>>
lateinit var reservations: Map<MemberEntity, List<ReservationEntity>>
beforeTest {
reservations = createDummyReservations()
reservations = testDataHelper.createDummyReservations()
}
test("관리자이면 정상 응답") {
login(MemberFixture.create(role = Role.ADMIN))
login(testDataHelper.createMember(role = Role.ADMIN))
Given {
port(port)
contentType(MediaType.APPLICATION_JSON_VALUE)
@ -173,13 +177,14 @@ class ReservationControllerTest(
}
context("GET /reservations-mine") {
lateinit var reservations: MutableMap<MemberEntity, MutableList<ReservationEntity>>
lateinit var reservations: Map<MemberEntity, List<ReservationEntity>>
beforeTest {
reservations = createDummyReservations()
reservations = testDataHelper.createDummyReservations()
}
test("로그인한 회원이 자신의 예약 목록을 조회한다.") {
val member: MemberEntity = login(reservations.keys.first())
val member = reservations.keys.first()
login(member)
val expectedReservations: Int = reservations[member]?.size ?: 0
Given {
@ -195,9 +200,9 @@ class ReservationControllerTest(
}
context("GET /reservations/search") {
lateinit var reservations: MutableMap<MemberEntity, MutableList<ReservationEntity>>
lateinit var reservations: Map<MemberEntity, List<ReservationEntity>>
beforeTest {
reservations = createDummyReservations()
reservations = testDataHelper.createDummyReservations()
}
test("관리자만 검색할 수 있다.") {
@ -216,7 +221,7 @@ class ReservationControllerTest(
}
test("파라미터를 지정하지 않으면 전체 목록 응답") {
login(MemberFixture.create(role = Role.ADMIN))
login(testDataHelper.createMember(role = Role.ADMIN))
Given {
port(port)
@ -230,7 +235,7 @@ class ReservationControllerTest(
}
test("시작 날짜가 종료 날짜 이전이면 예외 응답") {
login(MemberFixture.create(role = Role.ADMIN))
login(testDataHelper.createMember(role = Role.ADMIN))
val startDate = LocalDate.now().plusDays(1)
val endDate = LocalDate.now()
@ -250,8 +255,8 @@ class ReservationControllerTest(
}
test("동일한 회원의 모든 예약 응답") {
login(MemberFixture.create(role = Role.ADMIN))
val member: MemberEntity = reservations.keys.first()
login(testDataHelper.createMember(role = Role.ADMIN))
val member = reservations.keys.first()
Given {
port(port)
@ -266,7 +271,7 @@ class ReservationControllerTest(
}
test("동일한 테마의 모든 예약 응답") {
login(MemberFixture.create(role = Role.ADMIN))
login(testDataHelper.createMember(role = Role.ADMIN))
val themes = reservations.values.flatten().map { it.theme }
val requestThemeId: Long = themes.first().id!!
@ -278,12 +283,12 @@ class ReservationControllerTest(
get("/reservations/search")
}.Then {
statusCode(200)
body("data.reservations.size()", equalTo(themes.filter { it.id == requestThemeId }.size))
body("data.reservations.size()", equalTo(themes.count { it.id == requestThemeId }))
}
}
test("시작 날짜와 종료 날짜 사이의 예약 응답") {
login(MemberFixture.create(role = Role.ADMIN))
login(testDataHelper.createMember(role = Role.ADMIN))
val dateFrom: LocalDate = reservations.values.flatten().minOf { it.date }
val dateTo: LocalDate = reservations.values.flatten().maxOf { it.date }
@ -302,14 +307,14 @@ class ReservationControllerTest(
}
context("DELETE /reservations/{id}") {
lateinit var reservations: MutableMap<MemberEntity, MutableList<ReservationEntity>>
lateinit var reservations: Map<MemberEntity, List<ReservationEntity>>
beforeTest {
reservations = createDummyReservations()
reservations = testDataHelper.createDummyReservations()
}
test("관리자만 예약을 삭제할 수 있다.") {
login(MemberFixture.create(role = Role.MEMBER))
val reservation: ReservationEntity = reservations.values.flatten().first()
login(testDataHelper.createMember(role = Role.MEMBER))
val reservation = reservations.values.flatten().first()
val expectedError = AuthErrorCode.ACCESS_DENIED
Given {
@ -323,18 +328,12 @@ class ReservationControllerTest(
}
test("결제되지 않은 예약은 바로 제거") {
login(MemberFixture.create(role = Role.ADMIN))
val reservationId: Long = reservations.values.flatten().first().id!!
login(testDataHelper.createMember(role = Role.ADMIN))
val reservationId = reservations.values.flatten().first().id!!
transactionTemplate.execute {
val reservation: ReservationEntity = entityManager.find(
ReservationEntity::class.java,
reservationId
)
transactionTemplate.executeWithoutResult {
val reservation = entityManager.find(ReservationEntity::class.java, reservationId)
reservation.status = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
entityManager.persist(reservation)
entityManager.flush()
entityManager.clear()
}
Given {
@ -345,32 +344,18 @@ class ReservationControllerTest(
statusCode(HttpStatus.NO_CONTENT.value())
}
// 예약이 삭제되었는지 확인
transactionTemplate.executeWithoutResult {
val deletedReservation = entityManager.find(
ReservationEntity::class.java,
reservationId
)
deletedReservation shouldBe null
val deletedReservation = transactionTemplate.execute {
entityManager.find(ReservationEntity::class.java, reservationId)
}
deletedReservation shouldBe null
}
test("결제된 예약은 취소 후 제거") {
login(MemberFixture.create(role = Role.ADMIN))
val reservation: ReservationEntity = reservations.values.flatten().first()
lateinit var payment: PaymentEntity
login(testDataHelper.createMember(role = Role.ADMIN))
val reservation = reservations.values.flatten().first { it.status == ReservationStatus.CONFIRMED }
testDataHelper.createPayment(reservation)
transactionTemplate.execute {
payment = PaymentFixture.create(reservation = reservation).also {
entityManager.persist(it)
entityManager.flush()
entityManager.clear()
}
}
every {
paymentClient.cancel(any())
} returns PaymentFixture.createCancelResponse()
every { paymentClient.cancel(any()) } returns PaymentFixture.createCancelResponse()
val canceledPaymentSizeBeforeApiCall: Long = entityManager.createQuery(
"SELECT COUNT(c) FROM CanceledPaymentEntity c",
@ -396,15 +381,17 @@ class ReservationControllerTest(
context("POST /reservations/admin") {
test("관리자가 예약을 추가하면 결제 대기 상태로 예약 생성") {
val member = login(MemberFixture.create(role = Role.ADMIN))
val adminRequest: AdminReservationCreateRequest = createRequest().let {
AdminReservationCreateRequest(
date = it.date,
themeId = it.themeId,
timeId = it.timeId,
memberId = member.id!!,
)
}
val admin = testDataHelper.createMember(role = Role.ADMIN)
login(admin)
val theme = testDataHelper.createTheme()
val time = testDataHelper.createTime()
val adminRequest = AdminReservationCreateRequest(
date = LocalDate.now().plusDays(1),
themeId = theme.id!!,
timeId = time.id!!,
memberId = admin.id!!,
)
Given {
port(port)
@ -420,13 +407,13 @@ class ReservationControllerTest(
}
context("GET /reservations/waiting") {
lateinit var reservations: MutableMap<MemberEntity, MutableList<ReservationEntity>>
lateinit var reservations: Map<MemberEntity, List<ReservationEntity>>
beforeTest {
reservations = createDummyReservations()
reservations = testDataHelper.createDummyReservations(reservationCount = 5)
}
test("관리자가 아니면 조회할 수 없다.") {
login(MemberFixture.create(role = Role.MEMBER))
login(testDataHelper.createMember(role = Role.MEMBER))
val expectedError = AuthErrorCode.ACCESS_DENIED
Given {
@ -441,7 +428,7 @@ class ReservationControllerTest(
}
test("대기 중인 예약 목록을 조회한다.") {
login(MemberFixture.create(role = Role.ADMIN))
login(testDataHelper.createMember(role = Role.ADMIN))
val expected = reservations.values.flatten()
.count { it.status == ReservationStatus.WAITING }
@ -459,14 +446,16 @@ class ReservationControllerTest(
context("POST /reservations/waiting") {
test("회원이 대기 예약을 추가한다.") {
val member = login(MemberFixture.create(role = Role.MEMBER))
val waitingCreateRequest: WaitingCreateRequest = createRequest().let {
WaitingCreateRequest(
date = it.date,
themeId = it.themeId,
timeId = it.timeId
)
}
val member = testDataHelper.createMember(role = Role.MEMBER)
login(member)
val theme = testDataHelper.createTheme()
val time = testDataHelper.createTime()
val waitingCreateRequest = WaitingCreateRequest(
date = LocalDate.now().plusDays(1),
themeId = theme.id!!,
timeId = time.id!!
)
Given {
port(port)
@ -476,33 +465,30 @@ class ReservationControllerTest(
post("/reservations/waiting")
}.Then {
statusCode(201)
body("data.member.id", equalTo(member.id!!.toInt()))
body("data.member.id", equalTo(member.id!!))
body("data.status", equalTo(ReservationStatus.WAITING.name))
}
}
test("이미 예약된 시간, 테마로 대기 예약 요청 시 예외 응답") {
val member = login(MemberFixture.create(role = Role.MEMBER))
val reservationRequest = createRequest()
val member = testDataHelper.createMember(role = Role.MEMBER)
login(member)
val theme = testDataHelper.createTheme()
val time = testDataHelper.createTime()
val date = LocalDate.now().plusDays(1)
transactionTemplate.executeWithoutResult {
val reservation = ReservationFixture.create(
date = reservationRequest.date,
theme = entityManager.find(ThemeEntity::class.java, reservationRequest.themeId),
time = entityManager.find(TimeEntity::class.java, reservationRequest.timeId),
member = member,
status = ReservationStatus.WAITING
)
entityManager.persist(reservation)
entityManager.flush()
entityManager.clear()
}
testDataHelper.createReservation(
date = date,
theme = theme,
time = time,
member = member,
status = ReservationStatus.CONFIRMED
)
// 이미 예약된 시간, 테마로 대기 예약 요청
val waitingCreateRequest = WaitingCreateRequest(
date = reservationRequest.date,
themeId = reservationRequest.themeId,
timeId = reservationRequest.timeId
date = date,
themeId = theme.id!!,
timeId = time.id!!
)
val expectedError = ReservationErrorCode.ALREADY_RESERVE
@ -520,14 +506,10 @@ class ReservationControllerTest(
}
context("DELETE /reservations/waiting/{id}") {
lateinit var reservations: MutableMap<MemberEntity, MutableList<ReservationEntity>>
beforeTest {
reservations = createDummyReservations()
}
test("대기 중인 예약을 취소한다.") {
val member = login(MemberFixture.create(role = Role.MEMBER))
val waiting: ReservationEntity = createSingleReservation(
val member = testDataHelper.createMember(role = Role.MEMBER)
login(member)
val waiting = testDataHelper.createReservation(
member = member,
status = ReservationStatus.WAITING
)
@ -540,17 +522,16 @@ class ReservationControllerTest(
statusCode(HttpStatus.NO_CONTENT.value())
}
transactionTemplate.executeWithoutResult { _ ->
entityManager.find(
ReservationEntity::class.java,
waiting.id
) shouldBe null
val deleted = transactionTemplate.execute {
entityManager.find(ReservationEntity::class.java, waiting.id)
}
deleted shouldBe null
}
test("이미 확정된 예약을 삭제하면 예외 응답") {
val member = login(MemberFixture.create(role = Role.MEMBER))
val reservation: ReservationEntity = createSingleReservation(
val member = testDataHelper.createMember(role = Role.MEMBER)
login(member)
val reservation = testDataHelper.createReservation(
member = member,
status = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
)
@ -559,7 +540,7 @@ class ReservationControllerTest(
Given {
port(port)
}.When {
delete("/reservations/waiting/{id}", reservation.id)
delete("/reservations/waiting/${reservation.id}")
}.Then {
statusCode(expectedError.httpStatus.value())
body("code", equalTo(expectedError.errorCode))
@ -569,7 +550,7 @@ class ReservationControllerTest(
context("POST /reservations/waiting/{id}/confirm") {
test("관리자만 승인할 수 있다.") {
login(MemberFixture.create(role = Role.MEMBER))
login(testDataHelper.createMember(role = Role.MEMBER))
val expectedError = AuthErrorCode.ACCESS_DENIED
Given {
port(port)
@ -582,9 +563,8 @@ class ReservationControllerTest(
}
test("대기 예약을 승인하면 결제 대기 상태로 변경") {
val member = login(MemberFixture.create(role = Role.ADMIN))
val reservation = createSingleReservation(
member = member,
login(testDataHelper.createMember(role = Role.ADMIN))
val reservation = testDataHelper.createReservation(
status = ReservationStatus.WAITING
)
@ -596,39 +576,28 @@ class ReservationControllerTest(
statusCode(200)
}
transactionTemplate.executeWithoutResult { _ ->
entityManager.find(
ReservationEntity::class.java,
reservation.id
)?.also {
it.status shouldBe ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
} ?: throw AssertionError("Reservation not found")
val updatedReservation = transactionTemplate.execute {
entityManager.find(ReservationEntity::class.java, reservation.id)
}
updatedReservation?.status shouldBe ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
}
test("다른 확정된 예약을 승인하면 예외 응답") {
val admin = login(MemberFixture.create(role = Role.ADMIN))
val alreadyReserved = createSingleReservation(
val admin = testDataHelper.createMember(role = Role.ADMIN)
login(admin)
val alreadyReserved = testDataHelper.createReservation(
member = admin,
status = ReservationStatus.CONFIRMED
)
val member = MemberFixture.create(account = "account", role = Role.MEMBER).also { it ->
transactionTemplate.executeWithoutResult { _ ->
entityManager.persist(it)
}
}
val waiting = ReservationFixture.create(
val member = testDataHelper.createMember(role = Role.MEMBER)
val waiting = testDataHelper.createReservation(
date = alreadyReserved.date,
time = alreadyReserved.time,
theme = alreadyReserved.theme,
member = member,
status = ReservationStatus.WAITING
).also {
transactionTemplate.executeWithoutResult { _ ->
entityManager.persist(it)
}
}
)
val expectedError = ReservationErrorCode.CONFIRMED_RESERVATION_ALREADY_EXISTS
Given {
@ -636,7 +605,6 @@ class ReservationControllerTest(
}.When {
post("/reservations/waiting/${waiting.id!!}/confirm")
}.Then {
log().all()
statusCode(expectedError.httpStatus.value())
body("code", equalTo(expectedError.errorCode))
}
@ -645,7 +613,7 @@ class ReservationControllerTest(
context("POST /reservations/waiting/{id}/reject") {
test("관리자만 거절할 수 있다.") {
login(MemberFixture.create(role = Role.MEMBER))
login(testDataHelper.createMember(role = Role.MEMBER))
val expectedError = AuthErrorCode.ACCESS_DENIED
Given {
@ -659,9 +627,8 @@ class ReservationControllerTest(
}
test("거절된 예약은 삭제된다.") {
val member = login(MemberFixture.create(role = Role.ADMIN))
val reservation = createSingleReservation(
member = member,
login(testDataHelper.createMember(role = Role.ADMIN))
val reservation = testDataHelper.createReservation(
status = ReservationStatus.WAITING
)
@ -673,125 +640,91 @@ class ReservationControllerTest(
statusCode(204)
}
transactionTemplate.executeWithoutResult { _ ->
entityManager.find(
ReservationEntity::class.java,
reservation.id
) shouldBe null
val rejected = transactionTemplate.execute {
entityManager.find(ReservationEntity::class.java, reservation.id)
}
rejected shouldBe null
}
}
}
fun createSingleReservation(
date: LocalDate = LocalDate.now().plusDays(1),
time: LocalTime = LocalTime.now(),
themeName: String = "Default Theme",
member: MemberEntity = MemberFixture.create(role = Role.MEMBER),
status: ReservationStatus = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
): ReservationEntity {
return ReservationFixture.create(
date = date,
theme = ThemeFixture.create(name = themeName),
time = TimeFixture.create(startAt = time),
member = member,
status = status
).also { it ->
transactionTemplate.execute { _ ->
if (member.id == null) {
entityManager.persist(member)
}
entityManager.persist(it.time)
entityManager.persist(it.theme)
entityManager.persist(it)
entityManager.flush()
entityManager.clear()
}
}
}
fun createDummyReservations(): MutableMap<MemberEntity, MutableList<ReservationEntity>> {
val reservations: MutableMap<MemberEntity, MutableList<ReservationEntity>> = mutableMapOf()
val members: List<MemberEntity> = listOf(
MemberFixture.create(role = Role.MEMBER),
MemberFixture.create(role = Role.MEMBER)
)
transactionTemplate.executeWithoutResult {
members.forEach { member ->
entityManager.persist(member)
}
entityManager.flush()
entityManager.clear()
}
transactionTemplate.executeWithoutResult {
repeat(10) { index ->
val theme = ThemeFixture.create(name = "theme$index")
val time = TimeFixture.create(startAt = LocalTime.now().plusMinutes(index.toLong()))
entityManager.persist(theme)
entityManager.persist(time)
val reservation = ReservationFixture.create(
date = LocalDate.now().plusDays(index.toLong()),
theme = theme,
time = time,
member = members[index % members.size],
status = ReservationStatus.CONFIRMED
)
entityManager.persist(reservation)
reservations.getOrPut(reservation.member) { mutableListOf() }.add(reservation)
}
entityManager.flush()
entityManager.clear()
}
return reservations
}
fun createRequest(
theme: ThemeEntity = ThemeFixture.create(),
time: TimeEntity = TimeFixture.create(),
): ReservationCreateWithPaymentRequest {
lateinit var reservationCreateWithPaymentRequest: ReservationCreateWithPaymentRequest
transactionTemplate.executeWithoutResult {
entityManager.persist(theme)
entityManager.persist(time)
reservationCreateWithPaymentRequest = ReservationFixture.createRequest(
themeId = theme.id!!,
timeId = time.id!!,
)
entityManager.flush()
entityManager.clear()
}
return reservationCreateWithPaymentRequest
}
fun login(member: MemberEntity): MemberEntity {
if (member.id == null) {
transactionTemplate.executeWithoutResult {
entityManager.persist(member)
entityManager.flush()
entityManager.clear()
}
}
every {
jwtHandler.getMemberIdFromToken(any())
} returns member.id!!
every {
memberService.findById(member.id!!)
} returns member
every {
memberIdResolver.resolveArgument(any(), any(), any(), any())
} returns member.id!!
return member
}
}
class TestDataHelper(
private val entityManager: EntityManager,
private val transactionTemplate: TransactionTemplate,
) {
private var memberSequence = 0L
private var themeSequence = 0L
private var timeSequence = 0L
fun createMember(
role: Role = Role.MEMBER,
account: String = "member${++memberSequence}@test.com",
): MemberEntity {
val member = MemberFixture.create(role = role, account = account)
return persist(member)
}
fun createTheme(name: String = "theme-${++themeSequence}"): ThemeEntity {
val theme = ThemeFixture.create(name = name)
return persist(theme)
}
fun createTime(startAt: LocalTime = LocalTime.of(10, 0).plusMinutes(++timeSequence * 10)): TimeEntity {
val time = TimeFixture.create(startAt = startAt)
return persist(time)
}
fun createReservation(
date: LocalDate = LocalDate.now().plusDays(1),
theme: ThemeEntity = createTheme(),
time: TimeEntity = createTime(),
member: MemberEntity = createMember(),
status: ReservationStatus = ReservationStatus.CONFIRMED,
): ReservationEntity {
val reservation = ReservationFixture.create(
date = date,
theme = theme,
time = time,
member = member,
status = status
)
return persist(reservation)
}
fun createPayment(reservation: ReservationEntity): PaymentEntity {
val payment = PaymentFixture.create(reservation = reservation)
return persist(payment)
}
fun createReservationRequest(
theme: ThemeEntity = createTheme(),
time: TimeEntity = createTime(),
): ReservationCreateWithPaymentRequest {
return ReservationFixture.createRequest(
themeId = theme.id!!,
timeId = time.id!!,
)
}
fun createDummyReservations(
memberCount: Int = 2,
reservationCount: Int = 10,
): Map<MemberEntity, List<ReservationEntity>> {
val members = (1..memberCount).map { createMember(role = Role.MEMBER) }
val reservations = (1..reservationCount).map { index ->
createReservation(
member = members[index % memberCount],
status = ReservationStatus.CONFIRMED
)
}
return reservations.groupBy { it.member }
}
private fun <T> persist(entity: T): T {
transactionTemplate.executeWithoutResult {
entityManager.persist(entity)
}
return entity
}
}

View File

@ -13,12 +13,13 @@ import roomescape.theme.infrastructure.persistence.ThemeEntity
import roomescape.theme.infrastructure.persistence.ThemeRepository
import roomescape.theme.web.ThemeCreateRequest
import roomescape.theme.web.ThemeRetrieveResponse
import roomescape.util.TsidFactory
import roomescape.util.ThemeFixture
class ThemeServiceTest : FunSpec({
val themeRepository: ThemeRepository = mockk()
val themeService = ThemeService(themeRepository)
val themeService = ThemeService(TsidFactory, themeRepository)
context("findThemeById") {
val themeId = 1L

View File

@ -8,7 +8,7 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import roomescape.theme.util.TestThemeCreateUtil
import java.time.LocalDate
@DataJpaTest
@DataJpaTest(showSql = false)
class ThemeRepositoryTest(
val themeRepository: ThemeRepository,
val entityManager: EntityManager

View File

@ -10,6 +10,7 @@ import io.mockk.just
import io.mockk.runs
import org.hamcrest.Matchers.equalTo
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc
import roomescape.auth.exception.AuthErrorCode

View File

@ -14,6 +14,7 @@ import roomescape.time.exception.TimeErrorCode
import roomescape.time.exception.TimeException
import roomescape.time.infrastructure.persistence.TimeRepository
import roomescape.time.web.TimeCreateRequest
import roomescape.util.TsidFactory
import roomescape.util.TimeFixture
import java.time.LocalTime
@ -22,6 +23,7 @@ class TimeServiceTest : FunSpec({
val reservationRepository: ReservationRepository = mockk()
val timeService = TimeService(
tsidFactory = TsidFactory,
timeRepository = timeRepository,
reservationRepository = reservationRepository
)

View File

@ -7,7 +7,7 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import roomescape.util.TimeFixture
import java.time.LocalTime
@DataJpaTest
@DataJpaTest(showSql = false)
class TimeRepositoryTest(
val entityManager: EntityManager,
val timeRepository: TimeRepository,

View File

@ -9,6 +9,7 @@ import io.mockk.every
import org.hamcrest.Matchers.equalTo
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.context.annotation.Import
import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext
import org.springframework.data.repository.findByIdOrNull
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc
@ -27,7 +28,6 @@ import java.time.LocalDate
import java.time.LocalTime
@WebMvcTest(TimeController::class)
@Import(JacksonConfig::class)
class TimeControllerTest(
val mockMvc: MockMvc,
) : RoomescapeApiTest() {

View File

@ -1,7 +1,9 @@
package roomescape.util
import com.github.f4b6a3.tsid.TsidFactory
import roomescape.auth.infrastructure.jwt.JwtHandler
import roomescape.auth.web.LoginRequest
import roomescape.common.config.next
import roomescape.member.infrastructure.persistence.MemberEntity
import roomescape.member.infrastructure.persistence.Role
import roomescape.payment.infrastructure.client.PaymentApproveRequest
@ -20,11 +22,14 @@ import java.time.LocalDate
import java.time.LocalTime
import java.time.OffsetDateTime
val TsidFactory: TsidFactory = TsidFactory(0)
object MemberFixture {
const val NOT_LOGGED_IN_USERID: Long = 0
fun create(
id: Long? = null,
id: Long? = TsidFactory.next(),
name: String = "sangdol",
account: String = "default",
password: String = "password",
@ -56,14 +61,14 @@ object MemberFixture {
object TimeFixture {
fun create(
id: Long? = null,
id: Long? = TsidFactory.next(),
startAt: LocalTime = LocalTime.now().plusHours(1),
): TimeEntity = TimeEntity(id, startAt)
}
object ThemeFixture {
fun create(
id: Long? = null,
id: Long? = TsidFactory.next(),
name: String = "Default Theme",
description: String = "Default Description",
thumbnail: String = "https://example.com/default-thumbnail.jpg"
@ -72,7 +77,7 @@ object ThemeFixture {
object ReservationFixture {
fun create(
id: Long? = null,
id: Long? = TsidFactory.next(),
date: LocalDate = LocalDate.now().plusWeeks(1),
theme: ThemeEntity = ThemeFixture.create(),
time: TimeEntity = TimeFixture.create(),
@ -125,14 +130,14 @@ object PaymentFixture {
const val AMOUNT: Long = 10000L
fun create(
id: Long? = null,
id: Long? = TsidFactory.next(),
orderId: String = ORDER_ID,
paymentKey: String = PAYMENT_KEY,
totalAmount: Long = AMOUNT,
reservation: ReservationEntity = ReservationFixture.create(id = 1L),
approvedAt: OffsetDateTime = OffsetDateTime.now()
): PaymentEntity = PaymentEntity(
id = id,
_id = id,
orderId = orderId,
paymentKey = paymentKey,
totalAmount = totalAmount,
@ -141,14 +146,14 @@ object PaymentFixture {
)
fun createCanceled(
id: Long? = null,
id: Long? = TsidFactory.next(),
paymentKey: String = PAYMENT_KEY,
cancelReason: String = "Test Cancel",
cancelAmount: Long = AMOUNT,
approvedAt: OffsetDateTime = OffsetDateTime.now(),
canceledAt: OffsetDateTime = approvedAt.plusHours(1)
): CanceledPaymentEntity = CanceledPaymentEntity(
id = id,
_id = id,
paymentKey = paymentKey,
cancelReason = cancelReason,
cancelAmount = cancelAmount,

View File

@ -1,10 +1,16 @@
package roomescape.util
import com.fasterxml.jackson.databind.ObjectMapper
import com.github.f4b6a3.tsid.TsidFactory
import com.ninjasquad.springmockk.MockkBean
import com.ninjasquad.springmockk.SpykBean
import io.kotest.core.spec.style.BehaviorSpec
import io.mockk.every
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Import
import org.springframework.context.annotation.Primary
import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext
import org.springframework.data.repository.findByIdOrNull
import org.springframework.http.HttpHeaders
import org.springframework.http.MediaType
@ -21,6 +27,8 @@ import roomescape.member.infrastructure.persistence.MemberEntity
import roomescape.member.infrastructure.persistence.MemberRepository
import roomescape.util.MemberFixture.NOT_LOGGED_IN_USERID
@Import(TestConfig::class, JacksonConfig::class)
@MockkBean(JpaMetamodelMappingContext::class)
abstract class RoomescapeApiTest : BehaviorSpec() {
@SpykBean
@ -128,3 +136,10 @@ abstract class RoomescapeApiTest : BehaviorSpec() {
""".trimIndent()
)
}
@TestConfiguration
class TestConfig {
@Bean
@Primary
fun tsidFactory(): TsidFactory = TsidFactory
}