router = new ChainRouter($this->createMock(LoggerInterface::class)); $this->context = $this->createMock(RequestContext::class); } public function testPriority() { $this->assertEquals([], $this->router->all()); list($low, $high) = $this->createRouterMocks(); $this->router->add($low, 10); $this->router->add($high, 100); $this->assertEquals([ $high, $low, ], $this->router->all()); } public function testHasRouters() { $this->assertEquals([], $this->router->all()); $this->assertFalse($this->router->hasRouters()); list($low, $high) = $this->createRouterMocks(); $this->router->add($low, 10); $this->router->add($high, 100); $this->assertTrue($this->router->hasRouters()); } /** * Routers are supposed to be sorted only once. * This test will check that by trying to get all routers several times. * * @covers \Symfony\Cmf\Component\Routing\ChainRouter::sortRouters * @covers \Symfony\Cmf\Component\Routing\ChainRouter::all */ public function testSortRouters() { list($low, $medium, $high) = $this->createRouterMocks(); // We're using a mock here and not $this->router because we need to ensure that the sorting operation is done only once. /** @var $router ChainRouter|MockObject */ $router = $this->getMockBuilder(ChainRouter::class) ->disableOriginalConstructor() ->setMethods(['sortRouters']) ->getMock(); $router ->expects($this->once()) ->method('sortRouters') ->will( $this->returnValue( [$high, $medium, $low] ) ) ; $router->add($low, 10); $router->add($medium, 50); $router->add($high, 100); $expectedSortedRouters = [$high, $medium, $low]; // Let's get all routers 5 times, we should only sort once. for ($i = 0; $i < 5; ++$i) { $this->assertSame($expectedSortedRouters, $router->all()); } } /** * This test ensures that if a router is being added on the fly, the sorting is reset. * * @covers \Symfony\Cmf\Component\Routing\ChainRouter::sortRouters * @covers \Symfony\Cmf\Component\Routing\ChainRouter::all * @covers \Symfony\Cmf\Component\Routing\ChainRouter::add */ public function testReSortRouters() { list($low, $medium, $high) = $this->createRouterMocks(); $highest = clone $high; // We're using a mock here and not $this->router because we need to ensure that the sorting operation is done only once. /** @var $router ChainRouter|MockObject */ $router = $this->getMockBuilder(ChainRouter::class) ->disableOriginalConstructor() ->setMethods(['sortRouters']) ->getMock(); $router ->expects($this->exactly(2)) ->method('sortRouters') ->willReturnOnConsecutiveCalls( [$high, $medium, $low], // The second time sortRouters() is called, we're supposed to get the newly added router ($highest) [$highest, $high, $medium, $low] ) ; $router->add($low, 10); $router->add($medium, 50); $router->add($high, 100); $this->assertSame([$high, $medium, $low], $router->all()); // Now adding another router on the fly, sorting must have been reset $router->add($highest, 101); $this->assertSame([$highest, $high, $medium, $low], $router->all()); } /** * context must be propagated to chained routers and be stored locally. */ public function testContext() { list($low, $high) = $this->createRouterMocks(); $low ->expects($this->once()) ->method('setContext') ->with($this->context) ; $high ->expects($this->once()) ->method('setContext') ->with($this->context) ; $this->router->add($low, 10); $this->router->add($high, 100); $this->router->setContext($this->context); $this->assertSame($this->context, $this->router->getContext()); } /** * context must be propagated also when routers are added after context is set. */ public function testContextOrder() { list($low, $high) = $this->createRouterMocks(); $low ->expects($this->once()) ->method('setContext') ->with($this->context) ; $high ->expects($this->once()) ->method('setContext') ->with($this->context) ; $this->router->setContext($this->context); $this->router->add($low, 10); $this->router->add($high, 100); $this->router->all(); $this->assertSame($this->context, $this->router->getContext()); } /** * The first usable match is used, no further routers are queried once a match is found. */ public function testMatch() { $url = '/test'; list($lower, $low, $high) = $this->createRouterMocks(); $high ->expects($this->once()) ->method('match') ->with($url) ->will($this->throwException(new ResourceNotFoundException())) ; $low ->expects($this->once()) ->method('match') ->with($url) ->will($this->returnValue(['test'])) ; $lower ->expects($this->never()) ->method('match'); $this->router->add($lower, 5); $this->router->add($low, 10); $this->router->add($high, 100); $result = $this->router->match('/test'); $this->assertEquals(['test'], $result); } /** * The first usable match is used, no further routers are queried once a match is found. */ public function testMatchRequest() { $url = '/test'; list($lower, $low, $high) = $this->createRouterMocks(); $highest = $this->createMock(RequestMatcher::class); $request = Request::create('/test'); $highest ->expects($this->once()) ->method('matchRequest') ->will($this->throwException(new ResourceNotFoundException())) ; $high ->expects($this->once()) ->method('match') ->with($url) ->will($this->throwException(new ResourceNotFoundException())) ; $low ->expects($this->once()) ->method('match') ->with($url) ->will($this->returnValue(['test'])) ; $lower ->expects($this->never()) ->method('match') ; $this->router->add($lower, 5); $this->router->add($low, 10); $this->router->add($high, 100); $this->router->add($highest, 200); $result = $this->router->matchRequest($request); $this->assertEquals(['test'], $result); } /** * Call match on ChainRouter that has RequestMatcher in the chain. */ public function testMatchWithRequestMatchers() { $url = '/test'; list($low) = $this->createRouterMocks(); $high = $this->createMock(RequestMatcher::class); $high ->expects($this->once()) ->method('matchRequest') ->with($this->callback(function (Request $r) use ($url) { return $r->getPathInfo() === $url; })) ->will($this->throwException(new ResourceNotFoundException())) ; $low ->expects($this->once()) ->method('match') ->with($url) ->will($this->returnValue(['test'])) ; $this->router->add($low, 10); $this->router->add($high, 20); $result = $this->router->match($url); $this->assertEquals(['test'], $result); } public function provideBaseUrl() { return [ [''], ['/web'], ]; } /** * Call match on ChainRouter that has RequestMatcher in the chain. * * @dataProvider provideBaseUrl */ public function testMatchWithRequestMatchersAndContext($baseUrl) { $url = '//test'; list($low) = $this->createRouterMocks(); $high = $this->createMock(RequestMatcher::class); $high ->expects($this->once()) ->method('matchRequest') ->with($this->callback(function (Request $r) use ($url, $baseUrl) { return true === $r->isSecure() && 'foobar.com' === $r->getHost() && 4433 === $r->getPort() && $baseUrl === $r->getBaseUrl() && $url === $r->getPathInfo() ; })) ->will($this->throwException(new ResourceNotFoundException())) ; $low ->expects($this->once()) ->method('match') ->with($url) ->will($this->returnValue(['test'])) ; $this->router->add($low, 10); $this->router->add($high, 20); $requestContext = new RequestContext(); $requestContext->setScheme('https'); $requestContext->setHost('foobar.com'); $requestContext->setHttpsPort(4433); $requestContext->setBaseUrl($baseUrl); $this->router->setContext($requestContext); $result = $this->router->match($url); $this->assertEquals(['test'], $result); } /** * If there is a method not allowed but another router matches, that one is used. */ public function testMatchAndNotAllowed() { $url = '/test'; list($low, $high) = $this->createRouterMocks(); $high ->expects($this->once()) ->method('match') ->with($url) ->will($this->throwException(new MethodNotAllowedException([]))) ; $low ->expects($this->once()) ->method('match') ->with($url) ->will($this->returnValue( ['test'] )) ; $this->router->add($low, 10); $this->router->add($high, 100); $result = $this->router->match('/test'); $this->assertEquals(['test'], $result); } /** * If there is a method not allowed but another router matches, that one is used. */ public function testMatchRequestAndNotAllowed() { $url = '/test'; list($low, $high) = $this->createRouterMocks(); $high ->expects($this->once()) ->method('match') ->with($url) ->will($this->throwException(new MethodNotAllowedException([]))) ; $low ->expects($this->once()) ->method('match') ->with($url) ->will($this->returnValue(['test'])) ; $this->router->add($low, 10); $this->router->add($high, 100); $result = $this->router->matchRequest(Request::create('/test')); $this->assertEquals(['test'], $result); } public function testMatchNotFound() { $url = '/test'; list($low, $high) = $this->createRouterMocks(); $high ->expects($this->once()) ->method('match') ->with($url) ->will($this->throwException(new ResourceNotFoundException())) ; $low ->expects($this->once()) ->method('match') ->with($url) ->will($this->throwException(new ResourceNotFoundException())) ; $this->router->add($low, 10); $this->router->add($high, 100); $this->expectException(ResourceNotFoundException::class); $this->router->match('/test'); } public function testMatchRequestNotFound() { $url = '/test'; list($low, $high) = $this->createRouterMocks(); $high ->expects($this->once()) ->method('match') ->with($url) ->will($this->throwException(new ResourceNotFoundException())) ; $low ->expects($this->once()) ->method('match') ->with($url) ->will($this->throwException(new ResourceNotFoundException())) ; $this->router->add($low, 10); $this->router->add($high, 100); $this->expectException(ResourceNotFoundException::class); $this->router->matchRequest(Request::create('/test')); } /** * Call match on ChainRouter that has RequestMatcher in the chain. */ public function testMatchWithRequestMatchersNotFound() { $url = '/test'; $expected = Request::create('/test'); $expected->server->remove('REQUEST_TIME_FLOAT'); $high = $this->createMock(RequestMatcher::class); $high ->expects($this->once()) ->method('matchRequest') ->with($this->callback(function (Request $actual) use ($expected): bool { $actual->server->remove('REQUEST_TIME_FLOAT'); return $actual == $expected; })) ->will($this->throwException(new ResourceNotFoundException())) ; $this->router->add($high, 20); $this->expectException(ResourceNotFoundException::class); $this->expectExceptionMessage('None of the routers in the chain matched url \'/test\''); $this->router->match($url); } /** * If any of the routers throws a not allowed exception and no other matches, we need to see this. */ public function testMatchMethodNotAllowed() { $url = '/test'; list($low, $high) = $this->createRouterMocks(); $high ->expects($this->once()) ->method('match') ->with($url) ->will($this->throwException(new MethodNotAllowedException([]))) ; $low ->expects($this->once()) ->method('match') ->with($url) ->will($this->throwException(new ResourceNotFoundException())) ; $this->router->add($low, 10); $this->router->add($high, 100); $this->expectException(MethodNotAllowedException::class); $this->router->match('/test'); } /** * If any of the routers throws a not allowed exception and no other matches, we need to see this. */ public function testMatchRequestMethodNotAllowed() { $url = '/test'; list($low, $high) = $this->createRouterMocks(); $high ->expects($this->once()) ->method('match') ->with($url) ->will($this->throwException(new MethodNotAllowedException([]))) ; $low ->expects($this->once()) ->method('match') ->with($url) ->will($this->throwException(new ResourceNotFoundException())) ; $this->router->add($low, 10); $this->router->add($high, 100); $this->expectException(MethodNotAllowedException::class); $this->router->matchRequest(Request::create('/test')); } public function testGenerate() { $url = '/test'; $name = 'test'; $parameters = ['test' => 'value']; list($lower, $low, $high) = $this->createRouterMocks(); $high ->expects($this->once()) ->method('generate') ->with($name, $parameters, UrlGeneratorInterface::ABSOLUTE_PATH) ->will($this->throwException(new RouteNotFoundException())) ; $low ->expects($this->once()) ->method('generate') ->with($name, $parameters, UrlGeneratorInterface::ABSOLUTE_PATH) ->will($this->returnValue($url)) ; $lower ->expects($this->never()) ->method('generate') ; $this->router->add($low, 10); $this->router->add($high, 100); $result = $this->router->generate($name, $parameters); $this->assertEquals($url, $result); } public function testGenerateNotFound() { $name = 'test'; $parameters = ['test' => 'value']; list($low, $high) = $this->createRouterMocks(); $high ->expects($this->once()) ->method('generate') ->with($name, $parameters, UrlGeneratorInterface::ABSOLUTE_PATH) ->will($this->throwException(new RouteNotFoundException())) ; $low->expects($this->once()) ->method('generate') ->with($name, $parameters, UrlGeneratorInterface::ABSOLUTE_PATH) ->will($this->throwException(new RouteNotFoundException())) ; $this->router->add($low, 10); $this->router->add($high, 100); $this->expectException(RouteNotFoundException::class); $this->router->generate($name, $parameters); } /** * Route is an object but no versatile generator around to do the debug message. * * @group legacy * @expectedDeprecation Passing an object as route name is deprecated since version 2.3. Pass the `RouteObjectInterface::OBJECT_BASED_ROUTE_NAME` as route name and the object in the parameters with key `RouteObjectInterface::ROUTE_OBJECT`. */ public function testGenerateObjectNotFound() { if (!class_exists(ObjectRouteLoader::class)) { $this->markTestSkipped('Symfony 5 would throw a TypeError.'); } $name = new \stdClass(); $parameters = ['test' => 'value']; $defaultRouter = $this->createMock(RouterInterface::class); $defaultRouter ->expects($this->never()) ->method('generate') ; $this->router->add($defaultRouter, 200); $this->expectException(RouteNotFoundException::class); $this->router->generate($name, $parameters); } /** * A versatile router will generate the debug message. * * @group legacy * @expectedDeprecation Passing an object as route name is deprecated since version 2.3. Pass the `RouteObjectInterface::OBJECT_BASED_ROUTE_NAME` as route name and the object in the parameters with key `RouteObjectInterface::ROUTE_OBJECT`. */ public function testGenerateObjectNotFoundVersatile() { if (!class_exists(ObjectRouteLoader::class)) { $this->markTestSkipped('Symfony 5 would throw a TypeError.'); } $name = new \stdClass(); $parameters = ['test' => 'value']; $chainedRouter = $this->createMock(VersatileRouter::class); $chainedRouter ->expects($this->once()) ->method('supports') ->will($this->returnValue(true)) ; $chainedRouter->expects($this->once()) ->method('generate') ->with($name, $parameters, UrlGeneratorInterface::ABSOLUTE_PATH) ->will($this->throwException(new RouteNotFoundException())) ; $chainedRouter->expects($this->once()) ->method('getRouteDebugMessage') ->with($name, $parameters) ->will($this->returnValue('message')) ; $this->router->add($chainedRouter, 10); $this->expectException(RouteNotFoundException::class); $this->router->generate($name, $parameters); } /** * @group legacy * @expectedDeprecation Passing an object as route name is deprecated since version 2.3. Pass the `RouteObjectInterface::OBJECT_BASED_ROUTE_NAME` as route name and the object in the parameters with key `RouteObjectInterface::ROUTE_OBJECT`. */ public function testGenerateObjectName() { if (!class_exists(ObjectRouteLoader::class)) { $this->markTestSkipped('Symfony 5 would throw a TypeError.'); } $name = new \stdClass(); $parameters = ['test' => 'value']; $defaultRouter = $this->createMock(RouterInterface::class); $chainedRouter = $this->createMock(VersatileRouter::class); $defaultRouter ->expects($this->never()) ->method('generate') ; $chainedRouter ->expects($this->once()) ->method('supports') ->will($this->returnValue(true)) ; $chainedRouter ->expects($this->once()) ->method('generate') ->with($name, $parameters, UrlGeneratorInterface::ABSOLUTE_PATH) ->will($this->returnValue($name)) ; $this->router->add($defaultRouter, 200); $this->router->add($chainedRouter, 100); $result = $this->router->generate($name, $parameters); $this->assertEquals($name, $result); } /** * This test currently triggers a deprecation notice because of ChainRouter BC. */ public function testGenerateWithObjectNameInParametersNotFoundVersatile() { $name = RouteObjectInterface::OBJECT_BASED_ROUTE_NAME; $parameters = ['test' => 'value', '_route_object' => new \stdClass()]; $chainedRouter = $this->createMock(VersatileRouter::class); $chainedRouter ->expects($this->once()) ->method('supports') ->willReturn(true) ; $chainedRouter->expects($this->once()) ->method('generate') ->with($name, $parameters, UrlGeneratorInterface::ABSOLUTE_PATH) ->will($this->throwException(new RouteNotFoundException())) ; $chainedRouter->expects($this->once()) ->method('getRouteDebugMessage') ->with($name, $parameters) ->willReturn('message') ; $this->router->add($chainedRouter, 10); $this->expectException(RouteNotFoundException::class); $this->router->generate($name, $parameters); } public function testGenerateWithObjectNameInParameters() { $name = RouteObjectInterface::OBJECT_BASED_ROUTE_NAME; $parameters = ['test' => 'value', '_route_object' => new \stdClass()]; $defaultRouter = $this->createMock(RouterInterface::class); $defaultRouter ->expects($this->once()) ->method('generate') ->with($name, $parameters, UrlGeneratorInterface::ABSOLUTE_PATH) ->willReturn('/foo/bar') ; $this->router->add($defaultRouter, 200); $result = $this->router->generate($name, $parameters); $this->assertEquals('/foo/bar', $result); } public function testWarmup() { $dir = 'test_dir'; list($low) = $this->createRouterMocks(); $high = $this->createMock(WarmableRouterMock::class); $high ->expects($this->once()) ->method('warmUp') ->with($dir) ; $this->router->add($low, 10); $this->router->add($high, 100); $this->router->warmUp($dir); } public function testRouteCollection() { list($low, $high) = $this->createRouterMocks(); $lowcol = new RouteCollection(); $lowcol->add('low', $this->createMock(Route::class)); $highcol = new RouteCollection(); $highcol->add('high', $this->createMock(Route::class)); $low ->expects($this->once()) ->method('getRouteCollection') ->will($this->returnValue($lowcol)) ; $high ->expects($this->once()) ->method('getRouteCollection') ->will($this->returnValue($highcol)) ; $this->router->add($low, 10); $this->router->add($high, 100); $collection = $this->router->getRouteCollection(); $this->assertInstanceOf(RouteCollection::class, $collection); $names = []; foreach ($collection->all() as $name => $route) { $this->assertInstanceOf(Route::class, $route); $names[] = $name; } $this->assertEquals(['high', 'low'], $names); } /** * @group legacy */ public function testSupport() { $router = $this->createMock(VersatileRouter::class); $router ->expects($this->once()) ->method('supports') ->will($this->returnValue(false)) ; $router ->expects($this->never()) ->method('generate') ->will($this->returnValue(false)) ; $this->router->add($router); $this->expectException(RouteNotFoundException::class); $this->router->generate('foobar'); } /** * @return RouterInterface[]|MockObject[] */ protected function createRouterMocks() { return [ $this->createMock(RouterInterface::class), $this->createMock(RouterInterface::class), $this->createMock(RouterInterface::class), ]; } } abstract class WarmableRouterMock implements RouterInterface, WarmableInterface { } abstract class RequestMatcher implements RouterInterface, RequestMatcherInterface { } abstract class VersatileRouter implements VersatileGeneratorInterface, RequestMatcherInterface { }