307 lines
11 KiB
Kotlin

package roomescape.time.web
import com.ninjasquad.springmockk.MockkBean
import com.ninjasquad.springmockk.SpykBean
import io.kotest.assertions.assertSoftly
import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.shouldBe
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.repository.findByIdOrNull
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc
import roomescape.auth.exception.AuthErrorCode
import roomescape.common.config.JacksonConfig
import roomescape.reservation.infrastructure.persistence.ReservationRepository
import roomescape.time.business.TimeService
import roomescape.time.exception.TimeErrorCode
import roomescape.time.infrastructure.persistence.TimeEntity
import roomescape.time.infrastructure.persistence.TimeRepository
import roomescape.util.ReservationFixture
import roomescape.util.RoomescapeApiTest
import roomescape.util.ThemeFixture
import roomescape.util.TimeFixture
import java.time.LocalDate
import java.time.LocalTime
@WebMvcTest(TimeController::class)
@Import(JacksonConfig::class)
class TimeControllerTest(
val mockMvc: MockMvc,
) : RoomescapeApiTest() {
@SpykBean
private lateinit var timeService: TimeService
@MockkBean
private lateinit var timeRepository: TimeRepository
@MockkBean
private lateinit var reservationRepository: ReservationRepository
init {
Given("등록된 모든 시간을 조회할 때") {
val endpoint = "/times"
When("관리자인 경우") {
beforeTest {
loginAsAdmin()
}
Then("정상 응답") {
every {
timeRepository.findAll()
} returns listOf(
TimeFixture.create(id = 1L),
TimeFixture.create(id = 2L)
)
runGetTest(
mockMvc = mockMvc,
endpoint = endpoint,
) {
status { isOk() }
content {
contentType(MediaType.APPLICATION_JSON)
jsonPath("$.data.times[0].id") { value(1) }
jsonPath("$.data.times[1].id") { value(2) }
}
}
}
}
When("관리자가 아닌 경우") {
loginAsUser()
val expectedError = AuthErrorCode.ACCESS_DENIED
Then("에러 응답을 받는다.") {
runGetTest(
mockMvc = mockMvc,
endpoint = endpoint,
) {
status { isEqualTo(expectedError.httpStatus.value()) }
}.andExpect {
content {
contentType(MediaType.APPLICATION_JSON)
jsonPath("$.code") { value(expectedError.errorCode) }
}
}
}
}
}
Given("시간을 추가할 때") {
val endpoint = "/times"
When("관리자인 경우") {
beforeTest {
loginAsAdmin()
}
val time = LocalTime.of(10, 0)
val request = TimeCreateRequest(startAt = time)
Then("시간 형식이 HH:mm이 아니거나, 범위를 벗어나면 400 응답") {
listOf(
"{\"startAt\": \"23:30:30\"}",
"{\"startAt\": \"24:59\"}",
).forEach {
runPostTest(
mockMvc = mockMvc,
endpoint = endpoint,
body = it,
) {
status { isBadRequest() }
}
}
}
Then("정상 응답") {
every {
timeService.createTime(request)
} returns TimeCreateResponse(id = 1, startAt = time)
runPostTest(
mockMvc = mockMvc,
endpoint = endpoint,
body = request,
) {
status { isCreated() }
content {
contentType(MediaType.APPLICATION_JSON)
jsonPath("$.data.id") { value(1) }
jsonPath("$.data.startAt") { value("10:00") }
}
}
}
Then("동일한 시간이 존재하면 예외 응답") {
val expectedError = TimeErrorCode.TIME_DUPLICATED
every {
timeRepository.existsByStartAt(time)
} returns true
runPostTest(
mockMvc = mockMvc,
endpoint = endpoint,
body = request,
) {
status { isEqualTo(expectedError.httpStatus.value()) }
content {
contentType(MediaType.APPLICATION_JSON)
jsonPath("$.code") { value(expectedError.errorCode) }
}
}
}
}
When("관리자가 아닌 경우") {
loginAsUser()
Then("에러 응답을 받는다.") {
val expectedError = AuthErrorCode.ACCESS_DENIED
runPostTest(
mockMvc = mockMvc,
endpoint = endpoint,
body = TimeFixture.create(),
) {
status { isEqualTo(expectedError.httpStatus.value()) }
jsonPath("$.code", equalTo(expectedError.errorCode))
}
}
}
}
Given("시간을 삭제할 때") {
val endpoint = "/times/1"
When("관리자인 경우") {
beforeTest {
loginAsAdmin()
}
Then("정상 응답") {
every {
timeService.deleteTime(1L)
} returns Unit
runDeleteTest(
mockMvc = mockMvc,
endpoint = endpoint,
) {
status { isNoContent() }
}
}
Then("없는 시간을 조회하면 예외 응답") {
val id = 1L
val expectedError = TimeErrorCode.TIME_NOT_FOUND
every {
timeRepository.findByIdOrNull(id)
} returns null
runDeleteTest(
mockMvc = mockMvc,
endpoint = "/times/$id",
) {
status { isEqualTo(expectedError.httpStatus.value()) }
content {
contentType(MediaType.APPLICATION_JSON)
jsonPath("$.code") { value(expectedError.errorCode) }
}
}
}
Then("예약이 있는 시간을 삭제하면 예외 응답") {
val id = 1L
val expectedError = TimeErrorCode.TIME_ALREADY_RESERVED
every {
timeRepository.findByIdOrNull(id)
} returns TimeFixture.create(id = id)
every {
reservationRepository.findAllByTime(any())
} returns listOf(ReservationFixture.create())
runDeleteTest(
mockMvc = mockMvc,
endpoint = "/times/$id",
) {
status { isEqualTo(expectedError.httpStatus.value()) }
content {
contentType(MediaType.APPLICATION_JSON)
jsonPath("$.code") { value(expectedError.errorCode) }
}
}
}
}
When("관리자가 아닌 경우") {
loginAsUser()
Then("에러 응답을 받는다.") {
val expectedError = AuthErrorCode.ACCESS_DENIED
runDeleteTest(
mockMvc = mockMvc,
endpoint = endpoint,
) {
status { isEqualTo(expectedError.httpStatus.value()) }
jsonPath("$.code", equalTo(expectedError.errorCode))
}
}
}
}
Given("날짜, 테마가 주어졌을 때") {
loginAsUser()
val date: LocalDate = LocalDate.now()
val themeId = 1L
When("저장된 예약 시간이 있으면") {
val times: List<TimeEntity> = listOf(
TimeFixture.create(id = 1L, startAt = LocalTime.of(10, 0)),
TimeFixture.create(id = 2L, startAt = LocalTime.of(11, 0))
)
every {
timeRepository.findAll()
} returns times
Then("그 시간과, 해당 날짜와 테마에 대한 예약 여부가 담긴 목록을 응답") {
every {
reservationRepository.findByDateAndThemeId(date, themeId)
} returns listOf(
ReservationFixture.create(
id = 1L,
date = date,
theme = ThemeFixture.create(id = themeId),
time = times[0]
)
)
val response = runGetTest(
mockMvc = mockMvc,
endpoint = "/times/search?date=$date&themeId=$themeId",
) {
status { isOk() }
content {
contentType(MediaType.APPLICATION_JSON)
}
}.andReturn().readValue(TimeWithAvailabilityListResponse::class.java)
assertSoftly(response.times) {
this shouldHaveSize times.size
this[0].id shouldBe times[0].id
this[0].isAvailable shouldBe false
this[1].id shouldBe times[1].id
this[1].isAvailable shouldBe true
}
}
}
}
}
}