feat: 모든 스케쥴 API 테스트 추가

This commit is contained in:
이상진 2025-09-04 11:47:48 +09:00
parent 9fdbcba85a
commit 5d23216e17

View File

@ -0,0 +1,569 @@
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 io.restassured.module.kotlin.extensions.Extract
import io.restassured.module.kotlin.extensions.Given
import io.restassured.module.kotlin.extensions.When
import io.restassured.response.ValidatableResponse
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.CoreMatchers.notNullValue
import org.springframework.data.repository.findByIdOrNull
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import roomescape.auth.exception.AuthErrorCode
import roomescape.member.infrastructure.persistence.Role
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.ScheduleCreateRequest
import roomescape.schedule.web.ScheduleUpdateRequest
import roomescape.util.*
import java.time.LocalDate
import java.time.LocalTime
class ScheduleApiTest(
private val scheduleRepository: ScheduleRepository
) : FunSpecSpringbootTest() {
init {
context("관리자가 아니면 접근할 수 없다.") {
lateinit var token: String
beforeTest {
token = loginUtil.loginAsUser()
}
val commonAssertion: ValidatableResponse.() -> Unit = {
statusCode(HttpStatus.FORBIDDEN.value())
body(
"code",
equalTo(AuthErrorCode.ACCESS_DENIED.errorCode)
)
}
test("일정 상세: GET /schedules/{id}") {
runTest(
token = token,
on = {
get("/schedules/1")
},
expect = commonAssertion
)
}
test("일정 생성: POST /schedules") {
runTest(
token = token,
using = {
body(ScheduleFixture.createRequest)
},
on = {
get("/schedules/1")
},
expect = commonAssertion
)
}
test("일정 수정: PATCH /schedules/{id}") {
val createdSchedule: ScheduleEntity =
createDummySchedule(
ScheduleFixture.createRequest
)
runTest(
token = token,
using = {
body(ScheduleFixture.createRequest)
},
on = {
patch("/schedules/${createdSchedule.id}")
},
expect = commonAssertion
)
}
test("일정 삭제: DELETE /schedules/{id}") {
val createdSchedule: ScheduleEntity =
createDummySchedule(
ScheduleFixture.createRequest
)
runTest(
token = token,
on = {
delete("/schedules/${createdSchedule.id}")
},
expect = commonAssertion
)
}
}
context("일반 회원도 접근할 수 있다.") {
lateinit var token: String
beforeTest {
token = loginUtil.loginAsUser()
}
test("예약 가능 테마 조회: GET /schedules/themes?date={date}") {
val date = LocalDate.now().plusDays(1)
val time = LocalTime.now()
val createdSchedule: ScheduleEntity =
createDummySchedule(
ScheduleFixture.createRequest.copy(
date = date,
time = time
)
)
runTest(
token = token,
on = {
get("/schedules/themes?date=$date")
},
expect = {
statusCode(HttpStatus.OK.value())
body("data.themeIds.size()", equalTo(1))
body("data.themeIds[0]", equalTo(createdSchedule.themeId))
}
)
}
test("동일한 날짜, 테마에 대한 시간 조회: GET /schedules?date={date}&themeId={themeId}") {
val date = LocalDate.now().plusDays(1)
val time = LocalTime.now()
val createdSchedule: ScheduleEntity =
createDummySchedule(
ScheduleFixture.createRequest.copy(
date = date,
time = time
)
)
createDummySchedule(
ScheduleFixture.createRequest.copy(
date = date.plusDays(1L),
time = time,
themeId = createdSchedule.themeId
)
)
runTest(
token = token,
on = {
get("/schedules?date=$date&themeId=${createdSchedule.themeId}")
},
expect = {
statusCode(HttpStatus.OK.value())
body("data.schedules.size()", equalTo(1))
body("data.schedules[0].id", equalTo(createdSchedule.id))
assertProperties(
props = setOf("id", "time", "status"),
propsNameIfList = "schedules"
)
}
)
}
}
context("특정 날짜에 예약 가능한 테마 목록을 조회한다.") {
test("정상 응답") {
val date = LocalDate.now().plusDays(1)
for (i in 1..10) {
createDummySchedule(
ScheduleFixture.createRequest.copy(
date = date,
time = LocalTime.now().plusMinutes(i.toLong())
)
)
}
runTest(
token = loginUtil.loginAsUser(),
on = {
get("/schedules/themes?date=$date")
},
expect = {
statusCode(HttpStatus.OK.value())
body("data.themeIds.size()", equalTo(10))
}
)
}
}
context("동일한 날짜, 테마에 대한 모든 시간을 조회한다.") {
test("정상 응답") {
val date = LocalDate.now().plusDays(1)
val createdSchedule = createDummySchedule(
ScheduleFixture.createRequest.copy(
date = date,
time = LocalTime.now()
)
)
for (i in 1..10) {
createDummySchedule(
ScheduleFixture.createRequest.copy(
date = date,
time = LocalTime.now().plusMinutes(i.toLong()),
themeId = createdSchedule.themeId
)
)
}
runTest(
token = loginUtil.loginAsUser(),
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("관리자 페이지에서 특정 일정의 감사 정보를 조회한다.") {
test("정상 응답") {
val createdSchedule = createDummySchedule(ScheduleFixture.createRequest)
runTest(
token = loginUtil.loginAsAdmin(),
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<String>("data.createdBy") shouldNotBeNull {}
it.extract().path<String>("data.createdAt") shouldNotBeNull {}
it.extract().path<String>("data.createdAt") shouldNotBeNull {}
}
}
test("일정이 없으면 실패한다.") {
runTest(
token = loginUtil.loginAsAdmin(),
on = {
get("/schedules/1")
},
expect = {
statusCode(HttpStatus.NOT_FOUND.value())
body(
"code",
equalTo(ScheduleErrorCode.SCHEDULE_NOT_FOUND.errorCode)
)
}
)
}
}
context("일정을 생성한다.") {
lateinit var token: String
beforeTest {
token = loginUtil.loginAsAdmin()
}
test("정상 생성 및 감사 정보 확인") {
/**
* FK 제약조건 해소를 위한 테마 생성 API 호출 ID 획득
*/
val themeId: Long = Given {
contentType(MediaType.APPLICATION_JSON_VALUE)
header("Authorization", "Bearer $token")
body(ThemeFixtureV2.createRequest.copy(name = "theme-${System.currentTimeMillis()}"))
} When {
post("/admin/themes")
} Extract {
path("data.id")
}
/**
* 생성 테스트
*/
runTest(
token = token,
using = {
body(ScheduleFixture.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 ScheduleFixture.createRequest.date
createdSchedule.time.hour shouldBe ScheduleFixture.createRequest.time.hour
createdSchedule.time.minute shouldBe ScheduleFixture.createRequest.time.minute
createdSchedule.createdAt shouldNotBeNull {}
createdSchedule.createdBy shouldNotBeNull {}
createdSchedule.updatedAt shouldNotBeNull {}
createdSchedule.updatedBy shouldNotBeNull {}
}
}
test("이미 동일한 날짜, 시간, 테마인 일정이 있으면 실패한다.") {
val date = LocalDate.now().plusDays(1)
val time = LocalTime.of(10, 0)
val alreadyCreated: ScheduleEntity = createDummySchedule(
ScheduleFixture.createRequest.copy(
date = date, time = time
)
)
runTest(
token = token,
using = {
body(
ScheduleFixture.createRequest.copy(
date = date, time = time, themeId = alreadyCreated.themeId
)
)
},
on = {
post("/schedules")
},
expect = {
statusCode(HttpStatus.CONFLICT.value())
body("code", equalTo(ScheduleErrorCode.SCHEDULE_ALREADY_EXISTS.errorCode))
}
)
}
test("입력된 날짜 + 시간이 현재 시간 이전이면 실패한다.") {
runTest(
token = token,
using = {
body(
ScheduleFixture.createRequest.copy(
date = LocalDate.now(),
time = LocalTime.now().minusMinutes(1)
)
)
},
on = {
post("/schedules")
},
expect = {
statusCode(HttpStatus.BAD_REQUEST.value())
body("code", equalTo(ScheduleErrorCode.PAST_DATE_TIME.errorCode))
}
)
}
}
context("일정을 수정한다.") {
val updateRequest = ScheduleUpdateRequest(
time = LocalTime.now().plusHours(1),
status = ScheduleStatus.BLOCKED
)
test("정상 수정 및 감사 정보 변경 확인") {
val createdSchedule: ScheduleEntity = createDummySchedule(
ScheduleFixture.createRequest.copy(
date = LocalDate.now().plusDays(1),
time = LocalTime.now().plusMinutes(1),
)
)
val otherAdminToken = loginUtil.login("admin1@admin.com", "admin1", Role.ADMIN)
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 createdSchedule: ScheduleEntity = createDummySchedule(ScheduleFixture.createRequest)
runTest(
token = loginUtil.loginAsAdmin(),
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("일정이 없으면 실패한다.") {
runTest(
token = loginUtil.loginAsAdmin(),
using = {
body(updateRequest)
},
on = {
patch("/schedules/1")
},
expect = {
statusCode(HttpStatus.NOT_FOUND.value())
body("code", equalTo(ScheduleErrorCode.SCHEDULE_NOT_FOUND.errorCode))
}
)
}
test("입력된 날짜 + 시간이 현재 시간 이전이면 실패한다.") {
val createdSchedule: ScheduleEntity = createDummySchedule(
ScheduleFixture.createRequest.copy(date = LocalDate.now(), time = LocalTime.now().plusMinutes(1))
)
runTest(
token = loginUtil.loginAsAdmin(),
using = {
body(
updateRequest.copy(
time = LocalTime.now().minusMinutes(1)
)
)
},
on = {
patch("/schedules/${createdSchedule.id}")
},
expect = {
statusCode(HttpStatus.BAD_REQUEST.value())
body("code", equalTo(ScheduleErrorCode.PAST_DATE_TIME.errorCode))
}
)
}
}
context("일정을 삭제한다.") {
test("정상 삭제") {
val createdSchedule: ScheduleEntity = createDummySchedule(ScheduleFixture.createRequest)
runTest(
token = loginUtil.loginAsAdmin(),
on = {
delete("/schedules/${createdSchedule.id}")
},
expect = {
statusCode(HttpStatus.NO_CONTENT.value())
}
).also {
scheduleRepository.findByIdOrNull(createdSchedule.id) shouldBe null
}
}
test("예약 중이거나 예약이 완료된 일정이면 실패한다.") {
val createdSchedule: ScheduleEntity = createDummySchedule(ScheduleFixture.createRequest)
/*
* 테스트를 위해 수정 API를 호출하여 상태를 예약 상태로 변경
* 생성 API에서는 일정 생성 AVAILABLE을 기본 상태로 지정하기 때문.
*/
runTest(
token = loginUtil.loginAsAdmin(),
using = {
body(
ScheduleUpdateRequest(
status = ScheduleStatus.RESERVED
)
)
},
on = {
patch("/schedules/${createdSchedule.id}")
},
expect = {
statusCode(HttpStatus.OK.value())
}
)
/**
* 삭제 테스트
*/
runTest(
token = loginUtil.loginAsAdmin(),
on = {
delete("/schedules/${createdSchedule.id}")
},
expect = {
statusCode(HttpStatus.CONFLICT.value())
body("code", equalTo(ScheduleErrorCode.SCHEDULE_IN_USE.errorCode))
}
)
}
}
}
fun createDummySchedule(request: ScheduleCreateRequest): ScheduleEntity {
val token = loginUtil.loginAsAdmin()
val themeId: Long = if (request.themeId > 1L) {
request.themeId
} else {
Given {
contentType(MediaType.APPLICATION_JSON_VALUE)
header("Authorization", "Bearer $token")
body(ThemeFixtureV2.createRequest.copy(name = "theme-${System.currentTimeMillis()}"))
} When {
post("/admin/themes")
} Extract {
path("data.id")
}
}
val createdScheduleId: Long = Given {
contentType(MediaType.APPLICATION_JSON_VALUE)
header("Authorization", "Bearer $token")
body(request.copy(themeId = themeId))
} When {
post("/schedules")
} Extract {
path("data.id")
}
return scheduleRepository.findByIdOrNull(createdScheduleId)
?: throw RuntimeException("unreachable line")
}
}