package roomescape.theme 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.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.supports.* import roomescape.supports.ThemeFixture.createRequest import roomescape.theme.business.MIN_DURATION import roomescape.theme.business.MIN_PARTICIPANTS import roomescape.theme.business.MIN_PRICE import roomescape.theme.exception.ThemeErrorCode import roomescape.theme.infrastructure.persistence.ThemeEntity import roomescape.theme.infrastructure.persistence.ThemeRepository import roomescape.theme.web.ThemeUpdateRequest class HQAdminThemeApiTest( private val themeRepository: ThemeRepository ) : FunSpecSpringbootTest() { init { context("테마를 생성한다.") { val endpoint = "/admin/themes" context("권한이 없으면 접근할 수 없다.") { 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 ) } AdminPermissionLevel.entries.forEach { test("관리자: Type=${AdminType.STORE} / Permission=${it}") { val admin = AdminFixture.createStoreAdmin(permissionLevel = it) runExceptionTest( token = testAuthUtil.adminLogin(admin), method = HttpMethod.POST, requestBody = createRequest, endpoint = endpoint, expectedErrorCode = AuthErrorCode.ACCESS_DENIED ) } if (it == AdminPermissionLevel.READ_ALL || it == AdminPermissionLevel.READ_SUMMARY) { test("관리자: Type=${AdminType.HQ} / Permission=${it}") { val admin = AdminFixture.createHqAdmin(permissionLevel = it) runExceptionTest( token = testAuthUtil.adminLogin(admin), method = HttpMethod.POST, requestBody = createRequest, endpoint = endpoint, expectedErrorCode = AuthErrorCode.ACCESS_DENIED ) } } } } test("정상 생성 및 감사 정보 확인") { runTest( token = testAuthUtil.defaultHqAdminLogin(), using = { body(createRequest) }, on = { post(endpoint) }, expect = { statusCode(HttpStatus.CREATED.value()) } ).also { val createdThemeId: Long = it.extract().path("data.id") val createdTheme: ThemeEntity = themeRepository.findByIdOrNull(createdThemeId) ?: throw AssertionError("Unexpected Exception Occurred.") createdTheme.name shouldBe createRequest.name createdTheme.createdAt shouldNotBeNull {} createdTheme.createdBy shouldNotBeNull {} createdTheme.updatedAt shouldNotBeNull {} createdTheme.updatedBy shouldNotBeNull {} } } test("이미 동일한 이름의 테마가 있으면 실패한다.") { val token = testAuthUtil.defaultHqAdminLogin() val alreadyExistsName: String = initialize("테스트를 위한 테마 생성 및 이름 반환") { dummyInitializer.createTheme(token, createRequest).name } runTest( token = token, using = { body(createRequest.copy(name = alreadyExistsName)) }, on = { post(endpoint) }, expect = { statusCode(HttpStatus.BAD_REQUEST.value()) body("code", equalTo(ThemeErrorCode.THEME_NAME_DUPLICATED.errorCode)) } ) } test("금액이 ${MIN_PRICE}원 미만이면 실패한다.") { val token = testAuthUtil.defaultHqAdminLogin() runTest( token = token, using = { body(createRequest.copy(price = (MIN_PRICE - 1))) }, on = { post(endpoint) }, expect = { statusCode(HttpStatus.BAD_REQUEST.value()) body("code", equalTo(ThemeErrorCode.PRICE_BELOW_MINIMUM.errorCode)) } ) } context("입력된 시간이 ${MIN_DURATION}분 미만이면 실패한다.") { test("field: availableMinutes") { runExceptionTest( token = testAuthUtil.defaultHqAdminLogin(), method = HttpMethod.POST, endpoint = endpoint, requestBody = createRequest.copy(availableMinutes = (MIN_DURATION - 1).toShort()), expectedErrorCode = ThemeErrorCode.DURATION_BELOW_MINIMUM ) } test("field: expectedMinutesFrom") { runExceptionTest( token = testAuthUtil.defaultHqAdminLogin(), method = HttpMethod.POST, endpoint = endpoint, requestBody = createRequest.copy(expectedMinutesFrom = (MIN_DURATION - 1).toShort()), expectedErrorCode = ThemeErrorCode.DURATION_BELOW_MINIMUM ) } test("field: expectedMinutesTo") { runExceptionTest( token = testAuthUtil.defaultHqAdminLogin(), method = HttpMethod.POST, endpoint = endpoint, requestBody = createRequest.copy(expectedMinutesTo = (MIN_DURATION - 1).toShort()), expectedErrorCode = ThemeErrorCode.DURATION_BELOW_MINIMUM ) } } context("시간 범위가 잘못 지정되면 실패한다.") { test("최소 예상 시간 > 최대 예상 시간") { runExceptionTest( token = testAuthUtil.defaultHqAdminLogin(), method = HttpMethod.POST, endpoint = endpoint, requestBody = createRequest.copy(expectedMinutesFrom = 100, expectedMinutesTo = 99), expectedErrorCode = ThemeErrorCode.MIN_EXPECTED_TIME_EXCEEDS_MAX_EXPECTED_TIME ) } test("최대 예상 시간 > 이용 가능 시간") { runExceptionTest( token = testAuthUtil.defaultHqAdminLogin(), method = HttpMethod.POST, endpoint = endpoint, requestBody = createRequest.copy( availableMinutes = 100, expectedMinutesFrom = 101, expectedMinutesTo = 101 ), expectedErrorCode = ThemeErrorCode.EXPECTED_TIME_EXCEEDS_AVAILABLE_TIME ) } } context("입력된 인원이 ${MIN_PARTICIPANTS}명 미만이면 실패한다.") { test("field: minParticipants") { runExceptionTest( token = testAuthUtil.defaultHqAdminLogin(), method = HttpMethod.POST, endpoint = endpoint, requestBody = createRequest.copy(minParticipants = (MIN_PARTICIPANTS - 1).toShort()), expectedErrorCode = ThemeErrorCode.PARTICIPANT_BELOW_MINIMUM ) } test("field: maxParticipants") { runExceptionTest( token = testAuthUtil.defaultHqAdminLogin(), method = HttpMethod.POST, endpoint = endpoint, requestBody = createRequest.copy(maxParticipants = (MIN_PARTICIPANTS - 1).toShort()), expectedErrorCode = ThemeErrorCode.PARTICIPANT_BELOW_MINIMUM ) } } context("인원 범위가 잘못 지정되면 실패한다.") { test("최소 인원 > 최대 인원") { runExceptionTest( token = testAuthUtil.defaultHqAdminLogin(), method = HttpMethod.POST, endpoint = endpoint, requestBody = createRequest.copy(minParticipants = 10, maxParticipants = 9), expectedErrorCode = ThemeErrorCode.MIN_PARTICIPANT_EXCEEDS_MAX_PARTICIPANT ) } } } context("테마 요약 목록을 조회한다.") { val endpoint = "/admin/themes" context("권한이 없으면 접근할 수 없다.") { 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 ) } AdminPermissionLevel.entries.forEach { test("관리자: Type=${AdminType.STORE} / Permission=${it}") { val admin = AdminFixture.createStoreAdmin(permissionLevel = it) runExceptionTest( token = testAuthUtil.adminLogin(admin), method = HttpMethod.POST, requestBody = createRequest, endpoint = endpoint, expectedErrorCode = AuthErrorCode.ACCESS_DENIED ) } } } test("정상 응답") { val token = testAuthUtil.defaultHqAdminLogin() val themes: List = initialize("Active 상태인 테마 1개 / Inactive 상태인 테마 2개 생성") { listOf( dummyInitializer.createTheme(token, createRequest.copy(name = "active-1", isActive = true)), dummyInitializer.createTheme(token, createRequest.copy(name = "inactive-1", isActive = false)), dummyInitializer.createTheme(token, createRequest.copy(name = "inactive-2", isActive = false)) ) } runTest( token = token, on = { get(endpoint) }, expect = { body("data.themes.size()", equalTo(themes.size)) assertProperties( props = setOf("id", "name", "difficulty", "price", "isActive"), propsNameIfList = "themes", ) } ) } } context("관리자 페이지에서 특정 테마의 상세 정보를 조회한다.") { context("권한이 없으면 접근할 수 없다.") { val endpoint = "/admin/themes/$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 ) } AdminPermissionLevel.entries.forEach { test("관리자: Type=${AdminType.STORE} / Permission=${it}") { val admin = AdminFixture.createStoreAdmin(permissionLevel = it) runExceptionTest( token = testAuthUtil.adminLogin(admin), method = HttpMethod.GET, endpoint = endpoint, expectedErrorCode = AuthErrorCode.ACCESS_DENIED ) } if (it == AdminPermissionLevel.READ_SUMMARY) { test("관리자: Type=${AdminType.HQ} / Permission=${it}") { val admin = AdminFixture.createHqAdmin(permissionLevel = it) runExceptionTest( token = testAuthUtil.adminLogin(admin), method = HttpMethod.GET, endpoint = endpoint, expectedErrorCode = AuthErrorCode.ACCESS_DENIED ) } } } } test("정상 응답") { val token = testAuthUtil.defaultHqAdminLogin() val createdTheme = initialize("테스트를 위한 테마 생성") { dummyInitializer.createTheme(token, createRequest) } runTest( token = token, on = { get("/admin/themes/${createdTheme.id}") }, expect = { statusCode(HttpStatus.OK.value()) body("data.id", equalTo(createdTheme.id)) assertProperties( props = setOf( "id", "name", "description", "thumbnailUrl", "difficulty", "price", "isActive", "minParticipants", "maxParticipants", "availableMinutes", "expectedMinutesFrom", "expectedMinutesTo", "createdAt", "createdBy", "updatedAt", "updatedBy" ) ) } ) } test("테마가 없으면 실패한다.") { runExceptionTest( token = testAuthUtil.defaultHqAdminLogin(), method = HttpMethod.GET, endpoint = "/admin/themes/$INVALID_PK", expectedErrorCode = ThemeErrorCode.THEME_NOT_FOUND ) } } context("테마를 삭제한다.") { context("권한이 없으면 접근할 수 없다.") { val endpoint = "/admin/themes/${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 ) } AdminPermissionLevel.entries.forEach { test("관리자: Type=${AdminType.STORE} / Permission=${it}") { val admin = AdminFixture.createStoreAdmin(permissionLevel = it) runExceptionTest( token = testAuthUtil.adminLogin(admin), method = HttpMethod.DELETE, endpoint = endpoint, expectedErrorCode = AuthErrorCode.ACCESS_DENIED ) } if (it == AdminPermissionLevel.READ_ALL || it == AdminPermissionLevel.READ_SUMMARY) { test("관리자: Type=${AdminType.HQ} / Permission=${it}") { val admin = AdminFixture.createHqAdmin(permissionLevel = it) runExceptionTest( token = testAuthUtil.adminLogin(admin), method = HttpMethod.DELETE, endpoint = endpoint, expectedErrorCode = AuthErrorCode.ACCESS_DENIED ) } } } } test("정상 삭제") { val token = testAuthUtil.defaultHqAdminLogin() val createdTheme = initialize("테스트를 위한 테마 생성") { dummyInitializer.createTheme(token, createRequest) } runTest( token = token, on = { delete("/admin/themes/${createdTheme.id}") }, expect = { statusCode(HttpStatus.NO_CONTENT.value()) } ).also { themeRepository.findByIdOrNull(createdTheme.id) shouldBe null } } test("테마가 없으면 실패한다.") { runExceptionTest( token = testAuthUtil.defaultHqAdminLogin(), method = HttpMethod.DELETE, endpoint = "/admin/themes/$INVALID_PK", expectedErrorCode = ThemeErrorCode.THEME_NOT_FOUND ) } } context("테마를 수정한다.") { context("권한이 없으면 접근할 수 없다.") { val endpoint = "/admin/themes/${INVALID_PK}" val request = ThemeUpdateRequest(name = "hello") test("비회원") { runExceptionTest( method = HttpMethod.PATCH, endpoint = endpoint, requestBody = request, expectedErrorCode = AuthErrorCode.TOKEN_NOT_FOUND ) } test("회원") { runExceptionTest( token = testAuthUtil.defaultUserLogin(), method = HttpMethod.PATCH, endpoint = endpoint, requestBody = request, expectedErrorCode = AuthErrorCode.ACCESS_DENIED ) } AdminPermissionLevel.entries.forEach { test("관리자: Type=${AdminType.STORE} / Permission=${it}") { val admin = AdminFixture.createStoreAdmin(permissionLevel = it) runExceptionTest( token = testAuthUtil.adminLogin(admin), method = HttpMethod.PATCH, endpoint = endpoint, requestBody = request, expectedErrorCode = AuthErrorCode.ACCESS_DENIED ) } if (it == AdminPermissionLevel.READ_ALL || it == AdminPermissionLevel.READ_SUMMARY) { test("관리자: Type=${AdminType.HQ} / Permission=${it}") { val admin = AdminFixture.createHqAdmin(permissionLevel = it) runExceptionTest( token = testAuthUtil.adminLogin(admin), method = HttpMethod.PATCH, endpoint = endpoint, requestBody = request, expectedErrorCode = AuthErrorCode.ACCESS_DENIED ) } } } } val updateRequest = ThemeUpdateRequest(name = "modified") test("정상 수정 및 감사 정보 변경 확인") { val createdTheme = initialize("테스트를 위한 관리자1의 테마 생성") { dummyInitializer.createTheme(testAuthUtil.defaultHqAdminLogin(), createRequest) } val otherAdminToken: String = initialize("감사 정보 변경 확인을 위한 관리자2 로그인") { testAuthUtil.adminLogin( AdminFixture.createHqAdmin(permissionLevel = AdminPermissionLevel.WRITABLE) ) } runTest( token = otherAdminToken, using = { body(updateRequest) }, on = { patch("/admin/themes/${createdTheme.id}") }, expect = { statusCode(HttpStatus.OK.value()) } ).also { val updatedTheme = themeRepository.findByIdOrNull(createdTheme.id)!! updatedTheme.id shouldBe createdTheme.id updatedTheme.name shouldBe updateRequest.name updatedTheme.updatedBy shouldNotBe createdTheme.updatedBy updatedTheme.updatedAt shouldBeAfter createdTheme.updatedAt } } test("입력값이 없으면 수정하지 않는다.") { val token = testAuthUtil.defaultHqAdminLogin() val createdTheme = initialize("테스트를 위한 테마 생성") { dummyInitializer.createTheme(token, createRequest) } runTest( token = token, using = { body(ThemeUpdateRequest()) }, on = { patch("/admin/themes/${createdTheme.id}") }, expect = { statusCode(HttpStatus.OK.value()) } ).also { val updatedTheme = themeRepository.findByIdOrNull(createdTheme.id)!! updatedTheme.id shouldBe createdTheme.id updatedTheme.updatedAt shouldBe createdTheme.updatedAt } } test("테마가 없으면 실패한다.") { runExceptionTest( token = testAuthUtil.defaultHqAdminLogin(), method = HttpMethod.PATCH, endpoint = "/admin/themes/$INVALID_PK", requestBody = updateRequest, expectedErrorCode = ThemeErrorCode.THEME_NOT_FOUND ) } test("금액이 ${MIN_PRICE}원 미만이면 실패한다.") { val token = testAuthUtil.defaultHqAdminLogin() val createdTheme = initialize("테스트를 위한 테마 생성") { dummyInitializer.createTheme(token, createRequest) } runExceptionTest( token = token, method = HttpMethod.PATCH, endpoint = "/admin/themes/${createdTheme.id}", requestBody = updateRequest.copy(price = (MIN_PRICE - 1)), expectedErrorCode = ThemeErrorCode.PRICE_BELOW_MINIMUM ) } context("입력된 시간이 ${MIN_DURATION}분 미만이면 실패한다.") { test("field: availableMinutes") { val token = testAuthUtil.defaultHqAdminLogin() val createdTheme = initialize("테스트를 위한 테마 생성") { dummyInitializer.createTheme(token, createRequest) } runExceptionTest( token = token, method = HttpMethod.PATCH, endpoint = "/admin/themes/${createdTheme.id}", requestBody = updateRequest.copy(availableMinutes = (MIN_DURATION - 1).toShort()), expectedErrorCode = ThemeErrorCode.DURATION_BELOW_MINIMUM ) } test("field: expectedMinutesFrom") { val token = testAuthUtil.defaultHqAdminLogin() val createdTheme = initialize("테스트를 위한 테마 생성") { dummyInitializer.createTheme(token, createRequest) } runExceptionTest( token = token, method = HttpMethod.PATCH, endpoint = "/admin/themes/${createdTheme.id}", requestBody = updateRequest.copy(expectedMinutesFrom = (MIN_DURATION - 1).toShort()), expectedErrorCode = ThemeErrorCode.DURATION_BELOW_MINIMUM ) } test("field: expectedMinutesTo") { val token = testAuthUtil.defaultHqAdminLogin() val createdTheme = initialize("테스트를 위한 테마 생성") { dummyInitializer.createTheme(token, createRequest) } runExceptionTest( token = token, method = HttpMethod.PATCH, endpoint = "/admin/themes/${createdTheme.id}", requestBody = updateRequest.copy(expectedMinutesTo = (MIN_DURATION - 1).toShort()), expectedErrorCode = ThemeErrorCode.DURATION_BELOW_MINIMUM ) } } context("시간 범위가 잘못 지정되면 실패한다.") { test("최소 예상 시간 > 최대 예상 시간") { val token = testAuthUtil.defaultHqAdminLogin() val createdTheme = initialize("테스트를 위한 테마 생성") { dummyInitializer.createTheme(token, createRequest) } runExceptionTest( token = token, method = HttpMethod.PATCH, endpoint = "/admin/themes/${createdTheme.id}", requestBody = updateRequest.copy(expectedMinutesFrom = 100, expectedMinutesTo = 99), expectedErrorCode = ThemeErrorCode.MIN_EXPECTED_TIME_EXCEEDS_MAX_EXPECTED_TIME ) } test("최대 예상 시간 > 이용 가능 시간") { val token = testAuthUtil.defaultHqAdminLogin() val createdTheme = initialize("테스트를 위한 테마 생성") { dummyInitializer.createTheme(token, createRequest) } val requestBody = updateRequest.copy( availableMinutes = 100, expectedMinutesFrom = 101, expectedMinutesTo = 101 ) runExceptionTest( token = token, method = HttpMethod.PATCH, endpoint = "/admin/themes/${createdTheme.id}", requestBody = requestBody, expectedErrorCode = ThemeErrorCode.EXPECTED_TIME_EXCEEDS_AVAILABLE_TIME ) } } context("입력된 인원이 ${MIN_PARTICIPANTS}명 미만이면 실패한다.") { test("field: minParticipants") { val token = testAuthUtil.defaultHqAdminLogin() val createdTheme = initialize("테스트를 위한 테마 생성") { dummyInitializer.createTheme(token, createRequest) } runExceptionTest( token = token, method = HttpMethod.PATCH, endpoint = "/admin/themes/${createdTheme.id}", requestBody = updateRequest.copy(minParticipants = (MIN_PARTICIPANTS - 1).toShort()), expectedErrorCode = ThemeErrorCode.PARTICIPANT_BELOW_MINIMUM ) } test("field: maxParticipants") { val token = testAuthUtil.defaultHqAdminLogin() val createdTheme = initialize("테스트를 위한 테마 생성") { dummyInitializer.createTheme(token, createRequest) } runExceptionTest( token = token, method = HttpMethod.PATCH, endpoint = "/admin/themes/${createdTheme.id}", requestBody = updateRequest.copy(maxParticipants = (MIN_PARTICIPANTS - 1).toShort()), expectedErrorCode = ThemeErrorCode.PARTICIPANT_BELOW_MINIMUM ) } } context("인원 범위가 잘못 지정되면 실패한다.") { test("최소 인원 > 최대 인원") { val token = testAuthUtil.defaultHqAdminLogin() val createdTheme = initialize("테스트를 위한 테마 생성") { dummyInitializer.createTheme(token, createRequest) } runExceptionTest( token = token, method = HttpMethod.PATCH, endpoint = "/admin/themes/${createdTheme.id}", requestBody = updateRequest.copy(minParticipants = 10, maxParticipants = 9), expectedErrorCode = ThemeErrorCode.MIN_PARTICIPANT_EXCEEDS_MAX_PARTICIPANT ) } } } } }