ver. 0.297: REST API products endpoint — list, get, create, update

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-19 22:39:48 +01:00
parent 9cac0d1eeb
commit ebab220f7e
13 changed files with 1115 additions and 10 deletions

View File

@@ -0,0 +1,408 @@
<?php
namespace Tests\Unit\api\Controllers;
use PHPUnit\Framework\TestCase;
use api\Controllers\ProductsApiController;
use Domain\Product\ProductRepository;
class ProductsApiControllerTest extends TestCase
{
private $mockRepo;
private $controller;
protected function setUp(): void
{
$this->mockRepo = $this->createMock(ProductRepository::class);
$this->controller = new ProductsApiController($this->mockRepo);
$_SERVER['REQUEST_METHOD'] = 'GET';
$_GET = [];
}
protected function tearDown(): void
{
$_SERVER['REQUEST_METHOD'] = 'GET';
$_GET = [];
http_response_code(200);
}
// --- list ---
public function testListReturnsProducts(): void
{
$_SERVER['REQUEST_METHOD'] = 'GET';
$this->mockRepo->method('listForApi')
->willReturn([
'items' => [
['id' => 1, 'name' => 'Product A', 'price_brutto' => 99.99, 'status' => 1],
],
'total' => 1,
'page' => 1,
'per_page' => 50,
]);
ob_start();
$this->controller->list();
$output = ob_get_clean();
$json = json_decode($output, true);
$this->assertSame('ok', $json['status']);
$this->assertCount(1, $json['data']['items']);
$this->assertSame(1, $json['data']['total']);
}
public function testListRejectsPostMethod(): void
{
$_SERVER['REQUEST_METHOD'] = 'POST';
ob_start();
$this->controller->list();
$output = ob_get_clean();
$this->assertSame(405, http_response_code());
}
public function testListPassesFiltersToRepository(): void
{
$_SERVER['REQUEST_METHOD'] = 'GET';
$_GET['search'] = 'test';
$_GET['status'] = '1';
$_GET['promoted'] = '0';
$_GET['sort'] = 'price_brutto';
$_GET['sort_dir'] = 'ASC';
$_GET['page'] = '2';
$_GET['per_page'] = '25';
$this->mockRepo->expects($this->once())
->method('listForApi')
->with(
$this->callback(function ($filters) {
return $filters['search'] === 'test'
&& $filters['status'] === '1'
&& $filters['promoted'] === '0';
}),
'price_brutto',
'ASC',
2,
25
)
->willReturn(['items' => [], 'total' => 0, 'page' => 2, 'per_page' => 25]);
ob_start();
$this->controller->list();
ob_get_clean();
}
public function testListDefaultPagination(): void
{
$_SERVER['REQUEST_METHOD'] = 'GET';
$this->mockRepo->expects($this->once())
->method('listForApi')
->with(
$this->anything(),
'id',
'DESC',
1,
50
)
->willReturn(['items' => [], 'total' => 0, 'page' => 1, 'per_page' => 50]);
ob_start();
$this->controller->list();
ob_get_clean();
}
public function testListClampsPerPageTo100(): void
{
$_SERVER['REQUEST_METHOD'] = 'GET';
$_GET['per_page'] = '200';
$this->mockRepo->expects($this->once())
->method('listForApi')
->with(
$this->anything(),
$this->anything(),
$this->anything(),
$this->anything(),
100
)
->willReturn(['items' => [], 'total' => 0, 'page' => 1, 'per_page' => 100]);
ob_start();
$this->controller->list();
ob_get_clean();
}
// --- get ---
public function testGetReturnsProduct(): void
{
$_SERVER['REQUEST_METHOD'] = 'GET';
$_GET['id'] = '42';
$this->mockRepo->method('findForApi')
->with(42)
->willReturn([
'id' => 42,
'name' => 'Test Product',
'price_brutto' => 199.99,
'status' => 1,
'languages' => [],
'images' => [],
'categories' => [],
'attributes' => [],
]);
ob_start();
$this->controller->get();
$output = ob_get_clean();
$json = json_decode($output, true);
$this->assertSame('ok', $json['status']);
$this->assertSame(42, $json['data']['id']);
}
public function testGetReturns404WhenProductNotFound(): void
{
$_SERVER['REQUEST_METHOD'] = 'GET';
$_GET['id'] = '999';
$this->mockRepo->method('findForApi')
->with(999)
->willReturn(null);
ob_start();
$this->controller->get();
$output = ob_get_clean();
$this->assertSame(404, http_response_code());
$json = json_decode($output, true);
$this->assertSame('NOT_FOUND', $json['code']);
}
public function testGetReturns400WhenMissingId(): void
{
$_SERVER['REQUEST_METHOD'] = 'GET';
ob_start();
$this->controller->get();
$output = ob_get_clean();
$this->assertSame(400, http_response_code());
}
public function testGetRejectsPostMethod(): void
{
$_SERVER['REQUEST_METHOD'] = 'POST';
$_GET['id'] = '1';
ob_start();
$this->controller->get();
$output = ob_get_clean();
$this->assertSame(405, http_response_code());
}
// --- create ---
public function testCreateRejectsGetMethod(): void
{
$_SERVER['REQUEST_METHOD'] = 'GET';
ob_start();
$this->controller->create();
$output = ob_get_clean();
$this->assertSame(405, http_response_code());
}
public function testCreateReturns400WhenNoBody(): void
{
$_SERVER['REQUEST_METHOD'] = 'POST';
// php://input returns empty in test environment → getJsonBody() returns null
ob_start();
$this->controller->create();
$output = ob_get_clean();
$this->assertSame(400, http_response_code());
$json = json_decode($output, true);
$this->assertSame('BAD_REQUEST', $json['code']);
}
// --- update ---
public function testUpdateRejectsGetMethod(): void
{
$_SERVER['REQUEST_METHOD'] = 'GET';
$_GET['id'] = '1';
ob_start();
$this->controller->update();
$output = ob_get_clean();
$this->assertSame(405, http_response_code());
}
public function testUpdateReturns400WhenMissingId(): void
{
$_SERVER['REQUEST_METHOD'] = 'PUT';
ob_start();
$this->controller->update();
$output = ob_get_clean();
$this->assertSame(400, http_response_code());
}
public function testUpdateReturns404WhenProductNotFound(): void
{
$_SERVER['REQUEST_METHOD'] = 'PUT';
$_GET['id'] = '999';
$this->mockRepo->method('find')
->with(999)
->willReturn(null);
ob_start();
$this->controller->update();
$output = ob_get_clean();
$this->assertSame(404, http_response_code());
$json = json_decode($output, true);
$this->assertSame('NOT_FOUND', $json['code']);
}
public function testUpdateReturns400WhenNoBody(): void
{
$_SERVER['REQUEST_METHOD'] = 'PUT';
$_GET['id'] = '1';
$this->mockRepo->method('find')
->with(1)
->willReturn(['id' => 1, 'status' => 1, 'promoted' => 0]);
// php://input returns empty in test environment → getJsonBody() returns null
ob_start();
$this->controller->update();
$output = ob_get_clean();
$this->assertSame(400, http_response_code());
$json = json_decode($output, true);
$this->assertSame('BAD_REQUEST', $json['code']);
}
// --- mapApiToFormData (tested indirectly via reflection) ---
public function testMapApiToFormDataConvertsStatusToCheckbox(): void
{
$method = new \ReflectionMethod(ProductsApiController::class, 'mapApiToFormData');
$method->setAccessible(true);
$result = $method->invoke($this->controller, ['status' => 1, 'promoted' => 0]);
$this->assertSame('on', $result['status']);
$this->assertSame('', $result['promoted']);
}
public function testMapApiToFormDataMapsLanguages(): void
{
$method = new \ReflectionMethod(ProductsApiController::class, 'mapApiToFormData');
$method->setAccessible(true);
$body = [
'languages' => [
'pl' => ['name' => 'Nazwa PL', 'description' => 'Opis PL'],
'en' => ['name' => 'Name EN'],
],
];
$result = $method->invoke($this->controller, $body);
$this->assertSame('Nazwa PL', $result['name']['pl']);
$this->assertSame('Opis PL', $result['description']['pl']);
$this->assertSame('Name EN', $result['name']['en']);
}
public function testMapApiToFormDataMapsNumericFields(): void
{
$method = new \ReflectionMethod(ProductsApiController::class, 'mapApiToFormData');
$method->setAccessible(true);
$body = [
'price_brutto' => 99.99,
'vat' => 23,
'quantity' => 10,
'sku' => 'PROD-001',
'ean' => '5901234123457',
];
$result = $method->invoke($this->controller, $body);
$this->assertSame(99.99, $result['price_brutto']);
$this->assertSame(23, $result['vat']);
$this->assertSame(10, $result['quantity']);
$this->assertSame('PROD-001', $result['sku']);
$this->assertSame('5901234123457', $result['ean']);
}
public function testMapApiToFormDataMapsCategories(): void
{
$method = new \ReflectionMethod(ProductsApiController::class, 'mapApiToFormData');
$method->setAccessible(true);
$body = [
'categories' => [1, 5, 12],
];
$result = $method->invoke($this->controller, $body);
$this->assertSame([1, 5, 12], $result['categories']);
}
public function testMapApiToFormDataPartialUpdatePreservesExisting(): void
{
$method = new \ReflectionMethod(ProductsApiController::class, 'mapApiToFormData');
$method->setAccessible(true);
$existing = [
'status' => 1,
'promoted' => 0,
'price_brutto' => 50.00,
'vat' => 23,
'quantity' => 5,
'sku' => 'OLD-SKU',
];
// Only update price
$body = ['price_brutto' => 75.00];
$result = $method->invoke($this->controller, $body, $existing);
$this->assertSame(75.00, $result['price_brutto']);
$this->assertSame('on', $result['status']); // preserved from existing
$this->assertSame('', $result['promoted']); // preserved from existing (0 → '')
$this->assertSame(23, $result['vat']); // preserved
$this->assertSame('OLD-SKU', $result['sku']); // preserved
}
public function testMapApiToFormDataMapsForeignKeys(): void
{
$method = new \ReflectionMethod(ProductsApiController::class, 'mapApiToFormData');
$method->setAccessible(true);
$body = [
'set_id' => 3,
'producer_id' => 7,
'product_unit_id' => 2,
];
$result = $method->invoke($this->controller, $body);
$this->assertSame(3, $result['set']);
$this->assertSame(7, $result['producer_id']);
$this->assertSame(2, $result['product_unit']);
}
}