From 84c4631124d9cb542c502a42315fc88af6a81f01 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 20 Nov 2019 20:18:21 +0100 Subject: [PATCH 01/40] Deleted specific factory by replacing it by ConfigAbstractFactory --- module/Rest/config/dependencies.config.php | 4 +- .../Rest/src/Action/HealthActionFactory.php | 20 --------- module/Rest/src/Authentication/JWTService.php | 1 + .../test/Action/HealthActionFactoryTest.php | 44 ------------------- 4 files changed, 4 insertions(+), 65 deletions(-) delete mode 100644 module/Rest/src/Action/HealthActionFactory.php delete mode 100644 module/Rest/test/Action/HealthActionFactoryTest.php diff --git a/module/Rest/config/dependencies.config.php b/module/Rest/config/dependencies.config.php index 54f380b3..5ef1ecba 100644 --- a/module/Rest/config/dependencies.config.php +++ b/module/Rest/config/dependencies.config.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Rest; +use Doctrine\DBAL\Connection; use Psr\Log\LoggerInterface; use Shlinkio\Shlink\Core\Options\AppOptions; use Shlinkio\Shlink\Core\Service; @@ -20,7 +21,7 @@ return [ ApiKeyService::class => ConfigAbstractFactory::class, Action\AuthenticateAction::class => ConfigAbstractFactory::class, - Action\HealthAction::class => Action\HealthActionFactory::class, + Action\HealthAction::class => ConfigAbstractFactory::class, Action\ShortUrl\CreateShortUrlAction::class => ConfigAbstractFactory::class, Action\ShortUrl\SingleStepCreateShortUrlAction::class => ConfigAbstractFactory::class, Action\ShortUrl\EditShortUrlAction::class => ConfigAbstractFactory::class, @@ -48,6 +49,7 @@ return [ ApiKeyService::class => ['em'], Action\AuthenticateAction::class => [ApiKeyService::class, Authentication\JWTService::class, 'Logger_Shlink'], + Action\HealthAction::class => [Connection::class, AppOptions::class, 'Logger_Shlink'], Action\ShortUrl\CreateShortUrlAction::class => [ Service\UrlShortener::class, 'config.url_shortener.domain', diff --git a/module/Rest/src/Action/HealthActionFactory.php b/module/Rest/src/Action/HealthActionFactory.php deleted file mode 100644 index 269a8f4d..00000000 --- a/module/Rest/src/Action/HealthActionFactory.php +++ /dev/null @@ -1,20 +0,0 @@ -get(EntityManager::class); - $options = $container->get(AppOptions::class); - $logger = $container->get('Logger_Shlink'); - return new HealthAction($em->getConnection(), $options, $logger); - } -} diff --git a/module/Rest/src/Authentication/JWTService.php b/module/Rest/src/Authentication/JWTService.php index 841527e4..77ec6728 100644 --- a/module/Rest/src/Authentication/JWTService.php +++ b/module/Rest/src/Authentication/JWTService.php @@ -12,6 +12,7 @@ use UnexpectedValueException; use function time; +/** @deprecated */ class JWTService implements JWTServiceInterface { /** @var AppOptions */ diff --git a/module/Rest/test/Action/HealthActionFactoryTest.php b/module/Rest/test/Action/HealthActionFactoryTest.php deleted file mode 100644 index 3ecb15c1..00000000 --- a/module/Rest/test/Action/HealthActionFactoryTest.php +++ /dev/null @@ -1,44 +0,0 @@ -factory = new Action\HealthActionFactory(); - } - - /** @test */ - public function serviceIsCreatedExtractingConnectionFromEntityManager() - { - $em = $this->prophesize(EntityManager::class); - $conn = $this->prophesize(Connection::class); - - $getConnection = $em->getConnection()->willReturn($conn->reveal()); - - $sm = new ServiceManager(['services' => [ - 'Logger_Shlink' => $this->prophesize(LoggerInterface::class)->reveal(), - AppOptions::class => $this->prophesize(AppOptions::class)->reveal(), - EntityManager::class => $em->reveal(), - ]]); - - $instance = ($this->factory)($sm, ''); - - $this->assertInstanceOf(Action\HealthAction::class, $instance); - $getConnection->shouldHaveBeenCalledOnce(); - } -} From 98b6dba05d34cf2cf8a88db55b7f5a1044b83f0d Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 20 Nov 2019 20:21:02 +0100 Subject: [PATCH 02/40] Removed generic error handling from action that will usually be handled by ErrorHandler middleware --- .../ShortUrl/AbstractCreateShortUrlAction.php | 7 ------- .../Action/ShortUrl/CreateShortUrlActionTest.php | 16 ---------------- 2 files changed, 23 deletions(-) diff --git a/module/Rest/src/Action/ShortUrl/AbstractCreateShortUrlAction.php b/module/Rest/src/Action/ShortUrl/AbstractCreateShortUrlAction.php index e55f564e..f3eaee19 100644 --- a/module/Rest/src/Action/ShortUrl/AbstractCreateShortUrlAction.php +++ b/module/Rest/src/Action/ShortUrl/AbstractCreateShortUrlAction.php @@ -15,7 +15,6 @@ use Shlinkio\Shlink\Core\Service\UrlShortenerInterface; use Shlinkio\Shlink\Core\Transformer\ShortUrlDataTransformer; use Shlinkio\Shlink\Rest\Action\AbstractRestAction; use Shlinkio\Shlink\Rest\Util\RestUtils; -use Throwable; use Zend\Diactoros\Response\JsonResponse; use function sprintf; @@ -74,12 +73,6 @@ abstract class AbstractCreateShortUrlAction extends AbstractRestAction 'error' => RestUtils::getRestErrorCodeFromException($e), 'message' => sprintf('Provided slug %s is already in use. Try with a different one.', $customSlug), ], self::STATUS_BAD_REQUEST); - } catch (Throwable $e) { - $this->logger->error('Unexpected error creating short url. {e}', ['e' => $e]); - return new JsonResponse([ - 'error' => RestUtils::UNKNOWN_ERROR, - 'message' => 'Unexpected error occurred', - ], self::STATUS_INTERNAL_SERVER_ERROR); } } diff --git a/module/Rest/test/Action/ShortUrl/CreateShortUrlActionTest.php b/module/Rest/test/Action/ShortUrl/CreateShortUrlActionTest.php index cea67f0f..732cbc25 100644 --- a/module/Rest/test/Action/ShortUrl/CreateShortUrlActionTest.php +++ b/module/Rest/test/Action/ShortUrl/CreateShortUrlActionTest.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace ShlinkioTest\Shlink\Rest\Action\ShortUrl; -use Exception; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\Prophecy\ObjectProphecy; @@ -124,19 +123,4 @@ class CreateShortUrlActionTest extends TestCase $this->assertEquals(400, $response->getStatusCode()); $this->assertStringContainsString(RestUtils::INVALID_SLUG_ERROR, (string) $response->getBody()); } - - /** @test */ - public function aGenericExceptionWillReturnError(): void - { - $this->urlShortener->urlToShortCode(Argument::type(Uri::class), Argument::type('array'), Argument::cetera()) - ->willThrow(Exception::class) - ->shouldBeCalledOnce(); - - $request = (new ServerRequest())->withParsedBody([ - 'longUrl' => 'http://www.domain.com/foo/bar', - ]); - $response = $this->action->handle($request); - $this->assertEquals(500, $response->getStatusCode()); - $this->assertTrue(strpos($response->getBody()->getContents(), RestUtils::UNKNOWN_ERROR) > 0); - } } From ba6e8c4092e8b425930a7d6ad334b7ed3ec018da Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 20 Nov 2019 20:31:18 +0100 Subject: [PATCH 03/40] Created API tests for errors when editing a short URL --- bin/test/run-api-tests.sh | 2 +- module/Rest/src/Util/RestUtils.php | 4 ++- .../Action/EditShortUrlActionTest.php | 34 +++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 module/Rest/test-api/Action/EditShortUrlActionTest.php diff --git a/bin/test/run-api-tests.sh b/bin/test/run-api-tests.sh index 75a51387..cbc10a1a 100755 --- a/bin/test/run-api-tests.sh +++ b/bin/test/run-api-tests.sh @@ -9,7 +9,7 @@ echo 'Starting server...' vendor/bin/zend-expressive-swoole start -d sleep 2 -vendor/bin/phpunit --order-by=random -c phpunit-api.xml --testdox --colors=always +vendor/bin/phpunit --order-by=random -c phpunit-api.xml --testdox --colors=always $* testsExitCode=$? vendor/bin/zend-expressive-swoole stop diff --git a/module/Rest/src/Util/RestUtils.php b/module/Rest/src/Util/RestUtils.php index d4ad46dc..3b56fef0 100644 --- a/module/Rest/src/Util/RestUtils.php +++ b/module/Rest/src/Util/RestUtils.php @@ -9,6 +9,7 @@ use Shlinkio\Shlink\Core\Exception as Core; use Shlinkio\Shlink\Rest\Exception as Rest; use Throwable; +/** @deprecated */ class RestUtils { public const INVALID_SHORTCODE_ERROR = 'INVALID_SHORTCODE'; @@ -24,7 +25,8 @@ class RestUtils public const NOT_FOUND_ERROR = 'NOT_FOUND'; public const UNKNOWN_ERROR = 'UNKNOWN_ERROR'; - public static function getRestErrorCodeFromException(Throwable $e) + /** @deprecated */ + public static function getRestErrorCodeFromException(Throwable $e): string { switch (true) { case $e instanceof Core\InvalidShortCodeException: diff --git a/module/Rest/test-api/Action/EditShortUrlActionTest.php b/module/Rest/test-api/Action/EditShortUrlActionTest.php new file mode 100644 index 00000000..65f2fd1d --- /dev/null +++ b/module/Rest/test-api/Action/EditShortUrlActionTest.php @@ -0,0 +1,34 @@ +callApiWithKey(self::METHOD_PATCH, '/short-urls/invalid', [RequestOptions::JSON => []]); + ['error' => $error] = $this->getJsonResponsePayload($resp); + + $this->assertEquals(self::STATUS_NOT_FOUND, $resp->getStatusCode()); + $this->assertEquals(RestUtils::INVALID_SHORTCODE_ERROR, $error); + } + + /** @test */ + public function providingInvalidDataReturnsBadRequest(): void + { + $resp = $this->callApiWithKey(self::METHOD_PATCH, '/short-urls/invalid', [RequestOptions::JSON => [ + 'maxVisits' => 'not_a_number', + ]]); + ['error' => $error] = $this->getJsonResponsePayload($resp); + + $this->assertEquals(self::STATUS_BAD_REQUEST, $resp->getStatusCode()); + $this->assertEquals(RestUtils::INVALID_ARGUMENT_ERROR, $error); + } +} From 909631896836e4ad0e81da0d68a7ecded1314d3e Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 20 Nov 2019 20:38:19 +0100 Subject: [PATCH 04/40] Created API tests for errors when deleting short URLs --- module/Rest/src/Util/RestUtils.php | 1 - .../Action/DeleteShortUrlActionTest.php | 37 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 module/Rest/test-api/Action/DeleteShortUrlActionTest.php diff --git a/module/Rest/src/Util/RestUtils.php b/module/Rest/src/Util/RestUtils.php index 3b56fef0..60aade54 100644 --- a/module/Rest/src/Util/RestUtils.php +++ b/module/Rest/src/Util/RestUtils.php @@ -9,7 +9,6 @@ use Shlinkio\Shlink\Core\Exception as Core; use Shlinkio\Shlink\Rest\Exception as Rest; use Throwable; -/** @deprecated */ class RestUtils { public const INVALID_SHORTCODE_ERROR = 'INVALID_SHORTCODE'; diff --git a/module/Rest/test-api/Action/DeleteShortUrlActionTest.php b/module/Rest/test-api/Action/DeleteShortUrlActionTest.php new file mode 100644 index 00000000..60631565 --- /dev/null +++ b/module/Rest/test-api/Action/DeleteShortUrlActionTest.php @@ -0,0 +1,37 @@ +callApiWithKey(self::METHOD_DELETE, '/short-urls/invalid'); + ['error' => $error] = $this->getJsonResponsePayload($resp); + + $this->assertEquals(self::STATUS_NOT_FOUND, $resp->getStatusCode()); + $this->assertEquals(RestUtils::INVALID_SHORTCODE_ERROR, $error); + } + + /** @test */ + public function badRequestIsReturnedWhenTryingToDeleteUrlWithTooManyVisits(): void + { + // Generate visits first + for ($i = 0; $i < 20; $i++) { + $this->assertEquals(self::STATUS_FOUND, $this->callShortUrl('abc123')->getStatusCode()); + } + + $resp = $this->callApiWithKey(self::METHOD_DELETE, '/short-urls/abc123'); + ['error' => $error] = $this->getJsonResponsePayload($resp); + + $this->assertEquals(self::STATUS_BAD_REQUEST, $resp->getStatusCode()); + $this->assertEquals(RestUtils::INVALID_SHORTCODE_DELETION_ERROR, $error); + } +} From d044e1a5b771574933d974284ea91289e0fbad62 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 20 Nov 2019 20:44:03 +0100 Subject: [PATCH 05/40] Created API tests for errors when resolving short URLs --- module/Core/src/Service/UrlShortener.php | 2 -- .../src/Service/UrlShortenerInterface.php | 2 -- .../Action/ShortUrl/ResolveShortUrlAction.php | 19 ++----------- .../Action/DeleteShortUrlActionTest.php | 1 - .../Action/ResolveShortUrlActionTest.php | 21 ++++++++++++++ .../ShortUrl/ResolveShortUrlActionTest.php | 28 ------------------- 6 files changed, 24 insertions(+), 49 deletions(-) create mode 100644 module/Rest/test-api/Action/ResolveShortUrlActionTest.php diff --git a/module/Core/src/Service/UrlShortener.php b/module/Core/src/Service/UrlShortener.php index 6b04d63a..7d71ca7a 100644 --- a/module/Core/src/Service/UrlShortener.php +++ b/module/Core/src/Service/UrlShortener.php @@ -9,7 +9,6 @@ use Psr\Http\Message\UriInterface; use Shlinkio\Shlink\Core\Domain\Resolver\PersistenceDomainResolver; use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; -use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; use Shlinkio\Shlink\Core\Exception\InvalidUrlException; use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException; use Shlinkio\Shlink\Core\Model\ShortUrlMeta; @@ -130,7 +129,6 @@ class UrlShortener implements UrlShortenerInterface } /** - * @throws InvalidShortCodeException * @throws EntityDoesNotExistException */ public function shortCodeToUrl(string $shortCode, ?string $domain = null): ShortUrl diff --git a/module/Core/src/Service/UrlShortenerInterface.php b/module/Core/src/Service/UrlShortenerInterface.php index 74d626fc..9d6b291b 100644 --- a/module/Core/src/Service/UrlShortenerInterface.php +++ b/module/Core/src/Service/UrlShortenerInterface.php @@ -7,7 +7,6 @@ namespace Shlinkio\Shlink\Core\Service; use Psr\Http\Message\UriInterface; use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; -use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; use Shlinkio\Shlink\Core\Exception\InvalidUrlException; use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException; use Shlinkio\Shlink\Core\Exception\RuntimeException; @@ -24,7 +23,6 @@ interface UrlShortenerInterface public function urlToShortCode(UriInterface $url, array $tags, ShortUrlMeta $meta): ShortUrl; /** - * @throws InvalidShortCodeException * @throws EntityDoesNotExistException */ public function shortCodeToUrl(string $shortCode, ?string $domain = null): ShortUrl; diff --git a/module/Rest/src/Action/ShortUrl/ResolveShortUrlAction.php b/module/Rest/src/Action/ShortUrl/ResolveShortUrlAction.php index 57d3cd62..9599013a 100644 --- a/module/Rest/src/Action/ShortUrl/ResolveShortUrlAction.php +++ b/module/Rest/src/Action/ShortUrl/ResolveShortUrlAction.php @@ -4,12 +4,11 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Rest\Action\ShortUrl; -use Exception; +use InvalidArgumentException; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Log\LoggerInterface; use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; -use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; use Shlinkio\Shlink\Core\Service\UrlShortenerInterface; use Shlinkio\Shlink\Core\Transformer\ShortUrlDataTransformer; use Shlinkio\Shlink\Rest\Action\AbstractRestAction; @@ -41,7 +40,7 @@ class ResolveShortUrlAction extends AbstractRestAction /** * @param Request $request * @return Response - * @throws \InvalidArgumentException + * @throws InvalidArgumentException */ public function handle(Request $request): Response { @@ -52,24 +51,12 @@ class ResolveShortUrlAction extends AbstractRestAction try { $url = $this->urlShortener->shortCodeToUrl($shortCode, $domain); return new JsonResponse($transformer->transform($url)); - } catch (InvalidShortCodeException $e) { - $this->logger->warning('Provided short code with invalid format. {e}', ['e' => $e]); - return new JsonResponse([ - 'error' => RestUtils::getRestErrorCodeFromException($e), - 'message' => sprintf('Provided short code "%s" has an invalid format', $shortCode), - ], self::STATUS_BAD_REQUEST); } catch (EntityDoesNotExistException $e) { $this->logger->warning('Provided short code couldn\'t be found. {e}', ['e' => $e]); return new JsonResponse([ - 'error' => RestUtils::INVALID_ARGUMENT_ERROR, + 'error' => RestUtils::INVALID_ARGUMENT_ERROR, // FIXME Not correct. Use correct value on "type" 'message' => sprintf('No URL found for short code "%s"', $shortCode), ], self::STATUS_NOT_FOUND); - } catch (Exception $e) { - $this->logger->error('Unexpected error while resolving the URL behind a short code. {e}', ['e' => $e]); - return new JsonResponse([ - 'error' => RestUtils::UNKNOWN_ERROR, - 'message' => 'Unexpected error occurred', - ], self::STATUS_INTERNAL_SERVER_ERROR); } } } diff --git a/module/Rest/test-api/Action/DeleteShortUrlActionTest.php b/module/Rest/test-api/Action/DeleteShortUrlActionTest.php index 60631565..c90b7818 100644 --- a/module/Rest/test-api/Action/DeleteShortUrlActionTest.php +++ b/module/Rest/test-api/Action/DeleteShortUrlActionTest.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace ShlinkioApiTest\Shlink\Rest\Action; -use GuzzleHttp\RequestOptions; use Shlinkio\Shlink\Rest\Util\RestUtils; use Shlinkio\Shlink\TestUtils\ApiTest\ApiTestCase; diff --git a/module/Rest/test-api/Action/ResolveShortUrlActionTest.php b/module/Rest/test-api/Action/ResolveShortUrlActionTest.php new file mode 100644 index 00000000..117ae5a9 --- /dev/null +++ b/module/Rest/test-api/Action/ResolveShortUrlActionTest.php @@ -0,0 +1,21 @@ +callApiWithKey(self::METHOD_GET, '/short-urls/invalid'); + ['error' => $error] = $this->getJsonResponsePayload($resp); + + $this->assertEquals(self::STATUS_NOT_FOUND, $resp->getStatusCode()); + $this->assertEquals(RestUtils::INVALID_ARGUMENT_ERROR, $error); + } +} diff --git a/module/Rest/test/Action/ShortUrl/ResolveShortUrlActionTest.php b/module/Rest/test/Action/ShortUrl/ResolveShortUrlActionTest.php index e0c23d31..aa852de5 100644 --- a/module/Rest/test/Action/ShortUrl/ResolveShortUrlActionTest.php +++ b/module/Rest/test/Action/ShortUrl/ResolveShortUrlActionTest.php @@ -4,12 +4,10 @@ declare(strict_types=1); namespace ShlinkioTest\Shlink\Rest\Action\ShortUrl; -use Exception; use PHPUnit\Framework\TestCase; use Prophecy\Prophecy\ObjectProphecy; use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; -use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; use Shlinkio\Shlink\Core\Service\UrlShortener; use Shlinkio\Shlink\Rest\Action\ShortUrl\ResolveShortUrlAction; use Shlinkio\Shlink\Rest\Util\RestUtils; @@ -56,30 +54,4 @@ class ResolveShortUrlActionTest extends TestCase $this->assertEquals(200, $response->getStatusCode()); $this->assertTrue(strpos($response->getBody()->getContents(), 'http://domain.com/foo/bar') > 0); } - - /** @test */ - public function invalidShortCodeExceptionReturnsError(): void - { - $shortCode = 'abc123'; - $this->urlShortener->shortCodeToUrl($shortCode, null)->willThrow(InvalidShortCodeException::class) - ->shouldBeCalledOnce(); - - $request = (new ServerRequest())->withAttribute('shortCode', $shortCode); - $response = $this->action->handle($request); - $this->assertEquals(400, $response->getStatusCode()); - $this->assertTrue(strpos($response->getBody()->getContents(), RestUtils::INVALID_SHORTCODE_ERROR) > 0); - } - - /** @test */ - public function unexpectedExceptionWillReturnError(): void - { - $shortCode = 'abc123'; - $this->urlShortener->shortCodeToUrl($shortCode, null)->willThrow(Exception::class) - ->shouldBeCalledOnce(); - - $request = (new ServerRequest())->withAttribute('shortCode', $shortCode); - $response = $this->action->handle($request); - $this->assertEquals(500, $response->getStatusCode()); - $this->assertTrue(strpos($response->getBody()->getContents(), RestUtils::UNKNOWN_ERROR) > 0); - } } From 34e60ec5b80de829d04e0ee9c2f0a00b84f8658f Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 20 Nov 2019 20:58:16 +0100 Subject: [PATCH 06/40] Created API tests for errors when getting short URL visits --- module/Core/src/Service/VisitsTracker.php | 8 +++---- .../src/Service/VisitsTrackerInterface.php | 4 ++-- .../Rest/src/Action/Visit/GetVisitsAction.php | 6 +++--- .../test-api/Action/GetVisitsActionTest.php | 21 +++++++++++++++++++ .../test/Action/Visit/GetVisitsActionTest.php | 10 ++++----- 5 files changed, 34 insertions(+), 15 deletions(-) create mode 100644 module/Rest/test-api/Action/GetVisitsActionTest.php diff --git a/module/Core/src/Service/VisitsTracker.php b/module/Core/src/Service/VisitsTracker.php index 12af69ce..836c16a9 100644 --- a/module/Core/src/Service/VisitsTracker.php +++ b/module/Core/src/Service/VisitsTracker.php @@ -9,15 +9,13 @@ use Psr\EventDispatcher\EventDispatcherInterface; use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\Visit; use Shlinkio\Shlink\Core\EventDispatcher\ShortUrlVisited; -use Shlinkio\Shlink\Core\Exception\InvalidArgumentException; +use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; use Shlinkio\Shlink\Core\Model\Visitor; use Shlinkio\Shlink\Core\Model\VisitsParams; use Shlinkio\Shlink\Core\Paginator\Adapter\VisitsPaginatorAdapter; use Shlinkio\Shlink\Core\Repository\VisitRepository; use Zend\Paginator\Paginator; -use function sprintf; - class VisitsTracker implements VisitsTrackerInterface { /** @var ORM\EntityManagerInterface */ @@ -53,14 +51,14 @@ class VisitsTracker implements VisitsTrackerInterface * Returns the visits on certain short code * * @return Visit[]|Paginator - * @throws InvalidArgumentException + * @throws InvalidShortCodeException */ public function info(string $shortCode, VisitsParams $params): Paginator { /** @var ORM\EntityRepository $repo */ $repo = $this->em->getRepository(ShortUrl::class); if ($repo->count(['shortCode' => $shortCode]) < 1) { - throw new InvalidArgumentException(sprintf('Short code "%s" not found', $shortCode)); + throw InvalidShortCodeException::fromNotFoundShortCode($shortCode); } /** @var VisitRepository $repo */ diff --git a/module/Core/src/Service/VisitsTrackerInterface.php b/module/Core/src/Service/VisitsTrackerInterface.php index 03af8299..d3934992 100644 --- a/module/Core/src/Service/VisitsTrackerInterface.php +++ b/module/Core/src/Service/VisitsTrackerInterface.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Service; use Shlinkio\Shlink\Core\Entity\Visit; -use Shlinkio\Shlink\Core\Exception\InvalidArgumentException; +use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; use Shlinkio\Shlink\Core\Model\Visitor; use Shlinkio\Shlink\Core\Model\VisitsParams; use Zend\Paginator\Paginator; @@ -21,7 +21,7 @@ interface VisitsTrackerInterface * Returns the visits on certain short code * * @return Visit[]|Paginator - * @throws InvalidArgumentException + * @throws InvalidShortCodeException */ public function info(string $shortCode, VisitsParams $params): Paginator; } diff --git a/module/Rest/src/Action/Visit/GetVisitsAction.php b/module/Rest/src/Action/Visit/GetVisitsAction.php index 222a76b8..68912be2 100644 --- a/module/Rest/src/Action/Visit/GetVisitsAction.php +++ b/module/Rest/src/Action/Visit/GetVisitsAction.php @@ -7,8 +7,8 @@ namespace Shlinkio\Shlink\Rest\Action\Visit; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Log\LoggerInterface; -use Shlinkio\Shlink\Common\Exception\InvalidArgumentException; use Shlinkio\Shlink\Common\Paginator\Util\PaginatorUtilsTrait; +use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; use Shlinkio\Shlink\Core\Model\VisitsParams; use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface; use Shlinkio\Shlink\Rest\Action\AbstractRestAction; @@ -48,10 +48,10 @@ class GetVisitsAction extends AbstractRestAction return new JsonResponse([ 'visits' => $this->serializePaginator($visits), ]); - } catch (InvalidArgumentException $e) { + } catch (InvalidShortCodeException $e) { $this->logger->warning('Provided nonexistent short code {e}', ['e' => $e]); return new JsonResponse([ - 'error' => RestUtils::getRestErrorCodeFromException($e), + 'error' => RestUtils::INVALID_ARGUMENT_ERROR, // FIXME Wrong code. Use correct one in "type" 'message' => sprintf('Provided short code %s does not exist', $shortCode), ], self::STATUS_NOT_FOUND); } diff --git a/module/Rest/test-api/Action/GetVisitsActionTest.php b/module/Rest/test-api/Action/GetVisitsActionTest.php new file mode 100644 index 00000000..150c3066 --- /dev/null +++ b/module/Rest/test-api/Action/GetVisitsActionTest.php @@ -0,0 +1,21 @@ +callApiWithKey(self::METHOD_GET, '/short-urls/invalid/visits'); + ['error' => $error] = $this->getJsonResponsePayload($resp); + + $this->assertEquals(self::STATUS_NOT_FOUND, $resp->getStatusCode()); + $this->assertEquals(RestUtils::INVALID_ARGUMENT_ERROR, $error); + } +} diff --git a/module/Rest/test/Action/Visit/GetVisitsActionTest.php b/module/Rest/test/Action/Visit/GetVisitsActionTest.php index 6a4b533c..789469ca 100644 --- a/module/Rest/test/Action/Visit/GetVisitsActionTest.php +++ b/module/Rest/test/Action/Visit/GetVisitsActionTest.php @@ -8,8 +8,8 @@ use Cake\Chronos\Chronos; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\Prophecy\ObjectProphecy; -use Shlinkio\Shlink\Common\Exception\InvalidArgumentException; use Shlinkio\Shlink\Common\Util\DateRange; +use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; use Shlinkio\Shlink\Core\Model\VisitsParams; use Shlinkio\Shlink\Core\Service\VisitsTracker; use Shlinkio\Shlink\Rest\Action\Visit\GetVisitsAction; @@ -31,7 +31,7 @@ class GetVisitsActionTest extends TestCase } /** @test */ - public function providingCorrectShortCodeReturnsVisits() + public function providingCorrectShortCodeReturnsVisits(): void { $shortCode = 'abc123'; $this->visitsTracker->info($shortCode, Argument::type(VisitsParams::class))->willReturn( @@ -43,11 +43,11 @@ class GetVisitsActionTest extends TestCase } /** @test */ - public function providingInvalidShortCodeReturnsError() + public function providingInvalidShortCodeReturnsError(): void { $shortCode = 'abc123'; $this->visitsTracker->info($shortCode, Argument::type(VisitsParams::class))->willThrow( - InvalidArgumentException::class + InvalidShortCodeException::class )->shouldBeCalledOnce(); $response = $this->action->handle((new ServerRequest())->withAttribute('shortCode', $shortCode)); @@ -55,7 +55,7 @@ class GetVisitsActionTest extends TestCase } /** @test */ - public function paramsAreReadFromQuery() + public function paramsAreReadFromQuery(): void { $shortCode = 'abc123'; $this->visitsTracker->info($shortCode, new VisitsParams( From 8607d58e186db5a2dd1edb49bcbf9968e9b3ca2d Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 21 Nov 2019 18:49:55 +0100 Subject: [PATCH 07/40] Created API tests for errors when editting short URL tags --- .../Action/ShortUrl/ListShortUrlsAction.php | 23 ++++--------- .../Action/EditShortUrlActionTagsTest.php | 34 +++++++++++++++++++ .../ShortUrl/ListShortUrlsActionTest.php | 23 ------------- 3 files changed, 41 insertions(+), 39 deletions(-) create mode 100644 module/Rest/test-api/Action/EditShortUrlActionTagsTest.php diff --git a/module/Rest/src/Action/ShortUrl/ListShortUrlsAction.php b/module/Rest/src/Action/ShortUrl/ListShortUrlsAction.php index 8c4211c8..157fbe06 100644 --- a/module/Rest/src/Action/ShortUrl/ListShortUrlsAction.php +++ b/module/Rest/src/Action/ShortUrl/ListShortUrlsAction.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Rest\Action\ShortUrl; -use Exception; +use InvalidArgumentException; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Log\LoggerInterface; @@ -12,7 +12,6 @@ use Shlinkio\Shlink\Common\Paginator\Util\PaginatorUtilsTrait; use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface; use Shlinkio\Shlink\Core\Transformer\ShortUrlDataTransformer; use Shlinkio\Shlink\Rest\Action\AbstractRestAction; -use Shlinkio\Shlink\Rest\Util\RestUtils; use Zend\Diactoros\Response\JsonResponse; class ListShortUrlsAction extends AbstractRestAction @@ -40,23 +39,15 @@ class ListShortUrlsAction extends AbstractRestAction /** * @param Request $request * @return Response - * @throws \InvalidArgumentException + * @throws InvalidArgumentException */ public function handle(Request $request): Response { - try { - $params = $this->queryToListParams($request->getQueryParams()); - $shortUrls = $this->shortUrlService->listShortUrls(...$params); - return new JsonResponse(['shortUrls' => $this->serializePaginator($shortUrls, new ShortUrlDataTransformer( - $this->domainConfig - ))]); - } catch (Exception $e) { - $this->logger->error('Unexpected error while listing short URLs. {e}', ['e' => $e]); - return new JsonResponse([ - 'error' => RestUtils::UNKNOWN_ERROR, - 'message' => 'Unexpected error occurred', - ], self::STATUS_INTERNAL_SERVER_ERROR); - } + $params = $this->queryToListParams($request->getQueryParams()); + $shortUrls = $this->shortUrlService->listShortUrls(...$params); + return new JsonResponse(['shortUrls' => $this->serializePaginator($shortUrls, new ShortUrlDataTransformer( + $this->domainConfig + ))]); } /** diff --git a/module/Rest/test-api/Action/EditShortUrlActionTagsTest.php b/module/Rest/test-api/Action/EditShortUrlActionTagsTest.php new file mode 100644 index 00000000..d033fcee --- /dev/null +++ b/module/Rest/test-api/Action/EditShortUrlActionTagsTest.php @@ -0,0 +1,34 @@ +callApiWithKey(self::METHOD_PUT, '/short-urls/abc123/tags', [RequestOptions::JSON => []]); + ['error' => $error] = $this->getJsonResponsePayload($resp); + + $this->assertEquals(self::STATUS_BAD_REQUEST, $resp->getStatusCode()); + $this->assertEquals(RestUtils::INVALID_ARGUMENT_ERROR, $error); + } + + /** @test */ + public function providingInvalidShortCodeReturnsBadRequest(): void + { + $resp = $this->callApiWithKey(self::METHOD_PUT, '/short-urls/invalid/tags', [RequestOptions::JSON => [ + 'tags' => ['foo', 'bar'], + ]]); + ['error' => $error] = $this->getJsonResponsePayload($resp); + + $this->assertEquals(self::STATUS_NOT_FOUND, $resp->getStatusCode()); + $this->assertEquals(RestUtils::INVALID_SHORTCODE_ERROR, $error); + } +} diff --git a/module/Rest/test/Action/ShortUrl/ListShortUrlsActionTest.php b/module/Rest/test/Action/ShortUrl/ListShortUrlsActionTest.php index 7bb2d623..4197aba8 100644 --- a/module/Rest/test/Action/ShortUrl/ListShortUrlsActionTest.php +++ b/module/Rest/test/Action/ShortUrl/ListShortUrlsActionTest.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace ShlinkioTest\Shlink\Rest\Action\ShortUrl; -use Exception; use PHPUnit\Framework\TestCase; use Prophecy\Prophecy\ObjectProphecy; use Psr\Log\LoggerInterface; @@ -79,26 +78,4 @@ class ListShortUrlsActionTest extends TestCase 'tags' => $tags = ['one', 'two'], ], 2, null, $tags, $orderBy]; } - - /** @test */ - public function anExceptionReturnsErrorResponse(): void - { - $page = 3; - $e = new Exception(); - - $this->service->listShortUrls($page, null, [], null)->willThrow($e) - ->shouldBeCalledOnce(); - $logError = $this->logger->error( - 'Unexpected error while listing short URLs. {e}', - ['e' => $e] - )->will(function () { - }); - - $response = $this->action->handle((new ServerRequest())->withQueryParams([ - 'page' => $page, - ])); - - $this->assertEquals(500, $response->getStatusCode()); - $logError->shouldHaveBeenCalledOnce(); - } } From b3b67b051dc07489424f58e830b4863bc3be93d3 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 21 Nov 2019 19:03:34 +0100 Subject: [PATCH 08/40] Created API tests for errors when updating tags --- module/Rest/config/dependencies.config.php | 2 +- .../test-api/Action/UpdateTagActionTest.php | 45 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 module/Rest/test-api/Action/UpdateTagActionTest.php diff --git a/module/Rest/config/dependencies.config.php b/module/Rest/config/dependencies.config.php index 5ef1ecba..98a26bc1 100644 --- a/module/Rest/config/dependencies.config.php +++ b/module/Rest/config/dependencies.config.php @@ -27,9 +27,9 @@ return [ Action\ShortUrl\EditShortUrlAction::class => ConfigAbstractFactory::class, Action\ShortUrl\DeleteShortUrlAction::class => ConfigAbstractFactory::class, Action\ShortUrl\ResolveShortUrlAction::class => ConfigAbstractFactory::class, - Action\Visit\GetVisitsAction::class => ConfigAbstractFactory::class, Action\ShortUrl\ListShortUrlsAction::class => ConfigAbstractFactory::class, Action\ShortUrl\EditShortUrlTagsAction::class => ConfigAbstractFactory::class, + Action\Visit\GetVisitsAction::class => ConfigAbstractFactory::class, Action\Tag\ListTagsAction::class => ConfigAbstractFactory::class, Action\Tag\DeleteTagsAction::class => ConfigAbstractFactory::class, Action\Tag\CreateTagsAction::class => ConfigAbstractFactory::class, diff --git a/module/Rest/test-api/Action/UpdateTagActionTest.php b/module/Rest/test-api/Action/UpdateTagActionTest.php new file mode 100644 index 00000000..0f7c0400 --- /dev/null +++ b/module/Rest/test-api/Action/UpdateTagActionTest.php @@ -0,0 +1,45 @@ +callApiWithKey(self::METHOD_PUT, '/tags', [RequestOptions::JSON => $body]); + ['error' => $error] = $this->getJsonResponsePayload($resp); + + $this->assertEquals(self::STATUS_BAD_REQUEST, $resp->getStatusCode()); + $this->assertEquals(RestUtils::INVALID_ARGUMENT_ERROR, $error); + } + + public function provideInvalidBody(): iterable + { + yield [[]]; + yield [['oldName' => 'foo']]; + yield [['newName' => 'foo']]; + } + + /** @test */ + public function tryingToRenameInvalidTagReturnsNotFound(): void + { + $resp = $this->callApiWithKey(self::METHOD_PUT, '/tags', [RequestOptions::JSON => [ + 'oldName' => 'invalid_tag', + 'newName' => 'foo', + ]]); + ['error' => $error] = $this->getJsonResponsePayload($resp); + + $this->assertEquals(self::STATUS_NOT_FOUND, $resp->getStatusCode()); + $this->assertEquals(RestUtils::NOT_FOUND_ERROR, $error); + } +} From ad592a563c74ec14105b85da49381a82a1a6cd33 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 21 Nov 2019 19:22:04 +0100 Subject: [PATCH 09/40] Updated testing utils library --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 1c3ea632..f1bc1631 100644 --- a/composer.json +++ b/composer.json @@ -66,7 +66,7 @@ "phpunit/phpunit": "^8.3", "roave/security-advisories": "dev-master", "shlinkio/php-coding-standard": "~2.0.0", - "shlinkio/shlink-test-utils": "^1.0", + "shlinkio/shlink-test-utils": "^1.1", "symfony/dotenv": "^4.3", "symfony/var-dumper": "^4.3", "zendframework/zend-component-installer": "^2.1", From 6ddb60d04750643dfd397a719bf4cf4b82e7d24b Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 21 Nov 2019 20:05:06 +0100 Subject: [PATCH 10/40] Improved ValidationException to avoid polluting the message with invalid data but keeping it on the string representation --- .../src/Exception/ValidationException.php | 41 +++++++++++-------- .../Exception/ValidationExceptionTest.php | 8 ++-- .../ShortUrl/AbstractCreateShortUrlAction.php | 6 +-- .../Action/ShortUrl/CreateShortUrlAction.php | 30 ++++++-------- .../SingleStepCreateShortUrlAction.php | 13 +++--- .../SingleStepCreateShortUrlActionTest.php | 10 ++--- 6 files changed, 55 insertions(+), 53 deletions(-) diff --git a/module/Core/src/Exception/ValidationException.php b/module/Core/src/Exception/ValidationException.php index 1b767594..70dfe0d0 100644 --- a/module/Core/src/Exception/ValidationException.php +++ b/module/Core/src/Exception/ValidationException.php @@ -34,24 +34,34 @@ class ValidationException extends RuntimeException return static::fromArray($inputFilter->getMessages(), $prev); } - private static function fromArray(array $invalidData, ?Throwable $prev = null): self + public static function fromArray(array $invalidData, ?Throwable $prev = null): self { - return new self( - sprintf( - 'Provided data is not valid. These are the messages:%s%s%s', - PHP_EOL, - self::formMessagesToString($invalidData), - PHP_EOL - ), - $invalidData, - -1, - $prev + return new self('Provided data is not valid', $invalidData, -1, $prev); + } + + public function getInvalidElements(): array + { + return $this->invalidElements; + } + + public function __toString(): string + { + return sprintf( + '%s %s in %s:%s%s%sStack trace:%s%s', + __CLASS__, + $this->getMessage(), + $this->getFile(), + $this->getLine(), + $this->invalidElementsToString(), + PHP_EOL, + PHP_EOL, + $this->getTraceAsString() ); } - private static function formMessagesToString(array $messages = []): string + private function invalidElementsToString(): string { - return reduce_left($messages, function ($messageSet, $name, $_, string $acc) { + return reduce_left($this->invalidElements, function ($messageSet, string $name, $_, string $acc) { return $acc . sprintf( "\n '%s' => %s", $name, @@ -59,9 +69,4 @@ class ValidationException extends RuntimeException ); }, ''); } - - public function getInvalidElements(): array - { - return $this->invalidElements; - } } diff --git a/module/Core/test/Exception/ValidationExceptionTest.php b/module/Core/test/Exception/ValidationExceptionTest.php index 5cab422c..bd7855e2 100644 --- a/module/Core/test/Exception/ValidationExceptionTest.php +++ b/module/Core/test/Exception/ValidationExceptionTest.php @@ -55,12 +55,9 @@ class ValidationExceptionTest extends TestCase 'something' => ['baz', 'foo'], ]; $barValue = print_r(['baz', 'foo'], true); - $expectedMessage = << bar 'something' => {$barValue} - EOT; $inputFilter = $this->prophesize(InputFilterInterface::class); @@ -69,9 +66,10 @@ EOT; $e = ValidationException::fromInputFilter($inputFilter->reveal()); $this->assertEquals($invalidData, $e->getInvalidElements()); - $this->assertEquals($expectedMessage, $e->getMessage()); + $this->assertEquals('Provided data is not valid', $e->getMessage()); $this->assertEquals(-1, $e->getCode()); $this->assertEquals($prev, $e->getPrevious()); + $this->assertStringContainsString($expectedStringRepresentation, (string) $e); $getMessages->shouldHaveBeenCalledOnce(); } diff --git a/module/Rest/src/Action/ShortUrl/AbstractCreateShortUrlAction.php b/module/Rest/src/Action/ShortUrl/AbstractCreateShortUrlAction.php index f3eaee19..bc0d50ef 100644 --- a/module/Rest/src/Action/ShortUrl/AbstractCreateShortUrlAction.php +++ b/module/Rest/src/Action/ShortUrl/AbstractCreateShortUrlAction.php @@ -7,9 +7,9 @@ namespace Shlinkio\Shlink\Rest\Action\ShortUrl; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Log\LoggerInterface; -use Shlinkio\Shlink\Core\Exception\InvalidArgumentException; use Shlinkio\Shlink\Core\Exception\InvalidUrlException; use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException; +use Shlinkio\Shlink\Core\Exception\ValidationException; use Shlinkio\Shlink\Core\Model\CreateShortUrlData; use Shlinkio\Shlink\Core\Service\UrlShortenerInterface; use Shlinkio\Shlink\Core\Transformer\ShortUrlDataTransformer; @@ -44,7 +44,7 @@ abstract class AbstractCreateShortUrlAction extends AbstractRestAction { try { $shortUrlData = $this->buildShortUrlData($request); - } catch (InvalidArgumentException $e) { + } catch (ValidationException $e) { $this->logger->warning('Provided data is invalid. {e}', ['e' => $e]); return new JsonResponse([ 'error' => RestUtils::INVALID_ARGUMENT_ERROR, @@ -79,7 +79,7 @@ abstract class AbstractCreateShortUrlAction extends AbstractRestAction /** * @param Request $request * @return CreateShortUrlData - * @throws InvalidArgumentException + * @throws ValidationException */ abstract protected function buildShortUrlData(Request $request): CreateShortUrlData; } diff --git a/module/Rest/src/Action/ShortUrl/CreateShortUrlAction.php b/module/Rest/src/Action/ShortUrl/CreateShortUrlAction.php index 3b0a0b61..9c20de6c 100644 --- a/module/Rest/src/Action/ShortUrl/CreateShortUrlAction.php +++ b/module/Rest/src/Action/ShortUrl/CreateShortUrlAction.php @@ -6,7 +6,6 @@ namespace Shlinkio\Shlink\Rest\Action\ShortUrl; use Cake\Chronos\Chronos; use Psr\Http\Message\ServerRequestInterface as Request; -use Shlinkio\Shlink\Core\Exception\InvalidArgumentException; use Shlinkio\Shlink\Core\Exception\ValidationException; use Shlinkio\Shlink\Core\Model\CreateShortUrlData; use Shlinkio\Shlink\Core\Model\ShortUrlMeta; @@ -20,30 +19,27 @@ class CreateShortUrlAction extends AbstractCreateShortUrlAction /** * @param Request $request * @return CreateShortUrlData - * @throws InvalidArgumentException - * @throws \InvalidArgumentException + * @throws ValidationException */ protected function buildShortUrlData(Request $request): CreateShortUrlData { $postData = (array) $request->getParsedBody(); if (! isset($postData['longUrl'])) { - throw new InvalidArgumentException('A URL was not provided'); + throw ValidationException::fromArray([ + 'longUrl' => 'A URL was not provided', + ]); } - try { - $meta = ShortUrlMeta::createFromParams( - $this->getOptionalDate($postData, 'validSince'), - $this->getOptionalDate($postData, 'validUntil'), - $postData['customSlug'] ?? null, - $postData['maxVisits'] ?? null, - $postData['findIfExists'] ?? null, - $postData['domain'] ?? null - ); + $meta = ShortUrlMeta::createFromParams( + $this->getOptionalDate($postData, 'validSince'), + $this->getOptionalDate($postData, 'validUntil'), + $postData['customSlug'] ?? null, + $postData['maxVisits'] ?? null, + $postData['findIfExists'] ?? null, + $postData['domain'] ?? null + ); - return new CreateShortUrlData(new Uri($postData['longUrl']), (array) ($postData['tags'] ?? []), $meta); - } catch (ValidationException $e) { - throw new InvalidArgumentException('Provided meta data is not valid', -1, $e); - } + return new CreateShortUrlData(new Uri($postData['longUrl']), (array) ($postData['tags'] ?? []), $meta); } private function getOptionalDate(array $postData, string $fieldName): ?Chronos diff --git a/module/Rest/src/Action/ShortUrl/SingleStepCreateShortUrlAction.php b/module/Rest/src/Action/ShortUrl/SingleStepCreateShortUrlAction.php index 327df46e..834c6b12 100644 --- a/module/Rest/src/Action/ShortUrl/SingleStepCreateShortUrlAction.php +++ b/module/Rest/src/Action/ShortUrl/SingleStepCreateShortUrlAction.php @@ -6,7 +6,7 @@ namespace Shlinkio\Shlink\Rest\Action\ShortUrl; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Log\LoggerInterface; -use Shlinkio\Shlink\Core\Exception\InvalidArgumentException; +use Shlinkio\Shlink\Core\Exception\ValidationException; use Shlinkio\Shlink\Core\Model\CreateShortUrlData; use Shlinkio\Shlink\Core\Service\UrlShortenerInterface; use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface; @@ -33,19 +33,22 @@ class SingleStepCreateShortUrlAction extends AbstractCreateShortUrlAction /** * @param Request $request * @return CreateShortUrlData - * @throws \InvalidArgumentException - * @throws InvalidArgumentException + * @throws ValidationException */ protected function buildShortUrlData(Request $request): CreateShortUrlData { $query = $request->getQueryParams(); if (! $this->apiKeyService->check($query['apiKey'] ?? '')) { - throw new InvalidArgumentException('No API key was provided or it is not valid'); + throw ValidationException::fromArray([ + 'apiKey' => 'No API key was provided or it is not valid', + ]); } if (! isset($query['longUrl'])) { - throw new InvalidArgumentException('A URL was not provided'); + throw ValidationException::fromArray([ + 'longUrl' => 'A URL was not provided', + ]); } return new CreateShortUrlData(new Uri($query['longUrl'])); diff --git a/module/Rest/test/Action/ShortUrl/SingleStepCreateShortUrlActionTest.php b/module/Rest/test/Action/ShortUrl/SingleStepCreateShortUrlActionTest.php index f3452e9a..8bebc6c5 100644 --- a/module/Rest/test/Action/ShortUrl/SingleStepCreateShortUrlActionTest.php +++ b/module/Rest/test/Action/ShortUrl/SingleStepCreateShortUrlActionTest.php @@ -42,7 +42,7 @@ class SingleStepCreateShortUrlActionTest extends TestCase } /** @test */ - public function errorResponseIsReturnedIfInvalidApiKeyIsProvided() + public function errorResponseIsReturnedIfInvalidApiKeyIsProvided(): void { $request = (new ServerRequest())->withQueryParams(['apiKey' => 'abc123']); $findApiKey = $this->apiKeyService->check('abc123')->willReturn(false); @@ -53,12 +53,12 @@ class SingleStepCreateShortUrlActionTest extends TestCase $this->assertEquals(400, $resp->getStatusCode()); $this->assertEquals('INVALID_ARGUMENT', $payload['error']); - $this->assertEquals('No API key was provided or it is not valid', $payload['message']); + $this->assertEquals('Provided data is not valid', $payload['message']); $findApiKey->shouldHaveBeenCalled(); } /** @test */ - public function errorResponseIsReturnedIfNoUrlIsProvided() + public function errorResponseIsReturnedIfNoUrlIsProvided(): void { $request = (new ServerRequest())->withQueryParams(['apiKey' => 'abc123']); $findApiKey = $this->apiKeyService->check('abc123')->willReturn(true); @@ -69,12 +69,12 @@ class SingleStepCreateShortUrlActionTest extends TestCase $this->assertEquals(400, $resp->getStatusCode()); $this->assertEquals('INVALID_ARGUMENT', $payload['error']); - $this->assertEquals('A URL was not provided', $payload['message']); + $this->assertEquals('Provided data is not valid', $payload['message']); $findApiKey->shouldHaveBeenCalled(); } /** @test */ - public function properDataIsPassedWhenGeneratingShortCode() + public function properDataIsPassedWhenGeneratingShortCode(): void { $request = (new ServerRequest())->withQueryParams([ 'apiKey' => 'abc123', From a0510d6a691dec3d34e14b11d3ca99ae08cfe3db Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 22 Nov 2019 18:01:38 +0100 Subject: [PATCH 11/40] Removed content-based-error-handler in preparation for the problem details module --- composer.json | 1 - .../autoload/middleware-pipeline.global.php | 13 +++- config/config.php | 2 - module/Rest/config/error-handler.config.php | 21 ------- module/Rest/src/Action/HealthAction.php | 6 +- .../JsonErrorResponseGenerator.php | 44 ------------- module/Rest/test/ConfigProviderTest.php | 3 +- .../JsonErrorResponseGeneratorTest.php | 62 ------------------- 8 files changed, 16 insertions(+), 136 deletions(-) delete mode 100644 module/Rest/config/error-handler.config.php delete mode 100644 module/Rest/src/ErrorHandler/JsonErrorResponseGenerator.php delete mode 100644 module/Rest/test/ErrorHandler/JsonErrorResponseGeneratorTest.php diff --git a/composer.json b/composer.json index f1bc1631..4f4f3041 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,6 @@ "php": "^7.2", "ext-json": "*", "ext-pdo": "*", - "acelaya/ze-content-based-error-handler": "^3.0", "akrabat/ip-address-middleware": "^1.0", "cakephp/chronos": "^1.2", "cocur/slugify": "^3.0", diff --git a/config/autoload/middleware-pipeline.global.php b/config/autoload/middleware-pipeline.global.php index f9af01e3..9ea5e260 100644 --- a/config/autoload/middleware-pipeline.global.php +++ b/config/autoload/middleware-pipeline.global.php @@ -10,9 +10,20 @@ use Zend\Stratigility\Middleware\ErrorHandler; return [ 'middleware_pipeline' => [ - 'pre-routing' => [ + 'error-handler' => [ 'middleware' => [ ErrorHandler::class, + ], + 'priority' => 15, + ], +// 'error-handler-rest' => [ +// 'path' => '/rest', +// 'middleware' => [], +// 'priority' => 14, +// ], + + 'pre-routing' => [ + 'middleware' => [ Expressive\Helper\ContentLengthMiddleware::class, Common\Middleware\CloseDbConnectionMiddleware::class, ], diff --git a/config/config.php b/config/config.php index 352a27ca..2c817d1e 100644 --- a/config/config.php +++ b/config/config.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace Shlinkio\Shlink; -use Acelaya\ExpressiveErrorHandler; use Zend\ConfigAggregator; use Zend\Expressive; @@ -16,7 +15,6 @@ return (new ConfigAggregator\ConfigAggregator([ Expressive\Router\FastRouteRouter\ConfigProvider::class, Expressive\Plates\ConfigProvider::class, Expressive\Swoole\ConfigProvider::class, - ExpressiveErrorHandler\ConfigProvider::class, Common\ConfigProvider::class, IpGeolocation\ConfigProvider::class, Core\ConfigProvider::class, diff --git a/module/Rest/config/error-handler.config.php b/module/Rest/config/error-handler.config.php deleted file mode 100644 index c7503b72..00000000 --- a/module/Rest/config/error-handler.config.php +++ /dev/null @@ -1,21 +0,0 @@ - [ - 'plugins' => [ - 'invokables' => [ - 'application/json' => JsonErrorResponseGenerator::class, - ], - 'aliases' => [ - 'application/x-json' => 'application/json', - 'text/json' => 'application/json', - ], - ], - ], - -]; diff --git a/module/Rest/src/Action/HealthAction.php b/module/Rest/src/Action/HealthAction.php index 23090165..ac548fde 100644 --- a/module/Rest/src/Action/HealthAction.php +++ b/module/Rest/src/Action/HealthAction.php @@ -15,8 +15,8 @@ use Zend\Diactoros\Response\JsonResponse; class HealthAction extends AbstractRestAction { private const HEALTH_CONTENT_TYPE = 'application/health+json'; - private const PASS_STATUS = 'pass'; - private const FAIL_STATUS = 'fail'; + private const STATUS_PASS = 'pass'; + private const STATUS_FAIL = 'fail'; protected const ROUTE_PATH = '/health'; protected const ROUTE_ALLOWED_METHODS = [self::METHOD_GET]; @@ -48,7 +48,7 @@ class HealthAction extends AbstractRestAction $statusCode = $connected ? self::STATUS_OK : self::STATUS_SERVICE_UNAVAILABLE; return new JsonResponse([ - 'status' => $connected ? self::PASS_STATUS : self::FAIL_STATUS, + 'status' => $connected ? self::STATUS_PASS : self::STATUS_FAIL, 'version' => $this->options->getVersion(), 'links' => [ 'about' => 'https://shlink.io', diff --git a/module/Rest/src/ErrorHandler/JsonErrorResponseGenerator.php b/module/Rest/src/ErrorHandler/JsonErrorResponseGenerator.php deleted file mode 100644 index ca9ed12c..00000000 --- a/module/Rest/src/ErrorHandler/JsonErrorResponseGenerator.php +++ /dev/null @@ -1,44 +0,0 @@ -getStatusCode(); - $responsePhrase = $status < 400 ? 'Internal Server Error' : $response->getReasonPhrase(); - $status = $status < 400 ? self::STATUS_INTERNAL_SERVER_ERROR : $status; - - return new JsonResponse([ - 'error' => $this->responsePhraseToCode($responsePhrase), - 'message' => $responsePhrase, - ], $status); - } - - private function responsePhraseToCode(string $responsePhrase): string - { - return strtoupper(str_replace(' ', '_', $responsePhrase)); - } -} diff --git a/module/Rest/test/ConfigProviderTest.php b/module/Rest/test/ConfigProviderTest.php index 57959367..3cd574b3 100644 --- a/module/Rest/test/ConfigProviderTest.php +++ b/module/Rest/test/ConfigProviderTest.php @@ -18,11 +18,10 @@ class ConfigProviderTest extends TestCase } /** @test */ - public function properConfigIsReturned() + public function properConfigIsReturned(): void { $config = $this->configProvider->__invoke(); - $this->assertArrayHasKey('error_handler', $config); $this->assertArrayHasKey('routes', $config); $this->assertArrayHasKey('dependencies', $config); } diff --git a/module/Rest/test/ErrorHandler/JsonErrorResponseGeneratorTest.php b/module/Rest/test/ErrorHandler/JsonErrorResponseGeneratorTest.php deleted file mode 100644 index d9b99bdd..00000000 --- a/module/Rest/test/ErrorHandler/JsonErrorResponseGeneratorTest.php +++ /dev/null @@ -1,62 +0,0 @@ -errorHandler = new JsonErrorResponseGenerator(); - } - - /** @test */ - public function noErrorStatusReturnsInternalServerError(): void - { - /** @var Response\JsonResponse $response */ - $response = $this->errorHandler->__invoke(null, new ServerRequest(), new Response()); - $payload = $response->getPayload(); - - $this->assertInstanceOf(Response\JsonResponse::class, $response); - $this->assertEquals(500, $response->getStatusCode()); - $this->assertEquals('Internal Server Error', $payload['message']); - } - - /** - * @test - * @dataProvider provideStatus - */ - public function errorStatusReturnsThatStatus(int $status, string $message): void - { - /** @var Response\JsonResponse $response */ - $response = $this->errorHandler->__invoke( - null, - new ServerRequest(), - (new Response())->withStatus($status, $message) - ); - $payload = $response->getPayload(); - - $this->assertInstanceOf(Response\JsonResponse::class, $response); - $this->assertEquals($status, $response->getStatusCode()); - $this->assertEquals($message, $payload['message']); - } - - public function provideStatus(): iterable - { - return array_map(function (int $status) { - return [$status, 'Some message']; - }, range(400, 500, 20)); - } -} From 4e5ab21a47722e84c3e8fd2854c53223ef3b22c9 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 22 Nov 2019 18:03:11 +0100 Subject: [PATCH 12/40] Removed whoops dev dependency --- composer.json | 1 - config/autoload/errorhandler.local.php.dist | 29 --------------------- 2 files changed, 30 deletions(-) delete mode 100644 config/autoload/errorhandler.local.php.dist diff --git a/composer.json b/composer.json index 4f4f3041..5b222782 100644 --- a/composer.json +++ b/composer.json @@ -58,7 +58,6 @@ "require-dev": { "devster/ubench": "^2.0", "eaglewu/swoole-ide-helper": "dev-master", - "filp/whoops": "^2.4", "infection/infection": "^0.14.2", "phpstan/phpstan": "^0.11.16", "phpunit/phpcov": "^6.0", diff --git a/config/autoload/errorhandler.local.php.dist b/config/autoload/errorhandler.local.php.dist deleted file mode 100644 index 6dea98cb..00000000 --- a/config/autoload/errorhandler.local.php.dist +++ /dev/null @@ -1,29 +0,0 @@ - [ - 'invokables' => [ - 'Zend\Expressive\Whoops' => Whoops\Run::class, - 'Zend\Expressive\WhoopsPageHandler' => Whoops\Handler\PrettyPageHandler::class, - ], - ], - - 'whoops' => [ - 'json_exceptions' => [ - 'display' => true, - 'show_trace' => true, - 'ajax_only' => true, - ], - ], - - 'error_handler' => [ - 'plugins' => [ - 'factories' => [ - 'text/html' => WhoopsErrorResponseGeneratorFactory::class, - ], - ], - ], -]; From 74854b3dace92e178d35c2c811dc6d909fb98798 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 22 Nov 2019 19:49:14 +0100 Subject: [PATCH 13/40] Added zend problem details to the project --- composer.json | 3 +- config/autoload/error-handler.global.php | 26 ++++++++++++++++ .../autoload/middleware-pipeline.global.php | 31 ++++++++++++------- config/config.php | 2 ++ module/Core/src/Response/NotFoundHandler.php | 9 ------ .../test/Response/NotFoundHandlerTest.php | 24 -------------- 6 files changed, 49 insertions(+), 46 deletions(-) create mode 100644 config/autoload/error-handler.global.php diff --git a/composer.json b/composer.json index 5b222782..5d035b60 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ "phly/phly-event-dispatcher": "^1.0", "predis/predis": "^1.1", "pugx/shortid-php": "^0.5", - "shlinkio/shlink-common": "^2.2.1", + "shlinkio/shlink-common": "^2.3", "shlinkio/shlink-event-dispatcher": "^1.0", "shlinkio/shlink-installer": "^3.1", "shlinkio/shlink-ip-geolocation": "^1.1", @@ -52,6 +52,7 @@ "zendframework/zend-expressive-swoole": "^2.4", "zendframework/zend-inputfilter": "^2.10", "zendframework/zend-paginator": "^2.8", + "zendframework/zend-problem-details": "^1.0", "zendframework/zend-servicemanager": "^3.4", "zendframework/zend-stdlib": "^3.2" }, diff --git a/config/autoload/error-handler.global.php b/config/autoload/error-handler.global.php new file mode 100644 index 00000000..4ef36e0a --- /dev/null +++ b/config/autoload/error-handler.global.php @@ -0,0 +1,26 @@ + [ + 'listeners' => [Logger\ErrorLogger::class], + ], + + 'dependencies' => [ + 'delegators' => [ + ErrorHandler::class => [ + Logger\ErrorHandlerListenerAttachingDelegator::class, + ], + ProblemDetailsMiddleware::class => [ + Logger\ErrorHandlerListenerAttachingDelegator::class, + ], + ], + ], + +]; diff --git a/config/autoload/middleware-pipeline.global.php b/config/autoload/middleware-pipeline.global.php index 9ea5e260..2508b191 100644 --- a/config/autoload/middleware-pipeline.global.php +++ b/config/autoload/middleware-pipeline.global.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Shlinkio\Shlink; use Zend\Expressive; +use Zend\ProblemDetails; use Zend\Stratigility\Middleware\ErrorHandler; return [ @@ -14,20 +15,19 @@ return [ 'middleware' => [ ErrorHandler::class, ], - 'priority' => 15, ], -// 'error-handler-rest' => [ -// 'path' => '/rest', -// 'middleware' => [], -// 'priority' => 14, -// ], + 'error-handler-rest' => [ + 'path' => '/rest', + 'middleware' => [ + ProblemDetails\ProblemDetailsMiddleware::class, + ], + ], 'pre-routing' => [ 'middleware' => [ Expressive\Helper\ContentLengthMiddleware::class, Common\Middleware\CloseDbConnectionMiddleware::class, ], - 'priority' => 12, ], 'pre-routing-rest' => [ 'path' => '/rest', @@ -35,14 +35,12 @@ return [ Rest\Middleware\PathVersionMiddleware::class, Rest\Middleware\ShortUrl\ShortCodePathMiddleware::class, ], - 'priority' => 11, ], 'routing' => [ 'middleware' => [ Expressive\Router\Middleware\RouteMiddleware::class, ], - 'priority' => 10, ], 'rest' => [ @@ -53,15 +51,24 @@ return [ Rest\Middleware\BodyParserMiddleware::class, Rest\Middleware\AuthenticationMiddleware::class, ], - 'priority' => 5, ], - 'post-routing' => [ + 'dispatch' => [ 'middleware' => [ Expressive\Router\Middleware\DispatchMiddleware::class, + ], + ], + + 'not-found-rest' => [ + 'path' => '/rest', + 'middleware' => [ + ProblemDetails\ProblemDetailsNotFoundHandler::class, + ], + ], + 'not-found' => [ + 'middleware' => [ Core\Response\NotFoundHandler::class, ], - 'priority' => 1, ], ], ]; diff --git a/config/config.php b/config/config.php index 2c817d1e..ad18fd20 100644 --- a/config/config.php +++ b/config/config.php @@ -6,6 +6,7 @@ namespace Shlinkio\Shlink; use Zend\ConfigAggregator; use Zend\Expressive; +use Zend\ProblemDetails; use function Shlinkio\Shlink\Common\env; @@ -15,6 +16,7 @@ return (new ConfigAggregator\ConfigAggregator([ Expressive\Router\FastRouteRouter\ConfigProvider::class, Expressive\Plates\ConfigProvider::class, Expressive\Swoole\ConfigProvider::class, + ProblemDetails\ConfigProvider::class, Common\ConfigProvider::class, IpGeolocation\ConfigProvider::class, Core\ConfigProvider::class, diff --git a/module/Core/src/Response/NotFoundHandler.php b/module/Core/src/Response/NotFoundHandler.php index 6bf245c1..7b288b54 100644 --- a/module/Core/src/Response/NotFoundHandler.php +++ b/module/Core/src/Response/NotFoundHandler.php @@ -18,7 +18,6 @@ use Zend\Expressive\Template\TemplateRendererInterface; use function array_shift; use function explode; -use function Functional\contains; use function rtrim; class NotFoundHandler implements RequestHandlerInterface @@ -64,14 +63,6 @@ class NotFoundHandler implements RequestHandlerInterface $accept = array_shift($accepts); $status = StatusCodeInterface::STATUS_NOT_FOUND; - // If the first accepted type is json, return a json response - if (contains(['application/json', 'text/json', 'application/x-json'], $accept)) { - return new Response\JsonResponse([ - 'error' => 'NOT_FOUND', - 'message' => 'Not found', - ], $status); - } - $template = $routeResult->isFailure() ? self::NOT_FOUND_TEMPLATE : self::INVALID_SHORT_CODE_TEMPLATE; return new Response\HtmlResponse($this->renderer->render($template), $status); } diff --git a/module/Core/test/Response/NotFoundHandlerTest.php b/module/Core/test/Response/NotFoundHandlerTest.php index 8f1d6f51..50211229 100644 --- a/module/Core/test/Response/NotFoundHandlerTest.php +++ b/module/Core/test/Response/NotFoundHandlerTest.php @@ -13,7 +13,6 @@ use Shlinkio\Shlink\Core\Action\RedirectAction; use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions; use Shlinkio\Shlink\Core\Response\NotFoundHandler; use Zend\Diactoros\Response; -use Zend\Diactoros\ServerRequest; use Zend\Diactoros\ServerRequestFactory; use Zend\Diactoros\Uri; use Zend\Expressive\Router\Route; @@ -37,29 +36,6 @@ class NotFoundHandlerTest extends TestCase $this->delegate = new NotFoundHandler($this->renderer->reveal(), $this->redirectOptions, ''); } - /** - * @test - * @dataProvider provideResponses - */ - public function properResponseTypeIsReturned(string $expectedResponse, string $accept, int $renderCalls): void - { - $request = (new ServerRequest())->withHeader('Accept', $accept); - $render = $this->renderer->render(Argument::cetera())->willReturn(''); - - $resp = $this->delegate->handle($request); - - $this->assertInstanceOf($expectedResponse, $resp); - $render->shouldHaveBeenCalledTimes($renderCalls); - } - - public function provideResponses(): iterable - { - yield 'application/json' => [Response\JsonResponse::class, 'application/json', 0]; - yield 'text/json' => [Response\JsonResponse::class, 'text/json', 0]; - yield 'application/x-json' => [Response\JsonResponse::class, 'application/x-json', 0]; - yield 'text/html' => [Response\HtmlResponse::class, 'text/html', 1]; - } - /** * @test * @dataProvider provideRedirects From 89e373f775a9bd80017bc09f5bcc4fe6209e64ee Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 23 Nov 2019 10:11:34 +0100 Subject: [PATCH 14/40] Moved NotFoundHandler to ErrorHandler namespace --- config/autoload/middleware-pipeline.global.php | 2 +- module/Core/config/dependencies.config.php | 2 +- .../Core/src/{Response => ErrorHandler}/NotFoundHandler.php | 2 +- .../test/{Response => ErrorHandler}/NotFoundHandlerTest.php | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) rename module/Core/src/{Response => ErrorHandler}/NotFoundHandler.php (98%) rename module/Core/test/{Response => ErrorHandler}/NotFoundHandlerTest.php (97%) diff --git a/config/autoload/middleware-pipeline.global.php b/config/autoload/middleware-pipeline.global.php index 2508b191..d43eed5b 100644 --- a/config/autoload/middleware-pipeline.global.php +++ b/config/autoload/middleware-pipeline.global.php @@ -67,7 +67,7 @@ return [ ], 'not-found' => [ 'middleware' => [ - Core\Response\NotFoundHandler::class, + Core\ErrorHandler\NotFoundHandler::class, ], ], ], diff --git a/module/Core/config/dependencies.config.php b/module/Core/config/dependencies.config.php index 426d0969..55391fa8 100644 --- a/module/Core/config/dependencies.config.php +++ b/module/Core/config/dependencies.config.php @@ -6,8 +6,8 @@ namespace Shlinkio\Shlink\Core; use Doctrine\Common\Cache\Cache; use Psr\EventDispatcher\EventDispatcherInterface; +use Shlinkio\Shlink\Core\ErrorHandler\NotFoundHandler; use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions; -use Shlinkio\Shlink\Core\Response\NotFoundHandler; use Shlinkio\Shlink\PreviewGenerator\Service\PreviewGenerator; use Zend\Expressive\Router\RouterInterface; use Zend\Expressive\Template\TemplateRendererInterface; diff --git a/module/Core/src/Response/NotFoundHandler.php b/module/Core/src/ErrorHandler/NotFoundHandler.php similarity index 98% rename from module/Core/src/Response/NotFoundHandler.php rename to module/Core/src/ErrorHandler/NotFoundHandler.php index 7b288b54..e409c906 100644 --- a/module/Core/src/Response/NotFoundHandler.php +++ b/module/Core/src/ErrorHandler/NotFoundHandler.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Shlinkio\Shlink\Core\Response; +namespace Shlinkio\Shlink\Core\ErrorHandler; use Fig\Http\Message\StatusCodeInterface; use InvalidArgumentException; diff --git a/module/Core/test/Response/NotFoundHandlerTest.php b/module/Core/test/ErrorHandler/NotFoundHandlerTest.php similarity index 97% rename from module/Core/test/Response/NotFoundHandlerTest.php rename to module/Core/test/ErrorHandler/NotFoundHandlerTest.php index 50211229..5806e5ec 100644 --- a/module/Core/test/Response/NotFoundHandlerTest.php +++ b/module/Core/test/ErrorHandler/NotFoundHandlerTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ShlinkioTest\Shlink\Core\Response; +namespace ShlinkioTest\Shlink\Core\ErrorHandler; use PHPUnit\Framework\TestCase; use Prophecy\Argument; @@ -10,8 +10,8 @@ use Prophecy\Prophecy\ObjectProphecy; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Shlinkio\Shlink\Core\Action\RedirectAction; +use Shlinkio\Shlink\Core\ErrorHandler\NotFoundHandler; use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions; -use Shlinkio\Shlink\Core\Response\NotFoundHandler; use Zend\Diactoros\Response; use Zend\Diactoros\ServerRequestFactory; use Zend\Diactoros\Uri; From 1bafe54a756754e164cff6a207292a368bcffbb3 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 23 Nov 2019 10:25:12 +0100 Subject: [PATCH 15/40] Split NotFoundHandler into two different middlewares --- .../autoload/middleware-pipeline.global.php | 3 +- module/Core/config/dependencies.config.php | 12 ++-- ...andler.php => NotFoundRedirectHandler.php} | 41 ++----------- .../ErrorHandler/NotFoundTemplateHandler.php | 46 +++++++++++++++ ...st.php => NotFoundRedirectHandlerTest.php} | 50 +++------------- .../NotFoundTemplateHandlerTest.php | 59 +++++++++++++++++++ 6 files changed, 125 insertions(+), 86 deletions(-) rename module/Core/src/ErrorHandler/{NotFoundHandler.php => NotFoundRedirectHandler.php} (57%) create mode 100644 module/Core/src/ErrorHandler/NotFoundTemplateHandler.php rename module/Core/test/ErrorHandler/{NotFoundHandlerTest.php => NotFoundRedirectHandlerTest.php} (59%) create mode 100644 module/Core/test/ErrorHandler/NotFoundTemplateHandlerTest.php diff --git a/config/autoload/middleware-pipeline.global.php b/config/autoload/middleware-pipeline.global.php index d43eed5b..6013f56a 100644 --- a/config/autoload/middleware-pipeline.global.php +++ b/config/autoload/middleware-pipeline.global.php @@ -67,7 +67,8 @@ return [ ], 'not-found' => [ 'middleware' => [ - Core\ErrorHandler\NotFoundHandler::class, + Core\ErrorHandler\NotFoundRedirectHandler::class, + Core\ErrorHandler\NotFoundTemplateHandler::class, ], ], ], diff --git a/module/Core/config/dependencies.config.php b/module/Core/config/dependencies.config.php index 55391fa8..0ebdae7d 100644 --- a/module/Core/config/dependencies.config.php +++ b/module/Core/config/dependencies.config.php @@ -6,7 +6,7 @@ namespace Shlinkio\Shlink\Core; use Doctrine\Common\Cache\Cache; use Psr\EventDispatcher\EventDispatcherInterface; -use Shlinkio\Shlink\Core\ErrorHandler\NotFoundHandler; +use Shlinkio\Shlink\Core\ErrorHandler; use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions; use Shlinkio\Shlink\PreviewGenerator\Service\PreviewGenerator; use Zend\Expressive\Router\RouterInterface; @@ -17,7 +17,8 @@ return [ 'dependencies' => [ 'factories' => [ - NotFoundHandler::class => ConfigAbstractFactory::class, + ErrorHandler\NotFoundRedirectHandler::class => ConfigAbstractFactory::class, + ErrorHandler\NotFoundTemplateHandler::class => ConfigAbstractFactory::class, Options\AppOptions::class => ConfigAbstractFactory::class, Options\DeleteShortUrlsOptions::class => ConfigAbstractFactory::class, @@ -43,11 +44,8 @@ return [ ], ConfigAbstractFactory::class => [ - NotFoundHandler::class => [ - TemplateRendererInterface::class, - NotFoundRedirectOptions::class, - 'config.router.base_path', - ], + ErrorHandler\NotFoundRedirectHandler::class => [NotFoundRedirectOptions::class, 'config.router.base_path'], + ErrorHandler\NotFoundTemplateHandler::class => [TemplateRendererInterface::class], Options\AppOptions::class => ['config.app_options'], Options\DeleteShortUrlsOptions::class => ['config.delete_short_urls'], diff --git a/module/Core/src/ErrorHandler/NotFoundHandler.php b/module/Core/src/ErrorHandler/NotFoundRedirectHandler.php similarity index 57% rename from module/Core/src/ErrorHandler/NotFoundHandler.php rename to module/Core/src/ErrorHandler/NotFoundRedirectHandler.php index e409c906..f6a03395 100644 --- a/module/Core/src/ErrorHandler/NotFoundHandler.php +++ b/module/Core/src/ErrorHandler/NotFoundRedirectHandler.php @@ -4,67 +4,38 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\ErrorHandler; -use Fig\Http\Message\StatusCodeInterface; -use InvalidArgumentException; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\UriInterface; +use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Shlinkio\Shlink\Core\Action\RedirectAction; use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions; use Zend\Diactoros\Response; use Zend\Expressive\Router\RouteResult; -use Zend\Expressive\Template\TemplateRendererInterface; -use function array_shift; -use function explode; use function rtrim; -class NotFoundHandler implements RequestHandlerInterface +class NotFoundRedirectHandler implements MiddlewareInterface { - public const NOT_FOUND_TEMPLATE = 'ShlinkCore::error/404'; - public const INVALID_SHORT_CODE_TEMPLATE = 'ShlinkCore::invalid-short-code'; - - /** @var TemplateRendererInterface */ - private $renderer; /** @var NotFoundRedirectOptions */ private $redirectOptions; /** @var string */ private $shlinkBasePath; - public function __construct( - TemplateRendererInterface $renderer, - NotFoundRedirectOptions $redirectOptions, - string $shlinkBasePath - ) { - $this->renderer = $renderer; + public function __construct(NotFoundRedirectOptions $redirectOptions, string $shlinkBasePath) + { $this->redirectOptions = $redirectOptions; $this->shlinkBasePath = $shlinkBasePath; } - /** - * Dispatch the next available middleware and return the response. - * - * @param ServerRequestInterface $request - * - * @return ResponseInterface - * @throws InvalidArgumentException - */ - public function handle(ServerRequestInterface $request): ResponseInterface + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { /** @var RouteResult $routeResult */ $routeResult = $request->getAttribute(RouteResult::class, RouteResult::fromRouteFailure(null)); $redirectResponse = $this->createRedirectResponse($routeResult, $request->getUri()); - if ($redirectResponse !== null) { - return $redirectResponse; - } - $accepts = explode(',', $request->getHeaderLine('Accept')); - $accept = array_shift($accepts); - $status = StatusCodeInterface::STATUS_NOT_FOUND; - - $template = $routeResult->isFailure() ? self::NOT_FOUND_TEMPLATE : self::INVALID_SHORT_CODE_TEMPLATE; - return new Response\HtmlResponse($this->renderer->render($template), $status); + return $redirectResponse ?? $handler->handle($request); } private function createRedirectResponse(RouteResult $routeResult, UriInterface $uri): ?ResponseInterface diff --git a/module/Core/src/ErrorHandler/NotFoundTemplateHandler.php b/module/Core/src/ErrorHandler/NotFoundTemplateHandler.php new file mode 100644 index 00000000..7b84043d --- /dev/null +++ b/module/Core/src/ErrorHandler/NotFoundTemplateHandler.php @@ -0,0 +1,46 @@ +renderer = $renderer; + } + + /** + * Dispatch the next available middleware and return the response. + * + * @param ServerRequestInterface $request + * + * @return ResponseInterface + * @throws InvalidArgumentException + */ + public function handle(ServerRequestInterface $request): ResponseInterface + { + /** @var RouteResult $routeResult */ + $routeResult = $request->getAttribute(RouteResult::class, RouteResult::fromRouteFailure(null)); + $status = StatusCodeInterface::STATUS_NOT_FOUND; + + $template = $routeResult->isFailure() ? self::NOT_FOUND_TEMPLATE : self::INVALID_SHORT_CODE_TEMPLATE; + return new Response\HtmlResponse($this->renderer->render($template), $status); + } +} diff --git a/module/Core/test/ErrorHandler/NotFoundHandlerTest.php b/module/Core/test/ErrorHandler/NotFoundRedirectHandlerTest.php similarity index 59% rename from module/Core/test/ErrorHandler/NotFoundHandlerTest.php rename to module/Core/test/ErrorHandler/NotFoundRedirectHandlerTest.php index 5806e5ec..d2776179 100644 --- a/module/Core/test/ErrorHandler/NotFoundHandlerTest.php +++ b/module/Core/test/ErrorHandler/NotFoundRedirectHandlerTest.php @@ -5,35 +5,29 @@ declare(strict_types=1); namespace ShlinkioTest\Shlink\Core\ErrorHandler; use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\Prophecy\ObjectProphecy; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Server\RequestHandlerInterface; use Shlinkio\Shlink\Core\Action\RedirectAction; -use Shlinkio\Shlink\Core\ErrorHandler\NotFoundHandler; +use Shlinkio\Shlink\Core\ErrorHandler\NotFoundRedirectHandler; use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions; use Zend\Diactoros\Response; use Zend\Diactoros\ServerRequestFactory; use Zend\Diactoros\Uri; use Zend\Expressive\Router\Route; use Zend\Expressive\Router\RouteResult; -use Zend\Expressive\Template\TemplateRendererInterface; -class NotFoundHandlerTest extends TestCase +class NotFoundRedirectHandlerTest extends TestCase { - /** @var NotFoundHandler */ - private $delegate; - /** @var ObjectProphecy */ - private $renderer; + /** @var NotFoundRedirectHandler */ + private $middleware; /** @var NotFoundRedirectOptions */ private $redirectOptions; public function setUp(): void { - $this->renderer = $this->prophesize(TemplateRendererInterface::class); $this->redirectOptions = new NotFoundRedirectOptions(); - - $this->delegate = new NotFoundHandler($this->renderer->reveal(), $this->redirectOptions, ''); + $this->middleware = new NotFoundRedirectHandler($this->redirectOptions, ''); } /** @@ -48,11 +42,10 @@ class NotFoundHandlerTest extends TestCase $this->redirectOptions->regular404 = 'regular404'; $this->redirectOptions->baseUrl = 'baseUrl'; - $resp = $this->delegate->handle($request); + $resp = $this->middleware->process($request, $this->prophesize(RequestHandlerInterface::class)->reveal()); $this->assertInstanceOf(Response\RedirectResponse::class, $resp); $this->assertEquals($expectedRedirectTo, $resp->getHeaderLine('Location')); - $this->renderer->render(Argument::cetera())->shouldNotHaveBeenCalled(); } public function provideRedirects(): iterable @@ -86,33 +79,4 @@ class NotFoundHandlerTest extends TestCase 'invalidShortUrl', ]; } - - /** - * @test - * @dataProvider provideTemplates - */ - public function properErrorTemplateIsRendered(ServerRequestInterface $request, string $expectedTemplate): void - { - $request = $request->withHeader('Accept', 'text/html'); - $render = $this->renderer->render($expectedTemplate)->willReturn(''); - - $resp = $this->delegate->handle($request); - - $this->assertInstanceOf(Response\HtmlResponse::class, $resp); - $render->shouldHaveBeenCalledOnce(); - } - - public function provideTemplates(): iterable - { - $request = ServerRequestFactory::fromGlobals(); - - yield [$request, NotFoundHandler::NOT_FOUND_TEMPLATE]; - yield [ - $request->withAttribute( - RouteResult::class, - RouteResult::fromRoute(new Route('', $this->prophesize(MiddlewareInterface::class)->reveal())) - ), - NotFoundHandler::INVALID_SHORT_CODE_TEMPLATE, - ]; - } } diff --git a/module/Core/test/ErrorHandler/NotFoundTemplateHandlerTest.php b/module/Core/test/ErrorHandler/NotFoundTemplateHandlerTest.php new file mode 100644 index 00000000..7d763448 --- /dev/null +++ b/module/Core/test/ErrorHandler/NotFoundTemplateHandlerTest.php @@ -0,0 +1,59 @@ +renderer = $this->prophesize(TemplateRendererInterface::class); + $this->handler = new NotFoundTemplateHandler($this->renderer->reveal()); + } + + /** + * @test + * @dataProvider provideTemplates + */ + public function properErrorTemplateIsRendered(ServerRequestInterface $request, string $expectedTemplate): void + { + $request = $request->withHeader('Accept', 'text/html'); + $render = $this->renderer->render($expectedTemplate)->willReturn(''); + + $resp = $this->handler->handle($request); + + $this->assertInstanceOf(Response\HtmlResponse::class, $resp); + $render->shouldHaveBeenCalledOnce(); + } + + public function provideTemplates(): iterable + { + $request = ServerRequestFactory::fromGlobals(); + + yield [$request, NotFoundTemplateHandler::NOT_FOUND_TEMPLATE]; + yield [ + $request->withAttribute( + RouteResult::class, + RouteResult::fromRoute(new Route('', $this->prophesize(MiddlewareInterface::class)->reveal())) + ), + NotFoundTemplateHandler::INVALID_SHORT_CODE_TEMPLATE, + ]; + } +} From 850259290aef8de687220be9dc0663a8a1d16cd0 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 23 Nov 2019 10:28:58 +0100 Subject: [PATCH 16/40] Covered new use case on NotFoundRedirectHandlerTest --- .../NotFoundRedirectHandlerTest.php | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/module/Core/test/ErrorHandler/NotFoundRedirectHandlerTest.php b/module/Core/test/ErrorHandler/NotFoundRedirectHandlerTest.php index d2776179..b7796e61 100644 --- a/module/Core/test/ErrorHandler/NotFoundRedirectHandlerTest.php +++ b/module/Core/test/ErrorHandler/NotFoundRedirectHandlerTest.php @@ -42,10 +42,14 @@ class NotFoundRedirectHandlerTest extends TestCase $this->redirectOptions->regular404 = 'regular404'; $this->redirectOptions->baseUrl = 'baseUrl'; - $resp = $this->middleware->process($request, $this->prophesize(RequestHandlerInterface::class)->reveal()); + $next = $this->prophesize(RequestHandlerInterface::class); + $handle = $next->handle($request)->willReturn(new Response()); + + $resp = $this->middleware->process($request, $next->reveal()); $this->assertInstanceOf(Response\RedirectResponse::class, $resp); $this->assertEquals($expectedRedirectTo, $resp->getHeaderLine('Location')); + $handle->shouldNotHaveBeenCalled(); } public function provideRedirects(): iterable @@ -79,4 +83,19 @@ class NotFoundRedirectHandlerTest extends TestCase 'invalidShortUrl', ]; } + + /** @test */ + public function nextMiddlewareIsInvokedWhenNotRedirectNeedsToOccur(): void + { + $req = ServerRequestFactory::fromGlobals(); + $resp = new Response(); + + $next = $this->prophesize(RequestHandlerInterface::class); + $handle = $next->handle($req)->willReturn($resp); + + $result = $this->middleware->process($req, $next->reveal()); + + $this->assertSame($resp, $result); + $handle->shouldHaveBeenCalledOnce(); + } } From 09321eaa93a3fae6f30f01ce1606268e9f4027b8 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 23 Nov 2019 13:41:07 +0100 Subject: [PATCH 17/40] Updated InvalidShortCodeException to implement ProblemDetails --- .../Exception/InvalidShortCodeException.php | 31 ++++++++++++------- .../InvalidShortCodeExceptionTest.php | 23 +------------- .../Action/ShortUrl/DeleteShortUrlAction.php | 9 ------ module/Rest/src/Util/RestUtils.php | 4 ++- .../ShortUrl/DeleteShortUrlActionTest.php | 1 - 5 files changed, 23 insertions(+), 45 deletions(-) diff --git a/module/Core/src/Exception/InvalidShortCodeException.php b/module/Core/src/Exception/InvalidShortCodeException.php index 37ecfffb..77240aca 100644 --- a/module/Core/src/Exception/InvalidShortCodeException.php +++ b/module/Core/src/Exception/InvalidShortCodeException.php @@ -4,24 +4,31 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Exception; -use Throwable; +use Fig\Http\Message\StatusCodeInterface; +use Zend\ProblemDetails\Exception\CommonProblemDetailsExceptionTrait; +use Zend\ProblemDetails\Exception\ProblemDetailsExceptionInterface; use function sprintf; -class InvalidShortCodeException extends RuntimeException +class InvalidShortCodeException extends RuntimeException implements ProblemDetailsExceptionInterface { - public static function fromCharset(string $shortCode, string $charSet, ?Throwable $previous = null): self - { - $code = $previous !== null ? $previous->getCode() : -1; - return new static( - sprintf('Provided short code "%s" does not match the char set "%s"', $shortCode, $charSet), - $code, - $previous - ); - } + use CommonProblemDetailsExceptionTrait; + + public const TITLE = 'Invalid short code'; + public const TYPE = 'INVALID_SHORTCODE'; public static function fromNotFoundShortCode(string $shortCode): self { - return new static(sprintf('Provided short code "%s" does not belong to a short URL', $shortCode)); + $e = new self(sprintf('No URL found for short code "%s"', $shortCode)); + $e->detail = $e->getMessage(); + $e->title = self::TITLE; + $e->type = self::TYPE; + $e->status = StatusCodeInterface::STATUS_NOT_FOUND; + $e->additional = [ + 'error' => $e->type, + 'message' => $e->detail, + ]; + + return $e; } } diff --git a/module/Core/test/Exception/InvalidShortCodeExceptionTest.php b/module/Core/test/Exception/InvalidShortCodeExceptionTest.php index 02a3edf2..4639fda3 100644 --- a/module/Core/test/Exception/InvalidShortCodeExceptionTest.php +++ b/module/Core/test/Exception/InvalidShortCodeExceptionTest.php @@ -4,37 +4,16 @@ declare(strict_types=1); namespace ShlinkioTest\Shlink\Core\Exception; -use Exception; use PHPUnit\Framework\TestCase; use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; -use Throwable; class InvalidShortCodeExceptionTest extends TestCase { - /** - * @test - * @dataProvider providePrevious - */ - public function properlyCreatesExceptionFromCharset(?Throwable $prev): void - { - $e = InvalidShortCodeException::fromCharset('abc123', 'def456', $prev); - - $this->assertEquals('Provided short code "abc123" does not match the char set "def456"', $e->getMessage()); - $this->assertEquals($prev !== null ? $prev->getCode() : -1, $e->getCode()); - $this->assertEquals($prev, $e->getPrevious()); - } - - public function providePrevious(): iterable - { - yield 'null previous' => [null]; - yield 'instance previous' => [new Exception('Previous error', 10)]; - } - /** @test */ public function properlyCreatesExceptionFromNotFoundShortCode(): void { $e = InvalidShortCodeException::fromNotFoundShortCode('abc123'); - $this->assertEquals('Provided short code "abc123" does not belong to a short URL', $e->getMessage()); + $this->assertEquals('No URL found for short code "abc123"', $e->getMessage()); } } diff --git a/module/Rest/src/Action/ShortUrl/DeleteShortUrlAction.php b/module/Rest/src/Action/ShortUrl/DeleteShortUrlAction.php index 54755baa..f6873025 100644 --- a/module/Rest/src/Action/ShortUrl/DeleteShortUrlAction.php +++ b/module/Rest/src/Action/ShortUrl/DeleteShortUrlAction.php @@ -40,15 +40,6 @@ class DeleteShortUrlAction extends AbstractRestAction try { $this->deleteShortUrlService->deleteByShortCode($shortCode); return new EmptyResponse(); - } catch (Exception\InvalidShortCodeException $e) { - $this->logger->warning( - 'Provided short code {shortCode} does not belong to any URL. {e}', - ['e' => $e, 'shortCode' => $shortCode] - ); - return new JsonResponse([ - 'error' => RestUtils::getRestErrorCodeFromException($e), - 'message' => sprintf('No URL found for short code "%s"', $shortCode), - ], self::STATUS_NOT_FOUND); } catch (Exception\DeleteShortUrlException $e) { $this->logger->warning('Provided data is invalid. {e}', ['e' => $e]); $messagePlaceholder = diff --git a/module/Rest/src/Util/RestUtils.php b/module/Rest/src/Util/RestUtils.php index 60aade54..71c65e88 100644 --- a/module/Rest/src/Util/RestUtils.php +++ b/module/Rest/src/Util/RestUtils.php @@ -6,12 +6,14 @@ namespace Shlinkio\Shlink\Rest\Util; use Shlinkio\Shlink\Common\Exception as Common; use Shlinkio\Shlink\Core\Exception as Core; +use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; use Shlinkio\Shlink\Rest\Exception as Rest; use Throwable; class RestUtils { - public const INVALID_SHORTCODE_ERROR = 'INVALID_SHORTCODE'; + /** @deprecated */ + public const INVALID_SHORTCODE_ERROR = InvalidShortCodeException::TYPE; // FIXME Should be INVALID_SHORT_URL_DELETION public const INVALID_SHORTCODE_DELETION_ERROR = 'INVALID_SHORTCODE_DELETION'; public const INVALID_URL_ERROR = 'INVALID_URL'; diff --git a/module/Rest/test/Action/ShortUrl/DeleteShortUrlActionTest.php b/module/Rest/test/Action/ShortUrl/DeleteShortUrlActionTest.php index 35f87dc8..2e77d10a 100644 --- a/module/Rest/test/Action/ShortUrl/DeleteShortUrlActionTest.php +++ b/module/Rest/test/Action/ShortUrl/DeleteShortUrlActionTest.php @@ -59,7 +59,6 @@ class DeleteShortUrlActionTest extends TestCase public function provideExceptions(): iterable { - yield 'not found' => [new Exception\InvalidShortCodeException(), RestUtils::INVALID_SHORTCODE_ERROR, 404]; yield 'bad request' => [ new Exception\DeleteShortUrlException(5), RestUtils::INVALID_SHORTCODE_DELETION_ERROR, From 6f0afe269d63dfdcc9dd410171ed1537ebe8c6b6 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 24 Nov 2019 12:41:12 +0100 Subject: [PATCH 18/40] Moved InvalidShortCode exception handling to problem details --- config/autoload/error-handler.global.php | 8 ++ .../autoload/middleware-pipeline.global.php | 3 +- .../Exception/InvalidShortCodeException.php | 7 +- module/Rest/config/dependencies.config.php | 6 ++ .../Action/ShortUrl/EditShortUrlAction.php | 8 -- .../ShortUrl/EditShortUrlTagsAction.php | 14 +-- .../Rest/src/Action/Visit/GetVisitsAction.php | 5 -- module/Rest/src/ConfigProvider.php | 2 +- ...ardsCompatibleProblemDetailsMiddleware.php | 87 +++++++++++++++++++ .../src/Middleware/PathVersionMiddleware.php | 13 +-- .../ShortUrl/EditShortUrlActionTest.php | 26 +----- .../ShortUrl/EditShortUrlTagsActionTest.php | 19 +--- 12 files changed, 113 insertions(+), 85 deletions(-) create mode 100644 module/Rest/src/Middleware/BackwardsCompatibleProblemDetailsMiddleware.php diff --git a/config/autoload/error-handler.global.php b/config/autoload/error-handler.global.php index 4ef36e0a..964b90de 100644 --- a/config/autoload/error-handler.global.php +++ b/config/autoload/error-handler.global.php @@ -8,6 +8,14 @@ use Zend\Stratigility\Middleware\ErrorHandler; return [ + 'backwards_compatible_problem_details' => [ + 'default_type_fallbacks' => [ + 404 => 'NOT_FOUND', + 500 => 'INTERNAL_SERVER_ERROR', + ], + 'json_flags' => JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION, + ], + 'error_handler' => [ 'listeners' => [Logger\ErrorLogger::class], ], diff --git a/config/autoload/middleware-pipeline.global.php b/config/autoload/middleware-pipeline.global.php index 6013f56a..f3370a7a 100644 --- a/config/autoload/middleware-pipeline.global.php +++ b/config/autoload/middleware-pipeline.global.php @@ -13,19 +13,20 @@ return [ 'middleware_pipeline' => [ 'error-handler' => [ 'middleware' => [ + Expressive\Helper\ContentLengthMiddleware::class, ErrorHandler::class, ], ], 'error-handler-rest' => [ 'path' => '/rest', 'middleware' => [ + Rest\Middleware\BackwardsCompatibleProblemDetailsMiddleware::class, ProblemDetails\ProblemDetailsMiddleware::class, ], ], 'pre-routing' => [ 'middleware' => [ - Expressive\Helper\ContentLengthMiddleware::class, Common\Middleware\CloseDbConnectionMiddleware::class, ], ], diff --git a/module/Core/src/Exception/InvalidShortCodeException.php b/module/Core/src/Exception/InvalidShortCodeException.php index 77240aca..d75cdad7 100644 --- a/module/Core/src/Exception/InvalidShortCodeException.php +++ b/module/Core/src/Exception/InvalidShortCodeException.php @@ -14,20 +14,17 @@ class InvalidShortCodeException extends RuntimeException implements ProblemDetai { use CommonProblemDetailsExceptionTrait; - public const TITLE = 'Invalid short code'; + private const TITLE = 'Invalid short code'; public const TYPE = 'INVALID_SHORTCODE'; public static function fromNotFoundShortCode(string $shortCode): self { $e = new self(sprintf('No URL found for short code "%s"', $shortCode)); + $e->detail = $e->getMessage(); $e->title = self::TITLE; $e->type = self::TYPE; $e->status = StatusCodeInterface::STATUS_NOT_FOUND; - $e->additional = [ - 'error' => $e->type, - 'message' => $e->detail, - ]; return $e; } diff --git a/module/Rest/config/dependencies.config.php b/module/Rest/config/dependencies.config.php index 98a26bc1..17c30642 100644 --- a/module/Rest/config/dependencies.config.php +++ b/module/Rest/config/dependencies.config.php @@ -39,6 +39,7 @@ return [ Middleware\BodyParserMiddleware::class => InvokableFactory::class, Middleware\CrossDomainMiddleware::class => InvokableFactory::class, Middleware\PathVersionMiddleware::class => InvokableFactory::class, + Middleware\BackwardsCompatibleProblemDetailsMiddleware::class => ConfigAbstractFactory::class, Middleware\ShortUrl\CreateShortUrlContentNegotiationMiddleware::class => InvokableFactory::class, Middleware\ShortUrl\ShortCodePathMiddleware::class => InvokableFactory::class, ], @@ -75,6 +76,11 @@ return [ Action\Tag\DeleteTagsAction::class => [Service\Tag\TagService::class, LoggerInterface::class], Action\Tag\CreateTagsAction::class => [Service\Tag\TagService::class, LoggerInterface::class], Action\Tag\UpdateTagAction::class => [Service\Tag\TagService::class, LoggerInterface::class], + + Middleware\BackwardsCompatibleProblemDetailsMiddleware::class => [ + 'config.backwards_compatible_problem_details.default_type_fallbacks', + 'config.backwards_compatible_problem_details.json_flags', + ], ], ]; diff --git a/module/Rest/src/Action/ShortUrl/EditShortUrlAction.php b/module/Rest/src/Action/ShortUrl/EditShortUrlAction.php index fe609e03..8391db8a 100644 --- a/module/Rest/src/Action/ShortUrl/EditShortUrlAction.php +++ b/module/Rest/src/Action/ShortUrl/EditShortUrlAction.php @@ -15,8 +15,6 @@ use Shlinkio\Shlink\Rest\Util\RestUtils; use Zend\Diactoros\Response\EmptyResponse; use Zend\Diactoros\Response\JsonResponse; -use function sprintf; - class EditShortUrlAction extends AbstractRestAction { protected const ROUTE_PATH = '/short-urls/{shortCode}'; @@ -51,12 +49,6 @@ class EditShortUrlAction extends AbstractRestAction ShortUrlMeta::createFromRawData($postData) ); return new EmptyResponse(); - } catch (Exception\InvalidShortCodeException $e) { - $this->logger->warning('Provided data is invalid. {e}', ['e' => $e]); - return new JsonResponse([ - 'error' => RestUtils::getRestErrorCodeFromException($e), - 'message' => sprintf('No URL found for short code "%s"', $shortCode), - ], self::STATUS_NOT_FOUND); } catch (Exception\ValidationException $e) { $this->logger->warning('Provided data is invalid. {e}', ['e' => $e]); return new JsonResponse([ diff --git a/module/Rest/src/Action/ShortUrl/EditShortUrlTagsAction.php b/module/Rest/src/Action/ShortUrl/EditShortUrlTagsAction.php index 4daa0fac..9d82c608 100644 --- a/module/Rest/src/Action/ShortUrl/EditShortUrlTagsAction.php +++ b/module/Rest/src/Action/ShortUrl/EditShortUrlTagsAction.php @@ -7,14 +7,11 @@ namespace Shlinkio\Shlink\Rest\Action\ShortUrl; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Log\LoggerInterface; -use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface; use Shlinkio\Shlink\Rest\Action\AbstractRestAction; use Shlinkio\Shlink\Rest\Util\RestUtils; use Zend\Diactoros\Response\JsonResponse; -use function sprintf; - class EditShortUrlTagsAction extends AbstractRestAction { protected const ROUTE_PATH = '/short-urls/{shortCode}/tags'; @@ -47,14 +44,7 @@ class EditShortUrlTagsAction extends AbstractRestAction } $tags = $bodyParams['tags']; - try { - $shortUrl = $this->shortUrlService->setTagsByShortCode($shortCode, $tags); - return new JsonResponse(['tags' => $shortUrl->getTags()->toArray()]); - } catch (InvalidShortCodeException $e) { - return new JsonResponse([ - 'error' => RestUtils::getRestErrorCodeFromException($e), - 'message' => sprintf('No URL found for short code "%s"', $shortCode), - ], self::STATUS_NOT_FOUND); - } + $shortUrl = $this->shortUrlService->setTagsByShortCode($shortCode, $tags); + return new JsonResponse(['tags' => $shortUrl->getTags()->toArray()]); } } diff --git a/module/Rest/src/Action/Visit/GetVisitsAction.php b/module/Rest/src/Action/Visit/GetVisitsAction.php index 68912be2..ed614ae7 100644 --- a/module/Rest/src/Action/Visit/GetVisitsAction.php +++ b/module/Rest/src/Action/Visit/GetVisitsAction.php @@ -33,11 +33,6 @@ class GetVisitsAction extends AbstractRestAction $this->visitsTracker = $visitsTracker; } - /** - * @param Request $request - * @return Response - * @throws \InvalidArgumentException - */ public function handle(Request $request): Response { $shortCode = $request->getAttribute('shortCode'); diff --git a/module/Rest/src/ConfigProvider.php b/module/Rest/src/ConfigProvider.php index 11e6f811..d942cf51 100644 --- a/module/Rest/src/ConfigProvider.php +++ b/module/Rest/src/ConfigProvider.php @@ -11,7 +11,7 @@ use function sprintf; class ConfigProvider { - private const ROUTES_PREFIX = '/rest/v{version:1}'; + private const ROUTES_PREFIX = '/rest/v{version:1|2}'; public function __invoke() { diff --git a/module/Rest/src/Middleware/BackwardsCompatibleProblemDetailsMiddleware.php b/module/Rest/src/Middleware/BackwardsCompatibleProblemDetailsMiddleware.php new file mode 100644 index 00000000..9857465b --- /dev/null +++ b/module/Rest/src/Middleware/BackwardsCompatibleProblemDetailsMiddleware.php @@ -0,0 +1,87 @@ + 'type', + 'message' => 'detail', + ]; + + /** @var array */ + private $defaultTypeFallbacks; + /** @var int */ + private $jsonFlags; + + public function __construct(array $defaultTypeFallbacks, int $jsonFlags) + { + $this->defaultTypeFallbacks = $defaultTypeFallbacks; + $this->jsonFlags = $jsonFlags; + } + + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + $resp = $handler->handle($request); + + if ($resp->getHeaderLine('Content-type') !== 'application/problem+json') { + return $resp; + } + + try { + $body = (string) $resp->getBody(); + $payload = json_decode($body); + } catch (Throwable $e) { + return $resp; + } + + $payload = $this->mapStandardErrorTypes($payload, $resp->getStatusCode()); + + if ($this->isVersionOne($request)) { + $payload = $this->makePayloadBackwardsCompatible($payload); + } + + return new JsonResponse($payload, $resp->getStatusCode(), $resp->getHeaders(), $this->jsonFlags); + } + + private function mapStandardErrorTypes(array $payload, int $respStatusCode): array + { + $type = $payload['type'] ?? ''; + if (strpos($type, 'https://httpstatus.es') === 0) { + $payload['type'] = $this->defaultTypeFallbacks[$respStatusCode] ?? $type; + } + + return $payload; + } + + /** @deprecated When Shlink 2 is released, do not chekc the version */ + private function isVersionOne(ServerRequestInterface $request): bool + { + $uri = $request->getUri(); + $path = $uri->getPath(); + + return strpos($path, '/v') === false || strpos($path, '/v1') === 0; + } + + /** @deprecated When Shlink v2 is released, do not map old fields */ + private function makePayloadBackwardsCompatible(array $payload): array + { + return reduce_left(self::BACKWARDS_COMPATIBLE_FIELDS, function (string $newKey, string $oldKey, $c, $acc) { + $acc[$oldKey] = $acc[$newKey]; + return $acc; + }, $payload); + } +} diff --git a/module/Rest/src/Middleware/PathVersionMiddleware.php b/module/Rest/src/Middleware/PathVersionMiddleware.php index c08c8dbb..84921710 100644 --- a/module/Rest/src/Middleware/PathVersionMiddleware.php +++ b/module/Rest/src/Middleware/PathVersionMiddleware.php @@ -15,23 +15,12 @@ class PathVersionMiddleware implements MiddlewareInterface { // TODO The /health endpoint needs this middleware in order to work without the version. // Take it into account if this middleware is ever removed. - - /** - * Process an incoming server request and return a response, optionally delegating - * to the next middleware component to create the response. - * - * @param Request $request - * @param RequestHandlerInterface $handler - * - * @return Response - * @throws \InvalidArgumentException - */ public function process(Request $request, RequestHandlerInterface $handler): Response { $uri = $request->getUri(); $path = $uri->getPath(); - // If the path does not begin with the version number, prepend v1 by default for BC compatibility purposes + // If the path does not begin with the version number, prepend v1 by default for BC purposes if (strpos($path, '/v') !== 0) { $request = $request->withUri($uri->withPath('/v1' . $uri->getPath())); } diff --git a/module/Rest/test/Action/ShortUrl/EditShortUrlActionTest.php b/module/Rest/test/Action/ShortUrl/EditShortUrlActionTest.php index d8e62c20..55fcc2d7 100644 --- a/module/Rest/test/Action/ShortUrl/EditShortUrlActionTest.php +++ b/module/Rest/test/Action/ShortUrl/EditShortUrlActionTest.php @@ -8,7 +8,6 @@ use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\Prophecy\ObjectProphecy; use Shlinkio\Shlink\Core\Entity\ShortUrl; -use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface; use Shlinkio\Shlink\Rest\Action\ShortUrl\EditShortUrlAction; use Shlinkio\Shlink\Rest\Util\RestUtils; @@ -29,7 +28,7 @@ class EditShortUrlActionTest extends TestCase } /** @test */ - public function invalidDataReturnsError() + public function invalidDataReturnsError(): void { $request = (new ServerRequest())->withParsedBody([ 'maxVisits' => 'invalid', @@ -45,28 +44,7 @@ class EditShortUrlActionTest extends TestCase } /** @test */ - public function incorrectShortCodeReturnsError() - { - $request = (new ServerRequest())->withAttribute('shortCode', 'abc123') - ->withParsedBody([ - 'maxVisits' => 5, - ]); - $updateMeta = $this->shortUrlService->updateMetadataByShortCode(Argument::cetera())->willThrow( - InvalidShortCodeException::class - ); - - /** @var JsonResponse $resp */ - $resp = $this->action->handle($request); - $payload = $resp->getPayload(); - - $this->assertEquals(404, $resp->getStatusCode()); - $this->assertEquals(RestUtils::INVALID_SHORTCODE_ERROR, $payload['error']); - $this->assertEquals('No URL found for short code "abc123"', $payload['message']); - $updateMeta->shouldHaveBeenCalled(); - } - - /** @test */ - public function correctShortCodeReturnsSuccess() + public function correctShortCodeReturnsSuccess(): void { $request = (new ServerRequest())->withAttribute('shortCode', 'abc123') ->withParsedBody([ diff --git a/module/Rest/test/Action/ShortUrl/EditShortUrlTagsActionTest.php b/module/Rest/test/Action/ShortUrl/EditShortUrlTagsActionTest.php index d56bbaf1..5c0ec628 100644 --- a/module/Rest/test/Action/ShortUrl/EditShortUrlTagsActionTest.php +++ b/module/Rest/test/Action/ShortUrl/EditShortUrlTagsActionTest.php @@ -7,7 +7,6 @@ namespace ShlinkioTest\Shlink\Rest\Action\ShortUrl; use PHPUnit\Framework\TestCase; use Prophecy\Prophecy\ObjectProphecy; use Shlinkio\Shlink\Core\Entity\ShortUrl; -use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; use Shlinkio\Shlink\Core\Service\ShortUrlService; use Shlinkio\Shlink\Rest\Action\ShortUrl\EditShortUrlTagsAction; use Zend\Diactoros\ServerRequest; @@ -26,28 +25,14 @@ class EditShortUrlTagsActionTest extends TestCase } /** @test */ - public function notProvidingTagsReturnsError() + public function notProvidingTagsReturnsError(): void { $response = $this->action->handle((new ServerRequest())->withAttribute('shortCode', 'abc123')); $this->assertEquals(400, $response->getStatusCode()); } /** @test */ - public function anInvalidShortCodeReturnsNotFound() - { - $shortCode = 'abc123'; - $this->shortUrlService->setTagsByShortCode($shortCode, [])->willThrow(InvalidShortCodeException::class) - ->shouldBeCalledOnce(); - - $response = $this->action->handle( - (new ServerRequest())->withAttribute('shortCode', 'abc123') - ->withParsedBody(['tags' => []]) - ); - $this->assertEquals(404, $response->getStatusCode()); - } - - /** @test */ - public function tagsListIsReturnedIfCorrectShortCodeIsProvided() + public function tagsListIsReturnedIfCorrectShortCodeIsProvided(): void { $shortCode = 'abc123'; $this->shortUrlService->setTagsByShortCode($shortCode, [])->willReturn(new ShortUrl('')) From cdd36b6712eeecf331e96f1a3196b2b366fe8818 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 24 Nov 2019 13:24:52 +0100 Subject: [PATCH 19/40] Created BackwardsCompatibleProblemDetailsMiddlewareTest --- ...ardsCompatibleProblemDetailsMiddleware.php | 5 +- ...CompatibleProblemDetailsMiddlewareTest.php | 122 ++++++++++++++++++ 2 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 module/Rest/test/Middleware/BackwardsCompatibleProblemDetailsMiddlewareTest.php diff --git a/module/Rest/src/Middleware/BackwardsCompatibleProblemDetailsMiddleware.php b/module/Rest/src/Middleware/BackwardsCompatibleProblemDetailsMiddleware.php index 9857465b..0812c7e0 100644 --- a/module/Rest/src/Middleware/BackwardsCompatibleProblemDetailsMiddleware.php +++ b/module/Rest/src/Middleware/BackwardsCompatibleProblemDetailsMiddleware.php @@ -36,7 +36,6 @@ class BackwardsCompatibleProblemDetailsMiddleware implements MiddlewareInterface public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { $resp = $handler->handle($request); - if ($resp->getHeaderLine('Content-type') !== 'application/problem+json') { return $resp; } @@ -70,9 +69,7 @@ class BackwardsCompatibleProblemDetailsMiddleware implements MiddlewareInterface /** @deprecated When Shlink 2 is released, do not chekc the version */ private function isVersionOne(ServerRequestInterface $request): bool { - $uri = $request->getUri(); - $path = $uri->getPath(); - + $path = $request->getUri()->getPath(); return strpos($path, '/v') === false || strpos($path, '/v1') === 0; } diff --git a/module/Rest/test/Middleware/BackwardsCompatibleProblemDetailsMiddlewareTest.php b/module/Rest/test/Middleware/BackwardsCompatibleProblemDetailsMiddlewareTest.php new file mode 100644 index 00000000..4d47c4cb --- /dev/null +++ b/module/Rest/test/Middleware/BackwardsCompatibleProblemDetailsMiddlewareTest.php @@ -0,0 +1,122 @@ +handler = $this->prophesize(RequestHandlerInterface::class); + $this->middleware = new BackwardsCompatibleProblemDetailsMiddleware([ + 404 => 'NOT_FOUND', + 500 => 'INTERNAL_SERVER_ERROR', + ], 0); + } + + /** + * @test + * @dataProvider provideNonProcessableResponses + */ + public function nonProblemDetailsOrInvalidResponsesAreReturnedAsTheyAre(Response $response): void + { + $request = ServerRequestFactory::fromGlobals(); + $response = new Response(); + $handle = $this->handler->handle($request)->willReturn($response); + + $result = $this->middleware->process($request, $this->handler->reveal()); + + $this->assertSame($response, $result); + $handle->shouldHaveBeenCalledOnce(); + } + + public function provideNonProcessableResponses(): iterable + { + yield 'no problem details' => [new Response()]; + yield 'invalid JSON' => [(new Response('data://text/plain,{invalid-json'))->withHeader( + 'Content-Type', + 'application/problem+json' + )]; + } + + /** + * @test + * @dataProvider provideStatusAndTypes + */ + public function properlyMapsTypesBasedOnResponseStatus(Response\JsonResponse $response, string $expectedType): void + { + $request = ServerRequestFactory::fromGlobals()->withUri(new Uri('/v2/something')); + $handle = $this->handler->handle($request)->willReturn($response); + + /** @var Response\JsonResponse $result */ + $result = $this->middleware->process($request, $this->handler->reveal()); + $payload = $result->getPayload(); + + $this->assertEquals($expectedType, $payload['type']); + $this->assertArrayNotHasKey('error', $payload); + $this->assertArrayNotHasKey('message', $payload); + $handle->shouldHaveBeenCalledOnce(); + } + + public function provideStatusAndTypes(): iterable + { + yield [$this->jsonResponse(['type' => 'https://httpstatus.es/404'], 404), 'NOT_FOUND']; + yield [$this->jsonResponse(['type' => 'https://httpstatus.es/500'], 500), 'INTERNAL_SERVER_ERROR']; + yield [$this->jsonResponse(['type' => 'https://httpstatus.es/504'], 504), 'https://httpstatus.es/504']; + yield [$this->jsonResponse(['type' => 'something_else'], 404), 'something_else']; + yield [$this->jsonResponse(['type' => 'something_else'], 500), 'something_else']; + yield [$this->jsonResponse(['type' => 'something_else'], 504), 'something_else']; + } + + /** + * @test + * @dataProvider provideRequestPath + */ + public function mapsDeprecatedPropertiesWhenRequestIsPerformedToVersionOne(string $requestPath): void + { + $request = ServerRequestFactory::fromGlobals()->withUri(new Uri($requestPath)); + $response = $this->jsonResponse([ + 'type' => 'the_type', + 'detail' => 'the_detail', + ]); + $handle = $this->handler->handle($request)->willReturn($response); + + /** @var Response\JsonResponse $result */ + $result = $this->middleware->process($request, $this->handler->reveal()); + $payload = $result->getPayload(); + + $this->assertEquals([ + 'type' => 'the_type', + 'detail' => 'the_detail', + 'error' => 'the_type', + 'message' => 'the_detail', + ], $payload); + $handle->shouldHaveBeenCalledOnce(); + } + + public function provideRequestPath(): iterable + { + yield 'no version' => ['/foo']; + yield 'version one' => ['/v1/foo']; + } + + private function jsonResponse(array $payload, int $status = 200): Response\JsonResponse + { + $headers = ['Content-Type' => 'application/problem+json']; + return new Response\JsonResponse($payload, $status, $headers); + } +} From 2f1de4a162f224107a978c9ed9ca1aaf89f1fec7 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 24 Nov 2019 23:11:25 +0100 Subject: [PATCH 20/40] Renamed InvalidShortCodeException to ShortCodeNotFoundException --- .../src/Command/ShortUrl/DeleteShortUrlCommand.php | 2 +- module/CLI/src/Command/ShortUrl/ResolveUrlCommand.php | 4 ++-- .../Command/ShortUrl/DeleteShortUrlCommandTest.php | 2 +- .../test/Command/ShortUrl/ResolveUrlCommandTest.php | 4 ++-- module/Core/src/Action/AbstractTrackingAction.php | 4 ++-- module/Core/src/Action/PreviewAction.php | 4 ++-- module/Core/src/Action/QrCodeAction.php | 4 ++-- module/Core/src/Exception/DomainException.php | 11 +++++++++++ ...odeException.php => ShortUrlNotFoundException.php} | 4 ++-- .../src/Service/ShortUrl/DeleteShortUrlService.php | 2 +- .../ShortUrl/DeleteShortUrlServiceInterface.php | 2 +- .../Core/src/Service/ShortUrl/FindShortCodeTrait.php | 6 +++--- module/Core/src/Service/ShortUrlService.php | 6 +++--- module/Core/src/Service/ShortUrlServiceInterface.php | 6 +++--- module/Core/src/Service/VisitsTracker.php | 6 +++--- module/Core/src/Service/VisitsTrackerInterface.php | 4 ++-- module/Core/test/Action/PreviewActionTest.php | 4 ++-- module/Core/test/Action/QrCodeActionTest.php | 4 ++-- .../test/Exception/InvalidShortCodeExceptionTest.php | 4 ++-- module/Core/test/Service/ShortUrlServiceTest.php | 4 ++-- module/Rest/src/Action/Visit/GetVisitsAction.php | 4 ++-- module/Rest/src/Util/RestUtils.php | 6 +++--- module/Rest/test/Action/Visit/GetVisitsActionTest.php | 4 ++-- module/Rest/test/Util/RestUtilsTest.php | 4 ++-- 24 files changed, 58 insertions(+), 47 deletions(-) create mode 100644 module/Core/src/Exception/DomainException.php rename module/Core/src/Exception/{InvalidShortCodeException.php => ShortUrlNotFoundException.php} (82%) diff --git a/module/CLI/src/Command/ShortUrl/DeleteShortUrlCommand.php b/module/CLI/src/Command/ShortUrl/DeleteShortUrlCommand.php index 683fd893..f44e3e18 100644 --- a/module/CLI/src/Command/ShortUrl/DeleteShortUrlCommand.php +++ b/module/CLI/src/Command/ShortUrl/DeleteShortUrlCommand.php @@ -55,7 +55,7 @@ class DeleteShortUrlCommand extends Command try { $this->runDelete($io, $shortCode, $ignoreThreshold); return ExitCodes::EXIT_SUCCESS; - } catch (Exception\InvalidShortCodeException $e) { + } catch (Exception\ShortUrlNotFoundException $e) { $io->error(sprintf('Provided short code "%s" could not be found.', $shortCode)); return ExitCodes::EXIT_FAILURE; } catch (Exception\DeleteShortUrlException $e) { diff --git a/module/CLI/src/Command/ShortUrl/ResolveUrlCommand.php b/module/CLI/src/Command/ShortUrl/ResolveUrlCommand.php index bc159a79..b758ddd4 100644 --- a/module/CLI/src/Command/ShortUrl/ResolveUrlCommand.php +++ b/module/CLI/src/Command/ShortUrl/ResolveUrlCommand.php @@ -6,7 +6,7 @@ namespace Shlinkio\Shlink\CLI\Command\ShortUrl; use Shlinkio\Shlink\CLI\Util\ExitCodes; use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; -use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; +use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Service\UrlShortenerInterface; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -65,7 +65,7 @@ class ResolveUrlCommand extends Command $url = $this->urlShortener->shortCodeToUrl($shortCode, $domain); $output->writeln(sprintf('Long URL: %s', $url->getLongUrl())); return ExitCodes::EXIT_SUCCESS; - } catch (InvalidShortCodeException $e) { + } catch (ShortUrlNotFoundException $e) { $io->error(sprintf('Provided short code "%s" has an invalid format.', $shortCode)); return ExitCodes::EXIT_FAILURE; } catch (EntityDoesNotExistException $e) { diff --git a/module/CLI/test/Command/ShortUrl/DeleteShortUrlCommandTest.php b/module/CLI/test/Command/ShortUrl/DeleteShortUrlCommandTest.php index f0fad2cc..76414481 100644 --- a/module/CLI/test/Command/ShortUrl/DeleteShortUrlCommandTest.php +++ b/module/CLI/test/Command/ShortUrl/DeleteShortUrlCommandTest.php @@ -58,7 +58,7 @@ class DeleteShortUrlCommandTest extends TestCase { $shortCode = 'abc123'; $deleteByShortCode = $this->service->deleteByShortCode($shortCode, false)->willThrow( - Exception\InvalidShortCodeException::class + Exception\ShortUrlNotFoundException::class ); $this->commandTester->execute(['shortCode' => $shortCode]); diff --git a/module/CLI/test/Command/ShortUrl/ResolveUrlCommandTest.php b/module/CLI/test/Command/ShortUrl/ResolveUrlCommandTest.php index 283b3a4f..9e86bbd1 100644 --- a/module/CLI/test/Command/ShortUrl/ResolveUrlCommandTest.php +++ b/module/CLI/test/Command/ShortUrl/ResolveUrlCommandTest.php @@ -9,7 +9,7 @@ use Prophecy\Prophecy\ObjectProphecy; use Shlinkio\Shlink\CLI\Command\ShortUrl\ResolveUrlCommand; use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; -use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; +use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Service\UrlShortener; use Symfony\Component\Console\Application; use Symfony\Component\Console\Tester\CommandTester; @@ -63,7 +63,7 @@ class ResolveUrlCommandTest extends TestCase public function wrongShortCodeFormatOutputsErrorMessage(): void { $shortCode = 'abc123'; - $this->urlShortener->shortCodeToUrl($shortCode, null)->willThrow(new InvalidShortCodeException()) + $this->urlShortener->shortCodeToUrl($shortCode, null)->willThrow(new ShortUrlNotFoundException()) ->shouldBeCalledOnce(); $this->commandTester->execute(['shortCode' => $shortCode]); diff --git a/module/Core/src/Action/AbstractTrackingAction.php b/module/Core/src/Action/AbstractTrackingAction.php index 9bacaf39..5bf195f1 100644 --- a/module/Core/src/Action/AbstractTrackingAction.php +++ b/module/Core/src/Action/AbstractTrackingAction.php @@ -12,7 +12,7 @@ use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; -use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; +use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Model\Visitor; use Shlinkio\Shlink\Core\Options\AppOptions; use Shlinkio\Shlink\Core\Service\UrlShortenerInterface; @@ -72,7 +72,7 @@ abstract class AbstractTrackingAction implements MiddlewareInterface } return $this->createSuccessResp($this->buildUrlToRedirectTo($url, $query, $disableTrackParam)); - } catch (InvalidShortCodeException | EntityDoesNotExistException $e) { + } catch (ShortUrlNotFoundException | EntityDoesNotExistException $e) { $this->logger->warning('An error occurred while tracking short code. {e}', ['e' => $e]); return $this->createErrorResp($request, $handler); } diff --git a/module/Core/src/Action/PreviewAction.php b/module/Core/src/Action/PreviewAction.php index 4ac2a50c..9ba0eaf5 100644 --- a/module/Core/src/Action/PreviewAction.php +++ b/module/Core/src/Action/PreviewAction.php @@ -12,7 +12,7 @@ use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use Shlinkio\Shlink\Common\Response\ResponseUtilsTrait; use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; -use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; +use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Service\UrlShortenerInterface; use Shlinkio\Shlink\PreviewGenerator\Exception\PreviewGenerationException; use Shlinkio\Shlink\PreviewGenerator\Service\PreviewGeneratorInterface; @@ -56,7 +56,7 @@ class PreviewAction implements MiddlewareInterface $url = $this->urlShortener->shortCodeToUrl($shortCode); $imagePath = $this->previewGenerator->generatePreview($url->getLongUrl()); return $this->generateImageResponse($imagePath); - } catch (InvalidShortCodeException | EntityDoesNotExistException | PreviewGenerationException $e) { + } catch (ShortUrlNotFoundException | EntityDoesNotExistException | PreviewGenerationException $e) { $this->logger->warning('An error occurred while generating preview image. {e}', ['e' => $e]); return $handler->handle($request); } diff --git a/module/Core/src/Action/QrCodeAction.php b/module/Core/src/Action/QrCodeAction.php index a1fdae5b..1c5c08df 100644 --- a/module/Core/src/Action/QrCodeAction.php +++ b/module/Core/src/Action/QrCodeAction.php @@ -13,7 +13,7 @@ use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use Shlinkio\Shlink\Common\Response\QrCodeResponse; use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; -use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; +use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Service\UrlShortenerInterface; use Zend\Expressive\Router\Exception\RuntimeException; use Zend\Expressive\Router\RouterInterface; @@ -60,7 +60,7 @@ class QrCodeAction implements MiddlewareInterface try { $this->urlShortener->shortCodeToUrl($shortCode, $domain); - } catch (InvalidShortCodeException | EntityDoesNotExistException $e) { + } catch (ShortUrlNotFoundException | EntityDoesNotExistException $e) { $this->logger->warning('An error occurred while creating QR code. {e}', ['e' => $e]); return $handler->handle($request); } diff --git a/module/Core/src/Exception/DomainException.php b/module/Core/src/Exception/DomainException.php new file mode 100644 index 00000000..63134952 --- /dev/null +++ b/module/Core/src/Exception/DomainException.php @@ -0,0 +1,11 @@ + $shortCode, ]); if ($shortUrl === null) { - throw InvalidShortCodeException::fromNotFoundShortCode($shortCode); + throw ShortUrlNotFoundException::fromNotFoundShortCode($shortCode); } return $shortUrl; diff --git a/module/Core/src/Service/ShortUrlService.php b/module/Core/src/Service/ShortUrlService.php index 02977186..7719cafe 100644 --- a/module/Core/src/Service/ShortUrlService.php +++ b/module/Core/src/Service/ShortUrlService.php @@ -6,7 +6,7 @@ namespace Shlinkio\Shlink\Core\Service; use Doctrine\ORM; use Shlinkio\Shlink\Core\Entity\ShortUrl; -use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; +use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Model\ShortUrlMeta; use Shlinkio\Shlink\Core\Paginator\Adapter\ShortUrlRepositoryAdapter; use Shlinkio\Shlink\Core\Repository\ShortUrlRepository; @@ -45,7 +45,7 @@ class ShortUrlService implements ShortUrlServiceInterface /** * @param string[] $tags - * @throws InvalidShortCodeException + * @throws ShortUrlNotFoundException */ public function setTagsByShortCode(string $shortCode, array $tags = []): ShortUrl { @@ -57,7 +57,7 @@ class ShortUrlService implements ShortUrlServiceInterface } /** - * @throws InvalidShortCodeException + * @throws ShortUrlNotFoundException */ public function updateMetadataByShortCode(string $shortCode, ShortUrlMeta $shortUrlMeta): ShortUrl { diff --git a/module/Core/src/Service/ShortUrlServiceInterface.php b/module/Core/src/Service/ShortUrlServiceInterface.php index e519f7c9..cb2c7dca 100644 --- a/module/Core/src/Service/ShortUrlServiceInterface.php +++ b/module/Core/src/Service/ShortUrlServiceInterface.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Service; use Shlinkio\Shlink\Core\Entity\ShortUrl; -use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; +use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Model\ShortUrlMeta; use Zend\Paginator\Paginator; @@ -20,12 +20,12 @@ interface ShortUrlServiceInterface /** * @param string[] $tags - * @throws InvalidShortCodeException + * @throws ShortUrlNotFoundException */ public function setTagsByShortCode(string $shortCode, array $tags = []): ShortUrl; /** - * @throws InvalidShortCodeException + * @throws ShortUrlNotFoundException */ public function updateMetadataByShortCode(string $shortCode, ShortUrlMeta $shortUrlMeta): ShortUrl; } diff --git a/module/Core/src/Service/VisitsTracker.php b/module/Core/src/Service/VisitsTracker.php index 836c16a9..612ad4ee 100644 --- a/module/Core/src/Service/VisitsTracker.php +++ b/module/Core/src/Service/VisitsTracker.php @@ -9,7 +9,7 @@ use Psr\EventDispatcher\EventDispatcherInterface; use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\Visit; use Shlinkio\Shlink\Core\EventDispatcher\ShortUrlVisited; -use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; +use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Model\Visitor; use Shlinkio\Shlink\Core\Model\VisitsParams; use Shlinkio\Shlink\Core\Paginator\Adapter\VisitsPaginatorAdapter; @@ -51,14 +51,14 @@ class VisitsTracker implements VisitsTrackerInterface * Returns the visits on certain short code * * @return Visit[]|Paginator - * @throws InvalidShortCodeException + * @throws ShortUrlNotFoundException */ public function info(string $shortCode, VisitsParams $params): Paginator { /** @var ORM\EntityRepository $repo */ $repo = $this->em->getRepository(ShortUrl::class); if ($repo->count(['shortCode' => $shortCode]) < 1) { - throw InvalidShortCodeException::fromNotFoundShortCode($shortCode); + throw ShortUrlNotFoundException::fromNotFoundShortCode($shortCode); } /** @var VisitRepository $repo */ diff --git a/module/Core/src/Service/VisitsTrackerInterface.php b/module/Core/src/Service/VisitsTrackerInterface.php index d3934992..2786d23b 100644 --- a/module/Core/src/Service/VisitsTrackerInterface.php +++ b/module/Core/src/Service/VisitsTrackerInterface.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Service; use Shlinkio\Shlink\Core\Entity\Visit; -use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; +use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Model\Visitor; use Shlinkio\Shlink\Core\Model\VisitsParams; use Zend\Paginator\Paginator; @@ -21,7 +21,7 @@ interface VisitsTrackerInterface * Returns the visits on certain short code * * @return Visit[]|Paginator - * @throws InvalidShortCodeException + * @throws ShortUrlNotFoundException */ public function info(string $shortCode, VisitsParams $params): Paginator; } diff --git a/module/Core/test/Action/PreviewActionTest.php b/module/Core/test/Action/PreviewActionTest.php index bb52ec0e..909d70b7 100644 --- a/module/Core/test/Action/PreviewActionTest.php +++ b/module/Core/test/Action/PreviewActionTest.php @@ -12,7 +12,7 @@ use Psr\Http\Server\RequestHandlerInterface; use Shlinkio\Shlink\Core\Action\PreviewAction; use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; -use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; +use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Service\UrlShortener; use Shlinkio\Shlink\PreviewGenerator\Service\PreviewGenerator; use Zend\Diactoros\Response; @@ -74,7 +74,7 @@ class PreviewActionTest extends TestCase public function invalidShortCodeExceptionFallsBackToNextMiddleware(): void { $shortCode = 'abc123'; - $this->urlShortener->shortCodeToUrl($shortCode)->willThrow(InvalidShortCodeException::class) + $this->urlShortener->shortCodeToUrl($shortCode)->willThrow(ShortUrlNotFoundException::class) ->shouldBeCalledOnce(); $delegate = $this->prophesize(RequestHandlerInterface::class); $process = $delegate->handle(Argument::any())->willReturn(new Response()); diff --git a/module/Core/test/Action/QrCodeActionTest.php b/module/Core/test/Action/QrCodeActionTest.php index ddb062dd..24fe8c4b 100644 --- a/module/Core/test/Action/QrCodeActionTest.php +++ b/module/Core/test/Action/QrCodeActionTest.php @@ -12,7 +12,7 @@ use Shlinkio\Shlink\Common\Response\QrCodeResponse; use Shlinkio\Shlink\Core\Action\QrCodeAction; use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; -use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; +use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Service\UrlShortener; use Zend\Diactoros\Response; use Zend\Diactoros\ServerRequest; @@ -53,7 +53,7 @@ class QrCodeActionTest extends TestCase public function anInvalidShortCodeWillReturnNotFoundResponse(): void { $shortCode = 'abc123'; - $this->urlShortener->shortCodeToUrl($shortCode, '')->willThrow(InvalidShortCodeException::class) + $this->urlShortener->shortCodeToUrl($shortCode, '')->willThrow(ShortUrlNotFoundException::class) ->shouldBeCalledOnce(); $delegate = $this->prophesize(RequestHandlerInterface::class); $process = $delegate->handle(Argument::any())->willReturn(new Response()); diff --git a/module/Core/test/Exception/InvalidShortCodeExceptionTest.php b/module/Core/test/Exception/InvalidShortCodeExceptionTest.php index 4639fda3..c1815db4 100644 --- a/module/Core/test/Exception/InvalidShortCodeExceptionTest.php +++ b/module/Core/test/Exception/InvalidShortCodeExceptionTest.php @@ -5,14 +5,14 @@ declare(strict_types=1); namespace ShlinkioTest\Shlink\Core\Exception; use PHPUnit\Framework\TestCase; -use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; +use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; class InvalidShortCodeExceptionTest extends TestCase { /** @test */ public function properlyCreatesExceptionFromNotFoundShortCode(): void { - $e = InvalidShortCodeException::fromNotFoundShortCode('abc123'); + $e = ShortUrlNotFoundException::fromNotFoundShortCode('abc123'); $this->assertEquals('No URL found for short code "abc123"', $e->getMessage()); } diff --git a/module/Core/test/Service/ShortUrlServiceTest.php b/module/Core/test/Service/ShortUrlServiceTest.php index 07628f7c..3dae00a7 100644 --- a/module/Core/test/Service/ShortUrlServiceTest.php +++ b/module/Core/test/Service/ShortUrlServiceTest.php @@ -12,7 +12,7 @@ use Prophecy\Argument; use Prophecy\Prophecy\ObjectProphecy; use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\Tag; -use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; +use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Model\ShortUrlMeta; use Shlinkio\Shlink\Core\Repository\ShortUrlRepository; use Shlinkio\Shlink\Core\Service\ShortUrlService; @@ -62,7 +62,7 @@ class ShortUrlServiceTest extends TestCase ->shouldBeCalledOnce(); $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal()); - $this->expectException(InvalidShortCodeException::class); + $this->expectException(ShortUrlNotFoundException::class); $this->service->setTagsByShortCode($shortCode); } diff --git a/module/Rest/src/Action/Visit/GetVisitsAction.php b/module/Rest/src/Action/Visit/GetVisitsAction.php index ed614ae7..68d50533 100644 --- a/module/Rest/src/Action/Visit/GetVisitsAction.php +++ b/module/Rest/src/Action/Visit/GetVisitsAction.php @@ -8,7 +8,7 @@ use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Log\LoggerInterface; use Shlinkio\Shlink\Common\Paginator\Util\PaginatorUtilsTrait; -use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; +use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Model\VisitsParams; use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface; use Shlinkio\Shlink\Rest\Action\AbstractRestAction; @@ -43,7 +43,7 @@ class GetVisitsAction extends AbstractRestAction return new JsonResponse([ 'visits' => $this->serializePaginator($visits), ]); - } catch (InvalidShortCodeException $e) { + } catch (ShortUrlNotFoundException $e) { $this->logger->warning('Provided nonexistent short code {e}', ['e' => $e]); return new JsonResponse([ 'error' => RestUtils::INVALID_ARGUMENT_ERROR, // FIXME Wrong code. Use correct one in "type" diff --git a/module/Rest/src/Util/RestUtils.php b/module/Rest/src/Util/RestUtils.php index 71c65e88..13488b01 100644 --- a/module/Rest/src/Util/RestUtils.php +++ b/module/Rest/src/Util/RestUtils.php @@ -6,14 +6,14 @@ namespace Shlinkio\Shlink\Rest\Util; use Shlinkio\Shlink\Common\Exception as Common; use Shlinkio\Shlink\Core\Exception as Core; -use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; +use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Rest\Exception as Rest; use Throwable; class RestUtils { /** @deprecated */ - public const INVALID_SHORTCODE_ERROR = InvalidShortCodeException::TYPE; + public const INVALID_SHORTCODE_ERROR = ShortUrlNotFoundException::TYPE; // FIXME Should be INVALID_SHORT_URL_DELETION public const INVALID_SHORTCODE_DELETION_ERROR = 'INVALID_SHORTCODE_DELETION'; public const INVALID_URL_ERROR = 'INVALID_URL'; @@ -30,7 +30,7 @@ class RestUtils public static function getRestErrorCodeFromException(Throwable $e): string { switch (true) { - case $e instanceof Core\InvalidShortCodeException: + case $e instanceof Core\ShortUrlNotFoundException: return self::INVALID_SHORTCODE_ERROR; case $e instanceof Core\InvalidUrlException: return self::INVALID_URL_ERROR; diff --git a/module/Rest/test/Action/Visit/GetVisitsActionTest.php b/module/Rest/test/Action/Visit/GetVisitsActionTest.php index 789469ca..cb38dd85 100644 --- a/module/Rest/test/Action/Visit/GetVisitsActionTest.php +++ b/module/Rest/test/Action/Visit/GetVisitsActionTest.php @@ -9,7 +9,7 @@ use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\Prophecy\ObjectProphecy; use Shlinkio\Shlink\Common\Util\DateRange; -use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; +use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Model\VisitsParams; use Shlinkio\Shlink\Core\Service\VisitsTracker; use Shlinkio\Shlink\Rest\Action\Visit\GetVisitsAction; @@ -47,7 +47,7 @@ class GetVisitsActionTest extends TestCase { $shortCode = 'abc123'; $this->visitsTracker->info($shortCode, Argument::type(VisitsParams::class))->willThrow( - InvalidShortCodeException::class + ShortUrlNotFoundException::class )->shouldBeCalledOnce(); $response = $this->action->handle((new ServerRequest())->withAttribute('shortCode', $shortCode)); diff --git a/module/Rest/test/Util/RestUtilsTest.php b/module/Rest/test/Util/RestUtilsTest.php index c958594b..d8a45181 100644 --- a/module/Rest/test/Util/RestUtilsTest.php +++ b/module/Rest/test/Util/RestUtilsTest.php @@ -6,7 +6,7 @@ namespace ShlinkioTest\Shlink\Rest\Util; use PHPUnit\Framework\TestCase; use Shlinkio\Shlink\Common\Exception\InvalidArgumentException; -use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; +use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Exception\InvalidUrlException; use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException; use Shlinkio\Shlink\Rest\Exception\AuthenticationException; @@ -19,7 +19,7 @@ class RestUtilsTest extends TestCase { $this->assertEquals( RestUtils::INVALID_SHORTCODE_ERROR, - RestUtils::getRestErrorCodeFromException(new InvalidShortCodeException()) + RestUtils::getRestErrorCodeFromException(new ShortUrlNotFoundException()) ); $this->assertEquals( RestUtils::INVALID_URL_ERROR, From 0d7d53ab5babcb996faba1a339bb98768e9e97a0 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 24 Nov 2019 23:24:53 +0100 Subject: [PATCH 21/40] Converted InvalidUrlException into a problem details exception --- .../src/Exception/InvalidUrlException.php | 21 ++++++++++++++++--- .../Exception/InvalidUrlExceptionTest.php | 5 +++-- .../ShortUrl/AbstractCreateShortUrlAction.php | 7 ------- module/Rest/src/Util/RestUtils.php | 3 ++- .../ShortUrl/CreateShortUrlActionTest.php | 16 -------------- 5 files changed, 23 insertions(+), 29 deletions(-) diff --git a/module/Core/src/Exception/InvalidUrlException.php b/module/Core/src/Exception/InvalidUrlException.php index 5cd79af5..32758b4e 100644 --- a/module/Core/src/Exception/InvalidUrlException.php +++ b/module/Core/src/Exception/InvalidUrlException.php @@ -4,15 +4,30 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Exception; +use Fig\Http\Message\StatusCodeInterface; use Throwable; +use Zend\ProblemDetails\Exception\CommonProblemDetailsExceptionTrait; +use Zend\ProblemDetails\Exception\ProblemDetailsExceptionInterface; use function sprintf; -class InvalidUrlException extends RuntimeException +class InvalidUrlException extends DomainException implements ProblemDetailsExceptionInterface { + use CommonProblemDetailsExceptionTrait; + + private const TITLE = 'Invalid URL'; + public const TYPE = 'INVALID_URL'; + public static function fromUrl(string $url, ?Throwable $previous = null): self { - $code = $previous !== null ? $previous->getCode() : -1; - return new static(sprintf('Provided URL "%s" is not an existing and valid URL', $url), $code, $previous); + $status = StatusCodeInterface::STATUS_BAD_REQUEST; + $e = new self(sprintf('Provided URL %s is invalid. Try with a different one.', $url), $status, $previous); + + $e->detail = $e->getMessage(); + $e->title = self::TITLE; + $e->type = self::TYPE; + $e->status = $status; + + return $e; } } diff --git a/module/Core/test/Exception/InvalidUrlExceptionTest.php b/module/Core/test/Exception/InvalidUrlExceptionTest.php index a5d19341..1b7de449 100644 --- a/module/Core/test/Exception/InvalidUrlExceptionTest.php +++ b/module/Core/test/Exception/InvalidUrlExceptionTest.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace ShlinkioTest\Shlink\Core\Exception; use Exception; +use Fig\Http\Message\StatusCodeInterface; use PHPUnit\Framework\TestCase; use Shlinkio\Shlink\Core\Exception\InvalidUrlException; use Throwable; @@ -19,8 +20,8 @@ class InvalidUrlExceptionTest extends TestCase { $e = InvalidUrlException::fromUrl('http://the_url.com', $prev); - $this->assertEquals('Provided URL "http://the_url.com" is not an existing and valid URL', $e->getMessage()); - $this->assertEquals($prev !== null ? $prev->getCode() : -1, $e->getCode()); + $this->assertEquals('Provided URL http://the_url.com is invalid. Try with a different one.', $e->getMessage()); + $this->assertEquals(StatusCodeInterface::STATUS_BAD_REQUEST, $e->getCode()); $this->assertEquals($prev, $e->getPrevious()); } diff --git a/module/Rest/src/Action/ShortUrl/AbstractCreateShortUrlAction.php b/module/Rest/src/Action/ShortUrl/AbstractCreateShortUrlAction.php index bc0d50ef..d627b427 100644 --- a/module/Rest/src/Action/ShortUrl/AbstractCreateShortUrlAction.php +++ b/module/Rest/src/Action/ShortUrl/AbstractCreateShortUrlAction.php @@ -7,7 +7,6 @@ namespace Shlinkio\Shlink\Rest\Action\ShortUrl; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Log\LoggerInterface; -use Shlinkio\Shlink\Core\Exception\InvalidUrlException; use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException; use Shlinkio\Shlink\Core\Exception\ValidationException; use Shlinkio\Shlink\Core\Model\CreateShortUrlData; @@ -60,12 +59,6 @@ abstract class AbstractCreateShortUrlAction extends AbstractRestAction $transformer = new ShortUrlDataTransformer($this->domainConfig); return new JsonResponse($transformer->transform($shortUrl)); - } catch (InvalidUrlException $e) { - $this->logger->warning('Provided Invalid URL. {e}', ['e' => $e]); - return new JsonResponse([ - 'error' => RestUtils::getRestErrorCodeFromException($e), - 'message' => sprintf('Provided URL %s is invalid. Try with a different one.', $longUrl), - ], self::STATUS_BAD_REQUEST); } catch (NonUniqueSlugException $e) { $customSlug = $shortUrlMeta->getCustomSlug(); $this->logger->warning('Provided non-unique slug. {e}', ['e' => $e]); diff --git a/module/Rest/src/Util/RestUtils.php b/module/Rest/src/Util/RestUtils.php index 13488b01..279d7fca 100644 --- a/module/Rest/src/Util/RestUtils.php +++ b/module/Rest/src/Util/RestUtils.php @@ -16,7 +16,8 @@ class RestUtils public const INVALID_SHORTCODE_ERROR = ShortUrlNotFoundException::TYPE; // FIXME Should be INVALID_SHORT_URL_DELETION public const INVALID_SHORTCODE_DELETION_ERROR = 'INVALID_SHORTCODE_DELETION'; - public const INVALID_URL_ERROR = 'INVALID_URL'; + /** @deprecated */ + public const INVALID_URL_ERROR = Core\InvalidUrlException::TYPE; public const INVALID_ARGUMENT_ERROR = 'INVALID_ARGUMENT'; public const INVALID_SLUG_ERROR = 'INVALID_SLUG'; public const INVALID_CREDENTIALS_ERROR = 'INVALID_CREDENTIALS'; diff --git a/module/Rest/test/Action/ShortUrl/CreateShortUrlActionTest.php b/module/Rest/test/Action/ShortUrl/CreateShortUrlActionTest.php index 732cbc25..1cc5c889 100644 --- a/module/Rest/test/Action/ShortUrl/CreateShortUrlActionTest.php +++ b/module/Rest/test/Action/ShortUrl/CreateShortUrlActionTest.php @@ -8,7 +8,6 @@ use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\Prophecy\ObjectProphecy; use Shlinkio\Shlink\Core\Entity\ShortUrl; -use Shlinkio\Shlink\Core\Exception\InvalidUrlException; use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException; use Shlinkio\Shlink\Core\Model\ShortUrlMeta; use Shlinkio\Shlink\Core\Service\UrlShortener; @@ -61,21 +60,6 @@ class CreateShortUrlActionTest extends TestCase $this->assertTrue(strpos($response->getBody()->getContents(), $shortUrl->toString(self::DOMAIN_CONFIG)) > 0); } - /** @test */ - public function anInvalidUrlReturnsError(): void - { - $this->urlShortener->urlToShortCode(Argument::type(Uri::class), Argument::type('array'), Argument::cetera()) - ->willThrow(InvalidUrlException::class) - ->shouldBeCalledOnce(); - - $request = (new ServerRequest())->withParsedBody([ - 'longUrl' => 'http://www.domain.com/foo/bar', - ]); - $response = $this->action->handle($request); - $this->assertEquals(400, $response->getStatusCode()); - $this->assertTrue(strpos($response->getBody()->getContents(), RestUtils::INVALID_URL_ERROR) > 0); - } - /** * @test * @dataProvider provideInvalidDomains From c1eee2246bbe222c71ab6fde17dfa372f93a6192 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 24 Nov 2019 23:32:37 +0100 Subject: [PATCH 22/40] Converted NonUniqueSlugException into a problem details exception --- .../src/Exception/NonUniqueSlugException.php | 20 ++++++++++++++++-- .../Exception/NonUniqueSlugExceptionTest.php | 4 ++-- .../ShortUrl/AbstractCreateShortUrlAction.php | 19 ++++------------- module/Rest/src/Util/RestUtils.php | 3 ++- .../ShortUrl/CreateShortUrlActionTest.php | 21 ------------------- 5 files changed, 26 insertions(+), 41 deletions(-) diff --git a/module/Core/src/Exception/NonUniqueSlugException.php b/module/Core/src/Exception/NonUniqueSlugException.php index fb7e4503..9e22d354 100644 --- a/module/Core/src/Exception/NonUniqueSlugException.php +++ b/module/Core/src/Exception/NonUniqueSlugException.php @@ -4,10 +4,19 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Exception; +use Fig\Http\Message\StatusCodeInterface; +use Zend\ProblemDetails\Exception\CommonProblemDetailsExceptionTrait; +use Zend\ProblemDetails\Exception\ProblemDetailsExceptionInterface; + use function sprintf; -class NonUniqueSlugException extends InvalidArgumentException +class NonUniqueSlugException extends InvalidArgumentException implements ProblemDetailsExceptionInterface { + use CommonProblemDetailsExceptionTrait; + + private const TITLE = 'Invalid custom slug'; + public const TYPE = 'INVALID_SLUG'; + public static function fromSlug(string $slug, ?string $domain): self { $suffix = ''; @@ -15,6 +24,13 @@ class NonUniqueSlugException extends InvalidArgumentException $suffix = sprintf(' for domain "%s"', $domain); } - return new self(sprintf('Provided slug "%s" is not unique%s.', $slug, $suffix)); + $e = new self(sprintf('Provided slug "%s" is already in use%s.', $slug, $suffix)); + + $e->detail = $e->getMessage(); + $e->title = self::TITLE; + $e->type = self::TYPE; + $e->status = StatusCodeInterface::STATUS_BAD_REQUEST; + + return $e; } } diff --git a/module/Core/test/Exception/NonUniqueSlugExceptionTest.php b/module/Core/test/Exception/NonUniqueSlugExceptionTest.php index 71c4a276..d2008621 100644 --- a/module/Core/test/Exception/NonUniqueSlugExceptionTest.php +++ b/module/Core/test/Exception/NonUniqueSlugExceptionTest.php @@ -22,12 +22,12 @@ class NonUniqueSlugExceptionTest extends TestCase public function provideMessages(): iterable { yield 'without domain' => [ - 'Provided slug "foo" is not unique.', + 'Provided slug "foo" is already in use.', 'foo', null, ]; yield 'with domain' => [ - 'Provided slug "baz" is not unique for domain "bar".', + 'Provided slug "baz" is already in use for domain "bar".', 'baz', 'bar', ]; diff --git a/module/Rest/src/Action/ShortUrl/AbstractCreateShortUrlAction.php b/module/Rest/src/Action/ShortUrl/AbstractCreateShortUrlAction.php index d627b427..38daf905 100644 --- a/module/Rest/src/Action/ShortUrl/AbstractCreateShortUrlAction.php +++ b/module/Rest/src/Action/ShortUrl/AbstractCreateShortUrlAction.php @@ -7,7 +7,6 @@ namespace Shlinkio\Shlink\Rest\Action\ShortUrl; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Log\LoggerInterface; -use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException; use Shlinkio\Shlink\Core\Exception\ValidationException; use Shlinkio\Shlink\Core\Model\CreateShortUrlData; use Shlinkio\Shlink\Core\Service\UrlShortenerInterface; @@ -16,8 +15,6 @@ use Shlinkio\Shlink\Rest\Action\AbstractRestAction; use Shlinkio\Shlink\Rest\Util\RestUtils; use Zend\Diactoros\Response\JsonResponse; -use function sprintf; - abstract class AbstractCreateShortUrlAction extends AbstractRestAction { /** @var UrlShortenerInterface */ @@ -52,21 +49,13 @@ abstract class AbstractCreateShortUrlAction extends AbstractRestAction } $longUrl = $shortUrlData->getLongUrl(); + $tags = $shortUrlData->getTags(); $shortUrlMeta = $shortUrlData->getMeta(); - try { - $shortUrl = $this->urlShortener->urlToShortCode($longUrl, $shortUrlData->getTags(), $shortUrlMeta); - $transformer = new ShortUrlDataTransformer($this->domainConfig); + $shortUrl = $this->urlShortener->urlToShortCode($longUrl, $tags, $shortUrlMeta); + $transformer = new ShortUrlDataTransformer($this->domainConfig); - return new JsonResponse($transformer->transform($shortUrl)); - } catch (NonUniqueSlugException $e) { - $customSlug = $shortUrlMeta->getCustomSlug(); - $this->logger->warning('Provided non-unique slug. {e}', ['e' => $e]); - return new JsonResponse([ - 'error' => RestUtils::getRestErrorCodeFromException($e), - 'message' => sprintf('Provided slug %s is already in use. Try with a different one.', $customSlug), - ], self::STATUS_BAD_REQUEST); - } + return new JsonResponse($transformer->transform($shortUrl)); } /** diff --git a/module/Rest/src/Util/RestUtils.php b/module/Rest/src/Util/RestUtils.php index 279d7fca..24398eb9 100644 --- a/module/Rest/src/Util/RestUtils.php +++ b/module/Rest/src/Util/RestUtils.php @@ -19,7 +19,8 @@ class RestUtils /** @deprecated */ public const INVALID_URL_ERROR = Core\InvalidUrlException::TYPE; public const INVALID_ARGUMENT_ERROR = 'INVALID_ARGUMENT'; - public const INVALID_SLUG_ERROR = 'INVALID_SLUG'; + /** @deprecated */ + public const INVALID_SLUG_ERROR = Core\NonUniqueSlugException::TYPE; public const INVALID_CREDENTIALS_ERROR = 'INVALID_CREDENTIALS'; public const INVALID_AUTH_TOKEN_ERROR = 'INVALID_AUTH_TOKEN'; public const INVALID_AUTHORIZATION_ERROR = 'INVALID_AUTHORIZATION'; diff --git a/module/Rest/test/Action/ShortUrl/CreateShortUrlActionTest.php b/module/Rest/test/Action/ShortUrl/CreateShortUrlActionTest.php index 1cc5c889..c4f8970d 100644 --- a/module/Rest/test/Action/ShortUrl/CreateShortUrlActionTest.php +++ b/module/Rest/test/Action/ShortUrl/CreateShortUrlActionTest.php @@ -8,8 +8,6 @@ use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\Prophecy\ObjectProphecy; use Shlinkio\Shlink\Core\Entity\ShortUrl; -use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException; -use Shlinkio\Shlink\Core\Model\ShortUrlMeta; use Shlinkio\Shlink\Core\Service\UrlShortener; use Shlinkio\Shlink\Rest\Action\ShortUrl\CreateShortUrlAction; use Shlinkio\Shlink\Rest\Util\RestUtils; @@ -88,23 +86,4 @@ class CreateShortUrlActionTest extends TestCase yield ['127.0.0.1']; yield ['???/&%$&']; } - - /** @test */ - public function nonUniqueSlugReturnsError(): void - { - $this->urlShortener->urlToShortCode( - Argument::type(Uri::class), - Argument::type('array'), - ShortUrlMeta::createFromRawData(['customSlug' => 'foo']), - Argument::cetera() - )->willThrow(NonUniqueSlugException::class)->shouldBeCalledOnce(); - - $request = (new ServerRequest())->withParsedBody([ - 'longUrl' => 'http://www.domain.com/foo/bar', - 'customSlug' => 'foo', - ]); - $response = $this->action->handle($request); - $this->assertEquals(400, $response->getStatusCode()); - $this->assertStringContainsString(RestUtils::INVALID_SLUG_ERROR, (string) $response->getBody()); - } } From 32b3c72bdff6e91e089522e244dcba91a0e1232d Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 24 Nov 2019 23:45:40 +0100 Subject: [PATCH 23/40] Converted ValidationException into a problem details exception --- .../src/Exception/ValidationException.php | 23 ++++++++++++++-- .../Exception/ValidationExceptionTest.php | 3 ++- .../ShortUrl/AbstractCreateShortUrlAction.php | 16 +---------- .../Action/ShortUrl/EditShortUrlAction.php | 27 ++----------------- module/Rest/src/Util/RestUtils.php | 6 ++--- .../ShortUrl/CreateShortUrlActionTest.php | 17 +++++------- .../ShortUrl/EditShortUrlActionTest.php | 11 +++----- .../SingleStepCreateShortUrlActionTest.php | 22 +++++---------- 8 files changed, 47 insertions(+), 78 deletions(-) diff --git a/module/Core/src/Exception/ValidationException.php b/module/Core/src/Exception/ValidationException.php index 70dfe0d0..510a31b0 100644 --- a/module/Core/src/Exception/ValidationException.php +++ b/module/Core/src/Exception/ValidationException.php @@ -4,8 +4,11 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Exception; +use Fig\Http\Message\StatusCodeInterface; use Throwable; use Zend\InputFilter\InputFilterInterface; +use Zend\ProblemDetails\Exception\CommonProblemDetailsExceptionTrait; +use Zend\ProblemDetails\Exception\ProblemDetailsExceptionInterface; use function Functional\reduce_left; use function is_array; @@ -14,8 +17,13 @@ use function sprintf; use const PHP_EOL; -class ValidationException extends RuntimeException +class ValidationException extends RuntimeException implements ProblemDetailsExceptionInterface { + use CommonProblemDetailsExceptionTrait; + + private const TITLE = 'Invalid data'; + public const TYPE = 'INVALID_ARGUMENT'; + /** @var array */ private $invalidElements; @@ -36,7 +44,18 @@ class ValidationException extends RuntimeException public static function fromArray(array $invalidData, ?Throwable $prev = null): self { - return new self('Provided data is not valid', $invalidData, -1, $prev); + $status = StatusCodeInterface::STATUS_BAD_REQUEST; + $e = new self('Provided data is not valid', $invalidData, $status, $prev); + + $e->detail = $e->getMessage(); + $e->title = self::TITLE; + $e->type = self::TYPE; + $e->status = StatusCodeInterface::STATUS_BAD_REQUEST; + $e->additional = [ + 'invalidElements' => $invalidData, + ]; + + return $e; } public function getInvalidElements(): array diff --git a/module/Core/test/Exception/ValidationExceptionTest.php b/module/Core/test/Exception/ValidationExceptionTest.php index bd7855e2..eb47caed 100644 --- a/module/Core/test/Exception/ValidationExceptionTest.php +++ b/module/Core/test/Exception/ValidationExceptionTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace ShlinkioTest\Shlink\Core\Exception; +use Fig\Http\Message\StatusCodeInterface; use LogicException; use PHPUnit\Framework\TestCase; use RuntimeException; @@ -67,7 +68,7 @@ EOT; $this->assertEquals($invalidData, $e->getInvalidElements()); $this->assertEquals('Provided data is not valid', $e->getMessage()); - $this->assertEquals(-1, $e->getCode()); + $this->assertEquals(StatusCodeInterface::STATUS_BAD_REQUEST, $e->getCode()); $this->assertEquals($prev, $e->getPrevious()); $this->assertStringContainsString($expectedStringRepresentation, (string) $e); $getMessages->shouldHaveBeenCalledOnce(); diff --git a/module/Rest/src/Action/ShortUrl/AbstractCreateShortUrlAction.php b/module/Rest/src/Action/ShortUrl/AbstractCreateShortUrlAction.php index 38daf905..af392067 100644 --- a/module/Rest/src/Action/ShortUrl/AbstractCreateShortUrlAction.php +++ b/module/Rest/src/Action/ShortUrl/AbstractCreateShortUrlAction.php @@ -12,7 +12,6 @@ use Shlinkio\Shlink\Core\Model\CreateShortUrlData; use Shlinkio\Shlink\Core\Service\UrlShortenerInterface; use Shlinkio\Shlink\Core\Transformer\ShortUrlDataTransformer; use Shlinkio\Shlink\Rest\Action\AbstractRestAction; -use Shlinkio\Shlink\Rest\Util\RestUtils; use Zend\Diactoros\Response\JsonResponse; abstract class AbstractCreateShortUrlAction extends AbstractRestAction @@ -32,22 +31,9 @@ abstract class AbstractCreateShortUrlAction extends AbstractRestAction $this->domainConfig = $domainConfig; } - /** - * @param Request $request - * @return Response - */ public function handle(Request $request): Response { - try { - $shortUrlData = $this->buildShortUrlData($request); - } catch (ValidationException $e) { - $this->logger->warning('Provided data is invalid. {e}', ['e' => $e]); - return new JsonResponse([ - 'error' => RestUtils::INVALID_ARGUMENT_ERROR, - 'message' => $e->getMessage(), - ], self::STATUS_BAD_REQUEST); - } - + $shortUrlData = $this->buildShortUrlData($request); $longUrl = $shortUrlData->getLongUrl(); $tags = $shortUrlData->getTags(); $shortUrlMeta = $shortUrlData->getMeta(); diff --git a/module/Rest/src/Action/ShortUrl/EditShortUrlAction.php b/module/Rest/src/Action/ShortUrl/EditShortUrlAction.php index 8391db8a..33796f7d 100644 --- a/module/Rest/src/Action/ShortUrl/EditShortUrlAction.php +++ b/module/Rest/src/Action/ShortUrl/EditShortUrlAction.php @@ -7,13 +7,10 @@ namespace Shlinkio\Shlink\Rest\Action\ShortUrl; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Log\LoggerInterface; -use Shlinkio\Shlink\Core\Exception; use Shlinkio\Shlink\Core\Model\ShortUrlMeta; use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface; use Shlinkio\Shlink\Rest\Action\AbstractRestAction; -use Shlinkio\Shlink\Rest\Util\RestUtils; use Zend\Diactoros\Response\EmptyResponse; -use Zend\Diactoros\Response\JsonResponse; class EditShortUrlAction extends AbstractRestAction { @@ -29,32 +26,12 @@ class EditShortUrlAction extends AbstractRestAction $this->shortUrlService = $shortUrlService; } - /** - * Process an incoming server request and return a response, optionally delegating - * to the next middleware component to create the response. - * - * @param ServerRequestInterface $request - * - * @return ResponseInterface - * @throws \InvalidArgumentException - */ public function handle(ServerRequestInterface $request): ResponseInterface { $postData = (array) $request->getParsedBody(); $shortCode = $request->getAttribute('shortCode', ''); - try { - $this->shortUrlService->updateMetadataByShortCode( - $shortCode, - ShortUrlMeta::createFromRawData($postData) - ); - return new EmptyResponse(); - } catch (Exception\ValidationException $e) { - $this->logger->warning('Provided data is invalid. {e}', ['e' => $e]); - return new JsonResponse([ - 'error' => RestUtils::getRestErrorCodeFromException($e), - 'message' => 'Provided data is invalid.', - ], self::STATUS_BAD_REQUEST); - } + $this->shortUrlService->updateMetadataByShortCode($shortCode, ShortUrlMeta::createFromRawData($postData)); + return new EmptyResponse(); } } diff --git a/module/Rest/src/Util/RestUtils.php b/module/Rest/src/Util/RestUtils.php index 24398eb9..096db51a 100644 --- a/module/Rest/src/Util/RestUtils.php +++ b/module/Rest/src/Util/RestUtils.php @@ -6,19 +6,19 @@ namespace Shlinkio\Shlink\Rest\Util; use Shlinkio\Shlink\Common\Exception as Common; use Shlinkio\Shlink\Core\Exception as Core; -use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Rest\Exception as Rest; use Throwable; class RestUtils { /** @deprecated */ - public const INVALID_SHORTCODE_ERROR = ShortUrlNotFoundException::TYPE; + public const INVALID_SHORTCODE_ERROR = Core\ShortUrlNotFoundException::TYPE; // FIXME Should be INVALID_SHORT_URL_DELETION public const INVALID_SHORTCODE_DELETION_ERROR = 'INVALID_SHORTCODE_DELETION'; /** @deprecated */ public const INVALID_URL_ERROR = Core\InvalidUrlException::TYPE; - public const INVALID_ARGUMENT_ERROR = 'INVALID_ARGUMENT'; + /** @deprecated */ + public const INVALID_ARGUMENT_ERROR = Core\ValidationException::TYPE; /** @deprecated */ public const INVALID_SLUG_ERROR = Core\NonUniqueSlugException::TYPE; public const INVALID_CREDENTIALS_ERROR = 'INVALID_CREDENTIALS'; diff --git a/module/Rest/test/Action/ShortUrl/CreateShortUrlActionTest.php b/module/Rest/test/Action/ShortUrl/CreateShortUrlActionTest.php index c4f8970d..92a3c2aa 100644 --- a/module/Rest/test/Action/ShortUrl/CreateShortUrlActionTest.php +++ b/module/Rest/test/Action/ShortUrl/CreateShortUrlActionTest.php @@ -8,10 +8,9 @@ use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\Prophecy\ObjectProphecy; use Shlinkio\Shlink\Core\Entity\ShortUrl; +use Shlinkio\Shlink\Core\Exception\ValidationException; use Shlinkio\Shlink\Core\Service\UrlShortener; use Shlinkio\Shlink\Rest\Action\ShortUrl\CreateShortUrlAction; -use Shlinkio\Shlink\Rest\Util\RestUtils; -use Zend\Diactoros\Response\JsonResponse; use Zend\Diactoros\ServerRequest; use Zend\Diactoros\Uri; @@ -38,8 +37,8 @@ class CreateShortUrlActionTest extends TestCase /** @test */ public function missingLongUrlParamReturnsError(): void { - $response = $this->action->handle(new ServerRequest()); - $this->assertEquals(400, $response->getStatusCode()); + $this->expectException(ValidationException::class); + $this->action->handle(new ServerRequest()); } /** @test */ @@ -71,13 +70,11 @@ class CreateShortUrlActionTest extends TestCase 'longUrl' => 'http://www.domain.com/foo/bar', 'domain' => $domain, ]); - /** @var JsonResponse $response */ - $response = $this->action->handle($request); - $payload = $response->getPayload(); - $this->assertEquals(400, $response->getStatusCode()); - $this->assertEquals(RestUtils::INVALID_ARGUMENT_ERROR, $payload['error']); - $urlToShortCode->shouldNotHaveBeenCalled(); + $this->expectException(ValidationException::class); + $urlToShortCode->shouldNotBeCalled(); + + $this->action->handle($request); } public function provideInvalidDomains(): iterable diff --git a/module/Rest/test/Action/ShortUrl/EditShortUrlActionTest.php b/module/Rest/test/Action/ShortUrl/EditShortUrlActionTest.php index 55fcc2d7..63aa1ffb 100644 --- a/module/Rest/test/Action/ShortUrl/EditShortUrlActionTest.php +++ b/module/Rest/test/Action/ShortUrl/EditShortUrlActionTest.php @@ -8,6 +8,7 @@ use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\Prophecy\ObjectProphecy; use Shlinkio\Shlink\Core\Entity\ShortUrl; +use Shlinkio\Shlink\Core\Exception\ValidationException; use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface; use Shlinkio\Shlink\Rest\Action\ShortUrl\EditShortUrlAction; use Shlinkio\Shlink\Rest\Util\RestUtils; @@ -28,19 +29,15 @@ class EditShortUrlActionTest extends TestCase } /** @test */ - public function invalidDataReturnsError(): void + public function invalidDataThrowsError(): void { $request = (new ServerRequest())->withParsedBody([ 'maxVisits' => 'invalid', ]); - /** @var JsonResponse $resp */ - $resp = $this->action->handle($request); - $payload = $resp->getPayload(); + $this->expectException(ValidationException::class); - $this->assertEquals(400, $resp->getStatusCode()); - $this->assertEquals(RestUtils::INVALID_ARGUMENT_ERROR, $payload['error']); - $this->assertEquals('Provided data is invalid.', $payload['message']); + $this->action->handle($request); } /** @test */ diff --git a/module/Rest/test/Action/ShortUrl/SingleStepCreateShortUrlActionTest.php b/module/Rest/test/Action/ShortUrl/SingleStepCreateShortUrlActionTest.php index 8bebc6c5..b0d8c65d 100644 --- a/module/Rest/test/Action/ShortUrl/SingleStepCreateShortUrlActionTest.php +++ b/module/Rest/test/Action/ShortUrl/SingleStepCreateShortUrlActionTest.php @@ -10,11 +10,11 @@ use Prophecy\Argument; use Prophecy\Prophecy\ObjectProphecy; use Psr\Http\Message\UriInterface; use Shlinkio\Shlink\Core\Entity\ShortUrl; +use Shlinkio\Shlink\Core\Exception\ValidationException; use Shlinkio\Shlink\Core\Model\ShortUrlMeta; use Shlinkio\Shlink\Core\Service\UrlShortenerInterface; use Shlinkio\Shlink\Rest\Action\ShortUrl\SingleStepCreateShortUrlAction; use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface; -use Zend\Diactoros\Response\JsonResponse; use Zend\Diactoros\ServerRequest; class SingleStepCreateShortUrlActionTest extends TestCase @@ -47,14 +47,10 @@ class SingleStepCreateShortUrlActionTest extends TestCase $request = (new ServerRequest())->withQueryParams(['apiKey' => 'abc123']); $findApiKey = $this->apiKeyService->check('abc123')->willReturn(false); - /** @var JsonResponse $resp */ - $resp = $this->action->handle($request); - $payload = $resp->getPayload(); + $this->expectException(ValidationException::class); + $findApiKey->shouldBeCalledOnce(); - $this->assertEquals(400, $resp->getStatusCode()); - $this->assertEquals('INVALID_ARGUMENT', $payload['error']); - $this->assertEquals('Provided data is not valid', $payload['message']); - $findApiKey->shouldHaveBeenCalled(); + $this->action->handle($request); } /** @test */ @@ -63,14 +59,10 @@ class SingleStepCreateShortUrlActionTest extends TestCase $request = (new ServerRequest())->withQueryParams(['apiKey' => 'abc123']); $findApiKey = $this->apiKeyService->check('abc123')->willReturn(true); - /** @var JsonResponse $resp */ - $resp = $this->action->handle($request); - $payload = $resp->getPayload(); + $this->expectException(ValidationException::class); + $findApiKey->shouldBeCalledOnce(); - $this->assertEquals(400, $resp->getStatusCode()); - $this->assertEquals('INVALID_ARGUMENT', $payload['error']); - $this->assertEquals('Provided data is not valid', $payload['message']); - $findApiKey->shouldHaveBeenCalled(); + $this->action->handle($request); } /** @test */ From 310032e3035dc90c664955a22e1058863eb38d46 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 24 Nov 2019 23:56:02 +0100 Subject: [PATCH 24/40] Converted DeleteShortUrlException into a problem details exception --- .../src/Exception/DeleteShortUrlException.php | 20 +++++++++++-- .../Action/ShortUrl/DeleteShortUrlAction.php | 24 ++------------- module/Rest/src/Util/RestUtils.php | 4 +-- .../Action/DeleteShortUrlActionTest.php | 4 +-- .../ShortUrl/DeleteShortUrlActionTest.php | 30 ------------------- 5 files changed, 24 insertions(+), 58 deletions(-) diff --git a/module/Core/src/Exception/DeleteShortUrlException.php b/module/Core/src/Exception/DeleteShortUrlException.php index c1fd6dd7..037fe942 100644 --- a/module/Core/src/Exception/DeleteShortUrlException.php +++ b/module/Core/src/Exception/DeleteShortUrlException.php @@ -4,12 +4,20 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Exception; +use Fig\Http\Message\StatusCodeInterface; use Throwable; +use Zend\ProblemDetails\Exception\CommonProblemDetailsExceptionTrait; +use Zend\ProblemDetails\Exception\ProblemDetailsExceptionInterface; use function sprintf; -class DeleteShortUrlException extends RuntimeException +class DeleteShortUrlException extends DomainException implements ProblemDetailsExceptionInterface { + use CommonProblemDetailsExceptionTrait; + + private const TITLE = 'Cannot delete short URL'; + public const TYPE = 'INVALID_SHORTCODE_DELETION'; // FIXME Should be INVALID_SHORT_URL_DELETION + /** @var int */ private $visitsThreshold; @@ -21,11 +29,19 @@ class DeleteShortUrlException extends RuntimeException public static function fromVisitsThreshold(int $threshold, string $shortCode): self { - return new self($threshold, sprintf( + $e = new self($threshold, sprintf( 'Impossible to delete short URL with short code "%s" since it has more than "%s" visits.', $shortCode, $threshold )); + + $e->detail = $e->getMessage(); + $e->title = self::TITLE; + $e->type = self::TYPE; + $e->status = StatusCodeInterface::STATUS_UNPROCESSABLE_ENTITY; + $e->additional = ['threshold' => $threshold]; + + return $e; } public function getVisitsThreshold(): int diff --git a/module/Rest/src/Action/ShortUrl/DeleteShortUrlAction.php b/module/Rest/src/Action/ShortUrl/DeleteShortUrlAction.php index f6873025..ba39ec82 100644 --- a/module/Rest/src/Action/ShortUrl/DeleteShortUrlAction.php +++ b/module/Rest/src/Action/ShortUrl/DeleteShortUrlAction.php @@ -7,14 +7,9 @@ namespace Shlinkio\Shlink\Rest\Action\ShortUrl; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Log\LoggerInterface; -use Shlinkio\Shlink\Core\Exception; use Shlinkio\Shlink\Core\Service\ShortUrl\DeleteShortUrlServiceInterface; use Shlinkio\Shlink\Rest\Action\AbstractRestAction; -use Shlinkio\Shlink\Rest\Util\RestUtils; use Zend\Diactoros\Response\EmptyResponse; -use Zend\Diactoros\Response\JsonResponse; - -use function sprintf; class DeleteShortUrlAction extends AbstractRestAction { @@ -30,25 +25,10 @@ class DeleteShortUrlAction extends AbstractRestAction $this->deleteShortUrlService = $deleteShortUrlService; } - /** - * Handle the request and return a response. - */ public function handle(ServerRequestInterface $request): ResponseInterface { $shortCode = $request->getAttribute('shortCode', ''); - - try { - $this->deleteShortUrlService->deleteByShortCode($shortCode); - return new EmptyResponse(); - } catch (Exception\DeleteShortUrlException $e) { - $this->logger->warning('Provided data is invalid. {e}', ['e' => $e]); - $messagePlaceholder = - 'It is not possible to delete URL with short code "%s" because it has reached more than "%s" visits.'; - - return new JsonResponse([ - 'error' => RestUtils::getRestErrorCodeFromException($e), - 'message' => sprintf($messagePlaceholder, $shortCode, $e->getVisitsThreshold()), - ], self::STATUS_BAD_REQUEST); - } + $this->deleteShortUrlService->deleteByShortCode($shortCode); + return new EmptyResponse(); } } diff --git a/module/Rest/src/Util/RestUtils.php b/module/Rest/src/Util/RestUtils.php index 096db51a..97ca0d0b 100644 --- a/module/Rest/src/Util/RestUtils.php +++ b/module/Rest/src/Util/RestUtils.php @@ -13,8 +13,8 @@ class RestUtils { /** @deprecated */ public const INVALID_SHORTCODE_ERROR = Core\ShortUrlNotFoundException::TYPE; - // FIXME Should be INVALID_SHORT_URL_DELETION - public const INVALID_SHORTCODE_DELETION_ERROR = 'INVALID_SHORTCODE_DELETION'; + /** @deprecated */ + public const INVALID_SHORTCODE_DELETION_ERROR = Core\DeleteShortUrlException::TYPE; /** @deprecated */ public const INVALID_URL_ERROR = Core\InvalidUrlException::TYPE; /** @deprecated */ diff --git a/module/Rest/test-api/Action/DeleteShortUrlActionTest.php b/module/Rest/test-api/Action/DeleteShortUrlActionTest.php index c90b7818..042b8e90 100644 --- a/module/Rest/test-api/Action/DeleteShortUrlActionTest.php +++ b/module/Rest/test-api/Action/DeleteShortUrlActionTest.php @@ -20,7 +20,7 @@ class DeleteShortUrlActionTest extends ApiTestCase } /** @test */ - public function badRequestIsReturnedWhenTryingToDeleteUrlWithTooManyVisits(): void + public function unprocessableEntityIsReturnedWhenTryingToDeleteUrlWithTooManyVisits(): void { // Generate visits first for ($i = 0; $i < 20; $i++) { @@ -30,7 +30,7 @@ class DeleteShortUrlActionTest extends ApiTestCase $resp = $this->callApiWithKey(self::METHOD_DELETE, '/short-urls/abc123'); ['error' => $error] = $this->getJsonResponsePayload($resp); - $this->assertEquals(self::STATUS_BAD_REQUEST, $resp->getStatusCode()); + $this->assertEquals(self::STATUS_UNPROCESSABLE_ENTITY, $resp->getStatusCode()); $this->assertEquals(RestUtils::INVALID_SHORTCODE_DELETION_ERROR, $error); } } diff --git a/module/Rest/test/Action/ShortUrl/DeleteShortUrlActionTest.php b/module/Rest/test/Action/ShortUrl/DeleteShortUrlActionTest.php index 2e77d10a..782181ce 100644 --- a/module/Rest/test/Action/ShortUrl/DeleteShortUrlActionTest.php +++ b/module/Rest/test/Action/ShortUrl/DeleteShortUrlActionTest.php @@ -7,12 +7,8 @@ namespace ShlinkioTest\Shlink\Rest\Action\ShortUrl; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\Prophecy\ObjectProphecy; -use Shlinkio\Shlink\Core\Exception; use Shlinkio\Shlink\Core\Service\ShortUrl\DeleteShortUrlServiceInterface; use Shlinkio\Shlink\Rest\Action\ShortUrl\DeleteShortUrlAction; -use Shlinkio\Shlink\Rest\Util\RestUtils; -use Throwable; -use Zend\Diactoros\Response\JsonResponse; use Zend\Diactoros\ServerRequest; class DeleteShortUrlActionTest extends TestCase @@ -39,30 +35,4 @@ class DeleteShortUrlActionTest extends TestCase $this->assertEquals(204, $resp->getStatusCode()); $deleteByShortCode->shouldHaveBeenCalledOnce(); } - - /** - * @test - * @dataProvider provideExceptions - */ - public function returnsErrorResponseInCaseOfException(Throwable $e, string $error, int $statusCode): void - { - $deleteByShortCode = $this->service->deleteByShortCode(Argument::any())->willThrow($e); - - /** @var JsonResponse $resp */ - $resp = $this->action->handle(new ServerRequest()); - $payload = $resp->getPayload(); - - $this->assertEquals($statusCode, $resp->getStatusCode()); - $this->assertEquals($error, $payload['error']); - $deleteByShortCode->shouldHaveBeenCalledOnce(); - } - - public function provideExceptions(): iterable - { - yield 'bad request' => [ - new Exception\DeleteShortUrlException(5), - RestUtils::INVALID_SHORTCODE_DELETION_ERROR, - 400, - ]; - } } From 0c5eec7e95b7a7d6a817dd73316343f2d7281d47 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 25 Nov 2019 18:54:25 +0100 Subject: [PATCH 25/40] Replaced the use of EntityDoesNotExistException by ShorturlNotFoundException where applicable --- .../Command/ShortUrl/ResolveUrlCommand.php | 4 -- .../ShortUrl/ResolveUrlCommandTest.php | 19 ++-------- .../src/Action/AbstractTrackingAction.php | 3 +- module/Core/src/Action/PreviewAction.php | 3 +- module/Core/src/Action/QrCodeAction.php | 3 +- .../src/Exception/NonUniqueSlugException.php | 6 +-- .../Exception/ShortUrlNotFoundException.php | 5 ++- module/Core/src/Service/UrlShortener.php | 9 ++--- .../src/Service/UrlShortenerInterface.php | 6 +-- module/Core/test/Action/PreviewActionTest.php | 14 ------- module/Core/test/Action/QrCodeActionTest.php | 3 +- .../Core/test/Action/RedirectActionTest.php | 4 +- .../InvalidShortCodeExceptionTest.php | 19 ---------- .../ShortUrlNotFoundExceptionTest.php | 38 +++++++++++++++++++ .../Action/ShortUrl/ResolveShortUrlAction.php | 16 +------- .../Action/ResolveShortUrlActionTest.php | 2 +- .../ShortUrl/ResolveShortUrlActionTest.php | 15 -------- 17 files changed, 60 insertions(+), 109 deletions(-) delete mode 100644 module/Core/test/Exception/InvalidShortCodeExceptionTest.php create mode 100644 module/Core/test/Exception/ShortUrlNotFoundExceptionTest.php diff --git a/module/CLI/src/Command/ShortUrl/ResolveUrlCommand.php b/module/CLI/src/Command/ShortUrl/ResolveUrlCommand.php index b758ddd4..28564369 100644 --- a/module/CLI/src/Command/ShortUrl/ResolveUrlCommand.php +++ b/module/CLI/src/Command/ShortUrl/ResolveUrlCommand.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace Shlinkio\Shlink\CLI\Command\ShortUrl; use Shlinkio\Shlink\CLI\Util\ExitCodes; -use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Service\UrlShortenerInterface; use Symfony\Component\Console\Command\Command; @@ -66,9 +65,6 @@ class ResolveUrlCommand extends Command $output->writeln(sprintf('Long URL: %s', $url->getLongUrl())); return ExitCodes::EXIT_SUCCESS; } catch (ShortUrlNotFoundException $e) { - $io->error(sprintf('Provided short code "%s" has an invalid format.', $shortCode)); - return ExitCodes::EXIT_FAILURE; - } catch (EntityDoesNotExistException $e) { $io->error(sprintf('Provided short code "%s" could not be found.', $shortCode)); return ExitCodes::EXIT_FAILURE; } diff --git a/module/CLI/test/Command/ShortUrl/ResolveUrlCommandTest.php b/module/CLI/test/Command/ShortUrl/ResolveUrlCommandTest.php index 9e86bbd1..23b4ec28 100644 --- a/module/CLI/test/Command/ShortUrl/ResolveUrlCommandTest.php +++ b/module/CLI/test/Command/ShortUrl/ResolveUrlCommandTest.php @@ -8,12 +8,13 @@ use PHPUnit\Framework\TestCase; use Prophecy\Prophecy\ObjectProphecy; use Shlinkio\Shlink\CLI\Command\ShortUrl\ResolveUrlCommand; use Shlinkio\Shlink\Core\Entity\ShortUrl; -use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Service\UrlShortener; use Symfony\Component\Console\Application; use Symfony\Component\Console\Tester\CommandTester; +use function sprintf; + use const PHP_EOL; class ResolveUrlCommandTest extends TestCase @@ -51,23 +52,11 @@ class ResolveUrlCommandTest extends TestCase public function incorrectShortCodeOutputsErrorMessage(): void { $shortCode = 'abc123'; - $this->urlShortener->shortCodeToUrl($shortCode, null)->willThrow(EntityDoesNotExistException::class) + $this->urlShortener->shortCodeToUrl($shortCode, null)->willThrow(ShortUrlNotFoundException::class) ->shouldBeCalledOnce(); $this->commandTester->execute(['shortCode' => $shortCode]); $output = $this->commandTester->getDisplay(); - $this->assertStringContainsString('Provided short code "' . $shortCode . '" could not be found.', $output); - } - - /** @test */ - public function wrongShortCodeFormatOutputsErrorMessage(): void - { - $shortCode = 'abc123'; - $this->urlShortener->shortCodeToUrl($shortCode, null)->willThrow(new ShortUrlNotFoundException()) - ->shouldBeCalledOnce(); - - $this->commandTester->execute(['shortCode' => $shortCode]); - $output = $this->commandTester->getDisplay(); - $this->assertStringContainsString('Provided short code "' . $shortCode . '" has an invalid format.', $output); + $this->assertStringContainsString(sprintf('Provided short code "%s" could not be found', $shortCode), $output); } } diff --git a/module/Core/src/Action/AbstractTrackingAction.php b/module/Core/src/Action/AbstractTrackingAction.php index 5bf195f1..ff8d91f2 100644 --- a/module/Core/src/Action/AbstractTrackingAction.php +++ b/module/Core/src/Action/AbstractTrackingAction.php @@ -11,7 +11,6 @@ use Psr\Http\Server\RequestHandlerInterface; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use Shlinkio\Shlink\Core\Entity\ShortUrl; -use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Model\Visitor; use Shlinkio\Shlink\Core\Options\AppOptions; @@ -72,7 +71,7 @@ abstract class AbstractTrackingAction implements MiddlewareInterface } return $this->createSuccessResp($this->buildUrlToRedirectTo($url, $query, $disableTrackParam)); - } catch (ShortUrlNotFoundException | EntityDoesNotExistException $e) { + } catch (ShortUrlNotFoundException $e) { $this->logger->warning('An error occurred while tracking short code. {e}', ['e' => $e]); return $this->createErrorResp($request, $handler); } diff --git a/module/Core/src/Action/PreviewAction.php b/module/Core/src/Action/PreviewAction.php index 9ba0eaf5..d243f12c 100644 --- a/module/Core/src/Action/PreviewAction.php +++ b/module/Core/src/Action/PreviewAction.php @@ -11,7 +11,6 @@ use Psr\Http\Server\RequestHandlerInterface; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use Shlinkio\Shlink\Common\Response\ResponseUtilsTrait; -use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Service\UrlShortenerInterface; use Shlinkio\Shlink\PreviewGenerator\Exception\PreviewGenerationException; @@ -56,7 +55,7 @@ class PreviewAction implements MiddlewareInterface $url = $this->urlShortener->shortCodeToUrl($shortCode); $imagePath = $this->previewGenerator->generatePreview($url->getLongUrl()); return $this->generateImageResponse($imagePath); - } catch (ShortUrlNotFoundException | EntityDoesNotExistException | PreviewGenerationException $e) { + } catch (ShortUrlNotFoundException | PreviewGenerationException $e) { $this->logger->warning('An error occurred while generating preview image. {e}', ['e' => $e]); return $handler->handle($request); } diff --git a/module/Core/src/Action/QrCodeAction.php b/module/Core/src/Action/QrCodeAction.php index 1c5c08df..3cdee70e 100644 --- a/module/Core/src/Action/QrCodeAction.php +++ b/module/Core/src/Action/QrCodeAction.php @@ -12,7 +12,6 @@ use Psr\Http\Server\RequestHandlerInterface; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use Shlinkio\Shlink\Common\Response\QrCodeResponse; -use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Service\UrlShortenerInterface; use Zend\Expressive\Router\Exception\RuntimeException; @@ -60,7 +59,7 @@ class QrCodeAction implements MiddlewareInterface try { $this->urlShortener->shortCodeToUrl($shortCode, $domain); - } catch (ShortUrlNotFoundException | EntityDoesNotExistException $e) { + } catch (ShortUrlNotFoundException $e) { $this->logger->warning('An error occurred while creating QR code. {e}', ['e' => $e]); return $handler->handle($request); } diff --git a/module/Core/src/Exception/NonUniqueSlugException.php b/module/Core/src/Exception/NonUniqueSlugException.php index 9e22d354..74e03f06 100644 --- a/module/Core/src/Exception/NonUniqueSlugException.php +++ b/module/Core/src/Exception/NonUniqueSlugException.php @@ -19,11 +19,7 @@ class NonUniqueSlugException extends InvalidArgumentException implements Problem public static function fromSlug(string $slug, ?string $domain): self { - $suffix = ''; - if ($domain !== null) { - $suffix = sprintf(' for domain "%s"', $domain); - } - + $suffix = $domain === null ? '' : sprintf(' for domain "%s"', $domain); $e = new self(sprintf('Provided slug "%s" is already in use%s.', $slug, $suffix)); $e->detail = $e->getMessage(); diff --git a/module/Core/src/Exception/ShortUrlNotFoundException.php b/module/Core/src/Exception/ShortUrlNotFoundException.php index d8682047..e07624c7 100644 --- a/module/Core/src/Exception/ShortUrlNotFoundException.php +++ b/module/Core/src/Exception/ShortUrlNotFoundException.php @@ -17,9 +17,10 @@ class ShortUrlNotFoundException extends DomainException implements ProblemDetail private const TITLE = 'Short URL not found'; public const TYPE = 'INVALID_SHORTCODE'; - public static function fromNotFoundShortCode(string $shortCode): self + public static function fromNotFoundShortCode(string $shortCode, ?string $domain = null): self { - $e = new self(sprintf('No URL found for short code "%s"', $shortCode)); + $suffix = $domain === null ? '' : sprintf(' for domain "%s"', $domain); + $e = new self(sprintf('No URL found with short code "%s"%s', $shortCode, $suffix)); $e->detail = $e->getMessage(); $e->title = self::TITLE; diff --git a/module/Core/src/Service/UrlShortener.php b/module/Core/src/Service/UrlShortener.php index 7d71ca7a..0e4062ec 100644 --- a/module/Core/src/Service/UrlShortener.php +++ b/module/Core/src/Service/UrlShortener.php @@ -8,9 +8,9 @@ use Doctrine\ORM\EntityManagerInterface; use Psr\Http\Message\UriInterface; use Shlinkio\Shlink\Core\Domain\Resolver\PersistenceDomainResolver; use Shlinkio\Shlink\Core\Entity\ShortUrl; -use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; use Shlinkio\Shlink\Core\Exception\InvalidUrlException; use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException; +use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Model\ShortUrlMeta; use Shlinkio\Shlink\Core\Options\UrlShortenerOptions; use Shlinkio\Shlink\Core\Repository\ShortUrlRepository; @@ -129,7 +129,7 @@ class UrlShortener implements UrlShortenerInterface } /** - * @throws EntityDoesNotExistException + * @throws ShortUrlNotFoundException */ public function shortCodeToUrl(string $shortCode, ?string $domain = null): ShortUrl { @@ -137,10 +137,7 @@ class UrlShortener implements UrlShortenerInterface $shortUrlRepo = $this->em->getRepository(ShortUrl::class); $shortUrl = $shortUrlRepo->findOneByShortCode($shortCode, $domain); if ($shortUrl === null) { - throw EntityDoesNotExistException::createFromEntityAndConditions(ShortUrl::class, [ - 'shortCode' => $shortCode, - 'domain' => $domain, - ]); + throw ShortUrlNotFoundException::fromNotFoundShortCode($shortCode, $domain); } return $shortUrl; diff --git a/module/Core/src/Service/UrlShortenerInterface.php b/module/Core/src/Service/UrlShortenerInterface.php index 9d6b291b..ee9cc343 100644 --- a/module/Core/src/Service/UrlShortenerInterface.php +++ b/module/Core/src/Service/UrlShortenerInterface.php @@ -6,10 +6,9 @@ namespace Shlinkio\Shlink\Core\Service; use Psr\Http\Message\UriInterface; use Shlinkio\Shlink\Core\Entity\ShortUrl; -use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; use Shlinkio\Shlink\Core\Exception\InvalidUrlException; use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException; -use Shlinkio\Shlink\Core\Exception\RuntimeException; +use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Model\ShortUrlMeta; interface UrlShortenerInterface @@ -18,12 +17,11 @@ interface UrlShortenerInterface * @param string[] $tags * @throws NonUniqueSlugException * @throws InvalidUrlException - * @throws RuntimeException */ public function urlToShortCode(UriInterface $url, array $tags, ShortUrlMeta $meta): ShortUrl; /** - * @throws EntityDoesNotExistException + * @throws ShortUrlNotFoundException */ public function shortCodeToUrl(string $shortCode, ?string $domain = null): ShortUrl; } diff --git a/module/Core/test/Action/PreviewActionTest.php b/module/Core/test/Action/PreviewActionTest.php index 909d70b7..e2cb089c 100644 --- a/module/Core/test/Action/PreviewActionTest.php +++ b/module/Core/test/Action/PreviewActionTest.php @@ -11,7 +11,6 @@ use Prophecy\Prophecy\ObjectProphecy; use Psr\Http\Server\RequestHandlerInterface; use Shlinkio\Shlink\Core\Action\PreviewAction; use Shlinkio\Shlink\Core\Entity\ShortUrl; -use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Service\UrlShortener; use Shlinkio\Shlink\PreviewGenerator\Service\PreviewGenerator; @@ -38,19 +37,6 @@ class PreviewActionTest extends TestCase $this->action = new PreviewAction($this->previewGenerator->reveal(), $this->urlShortener->reveal()); } - /** @test */ - public function invalidShortCodeFallsBackToNextMiddleware(): void - { - $shortCode = 'abc123'; - $this->urlShortener->shortCodeToUrl($shortCode)->willThrow(EntityDoesNotExistException::class) - ->shouldBeCalledOnce(); - $delegate = $this->prophesize(RequestHandlerInterface::class); - $delegate->handle(Argument::cetera())->shouldBeCalledOnce() - ->willReturn(new Response()); - - $this->action->process((new ServerRequest())->withAttribute('shortCode', $shortCode), $delegate->reveal()); - } - /** @test */ public function correctShortCodeReturnsImageResponse(): void { diff --git a/module/Core/test/Action/QrCodeActionTest.php b/module/Core/test/Action/QrCodeActionTest.php index 24fe8c4b..6327ad69 100644 --- a/module/Core/test/Action/QrCodeActionTest.php +++ b/module/Core/test/Action/QrCodeActionTest.php @@ -11,7 +11,6 @@ use Psr\Http\Server\RequestHandlerInterface; use Shlinkio\Shlink\Common\Response\QrCodeResponse; use Shlinkio\Shlink\Core\Action\QrCodeAction; use Shlinkio\Shlink\Core\Entity\ShortUrl; -use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Service\UrlShortener; use Zend\Diactoros\Response; @@ -39,7 +38,7 @@ class QrCodeActionTest extends TestCase public function aNotFoundShortCodeWillDelegateIntoNextMiddleware(): void { $shortCode = 'abc123'; - $this->urlShortener->shortCodeToUrl($shortCode, '')->willThrow(EntityDoesNotExistException::class) + $this->urlShortener->shortCodeToUrl($shortCode, '')->willThrow(ShortUrlNotFoundException::class) ->shouldBeCalledOnce(); $delegate = $this->prophesize(RequestHandlerInterface::class); $process = $delegate->handle(Argument::any())->willReturn(new Response()); diff --git a/module/Core/test/Action/RedirectActionTest.php b/module/Core/test/Action/RedirectActionTest.php index a65927eb..55342cb5 100644 --- a/module/Core/test/Action/RedirectActionTest.php +++ b/module/Core/test/Action/RedirectActionTest.php @@ -10,7 +10,7 @@ use Prophecy\Prophecy\ObjectProphecy; use Psr\Http\Server\RequestHandlerInterface; use Shlinkio\Shlink\Core\Action\RedirectAction; use Shlinkio\Shlink\Core\Entity\ShortUrl; -use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; +use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Options; use Shlinkio\Shlink\Core\Service\UrlShortener; use Shlinkio\Shlink\Core\Service\VisitsTracker; @@ -76,7 +76,7 @@ class RedirectActionTest extends TestCase public function nextMiddlewareIsInvokedIfLongUrlIsNotFound(): void { $shortCode = 'abc123'; - $this->urlShortener->shortCodeToUrl($shortCode, '')->willThrow(EntityDoesNotExistException::class) + $this->urlShortener->shortCodeToUrl($shortCode, '')->willThrow(ShortUrlNotFoundException::class) ->shouldBeCalledOnce(); $this->visitTracker->track(Argument::cetera())->shouldNotBeCalled(); diff --git a/module/Core/test/Exception/InvalidShortCodeExceptionTest.php b/module/Core/test/Exception/InvalidShortCodeExceptionTest.php deleted file mode 100644 index c1815db4..00000000 --- a/module/Core/test/Exception/InvalidShortCodeExceptionTest.php +++ /dev/null @@ -1,19 +0,0 @@ -assertEquals('No URL found for short code "abc123"', $e->getMessage()); - } -} diff --git a/module/Core/test/Exception/ShortUrlNotFoundExceptionTest.php b/module/Core/test/Exception/ShortUrlNotFoundExceptionTest.php new file mode 100644 index 00000000..6f0d58f4 --- /dev/null +++ b/module/Core/test/Exception/ShortUrlNotFoundExceptionTest.php @@ -0,0 +1,38 @@ +assertEquals($expectedMessage, $e->getMessage()); + } + + public function provideMessages(): iterable + { + yield 'without domain' => [ + 'No URL found with short code "abc123"', + 'abc123', + null, + ]; + yield 'with domain' => [ + 'No URL found with short code "bar" for domain "foo"', + 'bar', + 'foo', + ]; + } +} diff --git a/module/Rest/src/Action/ShortUrl/ResolveShortUrlAction.php b/module/Rest/src/Action/ShortUrl/ResolveShortUrlAction.php index 9599013a..e041f4dc 100644 --- a/module/Rest/src/Action/ShortUrl/ResolveShortUrlAction.php +++ b/module/Rest/src/Action/ShortUrl/ResolveShortUrlAction.php @@ -8,15 +8,11 @@ use InvalidArgumentException; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Log\LoggerInterface; -use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; use Shlinkio\Shlink\Core\Service\UrlShortenerInterface; use Shlinkio\Shlink\Core\Transformer\ShortUrlDataTransformer; use Shlinkio\Shlink\Rest\Action\AbstractRestAction; -use Shlinkio\Shlink\Rest\Util\RestUtils; use Zend\Diactoros\Response\JsonResponse; -use function sprintf; - class ResolveShortUrlAction extends AbstractRestAction { protected const ROUTE_PATH = '/short-urls/{shortCode}'; @@ -48,15 +44,7 @@ class ResolveShortUrlAction extends AbstractRestAction $domain = $request->getQueryParams()['domain'] ?? null; $transformer = new ShortUrlDataTransformer($this->domainConfig); - try { - $url = $this->urlShortener->shortCodeToUrl($shortCode, $domain); - return new JsonResponse($transformer->transform($url)); - } catch (EntityDoesNotExistException $e) { - $this->logger->warning('Provided short code couldn\'t be found. {e}', ['e' => $e]); - return new JsonResponse([ - 'error' => RestUtils::INVALID_ARGUMENT_ERROR, // FIXME Not correct. Use correct value on "type" - 'message' => sprintf('No URL found for short code "%s"', $shortCode), - ], self::STATUS_NOT_FOUND); - } + $url = $this->urlShortener->shortCodeToUrl($shortCode, $domain); + return new JsonResponse($transformer->transform($url)); } } diff --git a/module/Rest/test-api/Action/ResolveShortUrlActionTest.php b/module/Rest/test-api/Action/ResolveShortUrlActionTest.php index 117ae5a9..4065517e 100644 --- a/module/Rest/test-api/Action/ResolveShortUrlActionTest.php +++ b/module/Rest/test-api/Action/ResolveShortUrlActionTest.php @@ -16,6 +16,6 @@ class ResolveShortUrlActionTest extends ApiTestCase ['error' => $error] = $this->getJsonResponsePayload($resp); $this->assertEquals(self::STATUS_NOT_FOUND, $resp->getStatusCode()); - $this->assertEquals(RestUtils::INVALID_ARGUMENT_ERROR, $error); + $this->assertEquals(RestUtils::INVALID_SHORTCODE_ERROR, $error); } } diff --git a/module/Rest/test/Action/ShortUrl/ResolveShortUrlActionTest.php b/module/Rest/test/Action/ShortUrl/ResolveShortUrlActionTest.php index aa852de5..b77a6e45 100644 --- a/module/Rest/test/Action/ShortUrl/ResolveShortUrlActionTest.php +++ b/module/Rest/test/Action/ShortUrl/ResolveShortUrlActionTest.php @@ -7,10 +7,8 @@ namespace ShlinkioTest\Shlink\Rest\Action\ShortUrl; use PHPUnit\Framework\TestCase; use Prophecy\Prophecy\ObjectProphecy; use Shlinkio\Shlink\Core\Entity\ShortUrl; -use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; use Shlinkio\Shlink\Core\Service\UrlShortener; use Shlinkio\Shlink\Rest\Action\ShortUrl\ResolveShortUrlAction; -use Shlinkio\Shlink\Rest\Util\RestUtils; use Zend\Diactoros\ServerRequest; use function strpos; @@ -28,19 +26,6 @@ class ResolveShortUrlActionTest extends TestCase $this->action = new ResolveShortUrlAction($this->urlShortener->reveal(), []); } - /** @test */ - public function incorrectShortCodeReturnsError(): void - { - $shortCode = 'abc123'; - $this->urlShortener->shortCodeToUrl($shortCode, null)->willThrow(EntityDoesNotExistException::class) - ->shouldBeCalledOnce(); - - $request = (new ServerRequest())->withAttribute('shortCode', $shortCode); - $response = $this->action->handle($request); - $this->assertEquals(404, $response->getStatusCode()); - $this->assertTrue(strpos($response->getBody()->getContents(), RestUtils::INVALID_ARGUMENT_ERROR) > 0); - } - /** @test */ public function correctShortCodeReturnsSuccess(): void { From a28ef1f1769ab28ed976bbdaad575fd097daa692 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 25 Nov 2019 19:15:46 +0100 Subject: [PATCH 26/40] Converted EntityDoesNotExistException into a problem details exception renamed as TagNotFoundException --- docs/swagger/paths/v1_short-urls_shorten.json | 4 +-- .../CLI/src/Command/Tag/RenameTagCommand.php | 4 +-- .../test/Command/Tag/RenameTagCommandTest.php | 4 +-- .../Exception/EntityDoesNotExistException.php | 30 ------------------ .../src/Exception/TagNotFoundException.php | 31 +++++++++++++++++++ module/Core/src/Service/Tag/TagService.php | 19 ++++-------- .../src/Service/Tag/TagServiceInterface.php | 12 ++----- .../Core/test/Service/Tag/TagServiceTest.php | 4 +-- .../Rest/src/Action/Tag/UpdateTagAction.php | 26 +++++----------- .../Rest/src/Action/Visit/GetVisitsAction.php | 21 +++---------- .../src/Exception/AuthenticationException.php | 1 + module/Rest/src/Util/RestUtils.php | 5 ++- .../test-api/Action/GetVisitsActionTest.php | 2 +- .../ShortUrl/EditShortUrlActionTest.php | 2 -- .../test/Action/Tag/UpdateTagActionTest.php | 22 +++---------- .../test/Action/Visit/GetVisitsActionTest.php | 13 -------- module/Rest/test/Util/RestUtilsTest.php | 2 +- 17 files changed, 70 insertions(+), 132 deletions(-) delete mode 100644 module/Core/src/Exception/EntityDoesNotExistException.php create mode 100644 module/Core/src/Exception/TagNotFoundException.php diff --git a/docs/swagger/paths/v1_short-urls_shorten.json b/docs/swagger/paths/v1_short-urls_shorten.json index 803d77d5..d0c3c4c7 100644 --- a/docs/swagger/paths/v1_short-urls_shorten.json +++ b/docs/swagger/paths/v1_short-urls_shorten.json @@ -112,10 +112,10 @@ }, "examples": { "application/json": { - "error": "UNKNOWN_ERROR", + "error": "INTERNAL_SERVER_ERROR", "message": "Unexpected error occurred" }, - "text/plain": "UNKNOWN_ERROR" + "text/plain": "INTERNAL_SERVER_ERROR" } } } diff --git a/module/CLI/src/Command/Tag/RenameTagCommand.php b/module/CLI/src/Command/Tag/RenameTagCommand.php index 3e9f021b..a7002f60 100644 --- a/module/CLI/src/Command/Tag/RenameTagCommand.php +++ b/module/CLI/src/Command/Tag/RenameTagCommand.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace Shlinkio\Shlink\CLI\Command\Tag; use Shlinkio\Shlink\CLI\Util\ExitCodes; -use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; +use Shlinkio\Shlink\Core\Exception\TagNotFoundException; use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -47,7 +47,7 @@ class RenameTagCommand extends Command $this->tagService->renameTag($oldName, $newName); $io->success('Tag properly renamed.'); return ExitCodes::EXIT_SUCCESS; - } catch (EntityDoesNotExistException $e) { + } catch (TagNotFoundException $e) { $io->error(sprintf('A tag with name "%s" was not found', $oldName)); return ExitCodes::EXIT_FAILURE; } diff --git a/module/CLI/test/Command/Tag/RenameTagCommandTest.php b/module/CLI/test/Command/Tag/RenameTagCommandTest.php index 228d854d..4fc2aaad 100644 --- a/module/CLI/test/Command/Tag/RenameTagCommandTest.php +++ b/module/CLI/test/Command/Tag/RenameTagCommandTest.php @@ -8,7 +8,7 @@ use PHPUnit\Framework\TestCase; use Prophecy\Prophecy\ObjectProphecy; use Shlinkio\Shlink\CLI\Command\Tag\RenameTagCommand; use Shlinkio\Shlink\Core\Entity\Tag; -use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; +use Shlinkio\Shlink\Core\Exception\TagNotFoundException; use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface; use Symfony\Component\Console\Application; use Symfony\Component\Console\Tester\CommandTester; @@ -38,7 +38,7 @@ class RenameTagCommandTest extends TestCase { $oldName = 'foo'; $newName = 'bar'; - $renameTag = $this->tagService->renameTag($oldName, $newName)->willThrow(EntityDoesNotExistException::class); + $renameTag = $this->tagService->renameTag($oldName, $newName)->willThrow(TagNotFoundException::class); $this->commandTester->execute([ 'oldName' => $oldName, diff --git a/module/Core/src/Exception/EntityDoesNotExistException.php b/module/Core/src/Exception/EntityDoesNotExistException.php deleted file mode 100644 index 9214aa47..00000000 --- a/module/Core/src/Exception/EntityDoesNotExistException.php +++ /dev/null @@ -1,30 +0,0 @@ - $value) { - $result[] = sprintf('"%s" => "%s"', $key, $value); - } - - return implode(', ', $result); - } -} diff --git a/module/Core/src/Exception/TagNotFoundException.php b/module/Core/src/Exception/TagNotFoundException.php new file mode 100644 index 00000000..6ff36bb2 --- /dev/null +++ b/module/Core/src/Exception/TagNotFoundException.php @@ -0,0 +1,31 @@ +detail = $e->getMessage(); + $e->title = self::TITLE; + $e->type = self::TYPE; + $e->status = StatusCodeInterface::STATUS_NOT_FOUND; + + return $e; + } +} diff --git a/module/Core/src/Service/Tag/TagService.php b/module/Core/src/Service/Tag/TagService.php index d5ac562e..40bdbfcd 100644 --- a/module/Core/src/Service/Tag/TagService.php +++ b/module/Core/src/Service/Tag/TagService.php @@ -7,7 +7,7 @@ namespace Shlinkio\Shlink\Core\Service\Tag; use Doctrine\Common\Collections\Collection; use Doctrine\ORM; use Shlinkio\Shlink\Core\Entity\Tag; -use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; +use Shlinkio\Shlink\Core\Exception\TagNotFoundException; use Shlinkio\Shlink\Core\Repository\TagRepository; use Shlinkio\Shlink\Core\Util\TagManagerTrait; @@ -35,8 +35,7 @@ class TagService implements TagServiceInterface } /** - * @param array $tagNames - * @return void + * @param string[] $tagNames */ public function deleteTags(array $tagNames): void { @@ -60,23 +59,17 @@ class TagService implements TagServiceInterface } /** - * @param string $oldName - * @param string $newName - * @return Tag - * @throws EntityDoesNotExistException - * @throws ORM\OptimisticLockException + * @throws TagNotFoundException */ - public function renameTag($oldName, $newName): Tag + public function renameTag(string $oldName, string $newName): Tag { - $criteria = ['name' => $oldName]; /** @var Tag|null $tag */ - $tag = $this->em->getRepository(Tag::class)->findOneBy($criteria); + $tag = $this->em->getRepository(Tag::class)->findOneBy(['name' => $oldName]); if ($tag === null) { - throw EntityDoesNotExistException::createFromEntityAndConditions(Tag::class, $criteria); + throw TagNotFoundException::fromTag($oldName); } $tag->rename($newName); - $this->em->flush(); return $tag; diff --git a/module/Core/src/Service/Tag/TagServiceInterface.php b/module/Core/src/Service/Tag/TagServiceInterface.php index 1eb11112..dec01e31 100644 --- a/module/Core/src/Service/Tag/TagServiceInterface.php +++ b/module/Core/src/Service/Tag/TagServiceInterface.php @@ -6,7 +6,7 @@ namespace Shlinkio\Shlink\Core\Service\Tag; use Doctrine\Common\Collections\Collection; use Shlinkio\Shlink\Core\Entity\Tag; -use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; +use Shlinkio\Shlink\Core\Exception\TagNotFoundException; interface TagServiceInterface { @@ -17,23 +17,17 @@ interface TagServiceInterface /** * @param string[] $tagNames - * @return void */ public function deleteTags(array $tagNames): void; /** - * Provided a list of tag names, creates all that do not exist yet - * * @param string[] $tagNames * @return Collection|Tag[] */ public function createTags(array $tagNames): Collection; /** - * @param string $oldName - * @param string $newName - * @return Tag - * @throws EntityDoesNotExistException + * @throws TagNotFoundException */ - public function renameTag($oldName, $newName): Tag; + public function renameTag(string $oldName, string $newName): Tag; } diff --git a/module/Core/test/Service/Tag/TagServiceTest.php b/module/Core/test/Service/Tag/TagServiceTest.php index 0dd63ff5..0131d9e9 100644 --- a/module/Core/test/Service/Tag/TagServiceTest.php +++ b/module/Core/test/Service/Tag/TagServiceTest.php @@ -10,7 +10,7 @@ use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\Prophecy\ObjectProphecy; use Shlinkio\Shlink\Core\Entity\Tag; -use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; +use Shlinkio\Shlink\Core\Exception\TagNotFoundException; use Shlinkio\Shlink\Core\Repository\TagRepository; use Shlinkio\Shlink\Core\Service\Tag\TagService; @@ -83,7 +83,7 @@ class TagServiceTest extends TestCase $find->shouldBeCalled(); $getRepo->shouldBeCalled(); - $this->expectException(EntityDoesNotExistException::class); + $this->expectException(TagNotFoundException::class); $this->service->renameTag('foo', 'bar'); } diff --git a/module/Rest/src/Action/Tag/UpdateTagAction.php b/module/Rest/src/Action/Tag/UpdateTagAction.php index 64ec7a21..175ed0a0 100644 --- a/module/Rest/src/Action/Tag/UpdateTagAction.php +++ b/module/Rest/src/Action/Tag/UpdateTagAction.php @@ -7,14 +7,10 @@ namespace Shlinkio\Shlink\Rest\Action\Tag; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Log\LoggerInterface; -use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; +use Shlinkio\Shlink\Core\Exception\ValidationException; use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface; use Shlinkio\Shlink\Rest\Action\AbstractRestAction; -use Shlinkio\Shlink\Rest\Util\RestUtils; use Zend\Diactoros\Response\EmptyResponse; -use Zend\Diactoros\Response\JsonResponse; - -use function sprintf; class UpdateTagAction extends AbstractRestAction { @@ -43,21 +39,13 @@ class UpdateTagAction extends AbstractRestAction { $body = $request->getParsedBody(); if (! isset($body['oldName'], $body['newName'])) { - return new JsonResponse([ - 'error' => RestUtils::INVALID_ARGUMENT_ERROR, - 'message' => - 'You have to provide both \'oldName\' and \'newName\' params in order to properly rename the tag', - ], self::STATUS_BAD_REQUEST); + throw ValidationException::fromArray([ + 'oldName' => 'oldName is required', + 'newName' => 'newName is required', + ]); } - try { - $this->tagService->renameTag($body['oldName'], $body['newName']); - return new EmptyResponse(); - } catch (EntityDoesNotExistException $e) { - return new JsonResponse([ - 'error' => RestUtils::NOT_FOUND_ERROR, - 'message' => sprintf('It was not possible to find a tag with name %s', $body['oldName']), - ], self::STATUS_NOT_FOUND); - } + $this->tagService->renameTag($body['oldName'], $body['newName']); + return new EmptyResponse(); } } diff --git a/module/Rest/src/Action/Visit/GetVisitsAction.php b/module/Rest/src/Action/Visit/GetVisitsAction.php index 68d50533..ca8c2838 100644 --- a/module/Rest/src/Action/Visit/GetVisitsAction.php +++ b/module/Rest/src/Action/Visit/GetVisitsAction.php @@ -8,15 +8,11 @@ use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Log\LoggerInterface; use Shlinkio\Shlink\Common\Paginator\Util\PaginatorUtilsTrait; -use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Model\VisitsParams; use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface; use Shlinkio\Shlink\Rest\Action\AbstractRestAction; -use Shlinkio\Shlink\Rest\Util\RestUtils; use Zend\Diactoros\Response\JsonResponse; -use function sprintf; - class GetVisitsAction extends AbstractRestAction { use PaginatorUtilsTrait; @@ -36,19 +32,10 @@ class GetVisitsAction extends AbstractRestAction public function handle(Request $request): Response { $shortCode = $request->getAttribute('shortCode'); + $visits = $this->visitsTracker->info($shortCode, VisitsParams::fromRawData($request->getQueryParams())); - try { - $visits = $this->visitsTracker->info($shortCode, VisitsParams::fromRawData($request->getQueryParams())); - - return new JsonResponse([ - 'visits' => $this->serializePaginator($visits), - ]); - } catch (ShortUrlNotFoundException $e) { - $this->logger->warning('Provided nonexistent short code {e}', ['e' => $e]); - return new JsonResponse([ - 'error' => RestUtils::INVALID_ARGUMENT_ERROR, // FIXME Wrong code. Use correct one in "type" - 'message' => sprintf('Provided short code %s does not exist', $shortCode), - ], self::STATUS_NOT_FOUND); - } + return new JsonResponse([ + 'visits' => $this->serializePaginator($visits), + ]); } } diff --git a/module/Rest/src/Exception/AuthenticationException.php b/module/Rest/src/Exception/AuthenticationException.php index 1613bac5..3e60edb6 100644 --- a/module/Rest/src/Exception/AuthenticationException.php +++ b/module/Rest/src/Exception/AuthenticationException.php @@ -6,6 +6,7 @@ namespace Shlinkio\Shlink\Rest\Exception; use Throwable; +/** @deprecated */ class AuthenticationException extends RuntimeException { public static function expiredJWT(?Throwable $prev = null): self diff --git a/module/Rest/src/Util/RestUtils.php b/module/Rest/src/Util/RestUtils.php index 97ca0d0b..3f4f1161 100644 --- a/module/Rest/src/Util/RestUtils.php +++ b/module/Rest/src/Util/RestUtils.php @@ -21,11 +21,14 @@ class RestUtils public const INVALID_ARGUMENT_ERROR = Core\ValidationException::TYPE; /** @deprecated */ public const INVALID_SLUG_ERROR = Core\NonUniqueSlugException::TYPE; + /** @deprecated */ public const INVALID_CREDENTIALS_ERROR = 'INVALID_CREDENTIALS'; public const INVALID_AUTH_TOKEN_ERROR = 'INVALID_AUTH_TOKEN'; public const INVALID_AUTHORIZATION_ERROR = 'INVALID_AUTHORIZATION'; public const INVALID_API_KEY_ERROR = 'INVALID_API_KEY'; - public const NOT_FOUND_ERROR = 'NOT_FOUND'; + /** @deprecated */ + public const NOT_FOUND_ERROR = Core\TagNotFoundException::TYPE; + /** @deprecated */ public const UNKNOWN_ERROR = 'UNKNOWN_ERROR'; /** @deprecated */ diff --git a/module/Rest/test-api/Action/GetVisitsActionTest.php b/module/Rest/test-api/Action/GetVisitsActionTest.php index 150c3066..d2c5f6eb 100644 --- a/module/Rest/test-api/Action/GetVisitsActionTest.php +++ b/module/Rest/test-api/Action/GetVisitsActionTest.php @@ -16,6 +16,6 @@ class GetVisitsActionTest extends ApiTestCase ['error' => $error] = $this->getJsonResponsePayload($resp); $this->assertEquals(self::STATUS_NOT_FOUND, $resp->getStatusCode()); - $this->assertEquals(RestUtils::INVALID_ARGUMENT_ERROR, $error); + $this->assertEquals(RestUtils::INVALID_SHORTCODE_ERROR, $error); } } diff --git a/module/Rest/test/Action/ShortUrl/EditShortUrlActionTest.php b/module/Rest/test/Action/ShortUrl/EditShortUrlActionTest.php index 63aa1ffb..ff86117e 100644 --- a/module/Rest/test/Action/ShortUrl/EditShortUrlActionTest.php +++ b/module/Rest/test/Action/ShortUrl/EditShortUrlActionTest.php @@ -11,8 +11,6 @@ use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Exception\ValidationException; use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface; use Shlinkio\Shlink\Rest\Action\ShortUrl\EditShortUrlAction; -use Shlinkio\Shlink\Rest\Util\RestUtils; -use Zend\Diactoros\Response\JsonResponse; use Zend\Diactoros\ServerRequest; class EditShortUrlActionTest extends TestCase diff --git a/module/Rest/test/Action/Tag/UpdateTagActionTest.php b/module/Rest/test/Action/Tag/UpdateTagActionTest.php index 9ded1716..3b068add 100644 --- a/module/Rest/test/Action/Tag/UpdateTagActionTest.php +++ b/module/Rest/test/Action/Tag/UpdateTagActionTest.php @@ -7,7 +7,7 @@ namespace ShlinkioTest\Shlink\Rest\Action\Tag; use PHPUnit\Framework\TestCase; use Prophecy\Prophecy\ObjectProphecy; use Shlinkio\Shlink\Core\Entity\Tag; -use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; +use Shlinkio\Shlink\Core\Exception\ValidationException; use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface; use Shlinkio\Shlink\Rest\Action\Tag\UpdateTagAction; use Zend\Diactoros\ServerRequest; @@ -32,9 +32,10 @@ class UpdateTagActionTest extends TestCase public function whenInvalidParamsAreProvidedAnErrorIsReturned(array $bodyParams): void { $request = (new ServerRequest())->withParsedBody($bodyParams); - $resp = $this->action->handle($request); - $this->assertEquals(400, $resp->getStatusCode()); + $this->expectException(ValidationException::class); + + $this->action->handle($request); } public function provideParams(): iterable @@ -44,21 +45,6 @@ class UpdateTagActionTest extends TestCase yield 'no params' => [[]]; } - /** @test */ - public function requestingInvalidTagReturnsError(): void - { - $request = (new ServerRequest())->withParsedBody([ - 'oldName' => 'foo', - 'newName' => 'bar', - ]); - $rename = $this->tagService->renameTag('foo', 'bar')->willThrow(EntityDoesNotExistException::class); - - $resp = $this->action->handle($request); - - $this->assertEquals(404, $resp->getStatusCode()); - $rename->shouldHaveBeenCalled(); - } - /** @test */ public function correctInvocationRenamesTag(): void { diff --git a/module/Rest/test/Action/Visit/GetVisitsActionTest.php b/module/Rest/test/Action/Visit/GetVisitsActionTest.php index cb38dd85..80e06085 100644 --- a/module/Rest/test/Action/Visit/GetVisitsActionTest.php +++ b/module/Rest/test/Action/Visit/GetVisitsActionTest.php @@ -9,7 +9,6 @@ use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\Prophecy\ObjectProphecy; use Shlinkio\Shlink\Common\Util\DateRange; -use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Model\VisitsParams; use Shlinkio\Shlink\Core\Service\VisitsTracker; use Shlinkio\Shlink\Rest\Action\Visit\GetVisitsAction; @@ -42,18 +41,6 @@ class GetVisitsActionTest extends TestCase $this->assertEquals(200, $response->getStatusCode()); } - /** @test */ - public function providingInvalidShortCodeReturnsError(): void - { - $shortCode = 'abc123'; - $this->visitsTracker->info($shortCode, Argument::type(VisitsParams::class))->willThrow( - ShortUrlNotFoundException::class - )->shouldBeCalledOnce(); - - $response = $this->action->handle((new ServerRequest())->withAttribute('shortCode', $shortCode)); - $this->assertEquals(404, $response->getStatusCode()); - } - /** @test */ public function paramsAreReadFromQuery(): void { diff --git a/module/Rest/test/Util/RestUtilsTest.php b/module/Rest/test/Util/RestUtilsTest.php index d8a45181..6097cdc6 100644 --- a/module/Rest/test/Util/RestUtilsTest.php +++ b/module/Rest/test/Util/RestUtilsTest.php @@ -6,8 +6,8 @@ namespace ShlinkioTest\Shlink\Rest\Util; use PHPUnit\Framework\TestCase; use Shlinkio\Shlink\Common\Exception\InvalidArgumentException; -use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Exception\InvalidUrlException; +use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException; use Shlinkio\Shlink\Rest\Exception\AuthenticationException; use Shlinkio\Shlink\Rest\Util\RestUtils; From 13e795d25d33ff9b15220b70e800e6016ddbc9c4 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 26 Nov 2019 20:58:38 +0100 Subject: [PATCH 27/40] Updated ValidationException's base exception --- module/Core/src/Exception/ValidationException.php | 2 +- module/Core/src/Service/UrlShortener.php | 1 + .../Authentication/Plugin/AuthorizationHeaderPlugin.php | 1 + module/Rest/src/Util/RestUtils.php | 8 ++++---- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/module/Core/src/Exception/ValidationException.php b/module/Core/src/Exception/ValidationException.php index 510a31b0..eea23a5a 100644 --- a/module/Core/src/Exception/ValidationException.php +++ b/module/Core/src/Exception/ValidationException.php @@ -17,7 +17,7 @@ use function sprintf; use const PHP_EOL; -class ValidationException extends RuntimeException implements ProblemDetailsExceptionInterface +class ValidationException extends InvalidArgumentException implements ProblemDetailsExceptionInterface { use CommonProblemDetailsExceptionTrait; diff --git a/module/Core/src/Service/UrlShortener.php b/module/Core/src/Service/UrlShortener.php index 0e4062ec..70112bd7 100644 --- a/module/Core/src/Service/UrlShortener.php +++ b/module/Core/src/Service/UrlShortener.php @@ -130,6 +130,7 @@ class UrlShortener implements UrlShortenerInterface /** * @throws ShortUrlNotFoundException + * @fixme Move this method to a different service */ public function shortCodeToUrl(string $shortCode, ?string $domain = null): ShortUrl { diff --git a/module/Rest/src/Authentication/Plugin/AuthorizationHeaderPlugin.php b/module/Rest/src/Authentication/Plugin/AuthorizationHeaderPlugin.php index e5280ac6..d479d9d3 100644 --- a/module/Rest/src/Authentication/Plugin/AuthorizationHeaderPlugin.php +++ b/module/Rest/src/Authentication/Plugin/AuthorizationHeaderPlugin.php @@ -16,6 +16,7 @@ use function explode; use function sprintf; use function strtolower; +/** @deprecated */ class AuthorizationHeaderPlugin implements AuthenticationPluginInterface { public const HEADER_NAME = 'Authorization'; diff --git a/module/Rest/src/Util/RestUtils.php b/module/Rest/src/Util/RestUtils.php index 3f4f1161..6e2b77f0 100644 --- a/module/Rest/src/Util/RestUtils.php +++ b/module/Rest/src/Util/RestUtils.php @@ -22,14 +22,14 @@ class RestUtils /** @deprecated */ public const INVALID_SLUG_ERROR = Core\NonUniqueSlugException::TYPE; /** @deprecated */ + public const NOT_FOUND_ERROR = Core\TagNotFoundException::TYPE; + /** @deprecated */ + public const UNKNOWN_ERROR = 'UNKNOWN_ERROR'; + public const INVALID_CREDENTIALS_ERROR = 'INVALID_CREDENTIALS'; public const INVALID_AUTH_TOKEN_ERROR = 'INVALID_AUTH_TOKEN'; public const INVALID_AUTHORIZATION_ERROR = 'INVALID_AUTHORIZATION'; public const INVALID_API_KEY_ERROR = 'INVALID_API_KEY'; - /** @deprecated */ - public const NOT_FOUND_ERROR = Core\TagNotFoundException::TYPE; - /** @deprecated */ - public const UNKNOWN_ERROR = 'UNKNOWN_ERROR'; /** @deprecated */ public static function getRestErrorCodeFromException(Throwable $e): string From 509c9fe2e8f782ebf7e1b728017c241d8e37313b Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 26 Nov 2019 21:29:25 +0100 Subject: [PATCH 28/40] Improved AuthenticationMiddleware API tests --- .../RequestToHttpAuthPlugin.php | 5 +- ...teShortUrlContentNegotiationMiddleware.php | 6 --- .../Action/CreateShortUrlActionTest.php | 3 +- .../Action/DeleteShortUrlActionTest.php | 5 +- .../Action/EditShortUrlActionTagsTest.php | 5 +- .../Action/EditShortUrlActionTest.php | 5 +- .../test-api/Action/GetVisitsActionTest.php | 3 +- .../Action/ResolveShortUrlActionTest.php | 3 +- .../test-api/Action/UpdateTagActionTest.php | 5 +- .../Middleware/AuthenticationTest.php | 50 +++++++++++++++++-- 10 files changed, 57 insertions(+), 33 deletions(-) diff --git a/module/Rest/src/Authentication/RequestToHttpAuthPlugin.php b/module/Rest/src/Authentication/RequestToHttpAuthPlugin.php index f0cc0fe4..9e3e28e0 100644 --- a/module/Rest/src/Authentication/RequestToHttpAuthPlugin.php +++ b/module/Rest/src/Authentication/RequestToHttpAuthPlugin.php @@ -36,10 +36,7 @@ class RequestToHttpAuthPlugin implements RequestToHttpAuthPluginInterface public function fromRequest(ServerRequestInterface $request): Plugin\AuthenticationPluginInterface { if (! $this->hasAnySupportedHeader($request)) { - throw NoAuthenticationException::fromExpectedTypes([ - Plugin\ApiKeyHeaderPlugin::HEADER_NAME, - Plugin\AuthorizationHeaderPlugin::HEADER_NAME, - ]); + throw NoAuthenticationException::fromExpectedTypes(self::SUPPORTED_AUTH_HEADERS); } return $this->authPluginManager->get($this->getFirstAvailableHeader($request)); diff --git a/module/Rest/src/Middleware/ShortUrl/CreateShortUrlContentNegotiationMiddleware.php b/module/Rest/src/Middleware/ShortUrl/CreateShortUrlContentNegotiationMiddleware.php index 64da53d5..98b64401 100644 --- a/module/Rest/src/Middleware/ShortUrl/CreateShortUrlContentNegotiationMiddleware.php +++ b/module/Rest/src/Middleware/ShortUrl/CreateShortUrlContentNegotiationMiddleware.php @@ -21,12 +21,6 @@ class CreateShortUrlContentNegotiationMiddleware implements MiddlewareInterface private const PLAIN_TEXT = 'text'; private const JSON = 'json'; - /** - * Process an incoming server request and return a response, optionally delegating - * response creation to a handler. - * @throws \RuntimeException - * @throws \InvalidArgumentException - */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { $response = $handler->handle($request); diff --git a/module/Rest/test-api/Action/CreateShortUrlActionTest.php b/module/Rest/test-api/Action/CreateShortUrlActionTest.php index 61c5c7a0..758bb4ac 100644 --- a/module/Rest/test-api/Action/CreateShortUrlActionTest.php +++ b/module/Rest/test-api/Action/CreateShortUrlActionTest.php @@ -6,7 +6,6 @@ namespace ShlinkioApiTest\Shlink\Rest\Action; use Cake\Chronos\Chronos; use GuzzleHttp\RequestOptions; -use Shlinkio\Shlink\Rest\Util\RestUtils; use Shlinkio\Shlink\TestUtils\ApiTest\ApiTestCase; use function Functional\map; @@ -44,7 +43,7 @@ class CreateShortUrlActionTest extends ApiTestCase [$statusCode, $payload] = $this->createShortUrl(['customSlug' => $slug, 'domain' => $domain]); $this->assertEquals(self::STATUS_BAD_REQUEST, $statusCode); - $this->assertEquals(RestUtils::INVALID_SLUG_ERROR, $payload['error']); + $this->assertEquals('INVALID_SLUG', $payload['error']); } /** @test */ diff --git a/module/Rest/test-api/Action/DeleteShortUrlActionTest.php b/module/Rest/test-api/Action/DeleteShortUrlActionTest.php index 042b8e90..c6cf443e 100644 --- a/module/Rest/test-api/Action/DeleteShortUrlActionTest.php +++ b/module/Rest/test-api/Action/DeleteShortUrlActionTest.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace ShlinkioApiTest\Shlink\Rest\Action; -use Shlinkio\Shlink\Rest\Util\RestUtils; use Shlinkio\Shlink\TestUtils\ApiTest\ApiTestCase; class DeleteShortUrlActionTest extends ApiTestCase @@ -16,7 +15,7 @@ class DeleteShortUrlActionTest extends ApiTestCase ['error' => $error] = $this->getJsonResponsePayload($resp); $this->assertEquals(self::STATUS_NOT_FOUND, $resp->getStatusCode()); - $this->assertEquals(RestUtils::INVALID_SHORTCODE_ERROR, $error); + $this->assertEquals('INVALID_SHORTCODE', $error); } /** @test */ @@ -31,6 +30,6 @@ class DeleteShortUrlActionTest extends ApiTestCase ['error' => $error] = $this->getJsonResponsePayload($resp); $this->assertEquals(self::STATUS_UNPROCESSABLE_ENTITY, $resp->getStatusCode()); - $this->assertEquals(RestUtils::INVALID_SHORTCODE_DELETION_ERROR, $error); + $this->assertEquals('INVALID_SHORTCODE_DELETION', $error); } } diff --git a/module/Rest/test-api/Action/EditShortUrlActionTagsTest.php b/module/Rest/test-api/Action/EditShortUrlActionTagsTest.php index d033fcee..e6cae9b6 100644 --- a/module/Rest/test-api/Action/EditShortUrlActionTagsTest.php +++ b/module/Rest/test-api/Action/EditShortUrlActionTagsTest.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace ShlinkioApiTest\Shlink\Rest\Action; use GuzzleHttp\RequestOptions; -use Shlinkio\Shlink\Rest\Util\RestUtils; use Shlinkio\Shlink\TestUtils\ApiTest\ApiTestCase; class EditShortUrlActionTagsTest extends ApiTestCase @@ -17,7 +16,7 @@ class EditShortUrlActionTagsTest extends ApiTestCase ['error' => $error] = $this->getJsonResponsePayload($resp); $this->assertEquals(self::STATUS_BAD_REQUEST, $resp->getStatusCode()); - $this->assertEquals(RestUtils::INVALID_ARGUMENT_ERROR, $error); + $this->assertEquals('INVALID_ARGUMENT', $error); } /** @test */ @@ -29,6 +28,6 @@ class EditShortUrlActionTagsTest extends ApiTestCase ['error' => $error] = $this->getJsonResponsePayload($resp); $this->assertEquals(self::STATUS_NOT_FOUND, $resp->getStatusCode()); - $this->assertEquals(RestUtils::INVALID_SHORTCODE_ERROR, $error); + $this->assertEquals('INVALID_SHORTCODE', $error); } } diff --git a/module/Rest/test-api/Action/EditShortUrlActionTest.php b/module/Rest/test-api/Action/EditShortUrlActionTest.php index 65f2fd1d..739ec191 100644 --- a/module/Rest/test-api/Action/EditShortUrlActionTest.php +++ b/module/Rest/test-api/Action/EditShortUrlActionTest.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace ShlinkioApiTest\Shlink\Rest\Action; use GuzzleHttp\RequestOptions; -use Shlinkio\Shlink\Rest\Util\RestUtils; use Shlinkio\Shlink\TestUtils\ApiTest\ApiTestCase; class EditShortUrlActionTest extends ApiTestCase @@ -17,7 +16,7 @@ class EditShortUrlActionTest extends ApiTestCase ['error' => $error] = $this->getJsonResponsePayload($resp); $this->assertEquals(self::STATUS_NOT_FOUND, $resp->getStatusCode()); - $this->assertEquals(RestUtils::INVALID_SHORTCODE_ERROR, $error); + $this->assertEquals('INVALID_SHORTCODE', $error); } /** @test */ @@ -29,6 +28,6 @@ class EditShortUrlActionTest extends ApiTestCase ['error' => $error] = $this->getJsonResponsePayload($resp); $this->assertEquals(self::STATUS_BAD_REQUEST, $resp->getStatusCode()); - $this->assertEquals(RestUtils::INVALID_ARGUMENT_ERROR, $error); + $this->assertEquals('INVALID_ARGUMENT', $error); } } diff --git a/module/Rest/test-api/Action/GetVisitsActionTest.php b/module/Rest/test-api/Action/GetVisitsActionTest.php index d2c5f6eb..f9c6f404 100644 --- a/module/Rest/test-api/Action/GetVisitsActionTest.php +++ b/module/Rest/test-api/Action/GetVisitsActionTest.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace ShlinkioApiTest\Shlink\Rest\Action; -use Shlinkio\Shlink\Rest\Util\RestUtils; use Shlinkio\Shlink\TestUtils\ApiTest\ApiTestCase; class GetVisitsActionTest extends ApiTestCase @@ -16,6 +15,6 @@ class GetVisitsActionTest extends ApiTestCase ['error' => $error] = $this->getJsonResponsePayload($resp); $this->assertEquals(self::STATUS_NOT_FOUND, $resp->getStatusCode()); - $this->assertEquals(RestUtils::INVALID_SHORTCODE_ERROR, $error); + $this->assertEquals('INVALID_SHORTCODE', $error); } } diff --git a/module/Rest/test-api/Action/ResolveShortUrlActionTest.php b/module/Rest/test-api/Action/ResolveShortUrlActionTest.php index 4065517e..46f07d53 100644 --- a/module/Rest/test-api/Action/ResolveShortUrlActionTest.php +++ b/module/Rest/test-api/Action/ResolveShortUrlActionTest.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace ShlinkioApiTest\Shlink\Rest\Action; -use Shlinkio\Shlink\Rest\Util\RestUtils; use Shlinkio\Shlink\TestUtils\ApiTest\ApiTestCase; class ResolveShortUrlActionTest extends ApiTestCase @@ -16,6 +15,6 @@ class ResolveShortUrlActionTest extends ApiTestCase ['error' => $error] = $this->getJsonResponsePayload($resp); $this->assertEquals(self::STATUS_NOT_FOUND, $resp->getStatusCode()); - $this->assertEquals(RestUtils::INVALID_SHORTCODE_ERROR, $error); + $this->assertEquals('INVALID_SHORTCODE', $error); } } diff --git a/module/Rest/test-api/Action/UpdateTagActionTest.php b/module/Rest/test-api/Action/UpdateTagActionTest.php index 0f7c0400..12f37296 100644 --- a/module/Rest/test-api/Action/UpdateTagActionTest.php +++ b/module/Rest/test-api/Action/UpdateTagActionTest.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace ShlinkioApiTest\Shlink\Rest\Action; use GuzzleHttp\RequestOptions; -use Shlinkio\Shlink\Rest\Util\RestUtils; use Shlinkio\Shlink\TestUtils\ApiTest\ApiTestCase; class UpdateTagActionTest extends ApiTestCase @@ -20,7 +19,7 @@ class UpdateTagActionTest extends ApiTestCase ['error' => $error] = $this->getJsonResponsePayload($resp); $this->assertEquals(self::STATUS_BAD_REQUEST, $resp->getStatusCode()); - $this->assertEquals(RestUtils::INVALID_ARGUMENT_ERROR, $error); + $this->assertEquals('INVALID_ARGUMENT', $error); } public function provideInvalidBody(): iterable @@ -40,6 +39,6 @@ class UpdateTagActionTest extends ApiTestCase ['error' => $error] = $this->getJsonResponsePayload($resp); $this->assertEquals(self::STATUS_NOT_FOUND, $resp->getStatusCode()); - $this->assertEquals(RestUtils::NOT_FOUND_ERROR, $error); + $this->assertEquals('TAG_NOT_FOUND', $error); } } diff --git a/module/Rest/test-api/Middleware/AuthenticationTest.php b/module/Rest/test-api/Middleware/AuthenticationTest.php index 02836c85..b918a369 100644 --- a/module/Rest/test-api/Middleware/AuthenticationTest.php +++ b/module/Rest/test-api/Middleware/AuthenticationTest.php @@ -4,9 +4,8 @@ declare(strict_types=1); namespace ShlinkioApiTest\Shlink\Rest\Middleware; -use Shlinkio\Shlink\Rest\Authentication\Plugin\ApiKeyHeaderPlugin; +use Shlinkio\Shlink\Rest\Authentication\Plugin; use Shlinkio\Shlink\Rest\Authentication\RequestToHttpAuthPlugin; -use Shlinkio\Shlink\Rest\Util\RestUtils; use Shlinkio\Shlink\TestUtils\ApiTest\ApiTestCase; use function implode; @@ -21,7 +20,7 @@ class AuthenticationTest extends ApiTestCase ['error' => $error, 'message' => $message] = $this->getJsonResponsePayload($resp); $this->assertEquals(self::STATUS_UNAUTHORIZED, $resp->getStatusCode()); - $this->assertEquals(RestUtils::INVALID_AUTHORIZATION_ERROR, $error); + $this->assertEquals('INVALID_AUTHORIZATION', $error); $this->assertEquals( sprintf( 'Expected one of the following authentication headers, but none were provided, ["%s"]', @@ -39,13 +38,13 @@ class AuthenticationTest extends ApiTestCase { $resp = $this->callApi(self::METHOD_GET, '/short-codes', [ 'headers' => [ - ApiKeyHeaderPlugin::HEADER_NAME => $apiKey, + Plugin\ApiKeyHeaderPlugin::HEADER_NAME => $apiKey, ], ]); ['error' => $error, 'message' => $message] = $this->getJsonResponsePayload($resp); $this->assertEquals(self::STATUS_UNAUTHORIZED, $resp->getStatusCode()); - $this->assertEquals(RestUtils::INVALID_API_KEY_ERROR, $error); + $this->assertEquals('INVALID_API_KEY', $error); $this->assertEquals('Provided API key does not exist or is invalid.', $message); } @@ -55,4 +54,45 @@ class AuthenticationTest extends ApiTestCase yield 'key which is expired' => ['expired_api_key']; yield 'key which is disabled' => ['disabled_api_key']; } + + /** + * @test + * @dataProvider provideInvalidAuthorizations + */ + public function authorizationErrorIsReturnedIfInvalidDataIsProvided( + string $authValue, + string $expectedMessage, + string $expectedError + ): void { + $resp = $this->callApi(self::METHOD_GET, '/short-codes', [ + 'headers' => [ + Plugin\AuthorizationHeaderPlugin::HEADER_NAME => $authValue, + ], + ]); + ['error' => $error, 'message' => $message] = $this->getJsonResponsePayload($resp); + + $this->assertEquals(self::STATUS_UNAUTHORIZED, $resp->getStatusCode()); + $this->assertEquals($expectedError, $error); + $this->assertEquals($expectedMessage, $message); + } + + public function provideInvalidAuthorizations(): iterable + { + yield 'no type' => [ + 'invalid', + 'You need to provide the Bearer type in the Authorization header.', + 'INVALID_AUTHORIZATION', + ]; + yield 'invalid type' => [ + 'Basic invalid', + 'Provided authorization type Basic is not supported. Use Bearer instead.', + 'INVALID_AUTHORIZATION', + ]; + yield 'invalid JWT' => [ + 'Bearer invalid', + 'Missing or invalid auth token provided. Perform a new authentication request and send provided ' + . 'token on every new request on the Authorization header', + 'INVALID_AUTH_TOKEN', + ]; + } } From f502eb0195b83a89c7c94f764d31aa5c5934ff38 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 26 Nov 2019 21:33:22 +0100 Subject: [PATCH 29/40] Added new test for the case in which an invalid URL is provided --- module/Rest/test-api/Action/CreateShortUrlActionTest.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/module/Rest/test-api/Action/CreateShortUrlActionTest.php b/module/Rest/test-api/Action/CreateShortUrlActionTest.php index 758bb4ac..2098ea0c 100644 --- a/module/Rest/test-api/Action/CreateShortUrlActionTest.php +++ b/module/Rest/test-api/Action/CreateShortUrlActionTest.php @@ -200,6 +200,15 @@ class CreateShortUrlActionTest extends ApiTestCase yield ['http://téstb.shlink.io']; // Redirects to http://tést.shlink.io } + /** @test */ + public function failsToCreateShortUrlWithInvalidOriginalUrl(): void + { + [$statusCode, $payload] = $this->createShortUrl(['longUrl' => 'https://this-has-to-be-invalid.com']); + + $this->assertEquals(self::STATUS_BAD_REQUEST, $statusCode); + $this->assertEquals('INVALID_URL', $payload['error']); + } + /** * @return array { * @var int $statusCode From 6f4e5175da5f82158f47a8cf311fa2ae9453b761 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 26 Nov 2019 21:43:29 +0100 Subject: [PATCH 30/40] Converted MissingAuthenticationException into a problem details exception --- .../RequestToHttpAuthPlugin.php | 8 ++--- .../RequestToHttpAuthPluginInterface.php | 6 ++-- .../MissingAuthenticationException.php | 36 +++++++++++++++++++ .../Exception/NoAuthenticationException.php | 19 ---------- .../Middleware/AuthenticationMiddleware.php | 25 +------------ module/Rest/src/Util/RestUtils.php | 3 +- .../RequestToAuthPluginTest.php | 4 +-- .../AuthenticationMiddlewareTest.php | 4 +-- 8 files changed, 48 insertions(+), 57 deletions(-) create mode 100644 module/Rest/src/Exception/MissingAuthenticationException.php delete mode 100644 module/Rest/src/Exception/NoAuthenticationException.php diff --git a/module/Rest/src/Authentication/RequestToHttpAuthPlugin.php b/module/Rest/src/Authentication/RequestToHttpAuthPlugin.php index 9e3e28e0..c10c0321 100644 --- a/module/Rest/src/Authentication/RequestToHttpAuthPlugin.php +++ b/module/Rest/src/Authentication/RequestToHttpAuthPlugin.php @@ -4,9 +4,8 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Rest\Authentication; -use Psr\Container; use Psr\Http\Message\ServerRequestInterface; -use Shlinkio\Shlink\Rest\Exception\NoAuthenticationException; +use Shlinkio\Shlink\Rest\Exception\MissingAuthenticationException; use function array_filter; use function array_reduce; @@ -30,13 +29,12 @@ class RequestToHttpAuthPlugin implements RequestToHttpAuthPluginInterface } /** - * @throws Container\ContainerExceptionInterface - * @throws NoAuthenticationException + * @throws MissingAuthenticationException */ public function fromRequest(ServerRequestInterface $request): Plugin\AuthenticationPluginInterface { if (! $this->hasAnySupportedHeader($request)) { - throw NoAuthenticationException::fromExpectedTypes(self::SUPPORTED_AUTH_HEADERS); + throw MissingAuthenticationException::fromExpectedTypes(self::SUPPORTED_AUTH_HEADERS); } return $this->authPluginManager->get($this->getFirstAvailableHeader($request)); diff --git a/module/Rest/src/Authentication/RequestToHttpAuthPluginInterface.php b/module/Rest/src/Authentication/RequestToHttpAuthPluginInterface.php index 18e68ee2..b8002431 100644 --- a/module/Rest/src/Authentication/RequestToHttpAuthPluginInterface.php +++ b/module/Rest/src/Authentication/RequestToHttpAuthPluginInterface.php @@ -4,15 +4,13 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Rest\Authentication; -use Psr\Container; use Psr\Http\Message\ServerRequestInterface; -use Shlinkio\Shlink\Rest\Exception\NoAuthenticationException; +use Shlinkio\Shlink\Rest\Exception\MissingAuthenticationException; interface RequestToHttpAuthPluginInterface { /** - * @throws Container\ContainerExceptionInterface - * @throws NoAuthenticationException + * @throws MissingAuthenticationException */ public function fromRequest(ServerRequestInterface $request): Plugin\AuthenticationPluginInterface; } diff --git a/module/Rest/src/Exception/MissingAuthenticationException.php b/module/Rest/src/Exception/MissingAuthenticationException.php new file mode 100644 index 00000000..6ab55458 --- /dev/null +++ b/module/Rest/src/Exception/MissingAuthenticationException.php @@ -0,0 +1,36 @@ +detail = $e->getMessage(); + $e->title = self::TITLE; + $e->type = self::TYPE; + $e->status = StatusCodeInterface::STATUS_UNAUTHORIZED; + $e->additional = ['expectedTypes' => $expectedTypes]; + + return $e; + } +} diff --git a/module/Rest/src/Exception/NoAuthenticationException.php b/module/Rest/src/Exception/NoAuthenticationException.php deleted file mode 100644 index b5e8bfc8..00000000 --- a/module/Rest/src/Exception/NoAuthenticationException.php +++ /dev/null @@ -1,19 +0,0 @@ -logger = $logger ?: new NullLogger(); } - /** - * Process an incoming server request and return a response, optionally delegating - * to the next middleware component to create the response. - * - * @param Request $request - * @param RequestHandlerInterface $handler - * - * @return Response - * @throws \InvalidArgumentException - */ public function process(Request $request, RequestHandlerInterface $handler): Response { /** @var RouteResult|null $routeResult */ @@ -67,15 +52,7 @@ class AuthenticationMiddleware implements MiddlewareInterface, StatusCodeInterfa return $handler->handle($request); } - try { - $plugin = $this->requestToAuthPlugin->fromRequest($request); - } catch (ContainerExceptionInterface | NoAuthenticationException $e) { - $this->logger->warning('Invalid or no authentication provided. {e}', ['e' => $e]); - return $this->createErrorResponse(sprintf( - 'Expected one of the following authentication headers, but none were provided, ["%s"]', - implode('", "', RequestToHttpAuthPlugin::SUPPORTED_AUTH_HEADERS) - )); - } + $plugin = $this->requestToAuthPlugin->fromRequest($request); try { $plugin->verify($request); diff --git a/module/Rest/src/Util/RestUtils.php b/module/Rest/src/Util/RestUtils.php index 6e2b77f0..1dad3908 100644 --- a/module/Rest/src/Util/RestUtils.php +++ b/module/Rest/src/Util/RestUtils.php @@ -28,7 +28,8 @@ class RestUtils public const INVALID_CREDENTIALS_ERROR = 'INVALID_CREDENTIALS'; public const INVALID_AUTH_TOKEN_ERROR = 'INVALID_AUTH_TOKEN'; - public const INVALID_AUTHORIZATION_ERROR = 'INVALID_AUTHORIZATION'; + /** @deprecated */ + public const INVALID_AUTHORIZATION_ERROR = Rest\MissingAuthenticationException::TYPE; public const INVALID_API_KEY_ERROR = 'INVALID_API_KEY'; /** @deprecated */ diff --git a/module/Rest/test/Authentication/RequestToAuthPluginTest.php b/module/Rest/test/Authentication/RequestToAuthPluginTest.php index e9d68489..d9005261 100644 --- a/module/Rest/test/Authentication/RequestToAuthPluginTest.php +++ b/module/Rest/test/Authentication/RequestToAuthPluginTest.php @@ -11,7 +11,7 @@ use Shlinkio\Shlink\Rest\Authentication\Plugin\ApiKeyHeaderPlugin; use Shlinkio\Shlink\Rest\Authentication\Plugin\AuthenticationPluginInterface; use Shlinkio\Shlink\Rest\Authentication\Plugin\AuthorizationHeaderPlugin; use Shlinkio\Shlink\Rest\Authentication\RequestToHttpAuthPlugin; -use Shlinkio\Shlink\Rest\Exception\NoAuthenticationException; +use Shlinkio\Shlink\Rest\Exception\MissingAuthenticationException; use Zend\Diactoros\ServerRequest; use function implode; @@ -35,7 +35,7 @@ class RequestToAuthPluginTest extends TestCase { $request = new ServerRequest(); - $this->expectException(NoAuthenticationException::class); + $this->expectException(MissingAuthenticationException::class); $this->expectExceptionMessage(sprintf( 'None of the valid authentication mechanisms where provided. Expected one of ["%s"]', implode('", "', RequestToHttpAuthPlugin::SUPPORTED_AUTH_HEADERS) diff --git a/module/Rest/test/Middleware/AuthenticationMiddlewareTest.php b/module/Rest/test/Middleware/AuthenticationMiddlewareTest.php index e373fbea..054db85e 100644 --- a/module/Rest/test/Middleware/AuthenticationMiddlewareTest.php +++ b/module/Rest/test/Middleware/AuthenticationMiddlewareTest.php @@ -19,7 +19,7 @@ use Shlinkio\Shlink\Rest\Action\AuthenticateAction; use Shlinkio\Shlink\Rest\Authentication\Plugin\AuthenticationPluginInterface; use Shlinkio\Shlink\Rest\Authentication\RequestToHttpAuthPlugin; use Shlinkio\Shlink\Rest\Authentication\RequestToHttpAuthPluginInterface; -use Shlinkio\Shlink\Rest\Exception\NoAuthenticationException; +use Shlinkio\Shlink\Rest\Exception\MissingAuthenticationException; use Shlinkio\Shlink\Rest\Exception\VerifyAuthenticationException; use Shlinkio\Shlink\Rest\Middleware\AuthenticationMiddleware; use Shlinkio\Shlink\Rest\Util\RestUtils; @@ -128,7 +128,7 @@ class AuthenticationMiddlewareTest extends TestCase }; yield 'container exception' => [$containerException]; - yield 'authentication exception' => [NoAuthenticationException::fromExpectedTypes([])]; + yield 'authentication exception' => [MissingAuthenticationException::fromExpectedTypes([])]; } /** @test */ From 5213faa0a17ce1173ff45d9a2c387fab73f67021 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 26 Nov 2019 22:03:40 +0100 Subject: [PATCH 31/40] Converted VerifyAuthenticationException into a problem details exception --- module/Rest/config/auth.config.php | 1 - module/Rest/src/Action/AuthenticateAction.php | 4 +- .../ShortUrl/EditShortUrlTagsAction.php | 2 +- .../Plugin/ApiKeyHeaderPlugin.php | 10 +-- .../Plugin/AuthorizationHeaderPlugin.php | 25 ++---- .../VerifyAuthenticationException.php | 81 ++++++++++++++----- .../Middleware/AuthenticationMiddleware.php | 36 ++------- module/Rest/src/Util/RestUtils.php | 46 +---------- .../VerifyAuthenticationExceptionTest.php | 30 ------- .../AuthenticationMiddlewareTest.php | 2 +- module/Rest/test/Util/RestUtilsTest.php | 41 ---------- 11 files changed, 79 insertions(+), 199 deletions(-) delete mode 100644 module/Rest/test/Util/RestUtilsTest.php diff --git a/module/Rest/config/auth.config.php b/module/Rest/config/auth.config.php index 7acb133e..9d0987e0 100644 --- a/module/Rest/config/auth.config.php +++ b/module/Rest/config/auth.config.php @@ -48,7 +48,6 @@ return [ Middleware\AuthenticationMiddleware::class => [ Authentication\RequestToHttpAuthPlugin::class, 'config.auth.routes_whitelist', - 'Logger_Shlink', ], ], diff --git a/module/Rest/src/Action/AuthenticateAction.php b/module/Rest/src/Action/AuthenticateAction.php index 1476f6de..bfaa745a 100644 --- a/module/Rest/src/Action/AuthenticateAction.php +++ b/module/Rest/src/Action/AuthenticateAction.php @@ -44,7 +44,7 @@ class AuthenticateAction extends AbstractRestAction $authData = $request->getParsedBody(); if (! isset($authData['apiKey'])) { return new JsonResponse([ - 'error' => RestUtils::INVALID_ARGUMENT_ERROR, + 'error' => 'INVALID_ARGUMENT', 'message' => 'You have to provide a valid API key under the "apiKey" param name.', ], self::STATUS_BAD_REQUEST); } @@ -53,7 +53,7 @@ class AuthenticateAction extends AbstractRestAction $apiKey = $this->apiKeyService->getByKey($authData['apiKey']); if ($apiKey === null || ! $apiKey->isValid()) { return new JsonResponse([ - 'error' => RestUtils::INVALID_API_KEY_ERROR, + 'error' => 'INVALID_API_KEY', 'message' => 'Provided API key does not exist or is invalid.', ], self::STATUS_UNAUTHORIZED); } diff --git a/module/Rest/src/Action/ShortUrl/EditShortUrlTagsAction.php b/module/Rest/src/Action/ShortUrl/EditShortUrlTagsAction.php index 9d82c608..1ab26cbb 100644 --- a/module/Rest/src/Action/ShortUrl/EditShortUrlTagsAction.php +++ b/module/Rest/src/Action/ShortUrl/EditShortUrlTagsAction.php @@ -38,7 +38,7 @@ class EditShortUrlTagsAction extends AbstractRestAction if (! isset($bodyParams['tags'])) { return new JsonResponse([ - 'error' => RestUtils::INVALID_ARGUMENT_ERROR, + 'error' => 'INVALID_ARGUMENT', 'message' => 'A list of tags was not provided', ], self::STATUS_BAD_REQUEST); } diff --git a/module/Rest/src/Authentication/Plugin/ApiKeyHeaderPlugin.php b/module/Rest/src/Authentication/Plugin/ApiKeyHeaderPlugin.php index d5cd38f9..14735f9a 100644 --- a/module/Rest/src/Authentication/Plugin/ApiKeyHeaderPlugin.php +++ b/module/Rest/src/Authentication/Plugin/ApiKeyHeaderPlugin.php @@ -8,7 +8,6 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Shlinkio\Shlink\Rest\Exception\VerifyAuthenticationException; use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface; -use Shlinkio\Shlink\Rest\Util\RestUtils; class ApiKeyHeaderPlugin implements AuthenticationPluginInterface { @@ -28,14 +27,9 @@ class ApiKeyHeaderPlugin implements AuthenticationPluginInterface public function verify(ServerRequestInterface $request): void { $apiKey = $request->getHeaderLine(self::HEADER_NAME); - if ($this->apiKeyService->check($apiKey)) { - return; + if (! $this->apiKeyService->check($apiKey)) { + throw VerifyAuthenticationException::forInvalidApiKey(); } - - throw VerifyAuthenticationException::withError( - RestUtils::INVALID_API_KEY_ERROR, - 'Provided API key does not exist or is invalid.' - ); } public function update(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface diff --git a/module/Rest/src/Authentication/Plugin/AuthorizationHeaderPlugin.php b/module/Rest/src/Authentication/Plugin/AuthorizationHeaderPlugin.php index d479d9d3..bf75d09b 100644 --- a/module/Rest/src/Authentication/Plugin/AuthorizationHeaderPlugin.php +++ b/module/Rest/src/Authentication/Plugin/AuthorizationHeaderPlugin.php @@ -8,7 +8,6 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Shlinkio\Shlink\Rest\Authentication\JWTServiceInterface; use Shlinkio\Shlink\Rest\Exception\VerifyAuthenticationException; -use Shlinkio\Shlink\Rest\Util\RestUtils; use Throwable; use function count; @@ -38,19 +37,13 @@ class AuthorizationHeaderPlugin implements AuthenticationPluginInterface $authToken = $request->getHeaderLine(self::HEADER_NAME); $authTokenParts = explode(' ', $authToken); if (count($authTokenParts) === 1) { - throw VerifyAuthenticationException::withError( - RestUtils::INVALID_AUTHORIZATION_ERROR, - sprintf('You need to provide the Bearer type in the %s header.', self::HEADER_NAME) - ); + throw VerifyAuthenticationException::forMissingAuthType(); } // Make sure the authorization type is Bearer [$authType, $jwt] = $authTokenParts; if (strtolower($authType) !== 'bearer') { - throw VerifyAuthenticationException::withError( - RestUtils::INVALID_AUTHORIZATION_ERROR, - sprintf('Provided authorization type %s is not supported. Use Bearer instead.', $authType) - ); + throw VerifyAuthenticationException::forInvalidAuthType($authType); } try { @@ -58,21 +51,13 @@ class AuthorizationHeaderPlugin implements AuthenticationPluginInterface throw $this->createInvalidTokenError(); } } catch (Throwable $e) { - throw $this->createInvalidTokenError($e); + throw $this->createInvalidTokenError(); } } - private function createInvalidTokenError(?Throwable $prev = null): VerifyAuthenticationException + private function createInvalidTokenError(): VerifyAuthenticationException { - return VerifyAuthenticationException::withError( - RestUtils::INVALID_AUTH_TOKEN_ERROR, - sprintf( - 'Missing or invalid auth token provided. Perform a new authentication request and send provided ' - . 'token on every new request on the %s header', - self::HEADER_NAME - ), - $prev - ); + return VerifyAuthenticationException::forInvalidAuthToken(); } public function update(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface diff --git a/module/Rest/src/Exception/VerifyAuthenticationException.php b/module/Rest/src/Exception/VerifyAuthenticationException.php index 4121685e..8720f421 100644 --- a/module/Rest/src/Exception/VerifyAuthenticationException.php +++ b/module/Rest/src/Exception/VerifyAuthenticationException.php @@ -4,38 +4,81 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Rest\Exception; -use Throwable; +use Fig\Http\Message\StatusCodeInterface; +use Zend\ProblemDetails\Exception\CommonProblemDetailsExceptionTrait; +use Zend\ProblemDetails\Exception\ProblemDetailsExceptionInterface; use function sprintf; -class VerifyAuthenticationException extends RuntimeException +class VerifyAuthenticationException extends RuntimeException implements ProblemDetailsExceptionInterface { + use CommonProblemDetailsExceptionTrait; + /** @var string */ private $errorCode; /** @var string */ private $publicMessage; - public function __construct( - string $errorCode, - string $publicMessage, - string $message = '', - int $code = 0, - ?Throwable $previous = null - ) { - parent::__construct($message, $code, $previous); - $this->errorCode = $errorCode; - $this->publicMessage = $publicMessage; + public static function forInvalidApiKey(): self + { + $e = new self('Provided API key does not exist or is invalid.'); + + $e->publicMessage = $e->getMessage(); + $e->errorCode = 'INVALID_API_KEY'; + $e->detail = $e->getMessage(); + $e->title = 'Invalid API key'; + $e->type = 'INVALID_API_KEY'; + $e->status = StatusCodeInterface::STATUS_UNAUTHORIZED; + + return $e; } - public static function withError(string $errorCode, string $publicMessage, ?Throwable $prev = null): self + /** @deprecated */ + public static function forInvalidAuthToken(): self { - return new self( - $errorCode, - $publicMessage, - sprintf('Authentication verification failed with the public message "%s"', $publicMessage), - 0, - $prev + $e = new self( + 'Missing or invalid auth token provided. Perform a new authentication request and send provided ' + . 'token on every new request on the Authorization header' ); + + $e->publicMessage = $e->getMessage(); + $e->errorCode = 'INVALID_AUTH_TOKEN'; + $e->detail = $e->getMessage(); + $e->title = 'Invalid auth token'; + $e->type = 'INVALID_AUTH_TOKEN'; + $e->status = StatusCodeInterface::STATUS_UNAUTHORIZED; + + return $e; + } + + /** @deprecated */ + public static function forMissingAuthType(): self + { + $e = new self('You need to provide the Bearer type in the Authorization header.'); + + $e->publicMessage = $e->getMessage(); + $e->errorCode = 'INVALID_AUTHORIZATION'; + $e->detail = $e->getMessage(); + $e->title = 'Invalid authorization'; + $e->type = 'INVALID_AUTHORIZATION'; + $e->status = StatusCodeInterface::STATUS_UNAUTHORIZED; + + return $e; + } + + /** @deprecated */ + public static function forInvalidAuthType(string $providedType): self + { + $e = new self(sprintf('Provided authorization type %s is not supported. Use Bearer instead.', $providedType)); + + $e->publicMessage = $e->getMessage(); + $e->errorCode = 'INVALID_AUTHORIZATION'; + $e->detail = $e->getMessage(); + $e->title = 'Invalid authorization'; + $e->type = 'INVALID_AUTHORIZATION'; + $e->status = StatusCodeInterface::STATUS_UNAUTHORIZED; + + return $e; } public function getErrorCode(): string diff --git a/module/Rest/src/Middleware/AuthenticationMiddleware.php b/module/Rest/src/Middleware/AuthenticationMiddleware.php index 41dec7dc..4fd44bcf 100644 --- a/module/Rest/src/Middleware/AuthenticationMiddleware.php +++ b/module/Rest/src/Middleware/AuthenticationMiddleware.php @@ -10,33 +10,22 @@ use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -use Psr\Log\LoggerInterface; -use Psr\Log\NullLogger; use Shlinkio\Shlink\Rest\Authentication\RequestToHttpAuthPluginInterface; -use Shlinkio\Shlink\Rest\Exception\VerifyAuthenticationException; -use Shlinkio\Shlink\Rest\Util\RestUtils; -use Zend\Diactoros\Response\JsonResponse; use Zend\Expressive\Router\RouteResult; use function Functional\contains; class AuthenticationMiddleware implements MiddlewareInterface, StatusCodeInterface, RequestMethodInterface { - /** @var LoggerInterface */ - private $logger; /** @var array */ private $routesWhitelist; /** @var RequestToHttpAuthPluginInterface */ private $requestToAuthPlugin; - public function __construct( - RequestToHttpAuthPluginInterface $requestToAuthPlugin, - array $routesWhitelist, - ?LoggerInterface $logger = null - ) { + public function __construct(RequestToHttpAuthPluginInterface $requestToAuthPlugin, array $routesWhitelist) + { $this->routesWhitelist = $routesWhitelist; $this->requestToAuthPlugin = $requestToAuthPlugin; - $this->logger = $logger ?: new NullLogger(); } public function process(Request $request, RequestHandlerInterface $handler): Response @@ -53,24 +42,9 @@ class AuthenticationMiddleware implements MiddlewareInterface, StatusCodeInterfa } $plugin = $this->requestToAuthPlugin->fromRequest($request); + $plugin->verify($request); + $response = $handler->handle($request); - try { - $plugin->verify($request); - $response = $handler->handle($request); - return $plugin->update($request, $response); - } catch (VerifyAuthenticationException $e) { - $this->logger->warning('Authentication verification failed. {e}', ['e' => $e]); - return $this->createErrorResponse($e->getPublicMessage(), $e->getErrorCode()); - } - } - - private function createErrorResponse( - string $message, - string $errorCode = RestUtils::INVALID_AUTHORIZATION_ERROR - ): JsonResponse { - return new JsonResponse([ - 'error' => $errorCode, - 'message' => $message, - ], self::STATUS_UNAUTHORIZED); + return $plugin->update($request, $response); } } diff --git a/module/Rest/src/Util/RestUtils.php b/module/Rest/src/Util/RestUtils.php index 1dad3908..a6c349bf 100644 --- a/module/Rest/src/Util/RestUtils.php +++ b/module/Rest/src/Util/RestUtils.php @@ -4,54 +4,10 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Rest\Util; -use Shlinkio\Shlink\Common\Exception as Common; -use Shlinkio\Shlink\Core\Exception as Core; use Shlinkio\Shlink\Rest\Exception as Rest; -use Throwable; +/** @deprecated */ class RestUtils { - /** @deprecated */ - public const INVALID_SHORTCODE_ERROR = Core\ShortUrlNotFoundException::TYPE; - /** @deprecated */ - public const INVALID_SHORTCODE_DELETION_ERROR = Core\DeleteShortUrlException::TYPE; - /** @deprecated */ - public const INVALID_URL_ERROR = Core\InvalidUrlException::TYPE; - /** @deprecated */ - public const INVALID_ARGUMENT_ERROR = Core\ValidationException::TYPE; - /** @deprecated */ - public const INVALID_SLUG_ERROR = Core\NonUniqueSlugException::TYPE; - /** @deprecated */ - public const NOT_FOUND_ERROR = Core\TagNotFoundException::TYPE; - /** @deprecated */ - public const UNKNOWN_ERROR = 'UNKNOWN_ERROR'; - - public const INVALID_CREDENTIALS_ERROR = 'INVALID_CREDENTIALS'; - public const INVALID_AUTH_TOKEN_ERROR = 'INVALID_AUTH_TOKEN'; - /** @deprecated */ public const INVALID_AUTHORIZATION_ERROR = Rest\MissingAuthenticationException::TYPE; - public const INVALID_API_KEY_ERROR = 'INVALID_API_KEY'; - - /** @deprecated */ - public static function getRestErrorCodeFromException(Throwable $e): string - { - switch (true) { - case $e instanceof Core\ShortUrlNotFoundException: - return self::INVALID_SHORTCODE_ERROR; - case $e instanceof Core\InvalidUrlException: - return self::INVALID_URL_ERROR; - case $e instanceof Core\NonUniqueSlugException: - return self::INVALID_SLUG_ERROR; - case $e instanceof Common\InvalidArgumentException: - case $e instanceof Core\InvalidArgumentException: - case $e instanceof Core\ValidationException: - return self::INVALID_ARGUMENT_ERROR; - case $e instanceof Rest\AuthenticationException: - return self::INVALID_CREDENTIALS_ERROR; - case $e instanceof Core\DeleteShortUrlException: - return self::INVALID_SHORTCODE_DELETION_ERROR; - default: - return self::UNKNOWN_ERROR; - } - } } diff --git a/module/Rest/test/Exception/VerifyAuthenticationExceptionTest.php b/module/Rest/test/Exception/VerifyAuthenticationExceptionTest.php index d19fe0f6..9554c62d 100644 --- a/module/Rest/test/Exception/VerifyAuthenticationExceptionTest.php +++ b/module/Rest/test/Exception/VerifyAuthenticationExceptionTest.php @@ -13,41 +13,11 @@ use Throwable; use function array_map; use function random_int; use function range; -use function sprintf; class VerifyAuthenticationExceptionTest extends TestCase { use StringUtilsTrait; - /** - * @test - * @dataProvider provideExceptionData - */ - public function withErrorCreatesExpectedException(string $code, string $message, ?Throwable $prev): void - { - $e = VerifyAuthenticationException::withError($code, $message, $prev); - - $this->assertEquals(0, $e->getCode()); - $this->assertEquals( - sprintf('Authentication verification failed with the public message "%s"', $message), - $e->getMessage() - ); - $this->assertEquals($code, $e->getErrorCode()); - $this->assertEquals($message, $e->getPublicMessage()); - $this->assertEquals($prev, $e->getPrevious()); - } - - public function provideExceptionData(): iterable - { - return array_map(function () { - return [ - $this->generateRandomString(), - $this->generateRandomString(50), - random_int(0, 1) === 1 ? new Exception('Prev') : null, - ]; - }, range(1, 10)); - } - /** * @test * @dataProvider provideConstructorData diff --git a/module/Rest/test/Middleware/AuthenticationMiddlewareTest.php b/module/Rest/test/Middleware/AuthenticationMiddlewareTest.php index 054db85e..eb150f57 100644 --- a/module/Rest/test/Middleware/AuthenticationMiddlewareTest.php +++ b/module/Rest/test/Middleware/AuthenticationMiddlewareTest.php @@ -138,7 +138,7 @@ class AuthenticationMiddlewareTest extends TestCase RouteResult::class, RouteResult::fromRoute(new Route('bar', $this->getDummyMiddleware()), []) ); - $e = VerifyAuthenticationException::withError('the_error', 'the_message'); + $e = VerifyAuthenticationException::forInvalidApiKey(); $plugin = $this->prophesize(AuthenticationPluginInterface::class); $verify = $plugin->verify($request)->willThrow($e); diff --git a/module/Rest/test/Util/RestUtilsTest.php b/module/Rest/test/Util/RestUtilsTest.php deleted file mode 100644 index 6097cdc6..00000000 --- a/module/Rest/test/Util/RestUtilsTest.php +++ /dev/null @@ -1,41 +0,0 @@ -assertEquals( - RestUtils::INVALID_SHORTCODE_ERROR, - RestUtils::getRestErrorCodeFromException(new ShortUrlNotFoundException()) - ); - $this->assertEquals( - RestUtils::INVALID_URL_ERROR, - RestUtils::getRestErrorCodeFromException(new InvalidUrlException()) - ); - $this->assertEquals( - RestUtils::INVALID_ARGUMENT_ERROR, - RestUtils::getRestErrorCodeFromException(new InvalidArgumentException()) - ); - $this->assertEquals( - RestUtils::INVALID_CREDENTIALS_ERROR, - RestUtils::getRestErrorCodeFromException(new AuthenticationException()) - ); - $this->assertEquals( - RestUtils::UNKNOWN_ERROR, - RestUtils::getRestErrorCodeFromException(new WrongIpException()) - ); - } -} From 3b56fc37608eeec4ea6c2237f804df46b900d060 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 26 Nov 2019 22:12:52 +0100 Subject: [PATCH 32/40] Refactored and fixed unit tests --- .../src/Exception/DeleteShortUrlException.php | 2 +- .../src/Exception/InvalidUrlException.php | 2 +- .../src/Exception/NonUniqueSlugException.php | 2 +- .../Exception/ShortUrlNotFoundException.php | 2 +- .../src/Exception/TagNotFoundException.php | 2 +- .../src/Exception/ValidationException.php | 2 +- module/Rest/src/Action/AuthenticateAction.php | 1 - .../ShortUrl/EditShortUrlTagsAction.php | 1 - .../MissingAuthenticationException.php | 2 +- .../VerifyAuthenticationException.php | 23 ------ module/Rest/src/Util/RestUtils.php | 13 ---- .../RequestToAuthPluginTest.php | 2 +- .../VerifyAuthenticationExceptionTest.php | 52 +------------ .../AuthenticationMiddlewareTest.php | 75 ------------------- phpstan.neon | 1 - 15 files changed, 11 insertions(+), 171 deletions(-) delete mode 100644 module/Rest/src/Util/RestUtils.php diff --git a/module/Core/src/Exception/DeleteShortUrlException.php b/module/Core/src/Exception/DeleteShortUrlException.php index 037fe942..984c2cbc 100644 --- a/module/Core/src/Exception/DeleteShortUrlException.php +++ b/module/Core/src/Exception/DeleteShortUrlException.php @@ -16,7 +16,7 @@ class DeleteShortUrlException extends DomainException implements ProblemDetailsE use CommonProblemDetailsExceptionTrait; private const TITLE = 'Cannot delete short URL'; - public const TYPE = 'INVALID_SHORTCODE_DELETION'; // FIXME Should be INVALID_SHORT_URL_DELETION + private const TYPE = 'INVALID_SHORTCODE_DELETION'; // FIXME Should be INVALID_SHORT_URL_DELETION /** @var int */ private $visitsThreshold; diff --git a/module/Core/src/Exception/InvalidUrlException.php b/module/Core/src/Exception/InvalidUrlException.php index 32758b4e..0b741910 100644 --- a/module/Core/src/Exception/InvalidUrlException.php +++ b/module/Core/src/Exception/InvalidUrlException.php @@ -16,7 +16,7 @@ class InvalidUrlException extends DomainException implements ProblemDetailsExcep use CommonProblemDetailsExceptionTrait; private const TITLE = 'Invalid URL'; - public const TYPE = 'INVALID_URL'; + private const TYPE = 'INVALID_URL'; public static function fromUrl(string $url, ?Throwable $previous = null): self { diff --git a/module/Core/src/Exception/NonUniqueSlugException.php b/module/Core/src/Exception/NonUniqueSlugException.php index 74e03f06..9ef7365d 100644 --- a/module/Core/src/Exception/NonUniqueSlugException.php +++ b/module/Core/src/Exception/NonUniqueSlugException.php @@ -15,7 +15,7 @@ class NonUniqueSlugException extends InvalidArgumentException implements Problem use CommonProblemDetailsExceptionTrait; private const TITLE = 'Invalid custom slug'; - public const TYPE = 'INVALID_SLUG'; + private const TYPE = 'INVALID_SLUG'; public static function fromSlug(string $slug, ?string $domain): self { diff --git a/module/Core/src/Exception/ShortUrlNotFoundException.php b/module/Core/src/Exception/ShortUrlNotFoundException.php index e07624c7..9617a486 100644 --- a/module/Core/src/Exception/ShortUrlNotFoundException.php +++ b/module/Core/src/Exception/ShortUrlNotFoundException.php @@ -15,7 +15,7 @@ class ShortUrlNotFoundException extends DomainException implements ProblemDetail use CommonProblemDetailsExceptionTrait; private const TITLE = 'Short URL not found'; - public const TYPE = 'INVALID_SHORTCODE'; + private const TYPE = 'INVALID_SHORTCODE'; public static function fromNotFoundShortCode(string $shortCode, ?string $domain = null): self { diff --git a/module/Core/src/Exception/TagNotFoundException.php b/module/Core/src/Exception/TagNotFoundException.php index 6ff36bb2..33cf1345 100644 --- a/module/Core/src/Exception/TagNotFoundException.php +++ b/module/Core/src/Exception/TagNotFoundException.php @@ -15,7 +15,7 @@ class TagNotFoundException extends DomainException implements ProblemDetailsExce use CommonProblemDetailsExceptionTrait; private const TITLE = 'Tag not found'; - public const TYPE = 'TAG_NOT_FOUND'; + private const TYPE = 'TAG_NOT_FOUND'; public static function fromTag(string $tag): self { diff --git a/module/Core/src/Exception/ValidationException.php b/module/Core/src/Exception/ValidationException.php index eea23a5a..c4ff2354 100644 --- a/module/Core/src/Exception/ValidationException.php +++ b/module/Core/src/Exception/ValidationException.php @@ -22,7 +22,7 @@ class ValidationException extends InvalidArgumentException implements ProblemDet use CommonProblemDetailsExceptionTrait; private const TITLE = 'Invalid data'; - public const TYPE = 'INVALID_ARGUMENT'; + private const TYPE = 'INVALID_ARGUMENT'; /** @var array */ private $invalidElements; diff --git a/module/Rest/src/Action/AuthenticateAction.php b/module/Rest/src/Action/AuthenticateAction.php index bfaa745a..b0919aae 100644 --- a/module/Rest/src/Action/AuthenticateAction.php +++ b/module/Rest/src/Action/AuthenticateAction.php @@ -10,7 +10,6 @@ use Psr\Log\LoggerInterface; use Shlinkio\Shlink\Rest\Authentication\JWTServiceInterface; use Shlinkio\Shlink\Rest\Service\ApiKeyService; use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface; -use Shlinkio\Shlink\Rest\Util\RestUtils; use Zend\Diactoros\Response\JsonResponse; /** @deprecated */ diff --git a/module/Rest/src/Action/ShortUrl/EditShortUrlTagsAction.php b/module/Rest/src/Action/ShortUrl/EditShortUrlTagsAction.php index 1ab26cbb..3f057e75 100644 --- a/module/Rest/src/Action/ShortUrl/EditShortUrlTagsAction.php +++ b/module/Rest/src/Action/ShortUrl/EditShortUrlTagsAction.php @@ -9,7 +9,6 @@ use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Log\LoggerInterface; use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface; use Shlinkio\Shlink\Rest\Action\AbstractRestAction; -use Shlinkio\Shlink\Rest\Util\RestUtils; use Zend\Diactoros\Response\JsonResponse; class EditShortUrlTagsAction extends AbstractRestAction diff --git a/module/Rest/src/Exception/MissingAuthenticationException.php b/module/Rest/src/Exception/MissingAuthenticationException.php index 6ab55458..6ed76e2a 100644 --- a/module/Rest/src/Exception/MissingAuthenticationException.php +++ b/module/Rest/src/Exception/MissingAuthenticationException.php @@ -16,7 +16,7 @@ class MissingAuthenticationException extends RuntimeException implements Problem use CommonProblemDetailsExceptionTrait; private const TITLE = 'Invalid authorization'; - public const TYPE = 'INVALID_AUTHORIZATION'; + private const TYPE = 'INVALID_AUTHORIZATION'; public static function fromExpectedTypes(array $expectedTypes): self { diff --git a/module/Rest/src/Exception/VerifyAuthenticationException.php b/module/Rest/src/Exception/VerifyAuthenticationException.php index 8720f421..3b2a4115 100644 --- a/module/Rest/src/Exception/VerifyAuthenticationException.php +++ b/module/Rest/src/Exception/VerifyAuthenticationException.php @@ -14,17 +14,10 @@ class VerifyAuthenticationException extends RuntimeException implements ProblemD { use CommonProblemDetailsExceptionTrait; - /** @var string */ - private $errorCode; - /** @var string */ - private $publicMessage; - public static function forInvalidApiKey(): self { $e = new self('Provided API key does not exist or is invalid.'); - $e->publicMessage = $e->getMessage(); - $e->errorCode = 'INVALID_API_KEY'; $e->detail = $e->getMessage(); $e->title = 'Invalid API key'; $e->type = 'INVALID_API_KEY'; @@ -41,8 +34,6 @@ class VerifyAuthenticationException extends RuntimeException implements ProblemD . 'token on every new request on the Authorization header' ); - $e->publicMessage = $e->getMessage(); - $e->errorCode = 'INVALID_AUTH_TOKEN'; $e->detail = $e->getMessage(); $e->title = 'Invalid auth token'; $e->type = 'INVALID_AUTH_TOKEN'; @@ -56,8 +47,6 @@ class VerifyAuthenticationException extends RuntimeException implements ProblemD { $e = new self('You need to provide the Bearer type in the Authorization header.'); - $e->publicMessage = $e->getMessage(); - $e->errorCode = 'INVALID_AUTHORIZATION'; $e->detail = $e->getMessage(); $e->title = 'Invalid authorization'; $e->type = 'INVALID_AUTHORIZATION'; @@ -71,8 +60,6 @@ class VerifyAuthenticationException extends RuntimeException implements ProblemD { $e = new self(sprintf('Provided authorization type %s is not supported. Use Bearer instead.', $providedType)); - $e->publicMessage = $e->getMessage(); - $e->errorCode = 'INVALID_AUTHORIZATION'; $e->detail = $e->getMessage(); $e->title = 'Invalid authorization'; $e->type = 'INVALID_AUTHORIZATION'; @@ -80,14 +67,4 @@ class VerifyAuthenticationException extends RuntimeException implements ProblemD return $e; } - - public function getErrorCode(): string - { - return $this->errorCode; - } - - public function getPublicMessage(): string - { - return $this->publicMessage; - } } diff --git a/module/Rest/src/Util/RestUtils.php b/module/Rest/src/Util/RestUtils.php deleted file mode 100644 index a6c349bf..00000000 --- a/module/Rest/src/Util/RestUtils.php +++ /dev/null @@ -1,13 +0,0 @@ -expectException(MissingAuthenticationException::class); $this->expectExceptionMessage(sprintf( - 'None of the valid authentication mechanisms where provided. Expected one of ["%s"]', + 'Expected one of the following authentication headers, but none were provided, ["%s"]', implode('", "', RequestToHttpAuthPlugin::SUPPORTED_AUTH_HEADERS) )); diff --git a/module/Rest/test/Exception/VerifyAuthenticationExceptionTest.php b/module/Rest/test/Exception/VerifyAuthenticationExceptionTest.php index 9554c62d..28563c5f 100644 --- a/module/Rest/test/Exception/VerifyAuthenticationExceptionTest.php +++ b/module/Rest/test/Exception/VerifyAuthenticationExceptionTest.php @@ -4,62 +4,16 @@ declare(strict_types=1); namespace ShlinkioTest\Shlink\Rest\Exception; -use Exception; use PHPUnit\Framework\TestCase; -use Shlinkio\Shlink\Common\Util\StringUtilsTrait; use Shlinkio\Shlink\Rest\Exception\VerifyAuthenticationException; -use Throwable; - -use function array_map; -use function random_int; -use function range; class VerifyAuthenticationExceptionTest extends TestCase { - use StringUtilsTrait; - - /** - * @test - * @dataProvider provideConstructorData - */ - public function constructCreatesExpectedException( - string $errorCode, - string $publicMessage, - string $message, - int $code, - ?Throwable $prev - ): void { - $e = new VerifyAuthenticationException($errorCode, $publicMessage, $message, $code, $prev); - - $this->assertEquals($code, $e->getCode()); - $this->assertEquals($message, $e->getMessage()); - $this->assertEquals($errorCode, $e->getErrorCode()); - $this->assertEquals($publicMessage, $e->getPublicMessage()); - $this->assertEquals($prev, $e->getPrevious()); - } - - public function provideConstructorData(): iterable - { - return array_map(function (int $i) { - return [ - $this->generateRandomString(), - $this->generateRandomString(30), - $this->generateRandomString(50), - $i, - random_int(0, 1) === 1 ? new Exception('Prev') : null, - ]; - }, range(10, 20)); - } - /** @test */ - public function defaultConstructorValuesAreKept(): void + public function createsExpectedExceptionForInvalidApiKey(): void { - $e = new VerifyAuthenticationException('foo', 'bar'); + $e = VerifyAuthenticationException::forInvalidApiKey(); - $this->assertEquals(0, $e->getCode()); - $this->assertEquals('', $e->getMessage()); - $this->assertEquals('foo', $e->getErrorCode()); - $this->assertEquals('bar', $e->getPublicMessage()); - $this->assertNull($e->getPrevious()); + $this->assertEquals('Provided API key does not exist or is invalid.', $e->getMessage()); } } diff --git a/module/Rest/test/Middleware/AuthenticationMiddlewareTest.php b/module/Rest/test/Middleware/AuthenticationMiddlewareTest.php index eb150f57..36dfaeee 100644 --- a/module/Rest/test/Middleware/AuthenticationMiddlewareTest.php +++ b/module/Rest/test/Middleware/AuthenticationMiddlewareTest.php @@ -4,12 +4,10 @@ declare(strict_types=1); namespace ShlinkioTest\Shlink\Rest\Middleware; -use Exception; use Fig\Http\Message\RequestMethodInterface; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\Prophecy\ObjectProphecy; -use Psr\Container\ContainerExceptionInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; @@ -17,20 +15,13 @@ use Psr\Http\Server\RequestHandlerInterface; use Psr\Log\LoggerInterface; use Shlinkio\Shlink\Rest\Action\AuthenticateAction; use Shlinkio\Shlink\Rest\Authentication\Plugin\AuthenticationPluginInterface; -use Shlinkio\Shlink\Rest\Authentication\RequestToHttpAuthPlugin; use Shlinkio\Shlink\Rest\Authentication\RequestToHttpAuthPluginInterface; -use Shlinkio\Shlink\Rest\Exception\MissingAuthenticationException; -use Shlinkio\Shlink\Rest\Exception\VerifyAuthenticationException; use Shlinkio\Shlink\Rest\Middleware\AuthenticationMiddleware; -use Shlinkio\Shlink\Rest\Util\RestUtils; -use Throwable; use Zend\Diactoros\Response; use Zend\Diactoros\ServerRequest; use Zend\Expressive\Router\Route; use Zend\Expressive\Router\RouteResult; -use function implode; -use function sprintf; use function Zend\Stratigility\middleware; class AuthenticationMiddlewareTest extends TestCase @@ -93,72 +84,6 @@ class AuthenticationMiddlewareTest extends TestCase )->withMethod(RequestMethodInterface::METHOD_OPTIONS)]; } - /** - * @test - * @dataProvider provideExceptions - */ - public function errorIsReturnedWhenNoValidAuthIsProvided(Throwable $e): void - { - $request = (new ServerRequest())->withAttribute( - RouteResult::class, - RouteResult::fromRoute(new Route('bar', $this->getDummyMiddleware()), []) - ); - $fromRequest = $this->requestToPlugin->fromRequest(Argument::any())->willThrow($e); - $logWarning = $this->logger->warning('Invalid or no authentication provided. {e}', ['e' => $e])->will( - function () { - } - ); - - /** @var Response\JsonResponse $response */ - $response = $this->middleware->process($request, $this->prophesize(RequestHandlerInterface::class)->reveal()); - $payload = $response->getPayload(); - - $this->assertEquals(RestUtils::INVALID_AUTHORIZATION_ERROR, $payload['error']); - $this->assertEquals(sprintf( - 'Expected one of the following authentication headers, but none were provided, ["%s"]', - implode('", "', RequestToHttpAuthPlugin::SUPPORTED_AUTH_HEADERS) - ), $payload['message']); - $fromRequest->shouldHaveBeenCalledOnce(); - $logWarning->shouldHaveBeenCalledOnce(); - } - - public function provideExceptions(): iterable - { - $containerException = new class extends Exception implements ContainerExceptionInterface { - }; - - yield 'container exception' => [$containerException]; - yield 'authentication exception' => [MissingAuthenticationException::fromExpectedTypes([])]; - } - - /** @test */ - public function errorIsReturnedWhenVerificationFails(): void - { - $request = (new ServerRequest())->withAttribute( - RouteResult::class, - RouteResult::fromRoute(new Route('bar', $this->getDummyMiddleware()), []) - ); - $e = VerifyAuthenticationException::forInvalidApiKey(); - $plugin = $this->prophesize(AuthenticationPluginInterface::class); - - $verify = $plugin->verify($request)->willThrow($e); - $fromRequest = $this->requestToPlugin->fromRequest(Argument::any())->willReturn($plugin->reveal()); - $logWarning = $this->logger->warning('Authentication verification failed. {e}', ['e' => $e])->will( - function () { - } - ); - - /** @var Response\JsonResponse $response */ - $response = $this->middleware->process($request, $this->prophesize(RequestHandlerInterface::class)->reveal()); - $payload = $response->getPayload(); - - $this->assertEquals('the_error', $payload['error']); - $this->assertEquals('the_message', $payload['message']); - $verify->shouldHaveBeenCalledOnce(); - $fromRequest->shouldHaveBeenCalledOnce(); - $logWarning->shouldHaveBeenCalledOnce(); - } - /** @test */ public function updatedResponseIsReturnedWhenVerificationPasses(): void { diff --git a/phpstan.neon b/phpstan.neon index 7df6d88f..2d9d960d 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,6 +1,5 @@ parameters: ignoreErrors: - - '#is not subtype of Throwable#' - '#Undefined variable: \$metadata#' - '#AbstractQuery::setParameters()#' - '#mustRun()#' From fffb2872ef36cc34309615200fb421ef1443bc84 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 26 Nov 2019 22:18:55 +0100 Subject: [PATCH 33/40] Replaced hardcoded error response by the use of a problem details action --- .../src/Action/ShortUrl/EditShortUrlTagsAction.php | 13 ++++--------- ...nTagsTest.php => EditShortUrlTagsActionTest.php} | 2 +- .../Action/ShortUrl/EditShortUrlTagsActionTest.php | 5 +++-- 3 files changed, 8 insertions(+), 12 deletions(-) rename module/Rest/test-api/Action/{EditShortUrlActionTagsTest.php => EditShortUrlTagsActionTest.php} (95%) diff --git a/module/Rest/src/Action/ShortUrl/EditShortUrlTagsAction.php b/module/Rest/src/Action/ShortUrl/EditShortUrlTagsAction.php index 3f057e75..208be169 100644 --- a/module/Rest/src/Action/ShortUrl/EditShortUrlTagsAction.php +++ b/module/Rest/src/Action/ShortUrl/EditShortUrlTagsAction.php @@ -7,6 +7,7 @@ namespace Shlinkio\Shlink\Rest\Action\ShortUrl; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Log\LoggerInterface; +use Shlinkio\Shlink\Core\Exception\ValidationException; use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface; use Shlinkio\Shlink\Rest\Action\AbstractRestAction; use Zend\Diactoros\Response\JsonResponse; @@ -25,21 +26,15 @@ class EditShortUrlTagsAction extends AbstractRestAction $this->shortUrlService = $shortUrlService; } - /** - * @param Request $request - * @return Response - * @throws \InvalidArgumentException - */ public function handle(Request $request): Response { $shortCode = $request->getAttribute('shortCode'); $bodyParams = $request->getParsedBody(); if (! isset($bodyParams['tags'])) { - return new JsonResponse([ - 'error' => 'INVALID_ARGUMENT', - 'message' => 'A list of tags was not provided', - ], self::STATUS_BAD_REQUEST); + throw ValidationException::fromArray([ + 'tags' => 'List of tags has to be provided', + ]); } $tags = $bodyParams['tags']; diff --git a/module/Rest/test-api/Action/EditShortUrlActionTagsTest.php b/module/Rest/test-api/Action/EditShortUrlTagsActionTest.php similarity index 95% rename from module/Rest/test-api/Action/EditShortUrlActionTagsTest.php rename to module/Rest/test-api/Action/EditShortUrlTagsActionTest.php index e6cae9b6..5116d1e6 100644 --- a/module/Rest/test-api/Action/EditShortUrlActionTagsTest.php +++ b/module/Rest/test-api/Action/EditShortUrlTagsActionTest.php @@ -7,7 +7,7 @@ namespace ShlinkioApiTest\Shlink\Rest\Action; use GuzzleHttp\RequestOptions; use Shlinkio\Shlink\TestUtils\ApiTest\ApiTestCase; -class EditShortUrlActionTagsTest extends ApiTestCase +class EditShortUrlTagsActionTest extends ApiTestCase { /** @test */ public function notProvidingTagsReturnsBadRequest(): void diff --git a/module/Rest/test/Action/ShortUrl/EditShortUrlTagsActionTest.php b/module/Rest/test/Action/ShortUrl/EditShortUrlTagsActionTest.php index 5c0ec628..17293f05 100644 --- a/module/Rest/test/Action/ShortUrl/EditShortUrlTagsActionTest.php +++ b/module/Rest/test/Action/ShortUrl/EditShortUrlTagsActionTest.php @@ -7,6 +7,7 @@ namespace ShlinkioTest\Shlink\Rest\Action\ShortUrl; use PHPUnit\Framework\TestCase; use Prophecy\Prophecy\ObjectProphecy; use Shlinkio\Shlink\Core\Entity\ShortUrl; +use Shlinkio\Shlink\Core\Exception\ValidationException; use Shlinkio\Shlink\Core\Service\ShortUrlService; use Shlinkio\Shlink\Rest\Action\ShortUrl\EditShortUrlTagsAction; use Zend\Diactoros\ServerRequest; @@ -27,8 +28,8 @@ class EditShortUrlTagsActionTest extends TestCase /** @test */ public function notProvidingTagsReturnsError(): void { - $response = $this->action->handle((new ServerRequest())->withAttribute('shortCode', 'abc123')); - $this->assertEquals(400, $response->getStatusCode()); + $this->expectException(ValidationException::class); + $this->action->handle((new ServerRequest())->withAttribute('shortCode', 'abc123')); } /** @test */ From 5266743a0c19842618f89c86f1b0546962c53eaa Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 27 Nov 2019 20:18:36 +0100 Subject: [PATCH 34/40] Added as much additional data as possible to exceptions --- .../ShortUrl/DeleteShortUrlCommandTest.php | 4 +-- .../src/Exception/DeleteShortUrlException.php | 19 ++++-------- .../src/Exception/InvalidUrlException.php | 1 + .../src/Exception/NonUniqueSlugException.php | 5 ++++ .../Exception/ShortUrlNotFoundException.php | 5 ++++ .../src/Exception/TagNotFoundException.php | 1 + .../src/Exception/ValidationException.php | 23 +++------------ .../Exception/DeleteShortUrlExceptionTest.php | 23 ++------------- .../Exception/ValidationExceptionTest.php | 29 ------------------- 9 files changed, 26 insertions(+), 84 deletions(-) diff --git a/module/CLI/test/Command/ShortUrl/DeleteShortUrlCommandTest.php b/module/CLI/test/Command/ShortUrl/DeleteShortUrlCommandTest.php index 76414481..6a036c62 100644 --- a/module/CLI/test/Command/ShortUrl/DeleteShortUrlCommandTest.php +++ b/module/CLI/test/Command/ShortUrl/DeleteShortUrlCommandTest.php @@ -83,7 +83,7 @@ class DeleteShortUrlCommandTest extends TestCase $ignoreThreshold = array_pop($args); if (!$ignoreThreshold) { - throw new Exception\DeleteShortUrlException(10); + throw Exception\DeleteShortUrlException::fromVisitsThreshold(10, ''); } } ); @@ -112,7 +112,7 @@ class DeleteShortUrlCommandTest extends TestCase { $shortCode = 'abc123'; $deleteByShortCode = $this->service->deleteByShortCode($shortCode, false)->willThrow( - new Exception\DeleteShortUrlException(10) + Exception\DeleteShortUrlException::fromVisitsThreshold(10, '') ); $this->commandTester->setInputs(['no']); diff --git a/module/Core/src/Exception/DeleteShortUrlException.php b/module/Core/src/Exception/DeleteShortUrlException.php index 984c2cbc..ddd77418 100644 --- a/module/Core/src/Exception/DeleteShortUrlException.php +++ b/module/Core/src/Exception/DeleteShortUrlException.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Exception; use Fig\Http\Message\StatusCodeInterface; -use Throwable; use Zend\ProblemDetails\Exception\CommonProblemDetailsExceptionTrait; use Zend\ProblemDetails\Exception\ProblemDetailsExceptionInterface; @@ -18,18 +17,9 @@ class DeleteShortUrlException extends DomainException implements ProblemDetailsE private const TITLE = 'Cannot delete short URL'; private const TYPE = 'INVALID_SHORTCODE_DELETION'; // FIXME Should be INVALID_SHORT_URL_DELETION - /** @var int */ - private $visitsThreshold; - - public function __construct(int $visitsThreshold, string $message = '', int $code = 0, ?Throwable $previous = null) - { - $this->visitsThreshold = $visitsThreshold; - parent::__construct($message, $code, $previous); - } - public static function fromVisitsThreshold(int $threshold, string $shortCode): self { - $e = new self($threshold, sprintf( + $e = new self(sprintf( 'Impossible to delete short URL with short code "%s" since it has more than "%s" visits.', $shortCode, $threshold @@ -39,13 +29,16 @@ class DeleteShortUrlException extends DomainException implements ProblemDetailsE $e->title = self::TITLE; $e->type = self::TYPE; $e->status = StatusCodeInterface::STATUS_UNPROCESSABLE_ENTITY; - $e->additional = ['threshold' => $threshold]; + $e->additional = [ + 'shortCode' => $shortCode, + 'threshold' => $threshold, + ]; return $e; } public function getVisitsThreshold(): int { - return $this->visitsThreshold; + return $this->additional['threshold']; } } diff --git a/module/Core/src/Exception/InvalidUrlException.php b/module/Core/src/Exception/InvalidUrlException.php index 0b741910..ffec94df 100644 --- a/module/Core/src/Exception/InvalidUrlException.php +++ b/module/Core/src/Exception/InvalidUrlException.php @@ -27,6 +27,7 @@ class InvalidUrlException extends DomainException implements ProblemDetailsExcep $e->title = self::TITLE; $e->type = self::TYPE; $e->status = $status; + $e->additional = ['url' => $url]; return $e; } diff --git a/module/Core/src/Exception/NonUniqueSlugException.php b/module/Core/src/Exception/NonUniqueSlugException.php index 9ef7365d..bcd40268 100644 --- a/module/Core/src/Exception/NonUniqueSlugException.php +++ b/module/Core/src/Exception/NonUniqueSlugException.php @@ -26,6 +26,11 @@ class NonUniqueSlugException extends InvalidArgumentException implements Problem $e->title = self::TITLE; $e->type = self::TYPE; $e->status = StatusCodeInterface::STATUS_BAD_REQUEST; + $e->additional = ['customSlug' => $slug]; + + if ($domain !== null) { + $e->additional['domain'] = $domain; + } return $e; } diff --git a/module/Core/src/Exception/ShortUrlNotFoundException.php b/module/Core/src/Exception/ShortUrlNotFoundException.php index 9617a486..aca1bce9 100644 --- a/module/Core/src/Exception/ShortUrlNotFoundException.php +++ b/module/Core/src/Exception/ShortUrlNotFoundException.php @@ -26,6 +26,11 @@ class ShortUrlNotFoundException extends DomainException implements ProblemDetail $e->title = self::TITLE; $e->type = self::TYPE; $e->status = StatusCodeInterface::STATUS_NOT_FOUND; + $e->additional = ['shortCode' => $shortCode]; + + if ($domain !== null) { + $e->additional['domain'] = $domain; + } return $e; } diff --git a/module/Core/src/Exception/TagNotFoundException.php b/module/Core/src/Exception/TagNotFoundException.php index 33cf1345..1924e89a 100644 --- a/module/Core/src/Exception/TagNotFoundException.php +++ b/module/Core/src/Exception/TagNotFoundException.php @@ -25,6 +25,7 @@ class TagNotFoundException extends DomainException implements ProblemDetailsExce $e->title = self::TITLE; $e->type = self::TYPE; $e->status = StatusCodeInterface::STATUS_NOT_FOUND; + $e->additional = ['tag' => $tag]; return $e; } diff --git a/module/Core/src/Exception/ValidationException.php b/module/Core/src/Exception/ValidationException.php index c4ff2354..f3a5c515 100644 --- a/module/Core/src/Exception/ValidationException.php +++ b/module/Core/src/Exception/ValidationException.php @@ -24,19 +24,6 @@ class ValidationException extends InvalidArgumentException implements ProblemDet private const TITLE = 'Invalid data'; private const TYPE = 'INVALID_ARGUMENT'; - /** @var array */ - private $invalidElements; - - public function __construct( - string $message = '', - array $invalidElements = [], - int $code = 0, - ?Throwable $previous = null - ) { - $this->invalidElements = $invalidElements; - parent::__construct($message, $code, $previous); - } - public static function fromInputFilter(InputFilterInterface $inputFilter, ?Throwable $prev = null): self { return static::fromArray($inputFilter->getMessages(), $prev); @@ -45,22 +32,20 @@ class ValidationException extends InvalidArgumentException implements ProblemDet public static function fromArray(array $invalidData, ?Throwable $prev = null): self { $status = StatusCodeInterface::STATUS_BAD_REQUEST; - $e = new self('Provided data is not valid', $invalidData, $status, $prev); + $e = new self('Provided data is not valid', $status, $prev); $e->detail = $e->getMessage(); $e->title = self::TITLE; $e->type = self::TYPE; $e->status = StatusCodeInterface::STATUS_BAD_REQUEST; - $e->additional = [ - 'invalidElements' => $invalidData, - ]; + $e->additional = ['invalidElements' => $invalidData]; return $e; } public function getInvalidElements(): array { - return $this->invalidElements; + return $this->additional['invalidElements']; } public function __toString(): string @@ -80,7 +65,7 @@ class ValidationException extends InvalidArgumentException implements ProblemDet private function invalidElementsToString(): string { - return reduce_left($this->invalidElements, function ($messageSet, string $name, $_, string $acc) { + return reduce_left($this->getInvalidElements(), function ($messageSet, string $name, $_, string $acc) { return $acc . sprintf( "\n '%s' => %s", $name, diff --git a/module/Core/test/Exception/DeleteShortUrlExceptionTest.php b/module/Core/test/Exception/DeleteShortUrlExceptionTest.php index 4b132eea..0e2012a2 100644 --- a/module/Core/test/Exception/DeleteShortUrlExceptionTest.php +++ b/module/Core/test/Exception/DeleteShortUrlExceptionTest.php @@ -5,17 +5,15 @@ declare(strict_types=1); namespace ShlinkioTest\Shlink\Core\Exception; use PHPUnit\Framework\TestCase; -use Shlinkio\Shlink\Common\Util\StringUtilsTrait; use Shlinkio\Shlink\Core\Exception\DeleteShortUrlException; use function Functional\map; use function range; +use function Shlinkio\Shlink\Core\generateRandomShortCode; use function sprintf; class DeleteShortUrlExceptionTest extends TestCase { - use StringUtilsTrait; - /** * @test * @dataProvider provideThresholds @@ -29,29 +27,12 @@ class DeleteShortUrlExceptionTest extends TestCase $this->assertEquals($threshold, $e->getVisitsThreshold()); $this->assertEquals($expectedMessage, $e->getMessage()); - $this->assertEquals(0, $e->getCode()); - $this->assertNull($e->getPrevious()); - } - - /** - * @test - * @dataProvider provideThresholds - */ - public function visitsThresholdIsProperlyReturned(int $threshold): void - { - $e = new DeleteShortUrlException($threshold); - - $this->assertEquals($threshold, $e->getVisitsThreshold()); - $this->assertEquals('', $e->getMessage()); - $this->assertEquals(0, $e->getCode()); - $this->assertNull($e->getPrevious()); } public function provideThresholds(): array { return map(range(5, 50, 5), function (int $number) { - $shortCode = $this->generateRandomString(6); - return [$number, $shortCode, sprintf( + return [$number, $shortCode = generateRandomShortCode(6), sprintf( 'Impossible to delete short URL with short code "%s" since it has more than "%s" visits.', $shortCode, $number diff --git a/module/Core/test/Exception/ValidationExceptionTest.php b/module/Core/test/Exception/ValidationExceptionTest.php index eb47caed..069506b5 100644 --- a/module/Core/test/Exception/ValidationExceptionTest.php +++ b/module/Core/test/Exception/ValidationExceptionTest.php @@ -13,38 +13,9 @@ use Throwable; use Zend\InputFilter\InputFilterInterface; use function print_r; -use function random_int; class ValidationExceptionTest extends TestCase { - /** - * @test - * @dataProvider provideExceptionData - */ - public function createsExceptionWrappingExpectedData( - array $args, - string $expectedMessage, - array $expectedInvalidElements, - int $expectedCode, - ?Throwable $expectedPrev - ): void { - $e = new ValidationException(...$args); - - $this->assertEquals($expectedMessage, $e->getMessage()); - $this->assertEquals($expectedInvalidElements, $e->getInvalidElements()); - $this->assertEquals($expectedCode, $e->getCode()); - $this->assertEquals($expectedPrev, $e->getPrevious()); - } - - public function provideExceptionData(): iterable - { - yield 'empty args' => [[], '', [], 0, null]; - yield 'with message' => [['something'], 'something', [], 0, null]; - yield 'with elements' => [['something_else', [1, 2, 3]], 'something_else', [1, 2, 3], 0, null]; - yield 'with code' => [['foo', [], $foo = random_int(-100, 100)], 'foo', [], $foo, null]; - yield 'with prev' => [['bar', [], 8, $e = new RuntimeException()], 'bar', [], 8, $e]; - } - /** * @test * @dataProvider provideExceptions From d83d2f82bdf27b512e416bce05677073087af593 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 27 Nov 2019 20:48:35 +0100 Subject: [PATCH 35/40] Added more strict checks on API errors tests --- .../Action/CreateShortUrlActionTest.php | 31 +++++++++-- .../Action/DeleteShortUrlActionTest.php | 22 ++++++-- .../Action/EditShortUrlActionTest.php | 23 +++++++-- .../Action/EditShortUrlTagsActionTest.php | 23 +++++++-- .../test-api/Action/GetVisitsActionTest.php | 12 ++++- .../Action/ResolveShortUrlActionTest.php | 12 ++++- .../test-api/Action/UpdateTagActionTest.php | 22 ++++++-- .../Middleware/AuthenticationTest.php | 51 ++++++++++++------- 8 files changed, 156 insertions(+), 40 deletions(-) diff --git a/module/Rest/test-api/Action/CreateShortUrlActionTest.php b/module/Rest/test-api/Action/CreateShortUrlActionTest.php index 2098ea0c..37a2630c 100644 --- a/module/Rest/test-api/Action/CreateShortUrlActionTest.php +++ b/module/Rest/test-api/Action/CreateShortUrlActionTest.php @@ -10,6 +10,7 @@ use Shlinkio\Shlink\TestUtils\ApiTest\ApiTestCase; use function Functional\map; use function range; +use function sprintf; class CreateShortUrlActionTest extends ApiTestCase { @@ -40,10 +41,25 @@ class CreateShortUrlActionTest extends ApiTestCase */ public function failsToCreateShortUrlWithDuplicatedSlug(string $slug, ?string $domain): void { + $suffix = $domain === null ? '' : sprintf(' for domain "%s"', $domain); + $detail = sprintf('Provided slug "%s" is already in use%s.', $slug, $suffix); + [$statusCode, $payload] = $this->createShortUrl(['customSlug' => $slug, 'domain' => $domain]); $this->assertEquals(self::STATUS_BAD_REQUEST, $statusCode); - $this->assertEquals('INVALID_SLUG', $payload['error']); + $this->assertEquals(self::STATUS_BAD_REQUEST, $payload['status']); + $this->assertEquals($detail, $payload['detail']); + $this->assertEquals($detail, $payload['message']); // Deprecated + $this->assertEquals('INVALID_SLUG', $payload['type']); + $this->assertEquals('INVALID_SLUG', $payload['error']); // Deprecated + $this->assertEquals('Invalid custom slug', $payload['title']); + $this->assertEquals($slug, $payload['customSlug']); + + if ($domain !== null) { + $this->assertEquals($domain, $payload['domain']); + } else { + $this->assertArrayNotHasKey('domain', $payload); + } } /** @test */ @@ -203,10 +219,19 @@ class CreateShortUrlActionTest extends ApiTestCase /** @test */ public function failsToCreateShortUrlWithInvalidOriginalUrl(): void { - [$statusCode, $payload] = $this->createShortUrl(['longUrl' => 'https://this-has-to-be-invalid.com']); + $url = 'https://this-has-to-be-invalid.com'; + $expectedDetail = sprintf('Provided URL %s is invalid. Try with a different one.', $url); + + [$statusCode, $payload] = $this->createShortUrl(['longUrl' => $url]); $this->assertEquals(self::STATUS_BAD_REQUEST, $statusCode); - $this->assertEquals('INVALID_URL', $payload['error']); + $this->assertEquals(self::STATUS_BAD_REQUEST, $payload['status']); + $this->assertEquals('INVALID_URL', $payload['type']); + $this->assertEquals('INVALID_URL', $payload['error']); // Deprecated + $this->assertEquals($expectedDetail, $payload['detail']); + $this->assertEquals($expectedDetail, $payload['message']); // Deprecated + $this->assertEquals('Invalid URL', $payload['title']); + $this->assertEquals($url, $payload['url']); } /** diff --git a/module/Rest/test-api/Action/DeleteShortUrlActionTest.php b/module/Rest/test-api/Action/DeleteShortUrlActionTest.php index c6cf443e..4680a6f6 100644 --- a/module/Rest/test-api/Action/DeleteShortUrlActionTest.php +++ b/module/Rest/test-api/Action/DeleteShortUrlActionTest.php @@ -11,11 +11,19 @@ class DeleteShortUrlActionTest extends ApiTestCase /** @test */ public function notFoundErrorIsReturnWhenDeletingInvalidUrl(): void { + $expectedDetail = 'No URL found with short code "invalid"'; + $resp = $this->callApiWithKey(self::METHOD_DELETE, '/short-urls/invalid'); - ['error' => $error] = $this->getJsonResponsePayload($resp); + $payload = $this->getJsonResponsePayload($resp); $this->assertEquals(self::STATUS_NOT_FOUND, $resp->getStatusCode()); - $this->assertEquals('INVALID_SHORTCODE', $error); + $this->assertEquals(self::STATUS_NOT_FOUND, $payload['status']); + $this->assertEquals('INVALID_SHORTCODE', $payload['type']); + $this->assertEquals('INVALID_SHORTCODE', $payload['error']); // Deprecated + $this->assertEquals($expectedDetail, $payload['detail']); + $this->assertEquals($expectedDetail, $payload['message']); // Deprecated + $this->assertEquals('Short URL not found', $payload['title']); + $this->assertEquals('invalid', $payload['shortCode']); } /** @test */ @@ -25,11 +33,17 @@ class DeleteShortUrlActionTest extends ApiTestCase for ($i = 0; $i < 20; $i++) { $this->assertEquals(self::STATUS_FOUND, $this->callShortUrl('abc123')->getStatusCode()); } + $expectedDetail = 'Impossible to delete short URL with short code "abc123" since it has more than "15" visits.'; $resp = $this->callApiWithKey(self::METHOD_DELETE, '/short-urls/abc123'); - ['error' => $error] = $this->getJsonResponsePayload($resp); + $payload = $this->getJsonResponsePayload($resp); $this->assertEquals(self::STATUS_UNPROCESSABLE_ENTITY, $resp->getStatusCode()); - $this->assertEquals('INVALID_SHORTCODE_DELETION', $error); + $this->assertEquals(self::STATUS_UNPROCESSABLE_ENTITY, $payload['status']); + $this->assertEquals('INVALID_SHORTCODE_DELETION', $payload['type']); + $this->assertEquals('INVALID_SHORTCODE_DELETION', $payload['error']); // Deprecated + $this->assertEquals($expectedDetail, $payload['detail']); + $this->assertEquals($expectedDetail, $payload['message']); // Deprecated + $this->assertEquals('Cannot delete short URL', $payload['title']); } } diff --git a/module/Rest/test-api/Action/EditShortUrlActionTest.php b/module/Rest/test-api/Action/EditShortUrlActionTest.php index 739ec191..bbcb043a 100644 --- a/module/Rest/test-api/Action/EditShortUrlActionTest.php +++ b/module/Rest/test-api/Action/EditShortUrlActionTest.php @@ -12,22 +12,37 @@ class EditShortUrlActionTest extends ApiTestCase /** @test */ public function tryingToEditInvalidUrlReturnsNotFoundError(): void { + $expectedDetail = 'No URL found with short code "invalid"'; + $resp = $this->callApiWithKey(self::METHOD_PATCH, '/short-urls/invalid', [RequestOptions::JSON => []]); - ['error' => $error] = $this->getJsonResponsePayload($resp); + $payload = $this->getJsonResponsePayload($resp); $this->assertEquals(self::STATUS_NOT_FOUND, $resp->getStatusCode()); - $this->assertEquals('INVALID_SHORTCODE', $error); + $this->assertEquals(self::STATUS_NOT_FOUND, $payload['status']); + $this->assertEquals('INVALID_SHORTCODE', $payload['type']); + $this->assertEquals('INVALID_SHORTCODE', $payload['error']); // Deprecated + $this->assertEquals($expectedDetail, $payload['detail']); + $this->assertEquals($expectedDetail, $payload['message']); // Deprecated + $this->assertEquals('Short URL not found', $payload['title']); + $this->assertEquals('invalid', $payload['shortCode']); } /** @test */ public function providingInvalidDataReturnsBadRequest(): void { + $expectedDetail = 'Provided data is not valid'; + $resp = $this->callApiWithKey(self::METHOD_PATCH, '/short-urls/invalid', [RequestOptions::JSON => [ 'maxVisits' => 'not_a_number', ]]); - ['error' => $error] = $this->getJsonResponsePayload($resp); + $payload = $this->getJsonResponsePayload($resp); $this->assertEquals(self::STATUS_BAD_REQUEST, $resp->getStatusCode()); - $this->assertEquals('INVALID_ARGUMENT', $error); + $this->assertEquals(self::STATUS_BAD_REQUEST, $payload['status']); + $this->assertEquals('INVALID_ARGUMENT', $payload['type']); + $this->assertEquals('INVALID_ARGUMENT', $payload['error']); // Deprecated + $this->assertEquals($expectedDetail, $payload['detail']); + $this->assertEquals($expectedDetail, $payload['message']); // Deprecated + $this->assertEquals('Invalid data', $payload['title']); } } diff --git a/module/Rest/test-api/Action/EditShortUrlTagsActionTest.php b/module/Rest/test-api/Action/EditShortUrlTagsActionTest.php index 5116d1e6..e2922092 100644 --- a/module/Rest/test-api/Action/EditShortUrlTagsActionTest.php +++ b/module/Rest/test-api/Action/EditShortUrlTagsActionTest.php @@ -12,22 +12,37 @@ class EditShortUrlTagsActionTest extends ApiTestCase /** @test */ public function notProvidingTagsReturnsBadRequest(): void { + $expectedDetail = 'Provided data is not valid'; + $resp = $this->callApiWithKey(self::METHOD_PUT, '/short-urls/abc123/tags', [RequestOptions::JSON => []]); - ['error' => $error] = $this->getJsonResponsePayload($resp); + $payload = $this->getJsonResponsePayload($resp); $this->assertEquals(self::STATUS_BAD_REQUEST, $resp->getStatusCode()); - $this->assertEquals('INVALID_ARGUMENT', $error); + $this->assertEquals(self::STATUS_BAD_REQUEST, $payload['status']); + $this->assertEquals('INVALID_ARGUMENT', $payload['type']); + $this->assertEquals('INVALID_ARGUMENT', $payload['error']); // Deprecated + $this->assertEquals($expectedDetail, $payload['detail']); + $this->assertEquals($expectedDetail, $payload['message']); // Deprecated + $this->assertEquals('Invalid data', $payload['title']); } /** @test */ public function providingInvalidShortCodeReturnsBadRequest(): void { + $expectedDetail = 'No URL found with short code "invalid"'; + $resp = $this->callApiWithKey(self::METHOD_PUT, '/short-urls/invalid/tags', [RequestOptions::JSON => [ 'tags' => ['foo', 'bar'], ]]); - ['error' => $error] = $this->getJsonResponsePayload($resp); + $payload = $this->getJsonResponsePayload($resp); $this->assertEquals(self::STATUS_NOT_FOUND, $resp->getStatusCode()); - $this->assertEquals('INVALID_SHORTCODE', $error); + $this->assertEquals(self::STATUS_NOT_FOUND, $payload['status']); + $this->assertEquals('INVALID_SHORTCODE', $payload['type']); + $this->assertEquals('INVALID_SHORTCODE', $payload['error']); // Deprecated + $this->assertEquals($expectedDetail, $payload['detail']); + $this->assertEquals($expectedDetail, $payload['message']); // Deprecated + $this->assertEquals('Short URL not found', $payload['title']); + $this->assertEquals('invalid', $payload['shortCode']); } } diff --git a/module/Rest/test-api/Action/GetVisitsActionTest.php b/module/Rest/test-api/Action/GetVisitsActionTest.php index f9c6f404..0db06848 100644 --- a/module/Rest/test-api/Action/GetVisitsActionTest.php +++ b/module/Rest/test-api/Action/GetVisitsActionTest.php @@ -11,10 +11,18 @@ class GetVisitsActionTest extends ApiTestCase /** @test */ public function tryingToGetVisitsForInvalidUrlReturnsNotFoundError(): void { + $expectedDetail = 'No URL found with short code "invalid"'; + $resp = $this->callApiWithKey(self::METHOD_GET, '/short-urls/invalid/visits'); - ['error' => $error] = $this->getJsonResponsePayload($resp); + $payload = $this->getJsonResponsePayload($resp); $this->assertEquals(self::STATUS_NOT_FOUND, $resp->getStatusCode()); - $this->assertEquals('INVALID_SHORTCODE', $error); + $this->assertEquals(self::STATUS_NOT_FOUND, $payload['status']); + $this->assertEquals('INVALID_SHORTCODE', $payload['type']); + $this->assertEquals('INVALID_SHORTCODE', $payload['error']); // Deprecated + $this->assertEquals($expectedDetail, $payload['detail']); + $this->assertEquals($expectedDetail, $payload['message']); // Deprecated + $this->assertEquals('Short URL not found', $payload['title']); + $this->assertEquals('invalid', $payload['shortCode']); } } diff --git a/module/Rest/test-api/Action/ResolveShortUrlActionTest.php b/module/Rest/test-api/Action/ResolveShortUrlActionTest.php index 46f07d53..e4d45f4a 100644 --- a/module/Rest/test-api/Action/ResolveShortUrlActionTest.php +++ b/module/Rest/test-api/Action/ResolveShortUrlActionTest.php @@ -11,10 +11,18 @@ class ResolveShortUrlActionTest extends ApiTestCase /** @test */ public function tryingToResolveInvalidUrlReturnsNotFoundError(): void { + $expectedDetail = 'No URL found with short code "invalid"'; + $resp = $this->callApiWithKey(self::METHOD_GET, '/short-urls/invalid'); - ['error' => $error] = $this->getJsonResponsePayload($resp); + $payload = $this->getJsonResponsePayload($resp); $this->assertEquals(self::STATUS_NOT_FOUND, $resp->getStatusCode()); - $this->assertEquals('INVALID_SHORTCODE', $error); + $this->assertEquals(self::STATUS_NOT_FOUND, $payload['status']); + $this->assertEquals('INVALID_SHORTCODE', $payload['type']); + $this->assertEquals('INVALID_SHORTCODE', $payload['error']); // Deprecated + $this->assertEquals($expectedDetail, $payload['detail']); + $this->assertEquals($expectedDetail, $payload['message']); // Deprecated + $this->assertEquals('Short URL not found', $payload['title']); + $this->assertEquals('invalid', $payload['shortCode']); } } diff --git a/module/Rest/test-api/Action/UpdateTagActionTest.php b/module/Rest/test-api/Action/UpdateTagActionTest.php index 12f37296..8c14774c 100644 --- a/module/Rest/test-api/Action/UpdateTagActionTest.php +++ b/module/Rest/test-api/Action/UpdateTagActionTest.php @@ -15,11 +15,18 @@ class UpdateTagActionTest extends ApiTestCase */ public function notProvidingTagsReturnsBadRequest(array $body): void { + $expectedDetail = 'Provided data is not valid'; + $resp = $this->callApiWithKey(self::METHOD_PUT, '/tags', [RequestOptions::JSON => $body]); - ['error' => $error] = $this->getJsonResponsePayload($resp); + $payload = $this->getJsonResponsePayload($resp); $this->assertEquals(self::STATUS_BAD_REQUEST, $resp->getStatusCode()); - $this->assertEquals('INVALID_ARGUMENT', $error); + $this->assertEquals(self::STATUS_BAD_REQUEST, $payload['status']); + $this->assertEquals('INVALID_ARGUMENT', $payload['type']); + $this->assertEquals('INVALID_ARGUMENT', $payload['error']); // Deprecated + $this->assertEquals($expectedDetail, $payload['detail']); + $this->assertEquals($expectedDetail, $payload['message']); // Deprecated + $this->assertEquals('Invalid data', $payload['title']); } public function provideInvalidBody(): iterable @@ -32,13 +39,20 @@ class UpdateTagActionTest extends ApiTestCase /** @test */ public function tryingToRenameInvalidTagReturnsNotFound(): void { + $expectedDetail = 'Tag with name "invalid_tag" could not be found'; + $resp = $this->callApiWithKey(self::METHOD_PUT, '/tags', [RequestOptions::JSON => [ 'oldName' => 'invalid_tag', 'newName' => 'foo', ]]); - ['error' => $error] = $this->getJsonResponsePayload($resp); + $payload = $this->getJsonResponsePayload($resp); $this->assertEquals(self::STATUS_NOT_FOUND, $resp->getStatusCode()); - $this->assertEquals('TAG_NOT_FOUND', $error); + $this->assertEquals(self::STATUS_NOT_FOUND, $payload['status']); + $this->assertEquals('TAG_NOT_FOUND', $payload['type']); + $this->assertEquals('TAG_NOT_FOUND', $payload['error']); // Deprecated + $this->assertEquals($expectedDetail, $payload['detail']); + $this->assertEquals($expectedDetail, $payload['message']); // Deprecated + $this->assertEquals('Tag not found', $payload['title']); } } diff --git a/module/Rest/test-api/Middleware/AuthenticationTest.php b/module/Rest/test-api/Middleware/AuthenticationTest.php index b918a369..3526c5f5 100644 --- a/module/Rest/test-api/Middleware/AuthenticationTest.php +++ b/module/Rest/test-api/Middleware/AuthenticationTest.php @@ -16,18 +16,21 @@ class AuthenticationTest extends ApiTestCase /** @test */ public function authorizationErrorIsReturnedIfNoApiKeyIsSent(): void { + $expectedDetail = sprintf( + 'Expected one of the following authentication headers, but none were provided, ["%s"]', + implode('", "', RequestToHttpAuthPlugin::SUPPORTED_AUTH_HEADERS) + ); + $resp = $this->callApi(self::METHOD_GET, '/short-codes'); - ['error' => $error, 'message' => $message] = $this->getJsonResponsePayload($resp); + $payload = $this->getJsonResponsePayload($resp); $this->assertEquals(self::STATUS_UNAUTHORIZED, $resp->getStatusCode()); - $this->assertEquals('INVALID_AUTHORIZATION', $error); - $this->assertEquals( - sprintf( - 'Expected one of the following authentication headers, but none were provided, ["%s"]', - implode('", "', RequestToHttpAuthPlugin::SUPPORTED_AUTH_HEADERS) - ), - $message - ); + $this->assertEquals(self::STATUS_UNAUTHORIZED, $payload['status']); + $this->assertEquals('INVALID_AUTHORIZATION', $payload['type']); + $this->assertEquals('INVALID_AUTHORIZATION', $payload['error']); // Deprecated + $this->assertEquals($expectedDetail, $payload['detail']); + $this->assertEquals($expectedDetail, $payload['message']); // Deprecated + $this->assertEquals('Invalid authorization', $payload['title']); } /** @@ -36,16 +39,22 @@ class AuthenticationTest extends ApiTestCase */ public function apiKeyErrorIsReturnedWhenProvidedApiKeyIsInvalid(string $apiKey): void { + $expectedDetail = 'Provided API key does not exist or is invalid.'; + $resp = $this->callApi(self::METHOD_GET, '/short-codes', [ 'headers' => [ Plugin\ApiKeyHeaderPlugin::HEADER_NAME => $apiKey, ], ]); - ['error' => $error, 'message' => $message] = $this->getJsonResponsePayload($resp); + $payload = $this->getJsonResponsePayload($resp); $this->assertEquals(self::STATUS_UNAUTHORIZED, $resp->getStatusCode()); - $this->assertEquals('INVALID_API_KEY', $error); - $this->assertEquals('Provided API key does not exist or is invalid.', $message); + $this->assertEquals(self::STATUS_UNAUTHORIZED, $payload['status']); + $this->assertEquals('INVALID_API_KEY', $payload['type']); + $this->assertEquals('INVALID_API_KEY', $payload['error']); // Deprecated + $this->assertEquals($expectedDetail, $payload['detail']); + $this->assertEquals($expectedDetail, $payload['message']); // Deprecated + $this->assertEquals('Invalid API key', $payload['title']); } public function provideInvalidApiKeys(): iterable @@ -61,19 +70,24 @@ class AuthenticationTest extends ApiTestCase */ public function authorizationErrorIsReturnedIfInvalidDataIsProvided( string $authValue, - string $expectedMessage, - string $expectedError + string $expectedDetail, + string $expectedType, + string $expectedTitle ): void { $resp = $this->callApi(self::METHOD_GET, '/short-codes', [ 'headers' => [ Plugin\AuthorizationHeaderPlugin::HEADER_NAME => $authValue, ], ]); - ['error' => $error, 'message' => $message] = $this->getJsonResponsePayload($resp); + $payload = $this->getJsonResponsePayload($resp); $this->assertEquals(self::STATUS_UNAUTHORIZED, $resp->getStatusCode()); - $this->assertEquals($expectedError, $error); - $this->assertEquals($expectedMessage, $message); + $this->assertEquals(self::STATUS_UNAUTHORIZED, $payload['status']); + $this->assertEquals($expectedType, $payload['type']); + $this->assertEquals($expectedType, $payload['error']); // Deprecated + $this->assertEquals($expectedDetail, $payload['detail']); + $this->assertEquals($expectedDetail, $payload['message']); // Deprecated + $this->assertEquals($expectedTitle, $payload['title']); } public function provideInvalidAuthorizations(): iterable @@ -82,17 +96,20 @@ class AuthenticationTest extends ApiTestCase 'invalid', 'You need to provide the Bearer type in the Authorization header.', 'INVALID_AUTHORIZATION', + 'Invalid authorization', ]; yield 'invalid type' => [ 'Basic invalid', 'Provided authorization type Basic is not supported. Use Bearer instead.', 'INVALID_AUTHORIZATION', + 'Invalid authorization', ]; yield 'invalid JWT' => [ 'Bearer invalid', 'Missing or invalid auth token provided. Perform a new authentication request and send provided ' . 'token on every new request on the Authorization header', 'INVALID_AUTH_TOKEN', + 'Invalid auth token', ]; } } From 5055ddf995717f88eb9b201ad857b5e0f09cfb7d Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 28 Nov 2019 18:42:27 +0100 Subject: [PATCH 36/40] Updated CLI commands to just print exception messages when possible --- .../CLI/src/Command/Api/DisableKeyCommand.php | 4 +- .../ShortUrl/DeleteShortUrlCommand.php | 13 ++---- .../ShortUrl/GenerateShortUrlCommand.php | 9 +--- .../Command/ShortUrl/ResolveUrlCommand.php | 2 +- .../CLI/src/Command/Tag/RenameTagCommand.php | 4 +- .../GeolocationDbUpdateFailedException.php | 12 ++---- module/CLI/src/Util/GeolocationDbUpdater.php | 3 -- .../Command/Api/DisableKeyCommandTest.php | 9 ++-- .../ShortUrl/DeleteShortUrlCommandTest.php | 14 +++---- .../ShortUrl/GenerateShortUrlCommandTest.php | 9 ++-- .../ShortUrl/ResolveUrlCommandTest.php | 7 ++-- .../test/Command/Tag/RenameTagCommandTest.php | 8 ++-- ...GeolocationDbUpdateFailedExceptionTest.php | 41 ------------------- .../src/Exception/NonUniqueSlugException.php | 2 +- 14 files changed, 40 insertions(+), 97 deletions(-) diff --git a/module/CLI/src/Command/Api/DisableKeyCommand.php b/module/CLI/src/Command/Api/DisableKeyCommand.php index e0f2b79c..f3eb7f6b 100644 --- a/module/CLI/src/Command/Api/DisableKeyCommand.php +++ b/module/CLI/src/Command/Api/DisableKeyCommand.php @@ -4,8 +4,8 @@ declare(strict_types=1); namespace Shlinkio\Shlink\CLI\Command\Api; -use InvalidArgumentException; use Shlinkio\Shlink\CLI\Util\ExitCodes; +use Shlinkio\Shlink\Common\Exception\InvalidArgumentException; use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -45,7 +45,7 @@ class DisableKeyCommand extends Command $io->success(sprintf('API key "%s" properly disabled', $apiKey)); return ExitCodes::EXIT_SUCCESS; } catch (InvalidArgumentException $e) { - $io->error(sprintf('API key "%s" does not exist.', $apiKey)); + $io->error($e->getMessage()); return ExitCodes::EXIT_FAILURE; } } diff --git a/module/CLI/src/Command/ShortUrl/DeleteShortUrlCommand.php b/module/CLI/src/Command/ShortUrl/DeleteShortUrlCommand.php index f44e3e18..c2e81d0d 100644 --- a/module/CLI/src/Command/ShortUrl/DeleteShortUrlCommand.php +++ b/module/CLI/src/Command/ShortUrl/DeleteShortUrlCommand.php @@ -56,21 +56,16 @@ class DeleteShortUrlCommand extends Command $this->runDelete($io, $shortCode, $ignoreThreshold); return ExitCodes::EXIT_SUCCESS; } catch (Exception\ShortUrlNotFoundException $e) { - $io->error(sprintf('Provided short code "%s" could not be found.', $shortCode)); + $io->error($e->getMessage()); return ExitCodes::EXIT_FAILURE; } catch (Exception\DeleteShortUrlException $e) { - return $this->retry($io, $shortCode, $e); + return $this->retry($io, $shortCode, $e->getMessage()); } } - private function retry(SymfonyStyle $io, string $shortCode, Exception\DeleteShortUrlException $e): int + private function retry(SymfonyStyle $io, string $shortCode, string $warningMsg): int { - $warningMsg = sprintf( - 'It was not possible to delete the short URL with short code "%s" because it has more than %s visits.', - $shortCode, - $e->getVisitsThreshold() - ); - $io->writeln('' . $warningMsg . ''); + $io->writeln(sprintf('%s', $warningMsg)); $forceDelete = $io->confirm('Do you want to delete it anyway?', false); if ($forceDelete) { diff --git a/module/CLI/src/Command/ShortUrl/GenerateShortUrlCommand.php b/module/CLI/src/Command/ShortUrl/GenerateShortUrlCommand.php index 9d9c464b..5cce7030 100644 --- a/module/CLI/src/Command/ShortUrl/GenerateShortUrlCommand.php +++ b/module/CLI/src/Command/ShortUrl/GenerateShortUrlCommand.php @@ -141,13 +141,8 @@ class GenerateShortUrlCommand extends Command sprintf('Generated short URL: %s', $shortUrl->toString($this->domainConfig)), ]); return ExitCodes::EXIT_SUCCESS; - } catch (InvalidUrlException $e) { - $io->error(sprintf('Provided URL "%s" is invalid. Try with a different one.', $longUrl)); - return ExitCodes::EXIT_FAILURE; - } catch (NonUniqueSlugException $e) { - $io->error( - sprintf('Provided slug "%s" is already in use by another URL. Try with a different one.', $customSlug) - ); + } catch (InvalidUrlException | NonUniqueSlugException $e) { + $io->error($e->getMessage()); return ExitCodes::EXIT_FAILURE; } } diff --git a/module/CLI/src/Command/ShortUrl/ResolveUrlCommand.php b/module/CLI/src/Command/ShortUrl/ResolveUrlCommand.php index 28564369..e8db28e2 100644 --- a/module/CLI/src/Command/ShortUrl/ResolveUrlCommand.php +++ b/module/CLI/src/Command/ShortUrl/ResolveUrlCommand.php @@ -65,7 +65,7 @@ class ResolveUrlCommand extends Command $output->writeln(sprintf('Long URL: %s', $url->getLongUrl())); return ExitCodes::EXIT_SUCCESS; } catch (ShortUrlNotFoundException $e) { - $io->error(sprintf('Provided short code "%s" could not be found.', $shortCode)); + $io->error($e->getMessage()); return ExitCodes::EXIT_FAILURE; } } diff --git a/module/CLI/src/Command/Tag/RenameTagCommand.php b/module/CLI/src/Command/Tag/RenameTagCommand.php index a7002f60..b3a21a2e 100644 --- a/module/CLI/src/Command/Tag/RenameTagCommand.php +++ b/module/CLI/src/Command/Tag/RenameTagCommand.php @@ -13,8 +13,6 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -use function sprintf; - class RenameTagCommand extends Command { public const NAME = 'tag:rename'; @@ -48,7 +46,7 @@ class RenameTagCommand extends Command $io->success('Tag properly renamed.'); return ExitCodes::EXIT_SUCCESS; } catch (TagNotFoundException $e) { - $io->error(sprintf('A tag with name "%s" was not found', $oldName)); + $io->error($e->getMessage()); return ExitCodes::EXIT_FAILURE; } } diff --git a/module/CLI/src/Exception/GeolocationDbUpdateFailedException.php b/module/CLI/src/Exception/GeolocationDbUpdateFailedException.php index fcb680d5..38bb4c5f 100644 --- a/module/CLI/src/Exception/GeolocationDbUpdateFailedException.php +++ b/module/CLI/src/Exception/GeolocationDbUpdateFailedException.php @@ -12,20 +12,16 @@ class GeolocationDbUpdateFailedException extends RuntimeException implements Exc /** @var bool */ private $olderDbExists; - public function __construct(bool $olderDbExists, string $message = '', int $code = 0, ?Throwable $previous = null) - { - $this->olderDbExists = $olderDbExists; - parent::__construct($message, $code, $previous); - } - public static function create(bool $olderDbExists, ?Throwable $prev = null): self { - return new self( - $olderDbExists, + $e = new self( 'An error occurred while updating geolocation database, and an older version could not be found', 0, $prev ); + $e->olderDbExists = $olderDbExists; + + return $e; } public function olderDbExists(): bool diff --git a/module/CLI/src/Util/GeolocationDbUpdater.php b/module/CLI/src/Util/GeolocationDbUpdater.php index ebff82aa..2e530a34 100644 --- a/module/CLI/src/Util/GeolocationDbUpdater.php +++ b/module/CLI/src/Util/GeolocationDbUpdater.php @@ -10,7 +10,6 @@ use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException; use Shlinkio\Shlink\IpGeolocation\Exception\RuntimeException; use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdaterInterface; use Symfony\Component\Lock\Factory as Locker; -use Throwable; class GeolocationDbUpdater implements GeolocationDbUpdaterInterface { @@ -40,8 +39,6 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface try { $this->downloadIfNeeded($mustBeUpdated, $handleProgress); - } catch (Throwable $e) { - throw $e; } finally { $lock->release(); } diff --git a/module/CLI/test/Command/Api/DisableKeyCommandTest.php b/module/CLI/test/Command/Api/DisableKeyCommandTest.php index ca17aa7a..37629091 100644 --- a/module/CLI/test/Command/Api/DisableKeyCommandTest.php +++ b/module/CLI/test/Command/Api/DisableKeyCommandTest.php @@ -29,7 +29,7 @@ class DisableKeyCommandTest extends TestCase } /** @test */ - public function providedApiKeyIsDisabled() + public function providedApiKeyIsDisabled(): void { $apiKey = 'abcd1234'; $this->apiKeyService->disable($apiKey)->shouldBeCalledOnce(); @@ -43,17 +43,18 @@ class DisableKeyCommandTest extends TestCase } /** @test */ - public function errorIsReturnedIfServiceThrowsException() + public function errorIsReturnedIfServiceThrowsException(): void { $apiKey = 'abcd1234'; - $disable = $this->apiKeyService->disable($apiKey)->willThrow(InvalidArgumentException::class); + $expectedMessage = 'API key "abcd1234" does not exist.'; + $disable = $this->apiKeyService->disable($apiKey)->willThrow(new InvalidArgumentException($expectedMessage)); $this->commandTester->execute([ 'apiKey' => $apiKey, ]); $output = $this->commandTester->getDisplay(); - $this->assertStringContainsString('API key "abcd1234" does not exist.', $output); + $this->assertStringContainsString($expectedMessage, $output); $disable->shouldHaveBeenCalledOnce(); } } diff --git a/module/CLI/test/Command/ShortUrl/DeleteShortUrlCommandTest.php b/module/CLI/test/Command/ShortUrl/DeleteShortUrlCommandTest.php index 6a036c62..85521835 100644 --- a/module/CLI/test/Command/ShortUrl/DeleteShortUrlCommandTest.php +++ b/module/CLI/test/Command/ShortUrl/DeleteShortUrlCommandTest.php @@ -58,13 +58,13 @@ class DeleteShortUrlCommandTest extends TestCase { $shortCode = 'abc123'; $deleteByShortCode = $this->service->deleteByShortCode($shortCode, false)->willThrow( - Exception\ShortUrlNotFoundException::class + Exception\ShortUrlNotFoundException::fromNotFoundShortCode($shortCode) ); $this->commandTester->execute(['shortCode' => $shortCode]); $output = $this->commandTester->getDisplay(); - $this->assertStringContainsString(sprintf('Provided short code "%s" could not be found.', $shortCode), $output); + $this->assertStringContainsString(sprintf('No URL found with short code "%s"', $shortCode), $output); $deleteByShortCode->shouldHaveBeenCalledOnce(); } @@ -79,11 +79,11 @@ class DeleteShortUrlCommandTest extends TestCase ): void { $shortCode = 'abc123'; $deleteByShortCode = $this->service->deleteByShortCode($shortCode, Argument::type('bool'))->will( - function (array $args) { + function (array $args) use ($shortCode) { $ignoreThreshold = array_pop($args); if (!$ignoreThreshold) { - throw Exception\DeleteShortUrlException::fromVisitsThreshold(10, ''); + throw Exception\DeleteShortUrlException::fromVisitsThreshold(10, $shortCode); } } ); @@ -93,7 +93,7 @@ class DeleteShortUrlCommandTest extends TestCase $output = $this->commandTester->getDisplay(); $this->assertStringContainsString(sprintf( - 'It was not possible to delete the short URL with short code "%s" because it has more than 10 visits.', + 'Impossible to delete short URL with short code "%s" since it has more than "10" visits.', $shortCode ), $output); $this->assertStringContainsString($expectedMessage, $output); @@ -112,7 +112,7 @@ class DeleteShortUrlCommandTest extends TestCase { $shortCode = 'abc123'; $deleteByShortCode = $this->service->deleteByShortCode($shortCode, false)->willThrow( - Exception\DeleteShortUrlException::fromVisitsThreshold(10, '') + Exception\DeleteShortUrlException::fromVisitsThreshold(10, $shortCode) ); $this->commandTester->setInputs(['no']); @@ -120,7 +120,7 @@ class DeleteShortUrlCommandTest extends TestCase $output = $this->commandTester->getDisplay(); $this->assertStringContainsString(sprintf( - 'It was not possible to delete the short URL with short code "%s" because it has more than 10 visits.', + 'Impossible to delete short URL with short code "%s" since it has more than "10" visits.', $shortCode ), $output); $this->assertStringContainsString('Short URL was not deleted.', $output); diff --git a/module/CLI/test/Command/ShortUrl/GenerateShortUrlCommandTest.php b/module/CLI/test/Command/ShortUrl/GenerateShortUrlCommandTest.php index d83bd042..abae0fe6 100644 --- a/module/CLI/test/Command/ShortUrl/GenerateShortUrlCommandTest.php +++ b/module/CLI/test/Command/ShortUrl/GenerateShortUrlCommandTest.php @@ -59,21 +59,22 @@ class GenerateShortUrlCommandTest extends TestCase /** @test */ public function exceptionWhileParsingLongUrlOutputsError(): void { - $this->urlShortener->urlToShortCode(Argument::cetera())->willThrow(new InvalidUrlException()) + $url = 'http://domain.com/invalid'; + $this->urlShortener->urlToShortCode(Argument::cetera())->willThrow(InvalidUrlException::fromUrl($url)) ->shouldBeCalledOnce(); - $this->commandTester->execute(['longUrl' => 'http://domain.com/invalid']); + $this->commandTester->execute(['longUrl' => $url]); $output = $this->commandTester->getDisplay(); $this->assertEquals(ExitCodes::EXIT_FAILURE, $this->commandTester->getStatusCode()); - $this->assertStringContainsString('Provided URL "http://domain.com/invalid" is invalid.', $output); + $this->assertStringContainsString('Provided URL http://domain.com/invalid is invalid.', $output); } /** @test */ public function providingNonUniqueSlugOutputsError(): void { $urlToShortCode = $this->urlShortener->urlToShortCode(Argument::cetera())->willThrow( - NonUniqueSlugException::class + NonUniqueSlugException::fromSlug('my-slug') ); $this->commandTester->execute(['longUrl' => 'http://domain.com/invalid', '--customSlug' => 'my-slug']); diff --git a/module/CLI/test/Command/ShortUrl/ResolveUrlCommandTest.php b/module/CLI/test/Command/ShortUrl/ResolveUrlCommandTest.php index 23b4ec28..11b549e5 100644 --- a/module/CLI/test/Command/ShortUrl/ResolveUrlCommandTest.php +++ b/module/CLI/test/Command/ShortUrl/ResolveUrlCommandTest.php @@ -52,11 +52,12 @@ class ResolveUrlCommandTest extends TestCase public function incorrectShortCodeOutputsErrorMessage(): void { $shortCode = 'abc123'; - $this->urlShortener->shortCodeToUrl($shortCode, null)->willThrow(ShortUrlNotFoundException::class) - ->shouldBeCalledOnce(); + $this->urlShortener->shortCodeToUrl($shortCode, null) + ->willThrow(ShortUrlNotFoundException::fromNotFoundShortCode($shortCode)) + ->shouldBeCalledOnce(); $this->commandTester->execute(['shortCode' => $shortCode]); $output = $this->commandTester->getDisplay(); - $this->assertStringContainsString(sprintf('Provided short code "%s" could not be found', $shortCode), $output); + $this->assertStringContainsString(sprintf('No URL found with short code "%s"', $shortCode), $output); } } diff --git a/module/CLI/test/Command/Tag/RenameTagCommandTest.php b/module/CLI/test/Command/Tag/RenameTagCommandTest.php index 4fc2aaad..c626e0c0 100644 --- a/module/CLI/test/Command/Tag/RenameTagCommandTest.php +++ b/module/CLI/test/Command/Tag/RenameTagCommandTest.php @@ -34,11 +34,11 @@ class RenameTagCommandTest extends TestCase } /** @test */ - public function errorIsPrintedIfExceptionIsThrown() + public function errorIsPrintedIfExceptionIsThrown(): void { $oldName = 'foo'; $newName = 'bar'; - $renameTag = $this->tagService->renameTag($oldName, $newName)->willThrow(TagNotFoundException::class); + $renameTag = $this->tagService->renameTag($oldName, $newName)->willThrow(TagNotFoundException::fromTag('foo')); $this->commandTester->execute([ 'oldName' => $oldName, @@ -46,12 +46,12 @@ class RenameTagCommandTest extends TestCase ]); $output = $this->commandTester->getDisplay(); - $this->assertStringContainsString('A tag with name "foo" was not found', $output); + $this->assertStringContainsString('Tag with name "foo" could not be found', $output); $renameTag->shouldHaveBeenCalled(); } /** @test */ - public function successIsPrintedIfNoErrorOccurs() + public function successIsPrintedIfNoErrorOccurs(): void { $oldName = 'foo'; $newName = 'bar'; diff --git a/module/CLI/test/Exception/GeolocationDbUpdateFailedExceptionTest.php b/module/CLI/test/Exception/GeolocationDbUpdateFailedExceptionTest.php index 51c87cb3..70a8cc6f 100644 --- a/module/CLI/test/Exception/GeolocationDbUpdateFailedExceptionTest.php +++ b/module/CLI/test/Exception/GeolocationDbUpdateFailedExceptionTest.php @@ -12,47 +12,6 @@ use Throwable; class GeolocationDbUpdateFailedExceptionTest extends TestCase { - /** - * @test - * @dataProvider provideOlderDbExists - */ - public function constructCreatesExceptionWithDefaultArgs(bool $olderDbExists): void - { - $e = new GeolocationDbUpdateFailedException($olderDbExists); - - $this->assertEquals($olderDbExists, $e->olderDbExists()); - $this->assertEquals('', $e->getMessage()); - $this->assertEquals(0, $e->getCode()); - $this->assertNull($e->getPrevious()); - } - - public function provideOlderDbExists(): iterable - { - yield 'with older DB' => [true]; - yield 'without older DB' => [false]; - } - - /** - * @test - * @dataProvider provideConstructorArgs - */ - public function constructCreatesException(bool $olderDbExists, string $message, int $code, ?Throwable $prev): void - { - $e = new GeolocationDbUpdateFailedException($olderDbExists, $message, $code, $prev); - - $this->assertEquals($olderDbExists, $e->olderDbExists()); - $this->assertEquals($message, $e->getMessage()); - $this->assertEquals($code, $e->getCode()); - $this->assertEquals($prev, $e->getPrevious()); - } - - public function provideConstructorArgs(): iterable - { - yield [true, 'This is a nice error message', 99, new Exception('prev')]; - yield [false, 'Another message', 0, new RuntimeException('prev')]; - yield [true, 'An yet another message', -50, null]; - } - /** * @test * @dataProvider provideCreateArgs diff --git a/module/Core/src/Exception/NonUniqueSlugException.php b/module/Core/src/Exception/NonUniqueSlugException.php index bcd40268..51beff82 100644 --- a/module/Core/src/Exception/NonUniqueSlugException.php +++ b/module/Core/src/Exception/NonUniqueSlugException.php @@ -17,7 +17,7 @@ class NonUniqueSlugException extends InvalidArgumentException implements Problem private const TITLE = 'Invalid custom slug'; private const TYPE = 'INVALID_SLUG'; - public static function fromSlug(string $slug, ?string $domain): self + public static function fromSlug(string $slug, ?string $domain = null): self { $suffix = $domain === null ? '' : sprintf(' for domain "%s"', $domain); $e = new self(sprintf('Provided slug "%s" is already in use%s.', $slug, $suffix)); From 60d3c09da53d409a800e5cabdced80ab83c43876 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 28 Nov 2019 19:37:22 +0100 Subject: [PATCH 37/40] Updated API docs to reference the use of application/problem+json --- docs/swagger/definitions/Error.json | 23 +++++++++- .../definitions/InvalidShortUrlMeta.json | 6 +++ docs/swagger/paths/v1_short-urls.json | 31 +++++++++++--- docs/swagger/paths/v1_short-urls_shorten.json | 15 ++++--- .../paths/v1_short-urls_{shortCode}.json | 42 ++++++++----------- .../paths/v1_short-urls_{shortCode}_tags.json | 2 +- .../v1_short-urls_{shortCode}_visits.json | 4 +- docs/swagger/paths/v1_tags.json | 12 +++--- docs/swagger/paths/{shortCode}.json | 10 ----- docs/swagger/paths/{shortCode}_preview.json | 10 ----- docs/swagger/paths/{shortCode}_qr-code.json | 10 ----- docs/swagger/paths/{shortCode}_track.json | 10 ----- docs/swagger/swagger.json | 12 +++--- 13 files changed, 94 insertions(+), 93 deletions(-) create mode 100644 docs/swagger/definitions/InvalidShortUrlMeta.json diff --git a/docs/swagger/definitions/Error.json b/docs/swagger/definitions/Error.json index 3585227d..006fa47a 100644 --- a/docs/swagger/definitions/Error.json +++ b/docs/swagger/definitions/Error.json @@ -1,13 +1,32 @@ { "type": "object", + "required": ["type", "title", "detail", "status"], "properties": { - "code": { + "type": { "type": "string", "description": "A machine unique code" }, + "title": { + "type": "string", + "description": "A unique title" + }, + "detail": { + "type": "string", + "description": "A human-friendly error description" + }, + "status": { + "type": "number", + "description": "HTTP response status code" + }, + "code": { + "type": "string", + "description": "**[Deprecated] Use type instead. Not returned for v2 of the REST API** A machine unique code", + "deprecated": true + }, "message": { "type": "string", - "description": "A human-friendly error message" + "description": "**[Deprecated] Use detail instead. Not returned for v2 of the REST API** A human-friendly error message", + "deprecated": true } } } diff --git a/docs/swagger/definitions/InvalidShortUrlMeta.json b/docs/swagger/definitions/InvalidShortUrlMeta.json new file mode 100644 index 00000000..ebe1fa34 --- /dev/null +++ b/docs/swagger/definitions/InvalidShortUrlMeta.json @@ -0,0 +1,6 @@ +{ + "type": "object", + "properties": { + + } +} diff --git a/docs/swagger/paths/v1_short-urls.json b/docs/swagger/paths/v1_short-urls.json index f8fa1ec8..07ed36cc 100644 --- a/docs/swagger/paths/v1_short-urls.json +++ b/docs/swagger/paths/v1_short-urls.json @@ -150,7 +150,7 @@ "500": { "description": "Unexpected error.", "content": { - "application/json": { + "application/problem+json": { "schema": { "$ref": "../definitions/Error.json" } @@ -256,11 +256,32 @@ } }, "400": { - "description": "The long URL was not provided or is invalid.", + "description": "Some of provided data is invalid. Check extra fields to know exactly what.", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "../definitions/Error.json" + "type": "object", + "allOf": [ + { + "$ref": "./Error.json" + }, + { + "type": "object", + "properties": { + "invalidElements": { + "$ref": "./InvalidShortUrlMeta.json" + }, + "url": { + "type": "string", + "description": "A URL that could not be verified, if the error type is INVALID_URL" + }, + "customSlug": { + "type": "string", + "description": "Provided custom slug when the error type is INVALID_SLUG" + } + } + } + ] } } } @@ -268,7 +289,7 @@ "500": { "description": "Unexpected error.", "content": { - "application/json": { + "application/problem+json": { "schema": { "$ref": "../definitions/Error.json" } diff --git a/docs/swagger/paths/v1_short-urls_shorten.json b/docs/swagger/paths/v1_short-urls_shorten.json index d0c3c4c7..d1887ddd 100644 --- a/docs/swagger/paths/v1_short-urls_shorten.json +++ b/docs/swagger/paths/v1_short-urls_shorten.json @@ -77,7 +77,7 @@ "400": { "description": "The long URL was not provided or is invalid.", "content": { - "application/json": { + "application/problem+json": { "schema": { "$ref": "../definitions/Error.json" } @@ -89,9 +89,12 @@ } }, "examples": { - "application/json": { - "error": "INVALID_URL", - "message": "Provided URL foo is invalid. Try with a different one." + "application/problem+json": { + "title": "Invalid URL", + "type": "INVALID_URL", + "detail": "Provided URL foo is invalid. Try with a different one.", + "status": 400, + "url": "https://invalid-url.com" }, "text/plain": "INVALID_URL" } @@ -99,7 +102,7 @@ "500": { "description": "Unexpected error.", "content": { - "application/json": { + "application/problem+json": { "schema": { "$ref": "../definitions/Error.json" } @@ -111,7 +114,7 @@ } }, "examples": { - "application/json": { + "application/problem+json": { "error": "INTERNAL_SERVER_ERROR", "message": "Unexpected error occurred" }, diff --git a/docs/swagger/paths/v1_short-urls_{shortCode}.json b/docs/swagger/paths/v1_short-urls_{shortCode}.json index bbb7145c..0a5cbed7 100644 --- a/docs/swagger/paths/v1_short-urls_{shortCode}.json +++ b/docs/swagger/paths/v1_short-urls_{shortCode}.json @@ -62,20 +62,10 @@ } } }, - "400": { - "description": "Provided shortCode does not match the character set currently used by the app to generate short codes.", - "content": { - "application/json": { - "schema": { - "$ref": "../definitions/Error.json" - } - } - } - }, "404": { "description": "No URL was found for provided short code.", "content": { - "application/json": { + "application/problem+json": { "schema": { "$ref": "../definitions/Error.json" } @@ -85,7 +75,7 @@ "500": { "description": "Unexpected error.", "content": { - "application/json": { + "application/problem+json": { "schema": { "$ref": "../definitions/Error.json" } @@ -153,7 +143,7 @@ "400": { "description": "Provided meta arguments are invalid.", "content": { - "application/json": { + "application/problem+json": { "schema": { "$ref": "../definitions/Error.json" } @@ -163,7 +153,7 @@ "404": { "description": "No short URL was found for provided short code.", "content": { - "application/json": { + "application/problem+json": { "schema": { "$ref": "../definitions/Error.json" } @@ -173,7 +163,7 @@ "500": { "description": "Unexpected error.", "content": { - "application/json": { + "application/problem+json": { "schema": { "$ref": "../definitions/Error.json" } @@ -242,7 +232,7 @@ "400": { "description": "Provided meta arguments are invalid.", "content": { - "application/json": { + "application/problem+json": { "schema": { "$ref": "../definitions/Error.json" } @@ -252,7 +242,7 @@ "404": { "description": "No short URL was found for provided short code.", "content": { - "application/json": { + "application/problem+json": { "schema": { "$ref": "../definitions/Error.json" } @@ -262,7 +252,7 @@ "500": { "description": "Unexpected error.", "content": { - "application/json": { + "application/problem+json": { "schema": { "$ref": "../definitions/Error.json" } @@ -302,26 +292,28 @@ "204": { "description": "The short URL has been properly deleted." }, - "400": { + "422": { "description": "The visits threshold in shlink does not allow this short URL to be deleted.", "content": { - "application/json": { + "application/problem+json": { "schema": { "$ref": "../definitions/Error.json" } } }, "examples": { - "application/json": { - "error": "INVALID_SHORTCODE_DELETION", - "message": "It is not possible to delete URL with short code \"abc123\" because it has reached more than \"15\" visits." + "application/problem+json": { + "title": "Cannot delete short URL", + "type": "INVALID_SHORTCODE_DELETION", + "detail": "It is not possible to delete URL with short code \"abc123\" because it has reached more than \"15\" visits.", + "status": 422 } } }, "404": { "description": "No short URL was found for provided short code.", "content": { - "application/json": { + "application/problem+json": { "schema": { "$ref": "../definitions/Error.json" } @@ -331,7 +323,7 @@ "500": { "description": "Unexpected error.", "content": { - "application/json": { + "application/problem+json": { "schema": { "$ref": "../definitions/Error.json" } diff --git a/docs/swagger/paths/v1_short-urls_{shortCode}_tags.json b/docs/swagger/paths/v1_short-urls_{shortCode}_tags.json index ab05d230..de3d0c9b 100644 --- a/docs/swagger/paths/v1_short-urls_{shortCode}_tags.json +++ b/docs/swagger/paths/v1_short-urls_{shortCode}_tags.json @@ -78,7 +78,7 @@ "400": { "description": "The request body does not contain a \"tags\" param with array type.", "content": { - "application/json": { + "application/problem+json": { "schema": { "$ref": "../definitions/Error.json" } diff --git a/docs/swagger/paths/v1_short-urls_{shortCode}_visits.json b/docs/swagger/paths/v1_short-urls_{shortCode}_visits.json index a3cf5f10..d5dd243c 100644 --- a/docs/swagger/paths/v1_short-urls_{shortCode}_visits.json +++ b/docs/swagger/paths/v1_short-urls_{shortCode}_visits.json @@ -132,7 +132,7 @@ "404": { "description": "The short code does not belong to any short URL.", "content": { - "application/json": { + "application/problem+json": { "schema": { "$ref": "../definitions/Error.json" } @@ -142,7 +142,7 @@ "500": { "description": "Unexpected error.", "content": { - "application/json": { + "application/problem+json": { "schema": { "$ref": "../definitions/Error.json" } diff --git a/docs/swagger/paths/v1_tags.json b/docs/swagger/paths/v1_tags.json index 5bf260bb..faa83ed9 100644 --- a/docs/swagger/paths/v1_tags.json +++ b/docs/swagger/paths/v1_tags.json @@ -53,7 +53,7 @@ "500": { "description": "Unexpected error.", "content": { - "application/json": { + "application/problem+json": { "schema": { "$ref": "../definitions/Error.json" } @@ -140,7 +140,7 @@ "500": { "description": "Unexpected error.", "content": { - "application/json": { + "application/problem+json": { "schema": { "$ref": "../definitions/Error.json" } @@ -197,7 +197,7 @@ "400": { "description": "You have not provided either the oldName or the newName params.", "content": { - "application/json": { + "application/problem+json": { "schema": { "$ref": "../definitions/Error.json" } @@ -207,7 +207,7 @@ "404": { "description": "There's no tag found with the name provided in oldName param.", "content": { - "application/json": { + "application/problem+json": { "schema": { "$ref": "../definitions/Error.json" } @@ -217,7 +217,7 @@ "500": { "description": "Unexpected error.", "content": { - "application/json": { + "application/problem+json": { "schema": { "$ref": "../definitions/Error.json" } @@ -263,7 +263,7 @@ "500": { "description": "Unexpected error.", "content": { - "application/json": { + "application/problem+json": { "schema": { "$ref": "../definitions/Error.json" } diff --git a/docs/swagger/paths/{shortCode}.json b/docs/swagger/paths/{shortCode}.json index eccd5ba1..bbebacbd 100644 --- a/docs/swagger/paths/{shortCode}.json +++ b/docs/swagger/paths/{shortCode}.json @@ -20,16 +20,6 @@ "responses": { "302": { "description": "Visit properly tracked and redirected" - }, - "500": { - "description": "Unexpected error.", - "content": { - "application/json": { - "schema": { - "$ref": "../definitions/Error.json" - } - } - } } } } diff --git a/docs/swagger/paths/{shortCode}_preview.json b/docs/swagger/paths/{shortCode}_preview.json index f6168b4d..98df559c 100644 --- a/docs/swagger/paths/{shortCode}_preview.json +++ b/docs/swagger/paths/{shortCode}_preview.json @@ -29,16 +29,6 @@ } } } - }, - "500": { - "description": "Unexpected error.", - "content": { - "application/json": { - "schema": { - "$ref": "../definitions/Error.json" - } - } - } } } } diff --git a/docs/swagger/paths/{shortCode}_qr-code.json b/docs/swagger/paths/{shortCode}_qr-code.json index 14a8fddc..300a7429 100644 --- a/docs/swagger/paths/{shortCode}_qr-code.json +++ b/docs/swagger/paths/{shortCode}_qr-code.json @@ -40,16 +40,6 @@ } } } - }, - "500": { - "description": "Unexpected error.", - "content": { - "application/json": { - "schema": { - "$ref": "../definitions/Error.json" - } - } - } } } } diff --git a/docs/swagger/paths/{shortCode}_track.json b/docs/swagger/paths/{shortCode}_track.json index b4b62bba..50f6bc5e 100644 --- a/docs/swagger/paths/{shortCode}_track.json +++ b/docs/swagger/paths/{shortCode}_track.json @@ -28,16 +28,6 @@ } } } - }, - "500": { - "description": "Unexpected error.", - "content": { - "application/json": { - "schema": { - "$ref": "../definitions/Error.json" - } - } - } } } } diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index 6dd3ff8f..2d0cb1c1 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -71,24 +71,24 @@ ], "paths": { - "/rest/v1/short-urls": { + "/rest/v2/short-urls": { "$ref": "paths/v1_short-urls.json" }, - "/rest/v1/short-urls/shorten": { + "/rest/v2/short-urls/shorten": { "$ref": "paths/v1_short-urls_shorten.json" }, - "/rest/v1/short-urls/{shortCode}": { + "/rest/v2/short-urls/{shortCode}": { "$ref": "paths/v1_short-urls_{shortCode}.json" }, - "/rest/v1/short-urls/{shortCode}/tags": { + "/rest/v2/short-urls/{shortCode}/tags": { "$ref": "paths/v1_short-urls_{shortCode}_tags.json" }, - "/rest/v1/tags": { + "/rest/v2/tags": { "$ref": "paths/v1_tags.json" }, - "/rest/v1/short-urls/{shortCode}/visits": { + "/rest/v2/short-urls/{shortCode}/visits": { "$ref": "paths/v1_short-urls_{shortCode}_visits.json" }, From 3cf1657d54b6d9d2282ef6d6403cf5f6211f081e Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 29 Nov 2019 18:55:27 +0100 Subject: [PATCH 38/40] Simplified invalidElements to be a plain list of keys when a ValidationException is cast into a problem details error --- module/Core/src/Exception/ValidationException.php | 9 +++++++-- module/Core/test/Exception/ValidationExceptionTest.php | 2 ++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/module/Core/src/Exception/ValidationException.php b/module/Core/src/Exception/ValidationException.php index f3a5c515..abceec91 100644 --- a/module/Core/src/Exception/ValidationException.php +++ b/module/Core/src/Exception/ValidationException.php @@ -10,6 +10,7 @@ use Zend\InputFilter\InputFilterInterface; use Zend\ProblemDetails\Exception\CommonProblemDetailsExceptionTrait; use Zend\ProblemDetails\Exception\ProblemDetailsExceptionInterface; +use function array_keys; use function Functional\reduce_left; use function is_array; use function print_r; @@ -24,6 +25,9 @@ class ValidationException extends InvalidArgumentException implements ProblemDet private const TITLE = 'Invalid data'; private const TYPE = 'INVALID_ARGUMENT'; + /** @var array */ + private $invalidElements; + public static function fromInputFilter(InputFilterInterface $inputFilter, ?Throwable $prev = null): self { return static::fromArray($inputFilter->getMessages(), $prev); @@ -38,14 +42,15 @@ class ValidationException extends InvalidArgumentException implements ProblemDet $e->title = self::TITLE; $e->type = self::TYPE; $e->status = StatusCodeInterface::STATUS_BAD_REQUEST; - $e->additional = ['invalidElements' => $invalidData]; + $e->invalidElements = $invalidData; + $e->additional = ['invalidElements' => array_keys($invalidData)]; return $e; } public function getInvalidElements(): array { - return $this->additional['invalidElements']; + return $this->invalidElements; } public function __toString(): string diff --git a/module/Core/test/Exception/ValidationExceptionTest.php b/module/Core/test/Exception/ValidationExceptionTest.php index 069506b5..11bb8026 100644 --- a/module/Core/test/Exception/ValidationExceptionTest.php +++ b/module/Core/test/Exception/ValidationExceptionTest.php @@ -12,6 +12,7 @@ use Shlinkio\Shlink\Core\Exception\ValidationException; use Throwable; use Zend\InputFilter\InputFilterInterface; +use function array_keys; use function print_r; class ValidationExceptionTest extends TestCase @@ -38,6 +39,7 @@ EOT; $e = ValidationException::fromInputFilter($inputFilter->reveal()); $this->assertEquals($invalidData, $e->getInvalidElements()); + $this->assertEquals(['invalidElements' => array_keys($invalidData)], $e->getAdditionalData()); $this->assertEquals('Provided data is not valid', $e->getMessage()); $this->assertEquals(StatusCodeInterface::STATUS_BAD_REQUEST, $e->getCode()); $this->assertEquals($prev, $e->getPrevious()); From 4685572deffa3d4f59d5dac0321fc76d0803e8da Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 29 Nov 2019 19:09:03 +0100 Subject: [PATCH 39/40] Added version param to endpoints --- .../definitions/InvalidShortUrlMeta.json | 6 ---- docs/swagger/parameters/version.json | 13 +++++++ docs/swagger/paths/v1_short-urls.json | 21 ++++++++++- docs/swagger/paths/v1_short-urls_shorten.json | 3 ++ .../paths/v1_short-urls_{shortCode}.json | 36 ++++++++++++++++++- .../paths/v1_short-urls_{shortCode}_tags.json | 3 ++ .../v1_short-urls_{shortCode}_visits.json | 3 ++ docs/swagger/paths/v1_tags.json | 18 ++++++++++ docs/swagger/swagger.json | 12 +++---- 9 files changed, 101 insertions(+), 14 deletions(-) delete mode 100644 docs/swagger/definitions/InvalidShortUrlMeta.json create mode 100644 docs/swagger/parameters/version.json diff --git a/docs/swagger/definitions/InvalidShortUrlMeta.json b/docs/swagger/definitions/InvalidShortUrlMeta.json deleted file mode 100644 index ebe1fa34..00000000 --- a/docs/swagger/definitions/InvalidShortUrlMeta.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "type": "object", - "properties": { - - } -} diff --git a/docs/swagger/parameters/version.json b/docs/swagger/parameters/version.json new file mode 100644 index 00000000..c2b1cc1a --- /dev/null +++ b/docs/swagger/parameters/version.json @@ -0,0 +1,13 @@ +{ + "name": "version", + "description": "The API version to be consumed", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "2", + "1" + ] + } +} diff --git a/docs/swagger/paths/v1_short-urls.json b/docs/swagger/paths/v1_short-urls.json index 07ed36cc..a8bf0368 100644 --- a/docs/swagger/paths/v1_short-urls.json +++ b/docs/swagger/paths/v1_short-urls.json @@ -7,6 +7,9 @@ "summary": "List short URLs", "description": "Returns the list of short URLs.

**Important note**: Before shlink v1.13, this endpoint used to use the `/short-codes` path instead of `/short-urls`. Both of them will keep working, while the first one is considered deprecated.", "parameters": [ + { + "$ref": "../parameters/version.json" + }, { "name": "page", "in": "query", @@ -175,6 +178,11 @@ "Bearer": [] } ], + "parameters": [ + { + "$ref": "../parameters/version.json" + } + ], "requestBody": { "description": "Request body.", "required": true, @@ -269,7 +277,18 @@ "type": "object", "properties": { "invalidElements": { - "$ref": "./InvalidShortUrlMeta.json" + "type": "array", + "items": { + "type": "string", + "enum": [ + "validSince", + "validUntil", + "customSlug", + "maxVisits", + "findIfExists", + "domain" + ] + } }, "url": { "type": "string", diff --git a/docs/swagger/paths/v1_short-urls_shorten.json b/docs/swagger/paths/v1_short-urls_shorten.json index d1887ddd..49233c49 100644 --- a/docs/swagger/paths/v1_short-urls_shorten.json +++ b/docs/swagger/paths/v1_short-urls_shorten.json @@ -7,6 +7,9 @@ "summary": "Create a short URL", "description": "Creates a short URL in a single API call. Useful for third party integrations.

**Important note**: Before shlink v1.13, this endpoint used to use the `/short-codes` path instead of `/short-urls`. Both of them will keep working, while the first one is considered deprecated.", "parameters": [ + { + "$ref": "../parameters/version.json" + }, { "name": "apiKey", "in": "query", diff --git a/docs/swagger/paths/v1_short-urls_{shortCode}.json b/docs/swagger/paths/v1_short-urls_{shortCode}.json index 0a5cbed7..1588d71c 100644 --- a/docs/swagger/paths/v1_short-urls_{shortCode}.json +++ b/docs/swagger/paths/v1_short-urls_{shortCode}.json @@ -7,6 +7,9 @@ "summary": "Parse short code", "description": "Get the long URL behind a short URL's short code.

**Important note**: Before shlink v1.13, this endpoint used to use the `/short-codes` path instead of `/short-urls`. Both of them will keep working, while the first one is considered deprecated.", "parameters": [ + { + "$ref": "../parameters/version.json" + }, { "name": "shortCode", "in": "path", @@ -93,6 +96,9 @@ "summary": "Edit short URL", "description": "Update certain meta arguments from an existing short URL.

**Important note**: Before shlink v1.13, this endpoint used to use the `/short-codes` path instead of `/short-urls`. Both of them will keep working, while the first one is considered deprecated.", "parameters": [ + { + "$ref": "../parameters/version.json" + }, { "name": "shortCode", "in": "path", @@ -145,7 +151,29 @@ "content": { "application/problem+json": { "schema": { - "$ref": "../definitions/Error.json" + "type": "object", + "allOf": [ + { + "$ref": "./Error.json" + }, + { + "type": "object", + "required": ["invalidElements"], + "properties": { + "invalidElements": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "validSince", + "validUntil", + "maxVisits" + ] + } + } + } + } + ] } } } @@ -182,6 +210,9 @@ "summary": "[DEPRECATED] Edit short URL", "description": "**[DEPRECATED]** Use [editShortUrl](#/Short_URLs/getShortUrl) instead", "parameters": [ + { + "$ref": "../parameters/version.json" + }, { "name": "shortCode", "in": "path", @@ -270,6 +301,9 @@ "summary": "Delete short URL", "description": "Deletes the short URL for provided short code.

**Important note**: Before shlink v1.13, this endpoint used to use the `/short-codes` path instead of `/short-urls`. Both of them will keep working, while the first one is considered deprecated.", "parameters": [ + { + "$ref": "../parameters/version.json" + }, { "name": "shortCode", "in": "path", diff --git a/docs/swagger/paths/v1_short-urls_{shortCode}_tags.json b/docs/swagger/paths/v1_short-urls_{shortCode}_tags.json index de3d0c9b..45142e62 100644 --- a/docs/swagger/paths/v1_short-urls_{shortCode}_tags.json +++ b/docs/swagger/paths/v1_short-urls_{shortCode}_tags.json @@ -7,6 +7,9 @@ "summary": "Edit tags on short URL", "description": "Edit the tags on URL identified by provided short code.

**Important note**: Before shlink v1.13, this endpoint used to use the `/short-codes` path instead of `/short-urls`. Both of them will keep working, while the first one is considered deprecated.", "parameters": [ + { + "$ref": "../parameters/version.json" + }, { "name": "shortCode", "in": "path", diff --git a/docs/swagger/paths/v1_short-urls_{shortCode}_visits.json b/docs/swagger/paths/v1_short-urls_{shortCode}_visits.json index d5dd243c..508449de 100644 --- a/docs/swagger/paths/v1_short-urls_{shortCode}_visits.json +++ b/docs/swagger/paths/v1_short-urls_{shortCode}_visits.json @@ -7,6 +7,9 @@ "summary": "List visits for short URL", "description": "Get the list of visits on the short URL behind provided short code.

**Important note**: Before shlink v1.13, this endpoint used to use the `/short-codes` path instead of `/short-urls`. Both of them will keep working, while the first one is considered deprecated.", "parameters": [ + { + "$ref": "../parameters/version.json" + }, { "name": "shortCode", "in": "path", diff --git a/docs/swagger/paths/v1_tags.json b/docs/swagger/paths/v1_tags.json index faa83ed9..da0159bd 100644 --- a/docs/swagger/paths/v1_tags.json +++ b/docs/swagger/paths/v1_tags.json @@ -14,6 +14,11 @@ "Bearer": [] } ], + "parameters": [ + { + "$ref": "../parameters/version.json" + } + ], "responses": { "200": { "description": "The list of tags", @@ -78,6 +83,11 @@ "Bearer": [] } ], + "parameters": [ + { + "$ref": "../parameters/version.json" + } + ], "requestBody": { "description": "Request body.", "required": true, @@ -165,6 +175,11 @@ "Bearer": [] } ], + "parameters": [ + { + "$ref": "../parameters/version.json" + } + ], "requestBody": { "description": "Request body.", "required": true, @@ -235,6 +250,9 @@ "summary": "Delete tags", "description": "Deletes provided list of tags", "parameters": [ + { + "$ref": "../parameters/version.json" + }, { "name": "tags[]", "in": "query", diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index 2d0cb1c1..d924b8a0 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -71,24 +71,24 @@ ], "paths": { - "/rest/v2/short-urls": { + "/rest/v{version}/short-urls": { "$ref": "paths/v1_short-urls.json" }, - "/rest/v2/short-urls/shorten": { + "/rest/v{version}/short-urls/shorten": { "$ref": "paths/v1_short-urls_shorten.json" }, - "/rest/v2/short-urls/{shortCode}": { + "/rest/v{version}/short-urls/{shortCode}": { "$ref": "paths/v1_short-urls_{shortCode}.json" }, - "/rest/v2/short-urls/{shortCode}/tags": { + "/rest/v{version}/short-urls/{shortCode}/tags": { "$ref": "paths/v1_short-urls_{shortCode}_tags.json" }, - "/rest/v2/tags": { + "/rest/v{version}/tags": { "$ref": "paths/v1_tags.json" }, - "/rest/v2/short-urls/{shortCode}/visits": { + "/rest/v{version}/short-urls/{shortCode}/visits": { "$ref": "paths/v1_short-urls_{shortCode}_visits.json" }, From 6c37905c1582a1a26bdb7a6d40944159eba078f0 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 29 Nov 2019 19:24:04 +0100 Subject: [PATCH 40/40] Fixed cross-domain headers being lost when ProblemDetailsMiddleware throws an error --- config/autoload/middleware-pipeline.global.php | 2 +- docs/swagger/paths/v1_short-urls.json | 2 +- docs/swagger/paths/v1_short-urls_{shortCode}.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config/autoload/middleware-pipeline.global.php b/config/autoload/middleware-pipeline.global.php index f3370a7a..35a9a16b 100644 --- a/config/autoload/middleware-pipeline.global.php +++ b/config/autoload/middleware-pipeline.global.php @@ -20,6 +20,7 @@ return [ 'error-handler-rest' => [ 'path' => '/rest', 'middleware' => [ + Rest\Middleware\CrossDomainMiddleware::class, Rest\Middleware\BackwardsCompatibleProblemDetailsMiddleware::class, ProblemDetails\ProblemDetailsMiddleware::class, ], @@ -47,7 +48,6 @@ return [ 'rest' => [ 'path' => '/rest', 'middleware' => [ - Rest\Middleware\CrossDomainMiddleware::class, Expressive\Router\Middleware\ImplicitOptionsMiddleware::class, Rest\Middleware\BodyParserMiddleware::class, Rest\Middleware\AuthenticationMiddleware::class, diff --git a/docs/swagger/paths/v1_short-urls.json b/docs/swagger/paths/v1_short-urls.json index a8bf0368..d0aebfec 100644 --- a/docs/swagger/paths/v1_short-urls.json +++ b/docs/swagger/paths/v1_short-urls.json @@ -271,7 +271,7 @@ "type": "object", "allOf": [ { - "$ref": "./Error.json" + "$ref": "../definitions/Error.json" }, { "type": "object", diff --git a/docs/swagger/paths/v1_short-urls_{shortCode}.json b/docs/swagger/paths/v1_short-urls_{shortCode}.json index 1588d71c..95532868 100644 --- a/docs/swagger/paths/v1_short-urls_{shortCode}.json +++ b/docs/swagger/paths/v1_short-urls_{shortCode}.json @@ -154,7 +154,7 @@ "type": "object", "allOf": [ { - "$ref": "./Error.json" + "$ref": "../definitions/Error.json" }, { "type": "object",