ver. 0.302: REST API product variants, attributes dictionary, attribute filtering

- Add variant CRUD endpoints (variants, create_variant, update_variant, delete_variant)
- Add dictionaries/attributes endpoint with multilingual names and values
- Add attribute_* filter params for product list filtering by attribute values
- Enrich product detail attributes with translated names (attribute_names, value_names)
- Include variants array in product detail response for parent products
- Add price_brutto validation on product create
- Batch-load attribute/value translations (4 queries instead of N+1)
- Add 43 new unit tests (730 total, 2066 assertions)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-22 14:42:52 +01:00
parent 38cc4f28c1
commit 6fdbe61916
18 changed files with 1721 additions and 22 deletions

View File

@@ -3,6 +3,7 @@ namespace Tests\Unit\api\Controllers;
use PHPUnit\Framework\TestCase;
use api\Controllers\DictionariesApiController;
use Domain\Attribute\AttributeRepository;
use Domain\ShopStatus\ShopStatusRepository;
use Domain\Transport\TransportRepository;
use Domain\PaymentMethod\PaymentMethodRepository;
@@ -12,6 +13,7 @@ class DictionariesApiControllerTest extends TestCase
private $mockStatusRepo;
private $mockTransportRepo;
private $mockPaymentRepo;
private $mockAttrRepo;
private $controller;
protected function setUp(): void
@@ -19,11 +21,13 @@ class DictionariesApiControllerTest extends TestCase
$this->mockStatusRepo = $this->createMock(ShopStatusRepository::class);
$this->mockTransportRepo = $this->createMock(TransportRepository::class);
$this->mockPaymentRepo = $this->createMock(PaymentMethodRepository::class);
$this->mockAttrRepo = $this->createMock(AttributeRepository::class);
$this->controller = new DictionariesApiController(
$this->mockStatusRepo,
$this->mockTransportRepo,
$this->mockPaymentRepo
$this->mockPaymentRepo,
$this->mockAttrRepo
);
$_SERVER['REQUEST_METHOD'] = 'GET';
@@ -136,4 +140,50 @@ class DictionariesApiControllerTest extends TestCase
$this->assertSame(405, http_response_code());
}
// --- attributes ---
public function testAttributesReturnsFormattedList(): void
{
$this->mockAttrRepo->method('listForApi')
->willReturn([
[
'id' => 5,
'type' => 0,
'status' => 1,
'names' => ['pl' => 'Rozmiar', 'en' => 'Size'],
'values' => [
[
'id' => 12,
'names' => ['pl' => 'M', 'en' => 'M'],
'is_default' => 1,
'impact_on_the_price' => null,
],
],
],
]);
ob_start();
$this->controller->attributes();
$output = ob_get_clean();
$json = json_decode($output, true);
$this->assertSame('ok', $json['status']);
$this->assertCount(1, $json['data']);
$this->assertSame(5, $json['data'][0]['id']);
$this->assertSame('Rozmiar', $json['data'][0]['names']['pl']);
$this->assertCount(1, $json['data'][0]['values']);
$this->assertSame(12, $json['data'][0]['values'][0]['id']);
}
public function testAttributesRejectsPostMethod(): void
{
$_SERVER['REQUEST_METHOD'] = 'POST';
ob_start();
$this->controller->attributes();
$output = ob_get_clean();
$this->assertSame(405, http_response_code());
}
}

View File

