count(3)->withPrices()->create([ 'status' => 'published', 'is_visible' => true, ]); // Should NOT appear: drafts, hidden, or trashed. Product::factory()->create(['status' => 'draft', 'is_visible' => true]); Product::factory()->create(['status' => 'published', 'is_visible' => false]); $response = $this->getJson(route('shop.products.index')); $response->assertOk(); $this->assertSame(3, $response->json('total')); $this->assertCount(3, $response->json('data')); } #[Test] public function products_index_honours_per_page_request_but_caps_to_max(): void { Product::factory()->count(8)->create([ 'status' => 'published', 'is_visible' => true, ]); // Within configured max — honoured verbatim. $small = $this->getJson(route('shop.products.index', ['per_page' => 2])); $this->assertSame(2, $small->json('per_page')); $this->assertCount(2, $small->json('data')); // Above the configured max — clamped down. $max = (int) config('shop.pagination.max_per_page'); $clamped = $this->getJson(route('shop.products.index', ['per_page' => $max + 999])); $this->assertLessThanOrEqual($max, (int) $clamped->json('per_page')); } #[Test] public function products_index_filters_by_category_slug(): void { $catA = ProductCategory::create(['name' => 'A', 'slug' => 'cat-a', 'is_visible' => true]); $catB = ProductCategory::create(['name' => 'B', 'slug' => 'cat-b', 'is_visible' => true]); $inA = Product::factory()->create(['status' => 'published', 'is_visible' => true]); $inB = Product::factory()->create(['status' => 'published', 'is_visible' => true]); $inA->categories()->attach($catA->id); $inB->categories()->attach($catB->id); $response = $this->getJson(route('shop.products.index', ['category' => 'cat-a'])); $response->assertOk(); $ids = array_column($response->json('data'), 'id'); $this->assertContains($inA->id, $ids); $this->assertNotContains($inB->id, $ids); } #[Test] public function products_index_filters_by_featured_flag(): void { $featured = Product::factory()->create([ 'status' => 'published', 'is_visible' => true, 'featured' => true, ]); $unfeatured = Product::factory()->create([ 'status' => 'published', 'is_visible' => true, 'featured' => false, ]); $response = $this->getJson(route('shop.products.index', ['featured' => 1])); $response->assertOk(); $ids = array_column($response->json('data'), 'id'); $this->assertContains($featured->id, $ids); $this->assertNotContains($unfeatured->id, $ids); } #[Test] public function products_index_filters_by_in_stock(): void { // Has ledger stock → considered in stock. $orderable = Product::factory()->withStocks(5)->create([ 'status' => 'published', 'is_visible' => true, 'manage_stock' => true, ]); // manage_stock=true with no ledger entries → out of stock. $depleted = Product::factory()->create([ 'status' => 'published', 'is_visible' => true, 'manage_stock' => true, ]); // manage_stock=false → always in stock by scope definition. $unlimited = Product::factory()->create([ 'status' => 'published', 'is_visible' => true, 'manage_stock' => false, ]); $response = $this->getJson(route('shop.products.index', ['in_stock' => 1])); $response->assertOk(); $ids = array_column($response->json('data'), 'id'); $this->assertContains($orderable->id, $ids); $this->assertContains($unlimited->id, $ids); $this->assertNotContains($depleted->id, $ids); } #[Test] public function products_show_returns_published_visible_product_by_slug(): void { $product = Product::factory()->withPrices()->create([ 'slug' => 'my-product', 'status' => 'published', 'is_visible' => true, ]); $response = $this->getJson(route('shop.products.show', ['slug' => 'my-product'])); $response->assertOk(); // show() wraps the model under `data` and merges helper fields onto it. $this->assertSame($product->id, $response->json('data.id')); $this->assertArrayHasKey('current_price', $response->json('data')); $this->assertArrayHasKey('on_sale', $response->json('data')); } #[Test] public function products_show_404s_on_unknown_or_invisible_slug(): void { Product::factory()->create([ 'slug' => 'hidden-product', 'status' => 'published', 'is_visible' => false, ]); $this->getJson(route('shop.products.show', ['slug' => 'hidden-product'])) ->assertNotFound(); $this->getJson(route('shop.products.show', ['slug' => 'does-not-exist'])) ->assertNotFound(); } // ------------------------------------------------------------------ // GET api/shop/categories // ------------------------------------------------------------------ #[Test] public function categories_index_returns_root_visible_categories_with_children(): void { $root = ProductCategory::create(['name' => 'Root', 'slug' => 'root', 'is_visible' => true, 'sort_order' => 1]); ProductCategory::create([ 'name' => 'Child', 'slug' => 'child', 'is_visible' => true, 'parent_id' => $root->id, 'sort_order' => 1, ]); // Hidden root + child — must not surface. ProductCategory::create(['name' => 'Hidden', 'slug' => 'hidden', 'is_visible' => false]); $response = $this->getJson(route('shop.categories.index')); $response->assertOk(); $data = $response->json('data'); $this->assertCount(1, $data, 'Only the visible root should be returned at the top level'); $this->assertSame('root', $data[0]['slug']); $this->assertCount(1, $data[0]['children'], 'Child relation should be eager-loaded onto the root'); $this->assertSame('child', $data[0]['children'][0]['slug']); } #[Test] public function categories_show_returns_category_by_slug_with_relations(): void { ProductCategory::create(['name' => 'C', 'slug' => 'c', 'is_visible' => true]); $response = $this->getJson(route('shop.categories.show', ['slug' => 'c'])); $response->assertOk(); $this->assertSame('c', $response->json('data.slug')); } #[Test] public function categories_show_404s_on_hidden_or_unknown(): void { ProductCategory::create(['name' => 'Hidden', 'slug' => 'hidden', 'is_visible' => false]); $this->getJson(route('shop.categories.show', ['slug' => 'hidden']))->assertNotFound(); $this->getJson(route('shop.categories.show', ['slug' => 'nope']))->assertNotFound(); } #[Test] public function categories_tree_returns_category_tree(): void { ProductCategory::create(['name' => 'A', 'slug' => 'a', 'is_visible' => true]); ProductCategory::create(['name' => 'B', 'slug' => 'b', 'is_visible' => true]); $response = $this->getJson(route('shop.categories.tree')); $response->assertOk(); $this->assertIsArray($response->json('data')); } }