diff --git a/src/main/kotlin/com/petqua/application/cart/CartProductService.kt b/src/main/kotlin/com/petqua/application/cart/CartProductService.kt index ac73b256..ab46eba9 100644 --- a/src/main/kotlin/com/petqua/application/cart/CartProductService.kt +++ b/src/main/kotlin/com/petqua/application/cart/CartProductService.kt @@ -1,5 +1,6 @@ package com.petqua.application.cart +import com.petqua.application.cart.dto.CartProductResponse import com.petqua.application.cart.dto.DeleteCartProductCommand import com.petqua.application.cart.dto.SaveCartProductCommand import com.petqua.application.cart.dto.UpdateCartProductOptionCommand @@ -76,4 +77,10 @@ class CartProductService( cartProduct.validateOwner(command.memberId) cartProductRepository.delete(cartProduct) } + + @Transactional(readOnly = true) + fun readAll(memberId: Long): List { + memberRepository.existByIdOrThrow(memberId, MemberException(NOT_FOUND_MEMBER)) + return cartProductRepository.findAllCartResultsByMemberId(memberId) + } } diff --git a/src/main/kotlin/com/petqua/application/cart/dto/CartProductDtos.kt b/src/main/kotlin/com/petqua/application/cart/dto/CartProductDtos.kt index 21dacd89..0fd8906f 100644 --- a/src/main/kotlin/com/petqua/application/cart/dto/CartProductDtos.kt +++ b/src/main/kotlin/com/petqua/application/cart/dto/CartProductDtos.kt @@ -3,6 +3,7 @@ package com.petqua.application.cart.dto import com.petqua.domain.cart.CartProduct import com.petqua.domain.cart.CartProductQuantity import com.petqua.domain.cart.DeliveryMethod +import com.petqua.domain.product.Product data class SaveCartProductCommand( val memberId: Long, @@ -35,3 +36,34 @@ data class DeleteCartProductCommand( val memberId: Long, val cartProductId: Long, ) + +data class CartProductResponse( + val id: Long, + val storeName: String, + val productId: Long, + val productName: String, + val productThumbnailUrl: String, + val productPrice: Int, + val productDiscountRate: Int, + val productDiscountPrice: Int, + val quantity: Int, + val isMale: Boolean, + val deliveryMethod: String, + val isOnSale: Boolean, +) { + + constructor(cartProduct: CartProduct, product: Product?, storeName: String?) : this( + id = cartProduct.id, + storeName = storeName ?: "", + productId = product?.id ?: 0L, + productName = product?.name ?: "", + productThumbnailUrl = product?.thumbnailUrl ?: "", + productPrice = product?.price?.intValueExact() ?: 0, + productDiscountRate = product?.discountRate ?: 0, + productDiscountPrice = product?.discountPrice?.intValueExact() ?: 0, + quantity = cartProduct.quantity.value, + isMale = cartProduct.isMale, + deliveryMethod = cartProduct.deliveryMethod.name, + isOnSale = product != null + ) +} diff --git a/src/main/kotlin/com/petqua/common/util/EntityManagerUtils.kt b/src/main/kotlin/com/petqua/common/util/EntityManagerUtils.kt index e876cbe0..9464380f 100644 --- a/src/main/kotlin/com/petqua/common/util/EntityManagerUtils.kt +++ b/src/main/kotlin/com/petqua/common/util/EntityManagerUtils.kt @@ -29,6 +29,17 @@ inline fun EntityManager.createQuery( .resultList } +inline fun EntityManager.createQuery( + query: SelectQuery<*>, + context: JpqlRenderContext, + renderer: JpqlRenderer, +): List { + val rendered = renderer.render(query, context) + return this.createQuery(rendered.query, T::class.java) + .apply { rendered.params.forEach { (name, value) -> setParameter(name, value) } } + .resultList +} + inline fun EntityManager.createCountQuery( query: SelectQuery<*>, context: JpqlRenderContext, diff --git a/src/main/kotlin/com/petqua/domain/cart/CartProductCustomRepository.kt b/src/main/kotlin/com/petqua/domain/cart/CartProductCustomRepository.kt new file mode 100644 index 00000000..9efec488 --- /dev/null +++ b/src/main/kotlin/com/petqua/domain/cart/CartProductCustomRepository.kt @@ -0,0 +1,8 @@ +package com.petqua.domain.cart + +import com.petqua.application.cart.dto.CartProductResponse + +interface CartProductCustomRepository { + + fun findAllCartResultsByMemberId(memberId: Long): List +} diff --git a/src/main/kotlin/com/petqua/domain/cart/CartProductCustomRepositoryImpl.kt b/src/main/kotlin/com/petqua/domain/cart/CartProductCustomRepositoryImpl.kt new file mode 100644 index 00000000..f21f3999 --- /dev/null +++ b/src/main/kotlin/com/petqua/domain/cart/CartProductCustomRepositoryImpl.kt @@ -0,0 +1,43 @@ +package com.petqua.domain.cart + +import com.linecorp.kotlinjdsl.dsl.jpql.jpql +import com.linecorp.kotlinjdsl.render.jpql.JpqlRenderContext +import com.linecorp.kotlinjdsl.render.jpql.JpqlRenderer +import com.petqua.application.cart.dto.CartProductResponse +import com.petqua.common.util.createQuery +import com.petqua.domain.product.Product +import com.petqua.domain.store.Store +import jakarta.persistence.EntityManager +import org.springframework.stereotype.Repository + +@Repository +class CartProductCustomRepositoryImpl( + private val entityManager: EntityManager, + private val jpqlRenderContext: JpqlRenderContext, + private val jpqlRenderer: JpqlRenderer, +) : CartProductCustomRepository { + + override fun findAllCartResultsByMemberId(memberId: Long): List { + val query = jpql { + selectNew( + entity(CartProduct::class), + entity(Product::class), + path(Store::name) + ).from( + entity(CartProduct::class), + leftJoin(Product::class).on(path(CartProduct::productId).eq(path(Product::id))), + leftJoin(Store::class).on(path(Product::storeId).eq(path(Store::id))), + ).where( + path(CartProduct::memberId).eq(memberId) + ).orderBy( + path(CartProduct::createdAt).desc() + ) + } + + return entityManager.createQuery( + query, + jpqlRenderContext, + jpqlRenderer + ) + } +} diff --git a/src/main/kotlin/com/petqua/domain/cart/CartProductRepository.kt b/src/main/kotlin/com/petqua/domain/cart/CartProductRepository.kt index 58df2a84..96cb539f 100644 --- a/src/main/kotlin/com/petqua/domain/cart/CartProductRepository.kt +++ b/src/main/kotlin/com/petqua/domain/cart/CartProductRepository.kt @@ -2,7 +2,7 @@ package com.petqua.domain.cart import org.springframework.data.jpa.repository.JpaRepository -interface CartProductRepository : JpaRepository { +interface CartProductRepository : JpaRepository, CartProductCustomRepository { fun findByMemberIdAndProductIdAndIsMaleAndDeliveryMethod( memberId: Long, @@ -11,5 +11,5 @@ interface CartProductRepository : JpaRepository { deliveryMethod: DeliveryMethod ): CartProduct? - fun findAllByIdIn(ids: List): List + fun findAllByMemberId(id: Long): List } diff --git a/src/main/kotlin/com/petqua/domain/product/ProductCustomRepository.kt b/src/main/kotlin/com/petqua/domain/product/ProductCustomRepository.kt index 335725f1..6872c3db 100644 --- a/src/main/kotlin/com/petqua/domain/product/ProductCustomRepository.kt +++ b/src/main/kotlin/com/petqua/domain/product/ProductCustomRepository.kt @@ -9,4 +9,6 @@ interface ProductCustomRepository { fun findAllByCondition(condition: ProductReadCondition, paging: ProductPaging): List fun countByCondition(condition: ProductReadCondition): Int + + fun findAllProductResponseByIdIn(ids: List): List } diff --git a/src/main/kotlin/com/petqua/domain/product/ProductCustomRepositoryImpl.kt b/src/main/kotlin/com/petqua/domain/product/ProductCustomRepositoryImpl.kt index e2d4eed9..4acaa7cb 100644 --- a/src/main/kotlin/com/petqua/domain/product/ProductCustomRepositoryImpl.kt +++ b/src/main/kotlin/com/petqua/domain/product/ProductCustomRepositoryImpl.kt @@ -96,4 +96,26 @@ class ProductCustomRepositoryImpl( jpqlRenderer ) } + + override fun findAllProductResponseByIdIn(ids: List): List { + val query = jpql { + selectNew( + entity(Product::class), + path(Store::name) + ).from( + entity(Product::class), + join(Store::class).on(path(Product::storeId).eq(path(Store::id))), + ).where( + predicateByIds(ids) + ) + } + + return entityManager.createQuery( + query, + jpqlRenderContext, + jpqlRenderer + ) + } + + private fun Jpql.predicateByIds(ids: List) = if (ids.isEmpty()) null else path(Product::id).`in`(ids) } diff --git a/src/main/kotlin/com/petqua/presentation/cart/CartProductController.kt b/src/main/kotlin/com/petqua/presentation/cart/CartProductController.kt index 6fe74bc2..c5c4a697 100644 --- a/src/main/kotlin/com/petqua/presentation/cart/CartProductController.kt +++ b/src/main/kotlin/com/petqua/presentation/cart/CartProductController.kt @@ -2,6 +2,7 @@ package com.petqua.presentation.cart import com.petqua.application.cart.CartProductService import com.petqua.application.cart.dto.DeleteCartProductCommand +import com.petqua.application.cart.dto.CartProductResponse import com.petqua.application.product.dto.ProductDetailResponse import com.petqua.domain.auth.Auth import com.petqua.domain.auth.LoginMember @@ -9,6 +10,7 @@ import com.petqua.presentation.cart.dto.SaveCartProductRequest import com.petqua.presentation.cart.dto.UpdateCartProductOptionRequest import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PatchMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping @@ -60,4 +62,12 @@ class CartProductController( cartProductService.delete(command) return ResponseEntity.noContent().build() } + + @GetMapping + fun readAll( + @Auth loginMember: LoginMember, + ): ResponseEntity> { + val responses = cartProductService.readAll(loginMember.memberId) + return ResponseEntity.ok(responses) + } } diff --git a/src/test/kotlin/com/petqua/application/cart/CartProductServiceTest.kt b/src/test/kotlin/com/petqua/application/cart/CartProductServiceTest.kt index 287492e1..12a06db3 100644 --- a/src/test/kotlin/com/petqua/application/cart/CartProductServiceTest.kt +++ b/src/test/kotlin/com/petqua/application/cart/CartProductServiceTest.kt @@ -10,6 +10,7 @@ import com.petqua.domain.cart.DeliveryMethod.COMMON import com.petqua.domain.cart.DeliveryMethod.SAFETY import com.petqua.domain.member.MemberRepository import com.petqua.domain.product.ProductRepository +import com.petqua.domain.store.StoreRepository import com.petqua.exception.cart.CartProductException import com.petqua.exception.cart.CartProductExceptionType.DUPLICATED_PRODUCT import com.petqua.exception.cart.CartProductExceptionType.FORBIDDEN_CART_PRODUCT @@ -22,6 +23,7 @@ import com.petqua.test.DataCleaner import com.petqua.test.fixture.cartProduct import com.petqua.test.fixture.member import com.petqua.test.fixture.product +import com.petqua.test.fixture.store import io.kotest.assertions.assertSoftly import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.BehaviorSpec @@ -35,6 +37,7 @@ class CartProductServiceTest( private val cartProductRepository: CartProductRepository, private val productRepository: ProductRepository, private val memberRepository: MemberRepository, + private val storeRepository: StoreRepository, private val dataCleaner: DataCleaner, ) : BehaviorSpec({ @@ -173,16 +176,16 @@ class CartProductServiceTest( cartProduct( memberId = memberId, productId = productId, - isMale = true, - deliveryMethod = COMMON + isMale = false, + deliveryMethod = SAFETY ) ) val command = UpdateCartProductOptionCommand( cartProductId = cartProduct.id, memberId = memberId, quantity = CartProductQuantity(3), - isMale = true, - deliveryMethod = COMMON, + isMale = false, + deliveryMethod = SAFETY, ) Then("예외가 발생 한다") { shouldThrow { @@ -251,6 +254,58 @@ class CartProductServiceTest( } } + Given("봉달 상품 조회시") { + val store = storeRepository.save(store(name = "store")) + val productAId = productRepository.save(product(storeId = store.id)).id + val productBId = productRepository.save(product(storeId = store.id)).id + val productCId = productRepository.save(product(storeId = store.id)).id + val memberId = memberRepository.save(member()).id + cartProductRepository.saveAll( + listOf( + cartProduct(memberId = memberId, productId = productAId), + cartProduct(memberId = memberId, productId = productBId), + cartProduct(memberId = memberId, productId = productCId), + ) + ) + + When("봉달 상품이 있는 회원이 조회 하는 경우") { + val result = cartProductService.readAll(memberId) + + Then("봉달 상품 리스트를 반환 한다") { + result.size shouldBe 3 + } + } + + When("봉달 상품이 없는 회원이 조회 하는 경우") { + val newMemberId = memberRepository.save(member()).id + val results = cartProductService.readAll(newMemberId) + + Then("빈 리스트를 반환 한다") { + results.size shouldBe 0 + } + } + + When("봉달에 담아둔 상품이 삭제된 경우") { + productRepository.deleteById(productAId) + val results = cartProductService.readAll(memberId) + + Then("상품의 판매 여부를 포함한 리스트를 반환 한다") { + assertSoftly(results) { + size shouldBe 3 + find { it.productId == 0L }!!.isOnSale shouldBe false + } + } + } + + When("존재 하지 않는 회원이 조회 하는 경우") { + Then("예외가 발생 한다") { + shouldThrow { + cartProductService.readAll(Long.MIN_VALUE) + }.exceptionType() shouldBe NOT_FOUND_MEMBER + } + } + } + afterContainer { dataCleaner.clean() } diff --git a/src/test/kotlin/com/petqua/domain/product/ProductCustomRepositoryImplTest.kt b/src/test/kotlin/com/petqua/domain/product/ProductCustomRepositoryImplTest.kt index 8fdf3ae7..c61d96fb 100644 --- a/src/test/kotlin/com/petqua/domain/product/ProductCustomRepositoryImplTest.kt +++ b/src/test/kotlin/com/petqua/domain/product/ProductCustomRepositoryImplTest.kt @@ -177,6 +177,22 @@ class ProductCustomRepositoryImplTest( } } + Given("다중 id로 ProductResponse를 조회 할 때") { + val product1 = productRepository.save(product(name = "상품1", storeId = store.id)) + val product2 = productRepository.save(product(name = "상품2", storeId = store.id)) + + When("id 목록을 입력하면") { + val products = productRepository.findAllProductResponseByIdIn(listOf(product1.id, product2.id)) + + Then("해당 id의 ProductResponse를 반환한다") { + products shouldContainExactly listOf( + ProductResponse(product1, store.name), + ProductResponse(product2, store.name), + ) + } + } + } + afterContainer { dataCleaner.clean() } diff --git a/src/test/kotlin/com/petqua/presentation/cart/CartProductControllerSteps.kt b/src/test/kotlin/com/petqua/presentation/cart/CartProductControllerSteps.kt index 97f0598e..7448615b 100644 --- a/src/test/kotlin/com/petqua/presentation/cart/CartProductControllerSteps.kt +++ b/src/test/kotlin/com/petqua/presentation/cart/CartProductControllerSteps.kt @@ -77,3 +77,18 @@ fun requestDeleteCartProduct( response() } } + +fun requestReadAllCartProducts( + accessToken: String +): Response { + return Given { + log().all() + .header(HttpHeaders.AUTHORIZATION, accessToken) + } When { + get("/carts") + } Then { + log().all() + } Extract { + response() + } +} diff --git a/src/test/kotlin/com/petqua/presentation/cart/CartProductControllerTest.kt b/src/test/kotlin/com/petqua/presentation/cart/CartProductControllerTest.kt index 1b17816f..bce4d2c6 100644 --- a/src/test/kotlin/com/petqua/presentation/cart/CartProductControllerTest.kt +++ b/src/test/kotlin/com/petqua/presentation/cart/CartProductControllerTest.kt @@ -1,7 +1,9 @@ package com.petqua.presentation.cart +import com.petqua.application.cart.dto.CartProductResponse import com.petqua.common.exception.ExceptionResponse import com.petqua.domain.product.ProductRepository +import com.petqua.domain.store.StoreRepository import com.petqua.exception.cart.CartProductExceptionType.DUPLICATED_PRODUCT import com.petqua.exception.cart.CartProductExceptionType.FORBIDDEN_CART_PRODUCT import com.petqua.exception.cart.CartProductExceptionType.INVALID_DELIVERY_METHOD @@ -12,9 +14,13 @@ import com.petqua.presentation.cart.dto.SaveCartProductRequest import com.petqua.presentation.cart.dto.UpdateCartProductOptionRequest import com.petqua.test.ApiTestConfig import com.petqua.test.fixture.product -import org.assertj.core.api.Assertions.assertThat -import org.assertj.core.api.SoftAssertions.assertSoftly +import com.petqua.test.fixture.store +import io.kotest.assertions.assertSoftly +import io.kotest.matchers.collections.shouldContainAll +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain import org.springframework.http.HttpHeaders +import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus.BAD_REQUEST import org.springframework.http.HttpStatus.CREATED import org.springframework.http.HttpStatus.FORBIDDEN @@ -23,11 +29,13 @@ import org.springframework.http.HttpStatus.NO_CONTENT class CartProductControllerTest( private val productRepository: ProductRepository, + private val storeRepository: StoreRepository, ) : ApiTestConfig() { init { + val storeId = storeRepository.save(store()).id Given("봉달에 상품 저장을") { val memberAuthResponse = signInAsMember() - val savedProduct = productRepository.save(product(id = 1L)) + val savedProduct = productRepository.save(product(storeId = storeId)) val request = SaveCartProductRequest( productId = savedProduct.id, quantity = 1, @@ -38,9 +46,9 @@ class CartProductControllerTest( val response = requestSaveCartProduct(request, memberAuthResponse.accessToken) Then("봉달 목록에 상품이 저장된다") { - assertSoftly { - it.assertThat(response.statusCode).isEqualTo(CREATED.value()) - it.assertThat(response.header(HttpHeaders.LOCATION)).contains("/carts/items") + assertSoftly(response) { + statusCode shouldBe CREATED.value() + header(HttpHeaders.LOCATION) shouldContain "/carts/items/1" } } } @@ -48,7 +56,7 @@ class CartProductControllerTest( Given("봉달에 상품 저장 요청시") { val memberAuthResponse = signInAsMember() - val savedProduct = productRepository.save(product(id = 1L)) + val savedProduct = productRepository.save(product(storeId = storeId)) When("지원하지 않는 배송 방식으로 요청 하면") { val invalidDeliveryMethodRequest = SaveCartProductRequest( productId = savedProduct.id, @@ -60,9 +68,9 @@ class CartProductControllerTest( Then("예외가 발생한다") { val errorResponse = response.`as`(ExceptionResponse::class.java) - assertSoftly { - it.assertThat(response.statusCode).isEqualTo(BAD_REQUEST.value()) - it.assertThat(errorResponse.message).isEqualTo(INVALID_DELIVERY_METHOD.errorMessage()) + assertSoftly(response) { + statusCode shouldBe BAD_REQUEST.value() + errorResponse.message shouldBe INVALID_DELIVERY_METHOD.errorMessage() } } } @@ -78,9 +86,9 @@ class CartProductControllerTest( Then("예외가 발생한다") { val errorResponse = response.`as`(ExceptionResponse::class.java) - assertSoftly { - it.assertThat(response.statusCode).isEqualTo(NOT_FOUND.value()) - it.assertThat(errorResponse.message).isEqualTo(NOT_FOUND_PRODUCT.errorMessage()) + assertSoftly(response) { + statusCode shouldBe NOT_FOUND.value() + errorResponse.message shouldBe NOT_FOUND_PRODUCT.errorMessage() } } } @@ -96,9 +104,9 @@ class CartProductControllerTest( Then("예외가 발생한다") { val errorResponse = response.`as`(ExceptionResponse::class.java) - assertSoftly { - it.assertThat(response.statusCode).isEqualTo(BAD_REQUEST.value()) - it.assertThat(errorResponse.message).isEqualTo(PRODUCT_QUANTITY_OVER_MAXIMUM.errorMessage()) + assertSoftly(response) { + statusCode shouldBe BAD_REQUEST.value() + errorResponse.message shouldBe PRODUCT_QUANTITY_OVER_MAXIMUM.errorMessage() } } } @@ -116,16 +124,16 @@ class CartProductControllerTest( Then("예외가 발생한다") { val errorResponse = response.`as`(ExceptionResponse::class.java) - assertSoftly { - it.assertThat(response.statusCode).isEqualTo(BAD_REQUEST.value()) - it.assertThat(errorResponse.message).isEqualTo(DUPLICATED_PRODUCT.errorMessage()) + assertSoftly(response) { + statusCode shouldBe BAD_REQUEST.value() + errorResponse.message shouldBe DUPLICATED_PRODUCT.errorMessage() } } } } Given("봉달 상품의 옵션 수정을") { - val savedProduct = productRepository.save(product(id = 1L)) + val savedProduct = productRepository.save(product(storeId = storeId)) val memberAuthResponse = signInAsMember() val cartProductId = saveCartProductAndReturnId(memberAuthResponse.accessToken, savedProduct.id) @@ -143,13 +151,13 @@ class CartProductControllerTest( ) Then("봉달 상품의 옵션이 수정된다") { - assertThat(response.statusCode).isEqualTo(NO_CONTENT.value()) + response.statusCode shouldBe NO_CONTENT.value() } } } Given("봉달 상품의 옵션 수정시") { - val savedProduct = productRepository.save(product(id = 1L)) + val savedProduct = productRepository.save(product(storeId = storeId)) val memberAuthResponse = signInAsMember() val cartProductId = saveCartProductAndReturnId(memberAuthResponse.accessToken, savedProduct.id) @@ -168,9 +176,9 @@ class CartProductControllerTest( Then("예외가 발생한다") { val errorResponse = response.`as`(ExceptionResponse::class.java) - assertSoftly { - it.assertThat(response.statusCode).isEqualTo(NOT_FOUND.value()) - it.assertThat(errorResponse.message).isEqualTo(NOT_FOUND_CART_PRODUCT.errorMessage()) + assertSoftly(response) { + statusCode shouldBe NOT_FOUND.value() + errorResponse.message shouldBe NOT_FOUND_CART_PRODUCT.errorMessage() } } } @@ -190,9 +198,9 @@ class CartProductControllerTest( Then("예외가 발생한다") { val errorResponse = response.`as`(ExceptionResponse::class.java) - assertSoftly { - it.assertThat(response.statusCode).isEqualTo(BAD_REQUEST.value()) - it.assertThat(errorResponse.message).isEqualTo(PRODUCT_QUANTITY_OVER_MAXIMUM.errorMessage()) + assertSoftly(response) { + statusCode shouldBe BAD_REQUEST.value() + errorResponse.message shouldBe PRODUCT_QUANTITY_OVER_MAXIMUM.errorMessage() } } } @@ -212,9 +220,9 @@ class CartProductControllerTest( Then("예외가 발생한다") { val errorResponse = response.`as`(ExceptionResponse::class.java) - assertSoftly { - it.assertThat(response.statusCode).isEqualTo(BAD_REQUEST.value()) - it.assertThat(errorResponse.message).isEqualTo(INVALID_DELIVERY_METHOD.errorMessage()) + assertSoftly(response) { + statusCode shouldBe BAD_REQUEST.value() + errorResponse.message shouldBe INVALID_DELIVERY_METHOD.errorMessage() } } } @@ -235,9 +243,9 @@ class CartProductControllerTest( Then("예외가 발생한다") { val errorResponse = response.`as`(ExceptionResponse::class.java) - assertSoftly { - it.assertThat(response.statusCode).isEqualTo(FORBIDDEN.value()) - it.assertThat(errorResponse.message).isEqualTo(FORBIDDEN_CART_PRODUCT.errorMessage()) + assertSoftly(response) { + statusCode shouldBe FORBIDDEN.value() + errorResponse.message shouldBe FORBIDDEN_CART_PRODUCT.errorMessage() } } } @@ -266,16 +274,16 @@ class CartProductControllerTest( Then("예외가 발생한다") { val errorResponse = response.`as`(ExceptionResponse::class.java) - assertSoftly { - it.assertThat(response.statusCode).isEqualTo(BAD_REQUEST.value()) - it.assertThat(errorResponse.message).isEqualTo(DUPLICATED_PRODUCT.errorMessage()) + assertSoftly(response) { + statusCode shouldBe BAD_REQUEST.value() + errorResponse.message shouldBe DUPLICATED_PRODUCT.errorMessage() } } } } Given("봉달 상품 삭제를") { - val product = productRepository.save(product(id = 1L)) + val product = productRepository.save(product(storeId = storeId)) val memberAuthResponse = signInAsMember() val cartProductAId = saveCartProductAndReturnId(memberAuthResponse.accessToken, product.id) @@ -287,13 +295,13 @@ class CartProductControllerTest( ) Then("봉달 상품이 삭제된다") { - assertThat(response.statusCode).isEqualTo(NO_CONTENT.value()) + response.statusCode shouldBe NO_CONTENT.value() } } } Given("봉달 상품 삭제시") { - val product = productRepository.save(product(id = 1L)) + val product = productRepository.save(product(storeId = storeId)) val memberAuthResponse = signInAsMember() val cartProductAId = saveCartProductAndReturnId(memberAuthResponse.accessToken, product.id) @@ -305,9 +313,9 @@ class CartProductControllerTest( Then("예외가 발생한다") { val errorResponse = response.`as`(ExceptionResponse::class.java) - assertSoftly { - it.assertThat(response.statusCode).isEqualTo(NOT_FOUND.value()) - it.assertThat(errorResponse.message).isEqualTo(NOT_FOUND_CART_PRODUCT.errorMessage()) + assertSoftly(response) { + statusCode shouldBe NOT_FOUND.value() + errorResponse.message shouldBe NOT_FOUND_CART_PRODUCT.errorMessage() } } } @@ -321,9 +329,62 @@ class CartProductControllerTest( Then("예외가 발생한다") { val errorResponse = response.`as`(ExceptionResponse::class.java) - assertSoftly { - it.assertThat(response.statusCode).isEqualTo(FORBIDDEN.value()) - it.assertThat(errorResponse.message).isEqualTo(FORBIDDEN_CART_PRODUCT.errorMessage()) + assertSoftly(response) { + statusCode shouldBe FORBIDDEN.value() + errorResponse.message shouldBe FORBIDDEN_CART_PRODUCT.errorMessage() + } + } + } + } + + Given("봉달 목록 전체 조회를") { + val productA = productRepository.save(product(name = "쿠아1", storeId = storeId)) + val productB = productRepository.save(product(name = "쿠아2", storeId = storeId)) + val productC = productRepository.save(product(name = "쿠아3", storeId = storeId)) + val memberAuthResponse = signInAsMember() + saveCartProductAndReturnId(memberAuthResponse.accessToken, productA.id) + saveCartProductAndReturnId(memberAuthResponse.accessToken, productB.id) + saveCartProductAndReturnId(memberAuthResponse.accessToken, productC.id) + + When("요청 하면") { + val response = requestReadAllCartProducts(memberAuthResponse.accessToken) + + Then("봉달 목록이 조회된다") { + val responseBody = response.`as`(Array::class.java) + + assertSoftly(response) { + statusCode shouldBe HttpStatus.OK.value() + responseBody.size shouldBe 3 + responseBody.map { it.productName }.toList() shouldContainAll listOf("쿠아1", "쿠아2", "쿠아3") + } + } + } + + When("봉달 목록이 없으면") { + val otherMemberResponse = signInAsMember() + val response = requestReadAllCartProducts(otherMemberResponse.accessToken) + + Then("빈 목록이 조회된다") { + val responseBody = response.`as`(Array::class.java) + + assertSoftly(response) { + statusCode shouldBe HttpStatus.OK.value() + responseBody shouldBe emptyArray() + } + } + } + + When("봉달에 담은 상품이 삭제 되면") { + productRepository.delete(productA) + val response = requestReadAllCartProducts(memberAuthResponse.accessToken) + + Then("삭제된 상품은 구매 불가능 하도록 조회된다") { + val responseBody = response.`as`(Array::class.java) + + assertSoftly(response) { + statusCode shouldBe HttpStatus.OK.value() + responseBody.size shouldBe 3 + responseBody.find { it.productId == 0L }!!.isOnSale shouldBe false } } } diff --git a/src/test/kotlin/com/petqua/presentation/product/ProductControllerTest.kt b/src/test/kotlin/com/petqua/presentation/product/ProductControllerTest.kt index 5b1a2f85..b2735b2c 100644 --- a/src/test/kotlin/com/petqua/presentation/product/ProductControllerTest.kt +++ b/src/test/kotlin/com/petqua/presentation/product/ProductControllerTest.kt @@ -10,12 +10,10 @@ import com.petqua.domain.product.Sorter.SALE_PRICE_DESC import com.petqua.domain.product.dto.ProductResponse import com.petqua.domain.recommendation.ProductRecommendationRepository import com.petqua.domain.store.StoreRepository -import com.petqua.test.DataCleaner +import com.petqua.test.ApiTestConfig import com.petqua.test.fixture.product import com.petqua.test.fixture.productRecommendation import com.petqua.test.fixture.store -import io.kotest.core.spec.style.BehaviorSpec -import io.restassured.RestAssured import io.restassured.module.kotlin.extensions.Extract import io.restassured.module.kotlin.extensions.Given import io.restassured.module.kotlin.extensions.Then @@ -23,324 +21,316 @@ import io.restassured.module.kotlin.extensions.When import java.math.BigDecimal import kotlin.Long.Companion.MIN_VALUE import org.assertj.core.api.SoftAssertions.assertSoftly -import org.springframework.boot.test.context.SpringBootTest -import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT -import org.springframework.boot.test.web.server.LocalServerPort import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus.BAD_REQUEST import org.springframework.http.HttpStatus.NOT_FOUND -@SpringBootTest(webEnvironment = RANDOM_PORT) class ProductControllerTest( - @LocalServerPort private val port: Int, private val productRepository: ProductRepository, private val storeRepository: StoreRepository, private val recommendationRepository: ProductRecommendationRepository, - private val dataCleaner: DataCleaner, -) : BehaviorSpec({ - - RestAssured.port = port - val store = storeRepository.save(store()) - - Given("개별 상품을 조회할 때") { - val productId = productRepository.save(product(storeId = store.id)).id - - When("상품 ID를 입력하면") { - val response = Given { - log().all() - pathParam("productId", productId) - } When { - get("/products/{productId}") - } Then { - log().all() - } Extract { - response() - } +) : ApiTestConfig() { - Then("해당 ID의 상품이 반환된다") { - val productDetailResponse = response.`as`(ProductDetailResponse::class.java) + init { + val store = storeRepository.save(store()) - assertSoftly { - it.assertThat(response.statusCode).isEqualTo(HttpStatus.OK.value()) - it.assertThat(productDetailResponse).isEqualTo( - ProductDetailResponse( - product = product(id = productId, storeId = store.id), - storeName = store.name, - reviewAverageScore = 0.0 - ) - ) - } - } - } + Given("개별 상품을 조회할 때") { + val productId = productRepository.save(product(storeId = store.id)).id - When("존재하지 않는 상품 ID를 입력하면") { - - Then("예외가 발생한다") { - Given { + When("상품 ID를 입력하면") { + val response = Given { log().all() - pathParam("productId", MIN_VALUE) + pathParam("productId", productId) } When { get("/products/{productId}") } Then { log().all() - statusCode(NOT_FOUND.value()) + } Extract { + response() + } + + Then("해당 ID의 상품이 반환된다") { + val productDetailResponse = response.`as`(ProductDetailResponse::class.java) + + assertSoftly { + it.assertThat(response.statusCode).isEqualTo(HttpStatus.OK.value()) + it.assertThat(productDetailResponse).isEqualTo( + ProductDetailResponse( + product = product(id = productId, storeId = store.id), + storeName = store.name, + reviewAverageScore = 0.0 + ) + ) + } + } + } + + When("존재하지 않는 상품 ID를 입력하면") { + + Then("예외가 발생한다") { + Given { + log().all() + pathParam("productId", MIN_VALUE) + } When { + get("/products/{productId}") + } Then { + log().all() + statusCode(NOT_FOUND.value()) + } } } } - } - Given("조건에 따라 상품을 조회할 때") { - val product1 = productRepository.save( - product( - name = "상품1", - storeId = store.id, - discountPrice = BigDecimal.ZERO, - reviewCount = 0, - reviewTotalScore = 0 + Given("조건에 따라 상품을 조회할 때") { + val product1 = productRepository.save( + product( + name = "상품1", + storeId = store.id, + discountPrice = BigDecimal.ZERO, + reviewCount = 0, + reviewTotalScore = 0 + ) ) - ) - val product2 = productRepository.save( - product( - name = "상품2", - storeId = store.id, - discountPrice = BigDecimal.ONE, - reviewCount = 1, - reviewTotalScore = 1 + val product2 = productRepository.save( + product( + name = "상품2", + storeId = store.id, + discountPrice = BigDecimal.ONE, + reviewCount = 1, + reviewTotalScore = 1 + ) ) - ) - val product3 = productRepository.save( - product( - name = "상품3", - storeId = store.id, - discountPrice = BigDecimal.ONE, - reviewCount = 1, - reviewTotalScore = 5 + val product3 = productRepository.save( + product( + name = "상품3", + storeId = store.id, + discountPrice = BigDecimal.ONE, + reviewCount = 1, + reviewTotalScore = 5 + ) ) - ) - val product4 = productRepository.save( - product( - name = "상품4", - storeId = store.id, - discountPrice = BigDecimal.TEN, - reviewCount = 2, - reviewTotalScore = 10 + val product4 = productRepository.save( + product( + name = "상품4", + storeId = store.id, + discountPrice = BigDecimal.TEN, + reviewCount = 2, + reviewTotalScore = 10 + ) ) - ) - - recommendationRepository.save(productRecommendation(productId = product1.id)) - recommendationRepository.save(productRecommendation(productId = product2.id)) - - When("마지막으로 조회한 Id를 입력하면") { - val response = Given { - log().all() - param("lastViewedId", product4.id) - } When { - get("/products") - } Then { - log().all() - } Extract { - response() - } - Then("해당 ID의 다음 상품들이 최신 등록 순으로 반환된다") { - val productsResponse = response.`as`(ProductsResponse::class.java) - - assertSoftly { - it.assertThat(response.statusCode).isEqualTo(HttpStatus.OK.value()) - it.assertThat(productsResponse).isEqualTo( - ProductsResponse( - products = listOf( - ProductResponse(product3, store.name), - ProductResponse(product2, store.name), - ProductResponse(product1, store.name) - ), - hasNextPage = false, - totalProductsCount = 4 - ) - ) - } - } - } - - When("개수 제한을 입력하면") { - val response = Given { - log().all() - param("limit", 1) - } When { - get("/products") - } Then { - log().all() - } Extract { - response() - } + recommendationRepository.save(productRecommendation(productId = product1.id)) + recommendationRepository.save(productRecommendation(productId = product2.id)) - Then("해당 개수와 함께 다음 페이지가 존재하는지 여부가 반환된다") { - val productsResponse = response.`as`(ProductsResponse::class.java) + When("마지막으로 조회한 Id를 입력하면") { + val response = Given { + log().all() + param("lastViewedId", product4.id) + } When { + get("/products") + } Then { + log().all() + } Extract { + response() + } - assertSoftly { - it.assertThat(response.statusCode).isEqualTo(HttpStatus.OK.value()) - it.assertThat(productsResponse).isEqualTo( - ProductsResponse( - products = listOf(ProductResponse(product4, store.name)), - hasNextPage = true, - totalProductsCount = 4 + Then("해당 ID의 다음 상품들이 최신 등록 순으로 반환된다") { + val productsResponse = response.`as`(ProductsResponse::class.java) + + assertSoftly { + it.assertThat(response.statusCode).isEqualTo(HttpStatus.OK.value()) + it.assertThat(productsResponse).isEqualTo( + ProductsResponse( + products = listOf( + ProductResponse(product3, store.name), + ProductResponse(product2, store.name), + ProductResponse(product1, store.name) + ), + hasNextPage = false, + totalProductsCount = 4 + ) ) - ) + } } } - } - When("추천 조건으로 조회하면") { - val response = Given { - log().all() - param("sourceType", HOME_RECOMMENDED.name) - } When { - get("/products") - } Then { - log().all() - } Extract { - response() - } + When("개수 제한을 입력하면") { + val response = Given { + log().all() + param("limit", 1) + } When { + get("/products") + } Then { + log().all() + } Extract { + response() + } - Then("추천 상품들이, 최신 등록 순으로 반환된다") { - val productsResponse = response.`as`(ProductsResponse::class.java) - - assertSoftly { - it.assertThat(response.statusCode).isEqualTo(HttpStatus.OK.value()) - it.assertThat(productsResponse).isEqualTo( - ProductsResponse( - products = listOf( - ProductResponse(product2, store.name), - ProductResponse(product1, store.name) - ), - hasNextPage = false, - totalProductsCount = 2 + Then("해당 개수와 함께 다음 페이지가 존재하는지 여부가 반환된다") { + val productsResponse = response.`as`(ProductsResponse::class.java) + + assertSoftly { + it.assertThat(response.statusCode).isEqualTo(HttpStatus.OK.value()) + it.assertThat(productsResponse).isEqualTo( + ProductsResponse( + products = listOf(ProductResponse(product4, store.name)), + hasNextPage = true, + totalProductsCount = 4 + ) ) - ) + } } } - } - When("추천 조건으로, 가격 낮은 순으로 조회하면") { - val response = Given { - log().all() - params( - "sourceType", HOME_RECOMMENDED.name, - "sorter", SALE_PRICE_ASC.name - ) - } When { - get("/products") - } Then { - log().all() - } Extract { - response() - } + When("추천 조건으로 조회하면") { + val response = Given { + log().all() + param("sourceType", HOME_RECOMMENDED.name) + } When { + get("/products") + } Then { + log().all() + } Extract { + response() + } - Then("추천 상품들이, 가격 낮은 순으로 반환된다") { - val productsResponse = response.`as`(ProductsResponse::class.java) - - assertSoftly { - it.assertThat(response.statusCode).isEqualTo(HttpStatus.OK.value()) - it.assertThat(productsResponse).isEqualTo( - ProductsResponse( - products = listOf( - ProductResponse(product1, store.name), - ProductResponse(product2, store.name) - ), - hasNextPage = false, - totalProductsCount = 2 + Then("추천 상품들이, 최신 등록 순으로 반환된다") { + val productsResponse = response.`as`(ProductsResponse::class.java) + + assertSoftly { + it.assertThat(response.statusCode).isEqualTo(HttpStatus.OK.value()) + it.assertThat(productsResponse).isEqualTo( + ProductsResponse( + products = listOf( + ProductResponse(product2, store.name), + ProductResponse(product1, store.name) + ), + hasNextPage = false, + totalProductsCount = 2 + ) ) - ) + } } } - } - When("신규 입고 조건으로 조회하면") { - val response = Given { - log().all() - param("sourceType", HOME_NEW_ENROLLMENT.name) - } When { - get("/products") - } Then { - log().all() - } Extract { - response() - } + When("추천 조건으로, 가격 낮은 순으로 조회하면") { + val response = Given { + log().all() + params( + "sourceType", HOME_RECOMMENDED.name, + "sorter", SALE_PRICE_ASC.name + ) + } When { + get("/products") + } Then { + log().all() + } Extract { + response() + } - Then("신규 입고 상품들이 반환된다") { - val productsResponse = response.`as`(ProductsResponse::class.java) - - assertSoftly { - it.assertThat(response.statusCode).isEqualTo(HttpStatus.OK.value()) - it.assertThat(productsResponse).isEqualTo( - ProductsResponse( - products = listOf( - ProductResponse(product4, store.name), - ProductResponse(product3, store.name), - ProductResponse(product2, store.name), - ProductResponse(product1, store.name) - ), - hasNextPage = false, - totalProductsCount = 4 + Then("추천 상품들이, 가격 낮은 순으로 반환된다") { + val productsResponse = response.`as`(ProductsResponse::class.java) + + assertSoftly { + it.assertThat(response.statusCode).isEqualTo(HttpStatus.OK.value()) + it.assertThat(productsResponse).isEqualTo( + ProductsResponse( + products = listOf( + ProductResponse(product1, store.name), + ProductResponse(product2, store.name) + ), + hasNextPage = false, + totalProductsCount = 2 + ) ) - ) + } } } - } - When("신규 입고 조건으로, 가격 높은 순으로 조회하면") { - val response = Given { - log().all() - params( - "sourceType", HOME_NEW_ENROLLMENT.name, - "sorter", SALE_PRICE_DESC.name - ) - } When { - get("/products") - } Then { - log().all() - } Extract { - response() - } + When("신규 입고 조건으로 조회하면") { + val response = Given { + log().all() + param("sourceType", HOME_NEW_ENROLLMENT.name) + } When { + get("/products") + } Then { + log().all() + } Extract { + response() + } - Then("상품들이 최신 등록 순으로 반환된다") { - val productsResponse = response.`as`(ProductsResponse::class.java) - - assertSoftly { - it.assertThat(response.statusCode).isEqualTo(HttpStatus.OK.value()) - it.assertThat(productsResponse).isEqualTo( - ProductsResponse( - products = listOf( - ProductResponse(product4, store.name), - ProductResponse(product3, store.name), - ProductResponse(product2, store.name), - ProductResponse(product1, store.name) - ), - hasNextPage = false, - totalProductsCount = 4 + Then("신규 입고 상품들이 반환된다") { + val productsResponse = response.`as`(ProductsResponse::class.java) + + assertSoftly { + it.assertThat(response.statusCode).isEqualTo(HttpStatus.OK.value()) + it.assertThat(productsResponse).isEqualTo( + ProductsResponse( + products = listOf( + ProductResponse(product4, store.name), + ProductResponse(product3, store.name), + ProductResponse(product2, store.name), + ProductResponse(product1, store.name) + ), + hasNextPage = false, + totalProductsCount = 4 + ) ) - ) + } } } - } - - When("조건을 잘못 기입해서 조회하면") { - Then("예외가 발생한다") { - Given { + When("신규 입고 조건으로, 가격 높은 순으로 조회하면") { + val response = Given { log().all() - param("sourceType", "wrongType") + params( + "sourceType", HOME_NEW_ENROLLMENT.name, + "sorter", SALE_PRICE_DESC.name + ) } When { get("/products") } Then { log().all() - statusCode(BAD_REQUEST.value()) + } Extract { + response() + } + + Then("상품들이 최신 등록 순으로 반환된다") { + val productsResponse = response.`as`(ProductsResponse::class.java) + + assertSoftly { + it.assertThat(response.statusCode).isEqualTo(HttpStatus.OK.value()) + it.assertThat(productsResponse).isEqualTo( + ProductsResponse( + products = listOf( + ProductResponse(product4, store.name), + ProductResponse(product3, store.name), + ProductResponse(product2, store.name), + ProductResponse(product1, store.name) + ), + hasNextPage = false, + totalProductsCount = 4 + ) + ) + } + } + } + + When("조건을 잘못 기입해서 조회하면") { + + Then("예외가 발생한다") { + Given { + log().all() + param("sourceType", "wrongType") + } When { + get("/products") + } Then { + log().all() + statusCode(BAD_REQUEST.value()) + } } } } } +} - afterContainer { - dataCleaner.clean() - } -}) diff --git a/src/test/kotlin/com/petqua/test/fixture/CartProductFixtures.kt b/src/test/kotlin/com/petqua/test/fixture/CartProductFixtures.kt index 07db9c7e..3f1d71e8 100644 --- a/src/test/kotlin/com/petqua/test/fixture/CartProductFixtures.kt +++ b/src/test/kotlin/com/petqua/test/fixture/CartProductFixtures.kt @@ -5,7 +5,7 @@ import com.petqua.domain.cart.CartProductQuantity import com.petqua.domain.cart.DeliveryMethod fun cartProduct( - id: Long = 1L, + id: Long = 0L, memberId: Long = 1L, productId: Long = 1L, quantity: Int = 1, diff --git a/src/test/kotlin/com/petqua/test/fixture/MemberFixtures.kt b/src/test/kotlin/com/petqua/test/fixture/MemberFixtures.kt index bc0d1832..02465a09 100644 --- a/src/test/kotlin/com/petqua/test/fixture/MemberFixtures.kt +++ b/src/test/kotlin/com/petqua/test/fixture/MemberFixtures.kt @@ -4,7 +4,7 @@ import com.petqua.domain.auth.Authority import com.petqua.domain.member.Member fun member( - id: Long = 1L, + id: Long = 0L, oauthId: String = "oauthId", oauthServerNumber: Int = 1, authority: Authority = Authority.MEMBER