@@ -3,17 +3,20 @@ namespace Tests\Unit\api\Controllers;
use PHPUnit\Framework\TestCase;
use api\Controllers\ProductsApiController;
use Domain\Attribute\AttributeRepository;
use Domain\Product\ProductRepository;
class ProductsApiControllerTest extends TestCase
{
private $mockRepo;
private $mockAttrRepo;
private $controller;
protected function setUp(): void
{
$this->mockRepo = $this->createMock(ProductRepository::class);
$this->controller = new ProductsApiController($this->mockRepo);
$this->mockAttrRepo = $this->createMock(AttributeRepository::class);
$this->controller = new ProductsApiController($this->mockRepo, $this->mockAttrRepo);
$_SERVER['REQUEST_METHOD'] = 'GET';
$_GET = [];
@@ -405,4 +408,257 @@ class ProductsApiControllerTest extends TestCase
$this->assertSame(7, $result['producer_id']);
$this->assertSame(2, $result['product_unit']);
}
// --- variants ---
public function testVariantsReturnsVariantsList(): void
{
$_SERVER['REQUEST_METHOD'] = 'GET';
$_GET['id'] = '1';
$this->mockRepo->method('find')
->with(1)
->willReturn(['id' => 1, 'parent_id' => null]);
$this->mockRepo->method('findVariantsForApi')
->with(1)
->willReturn([
[
'id' => 101,
'permutation_hash' => '5-12',
'sku' => 'SKU-M',
'attributes' => [['attribute_id' => 5, 'value_id' => 12]],
],
]);
$this->mockAttrRepo->method('listForApi')
->willReturn([
['id' => 5, 'type' => 0, 'status' => 1, 'names' => ['pl' => 'Rozmiar'], 'values' => []],
]);
ob_start();
$this->controller->variants();
$output = ob_get_clean();
$json = json_decode($output, true);
$this->assertSame('ok', $json['status']);
$this->assertSame(1, $json['data']['product_id']);
$this->assertCount(1, $json['data']['variants']);
$this->assertCount(1, $json['data']['available_attributes']);
}
public function testVariantsReturns400WhenMissingId(): void
{
$_SERVER['REQUEST_METHOD'] = 'GET';
ob_start();
$this->controller->variants();
$output = ob_get_clean();
$this->assertSame(400, http_response_code());
}
public function testVariantsReturns404WhenProductNotFound(): void
{
$_SERVER['REQUEST_METHOD'] = 'GET';
$_GET['id'] = '999';
$this->mockRepo->method('find')->willReturn(null);
ob_start();
$this->controller->variants();
$output = ob_get_clean();
$this->assertSame(404, http_response_code());
}
public function testVariantsReturns400ForVariantProduct(): void
{
$_SERVER['REQUEST_METHOD'] = 'GET';
$_GET['id'] = '101';
$this->mockRepo->method('find')
->with(101)
->willReturn(['id' => 101, 'parent_id' => 1]);
ob_start();
$this->controller->variants();
$output = ob_get_clean();
$this->assertSame(400, http_response_code());
}
public function testVariantsRejectsPostMethod(): void
{
$_SERVER['REQUEST_METHOD'] = 'POST';
$_GET['id'] = '1';
ob_start();
$this->controller->variants();
$output = ob_get_clean();
$this->assertSame(405, http_response_code());
}
// --- create_variant ---
public function testCreateVariantRejectsGetMethod(): void
{
$_SERVER['REQUEST_METHOD'] = 'GET';
$_GET['id'] = '1';
ob_start();
$this->controller->create_variant();
$output = ob_get_clean();
$this->assertSame(405, http_response_code());
}
public function testCreateVariantReturns400WhenMissingId(): void
{
$_SERVER['REQUEST_METHOD'] = 'POST';
ob_start();
$this->controller->create_variant();
$output = ob_get_clean();
$this->assertSame(400, http_response_code());
}
public function testCreateVariantReturns400WhenNoBody(): void
{
$_SERVER['REQUEST_METHOD'] = 'POST';
$_GET['id'] = '1';
ob_start();
$this->controller->create_variant();
$output = ob_get_clean();
$this->assertSame(400, http_response_code());
}
// --- update_variant ---
public function testUpdateVariantRejectsGetMethod(): void
{
$_SERVER['REQUEST_METHOD'] = 'GET';
$_GET['id'] = '101';
ob_start();
$this->controller->update_variant();
$output = ob_get_clean();
$this->assertSame(405, http_response_code());
}
public function testUpdateVariantReturns400WhenMissingId(): void
{
$_SERVER['REQUEST_METHOD'] = 'PUT';
ob_start();
$this->controller->update_variant();
$output = ob_get_clean();
$this->assertSame(400, http_response_code());
}
public function testUpdateVariantReturns400WhenNoBody(): void
{
$_SERVER['REQUEST_METHOD'] = 'PUT';
$_GET['id'] = '101';
ob_start();
$this->controller->update_variant();
$output = ob_get_clean();
$this->assertSame(400, http_response_code());
}
// --- delete_variant ---
public function testDeleteVariantRejectsGetMethod(): void
{
$_SERVER['REQUEST_METHOD'] = 'GET';
$_GET['id'] = '101';
ob_start();
$this->controller->delete_variant();
$output = ob_get_clean();
$this->assertSame(405, http_response_code());
}
public function testDeleteVariantReturns400WhenMissingId(): void
{
$_SERVER['REQUEST_METHOD'] = 'DELETE';
ob_start();
$this->controller->delete_variant();
$output = ob_get_clean();
$this->assertSame(400, http_response_code());
}
public function testDeleteVariantReturns404WhenNotFound(): void
{
$_SERVER['REQUEST_METHOD'] = 'DELETE';
$_GET['id'] = '999';
$this->mockRepo->method('deleteVariantForApi')
->with(999)
->willReturn(false);
ob_start();
$this->controller->delete_variant();
$output = ob_get_clean();
$this->assertSame(404, http_response_code());
}
public function testDeleteVariantSuccess(): void
{
$_SERVER['REQUEST_METHOD'] = 'DELETE';
$_GET['id'] = '101';
$this->mockRepo->method('deleteVariantForApi')
->with(101)
->willReturn(true);
ob_start();
$this->controller->delete_variant();
$output = ob_get_clean();
$json = json_decode($output, true);
$this->assertSame('ok', $json['status']);
$this->assertSame(101, $json['data']['id']);
$this->assertTrue($json['data']['deleted']);
}
// --- list with attribute filter ---
public function testListPassesAttributeFilters(): void
{
$_SERVER['REQUEST_METHOD'] = 'GET';
$_GET['attribute_5'] = '12';
$_GET['attribute_7'] = '18';
$this->mockRepo->expects($this->once())
->method('listForApi')
->with(
$this->callback(function ($filters) {
return isset($filters['attributes'])
&& $filters['attributes'][5] === 12
&& $filters['attributes'][7] === 18;
}),
$this->anything(),
$this->anything(),
$this->anything(),
$this->anything()
)
->willReturn(['items' => [], 'total' => 0, 'page' => 1, 'per_page' => 50]);
ob_start();
$this->controller->list();
ob_get_clean();
}
}