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:
@@ -359,4 +359,81 @@ class AttributeRepositoryTest extends TestCase
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- listForApi ---
|
||||
|
||||
public function testListForApiReturnsActiveAttributesWithValues(): void
|
||||
{
|
||||
$mockDb = $this->createMock(\medoo::class);
|
||||
|
||||
$mockDb->method('select')
|
||||
->willReturnCallback(function ($table, $columns, $where) {
|
||||
if ($table === 'pp_shop_attributes') {
|
||||
return [
|
||||
['id' => '5', 'type' => '0', 'status' => '1'],
|
||||
];
|
||||
}
|
||||
if ($table === 'pp_shop_attributes_langs') {
|
||||
return [
|
||||
['attribute_id' => '5', 'lang_id' => 'pl', 'name' => 'Rozmiar'],
|
||||
['attribute_id' => '5', 'lang_id' => 'en', 'name' => 'Size'],
|
||||
];
|
||||
}
|
||||
if ($table === 'pp_shop_attributes_values') {
|
||||
return [
|
||||
['id' => '12', 'attribute_id' => '5', 'is_default' => '1', 'impact_on_the_price' => null],
|
||||
['id' => '13', 'attribute_id' => '5', 'is_default' => '0', 'impact_on_the_price' => '10.00'],
|
||||
];
|
||||
}
|
||||
if ($table === 'pp_shop_attributes_values_langs') {
|
||||
return [
|
||||
['value_id' => '12', 'lang_id' => 'pl', 'name' => 'M'],
|
||||
['value_id' => '12', 'lang_id' => 'en', 'name' => 'M'],
|
||||
['value_id' => '13', 'lang_id' => 'pl', 'name' => 'L'],
|
||||
['value_id' => '13', 'lang_id' => 'en', 'name' => 'L'],
|
||||
];
|
||||
}
|
||||
if ($table === 'pp_langs') {
|
||||
return [['id' => 'pl', 'start' => 1, 'o' => 1]];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
$repository = new AttributeRepository($mockDb);
|
||||
$result = $repository->listForApi();
|
||||
|
||||
$this->assertCount(1, $result);
|
||||
$this->assertSame(5, $result[0]['id']);
|
||||
$this->assertSame(0, $result[0]['type']);
|
||||
$this->assertSame(1, $result[0]['status']);
|
||||
$this->assertSame('Rozmiar', $result[0]['names']['pl']);
|
||||
$this->assertSame('Size', $result[0]['names']['en']);
|
||||
$this->assertCount(2, $result[0]['values']);
|
||||
$this->assertSame(12, $result[0]['values'][0]['id']);
|
||||
$this->assertSame(1, $result[0]['values'][0]['is_default']);
|
||||
$this->assertNull($result[0]['values'][0]['impact_on_the_price']);
|
||||
$this->assertSame(13, $result[0]['values'][1]['id']);
|
||||
$this->assertSame(10.0, $result[0]['values'][1]['impact_on_the_price']);
|
||||
}
|
||||
|
||||
public function testListForApiReturnsEmptyWhenNoAttributes(): void
|
||||
{
|
||||
$mockDb = $this->createMock(\medoo::class);
|
||||
|
||||
$mockDb->method('select')
|
||||
->willReturnCallback(function ($table) {
|
||||
if ($table === 'pp_shop_attributes') {
|
||||
return [];
|
||||
}
|
||||
if ($table === 'pp_langs') {
|
||||
return [['id' => 'pl', 'start' => 1, 'o' => 1]];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
$repository = new AttributeRepository($mockDb);
|
||||
$result = $repository->listForApi();
|
||||
|
||||
$this->assertSame([], $result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -877,4 +877,419 @@ class ProductRepositoryTest extends TestCase
|
||||
|
||||
$this->assertEquals([], $repository->promotedProductIdsCached(6));
|
||||
}
|
||||
|
||||
// --- findVariantsForApi ---
|
||||
|
||||
public function testFindVariantsForApiReturnsVariants(): void
|
||||
{
|
||||
$mockDb = $this->createMock(\medoo::class);
|
||||
|
||||
$mockStmt = new class {
|
||||
public function fetchAll($mode = null): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'id' => '101',
|
||||
'permutation_hash' => '5-12|7-18',
|
||||
'sku' => 'SKU-M-RED',
|
||||
'ean' => null,
|
||||
'price_brutto' => '109.99',
|
||||
'price_brutto_promo' => null,
|
||||
'price_netto' => '89.42',
|
||||
'price_netto_promo' => null,
|
||||
'quantity' => '5',
|
||||
'stock_0_buy' => '0',
|
||||
'weight' => null,
|
||||
'status' => '1',
|
||||
],
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
$mockDb->method('query')->willReturn($mockStmt);
|
||||
$mockDb->method('select')->willReturnCallback(function ($table, $columns, $where) {
|
||||
if ($table === 'pp_shop_products_attributes') {
|
||||
return [
|
||||
['product_id' => '101', 'attribute_id' => '5', 'value_id' => '12'],
|
||||
['product_id' => '101', 'attribute_id' => '7', 'value_id' => '18'],
|
||||
];
|
||||
}
|
||||
if ($table === 'pp_shop_attributes_langs') {
|
||||
return [
|
||||
['attribute_id' => '5', 'lang_id' => 'pl', 'name' => 'Rozmiar'],
|
||||
['attribute_id' => '7', 'lang_id' => 'pl', 'name' => 'Kolor'],
|
||||
];
|
||||
}
|
||||
if ($table === 'pp_shop_attributes_values_langs') {
|
||||
return [
|
||||
['value_id' => '12', 'lang_id' => 'pl', 'name' => 'M'],
|
||||
['value_id' => '18', 'lang_id' => 'pl', 'name' => 'Czerwony'],
|
||||
];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
$mockDb->method('get')->willReturn(0);
|
||||
|
||||
$repository = new ProductRepository($mockDb);
|
||||
$result = $repository->findVariantsForApi(1);
|
||||
|
||||
$this->assertCount(1, $result);
|
||||
$this->assertSame(101, $result[0]['id']);
|
||||
$this->assertSame('5-12|7-18', $result[0]['permutation_hash']);
|
||||
$this->assertSame('SKU-M-RED', $result[0]['sku']);
|
||||
$this->assertSame(109.99, $result[0]['price_brutto']);
|
||||
$this->assertSame(5, $result[0]['quantity']);
|
||||
$this->assertCount(2, $result[0]['attributes']);
|
||||
}
|
||||
|
||||
public function testFindVariantsForApiReturnsEmptyWhenNoVariants(): void
|
||||
{
|
||||
$mockDb = $this->createMock(\medoo::class);
|
||||
|
||||
$mockStmt = new class {
|
||||
public function fetchAll($mode = null): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
$mockDb->method('query')->willReturn($mockStmt);
|
||||
|
||||
$repository = new ProductRepository($mockDb);
|
||||
$result = $repository->findVariantsForApi(1);
|
||||
|
||||
$this->assertSame([], $result);
|
||||
}
|
||||
|
||||
// --- findVariantForApi ---
|
||||
|
||||
public function testFindVariantForApiReturnsVariant(): void
|
||||
{
|
||||
$mockDb = $this->createMock(\medoo::class);
|
||||
|
||||
$mockDb->method('get')->willReturnCallback(function ($table, $columns, $where) {
|
||||
if ($table === 'pp_shop_products') {
|
||||
return [
|
||||
'id' => '101',
|
||||
'parent_id' => '1',
|
||||
'permutation_hash' => '5-12',
|
||||
'sku' => 'SKU-M',
|
||||
'ean' => null,
|
||||
'price_brutto' => '99.99',
|
||||
'price_brutto_promo' => null,
|
||||
'price_netto' => '81.29',
|
||||
'price_netto_promo' => null,
|
||||
'quantity' => '10',
|
||||
'stock_0_buy' => '0',
|
||||
'weight' => null,
|
||||
'status' => '1',
|
||||
];
|
||||
}
|
||||
return null;
|
||||
});
|
||||
$mockDb->method('select')->willReturnCallback(function ($table) {
|
||||
if ($table === 'pp_shop_products_attributes') {
|
||||
return [['attribute_id' => '5', 'value_id' => '12']];
|
||||
}
|
||||
if ($table === 'pp_shop_attributes_langs') {
|
||||
return [['attribute_id' => '5', 'lang_id' => 'pl', 'name' => 'Rozmiar']];
|
||||
}
|
||||
if ($table === 'pp_shop_attributes_values_langs') {
|
||||
return [['value_id' => '12', 'lang_id' => 'pl', 'name' => 'M']];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
$repository = new ProductRepository($mockDb);
|
||||
$result = $repository->findVariantForApi(101);
|
||||
|
||||
$this->assertNotNull($result);
|
||||
$this->assertSame(101, $result['id']);
|
||||
$this->assertSame(1, $result['parent_id']);
|
||||
$this->assertSame('5-12', $result['permutation_hash']);
|
||||
$this->assertSame(99.99, $result['price_brutto']);
|
||||
$this->assertCount(1, $result['attributes']);
|
||||
}
|
||||
|
||||
public function testFindVariantForApiReturnsNullForNonVariant(): void
|
||||
{
|
||||
$mockDb = $this->createMock(\medoo::class);
|
||||
|
||||
$mockDb->method('get')->willReturn([
|
||||
'id' => '1',
|
||||
'parent_id' => null,
|
||||
'permutation_hash' => '',
|
||||
]);
|
||||
|
||||
$repository = new ProductRepository($mockDb);
|
||||
$result = $repository->findVariantForApi(1);
|
||||
|
||||
$this->assertNull($result);
|
||||
}
|
||||
|
||||
public function testFindVariantForApiReturnsNullForNonexistent(): void
|
||||
{
|
||||
$mockDb = $this->createMock(\medoo::class);
|
||||
$mockDb->method('get')->willReturn(null);
|
||||
|
||||
$repository = new ProductRepository($mockDb);
|
||||
$result = $repository->findVariantForApi(999);
|
||||
|
||||
$this->assertNull($result);
|
||||
}
|
||||
|
||||
// --- createVariantForApi ---
|
||||
|
||||
public function testCreateVariantForApiSuccess(): void
|
||||
{
|
||||
$mockDb = $this->createMock(\medoo::class);
|
||||
|
||||
$callCount = 0;
|
||||
$mockDb->method('get')->willReturnCallback(function ($table, $columns, $where) use (&$callCount) {
|
||||
$callCount++;
|
||||
if ($callCount === 1) {
|
||||
// Parent exists
|
||||
return ['id' => '1', 'archive' => '0', 'parent_id' => null, 'vat' => '23'];
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
$mockDb->method('count')->willReturn(0);
|
||||
$mockDb->method('insert')->willReturn(true);
|
||||
$mockDb->method('id')->willReturn(101);
|
||||
|
||||
$repository = new ProductRepository($mockDb);
|
||||
$result = $repository->createVariantForApi(1, [
|
||||
'attributes' => [5 => 12, 7 => 18],
|
||||
'sku' => 'SKU-M-RED',
|
||||
'price_brutto' => 109.99,
|
||||
'quantity' => 5,
|
||||
]);
|
||||
|
||||
$this->assertNotNull($result);
|
||||
$this->assertSame(101, $result['id']);
|
||||
$this->assertSame('5-12|7-18', $result['permutation_hash']);
|
||||
}
|
||||
|
||||
public function testCreateVariantForApiReturnsNullForArchivedParent(): void
|
||||
{
|
||||
$mockDb = $this->createMock(\medoo::class);
|
||||
|
||||
$mockDb->method('get')->willReturn([
|
||||
'id' => '1', 'archive' => '1', 'parent_id' => null, 'vat' => '23',
|
||||
]);
|
||||
|
||||
$repository = new ProductRepository($mockDb);
|
||||
$result = $repository->createVariantForApi(1, [
|
||||
'attributes' => [5 => 12],
|
||||
]);
|
||||
|
||||
$this->assertNull($result);
|
||||
}
|
||||
|
||||
public function testCreateVariantForApiReturnsNullWhenParentIsVariant(): void
|
||||
{
|
||||
$mockDb = $this->createMock(\medoo::class);
|
||||
|
||||
$mockDb->method('get')->willReturn([
|
||||
'id' => '101', 'archive' => '0', 'parent_id' => '1', 'vat' => '23',
|
||||
]);
|
||||
|
||||
$repository = new ProductRepository($mockDb);
|
||||
$result = $repository->createVariantForApi(101, [
|
||||
'attributes' => [5 => 12],
|
||||
]);
|
||||
|
||||
$this->assertNull($result);
|
||||
}
|
||||
|
||||
public function testCreateVariantForApiReturnsNullForEmptyAttributes(): void
|
||||
{
|
||||
$mockDb = $this->createMock(\medoo::class);
|
||||
|
||||
$mockDb->method('get')->willReturn([
|
||||
'id' => '1', 'archive' => '0', 'parent_id' => null, 'vat' => '23',
|
||||
]);
|
||||
|
||||
$repository = new ProductRepository($mockDb);
|
||||
$result = $repository->createVariantForApi(1, ['attributes' => []]);
|
||||
|
||||
$this->assertNull($result);
|
||||
}
|
||||
|
||||
public function testCreateVariantForApiReturnsNullForDuplicateHash(): void
|
||||
{
|
||||
$mockDb = $this->createMock(\medoo::class);
|
||||
|
||||
$mockDb->method('get')->willReturn([
|
||||
'id' => '1', 'archive' => '0', 'parent_id' => null, 'vat' => '23',
|
||||
]);
|
||||
$mockDb->method('count')->willReturn(1);
|
||||
|
||||
$repository = new ProductRepository($mockDb);
|
||||
$result = $repository->createVariantForApi(1, [
|
||||
'attributes' => [5 => 12],
|
||||
]);
|
||||
|
||||
$this->assertNull($result);
|
||||
}
|
||||
|
||||
// --- updateVariantForApi ---
|
||||
|
||||
public function testUpdateVariantForApiSuccess(): void
|
||||
{
|
||||
$mockDb = $this->createMock(\medoo::class);
|
||||
|
||||
$mockDb->method('get')->willReturn([
|
||||
'id' => '101', 'parent_id' => '1',
|
||||
]);
|
||||
|
||||
$mockDb->expects($this->once())->method('update');
|
||||
|
||||
$repository = new ProductRepository($mockDb);
|
||||
$result = $repository->updateVariantForApi(101, [
|
||||
'sku' => 'NEW-SKU',
|
||||
'price_brutto' => 119.99,
|
||||
]);
|
||||
|
||||
$this->assertTrue($result);
|
||||
}
|
||||
|
||||
public function testUpdateVariantForApiReturnsFalseForNonVariant(): void
|
||||
{
|
||||
$mockDb = $this->createMock(\medoo::class);
|
||||
|
||||
$mockDb->method('get')->willReturn([
|
||||
'id' => '1', 'parent_id' => null,
|
||||
]);
|
||||
|
||||
$repository = new ProductRepository($mockDb);
|
||||
$result = $repository->updateVariantForApi(1, ['sku' => 'NEW']);
|
||||
|
||||
$this->assertFalse($result);
|
||||
}
|
||||
|
||||
public function testUpdateVariantForApiReturnsFalseForNonexistent(): void
|
||||
{
|
||||
$mockDb = $this->createMock(\medoo::class);
|
||||
$mockDb->method('get')->willReturn(null);
|
||||
|
||||
$repository = new ProductRepository($mockDb);
|
||||
$result = $repository->updateVariantForApi(999, ['sku' => 'NEW']);
|
||||
|
||||
$this->assertFalse($result);
|
||||
}
|
||||
|
||||
public function testUpdateVariantForApiFiltersUnallowedFields(): void
|
||||
{
|
||||
$mockDb = $this->createMock(\medoo::class);
|
||||
|
||||
$mockDb->method('get')->willReturn([
|
||||
'id' => '101', 'parent_id' => '1',
|
||||
]);
|
||||
|
||||
$mockDb->expects($this->once())
|
||||
->method('update')
|
||||
->with(
|
||||
'pp_shop_products',
|
||||
$this->callback(function ($data) {
|
||||
return isset($data['sku'])
|
||||
&& !isset($data['parent_id'])
|
||||
&& !isset($data['permutation_hash']);
|
||||
}),
|
||||
$this->anything()
|
||||
);
|
||||
|
||||
$repository = new ProductRepository($mockDb);
|
||||
$repository->updateVariantForApi(101, [
|
||||
'sku' => 'NEW',
|
||||
'parent_id' => 999,
|
||||
'permutation_hash' => 'hacked',
|
||||
]);
|
||||
}
|
||||
|
||||
public function testUpdateVariantForApiCastsTypes(): void
|
||||
{
|
||||
$mockDb = $this->createMock(\medoo::class);
|
||||
|
||||
$mockDb->method('get')->willReturn([
|
||||
'id' => '101', 'parent_id' => '1',
|
||||
]);
|
||||
|
||||
$mockDb->expects($this->once())
|
||||
->method('update')
|
||||
->with(
|
||||
'pp_shop_products',
|
||||
$this->callback(function ($data) {
|
||||
return $data['sku'] === '123'
|
||||
&& $data['price_brutto'] === 99.99
|
||||
&& $data['quantity'] === 5
|
||||
&& $data['weight'] === null
|
||||
&& $data['status'] === 1;
|
||||
}),
|
||||
$this->anything()
|
||||
);
|
||||
|
||||
$repository = new ProductRepository($mockDb);
|
||||
$repository->updateVariantForApi(101, [
|
||||
'sku' => 123,
|
||||
'price_brutto' => '99.99',
|
||||
'quantity' => '5',
|
||||
'weight' => '',
|
||||
'status' => '1',
|
||||
]);
|
||||
}
|
||||
|
||||
// --- deleteVariantForApi ---
|
||||
|
||||
public function testDeleteVariantForApiSuccess(): void
|
||||
{
|
||||
$mockDb = $this->createMock(\medoo::class);
|
||||
|
||||
$mockDb->method('get')->willReturn([
|
||||
'id' => '101', 'parent_id' => '1',
|
||||
]);
|
||||
|
||||
$deleteCalls = [];
|
||||
$mockDb->expects($this->exactly(3))
|
||||
->method('delete')
|
||||
->willReturnCallback(function ($table, $where) use (&$deleteCalls) {
|
||||
$deleteCalls[] = ['table' => $table, 'where' => $where];
|
||||
return true;
|
||||
});
|
||||
|
||||
$repository = new ProductRepository($mockDb);
|
||||
$result = $repository->deleteVariantForApi(101);
|
||||
|
||||
$this->assertTrue($result);
|
||||
$this->assertSame('pp_shop_products_langs', $deleteCalls[0]['table']);
|
||||
$this->assertSame(['product_id' => 101], $deleteCalls[0]['where']);
|
||||
$this->assertSame('pp_shop_products_attributes', $deleteCalls[1]['table']);
|
||||
$this->assertSame('pp_shop_products', $deleteCalls[2]['table']);
|
||||
}
|
||||
|
||||
public function testDeleteVariantForApiReturnsFalseForNonVariant(): void
|
||||
{
|
||||
$mockDb = $this->createMock(\medoo::class);
|
||||
|
||||
$mockDb->method('get')->willReturn([
|
||||
'id' => '1', 'parent_id' => null,
|
||||
]);
|
||||
|
||||
$repository = new ProductRepository($mockDb);
|
||||
$result = $repository->deleteVariantForApi(1);
|
||||
|
||||
$this->assertFalse($result);
|
||||
}
|
||||
|
||||
public function testDeleteVariantForApiReturnsFalseForNonexistent(): void
|
||||
{
|
||||
$mockDb = $this->createMock(\medoo::class);
|
||||
$mockDb->method('get')->willReturn(null);
|
||||
|
||||
$repository = new ProductRepository($mockDb);
|
||||
$result = $repository->deleteVariantForApi(999);
|
||||
|
||||
$this->assertFalse($result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user