diff --git a/src/test/kotlin/roomescape/store/AdminStoreApiTest.kt b/src/test/kotlin/roomescape/store/AdminStoreApiTest.kt new file mode 100644 index 00000000..2417311d --- /dev/null +++ b/src/test/kotlin/roomescape/store/AdminStoreApiTest.kt @@ -0,0 +1,519 @@ +package roomescape.store + +import io.kotest.assertions.assertSoftly +import io.kotest.matchers.date.shouldBeAfter +import io.kotest.matchers.shouldBe +import org.springframework.data.repository.findByIdOrNull +import org.springframework.http.HttpMethod +import org.springframework.http.HttpStatus +import roomescape.admin.infrastructure.persistence.AdminEntity +import roomescape.admin.infrastructure.persistence.AdminPermissionLevel +import roomescape.admin.infrastructure.persistence.AdminType +import roomescape.auth.exception.AuthErrorCode +import roomescape.store.exception.StoreErrorCode +import roomescape.store.infrastructure.persistence.StoreEntity +import roomescape.store.infrastructure.persistence.StoreRepository +import roomescape.store.infrastructure.persistence.StoreStatus +import roomescape.store.web.StoreUpdateRequest +import roomescape.supports.* + +class AdminStoreApiTest( + private val storeRepository: StoreRepository, +) : FunSpecSpringbootTest() { + + init { + context("매장 상세 정보를 조회한다.") { + context("권한이 없으면 접근할 수 없다.") { + test("비회원") { + runExceptionTest( + method = HttpMethod.GET, + endpoint = "/admin/stores/${INVALID_PK}/detail", + expectedErrorCode = AuthErrorCode.TOKEN_NOT_FOUND + ) + } + + test("회원") { + runExceptionTest( + token = testAuthUtil.defaultUserLogin().second, + method = HttpMethod.GET, + endpoint = "/admin/stores/${INVALID_PK}/detail", + expectedErrorCode = AuthErrorCode.ACCESS_DENIED + ) + } + + context("관리자") { + AdminPermissionLevel.entries.flatMap { + if (it == AdminPermissionLevel.READ_SUMMARY) { + listOf(AdminType.HQ to it, AdminType.STORE to it) + } else { + listOf(AdminType.STORE to it) + } + }.forEach { + test("type: ${it.first} / permission: ${it.second}") { + val admin = AdminFixture.create( + type = it.first, permissionLevel = it.second + ) + runExceptionTest( + token = testAuthUtil.adminLogin(admin).second, + method = HttpMethod.GET, + endpoint = "/admin/stores/${admin.storeId}/detail", + expectedErrorCode = AuthErrorCode.ACCESS_DENIED + ) + } + } + } + } + + test("정상 응답") { + val (admin, token) = testAuthUtil.defaultHqAdminLogin() + val store = initialize("상세 조회를 위한 매장 생성") { + dummyInitializer.createStore() + } + + runTest( + token = token, + on = { + get("/admin/stores/${store.id}/detail") + }, + expect = { + statusCode(HttpStatus.OK.value()) + assertProperties( + props = setOf("id", "name", "address", "contact", "businessRegNum", "region", "audit") + ) + assertProperties( + props = setOf("code", "sidoName", "sigunguName"), + propsNameIfList = "region" + ) + assertProperties( + props = setOf("createdAt", "createdBy", "updatedAt", "updatedBy"), + propsNameIfList = "audit" + ) + } + ) + } + + test("매장 정보가 없으면 실패한다.") { + runExceptionTest( + token = testAuthUtil.defaultHqAdminLogin().second, + method = HttpMethod.GET, + endpoint = "/admin/stores/${INVALID_PK}/detail", + expectedErrorCode = StoreErrorCode.STORE_NOT_FOUND + ) + } + } + + context("매장을 등록한다.") { + context("권한이 없으면 접근할 수 없다.") { + test("비회원") { + runExceptionTest( + method = HttpMethod.POST, + endpoint = "/admin/stores", + requestBody = StoreFixture.registerRequest, + expectedErrorCode = AuthErrorCode.TOKEN_NOT_FOUND + ) + } + + test("회원") { + runExceptionTest( + token = testAuthUtil.defaultUserLogin().second, + method = HttpMethod.POST, + endpoint = "/admin/stores", + requestBody = StoreFixture.registerRequest, + expectedErrorCode = AuthErrorCode.ACCESS_DENIED + ) + } + + context("관리자") { + AdminPermissionLevel.entries.flatMap { + if (it == AdminPermissionLevel.READ_SUMMARY || it == AdminPermissionLevel.READ_ALL) { + listOf(AdminType.HQ to it, AdminType.STORE to it) + } else { + listOf(AdminType.STORE to it) + } + }.forEach { + test("type: ${it.first} / permission: ${it.second}") { + val admin = AdminFixture.create( + type = it.first, permissionLevel = it.second + ) + runExceptionTest( + token = testAuthUtil.adminLogin(admin).second, + method = HttpMethod.POST, + endpoint = "/admin/stores", + requestBody = StoreFixture.registerRequest, + expectedErrorCode = AuthErrorCode.ACCESS_DENIED + ) + } + } + } + } + + test("정상 응답") { + val (admin, token) = testAuthUtil.defaultHqAdminLogin() + val registerRequest = StoreFixture.registerRequest + + runTest( + token = token, + using = { + body(registerRequest) + }, + on = { + post("/admin/stores") + }, + expect = { + statusCode(HttpStatus.OK.value()) + } + ).also { + assertSoftly(storeRepository.findByIdOrNull(it.extract().path("data.id"))!!) { + this.name shouldBe registerRequest.name + this.createdBy shouldBe admin.id + this.updatedBy shouldBe admin.id + } + } + } + + test("이름이 같은 매장이 있으면 실패한다.") { + val (admin, token) = testAuthUtil.defaultHqAdminLogin() + val store = initialize("중복 테스트를 위한 매장 등록") { + dummyInitializer.createStore() + } + + runExceptionTest( + token = token, + method = HttpMethod.POST, + endpoint = "/admin/stores", + requestBody = StoreFixture.registerRequest.copy(name = store.name), + expectedErrorCode = StoreErrorCode.STORE_NAME_DUPLICATED + ) + } + + test("연락처가 같은 매장이 있으면 실패한다.") { + val (admin, token) = testAuthUtil.defaultHqAdminLogin() + val store = initialize("중복 테스트를 위한 매장 등록") { + dummyInitializer.createStore() + } + + runExceptionTest( + token = token, + method = HttpMethod.POST, + endpoint = "/admin/stores", + requestBody = StoreFixture.registerRequest.copy(contact = store.contact), + expectedErrorCode = StoreErrorCode.STORE_CONTACT_DUPLICATED + ) + } + + test("주소가 같은 매장이 있으면 실패한다.") { + val (admin, token) = testAuthUtil.defaultHqAdminLogin() + val store = initialize("중복 테스트를 위한 매장 등록") { + dummyInitializer.createStore() + } + + runExceptionTest( + token = token, + method = HttpMethod.POST, + endpoint = "/admin/stores", + requestBody = StoreFixture.registerRequest.copy(address = store.address), + expectedErrorCode = StoreErrorCode.STORE_ADDRESS_DUPLICATED + ) + } + + test("사업자번호가 같은 매장이 있으면 실패한다.") { + val (admin, token) = testAuthUtil.defaultHqAdminLogin() + val store = initialize("중복 테스트를 위한 매장 등록") { + dummyInitializer.createStore() + } + + runExceptionTest( + token = token, + method = HttpMethod.POST, + endpoint = "/admin/stores", + requestBody = StoreFixture.registerRequest.copy(businessRegNum = store.businessRegNum), + expectedErrorCode = StoreErrorCode.STORE_BUSINESS_REG_NUM_DUPLICATED + ) + } + } + + context("매장 정보를 수정한다.") { + context("권한이 없으면 접근할 수 없다.") { + test("비회원") { + runExceptionTest( + method = HttpMethod.PATCH, + endpoint = "/admin/stores/${INVALID_PK}", + requestBody = StoreUpdateRequest(), + expectedErrorCode = AuthErrorCode.TOKEN_NOT_FOUND + ) + } + + test("회원") { + runExceptionTest( + token = testAuthUtil.defaultUserLogin().second, + method = HttpMethod.PATCH, + endpoint = "/admin/stores/${INVALID_PK}", + requestBody = StoreUpdateRequest(), + expectedErrorCode = AuthErrorCode.ACCESS_DENIED + ) + } + + context("관리자") { + AdminPermissionLevel.entries.flatMap { + if (it == AdminPermissionLevel.READ_SUMMARY || it == AdminPermissionLevel.READ_ALL) { + listOf(AdminType.HQ to it, AdminType.STORE to it) + } else { + listOf(AdminType.HQ to it) + } + }.forEach { + test("type: ${it.first} / permission: ${it.second}") { + val admin = AdminFixture.create( + type = it.first, permissionLevel = it.second + ) + runExceptionTest( + token = testAuthUtil.adminLogin(admin).second, + method = HttpMethod.PATCH, + endpoint = "/admin/stores/${INVALID_PK}", + requestBody = StoreUpdateRequest(), + expectedErrorCode = AuthErrorCode.ACCESS_DENIED + ) + } + } + } + } + + context("정상 응답") { + lateinit var admin: AdminEntity + lateinit var token: String + lateinit var store: StoreEntity + + beforeTest { + val adminTokenPair = testAuthUtil.defaultStoreAdminLogin() + admin = adminTokenPair.first + token = adminTokenPair.second + store = dummyInitializer.createStore() + } + + test("이름만 수정한다.") { + runTest( + token = token, + using = { + body(StoreUpdateRequest(name = "안녕하세요")) + }, + on = { + patch("/admin/stores/${store.id}") + }, + expect = { + statusCode(HttpStatus.OK.value()) + } + ).also { + assertSoftly(storeRepository.findByIdOrNull(store.id)!!) { + this.name shouldBe "안녕하세요" + this.address shouldBe store.address + this.contact shouldBe store.contact + this.updatedAt shouldBeAfter store.updatedAt + } + } + } + + test("연락처만 수정한다.") { + val newContact = randomPhoneNumber() + + runTest( + token = token, + using = { + body(StoreUpdateRequest(contact = newContact)) + }, + on = { + patch("/admin/stores/${store.id}") + }, + expect = { + statusCode(HttpStatus.OK.value()) + } + ).also { + assertSoftly(storeRepository.findByIdOrNull(store.id)!!) { + this.name shouldBe store.name + this.address shouldBe store.address + this.contact shouldBe newContact + this.updatedAt shouldBeAfter store.updatedAt + } + } + } + + test("주소만 수정한다.") { + val newAddress = randomString() + runTest( + token = token, + using = { + body(StoreUpdateRequest(address = newAddress)) + }, + on = { + patch("/admin/stores/${store.id}") + }, + expect = { + statusCode(HttpStatus.OK.value()) + } + ).also { + assertSoftly(storeRepository.findByIdOrNull(store.id)!!) { + this.name shouldBe store.name + this.address shouldBe newAddress + this.contact shouldBe store.contact + this.updatedAt shouldBeAfter store.updatedAt + } + } + } + + test("아무 값도 입력하지 않으면 바뀌지 않는다.") { + runTest( + token = token, + using = { + body(StoreUpdateRequest()) + }, + on = { + patch("/admin/stores/${store.id}") + }, + expect = { + statusCode(HttpStatus.OK.value()) + } + ).also { + assertSoftly(storeRepository.findByIdOrNull(store.id)!!) { + this.name shouldBe store.name + this.address shouldBe store.address + this.contact shouldBe store.contact + this.updatedAt shouldBe store.updatedAt + } + } + } + } + + context("실패 응답") { + lateinit var admin: AdminEntity + lateinit var token: String + lateinit var store: StoreEntity + + beforeTest { + val adminTokenPair = testAuthUtil.defaultStoreAdminLogin() + admin = adminTokenPair.first + token = adminTokenPair.second + store = dummyInitializer.createStore() + } + + test("매장이 없으면 실패한다.") { + runExceptionTest( + token = token, + method = HttpMethod.PATCH, + endpoint = "/admin/stores/${INVALID_PK}", + requestBody = StoreUpdateRequest(), + expectedErrorCode = StoreErrorCode.STORE_NOT_FOUND + ) + } + + test("이름이 같은 매장이 있으면 실패한다.") { + val newStore = dummyInitializer.createStore() + + runExceptionTest( + token = token, + method = HttpMethod.PATCH, + endpoint = "/admin/stores/${store.id}", + requestBody = StoreUpdateRequest(name = newStore.name), + expectedErrorCode = StoreErrorCode.STORE_NAME_DUPLICATED + ) + } + + test("연락처가 같은 매장이 있으면 실패한다.") { + val newStore = dummyInitializer.createStore() + + runExceptionTest( + token = token, + method = HttpMethod.PATCH, + endpoint = "/admin/stores/${store.id}", + requestBody = StoreUpdateRequest(contact = newStore.contact), + expectedErrorCode = StoreErrorCode.STORE_CONTACT_DUPLICATED + ) + } + + test("주소가 같은 매장이 있으면 실패한다.") { + val newStore = dummyInitializer.createStore() + + runExceptionTest( + token = token, + method = HttpMethod.PATCH, + endpoint = "/admin/stores/${store.id}", + requestBody = StoreUpdateRequest(address = newStore.address), + expectedErrorCode = StoreErrorCode.STORE_ADDRESS_DUPLICATED + ) + } + } + } + + context("매장을 비활성화한다.") { + context("권한이 없으면 접근할 수 없다.") { + test("비회원") { + runExceptionTest( + method = HttpMethod.POST, + endpoint = "/admin/stores/${INVALID_PK}/disable", + expectedErrorCode = AuthErrorCode.TOKEN_NOT_FOUND + ) + } + + test("회원") { + runExceptionTest( + token = testAuthUtil.defaultUserLogin().second, + method = HttpMethod.POST, + endpoint = "/admin/stores/${INVALID_PK}/disable", + expectedErrorCode = AuthErrorCode.ACCESS_DENIED + ) + } + + context("관리자") { + AdminPermissionLevel.entries.flatMap { + if (it == AdminPermissionLevel.READ_SUMMARY || it == AdminPermissionLevel.READ_ALL) { + listOf(AdminType.HQ to it, AdminType.STORE to it) + } else { + listOf(AdminType.STORE to it) + } + }.forEach { + test("type: ${it.first} / permission: ${it.second}") { + val admin = AdminFixture.create( + type = it.first, permissionLevel = it.second + ) + runExceptionTest( + token = testAuthUtil.adminLogin(admin).second, + method = HttpMethod.POST, + endpoint = "/admin/stores/${INVALID_PK}/disable", + requestBody = StoreUpdateRequest(), + expectedErrorCode = AuthErrorCode.ACCESS_DENIED + ) + } + } + } + } + + test("정상 응답") { + val (admin, token) = testAuthUtil.defaultHqAdminLogin() + val storeId = dummyInitializer.createStore().id + + runTest( + token = token, + on = { + post("/admin/stores/${storeId}/disable") + }, + expect = { + statusCode(HttpStatus.OK.value()) + } + ).also { + assertSoftly(storeRepository.findByIdOrNull(storeId)!!) { + this.status shouldBe StoreStatus.DISABLED + this.updatedBy shouldBe admin.id + } + } + } + + test("매장이 없으면 실패한다.") { + val (admin, token) = testAuthUtil.defaultHqAdminLogin() + + runExceptionTest( + token = token, + method = HttpMethod.POST, + endpoint = "/admin/stores/${INVALID_PK}/disable", + expectedErrorCode = StoreErrorCode.STORE_NOT_FOUND + ) + } + } + } +} diff --git a/src/test/kotlin/roomescape/store/StoreApiTest.kt b/src/test/kotlin/roomescape/store/StoreApiTest.kt new file mode 100644 index 00000000..006c94fc --- /dev/null +++ b/src/test/kotlin/roomescape/store/StoreApiTest.kt @@ -0,0 +1,104 @@ +package roomescape.store + +import org.hamcrest.CoreMatchers.equalTo +import org.springframework.http.HttpMethod +import org.springframework.http.HttpStatus +import roomescape.store.exception.StoreErrorCode +import roomescape.store.infrastructure.persistence.StoreEntity +import roomescape.supports.* + +class StoreApiTest: FunSpecSpringbootTest() { + + init { + context("모든 매장의 id / 이름을 조회한다.") { + context("정상 응답") { + lateinit var stores: List + + beforeTest { + stores = initialize("서울에 2개 매장, 부산에 1개 매장 생성") { + listOf( + dummyInitializer.createStore(regionCode = "1111000000"), + dummyInitializer.createStore(regionCode = "1114000000"), + dummyInitializer.createStore(regionCode = "2611000000") + ) + } + } + + test("지역 정보를 입력하지 않으면 전체 매장을 조회한다.") { + runTest( + on = { + get("/stores") + }, + expect = { + statusCode(HttpStatus.OK.value()) + body("data.stores.size()", equalTo(stores.size)) + } + ) + } + + test("시/도 로만 조회한다.") { + val sidoCode = "11" + runTest( + on = { + get("/stores?sido=${sidoCode}") + }, + expect = { + statusCode(HttpStatus.OK.value()) + body("data.stores.size()", equalTo(2)) + } + ) + } + + test("시/도 + 시/군/구로 조회한다.") { + val sidoCode = "11" + val sigunguCode = "110" + runTest( + on = { + get("/stores?sido=${sidoCode}&sigungu=${sigunguCode}") + }, + expect = { + statusCode(HttpStatus.OK.value()) + body("data.stores.size()", equalTo(1)) + } + ) + } + } + + test("시/도 입력 없이 시/군/구 만 입력하면 실패한다.") { + runExceptionTest( + method = HttpMethod.GET, + endpoint = "/stores?sigungu=110", + expectedErrorCode = StoreErrorCode.SIDO_CODE_REQUIRED + ) + } + } + + context("개별 매장 정보를 조회한다.") { + test("정상 응답") { + val store = initialize("조회용 매장 생성") { + dummyInitializer.createStore() + } + + runTest( + on = { + get("/stores/${store.id}") + }, + expect = { + statusCode(HttpStatus.OK.value()) + assertProperties( + props = setOf("id", "name", "address", "contact", "businessRegNum") + ) + } + ) + } + + test("매장이 없으면 실패한다.") { + runExceptionTest( + method = HttpMethod.GET, + endpoint = "/stores/${INVALID_PK}", + expectedErrorCode = StoreErrorCode.STORE_NOT_FOUND + ) + } + } + } +}