diff --git a/tests/ProductSearchTest.php b/tests/ProductSearchTest.php new file mode 100644 index 0000000..ed8e5f8 --- /dev/null +++ b/tests/ProductSearchTest.php @@ -0,0 +1,168 @@ +categories = new CategoryRepository($this->db); + $this->products = new ProductRepository($this->db); + $this->attributes = new AttributeRepository($this->db); + $this->variants = new VariantRepository($this->db); + $this->reviews = new ReviewRepository($this->db); + $this->search = new ProductSearch($this->db, $this->attributes, $this->variants, $this->reviews, $this->categories); + + $this->categoryIds['apparel'] = $this->categories->create('Apparel', 'apparel', ''); + $this->categoryIds['home'] = $this->categories->create('Home', 'home', ''); + $this->colorId = $this->attributes->create('Color', 'color'); + $this->colorValueIds['red'] = $this->attributes->createValue($this->colorId, 'Red', 'red'); + $this->colorValueIds['blue'] = $this->attributes->createValue($this->colorId, 'Blue', 'blue'); + + $this->product('apparel', 'Red Shirt', 1000, ['red']); + $this->product('apparel', 'Blue Shirt', 2000, ['blue']); + $this->product('apparel', 'Crimson Coat', 3000, ['red']); + $this->product('home', 'Wool Rug', 1500, []); + } + + private function product(string $category, string $name, int $priceCents, array $colors): int + { + $productId = $this->products->create([ + 'categoryId' => $this->categoryIds[$category], + 'name' => $name, + 'slug' => slugify($name), + 'description' => '', + 'priceCents' => $priceCents, + 'stockQuantity' => 10, + 'imageUrl' => '', + 'isActive' => true, + ]); + foreach ($colors as $index => $color) { + $variantId = $this->variants->create($productId, slugify($name) . '-' . $color, $priceCents, 5); + $this->variants->setOption($variantId, $this->colorId, $this->colorValueIds[$color]); + } + return $productId; + } + + public function testTotalReflectsAllMatchesNotJustThePage(): void + { + $result = $this->search->search(['perPage' => 2, 'page' => 1]); + + $this->assertSame(4, $result['total']); + $this->assertSame(2, $result['pages']); + $this->assertCount(2, $result['products'], 'page is limited to perPage'); + } + + public function testSecondPageReturnsRemainder(): void + { + $page2 = $this->search->search(['perPage' => 3, 'page' => 2]); + $this->assertSame(4, $page2['total']); + $this->assertCount(1, $page2['products']); + } + + public function testCategoryFilterNarrowsResults(): void + { + $result = $this->search->search(['categorySlug' => 'apparel']); + $this->assertSame(3, $result['total']); + } + + public function testPriceRangeFilter(): void + { + $result = $this->search->search(['minPriceCents' => 1000, 'maxPriceCents' => 2000]); + $names = array_column($result['products'], 'name'); + + $this->assertSame(3, $result['total']); + $this->assertContains('Wool Rug', $names); + $this->assertNotContains('Crimson Coat', $names); + } + + public function testAttributeFilterMatchesOnVariantOption(): void + { + $result = $this->search->search(['attributeSelections' => ['color' => ['red']]]); + $names = array_column($result['products'], 'name'); + + $this->assertSame(2, $result['total']); + $this->assertEqualsCanonicalizing(['Red Shirt', 'Crimson Coat'], $names); + } + + public function testCategoryFacetCounts(): void + { + $facet = $this->search->search([])['facets']['categories']; + $byName = []; + foreach ($facet as $entry) { + $byName[$entry['name']] = $entry['count']; + } + + $this->assertSame(3, $byName['Apparel']); + $this->assertSame(1, $byName['Home']); + } + + public function testAttributeFacetCountsIgnoreOwnSelection(): void + { + $result = $this->search->search(['attributeSelections' => ['color' => ['red']]]); + $colorFacet = null; + foreach ($result['facets']['attributes'] as $attribute) { + if ($attribute['slug'] === 'color') { + $colorFacet = $attribute; + } + } + $counts = []; + foreach ($colorFacet['values'] as $value) { + $counts[$value['slug']] = ['count' => $value['count'], 'selected' => $value['selected']]; + } + + $this->assertSame(2, $counts['red']['count']); + $this->assertSame(1, $counts['blue']['count'], 'blue count is not zeroed by the red selection'); + $this->assertTrue($counts['red']['selected']); + $this->assertFalse($counts['blue']['selected']); + } + + public function testPriceSortAscending(): void + { + $result = $this->search->search(['sort' => 'priceAsc', 'perPage' => 10]); + $prices = array_map('intval', array_column($result['products'], 'priceCents')); + + $sorted = $prices; + sort($sorted); + $this->assertSame($sorted, $prices); + } + + public function testRatingSortSurfacesTopRated(): void + { + $users = new UserRepository($this->db); + $userId = $users->create('r@bazaar.test', 'hash', 'R'); + $blueShirt = $this->products->findBySlug('blue-shirt'); + $reviewId = $this->reviews->create((int) $blueShirt['id'], $userId, 5, 'Top', 'Best'); + $this->reviews->setStatus($reviewId, 'approved'); + + $result = $this->search->search(['sort' => 'rating', 'perPage' => 10]); + $this->assertSame('Blue Shirt', $result['products'][0]['name']); + $this->assertSame(5.0, $result['products'][0]['ratingAverage']); + } + + public function testPriceRangeFacetBounds(): void + { + $range = $this->search->search([])['facets']['priceRange']; + $this->assertSame(1000, $range['minCents']); + $this->assertSame(3000, $range['maxCents']); + } +}