refactor: 일정 API 테스트 수정 및 관리자 / 비 관리자로 분리

This commit is contained in:
이상진 2025-09-18 15:30:45 +09:00
parent 7cfc7a4d9f
commit 4331acee31
2 changed files with 707 additions and 529 deletions

View File

@ -0,0 +1,657 @@
package roomescape.schedule
import io.kotest.assertions.assertSoftly
import io.kotest.matchers.date.shouldBeBefore
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import org.hamcrest.CoreMatchers.equalTo
import org.springframework.data.repository.findByIdOrNull
import org.springframework.http.HttpMethod
import org.springframework.http.HttpStatus
import roomescape.admin.infrastructure.persistence.AdminPermissionLevel
import roomescape.admin.infrastructure.persistence.AdminType
import roomescape.auth.exception.AuthErrorCode
import roomescape.common.dto.AuditConstant
import roomescape.common.dto.OperatorInfo
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.AdminScheduleSummaryResponse
import roomescape.schedule.web.ScheduleUpdateRequest
import roomescape.store.infrastructure.persistence.StoreEntity
import roomescape.supports.*
import java.time.LocalDate
import java.time.LocalTime
class AdminScheduleApiTest(
private var scheduleRepository: ScheduleRepository
) : FunSpecSpringbootTest() {
init {
context("날짜, 시간, 테마로 특정 매장의 일정을 검색한다.") {
context("권한이 없으면 접근할 수 없다.") {
test("비회원") {
runExceptionTest(
method = HttpMethod.GET,
endpoint = "/admin/stores/${INVALID_PK}/schedules",
expectedErrorCode = AuthErrorCode.TOKEN_NOT_FOUND
)
}
test("회원") {
runExceptionTest(
token = testAuthUtil.defaultUserLogin(),
method = HttpMethod.GET,
endpoint = "/admin/stores/${INVALID_PK}/schedules",
expectedErrorCode = AuthErrorCode.ACCESS_DENIED
)
}
}
context("정상 응답") {
lateinit var store: StoreEntity
lateinit var schedules: List<ScheduleEntity>
lateinit var token: String
beforeTest {
val today = LocalDate.now()
store = dummyInitializer.createStore()
val admin = AdminFixture.createStoreAdmin(storeId = store.id)
token = testAuthUtil.adminLogin(admin).second
schedules =
initialize("${today}인 일정 2개와 다음날의 일정 1개를 추가하며, 시간순 정렬 확인을 위해 ${today}인 일정 2개는 시간 내림차순으로 추가한다.") {
listOf(
dummyInitializer.createSchedule(
storeId = store.id,
request = ScheduleFixture.createRequest.copy(
date = today,
time = LocalTime.now().plusHours(2)
)
),
dummyInitializer.createSchedule(
storeId = store.id,
request = ScheduleFixture.createRequest.copy(
date = today,
time = LocalTime.now().plusHours(1)
)
),
dummyInitializer.createSchedule(
storeId = store.id,
request = ScheduleFixture.createRequest.copy(
date = today.plusDays(1),
time = LocalTime.of(11, 0)
)
)
)
}
}
test("날짜, 테마 미입력: 오늘 날짜의 전체 테마") {
runTest(
token = token,
on = {
get("/admin/stores/${store.id}/schedules")
},
expect = {
statusCode(HttpStatus.OK.value())
body("data.schedules.size()", equalTo(schedules.filter { it.date == LocalDate.now() }.size))
assertProperties(
props = setOf("id", "themeName", "startFrom", "endAt", "status"),
propsNameIfList = "schedules"
)
}
)
}
test("날짜만 입력: 입력된 날짜의 전체 테마") {
val date = schedules[0].date
runTest(
token = token,
on = {
get("/admin/stores/${store.id}/schedules?date=${date}")
},
expect = {
statusCode(HttpStatus.OK.value())
body("data.schedules.size()", equalTo(schedules.filter { it.date == date }.size))
assertProperties(
props = setOf("id", "themeName", "startFrom", "endAt", "status"),
propsNameIfList = "schedules"
)
}
)
}
test("테마만 입력: 오늘 날짜의 입력된 테마") {
val themeId = schedules[0].themeId
runTest(
token = token,
on = {
get("/admin/stores/${store.id}/schedules?themeId=${themeId}")
},
expect = {
statusCode(HttpStatus.OK.value())
body("data.schedules.size()", equalTo(schedules.filter { it.themeId == themeId }.size))
assertProperties(
props = setOf("id", "themeName", "startFrom", "endAt", "status"),
propsNameIfList = "schedules"
)
}
)
}
test("날짜, 테마 모두 입력") {
val date = schedules[0].date
val themeId = schedules[0].themeId
runTest(
token = token,
on = {
get("/admin/stores/${store.id}/schedules?date=${date}&themeId=${themeId}")
},
expect = {
statusCode(HttpStatus.OK.value())
body(
"data.schedules.size()",
equalTo(schedules.filter { it.date == date && it.themeId == themeId }.size)
)
assertProperties(
props = setOf("id", "themeName", "startFrom", "endAt", "status"),
propsNameIfList = "schedules"
)
}
)
}
test("결과는 시간 오름차순으로 정렬된다.") {
runTest(
token = token,
on = {
get("/admin/stores/${store.id}/schedules")
},
expect = {
statusCode(HttpStatus.OK.value())
}
).also {
val response: List<AdminScheduleSummaryResponse> =
ResponseParser.parseListResponse(it.extract().path("data.schedules"))
for (i in 0..<(response.size - 1)) {
response[i].startFrom shouldBeBefore response[i + 1].startFrom
}
}
}
}
}
context("일정의 감사 정보를 확인한다.") {
lateinit var store: StoreEntity
beforeTest {
store = dummyInitializer.createStore()
}
context("권한이 없으면 접근할 수 없다.") {
test("비회원") {
runExceptionTest(
method = HttpMethod.GET,
endpoint = "/admin/schedules/${INVALID_PK}/audits",
expectedErrorCode = AuthErrorCode.TOKEN_NOT_FOUND
)
}
test("회원") {
runExceptionTest(
token = testAuthUtil.defaultUserLogin(),
method = HttpMethod.GET,
endpoint = "/admin/schedules/${INVALID_PK}/audits",
expectedErrorCode = AuthErrorCode.ACCESS_DENIED
)
}
test("권한이 ${AdminPermissionLevel.READ_SUMMARY} 인 관리자") {
listOf(
AdminFixture.createStoreAdmin(permissionLevel = AdminPermissionLevel.READ_SUMMARY),
AdminFixture.createHqAdmin(permissionLevel = AdminPermissionLevel.READ_SUMMARY),
).forEach {
runExceptionTest(
token = testAuthUtil.adminLogin(it).second,
method = HttpMethod.GET,
endpoint = "/admin/schedules/${INVALID_PK}/audits",
expectedErrorCode = AuthErrorCode.ACCESS_DENIED
)
}
}
}
test("정상 응답") {
val (admin, scheduleId) = initialize("감사 이력을 남기기 위해, 로그인 후 일정을 생성한다.") {
val admin = AdminFixture.createStoreAdmin(storeId = store.id)
val themeId = initialize("일정 생성에 필요한 테마를 생성한다.") {
dummyInitializer.createTheme(ThemeFixture.createRequest).id
}
val scheduleId: Long = runTest(
token = testAuthUtil.adminLogin(admin).second,
using = {
body(ScheduleFixture.createRequest.copy(themeId = themeId))
},
on = {
post("/admin/stores/${store.id}/schedules")
},
expect = {
statusCode(HttpStatus.OK.value())
}
).extract().path("data.id")
admin to scheduleId
}
runTest(
token = testAuthUtil.adminLogin(admin).second,
on = {
get("/admin/schedules/${scheduleId}/audits")
},
expect = {
statusCode(HttpStatus.OK.value())
body("data.createdBy.id", equalTo(admin.id))
body("data.updatedBy.id", equalTo(admin.id))
}
)
}
test("감사 정보가 없으면 Unknown을 반환한다.") {
val schedule: ScheduleEntity = initialize("감사 이력을 남기지 않기 위해, 로그인 없이 일정을 생성한다.") {
dummyInitializer.createSchedule(storeId = store.id, request = ScheduleFixture.createRequest)
}
val unknownOperator: OperatorInfo = AuditConstant.UNKNOWN_OPERATOR
runTest(
token = testAuthUtil.defaultHqAdminLogin().second,
on = {
get("/admin/schedules/${schedule.id}/audits")
},
expect = {
statusCode(HttpStatus.OK.value())
body("data.createdBy.id", equalTo(unknownOperator.id.toInt()))
body("data.createdBy.name", equalTo(unknownOperator.name))
body("data.updatedBy.id", equalTo(unknownOperator.id.toInt()))
body("data.updatedBy.name", equalTo(unknownOperator.name))
}
)
}
}
context("일정을 생성한다.") {
context("권한이 없으면 접근할 수 없다.") {
test("비회원") {
runExceptionTest(
method = HttpMethod.POST,
endpoint = "/admin/stores/${INVALID_PK}/schedules",
requestBody = ScheduleFixture.createRequest,
expectedErrorCode = AuthErrorCode.TOKEN_NOT_FOUND
)
}
test("회원") {
runExceptionTest(
token = testAuthUtil.defaultUserLogin(),
method = HttpMethod.POST,
endpoint = "/admin/stores/${INVALID_PK}/schedules",
requestBody = ScheduleFixture.createRequest,
expectedErrorCode = AuthErrorCode.ACCESS_DENIED
)
}
context("관리자") {
listOf(
AdminPermissionLevel.READ_SUMMARY, AdminPermissionLevel.READ_ALL
).forEach {
test("type: ${AdminType.STORE} / permission: $it") {
val admin = AdminFixture.createStoreAdmin(permissionLevel = it)
runExceptionTest(
token = testAuthUtil.adminLogin(admin).second,
method = HttpMethod.POST,
endpoint = "/admin/stores/${INVALID_PK}/schedules",
requestBody = ScheduleFixture.createRequest,
expectedErrorCode = AuthErrorCode.ACCESS_DENIED
)
}
}
AdminPermissionLevel.entries.forEach {
test("type: ${AdminType.HQ} / permission: $it") {
val admin = AdminFixture.createHqAdmin(permissionLevel = it)
runExceptionTest(
token = testAuthUtil.adminLogin(admin).second,
method = HttpMethod.POST,
endpoint = "/admin/stores/${INVALID_PK}/schedules",
requestBody = ScheduleFixture.createRequest,
expectedErrorCode = AuthErrorCode.ACCESS_DENIED
)
}
}
}
}
test("정상 응답") {
val (admin, token) = testAuthUtil.defaultStoreAdminLogin()
val theme = dummyInitializer.createTheme()
val request = ScheduleFixture.createRequest.copy(themeId = theme.id)
runTest(
token = token,
using = {
body(request)
},
on = {
post("/admin/stores/${admin.storeId}/schedules")
},
expect = {
statusCode(HttpStatus.OK.value())
}
).also {
assertSoftly(scheduleRepository.findByIdOrNull(it.extract().path("data.id"))!!) {
this.createdBy shouldBe admin.id
this.updatedBy shouldBe admin.id
this.storeId shouldBe admin.storeId
this.themeId shouldBe theme.id
}
}
}
test("날짜, 시간, 테마가 동일한 일정이 있으면 실패한다.") {
val (admin, token) = testAuthUtil.defaultStoreAdminLogin()
val schedule = initialize("중복 테스트를 위한 사전 저장") {
dummyInitializer.createSchedule(storeId = admin.storeId!!)
}
val request = ScheduleFixture.createRequest.copy(
date = schedule.date,
time = schedule.time,
themeId = schedule.themeId
)
runExceptionTest(
token = token,
method = HttpMethod.POST,
endpoint = "/admin/stores/${admin.storeId}/schedules",
requestBody = request,
expectedErrorCode = ScheduleErrorCode.SCHEDULE_ALREADY_EXISTS
)
}
test("과거 시간을 선택하면 실패한다.") {
val (admin, token) = testAuthUtil.defaultStoreAdminLogin()
val date = LocalDate.now()
val time = LocalTime.now().minusMinutes(1)
val theme = dummyInitializer.createTheme()
val request = ScheduleFixture.createRequest.copy(date, time, theme.id)
runExceptionTest(
token = token,
method = HttpMethod.POST,
endpoint = "/admin/stores/${admin.storeId}/schedules",
requestBody = request,
expectedErrorCode = ScheduleErrorCode.PAST_DATE_TIME
)
}
test("겹치는 시간이 있으면 실패한다.") {
val (admin, token) = testAuthUtil.defaultStoreAdminLogin()
val tomorrow = LocalDate.now().plusDays(1)
val theme = dummyInitializer.createTheme(
ThemeFixture.createRequest.copy(availableMinutes = 100)
)
initialize("내일 14:00 ~ 15:40인 일정 생성") {
dummyInitializer.createSchedule(
storeId = admin.storeId!!,
request = ScheduleFixture.createRequest.copy(
tomorrow,
LocalTime.of(14, 0),
theme.id
)
)
}
val request = initialize("내일 14:30에 시작하는 요청 객체") {
ScheduleFixture.createRequest.copy(
tomorrow,
LocalTime.of(14, 30),
theme.id
)
}
runExceptionTest(
token = token,
method = HttpMethod.POST,
endpoint = "/admin/stores/${admin.storeId}/schedules",
requestBody = request,
expectedErrorCode = ScheduleErrorCode.SCHEDULE_TIME_CONFLICT
)
}
}
context("일정을 수정한다.") {
context("권한이 없으면 접근할 수 없다.") {
test("비회원") {
runExceptionTest(
method = HttpMethod.PATCH,
endpoint = "/admin/schedules/${INVALID_PK}",
requestBody = ScheduleUpdateRequest(),
expectedErrorCode = AuthErrorCode.TOKEN_NOT_FOUND
)
}
test("회원") {
runExceptionTest(
token = testAuthUtil.defaultUserLogin(),
method = HttpMethod.PATCH,
endpoint = "/admin/schedules/${INVALID_PK}",
requestBody = ScheduleUpdateRequest(),
expectedErrorCode = AuthErrorCode.ACCESS_DENIED
)
}
context("관리자") {
listOf(
AdminPermissionLevel.READ_SUMMARY, AdminPermissionLevel.READ_ALL
).forEach {
test("type: ${AdminType.STORE} / permission: $it") {
val admin = AdminFixture.createStoreAdmin(permissionLevel = it)
runExceptionTest(
token = testAuthUtil.adminLogin(admin).second,
method = HttpMethod.PATCH,
endpoint = "/admin/schedules/${INVALID_PK}",
requestBody = ScheduleUpdateRequest(),
expectedErrorCode = AuthErrorCode.ACCESS_DENIED
)
}
}
AdminPermissionLevel.entries.forEach {
test("type: ${AdminType.HQ} / permission: $it") {
val admin = AdminFixture.createHqAdmin(permissionLevel = it)
runExceptionTest(
token = testAuthUtil.adminLogin(admin).second,
method = HttpMethod.PATCH,
endpoint = "/admin/schedules/${INVALID_PK}",
requestBody = ScheduleUpdateRequest(),
expectedErrorCode = AuthErrorCode.ACCESS_DENIED
)
}
}
}
}
context("정상 응답") {
test("시간만 변경한다.") {
val (admin, token) = testAuthUtil.defaultStoreAdminLogin()
val schedule = initialize("수정을 위한 일정 생성") {
dummyInitializer.createSchedule()
}
val updateTime = schedule.time.plusHours(1)
runTest(
token = token,
using = {
body(ScheduleUpdateRequest(time = updateTime))
},
on = {
patch("/admin/schedules/${schedule.id}")
},
expect = {
statusCode(HttpStatus.OK.value())
}
).also {
val updated = scheduleRepository.findByIdOrNull(schedule.id)!!
updated.time shouldBe updateTime
updated.status shouldBe schedule.status
updated.updatedAt shouldNotBe schedule.updatedAt
}
}
test("상태만 변경한다.") {
val (admin, token) = testAuthUtil.defaultStoreAdminLogin()
val schedule = initialize("수정을 위한 일정 생성") {
dummyInitializer.createSchedule()
}
val updateStatus = ScheduleStatus.RESERVED
runTest(
token = token,
using = {
body(ScheduleUpdateRequest(status = updateStatus))
},
on = {
patch("/admin/schedules/${schedule.id}")
},
expect = {
statusCode(HttpStatus.OK.value())
}
).also {
val updated = scheduleRepository.findByIdOrNull(schedule.id)!!
updated.time shouldBe schedule.time
updated.status shouldBe updateStatus
updated.updatedAt shouldNotBe schedule.updatedAt
}
}
test("아무 값도 입력하지 않으면 바뀌지 않는다.") {
val (admin, token) = testAuthUtil.defaultStoreAdminLogin()
val schedule = initialize("수정을 위한 일정 생성") {
dummyInitializer.createSchedule()
}
runTest(
token = token,
using = {
body(ScheduleUpdateRequest())
},
on = {
patch("/admin/schedules/${schedule.id}")
},
expect = {
statusCode(HttpStatus.OK.value())
}
).also {
val updated = scheduleRepository.findByIdOrNull(schedule.id)!!
updated.time shouldBe schedule.time
updated.status shouldBe schedule.status
updated.updatedAt shouldBe schedule.updatedAt
}
}
}
}
context("일정을 삭제한다.") {
context("권한이 없으면 접근할 수 없다.") {
test("비회원") {
runExceptionTest(
method = HttpMethod.DELETE,
endpoint = "/admin/schedules/${INVALID_PK}",
expectedErrorCode = AuthErrorCode.TOKEN_NOT_FOUND
)
}
test("회원") {
runExceptionTest(
token = testAuthUtil.defaultUserLogin(),
method = HttpMethod.DELETE,
endpoint = "/admin/schedules/${INVALID_PK}",
expectedErrorCode = AuthErrorCode.ACCESS_DENIED
)
}
context("관리자") {
listOf(
AdminPermissionLevel.READ_SUMMARY, AdminPermissionLevel.READ_ALL
).forEach {
test("type: ${AdminType.STORE} / permission: $it") {
val admin = AdminFixture.createStoreAdmin(permissionLevel = it)
runExceptionTest(
token = testAuthUtil.adminLogin(admin).second,
method = HttpMethod.DELETE,
endpoint = "/admin/schedules/${INVALID_PK}",
expectedErrorCode = AuthErrorCode.ACCESS_DENIED
)
}
}
AdminPermissionLevel.entries.forEach {
test("type: ${AdminType.HQ} / permission: $it") {
val admin = AdminFixture.createHqAdmin(permissionLevel = it)
runExceptionTest(
token = testAuthUtil.adminLogin(admin).second,
method = HttpMethod.DELETE,
endpoint = "/admin/schedules/${INVALID_PK}",
expectedErrorCode = AuthErrorCode.ACCESS_DENIED
)
}
}
}
}
test("정상 응답") {
val (admin, token) = testAuthUtil.defaultStoreAdminLogin()
val schedule = initialize("삭제를 위한 일정 생성") {
dummyInitializer.createSchedule(storeId = admin.storeId!!)
}
runTest(
token = token,
on = {
delete("/admin/schedules/${schedule.id}")
},
expect = {
statusCode(HttpStatus.NO_CONTENT.value())
}
).also {
scheduleRepository.findByIdOrNull(schedule.id) shouldBe null
}
}
(ScheduleStatus.entries - listOf(ScheduleStatus.AVAILABLE, ScheduleStatus.BLOCKED)).forEach {
test("상태가 ${it}인 일정은 삭제할 수 없다.") {
val (admin, token) = testAuthUtil.defaultStoreAdminLogin()
val schedule = initialize("삭제를 위한 일정 생성") {
dummyInitializer.createSchedule(storeId = admin.storeId!!, status = it)
}
runExceptionTest(
token = token,
method = HttpMethod.DELETE,
endpoint = "/admin/schedules/${schedule.id}",
expectedErrorCode = ScheduleErrorCode.SCHEDULE_IN_USE
)
}
}
}
}
}

