Add functionality for converting exceptions to HTTP responses
[MAILPOET-2900]
This commit is contained in:
@ -4,6 +4,7 @@ namespace MailPoet\API\JSON;
|
|||||||
|
|
||||||
use MailPoet\API\JSON\Endpoint;
|
use MailPoet\API\JSON\Endpoint;
|
||||||
use MailPoet\Config\AccessControl;
|
use MailPoet\Config\AccessControl;
|
||||||
|
use MailPoet\Exception;
|
||||||
use MailPoet\Settings\SettingsController;
|
use MailPoet\Settings\SettingsController;
|
||||||
use MailPoet\Subscription\Captcha;
|
use MailPoet\Subscription\Captcha;
|
||||||
use MailPoet\Tracy\ApiPanel\ApiPanel;
|
use MailPoet\Tracy\ApiPanel\ApiPanel;
|
||||||
@ -12,6 +13,7 @@ use MailPoet\Util\Helpers;
|
|||||||
use MailPoet\Util\Security;
|
use MailPoet\Util\Security;
|
||||||
use MailPoet\WP\Functions as WPFunctions;
|
use MailPoet\WP\Functions as WPFunctions;
|
||||||
use MailPoetVendor\Psr\Container\ContainerInterface;
|
use MailPoetVendor\Psr\Container\ContainerInterface;
|
||||||
|
use Throwable;
|
||||||
use Tracy\Debugger;
|
use Tracy\Debugger;
|
||||||
use Tracy\ILogger;
|
use Tracy\ILogger;
|
||||||
|
|
||||||
@ -33,6 +35,9 @@ class API {
|
|||||||
/** @var AccessControl */
|
/** @var AccessControl */
|
||||||
private $accessControl;
|
private $accessControl;
|
||||||
|
|
||||||
|
/** @var ErrorHandler */
|
||||||
|
private $errorHandler;
|
||||||
|
|
||||||
/** @var WPFunctions */
|
/** @var WPFunctions */
|
||||||
private $wp;
|
private $wp;
|
||||||
|
|
||||||
@ -44,11 +49,13 @@ class API {
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
ContainerInterface $container,
|
ContainerInterface $container,
|
||||||
AccessControl $accessControl,
|
AccessControl $accessControl,
|
||||||
|
ErrorHandler $errorHandler,
|
||||||
SettingsController $settings,
|
SettingsController $settings,
|
||||||
WPFunctions $wp
|
WPFunctions $wp
|
||||||
) {
|
) {
|
||||||
$this->container = $container;
|
$this->container = $container;
|
||||||
$this->accessControl = $accessControl;
|
$this->accessControl = $accessControl;
|
||||||
|
$this->errorHandler = $errorHandler;
|
||||||
$this->settings = $settings;
|
$this->settings = $settings;
|
||||||
$this->wp = $wp;
|
$this->wp = $wp;
|
||||||
foreach ($this->availableApiVersions as $availableApiVersion) {
|
foreach ($this->availableApiVersions as $availableApiVersion) {
|
||||||
@ -192,7 +199,9 @@ class API {
|
|||||||
}
|
}
|
||||||
$response = $endpoint->{$this->requestMethod}($this->requestData);
|
$response = $endpoint->{$this->requestMethod}($this->requestData);
|
||||||
return $response;
|
return $response;
|
||||||
} catch (\Exception $e) {
|
} catch (Exception $e) {
|
||||||
|
return $this->errorHandler->convertToResponse($e);
|
||||||
|
} catch (Throwable $e) {
|
||||||
if (class_exists(Debugger::class) && Debugger::$logDirectory) {
|
if (class_exists(Debugger::class) && Debugger::$logDirectory) {
|
||||||
Debugger::log($e, ILogger::EXCEPTION);
|
Debugger::log($e, ILogger::EXCEPTION);
|
||||||
}
|
}
|
||||||
|
27
lib/API/JSON/ErrorHandler.php
Normal file
27
lib/API/JSON/ErrorHandler.php
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?php declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace MailPoet\API\JSON;
|
||||||
|
|
||||||
|
use MailPoet\Exception;
|
||||||
|
use MailPoet\HttpAwareException;
|
||||||
|
use MailPoet\WP\Functions as WPFunctions;
|
||||||
|
|
||||||
|
class ErrorHandler {
|
||||||
|
/** @var string[] */
|
||||||
|
private $defaultErrors;
|
||||||
|
|
||||||
|
public function __construct(WPFunctions $wp) {
|
||||||
|
$this->defaultErrors = [
|
||||||
|
Error::UNKNOWN => $wp->__('An unknown error occurred.', 'mailpoet'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function convertToResponse(\Throwable $e): ErrorResponse {
|
||||||
|
if ($e instanceof Exception) {
|
||||||
|
$errors = $e->getErrors() ?: $this->defaultErrors;
|
||||||
|
$statusCode = $e instanceof HttpAwareException ? $e->getHttpStatusCode() : Response::STATUS_UNKNOWN;
|
||||||
|
return new ErrorResponse($errors, [], $statusCode);
|
||||||
|
}
|
||||||
|
return new ErrorResponse($this->defaultErrors, [], Response::STATUS_UNKNOWN);
|
||||||
|
}
|
||||||
|
}
|
@ -59,6 +59,7 @@ class ContainerConfigurator implements IContainerConfigurator {
|
|||||||
->addArgument(new Reference(ContainerWrapper::class))
|
->addArgument(new Reference(ContainerWrapper::class))
|
||||||
->setAutowired(true)
|
->setAutowired(true)
|
||||||
->setPublic(true);
|
->setPublic(true);
|
||||||
|
$container->autowire(\MailPoet\API\JSON\ErrorHandler::class)->setPublic(true);
|
||||||
$container->autowire(\MailPoet\API\MP\v1\API::class)->setPublic(true);
|
$container->autowire(\MailPoet\API\MP\v1\API::class)->setPublic(true);
|
||||||
$container->autowire(\MailPoet\API\JSON\v1\Analytics::class)->setPublic(true);
|
$container->autowire(\MailPoet\API\JSON\v1\Analytics::class)->setPublic(true);
|
||||||
$container->autowire(\MailPoet\API\JSON\v1\AutomatedLatestContent::class)->setPublic(true);
|
$container->autowire(\MailPoet\API\JSON\v1\AutomatedLatestContent::class)->setPublic(true);
|
||||||
|
@ -3,6 +3,14 @@
|
|||||||
// phpcs:ignoreFile PSR1.Classes.ClassDeclaration
|
// phpcs:ignoreFile PSR1.Classes.ClassDeclaration
|
||||||
namespace MailPoet;
|
namespace MailPoet;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides information for converting exceptions to HTTP responses.
|
||||||
|
*/
|
||||||
|
interface HttpAwareException {
|
||||||
|
public function getHttpStatusCode(): int;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Frames all MailPoet exceptions ("$e instanceof MailPoet\Exception").
|
* Frames all MailPoet exceptions ("$e instanceof MailPoet\Exception").
|
||||||
*/
|
*/
|
||||||
@ -41,35 +49,57 @@ abstract class Exception extends \Exception {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* USE: Generic runtime error. When possible, use a more specific exception instead.
|
* USE: Generic runtime error. When possible, use a more specific exception instead.
|
||||||
|
* API: 500 Server Error (not HTTP-aware)
|
||||||
*/
|
*/
|
||||||
class RuntimeException extends Exception {}
|
class RuntimeException extends Exception {}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* USE: When wrong data VALUE is received.
|
* USE: When wrong data VALUE is received.
|
||||||
|
* API: 400 Bad Request
|
||||||
*/
|
*/
|
||||||
class UnexpectedValueException extends RuntimeException {}
|
class UnexpectedValueException extends RuntimeException implements HttpAwareException {
|
||||||
|
public function getHttpStatusCode(): int {
|
||||||
|
return 400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* USE: When an action is forbidden for given actor (although generally valid).
|
* USE: When an action is forbidden for given actor (although generally valid).
|
||||||
|
* API: 403 Forbidden
|
||||||
*/
|
*/
|
||||||
class AccessDeniedException extends UnexpectedValueException {}
|
class AccessDeniedException extends UnexpectedValueException implements HttpAwareException {
|
||||||
|
public function getHttpStatusCode(): int {
|
||||||
|
return 403;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* USE: When the main resource we're interested in doesn't exist.
|
* USE: When the main resource we're interested in doesn't exist.
|
||||||
|
* API: 404 Not Found
|
||||||
*/
|
*/
|
||||||
class NotFoundException extends UnexpectedValueException {}
|
class NotFoundException extends UnexpectedValueException implements HttpAwareException {
|
||||||
|
public function getHttpStatusCode(): int {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* USE: When the main action produces conflict (i.e. duplicate key).
|
* USE: When the main action produces conflict (i.e. duplicate key).
|
||||||
|
* API: 409 Conflict
|
||||||
*/
|
*/
|
||||||
class ConflictException extends UnexpectedValueException {}
|
class ConflictException extends UnexpectedValueException implements HttpAwareException {
|
||||||
|
public function getHttpStatusCode(): int {
|
||||||
|
return 409;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* USE: An application state that should not occur. Can be subclassed for feature-specific exceptions.
|
* USE: An application state that should not occur. Can be subclassed for feature-specific exceptions.
|
||||||
|
* API: 500 Server Error (not HTTP-aware)
|
||||||
*/
|
*/
|
||||||
class InvalidStateException extends RuntimeException {}
|
class InvalidStateException extends RuntimeException {}
|
||||||
|
@ -6,6 +6,7 @@ use Codeception\Stub;
|
|||||||
use Codeception\Stub\Expected;
|
use Codeception\Stub\Expected;
|
||||||
use MailPoet\API\JSON\API as JSONAPI;
|
use MailPoet\API\JSON\API as JSONAPI;
|
||||||
use MailPoet\API\JSON\Endpoint;
|
use MailPoet\API\JSON\Endpoint;
|
||||||
|
use MailPoet\API\JSON\ErrorHandler;
|
||||||
use MailPoet\API\JSON\Response;
|
use MailPoet\API\JSON\Response;
|
||||||
use MailPoet\API\JSON\Response as APIResponse;
|
use MailPoet\API\JSON\Response as APIResponse;
|
||||||
use MailPoet\API\JSON\SuccessResponse;
|
use MailPoet\API\JSON\SuccessResponse;
|
||||||
@ -29,6 +30,9 @@ class APITest extends \MailPoetTest {
|
|||||||
/** @var Container */
|
/** @var Container */
|
||||||
private $container;
|
private $container;
|
||||||
|
|
||||||
|
/** @var ErrorHandler */
|
||||||
|
private $errorHandler;
|
||||||
|
|
||||||
/** @var SettingsController */
|
/** @var SettingsController */
|
||||||
private $settings;
|
private $settings;
|
||||||
|
|
||||||
@ -48,10 +52,12 @@ class APITest extends \MailPoetTest {
|
|||||||
$this->container->autowire(APITestNamespacedEndpointStubV1::class)->setPublic(true);
|
$this->container->autowire(APITestNamespacedEndpointStubV1::class)->setPublic(true);
|
||||||
$this->container->autowire(APITestNamespacedEndpointStubV2::class)->setPublic(true);
|
$this->container->autowire(APITestNamespacedEndpointStubV2::class)->setPublic(true);
|
||||||
$this->container->compile();
|
$this->container->compile();
|
||||||
|
$this->errorHandler = $this->container->get(ErrorHandler::class);
|
||||||
$this->settings = $this->container->get(SettingsController::class);
|
$this->settings = $this->container->get(SettingsController::class);
|
||||||
$this->api = new \MailPoet\API\JSON\API(
|
$this->api = new \MailPoet\API\JSON\API(
|
||||||
$this->container,
|
$this->container,
|
||||||
$this->container->get(AccessControl::class),
|
$this->container->get(AccessControl::class),
|
||||||
|
$this->errorHandler,
|
||||||
$this->settings,
|
$this->settings,
|
||||||
new WPFunctions
|
new WPFunctions
|
||||||
);
|
);
|
||||||
@ -142,6 +148,25 @@ class APITest extends \MailPoetTest {
|
|||||||
expect($response->getData()['data'])->equals($data['data']);
|
expect($response->getData()['data'])->equals($data['data']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testItConvertsExceptionToErrorResponse() {
|
||||||
|
$namespace = [
|
||||||
|
'name' => 'MailPoet\API\JSON\v1',
|
||||||
|
'version' => 'v1',
|
||||||
|
];
|
||||||
|
$this->api->addEndpointNamespace($namespace['name'], $namespace['version']);
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'endpoint' => 'a_p_i_test_namespaced_endpoint_stub_v1',
|
||||||
|
'method' => 'testBadRequest',
|
||||||
|
'api_version' => 'v1',
|
||||||
|
'data' => ['test' => 'data'],
|
||||||
|
];
|
||||||
|
$this->api->setRequestData($data, Endpoint::TYPE_POST);
|
||||||
|
$response = $this->api->processRoute();
|
||||||
|
|
||||||
|
expect($response->errors)->equals([['error' => 'key', 'message' => 'value']]);
|
||||||
|
}
|
||||||
|
|
||||||
public function testItCallsAddedEndpointsForSpecificAPIVersion() {
|
public function testItCallsAddedEndpointsForSpecificAPIVersion() {
|
||||||
$namespace = [
|
$namespace = [
|
||||||
'name' => 'MailPoet\API\JSON\v2',
|
'name' => 'MailPoet\API\JSON\v2',
|
||||||
@ -211,7 +236,7 @@ class APITest extends \MailPoetTest {
|
|||||||
['validatePermission' => false]
|
['validatePermission' => false]
|
||||||
);
|
);
|
||||||
|
|
||||||
$api = new JSONAPI($this->container, $accessControl, $this->settings, new WPFunctions);
|
$api = new JSONAPI($this->container, $accessControl, $this->errorHandler, $this->settings, new WPFunctions);
|
||||||
$api->addEndpointNamespace($namespace['name'], $namespace['version']);
|
$api->addEndpointNamespace($namespace['name'], $namespace['version']);
|
||||||
$api->setRequestData($data, Endpoint::TYPE_POST);
|
$api->setRequestData($data, Endpoint::TYPE_POST);
|
||||||
$response = $api->processRoute();
|
$response = $api->processRoute();
|
||||||
@ -233,7 +258,7 @@ class APITest extends \MailPoetTest {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
$api = new JSONAPI($this->container, $accessControl, $this->settings, new WPFunctions);
|
$api = new JSONAPI($this->container, $accessControl, $this->errorHandler, $this->settings, new WPFunctions);
|
||||||
expect($api->validatePermissions(null, $permissions))->false();
|
expect($api->validatePermissions(null, $permissions))->false();
|
||||||
|
|
||||||
$accessControl = Stub::make(
|
$accessControl = Stub::make(
|
||||||
@ -245,7 +270,7 @@ class APITest extends \MailPoetTest {
|
|||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
$api = new JSONAPI($this->container, $accessControl, $this->settings, new WPFunctions);
|
$api = new JSONAPI($this->container, $accessControl, $this->errorHandler, $this->settings, new WPFunctions);
|
||||||
expect($api->validatePermissions(null, $permissions))->true();
|
expect($api->validatePermissions(null, $permissions))->true();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -267,7 +292,7 @@ class APITest extends \MailPoetTest {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
$api = new JSONAPI($this->container, $accessControl, $this->settings, new WPFunctions);
|
$api = new JSONAPI($this->container, $accessControl, $this->errorHandler, $this->settings, new WPFunctions);
|
||||||
expect($api->validatePermissions('test', $permissions))->false();
|
expect($api->validatePermissions('test', $permissions))->false();
|
||||||
|
|
||||||
$accessControl = Stub::make(
|
$accessControl = Stub::make(
|
||||||
@ -280,7 +305,7 @@ class APITest extends \MailPoetTest {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
$api = new JSONAPI($this->container, $accessControl, $this->settings, new WPFunctions);
|
$api = new JSONAPI($this->container, $accessControl, $this->errorHandler, $this->settings, new WPFunctions);
|
||||||
expect($api->validatePermissions('test', $permissions))->true();
|
expect($api->validatePermissions('test', $permissions))->true();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ namespace MailPoet\API\JSON\v1;
|
|||||||
|
|
||||||
use MailPoet\API\JSON\Endpoint as APIEndpoint;
|
use MailPoet\API\JSON\Endpoint as APIEndpoint;
|
||||||
use MailPoet\Config\AccessControl;
|
use MailPoet\Config\AccessControl;
|
||||||
|
use MailPoet\UnexpectedValueException;
|
||||||
|
|
||||||
class APITestNamespacedEndpointStubV1 extends APIEndpoint {
|
class APITestNamespacedEndpointStubV1 extends APIEndpoint {
|
||||||
public $permissions = [
|
public $permissions = [
|
||||||
@ -18,6 +19,10 @@ class APITestNamespacedEndpointStubV1 extends APIEndpoint {
|
|||||||
return $this->successResponse($data);
|
return $this->successResponse($data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testBadRequest($data) {
|
||||||
|
throw UnexpectedValueException::create()->withErrors(['key' => 'value']);
|
||||||
|
}
|
||||||
|
|
||||||
public function restricted($data) {
|
public function restricted($data) {
|
||||||
return $this->successResponse($data);
|
return $this->successResponse($data);
|
||||||
}
|
}
|
||||||
|
46
tests/unit/API/JSON/ErrorHandlerTest.php
Normal file
46
tests/unit/API/JSON/ErrorHandlerTest.php
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<?php declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace MailPoet\API\JSON;
|
||||||
|
|
||||||
|
use MailPoet\AccessDeniedException;
|
||||||
|
use MailPoet\ConflictException;
|
||||||
|
use MailPoet\Exception;
|
||||||
|
use MailPoet\InvalidStateException;
|
||||||
|
use MailPoet\NotFoundException;
|
||||||
|
use MailPoet\RuntimeException;
|
||||||
|
use MailPoet\UnexpectedValueException;
|
||||||
|
use MailPoet\WP\Functions as WPFunctions;
|
||||||
|
|
||||||
|
class ErrorHandlerTest extends \MailPoetUnitTest {
|
||||||
|
public function testItCovertsToBadRequest() {
|
||||||
|
$this->runErrorHandlerTest(new UnexpectedValueException(), 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testItCovertsToForbidden() {
|
||||||
|
$this->runErrorHandlerTest(new AccessDeniedException(), 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testItCovertsToNotFound() {
|
||||||
|
$this->runErrorHandlerTest(new NotFoundException(), 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testItCovertsToConflict() {
|
||||||
|
$this->runErrorHandlerTest(new ConflictException(), 409);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testItCovertsToServerError() {
|
||||||
|
$this->runErrorHandlerTest(new RuntimeException(), 500);
|
||||||
|
$this->runErrorHandlerTest(new InvalidStateException(), 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function runErrorHandlerTest(Exception $exception, int $expectedCode) {
|
||||||
|
$errorHandler = new ErrorHandler(new WPFunctions());
|
||||||
|
$response = $errorHandler->convertToResponse($exception->withErrors([
|
||||||
|
'key' => 'value',
|
||||||
|
]));
|
||||||
|
|
||||||
|
expect($response)->isInstanceOf(ErrorResponse::class);
|
||||||
|
expect($response->status)->equals($expectedCode);
|
||||||
|
expect($response->errors)->equals([['error' => 'key', 'message' => 'value']]);
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user