2025-11-25 16:14:00 +00:00
|
|
|
<?php
|
|
|
|
|
|
2025-12-30 09:55:06 +00:00
|
|
|
namespace Blax\Shop\Tests\Feature\Product;
|
2025-11-25 16:14:00 +00:00
|
|
|
|
2026-05-15 18:26:24 +00:00
|
|
|
use Blax\Shop\Enums\ProductAttributeType;
|
2025-11-25 16:14:00 +00:00
|
|
|
use Blax\Shop\Models\Product;
|
|
|
|
|
use Blax\Shop\Models\ProductAttribute;
|
|
|
|
|
use Blax\Shop\Tests\TestCase;
|
|
|
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
2025-12-24 18:40:10 +00:00
|
|
|
use PHPUnit\Framework\Attributes\Test;
|
2025-11-25 16:14:00 +00:00
|
|
|
|
|
|
|
|
class ProductAttributeTest extends TestCase
|
|
|
|
|
{
|
|
|
|
|
use RefreshDatabase;
|
|
|
|
|
|
2025-12-24 18:40:10 +00:00
|
|
|
#[Test]
|
2025-11-25 16:14:00 +00:00
|
|
|
public function it_can_create_a_product_attribute()
|
|
|
|
|
{
|
|
|
|
|
$product = Product::factory()->create();
|
|
|
|
|
|
|
|
|
|
$attribute = ProductAttribute::create([
|
|
|
|
|
'product_id' => $product->id,
|
|
|
|
|
'key' => 'Material',
|
|
|
|
|
'value' => 'Cotton',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->assertDatabaseHas('product_attributes', [
|
|
|
|
|
'id' => $attribute->id,
|
|
|
|
|
'product_id' => $product->id,
|
|
|
|
|
'key' => 'Material',
|
|
|
|
|
'value' => 'Cotton',
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-24 18:40:10 +00:00
|
|
|
#[Test]
|
2025-11-25 16:14:00 +00:00
|
|
|
public function attribute_belongs_to_product()
|
|
|
|
|
{
|
|
|
|
|
$product = Product::factory()->create();
|
|
|
|
|
|
|
|
|
|
$attribute = ProductAttribute::create([
|
|
|
|
|
'product_id' => $product->id,
|
|
|
|
|
'key' => 'Color',
|
|
|
|
|
'value' => 'Red',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->assertInstanceOf(Product::class, $attribute->product);
|
|
|
|
|
$this->assertEquals($product->id, $attribute->product->id);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-24 18:40:10 +00:00
|
|
|
#[Test]
|
2025-11-25 16:14:00 +00:00
|
|
|
public function product_can_have_multiple_attributes()
|
|
|
|
|
{
|
|
|
|
|
$product = Product::factory()->create();
|
|
|
|
|
|
|
|
|
|
ProductAttribute::create([
|
|
|
|
|
'product_id' => $product->id,
|
|
|
|
|
'key' => 'Size',
|
|
|
|
|
'value' => 'Large',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
ProductAttribute::create([
|
|
|
|
|
'product_id' => $product->id,
|
|
|
|
|
'key' => 'Color',
|
|
|
|
|
'value' => 'Blue',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
ProductAttribute::create([
|
|
|
|
|
'product_id' => $product->id,
|
|
|
|
|
'key' => 'Material',
|
|
|
|
|
'value' => 'Polyester',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->assertCount(3, $product->fresh()->attributes);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-24 18:40:10 +00:00
|
|
|
#[Test]
|
2025-11-25 16:14:00 +00:00
|
|
|
public function it_can_have_a_sort_order()
|
|
|
|
|
{
|
|
|
|
|
$product = Product::factory()->create();
|
|
|
|
|
|
|
|
|
|
$attr1 = ProductAttribute::create([
|
|
|
|
|
'product_id' => $product->id,
|
|
|
|
|
'key' => 'First',
|
|
|
|
|
'value' => 'Value1',
|
|
|
|
|
'sort_order' => 1,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$attr2 = ProductAttribute::create([
|
|
|
|
|
'product_id' => $product->id,
|
|
|
|
|
'key' => 'Second',
|
|
|
|
|
'value' => 'Value2',
|
|
|
|
|
'sort_order' => 2,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$attributes = ProductAttribute::where('product_id', $product->id)
|
|
|
|
|
->orderBy('sort_order')
|
|
|
|
|
->get();
|
|
|
|
|
|
|
|
|
|
$this->assertEquals($attr1->id, $attributes[0]->id);
|
|
|
|
|
$this->assertEquals($attr2->id, $attributes[1]->id);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-24 18:40:10 +00:00
|
|
|
#[Test]
|
2025-11-25 16:14:00 +00:00
|
|
|
public function it_can_store_metadata()
|
|
|
|
|
{
|
|
|
|
|
$product = Product::factory()->create();
|
|
|
|
|
|
2025-11-25 16:25:20 +00:00
|
|
|
// Attributes now store structured data in value or as separate attributes
|
|
|
|
|
$dimensionAttr = ProductAttribute::create([
|
2025-11-25 16:14:00 +00:00
|
|
|
'product_id' => $product->id,
|
|
|
|
|
'key' => 'Dimensions',
|
|
|
|
|
'value' => '10x20x30',
|
|
|
|
|
]);
|
|
|
|
|
|
2025-11-25 16:25:20 +00:00
|
|
|
$unitAttr = ProductAttribute::create([
|
|
|
|
|
'product_id' => $product->id,
|
|
|
|
|
'key' => 'Dimension Unit',
|
|
|
|
|
'value' => 'cm',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->assertEquals('10x20x30', $dimensionAttr->value);
|
|
|
|
|
$this->assertEquals('cm', $unitAttr->value);
|
2025-11-25 16:14:00 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-24 18:40:10 +00:00
|
|
|
#[Test]
|
2025-11-25 16:14:00 +00:00
|
|
|
public function it_can_update_attribute_value()
|
|
|
|
|
{
|
|
|
|
|
$product = Product::factory()->create();
|
|
|
|
|
|
|
|
|
|
$attribute = ProductAttribute::create([
|
|
|
|
|
'product_id' => $product->id,
|
|
|
|
|
'key' => 'Stock Status',
|
|
|
|
|
'value' => 'In Stock',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$attribute->update(['value' => 'Out of Stock']);
|
|
|
|
|
|
|
|
|
|
$this->assertEquals('Out of Stock', $attribute->fresh()->value);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-24 18:40:10 +00:00
|
|
|
#[Test]
|
2025-11-25 16:14:00 +00:00
|
|
|
public function deleting_product_deletes_attributes()
|
|
|
|
|
{
|
|
|
|
|
$product = Product::factory()->create();
|
|
|
|
|
|
|
|
|
|
$attribute = ProductAttribute::create([
|
|
|
|
|
'product_id' => $product->id,
|
|
|
|
|
'key' => 'Test',
|
|
|
|
|
'value' => 'Value',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$attributeId = $attribute->id;
|
|
|
|
|
|
|
|
|
|
$product->delete();
|
|
|
|
|
|
|
|
|
|
$this->assertDatabaseMissing('product_attributes', ['id' => $attributeId]);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-24 18:40:10 +00:00
|
|
|
#[Test]
|
2025-11-25 16:14:00 +00:00
|
|
|
public function it_can_filter_attributes_by_key()
|
|
|
|
|
{
|
|
|
|
|
$product = Product::factory()->create();
|
|
|
|
|
|
|
|
|
|
ProductAttribute::create([
|
|
|
|
|
'product_id' => $product->id,
|
|
|
|
|
'key' => 'Color',
|
|
|
|
|
'value' => 'Red',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
ProductAttribute::create([
|
|
|
|
|
'product_id' => $product->id,
|
|
|
|
|
'key' => 'Color',
|
|
|
|
|
'value' => 'Blue',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
ProductAttribute::create([
|
|
|
|
|
'product_id' => $product->id,
|
|
|
|
|
'key' => 'Size',
|
|
|
|
|
'value' => 'Large',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$colorAttributes = ProductAttribute::where('product_id', $product->id)
|
|
|
|
|
->where('key', 'Color')
|
|
|
|
|
->get();
|
|
|
|
|
|
|
|
|
|
$this->assertCount(2, $colorAttributes);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-24 18:40:10 +00:00
|
|
|
#[Test]
|
2025-11-25 16:14:00 +00:00
|
|
|
public function multiple_products_can_have_same_attribute_keys()
|
|
|
|
|
{
|
|
|
|
|
$product1 = Product::factory()->create();
|
|
|
|
|
$product2 = Product::factory()->create();
|
|
|
|
|
|
|
|
|
|
ProductAttribute::create([
|
|
|
|
|
'product_id' => $product1->id,
|
|
|
|
|
'key' => 'Brand',
|
|
|
|
|
'value' => 'Brand A',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
ProductAttribute::create([
|
|
|
|
|
'product_id' => $product2->id,
|
|
|
|
|
'key' => 'Brand',
|
|
|
|
|
'value' => 'Brand B',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->assertCount(1, $product1->attributes);
|
|
|
|
|
$this->assertCount(1, $product2->attributes);
|
|
|
|
|
$this->assertEquals('Brand A', $product1->attributes->first()->value);
|
|
|
|
|
$this->assertEquals('Brand B', $product2->attributes->first()->value);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-15 18:26:24 +00:00
|
|
|
#[Test]
|
|
|
|
|
public function type_and_meta_round_trip_through_mass_assignment(): void
|
|
|
|
|
{
|
|
|
|
|
// Regression: `type` and `meta` were missing from $fillable, so
|
|
|
|
|
// create([... 'type' => 'color' ...]) silently dropped them and every
|
|
|
|
|
// attribute came back as the default 'text' type. The cast on `type`
|
|
|
|
|
// now also resolves it to the ProductAttributeType enum on read.
|
|
|
|
|
$product = Product::factory()->create();
|
|
|
|
|
|
|
|
|
|
$color = ProductAttribute::create([
|
|
|
|
|
'product_id' => $product->id,
|
|
|
|
|
'key' => 'Accent color',
|
|
|
|
|
'value' => '#1e293b',
|
|
|
|
|
'type' => ProductAttributeType::COLOR,
|
|
|
|
|
'meta' => ['display_as' => 'swatch'],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$fresh = $color->fresh();
|
|
|
|
|
|
|
|
|
|
$this->assertSame(ProductAttributeType::COLOR, $fresh->type);
|
|
|
|
|
$this->assertSame(['display_as' => 'swatch'], $fresh->meta);
|
|
|
|
|
$this->assertDatabaseHas('product_attributes', [
|
|
|
|
|
'id' => $fresh->id,
|
|
|
|
|
'type' => ProductAttributeType::COLOR->value,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-24 18:40:10 +00:00
|
|
|
#[Test]
|
2025-11-25 16:14:00 +00:00
|
|
|
public function attributes_are_hidden_in_api_responses()
|
|
|
|
|
{
|
|
|
|
|
$product = Product::factory()->create();
|
|
|
|
|
|
|
|
|
|
$attribute = ProductAttribute::create([
|
|
|
|
|
'product_id' => $product->id,
|
|
|
|
|
'key' => 'Secret',
|
|
|
|
|
'value' => 'Value',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$array = $attribute->toArray();
|
|
|
|
|
|
|
|
|
|
$this->assertArrayNotHasKey('id', $array);
|
|
|
|
|
$this->assertArrayNotHasKey('product_id', $array);
|
|
|
|
|
$this->assertArrayNotHasKey('created_at', $array);
|
|
|
|
|
$this->assertArrayNotHasKey('updated_at', $array);
|
|
|
|
|
}
|
|
|
|
|
}
|