View File

@ -1,93 +1,50 @@
package roomescape.schedule package roomescape.schedule
import io.kotest.matchers.date.shouldBeAfter
import io.kotest.matchers.nulls.shouldNotBeNull
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.CoreMatchers.notNullValue
import org.springframework.data.repository.findByIdOrNull import org.springframework.data.repository.findByIdOrNull
import org.springframework.http.HttpMethod import org.springframework.http.HttpMethod
import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus
import roomescape.admin.infrastructure.persistence.AdminPermissionLevel import roomescape.admin.infrastructure.persistence.AdminPermissionLevel
import roomescape.admin.infrastructure.persistence.AdminType
import roomescape.auth.exception.AuthErrorCode import roomescape.auth.exception.AuthErrorCode
import roomescape.schedule.exception.ScheduleErrorCode import roomescape.schedule.exception.ScheduleErrorCode
import roomescape.schedule.infrastructure.persistence.ScheduleEntity
import roomescape.schedule.infrastructure.persistence.ScheduleRepository import roomescape.schedule.infrastructure.persistence.ScheduleRepository
import roomescape.schedule.infrastructure.persistence.ScheduleStatus import roomescape.schedule.infrastructure.persistence.ScheduleStatus
import roomescape.schedule.web.ScheduleUpdateRequest
import roomescape.supports.* import roomescape.supports.*
import roomescape.supports.ScheduleFixture.createRequest
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalTime import java.time.LocalTime
class ScheduleApiTest( class ScheduleApiTest(
private val scheduleRepository: ScheduleRepository private val scheduleRepository: ScheduleRepository
) : FunSpecSpringbootTest() { ) : FunSpecSpringbootTest() {
init { init {
context("특정 매장 + 날짜의 일정 및 테마 정보를 조회한다.") {
context("특정 날짜에 예약 가능한 테마 목록을 조회한다.") {
val date = LocalDate.now().plusDays(1)
val endpoint = "/schedules/themes?date=$date"
test("정상 응답") {
val adminToken = testAuthUtil.defaultHqAdminLogin()
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("정상 응답") { test("정상 응답") {
val size = 2
val date = LocalDate.now().plusDays(1) val date = LocalDate.now().plusDays(1)
val adminToken = testAuthUtil.defaultHqAdminLogin() val store = dummyInitializer.createStore()
val createdSchedule = dummyInitializer.createSchedule( initialize("조회를 위한 같은 날짜의 ${size}개의 일정 생성") {
adminToken = adminToken, for (i in 1..size) {
request = createRequest.copy(date = date, time = LocalTime.now()) dummyInitializer.createSchedule(
) storeId = store.id,
request = ScheduleFixture.createRequest.copy(
for (i in 1..10) {
dummyInitializer.createSchedule(
adminToken = adminToken,
request = createRequest.copy(
date = date, date = date,
time = LocalTime.now().plusMinutes(i.toLong()), time = LocalTime.now().plusHours(i.toLong())
themeId = createdSchedule.themeId )
) )
) }
} }
runTest( runTest(
token = testAuthUtil.defaultUserLogin(),
on = { on = {
get("/schedules?date=$date&themeId=${createdSchedule.themeId}") get("/stores/${store.id}/schedules?date=${date}")
}, },
expect = { expect = {
statusCode(HttpStatus.OK.value()) statusCode(HttpStatus.OK.value())
body("data.schedules.size()", equalTo(11)) body("data.schedules.size()", equalTo(size))
assertProperties( assertProperties(
props = setOf("id", "time", "status"), props = setOf("id", "startFrom", "endAt", "themeId", "themeName", "themeDifficulty", "status"),
propsNameIfList = "schedules" propsNameIfList = "schedules"
) )
} }
@ -95,507 +52,71 @@ class ScheduleApiTest(
} }
} }
context("관리자 페이지에서 특정 일정의 감사 정보를 조회한다.") { context("일정을 ${ScheduleStatus.HOLD} 상태로 변경한다.") {
context("권한이 없으면 접근할 수 없다.") { context("권한이 없으면 접근할 수 없다.") {
val endpoint = "/schedules/${INVALID_PK}"
test("비회원") { test("비회원") {
runExceptionTest( runExceptionTest(
method = HttpMethod.GET, method = HttpMethod.POST,
endpoint = endpoint, endpoint = "/schedules/${INVALID_PK}/hold",
expectedErrorCode = AuthErrorCode.TOKEN_NOT_FOUND expectedErrorCode = AuthErrorCode.TOKEN_NOT_FOUND
) )
} }
test("회원") { context("관리자") {
runExceptionTest( AdminPermissionLevel.entries.flatMap { permission ->
token = testAuthUtil.defaultUserLogin(), listOf(
method = HttpMethod.GET, AdminType.STORE to permission,
endpoint = endpoint, AdminType.HQ to permission,
expectedErrorCode = AuthErrorCode.ACCESS_DENIED )
) }.forEach { (type, permission) ->
} val admin = AdminFixture.create(
type = type,
permissionLevel = permission
)
test("권한이 ${AdminPermissionLevel.READ_SUMMARY}인 관리자") { test("type=${type} / permission=${permission}") {
val admin = AdminFixture.create(permissionLevel = AdminPermissionLevel.READ_SUMMARY) runExceptionTest(
token = testAuthUtil.adminLogin(admin).second,
runExceptionTest( method = HttpMethod.POST,
token = testAuthUtil.adminLogin(admin), endpoint = "/schedules/${INVALID_PK}/hold",
method = HttpMethod.GET, expectedErrorCode = AuthErrorCode.ACCESS_DENIED
endpoint = endpoint, )
expectedErrorCode = AuthErrorCode.ACCESS_DENIED }
) }
} }
} }
test("정상 응답") { test("정상 응답") {
val token = testAuthUtil.defaultHqAdminLogin() val schedule = dummyInitializer.createSchedule()
val createdSchedule = dummyInitializer.createSchedule(
adminToken = token,
request = createRequest
)
runTest( runTest(
token = token, token = testAuthUtil.defaultUserLogin(),
on = { on = {
get("/schedules/${createdSchedule.id}") post("/schedules/${schedule.id}/hold")
}, },
expect = { expect = {
statusCode(HttpStatus.OK.value()) statusCode(HttpStatus.OK.value())
body("data.id", equalTo(createdSchedule.id))
assertProperties(
props = setOf(
"id", "date", "time", "status",
"createdAt", "createdBy", "updatedAt", "updatedBy",
)
)
} }
).also { ).also {
it.extract().path<String>("data.createdAt") shouldNotBeNull {} val updated = scheduleRepository.findByIdOrNull(schedule.id)!!
it.extract().path<LinkedHashMap<String, Any>>("data.createdBy") shouldNotBeNull {} updated.status shouldBe ScheduleStatus.HOLD
it.extract().path<String>("data.updatedAt") shouldNotBeNull {}
it.extract().path<LinkedHashMap<String, Any>>("data.updatedBy") shouldNotBeNull {}
} }
} }
test("일정이 없으면 실패한다.") { context("일정이 ${ScheduleStatus.AVAILABLE}이 아니면 실패한다.") {
runExceptionTest( (ScheduleStatus.entries - ScheduleStatus.AVAILABLE).forEach {
token = testAuthUtil.defaultHqAdminLogin(), test("${it}") {
method = HttpMethod.GET, val schedule = dummyInitializer.createSchedule(status = it)
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( runExceptionTest(
token = testAuthUtil.adminLogin(admin), token = testAuthUtil.defaultUserLogin(),
method = HttpMethod.POST, method = HttpMethod.POST,
requestBody = createRequest, endpoint = "/schedules/${schedule.id}/hold",
endpoint = endpoint, expectedErrorCode = ScheduleErrorCode.SCHEDULE_NOT_AVAILABLE
expectedErrorCode = AuthErrorCode.ACCESS_DENIED
) )
} }
} }
} }
test("정상 생성 및 감사 정보 확인") {
val token = testAuthUtil.defaultHqAdminLogin()
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.defaultHqAdminLogin()
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.defaultHqAdminLogin()
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.defaultHqAdminLogin(),
method = HttpMethod.PATCH,
endpoint = endpoint,
expectedErrorCode = AuthErrorCode.ACCESS_DENIED
)
}
}
test("해당 일정이 ${ScheduleStatus.AVAILABLE} 상태이면 정상 응답") {
val createdSchedule: ScheduleEntity = dummyInitializer.createSchedule(
adminToken = testAuthUtil.defaultHqAdminLogin(),
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.defaultHqAdminLogin()
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.defaultHqAdminLogin(),
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.defaultHqAdminLogin()
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.defaultHqAdminLogin(),
method = HttpMethod.PATCH,
requestBody = updateRequest,
endpoint = "/schedules/${INVALID_PK}",
expectedErrorCode = ScheduleErrorCode.SCHEDULE_NOT_FOUND
)
}
test("입력된 날짜 + 시간이 현재 시간 이전이면 실패한다.") {
val token = testAuthUtil.defaultHqAdminLogin()
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.defaultHqAdminLogin()
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.defaultHqAdminLogin()
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
)
}
} }
} }
} }