602 lines
24 KiB
Kotlin

package roomescape.schedule
import io.kotest.matchers.date.shouldBeAfter
import io.kotest.matchers.nulls.shouldNotBeNull
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.CoreMatchers.notNullValue
import org.springframework.data.repository.findByIdOrNull
import org.springframework.http.HttpMethod
import org.springframework.http.HttpStatus
import roomescape.admin.infrastructure.persistence.AdminPermissionLevel
import roomescape.auth.exception.AuthErrorCode
import roomescape.schedule.exception.ScheduleErrorCode
import roomescape.schedule.infrastructure.persistence.ScheduleEntity
import roomescape.schedule.infrastructure.persistence.ScheduleRepository
import roomescape.schedule.infrastructure.persistence.ScheduleStatus
import roomescape.schedule.web.ScheduleUpdateRequest
import roomescape.supports.*
import roomescape.supports.ScheduleFixture.createRequest
import java.time.LocalDate
import java.time.LocalTime
class ScheduleApiTest(
private val scheduleRepository: ScheduleRepository
) : FunSpecSpringbootTest() {
init {
context("특정 날짜에 예약 가능한 테마 목록을 조회한다.") {
val date = LocalDate.now().plusDays(1)
val endpoint = "/schedules/themes?date=$date"
test("정상 응답") {
val adminToken = testAuthUtil.defaultStoreAdminLogin()
for (i in 1..10) {
dummyInitializer.createSchedule(
adminToken = adminToken,
request = createRequest.copy(
date = date,
time = LocalTime.now().plusMinutes(i.toLong())
)
)
}
runTest(
token = testAuthUtil.defaultUserLogin(),
on = {
get(endpoint)
},
expect = {
statusCode(HttpStatus.OK.value())
body("data.themeIds.size()", equalTo(10))
}
)
}
}
context("동일한 날짜, 테마에 대한 모든 시간을 조회한다.") {
test("정상 응답") {
val date = LocalDate.now().plusDays(1)
val adminToken = testAuthUtil.defaultStoreAdminLogin()
val createdSchedule = dummyInitializer.createSchedule(
adminToken = adminToken,
request = createRequest.copy(date = date, time = LocalTime.now())
)
for (i in 1..10) {
dummyInitializer.createSchedule(
adminToken = adminToken,
request = createRequest.copy(
date = date,
time = LocalTime.now().plusMinutes(i.toLong()),
themeId = createdSchedule.themeId
)
)
}
runTest(
token = testAuthUtil.defaultUserLogin(),
on = {
get("/schedules?date=$date&themeId=${createdSchedule.themeId}")
},
expect = {
statusCode(HttpStatus.OK.value())
body("data.schedules.size()", equalTo(11))
assertProperties(
props = setOf("id", "time", "status"),
propsNameIfList = "schedules"
)
}
)
}
}
context("관리자 페이지에서 특정 일정의 감사 정보를 조회한다.") {
context("권한이 없으면 접근할 수 없다.") {
val endpoint = "/schedules/${INVALID_PK}"
test("비회원") {
runExceptionTest(
method = HttpMethod.GET,
endpoint = endpoint,
expectedErrorCode = AuthErrorCode.TOKEN_NOT_FOUND
)
}
test("회원") {
runExceptionTest(
token = testAuthUtil.defaultUserLogin(),
method = HttpMethod.GET,
endpoint = endpoint,
expectedErrorCode = AuthErrorCode.ACCESS_DENIED
)
}
test("권한이 ${AdminPermissionLevel.READ_SUMMARY}인 관리자") {
val admin = AdminFixture.create(permissionLevel = AdminPermissionLevel.READ_SUMMARY)
runExceptionTest(
token = testAuthUtil.adminLogin(admin),
method = HttpMethod.GET,
endpoint = endpoint,
expectedErrorCode = AuthErrorCode.ACCESS_DENIED
)
}
}
test("정상 응답") {
val token = testAuthUtil.defaultStoreAdminLogin()
val createdSchedule = dummyInitializer.createSchedule(
adminToken = token,
request = createRequest
)
runTest(
token = token,
on = {
get("/schedules/${createdSchedule.id}")
},
expect = {
statusCode(HttpStatus.OK.value())
body("data.id", equalTo(createdSchedule.id))
assertProperties(
props = setOf(
"id", "date", "time", "status",
"createdAt", "createdBy", "updatedAt", "updatedBy",
)
)
}
).also {
it.extract().path<String>("data.createdAt") shouldNotBeNull {}
it.extract().path<LinkedHashMap<String, Any>>("data.createdBy") shouldNotBeNull {}
it.extract().path<String>("data.updatedAt") shouldNotBeNull {}
it.extract().path<LinkedHashMap<String, Any>>("data.updatedBy") shouldNotBeNull {}
}
}
test("일정이 없으면 실패한다.") {
runExceptionTest(
token = testAuthUtil.defaultStoreAdminLogin(),
method = HttpMethod.GET,
endpoint = "/schedules/$INVALID_PK",
expectedErrorCode = ScheduleErrorCode.SCHEDULE_NOT_FOUND
)
}
}
context("일정을 생성한다.") {
context("권한이 없으면 접근할 수 없다.") {
val endpoint = "/schedules"
test("비회원") {
runExceptionTest(
method = HttpMethod.POST,
requestBody = createRequest,
endpoint = endpoint,
expectedErrorCode = AuthErrorCode.TOKEN_NOT_FOUND
)
}
test("회원") {
runExceptionTest(
token = testAuthUtil.defaultUserLogin(),
method = HttpMethod.POST,
requestBody = createRequest,
endpoint = endpoint,
expectedErrorCode = AuthErrorCode.ACCESS_DENIED
)
}
listOf(AdminPermissionLevel.READ_SUMMARY, AdminPermissionLevel.READ_ALL).forEach {
test("권한이 ${it}인 관리자") {
val admin = AdminFixture.create(permissionLevel = it)
runExceptionTest(
token = testAuthUtil.adminLogin(admin),
method = HttpMethod.POST,
requestBody = createRequest,
endpoint = endpoint,
expectedErrorCode = AuthErrorCode.ACCESS_DENIED
)
}
}
}
test("정상 생성 및 감사 정보 확인") {
val token = testAuthUtil.defaultStoreAdminLogin()
val themeId: Long = dummyInitializer.createTheme(
adminToken = token,
request = ThemeFixture.createRequest.copy(name = "theme-${System.currentTimeMillis()}")
).id
/**
* 생성 테스트
*/
runTest(
token = token,
using = {
body(createRequest.copy(themeId = themeId))
},
on = {
post("/schedules")
},
expect = {
statusCode(HttpStatus.OK.value())
body("data.id", notNullValue())
}
).also {
val createdScheduleId: Long = it.extract().path("data.id")
val createdSchedule: ScheduleEntity = scheduleRepository.findByIdOrNull(createdScheduleId)
?: throw AssertionError("Unexpected Exception Occurred.")
createdSchedule.date shouldBe createRequest.date
createdSchedule.time.hour shouldBe createRequest.time.hour
createdSchedule.time.minute shouldBe createRequest.time.minute
createdSchedule.createdAt shouldNotBeNull {}
createdSchedule.createdBy shouldNotBeNull {}
createdSchedule.updatedAt shouldNotBeNull {}
createdSchedule.updatedBy shouldNotBeNull {}
}
}
test("이미 동일한 날짜, 시간, 테마인 일정이 있으면 실패한다.") {
val token = testAuthUtil.defaultStoreAdminLogin()
val date = LocalDate.now().plusDays(1)
val time = LocalTime.of(10, 0)
val alreadyCreated: ScheduleEntity = dummyInitializer.createSchedule(
adminToken = token,
request = createRequest.copy(date = date, time = time)
)
val body = createRequest.copy(date = date, time = time, themeId = alreadyCreated.themeId)
runExceptionTest(
token = token,
method = HttpMethod.POST,
endpoint = "/schedules",
requestBody = body,
expectedErrorCode = ScheduleErrorCode.SCHEDULE_ALREADY_EXISTS
)
}
test("입력된 날짜 + 시간이 현재 시간 이전이면 실패한다.") {
val token = testAuthUtil.defaultStoreAdminLogin()
val body = createRequest.copy(LocalDate.now(), LocalTime.now().minusMinutes(1))
runExceptionTest(
token = token,
method = HttpMethod.POST,
endpoint = "/schedules",
requestBody = body,
expectedErrorCode = ScheduleErrorCode.PAST_DATE_TIME
)
}
}
context("일정을 잠시 Hold 상태로 변경하여 중복 예약을 방지한다.") {
context("권한이 없으면 접근할 수 없다.") {
val endpoint = "/schedules/${INVALID_PK}/hold"
test("비회원") {
runExceptionTest(
method = HttpMethod.PATCH,
endpoint = endpoint,
expectedErrorCode = AuthErrorCode.TOKEN_NOT_FOUND
)
}
test("관리자") {
runExceptionTest(
token = testAuthUtil.defaultStoreAdminLogin(),
method = HttpMethod.PATCH,
endpoint = endpoint,
expectedErrorCode = AuthErrorCode.ACCESS_DENIED
)
}
}
test("해당 일정이 ${ScheduleStatus.AVAILABLE} 상태이면 정상 응답") {
val createdSchedule: ScheduleEntity = dummyInitializer.createSchedule(
adminToken = testAuthUtil.defaultStoreAdminLogin(),
request = createRequest
)
runTest(
token = testAuthUtil.defaultUserLogin(),
on = {
patch("/schedules/${createdSchedule.id}/hold")
},
expect = {
statusCode(HttpStatus.OK.value())
}
).also {
val updatedSchedule = scheduleRepository.findByIdOrNull(createdSchedule.id)
?: throw AssertionError("Unexpected Exception Occurred.")
updatedSchedule.status shouldBe ScheduleStatus.HOLD
}
}
test("예약이 없으면 실패한다.") {
runExceptionTest(
token = testAuthUtil.defaultUserLogin(),
method = HttpMethod.PATCH,
endpoint = "/schedules/$INVALID_PK/hold",
expectedErrorCode = ScheduleErrorCode.SCHEDULE_NOT_FOUND
)
}
test("해당 일정이 ${ScheduleStatus.AVAILABLE} 상태가 아니면 실패한다.") {
val adminToken = testAuthUtil.defaultStoreAdminLogin()
val createdSchedule: ScheduleEntity = dummyInitializer.createSchedule(
adminToken = adminToken,
request = createRequest
)
/*
* 테스트를 위해 수정 API를 호출하여 상태를 HOLD 상태로 변경
* 생성 API에서는 일정 생성 시 AVAILABLE을 기본 상태로 지정하기 때문.
*/
runTest(
token = adminToken,
using = {
body(
ScheduleUpdateRequest(status = ScheduleStatus.HOLD)
)
},
on = {
patch("/schedules/${createdSchedule.id}")
},
expect = {
statusCode(HttpStatus.OK.value())
}
)
runExceptionTest(
token = testAuthUtil.defaultUserLogin(),
method = HttpMethod.PATCH,
endpoint = "/schedules/${createdSchedule.id}/hold",
expectedErrorCode = ScheduleErrorCode.SCHEDULE_NOT_AVAILABLE
)
}
}
context("일정을 수정한다.") {
val updateRequest = ScheduleUpdateRequest(
time = LocalTime.now().plusHours(1),
status = ScheduleStatus.BLOCKED
)
context("권한이 없으면 접근할 수 없다.") {
val endpoint = "/schedules/${INVALID_PK}"
test("비회원") {
runExceptionTest(
method = HttpMethod.PATCH,
requestBody = updateRequest,
endpoint = endpoint,
expectedErrorCode = AuthErrorCode.TOKEN_NOT_FOUND
)
}
test("회원") {
runExceptionTest(
token = testAuthUtil.defaultUserLogin(),
method = HttpMethod.PATCH,
requestBody = updateRequest,
endpoint = endpoint,
expectedErrorCode = AuthErrorCode.ACCESS_DENIED
)
}
listOf(AdminPermissionLevel.READ_SUMMARY, AdminPermissionLevel.READ_ALL).forEach {
test("권한이 ${it}인 관리자") {
val admin = AdminFixture.create(permissionLevel = it)
runExceptionTest(
token = testAuthUtil.adminLogin(admin),
method = HttpMethod.PATCH,
requestBody = updateRequest,
endpoint = endpoint,
expectedErrorCode = AuthErrorCode.ACCESS_DENIED
)
}
}
}
test("정상 수정 및 감사 정보 변경 확인") {
val createdSchedule: ScheduleEntity = dummyInitializer.createSchedule(
adminToken = testAuthUtil.defaultStoreAdminLogin(),
request = createRequest.copy(
date = LocalDate.now().plusDays(1),
time = LocalTime.now().plusMinutes(1),
)
)
val otherAdminToken = testAuthUtil.adminLogin(
AdminFixture.create(account = "otherAdmin", phone = "01099999999")
)
runTest(
token = otherAdminToken,
using = {
body(updateRequest)
},
on = {
patch("/schedules/${createdSchedule.id}")
},
expect = {
statusCode(HttpStatus.OK.value())
}
).also {
val updatedSchedule = scheduleRepository.findByIdOrNull(createdSchedule.id)!!
updatedSchedule.id shouldBe createdSchedule.id
updatedSchedule.time.hour shouldBe updateRequest.time!!.hour
updatedSchedule.time.minute shouldBe updateRequest.time.minute
updatedSchedule.status shouldBe updateRequest.status
updatedSchedule.updatedBy shouldNotBe createdSchedule.updatedBy
updatedSchedule.updatedAt shouldBeAfter createdSchedule.updatedAt
}
}
test("입력값이 없으면 수정하지 않는다.") {
val token = testAuthUtil.defaultStoreAdminLogin()
val createdSchedule: ScheduleEntity = dummyInitializer.createSchedule(
adminToken = token,
request = createRequest
)
runTest(
token = token,
using = {
body(ScheduleUpdateRequest())
},
on = {
patch("/schedules/${createdSchedule.id}")
},
expect = {
statusCode(HttpStatus.OK.value())
}
).also {
val updatedSchedule = scheduleRepository.findByIdOrNull(createdSchedule.id)!!
updatedSchedule.id shouldBe createdSchedule.id
updatedSchedule.updatedAt shouldBe createdSchedule.updatedAt
}
}
test("일정이 없으면 실패한다.") {
runExceptionTest(
token = testAuthUtil.defaultStoreAdminLogin(),
method = HttpMethod.PATCH,
requestBody = updateRequest,
endpoint = "/schedules/${INVALID_PK}",
expectedErrorCode = ScheduleErrorCode.SCHEDULE_NOT_FOUND
)
}
test("입력된 날짜 + 시간이 현재 시간 이전이면 실패한다.") {
val token = testAuthUtil.defaultStoreAdminLogin()
val createdSchedule: ScheduleEntity = dummyInitializer.createSchedule(
adminToken = token,
request =
createRequest.copy(date = LocalDate.now(), time = LocalTime.now().plusMinutes(1))
)
runExceptionTest(
token = token,
method = HttpMethod.PATCH,
requestBody = updateRequest.copy(time = LocalTime.now().minusMinutes(1)),
endpoint = "/schedules/${createdSchedule.id}",
expectedErrorCode = ScheduleErrorCode.PAST_DATE_TIME
)
}
}
context("일정을 삭제한다.") {
context("권한이 없으면 접근할 수 없다.") {
val endpoint = "/schedules/${INVALID_PK}"
test("비회원") {
runExceptionTest(
method = HttpMethod.DELETE,
endpoint = endpoint,
expectedErrorCode = AuthErrorCode.TOKEN_NOT_FOUND
)
}
test("회원") {
runExceptionTest(
token = testAuthUtil.defaultUserLogin(),
method = HttpMethod.DELETE,
endpoint = endpoint,
expectedErrorCode = AuthErrorCode.ACCESS_DENIED
)
}
listOf(AdminPermissionLevel.READ_SUMMARY, AdminPermissionLevel.READ_ALL).forEach {
test("권한이 ${it}인 관리자") {
val admin = AdminFixture.create(permissionLevel = it)
runExceptionTest(
token = testAuthUtil.adminLogin(admin),
method = HttpMethod.DELETE,
endpoint = endpoint,
expectedErrorCode = AuthErrorCode.ACCESS_DENIED
)
}
}
}
test("정상 삭제") {
val token = testAuthUtil.defaultStoreAdminLogin()
val createdSchedule: ScheduleEntity = dummyInitializer.createSchedule(
adminToken = token,
request = createRequest
)
runTest(
token = token,
on = {
delete("/schedules/${createdSchedule.id}")
},
expect = {
statusCode(HttpStatus.NO_CONTENT.value())
}
).also {
scheduleRepository.findByIdOrNull(createdSchedule.id) shouldBe null
}
}
test("예약 중이거나 예약이 완료된 일정이면 실패한다.") {
val token = testAuthUtil.defaultStoreAdminLogin()
val createdSchedule: ScheduleEntity = dummyInitializer.createSchedule(
adminToken = token,
request = createRequest
)
/*
* 테스트를 위해 수정 API를 호출하여 상태를 예약 중 상태로 변경
* 생성 API에서는 일정 생성 시 AVAILABLE을 기본 상태로 지정하기 때문.
*/
runTest(
token = token,
using = {
body(
ScheduleUpdateRequest(status = ScheduleStatus.RESERVED)
)
},
on = {
patch("/schedules/${createdSchedule.id}")
},
expect = {
statusCode(HttpStatus.OK.value())
}
)
/**
* 삭제 테스트
*/
runExceptionTest(
token = token,
method = HttpMethod.DELETE,
endpoint = "/schedules/${createdSchedule.id}",
expectedErrorCode = ScheduleErrorCode.SCHEDULE_IN_USE
)
}
}
}
}