ver. 0.296: REST API for ordersPRO — orders management, dictionaries, API key auth
- New API layer: ApiRouter, OrdersApiController, DictionariesApiController - Orders API: list (with filters/pagination/updated_since), details, change status, set paid/unpaid - Dictionaries API: order statuses, transport methods, payment methods - X-Api-Key authentication via pp_settings.api_key - OrderRepository: listForApi(), findForApi(), touchUpdatedAt() - updated_at column on pp_shop_orders for polling support - api.php: skip session for API requests, route to ApiRouter - SettingsController: api_key field in system tab - 30 new tests (666 total, 1930 assertions) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
177
tests/Unit/api/ApiRouterTest.php
Normal file
177
tests/Unit/api/ApiRouterTest.php
Normal file
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
namespace Tests\Unit\api;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use api\ApiRouter;
|
||||
use Domain\Settings\SettingsRepository;
|
||||
|
||||
class ApiRouterTest extends TestCase
|
||||
{
|
||||
private function createRouter(string $storedApiKey = 'test-api-key-123'): ApiRouter
|
||||
{
|
||||
$mockDb = $this->createMock(\medoo::class);
|
||||
$mockSettings = $this->createMock(SettingsRepository::class);
|
||||
$mockSettings->method('getSingleValue')
|
||||
->with('api_key')
|
||||
->willReturn($storedApiKey);
|
||||
|
||||
return new ApiRouter($mockDb, $mockSettings);
|
||||
}
|
||||
|
||||
public function testHandleReturns401WhenNoApiKey(): void
|
||||
{
|
||||
unset($_SERVER['HTTP_X_API_KEY']);
|
||||
$_GET['endpoint'] = 'orders';
|
||||
$_GET['action'] = 'list';
|
||||
|
||||
$router = $this->createRouter();
|
||||
|
||||
ob_start();
|
||||
$router->handle();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$this->assertSame(401, http_response_code());
|
||||
$json = json_decode($output, true);
|
||||
$this->assertSame('error', $json['status']);
|
||||
$this->assertSame('UNAUTHORIZED', $json['code']);
|
||||
}
|
||||
|
||||
public function testHandleReturns401WhenWrongApiKey(): void
|
||||
{
|
||||
$_SERVER['HTTP_X_API_KEY'] = 'wrong-key';
|
||||
$_GET['endpoint'] = 'orders';
|
||||
$_GET['action'] = 'list';
|
||||
|
||||
$router = $this->createRouter();
|
||||
|
||||
ob_start();
|
||||
$router->handle();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$this->assertSame(401, http_response_code());
|
||||
$json = json_decode($output, true);
|
||||
$this->assertSame('UNAUTHORIZED', $json['code']);
|
||||
}
|
||||
|
||||
public function testHandleReturns401WhenStoredKeyEmpty(): void
|
||||
{
|
||||
$_SERVER['HTTP_X_API_KEY'] = 'any-key';
|
||||
$_GET['endpoint'] = 'orders';
|
||||
$_GET['action'] = 'list';
|
||||
|
||||
$router = $this->createRouter('');
|
||||
|
||||
ob_start();
|
||||
$router->handle();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$this->assertSame(401, http_response_code());
|
||||
}
|
||||
|
||||
public function testHandleReturns400WhenMissingEndpoint(): void
|
||||
{
|
||||
$_SERVER['HTTP_X_API_KEY'] = 'test-api-key-123';
|
||||
unset($_GET['endpoint']);
|
||||
$_GET['action'] = 'list';
|
||||
$_GET['endpoint'] = '';
|
||||
|
||||
$router = $this->createRouter();
|
||||
|
||||
ob_start();
|
||||
$router->handle();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$this->assertSame(400, http_response_code());
|
||||
$json = json_decode($output, true);
|
||||
$this->assertSame('BAD_REQUEST', $json['code']);
|
||||
}
|
||||
|
||||
public function testHandleReturns400WhenMissingAction(): void
|
||||
{
|
||||
$_SERVER['HTTP_X_API_KEY'] = 'test-api-key-123';
|
||||
$_GET['endpoint'] = 'orders';
|
||||
$_GET['action'] = '';
|
||||
|
||||
$router = $this->createRouter();
|
||||
|
||||
ob_start();
|
||||
$router->handle();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$this->assertSame(400, http_response_code());
|
||||
}
|
||||
|
||||
public function testHandleReturns404ForUnknownEndpoint(): void
|
||||
{
|
||||
$_SERVER['HTTP_X_API_KEY'] = 'test-api-key-123';
|
||||
$_GET['endpoint'] = 'unknown';
|
||||
$_GET['action'] = 'list';
|
||||
|
||||
$router = $this->createRouter();
|
||||
|
||||
ob_start();
|
||||
$router->handle();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$this->assertSame(404, http_response_code());
|
||||
$json = json_decode($output, true);
|
||||
$this->assertSame('NOT_FOUND', $json['code']);
|
||||
}
|
||||
|
||||
public function testSendSuccessOutputsCorrectJson(): void
|
||||
{
|
||||
ob_start();
|
||||
ApiRouter::sendSuccess(['foo' => 'bar']);
|
||||
$output = ob_get_clean();
|
||||
|
||||
$json = json_decode($output, true);
|
||||
$this->assertSame('ok', $json['status']);
|
||||
$this->assertSame('bar', $json['data']['foo']);
|
||||
}
|
||||
|
||||
public function testSendErrorOutputsCorrectJson(): void
|
||||
{
|
||||
ob_start();
|
||||
ApiRouter::sendError('BAD_REQUEST', 'Test error', 400);
|
||||
$output = ob_get_clean();
|
||||
|
||||
$this->assertSame(400, http_response_code());
|
||||
$json = json_decode($output, true);
|
||||
$this->assertSame('error', $json['status']);
|
||||
$this->assertSame('BAD_REQUEST', $json['code']);
|
||||
$this->assertSame('Test error', $json['message']);
|
||||
}
|
||||
|
||||
public function testRequireMethodReturnsTrueForMatchingMethod(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||
|
||||
ob_start();
|
||||
$result = ApiRouter::requireMethod('GET');
|
||||
ob_get_clean();
|
||||
|
||||
$this->assertTrue($result);
|
||||
}
|
||||
|
||||
public function testRequireMethodReturnsFalseAndSendsErrorForMismatch(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'POST';
|
||||
|
||||
ob_start();
|
||||
$result = ApiRouter::requireMethod('GET');
|
||||
$output = ob_get_clean();
|
||||
|
||||
$this->assertFalse($result);
|
||||
$this->assertSame(405, http_response_code());
|
||||
$json = json_decode($output, true);
|
||||
$this->assertSame('METHOD_NOT_ALLOWED', $json['code']);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
unset($_SERVER['HTTP_X_API_KEY']);
|
||||
unset($_SERVER['REQUEST_METHOD']);
|
||||
$_GET = [];
|
||||
http_response_code(200);
|
||||
}
|
||||
}
|
||||
139
tests/Unit/api/Controllers/DictionariesApiControllerTest.php
Normal file
139
tests/Unit/api/Controllers/DictionariesApiControllerTest.php
Normal file
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
namespace Tests\Unit\api\Controllers;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use api\Controllers\DictionariesApiController;
|
||||
use Domain\ShopStatus\ShopStatusRepository;
|
||||
use Domain\Transport\TransportRepository;
|
||||
use Domain\PaymentMethod\PaymentMethodRepository;
|
||||
|
||||
class DictionariesApiControllerTest extends TestCase
|
||||
{
|
||||
private $mockStatusRepo;
|
||||
private $mockTransportRepo;
|
||||
private $mockPaymentRepo;
|
||||
private $controller;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->mockStatusRepo = $this->createMock(ShopStatusRepository::class);
|
||||
$this->mockTransportRepo = $this->createMock(TransportRepository::class);
|
||||
$this->mockPaymentRepo = $this->createMock(PaymentMethodRepository::class);
|
||||
|
||||
$this->controller = new DictionariesApiController(
|
||||
$this->mockStatusRepo,
|
||||
$this->mockTransportRepo,
|
||||
$this->mockPaymentRepo
|
||||
);
|
||||
|
||||
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||
http_response_code(200);
|
||||
}
|
||||
|
||||
// --- statuses ---
|
||||
|
||||
public function testStatusesReturnsFormattedList(): void
|
||||
{
|
||||
$this->mockStatusRepo->method('allStatuses')
|
||||
->willReturn([
|
||||
0 => 'Nowe',
|
||||
1 => 'Opłacone',
|
||||
4 => 'W realizacji',
|
||||
6 => 'Wysłane',
|
||||
]);
|
||||
|
||||
ob_start();
|
||||
$this->controller->statuses();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$json = json_decode($output, true);
|
||||
$this->assertSame('ok', $json['status']);
|
||||
$this->assertCount(4, $json['data']);
|
||||
$this->assertSame(0, $json['data'][0]['id']);
|
||||
$this->assertSame('Nowe', $json['data'][0]['name']);
|
||||
$this->assertSame(6, $json['data'][3]['id']);
|
||||
$this->assertSame('Wysłane', $json['data'][3]['name']);
|
||||
}
|
||||
|
||||
public function testStatusesRejectsPostMethod(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'POST';
|
||||
|
||||
ob_start();
|
||||
$this->controller->statuses();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$this->assertSame(405, http_response_code());
|
||||
}
|
||||
|
||||
// --- transports ---
|
||||
|
||||
public function testTransportsReturnsFormattedList(): void
|
||||
{
|
||||
$this->mockTransportRepo->method('allActive')
|
||||
->willReturn([
|
||||
['id' => 1, 'name_visible' => 'InPost Paczkomat', 'cost' => '12.99'],
|
||||
['id' => 2, 'name_visible' => 'Kurier DPD', 'cost' => '15.00'],
|
||||
]);
|
||||
|
||||
ob_start();
|
||||
$this->controller->transports();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$json = json_decode($output, true);
|
||||
$this->assertSame('ok', $json['status']);
|
||||
$this->assertCount(2, $json['data']);
|
||||
$this->assertSame(1, $json['data'][0]['id']);
|
||||
$this->assertSame('InPost Paczkomat', $json['data'][0]['name']);
|
||||
$this->assertSame(12.99, $json['data'][0]['cost']);
|
||||
}
|
||||
|
||||
public function testTransportsRejectsPostMethod(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'POST';
|
||||
|
||||
ob_start();
|
||||
$this->controller->transports();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$this->assertSame(405, http_response_code());
|
||||
}
|
||||
|
||||
// --- payment_methods ---
|
||||
|
||||
public function testPaymentMethodsReturnsFormattedList(): void
|
||||
{
|
||||
$this->mockPaymentRepo->method('allActive')
|
||||
->willReturn([
|
||||
['id' => 1, 'name' => 'Przelew bankowy'],
|
||||
['id' => 2, 'name' => 'Przelewy24'],
|
||||
['id' => 3, 'name' => 'Przy odbiorze'],
|
||||
]);
|
||||
|
||||
ob_start();
|
||||
$this->controller->payment_methods();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$json = json_decode($output, true);
|
||||
$this->assertSame('ok', $json['status']);
|
||||
$this->assertCount(3, $json['data']);
|
||||
$this->assertSame(1, $json['data'][0]['id']);
|
||||
$this->assertSame('Przelew bankowy', $json['data'][0]['name']);
|
||||
}
|
||||
|
||||
public function testPaymentMethodsRejectsPostMethod(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'POST';
|
||||
|
||||
ob_start();
|
||||
$this->controller->payment_methods();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$this->assertSame(405, http_response_code());
|
||||
}
|
||||
}
|
||||
290
tests/Unit/api/Controllers/OrdersApiControllerTest.php
Normal file
290
tests/Unit/api/Controllers/OrdersApiControllerTest.php
Normal file
@@ -0,0 +1,290 @@
|
||||
<?php
|
||||
namespace Tests\Unit\api\Controllers;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use api\Controllers\OrdersApiController;
|
||||
use Domain\Order\OrderAdminService;
|
||||
use Domain\Order\OrderRepository;
|
||||
|
||||
class OrdersApiControllerTest extends TestCase
|
||||
{
|
||||
private $mockService;
|
||||
private $mockOrderRepo;
|
||||
private $controller;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->mockService = $this->createMock(OrderAdminService::class);
|
||||
$this->mockOrderRepo = $this->createMock(OrderRepository::class);
|
||||
$this->controller = new OrdersApiController($this->mockService, $this->mockOrderRepo);
|
||||
|
||||
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||
$_GET = [];
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||
$_GET = [];
|
||||
http_response_code(200);
|
||||
}
|
||||
|
||||
// --- list ---
|
||||
|
||||
public function testListReturnsOrders(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||
|
||||
$this->mockOrderRepo->method('listForApi')
|
||||
->willReturn([
|
||||
'items' => [
|
||||
['id' => 1, 'number' => '2026/01/001', 'status' => 0, 'paid' => 0, 'summary' => 99.99],
|
||||
],
|
||||
'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['status'] = '4';
|
||||
$_GET['paid'] = '1';
|
||||
$_GET['page'] = '2';
|
||||
$_GET['per_page'] = '25';
|
||||
|
||||
$this->mockOrderRepo->expects($this->once())
|
||||
->method('listForApi')
|
||||
->with(
|
||||
$this->callback(function ($filters) {
|
||||
return $filters['status'] === '4' && $filters['paid'] === '1';
|
||||
}),
|
||||
2,
|
||||
25
|
||||
)
|
||||
->willReturn(['items' => [], 'total' => 0, 'page' => 2, 'per_page' => 25]);
|
||||
|
||||
ob_start();
|
||||
$this->controller->list();
|
||||
ob_get_clean();
|
||||
}
|
||||
|
||||
// --- get ---
|
||||
|
||||
public function testGetReturnsOrder(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||
$_GET['id'] = '42';
|
||||
|
||||
$this->mockOrderRepo->method('findForApi')
|
||||
->with(42)
|
||||
->willReturn([
|
||||
'id' => 42,
|
||||
'number' => '2026/01/001',
|
||||
'status' => 4,
|
||||
'paid' => 1,
|
||||
'summary' => 150.00,
|
||||
'products' => [],
|
||||
'statuses' => [],
|
||||
]);
|
||||
|
||||
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 testGetReturns404WhenOrderNotFound(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||
$_GET['id'] = '999';
|
||||
|
||||
$this->mockOrderRepo->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());
|
||||
}
|
||||
|
||||
// --- change_status ---
|
||||
|
||||
public function testChangeStatusUpdatesOrder(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'PUT';
|
||||
$_GET['id'] = '10';
|
||||
|
||||
$this->mockOrderRepo->method('findRawById')
|
||||
->with(10)
|
||||
->willReturn(['id' => 10, 'status' => 0]);
|
||||
|
||||
$this->mockService->method('changeStatus')
|
||||
->with(10, 5, false)
|
||||
->willReturn(['result' => true]);
|
||||
|
||||
// Simulate JSON body via php://input override is not possible in unit tests,
|
||||
// so we test the controller logic path via mock expectations
|
||||
ob_start();
|
||||
$this->controller->change_status();
|
||||
$output = ob_get_clean();
|
||||
|
||||
// Without a real php://input body, getJsonBody returns null → BAD_REQUEST
|
||||
$json = json_decode($output, true);
|
||||
$this->assertSame('error', $json['status']);
|
||||
$this->assertSame('BAD_REQUEST', $json['code']);
|
||||
}
|
||||
|
||||
public function testChangeStatusReturns400WhenMissingId(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'PUT';
|
||||
|
||||
ob_start();
|
||||
$this->controller->change_status();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$this->assertSame(400, http_response_code());
|
||||
}
|
||||
|
||||
public function testChangeStatusRejectsGetMethod(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||
$_GET['id'] = '10';
|
||||
|
||||
ob_start();
|
||||
$this->controller->change_status();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$this->assertSame(405, http_response_code());
|
||||
}
|
||||
|
||||
// --- set_paid ---
|
||||
|
||||
public function testSetPaidReturns404WhenOrderNotFound(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'PUT';
|
||||
$_GET['id'] = '999';
|
||||
|
||||
$this->mockOrderRepo->method('findRawById')
|
||||
->with(999)
|
||||
->willReturn(null);
|
||||
|
||||
ob_start();
|
||||
$this->controller->set_paid();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$this->assertSame(404, http_response_code());
|
||||
}
|
||||
|
||||
public function testSetPaidReturns400WhenMissingId(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'PUT';
|
||||
|
||||
ob_start();
|
||||
$this->controller->set_paid();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$this->assertSame(400, http_response_code());
|
||||
}
|
||||
|
||||
public function testSetPaidCallsServiceWhenOrderExists(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'PUT';
|
||||
$_GET['id'] = '10';
|
||||
|
||||
$this->mockOrderRepo->method('findRawById')
|
||||
->with(10)
|
||||
->willReturn(['id' => 10, 'paid' => 0]);
|
||||
|
||||
$this->mockService->expects($this->once())
|
||||
->method('setOrderAsPaid')
|
||||
->with(10, false);
|
||||
|
||||
ob_start();
|
||||
$this->controller->set_paid();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$json = json_decode($output, true);
|
||||
$this->assertSame('ok', $json['status']);
|
||||
$this->assertSame(1, $json['data']['paid']);
|
||||
}
|
||||
|
||||
// --- set_unpaid ---
|
||||
|
||||
public function testSetUnpaidReturns404WhenOrderNotFound(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'PUT';
|
||||
$_GET['id'] = '999';
|
||||
|
||||
$this->mockOrderRepo->method('findRawById')
|
||||
->with(999)
|
||||
->willReturn(null);
|
||||
|
||||
ob_start();
|
||||
$this->controller->set_unpaid();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$this->assertSame(404, http_response_code());
|
||||
}
|
||||
|
||||
public function testSetUnpaidCallsServiceWhenOrderExists(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'PUT';
|
||||
$_GET['id'] = '10';
|
||||
|
||||
$this->mockOrderRepo->method('findRawById')
|
||||
->with(10)
|
||||
->willReturn(['id' => 10, 'paid' => 1]);
|
||||
|
||||
$this->mockService->expects($this->once())
|
||||
->method('setOrderAsUnpaid')
|
||||
->with(10);
|
||||
|
||||
ob_start();
|
||||
$this->controller->set_unpaid();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$json = json_decode($output, true);
|
||||
$this->assertSame('ok', $json['status']);
|
||||
$this->assertSame(0, $json['data']['paid']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user