diff --git a/CHANGELOG.md b/CHANGELOG.md index b3c67c7..acede3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,25 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [Unreleased][unreleased] +## 3.0.0 + +### Changed +* Support for semantic versioning api. +* [BC] DefaultHandler response code 404 instead 400 +* [BC] Added Container to API Decider +* [BC] Output Configurator, Allows different methods for output configuration. Needs to be added to config services. +* [BC] Error handler, Allows for custom error handling of handle method. Needs to be added to config services. +* Query configurator rework + +### Added +* CorsPreflightHandlerInterface - resolve multiple service registered handler error +* Lazy API handlers + ## 2.12.0 ### Added * Button to copy `Body` content in api console +* Ability to disable schema validation and provide additional error info with get parameters. ### Changed * Handler tag wrapper has changed class from `btn` to `label` diff --git a/README.md b/README.md index 786f1bc..4ea800e 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,20 @@ application: Api: Tomaj\NetteApi\Presenters\*Presenter ``` +Register your preferred output configurator in *config.neon* services: + +```neon +services: + apiOutputConfigurator: Tomaj\NetteApi\Output\Configurator\DebuggerConfigurator +``` + +Register your preferred error handler in *config.neon* services: + +```neon +services: + apiErrorHandler: Tomaj\NetteApi\Error\DefaultErrorHandler +``` + And add route to you RouterFactory: ```php @@ -64,13 +78,26 @@ After that you need only register your API handlers to *apiDecider* [ApiDecider] ```neon services: - - Tomaj\NetteApi\Link\ApiLink - - Tomaj\NetteApi\Misc\IpDetector - apiDecider: + - Tomaj\NetteApi\Link\ApiLink + - Tomaj\NetteApi\Misc\IpDetector + apiDecider: + factory: Tomaj\NetteApi\ApiDecider + setup: + - addApi(\Tomaj\NetteApi\EndpointIdentifier('GET', 1, 'users'), \App\MyApi\v1\Handlers\UsersListingHandler(), \Tomaj\NetteApi\Authorization\NoAuthorization()) + - addApi(\Tomaj\NetteApi\EndpointIdentifier('POST', 1, 'users', 'send-email'), \App\MyApi\v1\Handlers\SendEmailHandler(), \Tomaj\NetteApi\Authorization\BearerTokenAuthorization()) + +``` + +or lazy (preferred because of performance) +```neon +services: + - App\MyApi\v1\Handlers\SendEmailLazyHandler() + sendEmailLazyNamed: App\MyApi\v1\Handlers\SendEmailLazyNamedHandler() + factory: Tomaj\NetteApi\ApiDecider setup: - - addApi(\Tomaj\NetteApi\EndpointIdentifier('GET', 1, 'users'), \App\MyApi\v1\Handlers\UsersListingHandler(), \Tomaj\NetteApi\Authorization\NoAuthorization()) - - addApi(\Tomaj\NetteApi\EndpointIdentifier('POST', 1, 'users', 'send-email'), \App\MyApi\v1\Handlers\SendEmailHandler(), \Tomaj\NetteApi\Authorization\BearerTokenAuthorization()) + - addApi(\Tomaj\NetteApi\EndpointIdentifier('POST', 1, 'users', 'send-email-lazy'), 'App\MyApi\v1\Handlers\SendEmailHandler', \Tomaj\NetteApi\Authorization\BearerTokenAuthorization()) + - addApi(\Tomaj\NetteApi\EndpointIdentifier('POST', 1, 'users', 'send-email-lazy-named'), '@sendEmailLazyNamed', \Tomaj\NetteApi\Authorization\BearerTokenAuthorization()) ``` As you can see in example, you can register as many endpoints as you want with different configurations. Nette-Api supports API versioning from the beginning. @@ -329,6 +356,7 @@ services: - addApi(\Tomaj\NetteApi\EndpointIdentifier('GET', 1, 'users'), \App\MyApi\v1\Handlers\UsersListingHandler(), \Tomaj\NetteApi\Authorization\BasicBasicAuthentication(['first-user': 'first-password', 'second-user': 'second-password'])) ``` + ### Bearer token authentication For simple use of Bearer token authorization with few tokens, you can use [StaticTokenRepository](src/Misc/StaticTokenRepository.php) (Tomaj\NetteApi\Misc\StaticTokenRepository). diff --git a/src/Api.php b/src/Api.php index 0e33705..5c53491 100644 --- a/src/Api.php +++ b/src/Api.php @@ -19,9 +19,15 @@ class Api private $rateLimit; + /** + * @param EndpointInterface $endpoint + * @param ApiHandlerInterface|string $handler + * @param ApiAuthorizationInterface $authorization + * @param RateLimitInterface|null $rateLimit + */ public function __construct( EndpointInterface $endpoint, - ApiHandlerInterface $handler, + $handler, ApiAuthorizationInterface $authorization, ?RateLimitInterface $rateLimit = null ) { @@ -36,7 +42,10 @@ public function getEndpoint(): EndpointInterface return $this->endpoint; } - public function getHandler(): ApiHandlerInterface + /** + * @return ApiHandlerInterface|string + */ + public function getHandler() { return $this->handler; } diff --git a/src/ApiDecider.php b/src/ApiDecider.php index 533c62f..541bd2e 100644 --- a/src/ApiDecider.php +++ b/src/ApiDecider.php @@ -4,6 +4,7 @@ namespace Tomaj\NetteApi; +use Nette\DI\Container; use Nette\Http\Response; use Tomaj\NetteApi\Authorization\ApiAuthorizationInterface; use Tomaj\NetteApi\Authorization\NoAuthorization; @@ -11,27 +12,36 @@ use Tomaj\NetteApi\Handlers\CorsPreflightHandler; use Tomaj\NetteApi\Handlers\DefaultHandler; use Tomaj\NetteApi\RateLimit\RateLimitInterface; +use Tomaj\NetteApi\Handlers\CorsPreflightHandlerInterface; class ApiDecider { + /** @var Container */ + private $container; + /** @var Api[] */ private $apis = []; /** @var ApiHandlerInterface|null */ private $globalPreflightHandler = null; + public function __construct(Container $container) + { + $this->container = $container; + } + /** * Get api handler that match input method, version, package and apiAction. * If decider cannot find handler for given handler, returns defaults. * * @param string $method - * @param integer $version + * @param string $version * @param string $package * @param string $apiAction * * @return Api */ - public function getApi(string $method, int $version, string $package, ?string $apiAction = null) + public function getApi(string $method, string $version, string $package, ?string $apiAction = null) { $method = strtoupper($method); $apiAction = $apiAction === '' ? null : $apiAction; @@ -40,8 +50,9 @@ public function getApi(string $method, int $version, string $package, ?string $a $identifier = $api->getEndpoint(); if ($method === $identifier->getMethod() && $identifier->getVersion() === $version && $identifier->getPackage() === $package && $identifier->getApiAction() === $apiAction) { $endpointIdentifier = new EndpointIdentifier($method, $version, $package, $apiAction); - $api->getHandler()->setEndpointIdentifier($endpointIdentifier); - return $api; + $handler = $this->getHandler($api); + $handler->setEndpointIdentifier($endpointIdentifier); + return new Api($api->getEndpoint(), $handler, $api->getAuthorization(), $api->getRateLimit()); } if ($method === 'OPTIONS' && $this->globalPreflightHandler && $identifier->getVersion() === $version && $identifier->getPackage() === $package && $identifier->getApiAction() === $apiAction) { return new Api(new EndpointIdentifier('OPTIONS', $version, $package, $apiAction), $this->globalPreflightHandler, new NoAuthorization()); @@ -50,7 +61,7 @@ public function getApi(string $method, int $version, string $package, ?string $a return new Api(new EndpointIdentifier($method, $version, $package, $apiAction), new DefaultHandler(), new NoAuthorization()); } - public function enableGlobalPreflight(ApiHandlerInterface $corsHandler = null) + public function enableGlobalPreflight(CorsPreflightHandlerInterface $corsHandler = null) { if (!$corsHandler) { $corsHandler = new CorsPreflightHandler(new Response()); @@ -62,12 +73,12 @@ public function enableGlobalPreflight(ApiHandlerInterface $corsHandler = null) * Register new api handler * * @param EndpointInterface $endpointIdentifier - * @param ApiHandlerInterface $handler + * @param ApiHandlerInterface|string $handler * @param ApiAuthorizationInterface $apiAuthorization * @param RateLimitInterface|null $rateLimit * @return self */ - public function addApi(EndpointInterface $endpointIdentifier, ApiHandlerInterface $handler, ApiAuthorizationInterface $apiAuthorization, RateLimitInterface $rateLimit = null): self + public function addApi(EndpointInterface $endpointIdentifier, $handler, ApiAuthorizationInterface $apiAuthorization, RateLimitInterface $rateLimit = null): self { $this->apis[] = new Api($endpointIdentifier, $handler, $apiAuthorization, $rateLimit); return $this; @@ -80,6 +91,25 @@ public function addApi(EndpointInterface $endpointIdentifier, ApiHandlerInterfac */ public function getApis(): array { - return $this->apis; + $apis = []; + foreach ($this->apis as $api) { + $handler = $this->getHandler($api); + $apis[] = new Api($api->getEndpoint(), $handler, $api->getAuthorization(), $api->getRateLimit()); + } + return $apis; + } + + private function getHandler(Api $api): ApiHandlerInterface + { + $handler = $api->getHandler(); + if (!is_string($handler)) { + return $handler; + } + + if (str_starts_with($handler, '@')) { + return $this->container->getByName(substr($handler, 1)); + } + + return $this->container->getByType($handler); } } diff --git a/src/Component/ApiListingControl.php b/src/Component/ApiListingControl.php index 6628f95..4965a98 100644 --- a/src/Component/ApiListingControl.php +++ b/src/Component/ApiListingControl.php @@ -37,7 +37,7 @@ public function render(): void $template->render(); } - public function handleSelect(string $method, int $version, string $package, ?string $apiAction = null): void + public function handleSelect(string $method, $version, string $package, ?string $apiAction = null): void { $this->onClick($method, $version, $package, $apiAction); } diff --git a/src/EndpointIdentifier.php b/src/EndpointIdentifier.php index d806524..43fae40 100644 --- a/src/EndpointIdentifier.php +++ b/src/EndpointIdentifier.php @@ -4,6 +4,8 @@ namespace Tomaj\NetteApi; +use InvalidArgumentException; + class EndpointIdentifier implements EndpointInterface { private $method; @@ -14,9 +16,19 @@ class EndpointIdentifier implements EndpointInterface private $apiAction; - public function __construct(string $method, int $version, string $package, ?string $apiAction = null) + /** + * @param string $method example: "GET", "POST", "PUT", "DELETE" + * @param string|int $version Version must have semantic numbering. For example "1", "1.1", "0.13.2" etc. + * @param string $package example: "users" + * @param string|null $apiAction example: "query" + */ + public function __construct(string $method, $version, string $package, ?string $apiAction = null) { + $version = (string) $version; $this->method = strtoupper($method); + if (strpos($version, '/') !== false) { + throw new InvalidArgumentException('Version must have semantic numbering. For example "1", "1.1", "0.13.2" etc.'); + } $this->version = $version; $this->package = $package; $this->apiAction = $apiAction; @@ -27,7 +39,7 @@ public function getMethod(): string return $this->method; } - public function getVersion(): int + public function getVersion(): string { return $this->version; } diff --git a/src/EndpointInterface.php b/src/EndpointInterface.php index bcf0e60..fe4b60b 100644 --- a/src/EndpointInterface.php +++ b/src/EndpointInterface.php @@ -8,7 +8,7 @@ interface EndpointInterface { public function getMethod(): string; - public function getVersion(): int; + public function getVersion(): string; public function getPackage(): string; diff --git a/src/Error/DefaultErrorHandler.php b/src/Error/DefaultErrorHandler.php new file mode 100644 index 0000000..9dcb512 --- /dev/null +++ b/src/Error/DefaultErrorHandler.php @@ -0,0 +1,66 @@ +outputConfigurator = $outputConfigurator; + } + + public function handle(Throwable $exception, array $params): JsonApiResponse + { + Debugger::log($exception, Debugger::EXCEPTION); + if ($this->outputConfigurator->showErrorDetail()) { + $response = new JsonApiResponse(Response::S500_INTERNAL_SERVER_ERROR, ['status' => 'error', 'message' => 'Internal server error', 'detail' => $exception->getMessage()]); + } else { + $response = new JsonApiResponse(Response::S500_INTERNAL_SERVER_ERROR, ['status' => 'error', 'message' => 'Internal server error']); + } + return $response; + } + + public function handleInputParams(array $errors): JsonApiResponse + { + if ($this->outputConfigurator->showErrorDetail()) { + $response = new JsonApiResponse(Response::S400_BAD_REQUEST, ['status' => 'error', 'message' => 'wrong input', 'detail' => $errors]); + } else { + $response = new JsonApiResponse(Response::S400_BAD_REQUEST, ['status' => 'error', 'message' => 'wrong input']); + } + return $response; + } + + public function handleSchema(array $errors, array $params): JsonApiResponse + { + Debugger::log($errors, Debugger::ERROR); + + if ($this->outputConfigurator->showErrorDetail()) { + $response = new JsonApiResponse(Response::S500_INTERNAL_SERVER_ERROR, ['status' => 'error', 'message' => 'Internal server error', 'detail' => $errors]); + } else { + $response = new JsonApiResponse(Response::S500_INTERNAL_SERVER_ERROR, ['status' => 'error', 'message' => 'Internal server error']); + } + return $response; + } + + public function handleAuthorization(ApiAuthorizationInterface $auth, array $params): JsonApiResponse + { + return new JsonApiResponse(Response::S401_UNAUTHORIZED, ['status' => 'error', 'message' => $auth->getErrorMessage()]); + } + + public function handleAuthorizationException(Throwable $exception, array $params): JsonApiResponse + { + return new JsonApiResponse(Response::S500_INTERNAL_SERVER_ERROR, ['status' => 'error', 'message' => $exception->getMessage()]); + } +} diff --git a/src/Error/ErrorHandlerInterface.php b/src/Error/ErrorHandlerInterface.php new file mode 100644 index 0000000..91974e4 --- /dev/null +++ b/src/Error/ErrorHandlerInterface.php @@ -0,0 +1,39 @@ + $params + */ + public function handle(Throwable $exception, array $params): JsonApiResponse; + + /** + * @param array $errors + * @param array $params + */ + public function handleInputParams(array $errors): JsonApiResponse; + + /** + * @param array $errors + * @param array $params + */ + public function handleSchema(array $errors, array $params): JsonApiResponse; + + /** + * @param array $params + */ + public function handleAuthorization(ApiAuthorizationInterface $auth, array $params): JsonApiResponse; + + /** + * @param array $params + */ + public function handleAuthorizationException(Throwable $exception, array $params): JsonApiResponse; +} diff --git a/src/Handlers/ApiListingHandler.php b/src/Handlers/ApiListingHandler.php index fefc08e..0d0be0e 100644 --- a/src/Handlers/ApiListingHandler.php +++ b/src/Handlers/ApiListingHandler.php @@ -53,7 +53,7 @@ public function handle(array $params): ResponseInterface * * @return array */ - private function getApiList(int $version): array + private function getApiList(string $version): array { $versionApis = array_filter($this->apiDecider->getApis(), function (Api $api) use ($version) { return $version === $api->getEndpoint()->getVersion(); diff --git a/src/Handlers/CorsPreflightHandler.php b/src/Handlers/CorsPreflightHandler.php index 571d9c2..1213798 100644 --- a/src/Handlers/CorsPreflightHandler.php +++ b/src/Handlers/CorsPreflightHandler.php @@ -8,7 +8,7 @@ use Tomaj\NetteApi\Response\JsonApiResponse; use Tomaj\NetteApi\Response\ResponseInterface; -class CorsPreflightHandler extends BaseHandler +class CorsPreflightHandler extends BaseHandler implements CorsPreflightHandlerInterface { private $response; diff --git a/src/Handlers/CorsPreflightHandlerInterface.php b/src/Handlers/CorsPreflightHandlerInterface.php new file mode 100644 index 0000000..e4e87e6 --- /dev/null +++ b/src/Handlers/CorsPreflightHandlerInterface.php @@ -0,0 +1,15 @@ + 'error', 'message' => 'Unknown api endpoint']); + return new JsonApiResponse(IResponse::S404_NOT_FOUND, ['status' => 'error', 'message' => 'Unknown api endpoint']); } } diff --git a/src/Handlers/OpenApiHandler.php b/src/Handlers/OpenApiHandler.php index 60b5a6d..7f372c4 100644 --- a/src/Handlers/OpenApiHandler.php +++ b/src/Handlers/OpenApiHandler.php @@ -227,7 +227,7 @@ public function handle(array $params): ResponseInterface return new JsonApiResponse(IResponse::S200_OK, $data); } - private function getApis(int $version): array + private function getApis(string $version): array { return array_filter($this->apiDecider->getApis(), function (Api $api) use ($version) { return $version === $api->getEndpoint()->getVersion(); @@ -313,6 +313,17 @@ private function getPaths(array $versionApis, string $baseUrl, string $basePath) ], ] ]; + if (!empty($examples = $output->getExamples())) { + if (count($examples) === 1) { + $example = is_array($output->getExample())? $output->getExample() : json_decode($output->getExample(), true); + $responses[$output->getCode()]['content']['application/json; charset=utf-8']['example'] = $example; + } else { + foreach ($examples as $exampleKey => $example) { + $example = is_array($example)? $example : json_decode($example, true); + $responses[$output->getCode()]['content']['application/json; charset=utf-8']['examples'][$exampleKey] = $example; + } + } + } } else { if (!isset($responses[$output->getCode()]['content']['application/json; charset=utf-8']['schema']['oneOf'])) { $tmp = $responses[$output->getCode()]['content']['application/json; charset=utf-8']['schema']; @@ -476,8 +487,14 @@ private function createRequestBody(ApiHandlerInterface $handler) foreach ($handler->params() as $param) { if ($param instanceof JsonInputParam) { $schema = json_decode($param->getSchema(), true); - if ($param->getExample()) { - $schema['example'] = $param->getExample(); + if (!empty($examples = $param->getExamples())) { + if (count($examples) === 1) { + $schema['example'] = is_array($param->getExample())? $param->getExample() : json_decode($param->getExample(), true); + } else { + foreach ($examples as $exampleKey => $example) { + $schema['examples'][$exampleKey] = is_array($example)? $example : json_decode($example, true); + } + } } return [ 'description' => $param->getDescription(), @@ -493,8 +510,12 @@ private function createRequestBody(ApiHandlerInterface $handler) $schema = [ 'type' => 'string', ]; - if ($param->getExample()) { - $schema['example'] = $param->getExample(); + if (!empty($examples = $param->getExamples())) { + if (count($examples) === 1) { + $schema['example'] = $param->getExample(); + } else { + $schema['examples'] = $examples; + } } return [ 'description' => $param->getDescription(), diff --git a/src/Output/AbstractOutput.php b/src/Output/AbstractOutput.php index c36f779..ed2a0f2 100644 --- a/src/Output/AbstractOutput.php +++ b/src/Output/AbstractOutput.php @@ -10,6 +10,9 @@ abstract class AbstractOutput implements OutputInterface protected $description; + /** @var array */ + protected $examples = []; + public function __construct(int $code, string $description = '') { $this->code = $code; @@ -25,4 +28,48 @@ public function getDescription(): string { return $this->description; } + + /** + * @param string $name Example name + * @param mixed $example Example + * @return Self + */ + public function addExample(string $name, $example): self + { + $this->examples[$name] = $example; + return $this; + } + + /** + * Set default example + * @param mixed $example + * @return self + * @deprecated Use addExample instead + */ + public function setExample($example): self + { + $this->examples["default"] = $example; + return $this; + } + + /** + * Returns first example + * @return mixed + */ + public function getExample() + { + if (empty($this->examples)) { + return null; + } + return reset($this->examples); + } + + /** + * Returns all examples + * @return array + */ + public function getExamples(): array + { + return $this->examples; + } } diff --git a/src/Output/Configurator/ConfiguratorInterface.php b/src/Output/Configurator/ConfiguratorInterface.php new file mode 100644 index 0000000..64e4e89 --- /dev/null +++ b/src/Output/Configurator/ConfiguratorInterface.php @@ -0,0 +1,12 @@ +envVariable = $envVariable; + $this->productionValue = $productionValue; + } + + public function validateSchema(): bool + { + $appEnv = getenv($this->envVariable); + if ($appEnv === $this->productionValue) { + return false; + } + return true; + } + + public function showErrorDetail(): bool + { + $appEnv = getenv($this->envVariable); + if ($appEnv === $this->productionValue) { + return false; + } + return true; + } +} diff --git a/src/Output/Configurator/QueryConfigurator.php b/src/Output/Configurator/QueryConfigurator.php new file mode 100644 index 0000000..0a4fba1 --- /dev/null +++ b/src/Output/Configurator/QueryConfigurator.php @@ -0,0 +1,40 @@ +request = $request; + $this->schemaValidateParam = $schemaValidateParam; + $this->errorDetailParam = $errorDetailParam; + } + + public function validateSchema(): bool + { + $getParam = $this->request->getQuery($this->schemaValidateParam); + return $getParam !== null && $getParam !== '0' && $getParam !== 'false'; + } + + public function showErrorDetail(): bool + { + $getParam = $this->request->getQuery($this->errorDetailParam); + return $getParam !== null && $getParam !== '0' && $getParam !== 'false'; + } +} diff --git a/src/Params/InputParam.php b/src/Params/InputParam.php index 6cd8f02..468520f 100644 --- a/src/Params/InputParam.php +++ b/src/Params/InputParam.php @@ -44,8 +44,8 @@ abstract class InputParam implements ParamInterface /** @var mixed */ protected $default; - /** @var mixed */ - protected $example; + /** @var array */ + protected $examples = []; public function __construct(string $key) { @@ -128,21 +128,48 @@ public function getDefault() } /** + * Add example, can be used multiple times to add many examples + * @param string $name Example name + * @param mixed $example Example + * @return Self + */ + public function addExample(string $name, $example): self + { + $this->examples[$name] = $example; + return $this; + } + + /** + * Set default example * @param mixed $example * @return self + * @deprecated Use addExample instead */ public function setExample($example): self { - $this->example = $example; + $this->examples["default"] = $example; return $this; } /** + * Returns first example * @return mixed */ public function getExample() { - return $this->example; + if (empty($this->examples)) { + return null; + } + return reset($this->examples); + } + + /** + * Returns all examples + * @return array + */ + public function getExamples(): array + { + return $this->examples; } public function updateConsoleForm(Form $form): void diff --git a/src/Params/JsonInputParam.php b/src/Params/JsonInputParam.php index 6b1e3f1..f7ab440 100644 --- a/src/Params/JsonInputParam.php +++ b/src/Params/JsonInputParam.php @@ -66,10 +66,48 @@ public function getSchema(): string protected function addFormInput(Form $form, string $key): BaseControl { + $fullSchema = json_decode($this->schema, true); + + if (!empty($examples = $this->getExamples())) { + if (count($examples) === 1) { + $fullSchema['example'] = is_array($this->getExample())? $this->getExample() : json_decode($this->getExample(), true); + } else { + foreach ($examples as $exampleKey => $example) { + $fullSchema['examples'][$exampleKey] = is_array($example)? $example : json_decode($example, true); + // pretty formatting of json example if decoded + } + } + } + + + if (!empty($fullSchema['examples'])) { + $this->description .= <<< HTML +
+ Select Example:  +HTML; + foreach ($fullSchema['examples'] as $exampleKey => $exampleValue) { + $example = htmlentities(json_encode($exampleValue, JSON_PRETTY_PRINT)); + $this->description .= <<< HTML +
+ {$exampleKey} +
+HTML; + } + $this->description .= <<< HTML + +
+ +HTML; + } $this->description .= ' '; + . nl2br(str_replace(' ', ' ', json_encode($fullSchema, JSON_PRETTY_PRINT))) . ''; return $form->addTextArea('post_raw', $this->getParamLabel()) ->setHtmlAttribute('rows', 10); diff --git a/src/Presenters/ApiPresenter.php b/src/Presenters/ApiPresenter.php index cd06807..ea32b33 100644 --- a/src/Presenters/ApiPresenter.php +++ b/src/Presenters/ApiPresenter.php @@ -14,13 +14,13 @@ use Tomaj\NetteApi\Api; use Tomaj\NetteApi\ApiDecider; use Tomaj\NetteApi\Authorization\ApiAuthorizationInterface; +use Tomaj\NetteApi\Error\ErrorHandlerInterface; use Tomaj\NetteApi\Logger\ApiLoggerInterface; use Tomaj\NetteApi\Misc\IpDetectorInterface; +use Tomaj\NetteApi\Output\Configurator\ConfiguratorInterface; use Tomaj\NetteApi\Output\OutputInterface; use Tomaj\NetteApi\Params\ParamsProcessor; use Tomaj\NetteApi\RateLimit\RateLimitInterface; -use Tomaj\NetteApi\Response\JsonApiResponse; -use Tracy\Debugger; final class ApiPresenter implements IPresenter { @@ -33,6 +33,12 @@ final class ApiPresenter implements IPresenter /** @var Container @inject */ public $context; + /** @var ConfiguratorInterface @inject */ + public $outputConfigurator; + + /** @var ErrorHandlerInterface @inject */ + public $errorHandler; + /** * CORS header settings * @@ -67,14 +73,10 @@ public function run(Request $request): IResponse $api = $this->getApi($request); $handler = $api->getHandler(); + $authorization = $api->getAuthorization(); $rateLimit = $api->getRateLimit(); - $authResponse = $this->checkAuth($authorization); - if ($authResponse !== null) { - return $authResponse; - } - $rateLimitResponse = $this->checkRateLimit($rateLimit); if ($rateLimitResponse !== null) { return $rateLimitResponse; @@ -82,47 +84,45 @@ public function run(Request $request): IResponse $paramsProcessor = new ParamsProcessor($handler->params()); if ($paramsProcessor->isError()) { - $this->response->setCode(Response::S400_BAD_REQUEST); - if (!Debugger::$productionMode) { - $response = new JsonResponse(['status' => 'error', 'message' => 'wrong input', 'detail' => $paramsProcessor->getErrors()]); - } else { - $response = new JsonResponse(['status' => 'error', 'message' => 'wrong input']); - } + $response = $this->errorHandler->handleInputParams($paramsProcessor->getErrors()); + $this->response->setCode($response->getCode()); return $response; } $params = $paramsProcessor->getValues(); + $authResponse = $this->checkAuth($authorization, $params); + if ($authResponse !== null) { + return $authResponse; + } + try { $response = $handler->handle($params); - $outputValid = count($handler->outputs()) === 0; // back compatibility for handlers with no outputs defined - $outputValidatorErrors = []; - foreach ($handler->outputs() as $output) { - if (!$output instanceof OutputInterface) { - $outputValidatorErrors[] = ["Output does not implement OutputInterface"]; - continue; - } - $validationResult = $output->validate($response); - if ($validationResult->isOk()) { - $outputValid = true; - break; + $code = $response->getCode(); + + if ($this->outputConfigurator->validateSchema()) { + $outputs = $handler->outputs(); + $outputValid = count($outputs) === 0; // back compatibility for handlers with no outputs defined + $outputValidatorErrors = []; + foreach ($outputs as $output) { + if (!$output instanceof OutputInterface) { + $outputValidatorErrors[] = ["Output does not implement OutputInterface"]; + continue; + } + $validationResult = $output->validate($response); + if ($validationResult->isOk()) { + $outputValid = true; + break; + } + $outputValidatorErrors[] = $validationResult->getErrors(); } - $outputValidatorErrors[] = $validationResult->getErrors(); - } - if (!$outputValid) { - Debugger::log($outputValidatorErrors, Debugger::ERROR); - if (!Debugger::$productionMode) { - $response = new JsonApiResponse(Response::S500_INTERNAL_SERVER_ERROR, ['status' => 'error', 'message' => 'Internal server error', 'details' => $outputValidatorErrors]); + if (!$outputValid) { + $response = $this->errorHandler->handleSchema($outputValidatorErrors, $params); + $code = $response->getCode(); } } - $code = $response->getCode(); } catch (Throwable $exception) { - if (!Debugger::$productionMode) { - $response = new JsonApiResponse(Response::S500_INTERNAL_SERVER_ERROR, ['status' => 'error', 'message' => 'Internal server error', 'detail' => $exception->getMessage()]); - } else { - $response = new JsonApiResponse(Response::S500_INTERNAL_SERVER_ERROR, ['status' => 'error', 'message' => 'Internal server error']); - } + $response = $this->errorHandler->handle($exception, $params); $code = $response->getCode(); - Debugger::log($exception, Debugger::EXCEPTION); } $end = microtime(true); @@ -142,17 +142,24 @@ private function getApi(Request $request): Api { return $this->apiDecider->getApi( $request->getMethod(), - (int) $request->getParameter('version'), + $request->getParameter('version'), $request->getParameter('package'), $request->getParameter('apiAction') ); } - private function checkAuth(ApiAuthorizationInterface $authorization): ?IResponse + private function checkAuth(ApiAuthorizationInterface $authorization, array $params): ?IResponse { - if (!$authorization->authorized()) { - $this->response->setCode(Response::S403_FORBIDDEN); - return new JsonResponse(['status' => 'error', 'message' => $authorization->getErrorMessage()]); + try { + if (!$authorization->authorized()) { + $response = $this->errorHandler->handleAuthorization($authorization, $params); + $this->response->setCode($response->getCode()); + return $response; + } + } catch (Throwable $exception) { + $response = $this->errorHandler->handleAuthorizationException($exception, $params); + $this->response->setCode($response->getCode()); + return $response; } return null; } diff --git a/tests/ApiDeciderTest.php b/tests/ApiDeciderTest.php index 7de42d3..ab87135 100644 --- a/tests/ApiDeciderTest.php +++ b/tests/ApiDeciderTest.php @@ -4,6 +4,7 @@ namespace Tomaj\NetteApi\Test\Params; +use Nette\DI\Container; use PHPUnit\Framework\TestCase; use Tomaj\NetteApi\ApiDecider; use Tomaj\NetteApi\Authorization\NoAuthorization; @@ -14,10 +15,19 @@ class ApiDeciderTest extends TestCase { + /** @var Container */ + private $container; + + protected function setUp(): void + { + $this->container = new Container(); + } + public function testDefaultHandlerWithNoRegisteredHandlers() { - $apiDecider = new ApiDecider(); - $result = $apiDecider->getApi('POST', 1, 'article', 'list'); + + $apiDecider = new ApiDecider($this->container); + $result = $apiDecider->getApi('POST', '1', 'article', 'list'); $this->assertInstanceOf(EndpointIdentifier::class, $result->getEndpoint()); $this->assertInstanceOf(NoAuthorization::class, $result->getAuthorization()); @@ -26,33 +36,33 @@ public function testDefaultHandlerWithNoRegisteredHandlers() public function testFindRightHandler() { - $apiDecider = new ApiDecider(); + $apiDecider = new ApiDecider($this->container); $apiDecider->addApi( - new EndpointIdentifier('POST', 2, 'comments', 'list'), + new EndpointIdentifier('POST', '2', 'comments', 'list'), new AlwaysOkHandler(), new NoAuthorization() ); - $result = $apiDecider->getApi('POST', 2, 'comments', 'list'); + $result = $apiDecider->getApi('POST', '2', 'comments', 'list'); $this->assertInstanceOf(EndpointIdentifier::class, $result->getEndpoint()); $this->assertInstanceOf(NoAuthorization::class, $result->getAuthorization()); $this->assertInstanceOf(AlwaysOkHandler::class, $result->getHandler()); $this->assertEquals('POST', $result->getEndpoint()->getMethod()); - $this->assertEquals(2, $result->getEndpoint()->getVersion()); + $this->assertEquals('2', $result->getEndpoint()->getVersion()); $this->assertEquals('comments', $result->getEndpoint()->getPackage()); $this->assertEquals('list', $result->getEndpoint()->getApiAction()); } public function testGetHandlers() { - $apiDecider = new ApiDecider(); + $apiDecider = new ApiDecider($this->container); $this->assertEquals(0, count($apiDecider->getApis())); $apiDecider->addApi( - new EndpointIdentifier('POST', 2, 'comments', 'list'), + new EndpointIdentifier('POST', '2', 'comments', 'list'), new AlwaysOkHandler(), new NoAuthorization() ); @@ -62,20 +72,20 @@ public function testGetHandlers() public function testGlobalPreflight() { - $apiDecider = new ApiDecider(); + $apiDecider = new ApiDecider($this->container); $apiDecider->enableGlobalPreflight(); $this->assertEquals(0, count($apiDecider->getApis())); $apiDecider->addApi( - new EndpointIdentifier('POST', 2, 'comments', 'list'), + new EndpointIdentifier('POST', '2', 'comments', 'list'), new AlwaysOkHandler(), new NoAuthorization() ); $this->assertEquals(1, count($apiDecider->getApis())); - $handler = $apiDecider->getApi('OPTIONS', 2, 'comments', 'list'); + $handler = $apiDecider->getApi('OPTIONS', '2', 'comments', 'list'); $this->assertInstanceOf(CorsPreflightHandler::class, $handler->getHandler()); } } diff --git a/tests/EndpoinIdentifierTest.php b/tests/EndpoinIdentifierTest.php index d698ae4..26d5c9f 100644 --- a/tests/EndpoinIdentifierTest.php +++ b/tests/EndpoinIdentifierTest.php @@ -4,6 +4,7 @@ namespace Tomaj\NetteApi\Test\Params; +use InvalidArgumentException; use PHPUnit\Framework\TestCase; use Tomaj\NetteApi\EndpointIdentifier; @@ -11,19 +12,49 @@ class EndpointIdentifierTest extends TestCase { public function testValidation() { - $endpoint = new EndpointIdentifier('POST', 1, 'core', 'show'); + $endpoint = new EndpointIdentifier('POST', '1', 'core', 'show'); + $this->assertSame('1', $endpoint->getVersion()); $this->assertEquals('POST', $endpoint->getMethod()); - $this->assertEquals(1, $endpoint->getVersion()); $this->assertEquals('core', $endpoint->getPackage()); $this->assertEquals('show', $endpoint->getApiAction()); $this->assertEquals('v1/core/show', $endpoint->getUrl()); + + $endpoint = new EndpointIdentifier('POST', '1.1', 'core', 'show'); + $this->assertEquals('v1.1/core/show', $endpoint->getUrl()); + + $endpoint = new EndpointIdentifier('POST', '1.1.2', 'core', 'show'); + $this->assertEquals('v1.1.2/core/show', $endpoint->getUrl()); } public function testSimpleUrl() { - $endpoint = new EndpointIdentifier('get', 2, 'main', ''); + $endpoint = new EndpointIdentifier('get', '2', 'main', ''); $this->assertNull($endpoint->getApiAction()); $this->assertEquals('GET', $endpoint->getMethod()); } + + public function testSupportedVersions() + { + $endpoint = new EndpointIdentifier('GET', '0', 'core', 'show'); + $this->assertEquals('v0/core/show', $endpoint->getUrl()); + $endpoint = new EndpointIdentifier('GET', '1', 'core', 'show'); + $this->assertEquals('v1/core/show', $endpoint->getUrl()); + $endpoint = new EndpointIdentifier('GET', '1.0', 'core', 'show'); + $this->assertEquals('v1.0/core/show', $endpoint->getUrl()); + $endpoint = new EndpointIdentifier('GET', '1.1', 'core', 'show'); + $this->assertEquals('v1.1/core/show', $endpoint->getUrl()); + $endpoint = new EndpointIdentifier('GET', '1.33', 'core', 'show'); + $this->assertEquals('v1.33/core/show', $endpoint->getUrl()); + $endpoint = new EndpointIdentifier('GET', '1.33-dev', 'core', 'show'); + $this->assertEquals('v1.33-dev/core/show', $endpoint->getUrl()); + $endpoint = new EndpointIdentifier('GET', '0.33.43', 'core', 'show'); + $this->assertEquals('v0.33.43/core/show', $endpoint->getUrl()); + } + + public function testFailVersion() + { + $this->expectException(InvalidArgumentException::class); + new EndpointIdentifier('GET', '1.0/dev', 'core', 'show'); + } } diff --git a/tests/Handler/ApiListingHandlerTest.php b/tests/Handler/ApiListingHandlerTest.php index 791e6f7..fc3183d 100644 --- a/tests/Handler/ApiListingHandlerTest.php +++ b/tests/Handler/ApiListingHandlerTest.php @@ -6,6 +6,7 @@ use Nette\Application\LinkGenerator; use Nette\Application\Routers\SimpleRouter; +use Nette\DI\Container; use Nette\Http\UrlScript; use PHPUnit\Framework\TestCase; use Tomaj\NetteApi\ApiDecider; @@ -18,25 +19,33 @@ class ApiListingHandlerTest extends TestCase { + /** @var Container */ + private $container; + + protected function setUp(): void + { + $this->container = new Container(); + } + public function testDefaultHandle() { $linkGenerator = new LinkGenerator(new SimpleRouter([]), new UrlScript('http://test/')); $apiLink = new ApiLink($linkGenerator); - $apiDecider = new ApiDecider(); + $apiDecider = new ApiDecider($this->container); $apiDecider->addApi( - new EndpointIdentifier('POST', 2, 'comments', 'list'), + new EndpointIdentifier('POST', '2', 'comments', 'list'), new AlwaysOkHandler(), new NoAuthorization() ); $apiDecider->addApi( - new EndpointIdentifier('GET', 2, 'endpoints'), + new EndpointIdentifier('GET', '2', 'endpoints'), new ApiListingHandler($apiDecider, $apiLink), new NoAuthorization() ); - $result = $apiDecider->getApi('GET', 2, 'endpoints'); + $result = $apiDecider->getApi('GET', '2', 'endpoints'); $handler = $result->getHandler(); $response = $handler->handle([]); @@ -50,20 +59,20 @@ public function testHandlerWithParam() $linkGenerator = new LinkGenerator(new SimpleRouter([]), new UrlScript('http://test/')); $apiLink = new ApiLink($linkGenerator); - $apiDecider = new ApiDecider(); + $apiDecider = new ApiDecider($this->container); $apiDecider->addApi( - new EndpointIdentifier('POST', 1, 'comments', 'list'), + new EndpointIdentifier('POST', '1', 'comments', 'list'), new EchoHandler(), new NoAuthorization() ); $apiDecider->addApi( - new EndpointIdentifier('GET', 1, 'endpoints'), + new EndpointIdentifier('GET', '1', 'endpoints'), new ApiListingHandler($apiDecider, $apiLink), new NoAuthorization() ); - $result = $apiDecider->getApi('GET', 1, 'endpoints'); + $result = $apiDecider->getApi('GET', '1', 'endpoints'); $handler = $result->getHandler(); $response = $handler->handle([]); diff --git a/tests/Handler/CorsPreflightHandlerTest.php b/tests/Handler/CorsPreflightHandlerTest.php index eacd8ca..fea17da 100644 --- a/tests/Handler/CorsPreflightHandlerTest.php +++ b/tests/Handler/CorsPreflightHandlerTest.php @@ -28,7 +28,7 @@ public function testEndpointSetter() $defaultHandler = new CorsPreflightHandler(new Response()); $this->assertNull($defaultHandler->getEndpoint()); - $endpointIdentifier = new EndpointIdentifier('OPTIONS', 1, 'article', 'detail'); + $endpointIdentifier = new EndpointIdentifier('OPTIONS', '1', 'article', 'detail'); $defaultHandler->setEndpointIdentifier($endpointIdentifier); $this->assertEquals($endpointIdentifier, $defaultHandler->getEndpoint()); } @@ -61,7 +61,7 @@ public function testCreateLink() $linkGenerator = new LinkGenerator(new SimpleRouter([]), new UrlScript('http://test/')); $defaultHandler->setupLinkGenerator($linkGenerator); - $endpointIdentifier = new EndpointIdentifier('OPTIONS', 1, 'article', 'detail'); + $endpointIdentifier = new EndpointIdentifier('OPTIONS', '1', 'article', 'detail'); $defaultHandler->setEndpointIdentifier($endpointIdentifier); $this->assertEquals('http://test/?version=1&package=article&apiAction=detail&page=2&action=default&presenter=Api%3AApi', $defaultHandler->createLink(['page' => 2])); diff --git a/tests/Handler/DefaultHandlerTest.php b/tests/Handler/DefaultHandlerTest.php index 13abd66..00e0160 100644 --- a/tests/Handler/DefaultHandlerTest.php +++ b/tests/Handler/DefaultHandlerTest.php @@ -18,7 +18,7 @@ public function testResponse() { $defaultHandler = new DefaultHandler(); $result = $defaultHandler->handle([]); - $this->assertEquals(400, $result->getCode()); + $this->assertEquals(404, $result->getCode()); $this->assertEquals('application/json', $result->getContentType()); $this->assertEquals('utf-8', $result->getCharset()); $this->assertEquals(['status' => 'error', 'message' => 'Unknown api endpoint'], $result->getPayload()); @@ -29,7 +29,7 @@ public function testEndpointSetter() $defaultHandler = new DefaultHandler(); $this->assertNull($defaultHandler->getEndpoint()); - $endpointIdentifier = new EndpointIdentifier('POST', 1, 'article', 'detail'); + $endpointIdentifier = new EndpointIdentifier('POST', '1', 'article', 'detail'); $defaultHandler->setEndpointIdentifier($endpointIdentifier); $this->assertEquals($endpointIdentifier, $defaultHandler->getEndpoint()); } @@ -62,7 +62,7 @@ public function testCreateLink() $linkGenerator = new LinkGenerator(new SimpleRouter([]), new UrlScript('http://test/')); $defaultHandler->setupLinkGenerator($linkGenerator); - $endpointIdentifier = new EndpointIdentifier('POST', 1, 'article', 'detail'); + $endpointIdentifier = new EndpointIdentifier('POST', '1', 'article', 'detail'); $defaultHandler->setEndpointIdentifier($endpointIdentifier); $this->assertEquals('http://test/?version=1&package=article&apiAction=detail&page=2&action=default&presenter=Api%3AApi', $defaultHandler->createLink(['page' => 2])); diff --git a/tests/Handler/OpenApiHandlerTest.php b/tests/Handler/OpenApiHandlerTest.php index ccb9d0e..5667173 100644 --- a/tests/Handler/OpenApiHandlerTest.php +++ b/tests/Handler/OpenApiHandlerTest.php @@ -6,6 +6,7 @@ use Nette\Application\LinkGenerator; use Nette\Application\Routers\SimpleRouter; +use Nette\DI\Container; use Nette\Http\UrlScript; use PHPUnit\Framework\TestCase; use Tomaj\NetteApi\ApiDecider; @@ -17,26 +18,34 @@ class OpenApiHandlerTest extends TestCase { + /** @var Container */ + private $container; + + protected function setUp(): void + { + $this->container = new Container(); + } + public function testHandlerWithMultipleResponseSchemas() { $linkGenerator = new LinkGenerator(new SimpleRouter([]), new UrlScript('http://test/')); $apiLink = new ApiLink($linkGenerator); $request = new Request(new UrlScript('http://test/')); - $apiDecider = new ApiDecider(); + $apiDecider = new ApiDecider($this->container); $apiDecider->addApi( - new EndpointIdentifier('GET', 1, 'test'), + new EndpointIdentifier('GET', '1', 'test'), new MultipleOutputTestHandler(), new NoAuthorization() ); $apiDecider->addApi( - new EndpointIdentifier('GET', 1, 'docs', 'open-api'), + new EndpointIdentifier('GET', '1', 'docs', 'open-api'), new OpenApiHandler($apiDecider, $apiLink, $request), new NoAuthorization() ); - $result = $apiDecider->getApi('GET', 1, 'docs', 'open-api'); + $result = $apiDecider->getApi('GET', '1', 'docs', 'open-api'); $handler = $result->getHandler(); $response = $handler->handle(['format' => 'json']); diff --git a/tests/Presenters/ApiPresenterTest.php b/tests/Presenters/ApiPresenterTest.php index ef1dea1..2d194d5 100644 --- a/tests/Presenters/ApiPresenterTest.php +++ b/tests/Presenters/ApiPresenterTest.php @@ -4,37 +4,47 @@ namespace Tomaj\NetteApi\Test\Presenters; -use PHPUnit\Framework\TestCase; use Nette\Application\Request; use Nette\DI\Container; use Nette\Http\Response as HttpResponse; -use Nette\Http\Request as HttpRequest; -use Nette\Http\UrlScript; +use PHPUnit\Framework\TestCase; use Tomaj\NetteApi\ApiDecider; use Tomaj\NetteApi\Authorization\BearerTokenAuthorization; use Tomaj\NetteApi\Authorization\NoAuthorization; use Tomaj\NetteApi\EndpointIdentifier; +use Tomaj\NetteApi\Error\DefaultErrorHandler; use Tomaj\NetteApi\Handlers\AlwaysOkHandler; use Tomaj\NetteApi\Handlers\EchoHandler; use Tomaj\NetteApi\Misc\IpDetector; use Tomaj\NetteApi\Misc\StaticTokenRepository; +use Tomaj\NetteApi\Output\Configurator\DebuggerConfigurator; use Tomaj\NetteApi\Presenters\ApiPresenter; use Tomaj\NetteApi\Test\Handler\TestHandler; use Tracy\Debugger; class ApiPresenterTest extends TestCase { + /** @var Container */ + private $container; + + protected function setUp(): void + { + $this->container = new Container(); + } + public function testSimpleResponse() { - $apiDecider = new ApiDecider(); - $apiDecider->addApi(new EndpointIdentifier('GET', 1, 'test', 'api'), new AlwaysOkHandler(), new NoAuthorization()); + $apiDecider = new ApiDecider($this->container); + $apiDecider->addApi(new EndpointIdentifier('GET', '1', 'test', 'api'), new AlwaysOkHandler(), new NoAuthorization()); $presenter = new ApiPresenter(); $presenter->apiDecider = $apiDecider; $presenter->response = new HttpResponse(); $presenter->context = new Container(); + $presenter->outputConfigurator = new DebuggerConfigurator(); + $presenter->errorHandler = new DefaultErrorHandler($presenter->outputConfigurator); - $request = new Request('Api:Api:default', 'GET', ['version' => 1, 'package' => 'test', 'apiAction' => 'api']); + $request = new Request('Api:Api:default', 'GET', ['version' => '1', 'package' => 'test', 'apiAction' => 'api']); $result = $presenter->run($request); $this->assertEquals(200, $result->getCode()); @@ -45,9 +55,9 @@ public function testSimpleResponse() public function testWithAuthorization() { - $apiDecider = new ApiDecider(); + $apiDecider = new ApiDecider($this->container); $apiDecider->addApi( - new EndpointIdentifier('GET', 1, 'test', 'api'), + new EndpointIdentifier('GET', '1', 'test', 'api'), new AlwaysOkHandler(), new BearerTokenAuthorization(new StaticTokenRepository([]), new IpDetector()) ); @@ -56,8 +66,10 @@ public function testWithAuthorization() $presenter->apiDecider = $apiDecider; $presenter->response = new HttpResponse(); $presenter->context = new Container(); + $presenter->outputConfigurator = new DebuggerConfigurator(); + $presenter->errorHandler = new DefaultErrorHandler($presenter->outputConfigurator); - $request = new Request('Api:Api:default', 'GET', ['version' => 1, 'package' => 'test', 'apiAction' => 'api']); + $request = new Request('Api:Api:default', 'GET', ['version' => '1', 'package' => 'test', 'apiAction' => 'api']); $result = $presenter->run($request); $this->assertEquals(['status' => 'error', 'message' => 'Authorization header HTTP_Authorization is not set'], $result->getPayload()); @@ -66,9 +78,9 @@ public function testWithAuthorization() public function testWithParams() { - $apiDecider = new ApiDecider(); + $apiDecider = new ApiDecider($this->container); $apiDecider->addApi( - new EndpointIdentifier('GET', 1, 'test', 'api'), + new EndpointIdentifier('GET', '1', 'test', 'api'), new EchoHandler(), new NoAuthorization() ); @@ -77,10 +89,12 @@ public function testWithParams() $presenter->apiDecider = $apiDecider; $presenter->response = new HttpResponse(); $presenter->context = new Container(); + $presenter->outputConfigurator = new DebuggerConfigurator(); + $presenter->errorHandler = new DefaultErrorHandler($presenter->outputConfigurator); Debugger::$productionMode = Debugger::PRODUCTION; - $request = new Request('Api:Api:default', 'GET', ['version' => 1, 'package' => 'test', 'apiAction' => 'api']); + $request = new Request('Api:Api:default', 'GET', ['version' => '1', 'package' => 'test', 'apiAction' => 'api']); $result = $presenter->run($request); $this->assertEquals(['status' => 'error', 'message' => 'wrong input'], $result->getPayload()); @@ -95,9 +109,9 @@ public function testWithParams() public function testWithOutputs() { - $apiDecider = new ApiDecider(); + $apiDecider = new ApiDecider($this->container); $apiDecider->addApi( - new EndpointIdentifier('GET', 1, 'test', 'api'), + new EndpointIdentifier('GET', '1', 'test', 'api'), new TestHandler(), new NoAuthorization() ); @@ -106,8 +120,10 @@ public function testWithOutputs() $presenter->apiDecider = $apiDecider; $presenter->response = new HttpResponse(); $presenter->context = new Container(); + $presenter->outputConfigurator = new DebuggerConfigurator(); + $presenter->errorHandler = new DefaultErrorHandler($presenter->outputConfigurator); - $request = new Request('Api:Api:default', 'GET', ['version' => 1, 'package' => 'test', 'apiAction' => 'api']); + $request = new Request('Api:Api:default', 'GET', ['version' => '1', 'package' => 'test', 'apiAction' => 'api']); $result = $presenter->run($request); $this->assertEquals(['hello' => 'world'], $result->getPayload());