- 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>
178 lines
4.9 KiB
PHP
178 lines
4.9 KiB
PHP
<?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);
|
|
}
|
|
}
|