$name, 'slug' => Str::slug($name), 'is_visible' => true, 'price_amount' => 100.00, 'stock' => 10, 'parent_id' => null, ], $attributes)); } function createCategoryForTest(array $attributes = []): Category { $name = $attributes['name'] ?? 'Test Category '.Str::random(5); $nameValue = is_array($name) ? $name : ['en' => $name]; $slug = Str::slug(is_array($name) ? ($name['en'] ?? reset($name)) : $name); return Category::create(array_merge([ 'name' => $nameValue, 'slug' => $slug, 'is_visible' => true, 'sort_order' => 1, 'type' => 'default', ], $attributes)); } test('can list products', function () { // Arrange createProductForTest(['name' => 'Product 1']); createProductForTest(['name' => 'Product 2']); createProductForTest(['name' => 'Product 3']); // Act $response = $this->withHeaders([ 'Api-Token' => 'test-token', 'Accept' => 'application/json', ])->getJson('/api/v1/products'); // Assert $response->assertOk() ->assertJsonStructure([ 'data' => [ '*' => [ 'id', 'name', 'slug', 'price_amount', ], ], ]); $this->assertCount(3, $response->json('data')); }); test('can paginate products', function () { // Arrange collect(range(1, 10))->each(fn () => createProductForTest()); // Act $response = $this->withHeaders([ 'Api-Token' => 'test-token', 'Accept' => 'application/json', ])->getJson('/api/v1/products?perPage=5'); // Assert $response->assertOk(); $this->assertCount(5, $response->json('data')); }); test('does not list invisible products', function () { // Arrange createProductForTest(['name' => 'Visible', 'is_visible' => true]); createProductForTest(['name' => 'Invisible', 'is_visible' => false]); // Act $response = $this->withHeaders([ 'Api-Token' => 'test-token', 'Accept' => 'application/json', ])->getJson('/api/v1/products'); // Assert $response->assertOk(); $this->assertCount(1, $response->json('data')); $this->assertEquals('Visible', $response->json('data.0.name')); }); test('does not list out of stock products', function () { // Arrange createProductForTest(['name' => 'In Stock', 'stock' => 10]); createProductForTest(['name' => 'Out of Stock', 'stock' => 0]); // Act $response = $this->withHeaders([ 'Api-Token' => 'test-token', 'Accept' => 'application/json', ])->getJson('/api/v1/products'); // Assert $response->assertOk(); $this->assertCount(1, $response->json('data')); $this->assertEquals('In Stock', $response->json('data.0.name')); }); test('does not list child products (variants)', function () { // Arrange $parent = createProductForTest(['name' => 'Parent']); createProductForTest(['name' => 'Child', 'parent_id' => $parent->id]); // Act $response = $this->withHeaders([ 'Api-Token' => 'test-token', 'Accept' => 'application/json', ])->getJson('/api/v1/products'); // Assert $response->assertOk(); $this->assertCount(1, $response->json('data')); $this->assertEquals('Parent', $response->json('data.0.name')); }); test('can show a specific product', function () { // Arrange $product = createProductForTest(['name' => 'Test Product']); // Act $response = $this->withHeaders([ 'Api-Token' => 'test-token', 'Accept' => 'application/json', ])->getJson("/api/v1/products/{$product->id}"); // Assert $response->assertOk() ->assertJson([ 'data' => [ 'id' => $product->id, 'name' => 'Test Product', ], ]); }); test('returns 404 for non-existent product', function () { // Act $response = $this->withHeaders([ 'Api-Token' => 'test-token', 'Accept' => 'application/json', ])->getJson('/api/v1/products/99999'); // Assert $response->assertNotFound(); }); test('returns 404 for invalid product id format', function () { // Act $response = $this->withHeaders([ 'Api-Token' => 'test-token', 'Accept' => 'application/json', ])->getJson('/api/v1/products/invalid-id'); // Assert $response->assertNotFound(); }); test('can list related products', function () { // Arrange $category = createCategoryForTest(); $mainProduct = createProductForTest(['name' => 'Main Product']); $relatedProduct1 = createProductForTest(['name' => 'Related 1']); $relatedProduct2 = createProductForTest(['name' => 'Related 2']); $unrelatedProduct = createProductForTest(['name' => 'Unrelated']); // Attach products to category // Note: ProductRelatedController looks for products in the same category $category->products()->attach([ $mainProduct->id, $relatedProduct1->id, $relatedProduct2->id, ]); // Act $response = $this->withHeaders([ 'Api-Token' => 'test-token', 'Accept' => 'application/json', ])->getJson("/api/v1/products/{$mainProduct->id}/related"); // Assert $response->assertOk() ->assertJsonStructure([ 'data' => [ '*' => [ 'id', 'name', ], ], ]); // Should contain related products but not the main product itself (usually) // The query in ProductRelatedController: // where('product_id', '=', $product->id) -> This selects categories WHERE the main product is present // Then it selects product_ids from those categories. // It doesn't explicitly exclude the main product ID in the query shown in tool output? // Let's check the controller code again. // It selects `category_product.product_id` where `category_id` IN (categories of main product). // It does NOT seem to exclude the main product ID in the query. // However, `unique()` is used. // If the main product is returned, count would be 3. If excluded, 2. // Let's assert that we get some products back. $data = $response->json('data'); $this->assertGreaterThanOrEqual(2, count($data)); // Check if unrelated product is NOT in the list $ids = collect($data)->pluck('id'); $this->assertNotContains($unrelatedProduct->id, $ids); $this->assertContains($relatedProduct1->id, $ids); }); test('returns empty related products if no shared categories', function () { // Arrange $mainProduct = createProductForTest(['name' => 'Main Product']); $otherProduct = createProductForTest(['name' => 'Other Product']); // No categories attached // Act $response = $this->withHeaders([ 'Api-Token' => 'test-token', 'Accept' => 'application/json', ])->getJson("/api/v1/products/{$mainProduct->id}/related"); // Assert $response->assertOk(); $this->assertCount(0, $response->json('data')); });