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:
408
tests/Unit/api/Controllers/ProductsApiControllerTest.php
Normal file
408
tests/Unit/api/Controllers/ProductsApiControllerTest.php
Normal 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']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user