diff --git a/src/bundle/EventListener/RequestListener.php b/src/bundle/EventListener/RequestListener.php index 5b67493a..890486ef 100644 --- a/src/bundle/EventListener/RequestListener.php +++ b/src/bundle/EventListener/RequestListener.php @@ -6,24 +6,39 @@ */ namespace Ibexa\Bundle\Rest\EventListener; +use Ibexa\Bundle\Rest\UriParser\UriParser; +use Ibexa\Contracts\Rest\UriParser\UriParserInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\KernelEvents; /** + * @internal + * * REST request listener. * * Flags a REST request as such using the is_rest_request attribute. */ class RequestListener implements EventSubscriberInterface { - public const REST_PREFIX_PATTERN = '/^\/api\/[a-zA-Z0-9-_]+\/v\d+(\.\d+)?\//'; + /** + * @deprecated rely on \Ibexa\Contracts\Rest\UriParser\UriParserInterface::isRestRequest instead. + * @see \Ibexa\Contracts\Rest\UriParser\UriParserInterface::isRestRequest() + */ + public const REST_PREFIX_PATTERN = UriParser::DEFAULT_REST_PREFIX_PATTERN; + + private UriParserInterface $uriParser; + + public function __construct(UriParserInterface $uriParser) + { + $this->uriParser = $uriParser; + } /** * @return array */ - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return [ // 10001 is to ensure that REST requests are tagged before CorsListener is called @@ -33,24 +48,22 @@ public static function getSubscribedEvents() /** * If the request is a REST one, sets the is_rest_request request attribute. - * - * @param \Symfony\Component\HttpKernel\Event\RequestEvent $event */ - public function onKernelRequest(RequestEvent $event) + public function onKernelRequest(RequestEvent $event): void { - $isRestRequest = true; - - if (!$this->hasRestPrefix($event->getRequest())) { - $isRestRequest = false; - } - - $event->getRequest()->attributes->set('is_rest_request', $isRestRequest); + $event->getRequest()->attributes->set( + 'is_rest_request', + $this->uriParser->isRestRequest($event->getRequest()) + ); } /** * @param \Symfony\Component\HttpFoundation\Request $request * * @return bool + * + * @deprecated use \Ibexa\Contracts\Rest\UriParser\UriParserInterface::isRestRequest instead + * @see \Ibexa\Contracts\Rest\UriParser\UriParserInterface::isRestRequest() */ protected function hasRestPrefix(Request $request) { diff --git a/src/bundle/RequestParser/Router.php b/src/bundle/RequestParser/Router.php index 766500d7..7483d888 100644 --- a/src/bundle/RequestParser/Router.php +++ b/src/bundle/RequestParser/Router.php @@ -6,55 +6,37 @@ */ namespace Ibexa\Bundle\Rest\RequestParser; -use Ibexa\Contracts\Rest\Exceptions\InvalidArgumentException; +use Ibexa\Contracts\Rest\UriParser\UriParserInterface; use Ibexa\Rest\RequestParser; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Symfony\Component\Routing\RouterInterface; /** + * @deprecated use \Ibexa\Contracts\Rest\UriParser\UriParserInterface instead + * @see \Ibexa\Contracts\Rest\UriParser\UriParserInterface + * * Router based request parser. */ class Router implements RequestParser { - /** - * @var \Symfony\Cmf\Component\Routing\ChainRouter - */ - private $router; + private RouterInterface $router; + + private UriParserInterface $uriParser; - public function __construct(RouterInterface $router) + public function __construct(RouterInterface $router, UriParserInterface $uriParser) { $this->router = $router; + $this->uriParser = $uriParser; } /** - * @throws \Symfony\Component\Routing\Exception\ResourceNotFoundException If no match was found + * @return array matched route configuration and parameters + * + * @throws \Ibexa\Contracts\Rest\Exceptions\InvalidArgumentException If no match was found */ - public function parse($url) + public function parse($url): array { - // we create a request with a new context in order to match $url to a route and get its properties - $request = Request::create($url, 'GET'); - $originalContext = $this->router->getContext(); - $context = clone $originalContext; - $context->fromRequest($request); - $this->router->setContext($context); - - try { - $matchResult = $this->router->matchRequest($request); - } catch (ResourceNotFoundException $e) { - // Note: this probably won't occur in real life because of the legacy matcher - $this->router->setContext($originalContext); - throw new InvalidArgumentException("No route matched '$url'"); - } - - if (!$this->matchesRestRequest($matchResult)) { - $this->router->setContext($originalContext); - throw new InvalidArgumentException("No route matched '$url'"); - } - - $this->router->setContext($originalContext); - - return $matchResult; + return $this->uriParser->matchUri($url); } public function generate($type, array $values = []) @@ -63,35 +45,11 @@ public function generate($type, array $values = []) } /** - * @throws \Ibexa\Core\Base\Exceptions\InvalidArgumentException If $attribute wasn't found in the match + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException If $attribute wasn't found in the match */ public function parseHref($href, $attribute) { - $parsingResult = $this->parse($href); - - if (!isset($parsingResult[$attribute])) { - throw new InvalidArgumentException("No attribute '$attribute' in route matched from $href"); - } - - return $parsingResult[$attribute]; - } - - /** - * Checks if a router match response matches a REST resource. - * - * @param array $match Match array returned by Router::match() / Router::matchRequest() - * - * @throws \Ibexa\Contracts\Rest\Exceptions\InvalidArgumentException if the \$match isn't valid - * - * @return bool - */ - private function matchesRestRequest(array $match) - { - if (!isset($match['_route'])) { - throw new InvalidArgumentException('Invalid $match parameter, no _route key'); - } - - return strpos($match['_route'], 'ibexa.rest.') === 0; + return $this->uriParser->getAttributeFromUri($href, $attribute); } } diff --git a/src/bundle/Resources/config/services.yml b/src/bundle/Resources/config/services.yml index bf050962..56b7b449 100644 --- a/src/bundle/Resources/config/services.yml +++ b/src/bundle/Resources/config/services.yml @@ -6,6 +6,7 @@ parameters: - '(^application/vnd\.ibexa\.api\.[A-Za-z]+\+xml$)' - '(^application/xml$)' - '(^.*/.*$)' + ibexa.rest.path_prefix.pattern: !php/const \Ibexa\Bundle\Rest\UriParser\UriParser::DEFAULT_REST_PREFIX_PATTERN services: Ibexa\Bundle\Rest\Serializer\SerializerFactory: @@ -43,7 +44,15 @@ services: Ibexa\Bundle\Rest\RequestParser\Router: arguments: - - "@router" + $router: '@router' + $uriParser: '@Ibexa\Contracts\Rest\UriParser\UriParserInterface' + + Ibexa\Contracts\Rest\UriParser\UriParserInterface: '@Ibexa\Bundle\Rest\UriParser\UriParser' + + Ibexa\Bundle\Rest\UriParser\UriParser: + arguments: + $urlMatcher: '@Symfony\Component\Routing\Matcher\UrlMatcherInterface' + $restPrefixPattern: '%ibexa.rest.path_prefix.pattern%' Ibexa\Rest\Input\ParserTools: ~ @@ -193,6 +202,8 @@ services: tags: [controller.service_arguments] Ibexa\Bundle\Rest\EventListener\RequestListener: + arguments: + $uriParser: '@Ibexa\Contracts\Rest\UriParser\UriParserInterface' tags: - { name: kernel.event_subscriber } diff --git a/src/bundle/UriParser/UriParser.php b/src/bundle/UriParser/UriParser.php new file mode 100644 index 00000000..f2feb460 --- /dev/null +++ b/src/bundle/UriParser/UriParser.php @@ -0,0 +1,95 @@ +urlMatcher = $urlMatcher; + $this->restPrefixPattern = $restPrefixPattern; + } + + public function matchUri(string $uri, string $method = 'GET'): array + { + if (!$this->hasRestPrefix($uri)) { + // keeping the original exception message for BC, otherwise could be more verbose + throw new InvalidArgumentException("No route matched '$uri'"); + } + + $request = Request::create($uri, $method); + + $originalContext = $this->urlMatcher->getContext(); + $context = clone $originalContext; + $context->fromRequest($request); + $this->urlMatcher->setContext($context); + + try { + return $this->urlMatcher->match($request->getPathInfo()); + } catch (MethodNotAllowedException $e) { + // seems MethodNotAllowedException has no message set + $allowedMethods = implode(', ', $e->getAllowedMethods()); + throw new InvalidArgumentException( + "Method '$method' is not allowed for '$uri'. Allowed: [$allowedMethods]", + $e->getCode(), + $e + ); + } catch (ResourceNotFoundException $e) { + throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); + } finally { + $this->urlMatcher->setContext($originalContext); + } + } + + public function getAttributeFromUri(string $uri, string $attribute, string $method = 'GET'): string + { + $parsingResult = $this->matchUri($uri, $method); + + if (!isset($parsingResult[$attribute])) { + throw new InvalidArgumentException("No attribute '$attribute' in route matched from $uri"); + } + + return (string)$parsingResult[$attribute]; + } + + public function isRestRequest(Request $request): bool + { + return $this->hasRestPrefix($request->getPathInfo()); + } + + public function hasRestPrefix(string $uri): bool + { + return (bool)preg_match($this->restPrefixPattern, $uri); + } +} diff --git a/src/contracts/UriParser/UriParserInterface.php b/src/contracts/UriParser/UriParserInterface.php new file mode 100644 index 00000000..2349e87b --- /dev/null +++ b/src/contracts/UriParser/UriParserInterface.php @@ -0,0 +1,32 @@ + matched route configuration and parameters + * + * @throws \Ibexa\Contracts\Rest\Exceptions\InvalidArgumentException + */ + public function matchUri(string $uri, string $method = 'GET'): array; +} diff --git a/tests/bundle/EventListener/RequestListenerTest.php b/tests/bundle/EventListener/RequestListenerTest.php index 1a0fb7cb..2b9ef6ba 100644 --- a/tests/bundle/EventListener/RequestListenerTest.php +++ b/tests/bundle/EventListener/RequestListenerTest.php @@ -7,18 +7,22 @@ namespace Ibexa\Tests\Bundle\Rest\EventListener; use Ibexa\Bundle\Rest\EventListener\RequestListener; -use Ibexa\Rest\Server\View\AcceptHeaderVisitorDispatcher; +use Ibexa\Bundle\Rest\UriParser\UriParser; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\Routing\Matcher\UrlMatcherInterface; -class RequestListenerTest extends EventListenerTest +final class RequestListenerTest extends EventListenerTest { public const REST_ROUTE = '/api/ibexa/v2/rest-route'; public const NON_REST_ROUTE = '/non-rest-route'; - public function provideExpectedSubscribedEventTypes() + /** + * @return array + */ + public function provideExpectedSubscribedEventTypes(): array { return [ [ @@ -27,97 +31,69 @@ public function provideExpectedSubscribedEventTypes() ]; } - public static function restRequestUrisProvider() + /** + * @retirm array + */ + public static function getDataForTestOnKernelRequest(): array { return [ - ['/api/ibexa/v2/true'], - ['/api/bundle-name/v2/true'], - ['/api/MyBundle12/v2/true'], - ['/api/ThisIs_Bundle123/v2/true'], - ['/api/my-bundle/v1/true'], - ['/api/my-bundle/v2/true'], - ['/api/my-bundle/v2.7/true'], - ['/api/my-bundle/v122.73/true'], + // REST requests + [self::REST_ROUTE, true], + ['/api/ibexa/v2/true', true], + ['/api/bundle-name/v2/true', true], + ['/api/MyBundle12/v2/true', true], + ['/api/ThisIs_Bundle123/v2/true', true], + ['/api/my-bundle/v1/true', true], + ['/api/my-bundle/v2/true', true], + ['/api/my-bundle/v2.7/true', true], + ['/api/my-bundle/v122.73/true', true], + // non-REST requests + [self::NON_REST_ROUTE, false], + ['/ap/ezp/v2/false', false], + ['/api/bundle name/v2/false', false], + ['/api/My/Bundle/v2/false', false], + ['/api//v2/false', false], + ['/api/my-bundle/v/false', false], + ['/api/my-bundle/v2-2/false', false], + ['/api/my-bundle/v2 7/false', false], + ['/api/my-bundle/v/7/false', false], ]; } - public static function nonRestRequestsUrisProvider() + /** + * @return array + */ + public static function nonRestRequestsUrisProvider(): array { return [ - ['/ap/ezp/v2/false'], - ['/api/bundle name/v2/false'], - ['/api/My/Bundle/v2/false'], - ['/api//v2/false'], - ['/api/my-bundle/v/false'], - ['/api/my-bundle/v2-2/false'], - ['/api/my-bundle/v2 7/false'], - ['/api/my-bundle/v/7/false'], ]; } - public function testOnKernelRequestNotMasterRequest() + public function testOnKernelRequestNotMasterRequest(): void { $request = $this->performFakeRequest(self::REST_ROUTE, HttpKernelInterface::SUB_REQUEST); self::assertTrue($request->attributes->get('is_rest_request')); } - public function testOnKernelRequestNotRestRequest() - { - $request = $this->performFakeRequest(self::NON_REST_ROUTE); - - self::assertFalse($request->attributes->get('is_rest_request')); - } - - public function testOnKernelRequestRestRequest() - { - $request = $this->performFakeRequest(self::REST_ROUTE); - - self::assertTrue($request->attributes->get('is_rest_request')); - } - - /** - * @dataProvider restRequestUrisProvider - */ - public function testRestRequestVariations($uri) - { - $request = $this->performFakeRequest($uri); - - self::assertTrue($request->attributes->get('is_rest_request')); - } - /** - * @dataProvider nonRestRequestsUrisProvider + * @dataProvider getDataForTestOnKernelRequest */ - public function testNonRestRequestVariations($uri) + public function testOnKernelRequest(string $uri, bool $isExpectedRestRequest): void { $request = $this->performFakeRequest($uri); - self::assertFalse($request->attributes->get('is_rest_request')); + self::assertSame($isExpectedRestRequest, $request->attributes->get('is_rest_request')); } - /** - * @return \Ibexa\Bundle\Rest\EventListener\RequestListener - */ - protected function getEventListener() + protected function getEventListener(): RequestListener { return new RequestListener( - $this->getVisitorDispatcherMock() + new UriParser($this->createMock(UrlMatcherInterface::class)) ); } - /** - * @return \Ibexa\Rest\Server\View\AcceptHeaderVisitorDispatcher|\PHPUnit\Framework\MockObject\MockObject - */ - public function getVisitorDispatcherMock() - { - return $this->createMock(AcceptHeaderVisitorDispatcher::class); - } - - /** - * @return \Symfony\Component\HttpFoundation\Request - */ - protected function performFakeRequest($uri, $type = HttpKernelInterface::MASTER_REQUEST) + protected function performFakeRequest(string $uri, int $type = HttpKernelInterface::MAIN_REQUEST): Request { $event = new RequestEvent( $this->createMock(HttpKernelInterface::class), diff --git a/tests/bundle/RequestParser/RouterTest.php b/tests/bundle/RequestParser/RouterTest.php index a234dfe7..367c7734 100644 --- a/tests/bundle/RequestParser/RouterTest.php +++ b/tests/bundle/RequestParser/RouterTest.php @@ -4,39 +4,39 @@ * @copyright Copyright (C) Ibexa AS. All rights reserved. * @license For full copyright and license information view LICENSE file distributed with this source code. */ +declare(strict_types=1); + namespace Ibexa\Tests\Bundle\Rest\RequestParser; use Ibexa\Bundle\Rest\RequestParser\Router as RouterRequestParser; +use Ibexa\Bundle\Rest\UriParser\UriParser; use Ibexa\Contracts\Rest\Exceptions\InvalidArgumentException; +use Ibexa\Rest\RequestParser; +use PHPUnit\Framework\MockObject\Builder\InvocationMocker; +use PHPUnit\Framework\MockObject\Rule\InvokedCount as InvokedCountMatcher; use PHPUnit\Framework\TestCase; -use Symfony\Cmf\Component\Routing\ChainRouter; -use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Symfony\Component\Routing\RequestContext; +use Symfony\Component\Routing\RouterInterface; -class RouterTest extends TestCase +final class RouterTest extends TestCase { - /** - * @var \Symfony\Cmf\Component\Routing\ChainRouter - */ - private $router; + private RouterInterface $router; - protected static $routePrefix = '/api/test/v1'; + private static $routePrefix = '/api/test/v1'; - public function testParse() + public function testParse(): void { $uri = self::$routePrefix . '/'; - $request = Request::create($uri, 'GET'); $expectedMatchResult = [ '_route' => 'ibexa.rest.test_route', '_controller' => '', ]; - $this->getRouterMock() - ->expects($this->once()) - ->method('matchRequest') - ->willReturn($expectedMatchResult); + $this->getRouterInvocationMockerForMatchingUri($uri) + ->willReturn($expectedMatchResult) + ; self::assertEquals( $expectedMatchResult, @@ -44,37 +44,36 @@ public function testParse() ); } - public function testParseNoMatch() + public function testParseNoMatch(): void { $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('No route matched \'/api/test/v1/nomatch\''); + $exceptionMessage = 'No route matched \'/api/test/v1/nomatch\''; + $this->expectExceptionMessage($exceptionMessage); $uri = self::$routePrefix . '/nomatch'; - $this->getRouterMock() - ->expects($this->once()) - ->method('matchRequest') - ->will($this->throwException(new ResourceNotFoundException())); + $this->getRouterInvocationMockerForMatchingUri($uri) + ->willThrowException(new ResourceNotFoundException($exceptionMessage)) + ; $this->getRequestParser()->parse($uri); } - public function testParseNoPrefix() + public function testParseNoPrefix(): void { $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('No route matched \'/no/prefix\''); + $exceptionMessage = 'No route matched \'/no/prefix\''; + $this->expectExceptionMessage($exceptionMessage); $uri = '/no/prefix'; - $this->getRouterMock() - ->expects($this->once()) - ->method('matchRequest') - ->will($this->throwException(new ResourceNotFoundException())); + // invalid prefix should cause internal url matcher not to be called + $this->getRouterInvocationMockerForMatchingUri($uri, self::never()); $this->getRequestParser()->parse($uri); } - public function testParseHref() + public function testParseHref(): void { $href = '/api/test/v1/content/objects/1'; @@ -83,18 +82,19 @@ public function testParseHref() 'contentId' => 1, ]; - $this->getRouterMock() - ->expects($this->once()) - ->method('matchRequest') - ->willReturn($expectedMatchResult); + $this->getRouterInvocationMockerForMatchingUri($href) + ->willReturn($expectedMatchResult) + ; self::assertEquals(1, $this->getRequestParser()->parseHref($href, 'contentId')); } - public function testParseHrefAttributeNotFound() + public function testParseHrefAttributeNotFound(): void { $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('No attribute \'badAttribute\' in route matched from /api/test/v1/content/no-attribute'); + $this->expectExceptionMessage( + 'No attribute \'badAttribute\' in route matched from /api/test/v1/content/no-attribute' + ); $href = '/api/test/v1/content/no-attribute'; @@ -102,25 +102,25 @@ public function testParseHrefAttributeNotFound() '_route' => 'ibexa.rest.test_parse_href_attribute_not_found', ]; - $this->getRouterMock() - ->expects($this->once()) - ->method('matchRequest') - ->willReturn($matchResult); + $this->getRouterInvocationMockerForMatchingUri($href) + ->willReturn($matchResult) + ; self::assertEquals(1, $this->getRequestParser()->parseHref($href, 'badAttribute')); } - public function testGenerate() + public function testGenerate(): void { $routeName = 'ibexa.rest.test_generate'; $arguments = ['arg1' => 1]; $expectedResult = self::$routePrefix . '/generate/' . $arguments['arg1']; $this->getRouterMock() - ->expects($this->once()) - ->method('generate') - ->with($routeName, $arguments) - ->willReturn($expectedResult); + ->expects($this->once()) + ->method('generate') + ->with($routeName, $arguments) + ->willReturn($expectedResult) + ; self::assertEquals( $expectedResult, @@ -128,32 +128,43 @@ public function testGenerate() ); } - /** - * @return \Ibexa\Bundle\Rest\RequestParser\Router - */ - private function getRequestParser() + private function getRequestParser(): RequestParser { + $routerMock = $this->getRouterMock(); + return new RouterRequestParser( - $this->getRouterMock() + $routerMock, + new UriParser($routerMock) ); } /** - * @return \Symfony\Cmf\Component\Routing\ChainRouter|\PHPUnit\Framework\MockObject\MockObject + * @return \Symfony\Component\Routing\RouterInterface&\PHPUnit\Framework\MockObject\MockObject */ - private function getRouterMock() + private function getRouterMock(): RouterInterface { if (!isset($this->router)) { - $this->router = $this->createMock(ChainRouter::class); + $this->router = $this->createMock(RouterInterface::class); $this->router - ->expects($this->any()) ->method('getContext') - ->willReturn(new RequestContext()); + ->willReturn(new RequestContext()) + ; } return $this->router; } + + private function getRouterInvocationMockerForMatchingUri( + string $uri, + ?InvokedCountMatcher $invokedCount = null + ): InvocationMocker { + return $this->getRouterMock() + ->expects($invokedCount ?? self::once()) + ->method('match') + ->with($uri) + ; + } } class_alias(RouterTest::class, 'EzSystems\EzPlatformRestBundle\Tests\RequestParser\RouterTest'); diff --git a/tests/integration/IbexaTestKernel.php b/tests/integration/IbexaTestKernel.php index 91c2f0c1..6705424f 100644 --- a/tests/integration/IbexaTestKernel.php +++ b/tests/integration/IbexaTestKernel.php @@ -10,6 +10,7 @@ use Hautelook\TemplatedUriBundle\HautelookTemplatedUriBundle; use Ibexa\Bundle\Rest\IbexaRestBundle; +use Ibexa\Contracts\Rest\UriParser\UriParserInterface; use Ibexa\Contracts\Test\Core\IbexaTestKernel as CoreIbexaTestKernel; use Ibexa\Rest\Server\Controller\Root as RestRootController; use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface; @@ -41,6 +42,7 @@ protected static function getExposedServicesByClass(): iterable { yield from parent::getExposedServicesByClass(); yield RestRootController::class; + yield UriParserInterface::class; } private static function loadRouting(ContainerBuilder $container): void diff --git a/tests/integration/UriParser/UriParserTest.php b/tests/integration/UriParser/UriParserTest.php new file mode 100644 index 00000000..eb7ba0c2 --- /dev/null +++ b/tests/integration/UriParser/UriParserTest.php @@ -0,0 +1,214 @@ +uriParser = $this->getIbexaTestCore()->getServiceByClassName(UriParserInterface::class); + } + + /** + * @return iterable + */ + public static function getDataForTestGetAttributeFromUri(): iterable + { + yield 'Get sectionId attribute from ibexa.rest.load_section GET route' => [ + 'GET', + '/api/ibexa/v2/content/sections/2', + 'sectionId', + '2', + ]; + + yield 'Get sessionId attribute from ibexa.rest.refresh_session POST route' => [ + 'POST', + '/api/ibexa/v2/user/sessions/MySession/refresh', + 'sessionId', + 'MySession', + ]; + } + + /** + * @dataProvider getDataForTestGetAttributeFromUri + * + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException + */ + public function testGetAttributeFromUri( + string $method, + string $uri, + string $attributeName, + string $expectedValue + ): void { + self::assertSame($expectedValue, $this->uriParser->getAttributeFromUri($uri, $attributeName, $method)); + } + + /** + * @return iterable + */ + public static function getDataForTestGetAttributeFromUriThrowsException(): iterable + { + $uri = '/api/ibexa/v2/user/sessions/MySession/refresh'; + yield 'Invalid attribute' => [ + 'POST', + $uri, + 'session', + "No attribute 'session' in route matched from $uri", + ]; + + yield 'Invalid method' => [ + 'GET', + $uri, + 'sessionId', + "Method 'GET' is not allowed for '$uri'. Allowed: [POST]", + ]; + + yield 'Invalid route' => [ + 'GET', + '/api/ibexa/v2/foo-bar-baz', + 'foo', + 'No routes found for "/api/ibexa/v2/foo-bar-baz/"', + ]; + + yield 'Non-REST route' => [ + 'GET', + '/admin', + 'foo', + // The real exception message got covered by this one due to BC for the original Router-based Request Parser + 'No route matched \'/admin\'', + ]; + } + + /** + * @dataProvider getDataForTestGetAttributeFromUriThrowsException + * + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException + */ + public function testGetAttributeFromUriThrowsException( + string $method, + string $uri, + string $attributeName, + string $expectedExceptionMessage + ): void { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage($expectedExceptionMessage); + $this->uriParser->getAttributeFromUri($uri, $attributeName, $method); + } + + public static function getDataForTestIsRestRequest(): iterable + { + yield ($uri = '/api/ibexa/v2/foo') => [ + Request::create($uri), + true, + ]; + + yield ($uri = '/api/acme/v1.5/bar') => [ + Request::create($uri), + true, + ]; + + yield ($uri = '/baz') => [ + Request::create($uri), + false, + ]; + } + + /** + * @dataProvider getDataForTestIsRestRequest + */ + public function testIsRestRequest(Request $request, bool $isRestRequest): void + { + self::assertSame($isRestRequest, $this->uriParser->isRestRequest($request)); + } + + /** + * @dataProvider getDataForTestIsRestRequest + */ + public function testHasRestPrefix(Request $request, bool $hasRestPrefix): void + { + self::assertSame($hasRestPrefix, $this->uriParser->hasRestPrefix($request->getPathInfo())); + } + + /** + * @return iterable}> + */ + public static function getDataForTestMatchUri(): iterable + { + yield ($uri = '/api/ibexa/v2/content/objectstategroups/123/objectstates/456') => [ + $uri, + 'PATCH', + [ + '_route' => 'ibexa.rest.update_object_state', + '_controller' => 'Ibexa\Rest\Server\Controller\ObjectState:updateObjectState', + 'objectStateGroupId' => '123', + 'objectStateId' => '456', + ], + ]; + } + + /** + * @dataProvider getDataForTestMatchUri + * + * @param array $expectedMatch + * + * @throws \Ibexa\Contracts\Rest\Exceptions\InvalidArgumentException + */ + public function testMatchUri(string $uri, string $method, array $expectedMatch): void + { + $actualMatch = $this->uriParser->matchUri($uri, $method); + foreach ($expectedMatch as $expectedKey => $expectedValue) { + self::assertArrayHasKey($expectedKey, $actualMatch); + self::assertSame($expectedValue, $actualMatch[$expectedKey]); + } + } + + /** + * @return iterable + */ + public static function getInvalidDataForTestMatchUri(): iterable + { + // unknown route + yield ($uri = '/api/ibexa/v2/foo/123') => [ + $uri, + 'GET', + "No routes found for \"$uri/\"", + ]; + + // the route exists only for POST method + yield ($uri = '/user/sessions/MySessionID/refresh') => [ + $uri, + 'GET', + 'No route matched \'/user/sessions/MySessionID/refresh\'', + ]; + } + + /** + * @dataProvider getInvalidDataForTestMatchUri + * + * @throws \Ibexa\Contracts\Rest\Exceptions\InvalidArgumentException + */ + public function testMatchUriThrowsException(string $uri, string $method, string $expectedExceptionMessage): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage($expectedExceptionMessage); + $this->uriParser->matchUri($uri, $method); + } +}