diff --git a/src/test/kotlin/roomescape/schedule/AdminScheduleApiTest.kt b/src/test/kotlin/roomescape/schedule/AdminScheduleApiTest.kt new file mode 100644 index 00000000..2a953750 --- /dev/null +++ b/src/test/kotlin/roomescape/schedule/AdminScheduleApiTest.kt @@ -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 + 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 = + 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 + ) + } + } + } + } +} diff --git a/src/test/kotlin/roomescape/schedule/ScheduleApiTest.kt b/src/test/kotlin/roomescape/schedule/ScheduleApiTest.kt index e4d0fb0b..b74060aa 100644 --- a/src/test/kotlin/roomescape/schedule/ScheduleApiTest.kt +++ b/src/test/kotlin/roomescape/schedule/ScheduleApiTest.kt @@ -1,93 +1,50 @@ 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.admin.infrastructure.persistence.AdminType 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.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("동일한 날짜, 테마에 대한 모든 시간을 조회한다.") { - + context("특정 매장 + 날짜의 일정 및 테마 정보를 조회한다.") { test("정상 응답") { + val size = 2 val date = LocalDate.now().plusDays(1) - val adminToken = testAuthUtil.defaultHqAdminLogin() - 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( + val store = dummyInitializer.createStore() + initialize("조회를 위한 같은 날짜의 ${size}개의 일정 생성") { + for (i in 1..size) { + dummyInitializer.createSchedule( + storeId = store.id, + request = ScheduleFixture.createRequest.copy( date = date, - time = LocalTime.now().plusMinutes(i.toLong()), - themeId = createdSchedule.themeId + time = LocalTime.now().plusHours(i.toLong()) + ) ) - ) + } } runTest( - token = testAuthUtil.defaultUserLogin(), on = { - get("/schedules?date=$date&themeId=${createdSchedule.themeId}") + get("/stores/${store.id}/schedules?date=${date}") }, expect = { statusCode(HttpStatus.OK.value()) - body("data.schedules.size()", equalTo(11)) + body("data.schedules.size()", equalTo(size)) assertProperties( - props = setOf("id", "time", "status"), + props = setOf("id", "startFrom", "endAt", "themeId", "themeName", "themeDifficulty", "status"), propsNameIfList = "schedules" ) } @@ -95,507 +52,71 @@ class ScheduleApiTest( } } - context("관리자 페이지에서 특정 일정의 감사 정보를 조회한다.") { + context("일정을 ${ScheduleStatus.HOLD} 상태로 변경한다.") { context("권한이 없으면 접근할 수 없다.") { - val endpoint = "/schedules/${INVALID_PK}" - test("비회원") { runExceptionTest( - method = HttpMethod.GET, - endpoint = endpoint, + method = HttpMethod.POST, + endpoint = "/schedules/${INVALID_PK}/hold", expectedErrorCode = AuthErrorCode.TOKEN_NOT_FOUND ) } - test("회원") { - runExceptionTest( - token = testAuthUtil.defaultUserLogin(), - method = HttpMethod.GET, - endpoint = endpoint, - expectedErrorCode = AuthErrorCode.ACCESS_DENIED - ) - } + context("관리자") { + AdminPermissionLevel.entries.flatMap { permission -> + listOf( + AdminType.STORE to permission, + AdminType.HQ to permission, + ) + }.forEach { (type, permission) -> + val admin = AdminFixture.create( + type = type, + permissionLevel = permission + ) - 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("type=${type} / permission=${permission}") { + runExceptionTest( + token = testAuthUtil.adminLogin(admin).second, + method = HttpMethod.POST, + endpoint = "/schedules/${INVALID_PK}/hold", + expectedErrorCode = AuthErrorCode.ACCESS_DENIED + ) + } + } } } test("정상 응답") { - val token = testAuthUtil.defaultHqAdminLogin() - - val createdSchedule = dummyInitializer.createSchedule( - adminToken = token, - request = createRequest - ) + val schedule = dummyInitializer.createSchedule() runTest( - token = token, + token = testAuthUtil.defaultUserLogin(), on = { - get("/schedules/${createdSchedule.id}") + post("/schedules/${schedule.id}/hold") }, 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("data.createdAt") shouldNotBeNull {} - it.extract().path>("data.createdBy") shouldNotBeNull {} - it.extract().path("data.updatedAt") shouldNotBeNull {} - it.extract().path>("data.updatedBy") shouldNotBeNull {} + val updated = scheduleRepository.findByIdOrNull(schedule.id)!! + updated.status shouldBe ScheduleStatus.HOLD } } - test("일정이 없으면 실패한다.") { - runExceptionTest( - token = testAuthUtil.defaultHqAdminLogin(), - 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) + context("일정이 ${ScheduleStatus.AVAILABLE}이 아니면 실패한다.") { + (ScheduleStatus.entries - ScheduleStatus.AVAILABLE).forEach { + test("${it}") { + val schedule = dummyInitializer.createSchedule(status = it) runExceptionTest( - token = testAuthUtil.adminLogin(admin), + token = testAuthUtil.defaultUserLogin(), method = HttpMethod.POST, - requestBody = createRequest, - endpoint = endpoint, - expectedErrorCode = AuthErrorCode.ACCESS_DENIED + endpoint = "/schedules/${schedule.id}/hold", + expectedErrorCode = ScheduleErrorCode.SCHEDULE_NOT_AVAILABLE ) } } } - - 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 - ) - } } } }