From 8afa9d4fb1eec4f1c6f1c1dc96963dc95338bf45 Mon Sep 17 00:00:00 2001 From: Ethan Date: Thu, 20 Jun 2024 20:37:19 +0900 Subject: [PATCH] =?UTF-8?q?6/20=20=EB=B0=B0=ED=8F=AC=20=EC=9E=91=EC=97=85?= =?UTF-8?q?=20(#104)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/db_backup.yml | 2 +- build.gradle | 4 + .../domain/repository/MacbookRepository.kt | 4 +- .../repository/MacbookRepositoryImpl.kt | 2 +- .../crawl/macbook/service/MacbookService.kt | 10 +- .../itracker/tracker/common/response/Pages.kt | 4 + .../product/controller/HomeController.kt | 33 +++ .../product/controller/ProductController.kt | 6 +- .../tracker/product/handler/AirPodsHandler.kt | 7 +- .../tracker/product/handler/MacbookHandler.kt | 4 +- .../response/product/CommonProductModel.kt | 5 +- .../product/airpods/AirPodsResponse.kt | 4 + .../product/macbook/MacbookDetailResponse.kt | 42 ++++ .../product/macbook/MacbookResponse.kt | 6 +- .../itracker/config/AssuredTestConfig.kt | 38 ++++ .../itracker/config/DatabaseTruncater.kt | 36 +++ .../itracker/config/RepositoryTestConfig.kt | 10 + .../itracker/config/ServiceTestConfig.kt | 20 ++ .../crawl/macbook/domain/MacbookPricesTest.kt | 113 +++++++++- .../crawl/macbook/domain/MacbookTest.kt | 199 +++++++++++++++- .../fixtures/MacbookFilterConditionFixture.kt | 24 ++ .../crawl/macbook/fixtures/MacbookFixture.kt | 100 ++++++-- .../macbook/fixtures/MacbookPriceFixture.kt | 16 +- .../macbook/service/MacbookServiceTest.kt | 213 +++++++++++++++--- .../itracker/tracker/common/PathParams.kt | 10 + .../assured/ProductControllerAssuredTest.kt | 22 ++ .../assured/ProductMacbookAssuredTest.kt | 187 +++++++++++++++ 27 files changed, 1058 insertions(+), 63 deletions(-) create mode 100644 src/main/kotlin/backend/itracker/tracker/product/controller/HomeController.kt create mode 100644 src/test/kotlin/backend/itracker/config/AssuredTestConfig.kt create mode 100644 src/test/kotlin/backend/itracker/config/DatabaseTruncater.kt create mode 100644 src/test/kotlin/backend/itracker/config/RepositoryTestConfig.kt create mode 100644 src/test/kotlin/backend/itracker/config/ServiceTestConfig.kt create mode 100644 src/test/kotlin/backend/itracker/crawl/macbook/fixtures/MacbookFilterConditionFixture.kt create mode 100644 src/test/kotlin/backend/itracker/tracker/common/PathParams.kt create mode 100644 src/test/kotlin/backend/itracker/tracker/product/assured/ProductControllerAssuredTest.kt create mode 100644 src/test/kotlin/backend/itracker/tracker/product/assured/ProductMacbookAssuredTest.kt diff --git a/.github/workflows/db_backup.yml b/.github/workflows/db_backup.yml index 2edabf0..b389081 100644 --- a/.github/workflows/db_backup.yml +++ b/.github/workflows/db_backup.yml @@ -2,7 +2,7 @@ name: db_backup on: schedule: - - cron: '0 3 * * *' + - cron: '0 20 * * *' jobs: cron: diff --git a/build.gradle b/build.gradle index 9f5f37a..ff52508 100644 --- a/build.gradle +++ b/build.gradle @@ -55,6 +55,10 @@ dependencies { runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' + + // RestAssured + testImplementation 'io.rest-assured:rest-assured' + testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.boot:spring-boot-testcontainers' testImplementation 'org.testcontainers:junit-jupiter' diff --git a/src/main/kotlin/backend/itracker/crawl/macbook/domain/repository/MacbookRepository.kt b/src/main/kotlin/backend/itracker/crawl/macbook/domain/repository/MacbookRepository.kt index d7e3984..b5e626f 100644 --- a/src/main/kotlin/backend/itracker/crawl/macbook/domain/repository/MacbookRepository.kt +++ b/src/main/kotlin/backend/itracker/crawl/macbook/domain/repository/MacbookRepository.kt @@ -20,7 +20,7 @@ interface MacbookRepository: JpaRepository, MacbookRepositoryCust """ select m from Macbook m - join fetch m.prices.macbookPrices + left join fetch m.prices.macbookPrices where m.category = :category """ ) @@ -30,7 +30,7 @@ interface MacbookRepository: JpaRepository, MacbookRepositoryCust """ select m from Macbook m - join fetch m.prices.macbookPrices + left join fetch m.prices.macbookPrices where m.id = :id """ ) diff --git a/src/main/kotlin/backend/itracker/crawl/macbook/domain/repository/MacbookRepositoryImpl.kt b/src/main/kotlin/backend/itracker/crawl/macbook/domain/repository/MacbookRepositoryImpl.kt index 63ae135..79adc76 100644 --- a/src/main/kotlin/backend/itracker/crawl/macbook/domain/repository/MacbookRepositoryImpl.kt +++ b/src/main/kotlin/backend/itracker/crawl/macbook/domain/repository/MacbookRepositoryImpl.kt @@ -34,7 +34,7 @@ class MacbookRepositoryImpl( ): List { return jpaQueryFactory .selectFrom(macbook) - .join(macbook.prices.macbookPrices, macbookPrice).fetchJoin() + .leftJoin(macbook.prices.macbookPrices, macbookPrice).fetchJoin() .where( equalSize(filterCondition.size), equalColor(filterCondition.color), diff --git a/src/main/kotlin/backend/itracker/crawl/macbook/service/MacbookService.kt b/src/main/kotlin/backend/itracker/crawl/macbook/service/MacbookService.kt index 75ad92f..9fa1ce0 100644 --- a/src/main/kotlin/backend/itracker/crawl/macbook/service/MacbookService.kt +++ b/src/main/kotlin/backend/itracker/crawl/macbook/service/MacbookService.kt @@ -38,19 +38,21 @@ class MacbookService( fun findAllFetchByCategory( category: MacbookCategory, ): List { - return macbookRepository.findAllFetchByProductCategory(category) + val findAllFetchByProductCategory = macbookRepository.findAllFetchByProductCategory(category) + return findAllFetchByProductCategory } @Transactional(readOnly = true) - fun findAllByProductCategoryAndFilter( + fun findAllByCategoryAndFilter( category: MacbookCategory, filterCondition: MacbookFilterCondition ): List { - return macbookRepository.findAllByFilterCondition(category, filterCondition) + val findAllByFilterCondition = macbookRepository.findAllByFilterCondition(category, filterCondition) + return findAllByFilterCondition } @Transactional(readOnly = true) - fun findAllProductsByFilter( + fun findAllFetchByCategoryAndFilter( category: MacbookCategory, macbookFilterCondition: MacbookFilterCondition, ): List { diff --git a/src/main/kotlin/backend/itracker/tracker/common/response/Pages.kt b/src/main/kotlin/backend/itracker/tracker/common/response/Pages.kt index bd3c62f..3f32a71 100644 --- a/src/main/kotlin/backend/itracker/tracker/common/response/Pages.kt +++ b/src/main/kotlin/backend/itracker/tracker/common/response/Pages.kt @@ -10,6 +10,10 @@ data class Pages( ) { companion object { + fun withPagination(data: List) = Pages( + data = data, + pageInfo = PageInfo(currentPage = 1, lastPage = 1, elementSize = data.size) + ) fun withPagination(pagesData: Page) = Pages( data = pagesData.content, pageInfo = PageInfo.from(pagesData) diff --git a/src/main/kotlin/backend/itracker/tracker/product/controller/HomeController.kt b/src/main/kotlin/backend/itracker/tracker/product/controller/HomeController.kt new file mode 100644 index 0000000..2ce8270 --- /dev/null +++ b/src/main/kotlin/backend/itracker/tracker/product/controller/HomeController.kt @@ -0,0 +1,33 @@ +package backend.itracker.tracker.product.controller + +import backend.itracker.crawl.common.ProductCategory +import backend.itracker.tracker.common.response.Pages +import backend.itracker.tracker.product.handler.ProductHandlerImpl +import backend.itracker.tracker.product.response.product.CommonProductModel +import backend.itracker.tracker.product.vo.Limit +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + +@RestController +class HomeController( + private val productHandler: ProductHandlerImpl, +) { + + @GetMapping("/api/v1/products") + fun getAllProductsByFilter( + @RequestParam(defaultValue = "10") limit: Int + ): ResponseEntity> { + val macbookAirs = + productHandler.findTopDiscountPercentageProducts(ProductCategory.MACBOOK_AIR, Limit(limit)) + val macbookPros = + productHandler.findTopDiscountPercentageProducts(ProductCategory.MACBOOK_PRO, Limit(limit)) + val airPods = + productHandler.findTopDiscountPercentageProducts(ProductCategory.AIRPODS, Limit(limit)) + + val allProducts = macbookAirs + macbookPros + airPods + return ResponseEntity.ok(Pages.withPagination(allProducts.sortedBy { it.discountPercentage() } + .take(limit))) + } +} diff --git a/src/main/kotlin/backend/itracker/tracker/product/controller/ProductController.kt b/src/main/kotlin/backend/itracker/tracker/product/controller/ProductController.kt index f2591f3..584ba28 100644 --- a/src/main/kotlin/backend/itracker/tracker/product/controller/ProductController.kt +++ b/src/main/kotlin/backend/itracker/tracker/product/controller/ProductController.kt @@ -27,7 +27,7 @@ class ProductController( ) { @GetMapping("/api/v1/products/{category}") - fun findProductsByCategory( + fun findTopDiscountPercentageProductsByCategory( @PathVariable category: ProductCategory, @RequestParam(defaultValue = "5") limit: Int, ): ResponseEntity> { @@ -50,7 +50,7 @@ class ProductController( } @GetMapping("/api/v1/products/{category}/search") - fun findFilterdMacbookAir( + fun findFilterdProducts( @PathVariable category: ProductCategory, @RequestParam filterCondition: Map, @ModelAttribute pageParams: PageParams, @@ -66,7 +66,7 @@ class ProductController( } @GetMapping("/api/v1/products/{category}/{productId}") - fun findFilterdMacbookAir( + fun findFilterdProductDetail( @PathVariable category: ProductCategory, @PathVariable productId: Long, ): ResponseEntity { diff --git a/src/main/kotlin/backend/itracker/tracker/product/handler/AirPodsHandler.kt b/src/main/kotlin/backend/itracker/tracker/product/handler/AirPodsHandler.kt index 30f50de..bdab2c7 100644 --- a/src/main/kotlin/backend/itracker/tracker/product/handler/AirPodsHandler.kt +++ b/src/main/kotlin/backend/itracker/tracker/product/handler/AirPodsHandler.kt @@ -27,7 +27,12 @@ class AirPodsHandler( productCategory: ProductCategory, limit: Int ): List { - throw NotSupportedException("AirPodsHandler는 최저가 순위를 지원하지 않습니다.") + val airpods = airPodsService.findAllFetch() + val contents = airpods.map { AirPodsResponse.from(it) } + .sortedBy { it.discountPercentage } + .take(limit) + + return contents } override fun findFilter(productCategory: ProductCategory, filterCondition: ProductFilter): CommonFilterModel { diff --git a/src/main/kotlin/backend/itracker/tracker/product/handler/MacbookHandler.kt b/src/main/kotlin/backend/itracker/tracker/product/handler/MacbookHandler.kt index 2382f7f..f198a18 100644 --- a/src/main/kotlin/backend/itracker/tracker/product/handler/MacbookHandler.kt +++ b/src/main/kotlin/backend/itracker/tracker/product/handler/MacbookHandler.kt @@ -41,7 +41,7 @@ class MacbookHandler( category: ProductCategory, filter: ProductFilter ): CommonFilterModel { - val macbooks = macbookService.findAllByProductCategoryAndFilter(category.toMacbookCategory(), MacbookFilterCondition(filter.value)) + val macbooks = macbookService.findAllByCategoryAndFilter(category.toMacbookCategory(), MacbookFilterCondition(filter.value)) return MacbookFilterResponse.from(macbooks) } @@ -51,7 +51,7 @@ class MacbookHandler( filter: ProductFilter, pageable: Pageable, ): Page { - val macbooks = macbookService.findAllProductsByFilter( + val macbooks = macbookService.findAllFetchByCategoryAndFilter( category.toMacbookCategory(), MacbookFilterCondition(filter.value), ) diff --git a/src/main/kotlin/backend/itracker/tracker/product/response/product/CommonProductModel.kt b/src/main/kotlin/backend/itracker/tracker/product/response/product/CommonProductModel.kt index 3113632..11c985b 100644 --- a/src/main/kotlin/backend/itracker/tracker/product/response/product/CommonProductModel.kt +++ b/src/main/kotlin/backend/itracker/tracker/product/response/product/CommonProductModel.kt @@ -5,7 +5,10 @@ import java.time.LocalDateTime import java.time.Period import java.time.format.DateTimeFormatter -interface CommonProductModel +interface CommonProductModel { + + fun discountPercentage(): Int +} abstract class CommonProductDetailModel( diff --git a/src/main/kotlin/backend/itracker/tracker/product/response/product/airpods/AirPodsResponse.kt b/src/main/kotlin/backend/itracker/tracker/product/response/product/airpods/AirPodsResponse.kt index d05f4e0..7ecea6f 100644 --- a/src/main/kotlin/backend/itracker/tracker/product/response/product/airpods/AirPodsResponse.kt +++ b/src/main/kotlin/backend/itracker/tracker/product/response/product/airpods/AirPodsResponse.kt @@ -43,4 +43,8 @@ data class AirPodsResponse( ) } } + + override fun discountPercentage(): Int { + return discountPercentage + } } diff --git a/src/main/kotlin/backend/itracker/tracker/product/response/product/macbook/MacbookDetailResponse.kt b/src/main/kotlin/backend/itracker/tracker/product/response/product/macbook/MacbookDetailResponse.kt index 0791fcd..f2119c9 100644 --- a/src/main/kotlin/backend/itracker/tracker/product/response/product/macbook/MacbookDetailResponse.kt +++ b/src/main/kotlin/backend/itracker/tracker/product/response/product/macbook/MacbookDetailResponse.kt @@ -69,4 +69,46 @@ class MacbookDetailResponse( ) } } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MacbookDetailResponse + + if (id != other.id) return false + if (title != other.title) return false + if (category != other.category) return false + if (size != other.size) return false + if (chip != other.chip) return false + if (cpu != other.cpu) return false + if (gpu != other.gpu) return false + if (storage != other.storage) return false + if (memory != other.memory) return false + if (color != other.color) return false + if (label != other.label) return false + if (imageUrl != other.imageUrl) return false + if (coupangUrl != other.coupangUrl) return false + if (isOutOfStock != other.isOutOfStock) return false + + return true + } + + override fun hashCode(): Int { + var result = id.hashCode() + result = 31 * result + title.hashCode() + result = 31 * result + category.hashCode() + result = 31 * result + size + result = 31 * result + chip.hashCode() + result = 31 * result + cpu.hashCode() + result = 31 * result + gpu.hashCode() + result = 31 * result + storage.hashCode() + result = 31 * result + memory.hashCode() + result = 31 * result + color.hashCode() + result = 31 * result + label.hashCode() + result = 31 * result + imageUrl.hashCode() + result = 31 * result + coupangUrl.hashCode() + result = 31 * result + isOutOfStock.hashCode() + return result + } } diff --git a/src/main/kotlin/backend/itracker/tracker/product/response/product/macbook/MacbookResponse.kt b/src/main/kotlin/backend/itracker/tracker/product/response/product/macbook/MacbookResponse.kt index d100e42..f08ca8f 100644 --- a/src/main/kotlin/backend/itracker/tracker/product/response/product/macbook/MacbookResponse.kt +++ b/src/main/kotlin/backend/itracker/tracker/product/response/product/macbook/MacbookResponse.kt @@ -42,12 +42,16 @@ data class MacbookResponse( storage = "${macbook.storage} SSD 저장 장치", memory = "${macbook.memory} 통합 메모리", color = macbook.color, - currentPrice = macbook.findCurrentPrice(), + currentPrice = macbook.findCurrentPrice().setScale(0), label = macbook.isAllTimeLowPrice(), imageUrl = macbook.thumbnail, isOutOfStock = macbook.isOutOfStock() ) } } + + override fun discountPercentage(): Int { + return discountPercentage + } } diff --git a/src/test/kotlin/backend/itracker/config/AssuredTestConfig.kt b/src/test/kotlin/backend/itracker/config/AssuredTestConfig.kt new file mode 100644 index 0000000..9790138 --- /dev/null +++ b/src/test/kotlin/backend/itracker/config/AssuredTestConfig.kt @@ -0,0 +1,38 @@ +package backend.itracker.config + +import backend.itracker.tracker.common.PathParams +import backend.itracker.tracker.common.QueryParams +import io.restassured.RestAssured +import org.junit.jupiter.api.BeforeEach +import org.springframework.boot.test.web.server.LocalServerPort +import org.springframework.http.MediaType + + +class AssuredTestConfig : ServiceTestConfig() { + + @BeforeEach + fun assuredTestSetUp(@LocalServerPort port: Int) { + RestAssured.port = port + } + + fun get(url: String) = given() + .`when`().get(url) + .then().log().ifError() + .extract() + + fun get(url: String, pathParams: PathParams) = given() + .pathParams(pathParams.values) + .`when`().get(url) + .then().log().ifError() + .extract() + + fun get(url: String, pathParams: PathParams, queryParams: QueryParams) = given() + .pathParams(pathParams.values) + .queryParams(queryParams.values) + .`when`().get(url) + .then().log().ifError() + .extract() + + private fun given() = RestAssured.given().log().ifValidationFails() + .contentType(MediaType.APPLICATION_JSON_VALUE) +} diff --git a/src/test/kotlin/backend/itracker/config/DatabaseTruncater.kt b/src/test/kotlin/backend/itracker/config/DatabaseTruncater.kt new file mode 100644 index 0000000..e3498f5 --- /dev/null +++ b/src/test/kotlin/backend/itracker/config/DatabaseTruncater.kt @@ -0,0 +1,36 @@ +package backend.itracker.config + +import org.springframework.jdbc.core.JdbcTemplate +import org.springframework.test.context.TestContext +import org.springframework.test.context.support.AbstractTestExecutionListener + +class DatabaseTruncater : AbstractTestExecutionListener() { + + override fun afterTestMethod(testContext: TestContext) { + val jdbcTemplate = getJdbcTemplate(testContext) + val truncateQueries = getTruncateQueries(jdbcTemplate) + truncateTables(jdbcTemplate, truncateQueries) + } + + private fun getTruncateQueries(jdbcTemplate: JdbcTemplate): List { + return jdbcTemplate.queryForList( + """ + SELECT Concat('TRUNCATE TABLE ', TABLE_NAME, ';') AS q + FROM INFORMATION_SCHEMA.TABLES + WHERE TABLE_SCHEMA = 'PUBLIC' + """, String::class.java + ) + } + + private fun getJdbcTemplate(testContext: TestContext): JdbcTemplate { + return testContext.applicationContext.getBean(JdbcTemplate::class.java) + } + + private fun truncateTables(jdbcTemplate: JdbcTemplate, truncateQueries: List) { + execute(jdbcTemplate, "SET REFERENTIAL_INTEGRITY FALSE") + truncateQueries.forEach { execute(jdbcTemplate, it) } + execute(jdbcTemplate, "SET REFERENTIAL_INTEGRITY TRUE") + } + + private fun execute(jdbcTemplate: JdbcTemplate, query: String) = jdbcTemplate.execute(query) +} diff --git a/src/test/kotlin/backend/itracker/config/RepositoryTestConfig.kt b/src/test/kotlin/backend/itracker/config/RepositoryTestConfig.kt new file mode 100644 index 0000000..62906f2 --- /dev/null +++ b/src/test/kotlin/backend/itracker/config/RepositoryTestConfig.kt @@ -0,0 +1,10 @@ +package backend.itracker.config + +import backend.itracker.crawl.macbook.domain.repository.MacbookRepository +import org.springframework.beans.factory.annotation.Autowired + +abstract class RepositoryTestConfig { + + @Autowired + lateinit var macbookRepository: MacbookRepository +} diff --git a/src/test/kotlin/backend/itracker/config/ServiceTestConfig.kt b/src/test/kotlin/backend/itracker/config/ServiceTestConfig.kt new file mode 100644 index 0000000..08e49f5 --- /dev/null +++ b/src/test/kotlin/backend/itracker/config/ServiceTestConfig.kt @@ -0,0 +1,20 @@ +package backend.itracker.config + +import backend.itracker.crawl.macbook.domain.Macbook +import backend.itracker.crawl.macbook.service.MacbookService +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.context.TestExecutionListeners + +@TestExecutionListeners(value = [DatabaseTruncater::class], mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +abstract class ServiceTestConfig : RepositoryTestConfig() { + + @Autowired + lateinit var macbookService: MacbookService + + fun saveMacbook(macbook: Macbook) : Macbook { + macbookRepository.save(macbook) + return macbook + } +} diff --git a/src/test/kotlin/backend/itracker/crawl/macbook/domain/MacbookPricesTest.kt b/src/test/kotlin/backend/itracker/crawl/macbook/domain/MacbookPricesTest.kt index 251010b..fcc9022 100644 --- a/src/test/kotlin/backend/itracker/crawl/macbook/domain/MacbookPricesTest.kt +++ b/src/test/kotlin/backend/itracker/crawl/macbook/domain/MacbookPricesTest.kt @@ -3,21 +3,31 @@ package backend.itracker.crawl.macbook.domain import backend.itracker.crawl.macbook.fixtures.MacbookPriceFixture import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Named.named import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.time.Period +import java.util.stream.Stream class MacbookPricesTest { private lateinit var macbookPrices: MacbookPrices + private var allTimeLowPrice: Long = 0 + private var allTimeHighPrice: Long = 0 @BeforeEach fun setUp() { + allTimeLowPrice = 1_000_000 + allTimeHighPrice = 1_800_000 macbookPrices = MacbookPrices( mutableListOf( - MacbookPriceFixture.macbookPrice(50, 2_000_000, 1_000_000, 1), + MacbookPriceFixture.macbookPrice(50, 2_000_000, allTimeLowPrice, 1), MacbookPriceFixture.macbookPrice(20, 2_000_000, 1_600_000, 2), MacbookPriceFixture.macbookPrice(50, 2_000_000, 1_000_000, 3), MacbookPriceFixture.macbookPrice(20, 2_000_000, 1_600_000, 4), - MacbookPriceFixture.macbookPrice(10, 2_000_000, 1_800_000, 5) + MacbookPriceFixture.macbookPrice(10, 2_000_000, allTimeHighPrice, 5) ) ) } @@ -43,4 +53,103 @@ class MacbookPricesTest { // then assertThat(actual).isEqualTo(-29) } + + @Test + fun `평균 가격을 구한다`() { + // given + val macbookPricesValues = macbookPrices.macbookPrices + val expected = macbookPricesValues.sumOf { it.currentPrice }.toLong() / macbookPricesValues.size + + // when + val actual = macbookPrices.findAveragePrice() + + // then + assertThat(actual.toLong()).isEqualTo(expected) + } + + @Test + fun `역대 최고가를 구한다`() { + // when + val actual = macbookPrices.findAllTimeHighPrice() + + // then + assertThat(actual).isEqualTo(allTimeHighPrice.toBigDecimal()) + } + + @Test + fun `역대 최저가를 구한다`() { + // when + val actual = macbookPrices.findAllTimeLowPrice() + + // then + assertThat(actual).isEqualTo(allTimeLowPrice.toBigDecimal()) + } + + @ParameterizedTest(name = "가격의 outOfStock이 {1}이면 {1}이 나온다") + @MethodSource("isOutOfStock") + fun `품절 여부를 확인한다`(price: MacbookPrice, expected: Boolean) { + // given + macbookPrices.add(price) + + // when + val actual = macbookPrices.isOutOfStock() + + // then + assertThat(actual).isEqualTo(expected) + } + + @ParameterizedTest + @MethodSource("isAllTimeLowPrice") + fun `역대 최저가인지 확인한다`(price: MacbookPrice, expected: Boolean) { + // given + macbookPrices.add(price) + + // when + val actual = macbookPrices.isAllTimeLowPrice() + + // then + assertThat(actual).isEqualTo(expected) + } + + @ParameterizedTest(name = "{0} {1}") + @MethodSource("getRecentPricesByPeriod") + fun `기간별로 가격을 확인한다`(period: Period, expectedSize: Int) { + // given + macbookPrices.add(MacbookPriceFixture.macbookPrice(50, 2_000_000, 1_000_000)) + + // when + val actual = macbookPrices.getRecentPricesByPeriod(period) + + // then + assertThat(actual.macbookPrices).hasSize(expectedSize) + } + + companion object { + @JvmStatic + fun isOutOfStock() = Stream.of( + Arguments.of(MacbookPriceFixture.macbookPrice(10, 100_000, 90_000, true), true), + Arguments.of(MacbookPriceFixture.macbookPrice(10, 100_000, 90_000, false), false) + ) + + @JvmStatic + fun isAllTimeLowPrice() = Stream.of( + Arguments.of(MacbookPriceFixture.macbookPrice(90, 100_000, 10_000), true), + Arguments.of(MacbookPriceFixture.macbookPrice(10, 5_000_000, 4_500_000), false) + ) + + @JvmStatic + private fun getRecentPricesByPeriod(): Stream { + return Stream.of( + Arguments.of( + named("1일치를 조회하면", Period.of(0, 0, 1)), + named("1개만 조회된다", 1) + ), + + Arguments.of( + named("2일치를 조회하면", Period.of(0, 0, 2)), + named("2개만 조회된다", 2) + ), + ) + } + } } diff --git a/src/test/kotlin/backend/itracker/crawl/macbook/domain/MacbookTest.kt b/src/test/kotlin/backend/itracker/crawl/macbook/domain/MacbookTest.kt index 71eb468..2421165 100644 --- a/src/test/kotlin/backend/itracker/crawl/macbook/domain/MacbookTest.kt +++ b/src/test/kotlin/backend/itracker/crawl/macbook/domain/MacbookTest.kt @@ -3,8 +3,16 @@ package backend.itracker.crawl.macbook.domain import backend.itracker.crawl.macbook.fixtures.MacbookFixture import backend.itracker.crawl.macbook.fixtures.MacbookPriceFixture import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions.assertAll import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Named.named import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import org.junit.jupiter.params.provider.ValueSource +import java.time.Period +import java.util.stream.Stream class MacbookTest { @@ -12,11 +20,11 @@ class MacbookTest { @BeforeEach fun setUp() { - macbook = MacbookFixture.createDefaultMacbookAir() + macbook = MacbookFixture.default() } @Test - fun `맥북에 가격에 추가한다`() { + fun `맥북에 가격을 추가한다`() { // given val targetPrice = MacbookPriceFixture.macbookPrice(2, 2_400_000, 2_351_000) @@ -28,7 +36,7 @@ class MacbookTest { } @Test - fun `맥북에 가격들을 추가한다`() { + fun `맥북에 가격들을 모두 추가한다`() { // given val targetPrices = MacbookPrices( mutableListOf( @@ -70,6 +78,74 @@ class MacbookTest { assertThat(actual).isEqualTo(expectedCurrentPrice.toBigDecimal()) } + @Test + fun `평균가를 가지고 온다`() { + // given + val firstCurrentPrice: Long = 1_000_000 + val secondCurrentPrice: Long = 800_000 + val thirdCurrentPrice: Long = 600_000 + val expected = (firstCurrentPrice + secondCurrentPrice + thirdCurrentPrice) / 3 + macbook.addAllPrices( + MacbookPrices( + mutableListOf( + MacbookPriceFixture.macbookPrice(50, 2_000_000, firstCurrentPrice), + MacbookPriceFixture.macbookPrice(10, 2_000_000, secondCurrentPrice, 1), + MacbookPriceFixture.macbookPrice(30, 2_400_000, thirdCurrentPrice, 2) + ) + ) + ) + + // when + val actual = macbook.findAveragePrice() + + // then + assertThat(actual.toLong()).isEqualTo(expected) + } + + @Test + fun `역대 최고가를 가지고 온다`() { + // given + val allTimeHighPrice: Long = 3_600_000 + + macbook.addAllPrices( + MacbookPrices( + mutableListOf( + MacbookPriceFixture.macbookPrice(50, 2_000_000, 1_000_000), + MacbookPriceFixture.macbookPrice(10, 2_000_000, 800_000, 1), + MacbookPriceFixture.macbookPrice(30, 2_400_000, allTimeHighPrice, 2) + ) + ) + ) + + // when + val actual = macbook.findAllTimeHighPrice() + + // then + assertThat(actual.toLong()).isEqualTo(allTimeHighPrice) + } + + @Test + fun `역대 최저가를 가지고 온다`() { + // given + val allTimeLowPrice: Long = 100_000 + + macbook.addAllPrices( + MacbookPrices( + mutableListOf( + MacbookPriceFixture.macbookPrice(50, 2_000_000, 1_000_000), + MacbookPriceFixture.macbookPrice(10, 2_000_000, 800_000, 1), + MacbookPriceFixture.macbookPrice(30, 2_400_000, allTimeLowPrice, 2) + ) + ) + ) + + // when + val actual = macbook.findAllTimeLowPrice() + + // then + assertThat(actual.toLong()).isEqualTo(allTimeLowPrice) + } + @Test fun `할인율을 가지고 온다`() { // given @@ -89,4 +165,121 @@ class MacbookTest { // then assertThat(actual).isEqualTo(-33) } + + @Test + fun `파트너스 링크를 변경한다`() { + // given + val origin = macbook.partnersLink + + // when + val expected = "changed-link" + macbook.changePartnersLink(expected) + + // then + val actual = macbook.partnersLink + assertAll( + { assertThat(origin).isNotEqualTo(expected) }, + { assertThat(actual).isEqualTo(expected) } + ) + } + + @ParameterizedTest(name = "품절 상태가 {0}인지 확인한다") + @ValueSource(booleans = [true, false]) + fun `품절인지 확인한다`(outOfStockStatus: Boolean) { + // given + macbook.addAllPrices( + MacbookPrices( + mutableListOf( + MacbookPriceFixture.macbookPrice(50, 2_000_000, 1_000_000, outOfStockStatus), + MacbookPriceFixture.macbookPrice(10, 2_000_000, 1_800_000, 1), + MacbookPriceFixture.macbookPrice(30, 2_400_000, 1_680_000, 2) + ) + ) + ) + + // when + val actual = macbook.isOutOfStock() + + // then + assertThat(actual).isEqualTo(outOfStockStatus) + } + + @ParameterizedTest(name = "{0} {1}") + @MethodSource("getRecentPricesByPeriod") + fun `기간에 따라서 값들을 가지고 온다`(period: Period, expectedSize: Int) { + // given + macbook.addAllPrices( + MacbookPrices( + mutableListOf( + MacbookPriceFixture.macbookPrice(50, 2_000_000, 1_000_000), + MacbookPriceFixture.macbookPrice(10, 2_000_000, 1_800_000, 1), + MacbookPriceFixture.macbookPrice(30, 2_400_000, 1_680_000, 2) + ) + ) + ) + + // when + val actual = macbook.getRecentPricesByPeriod(period) + + // then + assertThat(actual.macbookPrices).hasSize(expectedSize) + } + + @ParameterizedTest(name = "{0} {1}이 반환된다.") + @MethodSource("isAllTimeLowPrice") + fun `역대 최저가를 판단한다`(macbookPrices: MacbookPrices, expected: Boolean) { + // given + macbook.addAllPrices(macbookPrices) + + // when + val actual = macbook.isAllTimeLowPrice() + + // then + assertThat(actual).isEqualTo(expected) + } + + companion object { + @JvmStatic + private fun getRecentPricesByPeriod(): Stream { + return Stream.of( + Arguments.of( + named("1일치를 조회하면", Period.of(0, 0, 1)), + named("1개만 조회된다", 1) + ), + + Arguments.of( + named("2일치를 조회하면", Period.of(0, 0, 2)), + named("2개만 조회된다", 2) + ), + ) + } + + @JvmStatic + private fun isAllTimeLowPrice() = Stream.of( + Arguments.of( + named( + "오늘이 최저가이면", MacbookPrices( + mutableListOf( + MacbookPriceFixture.macbookPrice(50, 2_000_000, 1_000_000), + MacbookPriceFixture.macbookPrice(10, 2_000_000, 1_800_000, 1), + MacbookPriceFixture.macbookPrice(30, 2_400_000, 1_680_000, 2) + ) + ) + ), true + ), + Arguments.of( + named( + "오늘이 최저가가 아니면", MacbookPrices( + mutableListOf( + MacbookPriceFixture.macbookPrice(50, 2_000_000, 2_000_000), + MacbookPriceFixture.macbookPrice(10, 2_000_000, 1_800_000, 1), + MacbookPriceFixture.macbookPrice(30, 2_400_000, 1_680_000, 2) + ) + ) + ), false + ), + ) + + } } + diff --git a/src/test/kotlin/backend/itracker/crawl/macbook/fixtures/MacbookFilterConditionFixture.kt b/src/test/kotlin/backend/itracker/crawl/macbook/fixtures/MacbookFilterConditionFixture.kt new file mode 100644 index 0000000..3750847 --- /dev/null +++ b/src/test/kotlin/backend/itracker/crawl/macbook/fixtures/MacbookFilterConditionFixture.kt @@ -0,0 +1,24 @@ +package backend.itracker.crawl.macbook.fixtures + +import backend.itracker.crawl.macbook.service.dto.MacbookFilterCondition + +abstract class MacbookFilterConditionFixture { + + companion object { + fun create( + size: Int? = null, + color: String? = null, + processor: String? = null, + storage: String? = null, + memory: String? = null + ): MacbookFilterCondition { + return MacbookFilterCondition( + size = size, + color = color, + processor = processor, + storage = storage, + memory = memory + ) + } + } +} diff --git a/src/test/kotlin/backend/itracker/crawl/macbook/fixtures/MacbookFixture.kt b/src/test/kotlin/backend/itracker/crawl/macbook/fixtures/MacbookFixture.kt index cc6b6cf..8010a44 100644 --- a/src/test/kotlin/backend/itracker/crawl/macbook/fixtures/MacbookFixture.kt +++ b/src/test/kotlin/backend/itracker/crawl/macbook/fixtures/MacbookFixture.kt @@ -6,6 +6,93 @@ import backend.itracker.crawl.macbook.domain.MacbookCategory abstract class MacbookFixture { companion object { + fun macbook( + category: MacbookCategory, + coupangId: Long + ): Macbook { + return Macbook( + coupangId = coupangId, + company = "Apple", + name = "Apple 2024 맥북 에어 13 M3, 미드나이트, M3 8코어, 10코어 GPU, 1TB, 16GB, 35W 듀얼, 한글", + category = category, + chip = "M3", + cpu = "8코어", + gpu = "10코어", + storage = "1TB", + memory = "16GB", + language = "한글", + color = "미드나이트", + size = 13, + releaseYear = 2024, + productLink = "https://www.coupang.com/vp/products/7975088162?itemId=22505523317&vendorItemId=89547640201&sourceType=cmgoms&omsPageId=84871&omsPageUrl=84871", + thumbnail = "https://thumbnail10.coupangcdn.com/thumbnails/remote/230x230ex/image/retail/images/15943638430632-244768e7-86d1-4484-b772-013d185666b8.jpg", + ) + } + + fun macbook( + coupangId: Long, + productLink: String + ): Macbook { + return Macbook( + coupangId = coupangId, + company = "Apple", + name = "Apple 2024 맥북 에어 13 M3, 미드나이트, M3 8코어, 10코어 GPU, 1TB, 16GB, 35W 듀얼, 한글", + category = MacbookCategory.MACBOOK_AIR, + chip = "M3", + cpu = "8코어", + gpu = "10코어", + storage = "1TB", + memory = "16GB", + language = "한글", + color = "미드나이트", + size = 13, + releaseYear = 2024, + productLink = productLink, + thumbnail = "https://thumbnail10.coupangcdn.com/thumbnails/remote/230x230ex/image/retail/images/15943638430632-244768e7-86d1-4484-b772-013d185666b8.jpg", + ) + } + + fun default(): Macbook { + return macbook( + category = MacbookCategory.MACBOOK_AIR, + chip = "M3", + cpu = "8코어", + gpu = "10코어", + storage = "1TB", + memory = "16GB", + color = "미드나이트", + size = 13 + ) + } + + fun macbook( + coupangId: Long, + category: MacbookCategory, + size: Int = 0, + color: String = "", + chip: String = "", + storage: String = "", + memory: String = "" + ): Macbook { + return Macbook( + coupangId = coupangId, + company = "Apple", + name = "Apple 2024 맥북 에어 13 M3, 미드나이트, M3 8코어, 10코어 GPU, 1TB, 16GB, 35W 듀얼, 한글", + category = category, + chip = chip, + cpu = "8코어", + gpu = "10코어", + storage = storage, + memory = memory, + language = "한글", + color = color, + size = size, + releaseYear = 2024, + productLink = "https://www.coupang.com/vp/products/7975088162?itemId=22505523317&vendorItemId=89547640201&sourceType=cmgoms&omsPageId=84871&omsPageUrl=84871", + thumbnail = "https://thumbnail10.coupangcdn.com/thumbnails/remote/230x230ex/image/retail/images/15943638430632-244768e7-86d1-4484-b772-013d185666b8.jpg", + ) + } + fun macbook( category: MacbookCategory, size: Int, @@ -34,18 +121,5 @@ abstract class MacbookFixture { thumbnail = "https://thumbnail10.coupangcdn.com/thumbnails/remote/230x230ex/image/retail/images/15943638430632-244768e7-86d1-4484-b772-013d185666b8.jpg", ) } - - fun createDefaultMacbookAir(): Macbook { - return macbook( - category = MacbookCategory.MACBOOK_AIR, - chip = "M3", - cpu = "8코어", - gpu = "10코어", - storage = "1TB", - memory = "16GB", - color = "미드나이트", - size = 13 - ) - } } } diff --git a/src/test/kotlin/backend/itracker/crawl/macbook/fixtures/MacbookPriceFixture.kt b/src/test/kotlin/backend/itracker/crawl/macbook/fixtures/MacbookPriceFixture.kt index e70256b..f905d87 100644 --- a/src/test/kotlin/backend/itracker/crawl/macbook/fixtures/MacbookPriceFixture.kt +++ b/src/test/kotlin/backend/itracker/crawl/macbook/fixtures/MacbookPriceFixture.kt @@ -9,21 +9,31 @@ abstract class MacbookPriceFixture { fun macbookPrice( discountPercentage: Int, basePrice: Long, - currentPrice: Long + currentPrice: Long, + isOutOfStock: Boolean ) = MacbookPrice( discountPercentage = discountPercentage, basePrice = BigDecimal.valueOf(basePrice), currentPrice = BigDecimal.valueOf(currentPrice), - isOutOfStock = false + isOutOfStock = isOutOfStock ) + fun macbookPrice( + discountPercentage: Int, + basePrice: Long, + currentPrice: Long + ): MacbookPrice { + val macbookPrice = macbookPrice(discountPercentage, basePrice, currentPrice, false) + return macbookPrice + } + fun macbookPrice( discountPercentage: Int, basePrice: Long, currentPrice: Long, beforeDay: Long ): MacbookPrice { - val macbookPrice = macbookPrice(discountPercentage, basePrice, currentPrice) + val macbookPrice = macbookPrice(discountPercentage, basePrice, currentPrice, false) macbookPrice.createdAt = macbookPrice.createdAt.minusDays(beforeDay) return macbookPrice } diff --git a/src/test/kotlin/backend/itracker/crawl/macbook/service/MacbookServiceTest.kt b/src/test/kotlin/backend/itracker/crawl/macbook/service/MacbookServiceTest.kt index c4c746b..d3a004d 100644 --- a/src/test/kotlin/backend/itracker/crawl/macbook/service/MacbookServiceTest.kt +++ b/src/test/kotlin/backend/itracker/crawl/macbook/service/MacbookServiceTest.kt @@ -1,49 +1,210 @@ package backend.itracker.crawl.macbook.service -import backend.itracker.crawl.macbook.domain.Macbook -import backend.itracker.crawl.macbook.domain.repository.MacbookRepository +import backend.itracker.config.ServiceTestConfig +import backend.itracker.crawl.common.PartnersLinkInfo +import backend.itracker.crawl.macbook.domain.MacbookCategory +import backend.itracker.crawl.macbook.domain.repository.findByIdAllFetch +import backend.itracker.crawl.macbook.fixtures.MacbookFilterConditionFixture import backend.itracker.crawl.macbook.fixtures.MacbookFixture import backend.itracker.crawl.macbook.fixtures.MacbookPriceFixture import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Assertions.assertAll import org.junit.jupiter.api.Test -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.context.SpringBootTest -@SpringBootTest -class MacbookServiceTest { +class MacbookServiceTest : ServiceTestConfig() { - @Autowired - lateinit var macbookService: MacbookService + @Test + fun `없는 맥북을 저장한다`() { + // given + val first = MacbookFixture.macbook(MacbookCategory.MACBOOK_PRO, 1) + val second = MacbookFixture.macbook(MacbookCategory.MACBOOK_PRO, 2) - @Autowired - lateinit var macbookRepository: MacbookRepository + // when + macbookService.saveAll(listOf(first, second)) - private lateinit var macbook: Macbook + // then + val savedMacbooks = macbookRepository.findAll() + assertThat(savedMacbooks).hasSize(2) + } - @BeforeEach - fun setUp() { - macbook = MacbookFixture.createDefaultMacbookAir() + @Test + fun `이미 있는 맥북의 가격을 추가한다`() { + // given + val first = saveMacbook(MacbookFixture.macbook(MacbookCategory.MACBOOK_AIR, 1)) + val second = saveMacbook(MacbookFixture.macbook(MacbookCategory.MACBOOK_PRO, 2)) + val firstMacbookPrice = MacbookPriceFixture.macbookPrice(10, 100_000, 90_000) + val secondMacbookPrice = MacbookPriceFixture.macbookPrice(10, 100_000, 90_000) + first.addPrice(firstMacbookPrice) + second.addPrice(secondMacbookPrice) + + // when + macbookService.saveAll(listOf(first, second)) + + // then + val firstMacbook = macbookRepository.findByIdAllFetch(first.id) + val secondMacbook = macbookRepository.findByIdAllFetch(second.id) + assertAll({ assertThat(firstMacbook.prices.macbookPrices).containsOnly(firstMacbookPrice) }, + { assertThat(secondMacbook.prices.macbookPrices).containsOnly(secondMacbookPrice) }) } @Test - fun `전체 저장 테스트`() { + fun `맥북의 파트너스 링크를 변경한다`() { // given - macbook.addPrice( - MacbookPriceFixture.macbookPrice( - discountPercentage = 2, - basePrice = 2400000, - currentPrice = 2351000 - ) + val firstMacbook = saveMacbook(MacbookFixture.macbook(1, "first partners link")) + val secondMacbook = saveMacbook(MacbookFixture.macbook(2, "second partners link")) + + val firstPartnersLink = "new partners link 1" + val secondPartnersLink = "new partners link 2" + val partnersLinkInformation = listOf( + PartnersLinkInfo(firstMacbook.productLink, firstPartnersLink), + PartnersLinkInfo(secondMacbook.productLink, secondPartnersLink) ) // when - macbookService.saveAll(listOf(macbook)) + macbookService.updateAllPartnersLink(partnersLinkInformation) // then - val savedMacbooks = macbookRepository.findAll() - assertThat(savedMacbooks).hasSize(1) + assertThat(macbookRepository.findAll().map { it.partnersLink }.toList()).containsExactly( + firstPartnersLink, secondPartnersLink + ) + } + + @Test + fun `카테고리가 일치하는 맥북을 모두 fetch join 한다`() { + // given + val macbookAir1 = saveMacbook(MacbookFixture.macbook(MacbookCategory.MACBOOK_AIR, 1)) + val macbookAir2 = saveMacbook(MacbookFixture.macbook(MacbookCategory.MACBOOK_AIR, 2)) + val macbookAir3 = saveMacbook(MacbookFixture.macbook(MacbookCategory.MACBOOK_AIR, 3)) + val macbookPro1 = saveMacbook(MacbookFixture.macbook(MacbookCategory.MACBOOK_PRO, 4)) + val macbookPro2 = saveMacbook(MacbookFixture.macbook(MacbookCategory.MACBOOK_PRO, 5)) + + // when + val macbookAirs = macbookService.findAllFetchByCategory(MacbookCategory.MACBOOK_AIR) + val macbookPros = macbookService.findAllFetchByCategory(MacbookCategory.MACBOOK_PRO) + + // then + assertAll( + { assertThat(macbookAirs).containsExactly(macbookAir1, macbookAir2, macbookAir3) }, + { assertThat(macbookPros).containsExactly(macbookPro1, macbookPro2) } + ) } -} + @Test + fun `카테고리와 필터가 일치한 맥북을 찾는다`() { + // given + val macbookAir1 = saveMacbook(MacbookFixture.macbook(1, MacbookCategory.MACBOOK_AIR, size = 13)) + val macbookAir2 = saveMacbook(MacbookFixture.macbook(2, MacbookCategory.MACBOOK_AIR, chip = "M1")) + val macbookAir3 = saveMacbook(MacbookFixture.macbook(3, MacbookCategory.MACBOOK_AIR, storage = "256GB")) + val macbookAir4 = saveMacbook(MacbookFixture.macbook(4, MacbookCategory.MACBOOK_AIR, memory = "16GB")) + val macbookAir5 = saveMacbook(MacbookFixture.macbook(5, MacbookCategory.MACBOOK_AIR, color = "실버")) + + // when + // then + assertAll( + { + assertThat(macbookService.findAllByCategoryAndFilter( + MacbookCategory.MACBOOK_AIR, + MacbookFilterConditionFixture.create(size = 13) + ).map { it.coupangId }).containsExactly(macbookAir1.coupangId) + }, + { + assertThat(macbookService.findAllByCategoryAndFilter( + MacbookCategory.MACBOOK_AIR, + MacbookFilterConditionFixture.create(processor = "M1") + ).map { it.coupangId }).containsExactly(macbookAir2.coupangId) + }, + { + assertThat(macbookService.findAllByCategoryAndFilter( + MacbookCategory.MACBOOK_AIR, + MacbookFilterConditionFixture.create(storage = "256GB") + ).map { it.coupangId }).containsExactly(macbookAir3.coupangId) + }, + { + assertThat(macbookService.findAllByCategoryAndFilter( + MacbookCategory.MACBOOK_AIR, + MacbookFilterConditionFixture.create(memory = "16GB") + ).map { it.coupangId }).containsExactly(macbookAir4.coupangId) + }, + { + assertThat(macbookService.findAllByCategoryAndFilter( + MacbookCategory.MACBOOK_AIR, + MacbookFilterConditionFixture.create(color = "실버") + ).map { it.coupangId }).containsExactly(macbookAir5.coupangId) + } + ) + } + + @Test + fun `카테고리와 필터가 일치한 맥북을 fetch jion 한다`() { + // given + val macbookAir1 = saveMacbook(MacbookFixture.macbook(1, MacbookCategory.MACBOOK_AIR, size = 13)) + val macbookAir2 = saveMacbook(MacbookFixture.macbook(2, MacbookCategory.MACBOOK_AIR, chip = "M1")) + val macbookAir3 = saveMacbook(MacbookFixture.macbook(3, MacbookCategory.MACBOOK_AIR, storage = "256GB")) + val macbookAir4 = saveMacbook(MacbookFixture.macbook(4, MacbookCategory.MACBOOK_AIR, memory = "16GB")) + val macbookAir5 = saveMacbook(MacbookFixture.macbook(5, MacbookCategory.MACBOOK_AIR, color = "실버")) + + // when + // then + assertAll( + { + assertThat(macbookService.findAllFetchByCategoryAndFilter( + MacbookCategory.MACBOOK_AIR, + MacbookFilterConditionFixture.create(size = 13) + ).map { it.coupangId }).containsExactly(macbookAir1.coupangId) + }, + { + assertThat(macbookService.findAllFetchByCategoryAndFilter( + MacbookCategory.MACBOOK_AIR, + MacbookFilterConditionFixture.create(processor = "M1") + ).map { it.coupangId }).containsExactly(macbookAir2.coupangId) + }, + { + assertThat(macbookService.findAllFetchByCategoryAndFilter( + MacbookCategory.MACBOOK_AIR, + MacbookFilterConditionFixture.create(storage = "256GB") + ).map { it.coupangId }).containsExactly(macbookAir3.coupangId) + }, + { + assertThat(macbookService.findAllFetchByCategoryAndFilter( + MacbookCategory.MACBOOK_AIR, + MacbookFilterConditionFixture.create(memory = "16GB") + ).map { it.coupangId }).containsExactly(macbookAir4.coupangId) + }, + { + assertThat(macbookService.findAllFetchByCategoryAndFilter( + MacbookCategory.MACBOOK_AIR, + MacbookFilterConditionFixture.create(color = "실버") + ).map { it.coupangId }).containsExactly(macbookAir5.coupangId) + } + ) + } + + @Test + fun `id로 맥북을 fetch join 한다`() { + // given + val expected = saveMacbook(MacbookFixture.default()) + val savedId = expected.id + + // when + val actual = macbookService.findMacbookById(savedId) + + // then + assertThat(actual).isEqualTo(expected) + } + + @Test + fun `id 범위로 맥북을 찾는다`() { + // given + saveMacbook(MacbookFixture.default()) + val startMacbook = saveMacbook(MacbookFixture.default()) + val endMacbook = saveMacbook(MacbookFixture.default()) + saveMacbook(MacbookFixture.default()) + + // when + val actual = macbookService.findByIdBetween(startMacbook.id, endMacbook.id) + + // then + assertThat(actual).containsExactly(startMacbook, endMacbook) + } +} diff --git a/src/test/kotlin/backend/itracker/tracker/common/PathParams.kt b/src/test/kotlin/backend/itracker/tracker/common/PathParams.kt new file mode 100644 index 0000000..834f6fb --- /dev/null +++ b/src/test/kotlin/backend/itracker/tracker/common/PathParams.kt @@ -0,0 +1,10 @@ +package backend.itracker.tracker.common + +data class PathParams( + val values: Map +) + +data class QueryParams( + val values: Map +) + diff --git a/src/test/kotlin/backend/itracker/tracker/product/assured/ProductControllerAssuredTest.kt b/src/test/kotlin/backend/itracker/tracker/product/assured/ProductControllerAssuredTest.kt new file mode 100644 index 0000000..c47239e --- /dev/null +++ b/src/test/kotlin/backend/itracker/tracker/product/assured/ProductControllerAssuredTest.kt @@ -0,0 +1,22 @@ +package backend.itracker.tracker.product.assured + +import backend.itracker.config.AssuredTestConfig +import backend.itracker.crawl.common.ProductCategory +import backend.itracker.tracker.product.response.CategoryResponses +import io.restassured.common.mapper.TypeRef +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class ProductControllerAssuredTest : AssuredTestConfig() { + + @Test + fun `모든 카테고리를 조회한다`() { + // when + val response = get("/api/v1/category") + .`as`(object : TypeRef() {}) + + // then + val expected = ProductCategory.entries.map { it.name.lowercase() }.toList() + assertThat(response.categories).isEqualTo(expected) + } +} diff --git a/src/test/kotlin/backend/itracker/tracker/product/assured/ProductMacbookAssuredTest.kt b/src/test/kotlin/backend/itracker/tracker/product/assured/ProductMacbookAssuredTest.kt new file mode 100644 index 0000000..b3e4189 --- /dev/null +++ b/src/test/kotlin/backend/itracker/tracker/product/assured/ProductMacbookAssuredTest.kt @@ -0,0 +1,187 @@ +package backend.itracker.tracker.product.assured + +import backend.itracker.config.AssuredTestConfig +import backend.itracker.crawl.macbook.domain.MacbookCategory +import backend.itracker.crawl.macbook.fixtures.MacbookFixture +import backend.itracker.crawl.macbook.fixtures.MacbookPriceFixture +import backend.itracker.tracker.common.PathParams +import backend.itracker.tracker.common.QueryParams +import backend.itracker.tracker.common.response.Pages +import backend.itracker.tracker.common.response.SingleData +import backend.itracker.tracker.product.response.filter.MacbookFilterResponse +import backend.itracker.tracker.product.response.product.macbook.MacbookDetailResponse +import backend.itracker.tracker.product.response.product.macbook.MacbookResponse +import io.restassured.common.mapper.TypeRef +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertAll +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.stream.Stream + +class ProductMacbookAssuredTest : AssuredTestConfig() { + + @Test + fun `맥북 필터를 조회한다`() { + // given + saveMacbook( + MacbookFixture.macbook( + coupangId = 1, + category = MacbookCategory.MACBOOK_AIR, + size = 13, + color = "스페이스 그레이", + chip = "M1", + storage = "256GB", + memory = "8GB" + ) + ) + saveMacbook( + MacbookFixture.macbook( + coupangId = 2, + category = MacbookCategory.MACBOOK_AIR, + size = 15, + color = "실버", + chip = "M1", + storage = "512GB", + memory = "16GB" + ) + ) + + // when + val pathParams = PathParams(mapOf("category" to "macbook_air")) + val response = get("/api/v1/products/{category}/filter", pathParams) + .`as`(object : TypeRef>() {}) + + // then + assertAll( + { assertThat(response.data.size).containsExactly(13, 15) }, + { assertThat(response.data.color).containsExactly("스페이스 그레이", "실버") }, + { assertThat(response.data.processor).containsExactly("M1") }, + { assertThat(response.data.storage).containsExactly("256GB", "512GB") }, + { assertThat(response.data.memory).containsExactly("8GB", "16GB") } + ) + } + + @Test + fun `평균가와 비교해서 할인율이 높은 5개 상품을 조회한다`() { + // given + val fifth = saveMacbook(MacbookFixture.default().apply { + addPrice(MacbookPriceFixture.macbookPrice(0, 100_000, 100_000)) + addPrice(MacbookPriceFixture.macbookPrice(10, 10_000, 9_000)) + }) + val fourth = saveMacbook(MacbookFixture.default().apply { + addPrice(MacbookPriceFixture.macbookPrice(0, 100_000, 100_000)) + addPrice(MacbookPriceFixture.macbookPrice(20, 10_000, 8_000)) + }) + val first = saveMacbook(MacbookFixture.default().apply { + addPrice(MacbookPriceFixture.macbookPrice(0, 100_000, 100_000)) + addPrice(MacbookPriceFixture.macbookPrice(90, 10_000, 1_000)) + }) + saveMacbook(MacbookFixture.default().apply { + addPrice(MacbookPriceFixture.macbookPrice(0, 100_000, 100_000)) + addPrice(MacbookPriceFixture.macbookPrice(50, 20_000, 10_000)) + }) + val third = saveMacbook(MacbookFixture.default().apply { + addPrice(MacbookPriceFixture.macbookPrice(0, 100_000, 100_000)) + addPrice(MacbookPriceFixture.macbookPrice(30, 10_000, 7_000)) + }) + val second = saveMacbook(MacbookFixture.default().apply { + addPrice(MacbookPriceFixture.macbookPrice(0, 100_000, 100_000)) + addPrice(MacbookPriceFixture.macbookPrice(40, 10_000, 6_000)) + }) + + val expected = listOf( + MacbookResponse.from(first), + MacbookResponse.from(second), + MacbookResponse.from(third), + MacbookResponse.from(fourth), + MacbookResponse.from(fifth), + ) + + // when + val pathParams = PathParams(mapOf("category" to "macbook_air")) + val response = get("/api/v1/products/{category}", pathParams) + .`as`(object : TypeRef>() {}) + + // then + assertAll( + { assertThat(response.data).hasSize(5) }, + { assertThat(response.data).isEqualTo(expected) } + ) + } + + @Test + fun `맥북을 상세 조회한다`() { + // given + val expected = saveMacbook( + MacbookFixture.macbook( + coupangId = 1, + category = MacbookCategory.MACBOOK_AIR, + size = 13, + color = "스페이스 그레이", + chip = "M1", + storage = "256GB", + memory = "8GB" + ).apply { addPrice(MacbookPriceFixture.macbookPrice(0, 100_000, 100_000)) } + ) + // when + val pathParams = PathParams(mapOf("category" to "macbook_air", "productId" to expected.id)) + val response = get("/api/v1/products/{category}/{productId}", pathParams) + .`as`(object : TypeRef() {}) + + // then + assertThat(response).isEqualTo(MacbookDetailResponse.from(expected)) + } + + @ParameterizedTest + @MethodSource("findFilteredMacbook") + fun `필터링된 맥북을 조회한다`(queryParams: QueryParams) { + // given + val expected = saveMacbook( + MacbookFixture.macbook( + coupangId = 1, + category = MacbookCategory.MACBOOK_AIR, + size = 13, + color = "스페이스 그레이", + chip = "M1", + storage = "256GB", + memory = "8GB" + ).apply { addPrice(MacbookPriceFixture.macbookPrice(0, 100_000, 100_000)) } + ) + saveMacbook( + MacbookFixture.macbook( + coupangId = 2, + category = MacbookCategory.MACBOOK_AIR, + size = 15, + color = "실버", + chip = "M2", + storage = "512GB", + memory = "16GB" + ).apply { addPrice(MacbookPriceFixture.macbookPrice(0, 100_000, 100_000)) } + ) + + // when + val pathParams = PathParams(mapOf("category" to "macbook_air")) + val response = get("/api/v1/products/{category}/search", pathParams, queryParams) + .`as`(object : TypeRef>() {}) + + // then + assertAll( + { assertThat(response.data).hasSize(1) }, + { assertThat(response.data).containsExactly(MacbookResponse.from(expected)) } + ) + } + + companion object { + + @JvmStatic + fun findFilteredMacbook() = Stream.of( + Arguments.of(QueryParams(mapOf("size" to "13"))), + Arguments.of(QueryParams(mapOf("processor" to "M1"))), + Arguments.of(QueryParams(mapOf("color" to "스페이스 그레이"))), + Arguments.of(QueryParams(mapOf("storage" to "256GB"))), + Arguments.of(QueryParams(mapOf("memory" to "8GB"))) + ) + } +}