3 namespace Drupal\Tests\Core\Routing;
5 use Drupal\Core\Cache\Cache;
6 use Drupal\Core\DependencyInjection\ContainerBuilder;
7 use Drupal\Core\PathProcessor\OutboundPathProcessorInterface;
8 use Drupal\Core\PathProcessor\PathProcessorAlias;
9 use Drupal\Core\PathProcessor\PathProcessorManager;
10 use Drupal\Core\Render\BubbleableMetadata;
11 use Drupal\Core\Routing\RequestContext;
12 use Drupal\Core\Routing\RouteProviderInterface;
13 use Drupal\Core\Routing\UrlGenerator;
14 use Drupal\Tests\UnitTestCase;
15 use Prophecy\Argument;
16 use Symfony\Component\HttpFoundation\Request;
17 use Symfony\Component\HttpFoundation\RequestStack;
18 use Symfony\Component\Routing\Route;
19 use Symfony\Component\Routing\RouteCollection;
22 * Confirm that the UrlGenerator is functioning properly.
24 * @coversDefaultClass \Drupal\Core\Routing\UrlGenerator
27 class UrlGeneratorTest extends UnitTestCase {
32 * @var \Drupal\Core\Routing\RouteProviderInterface
37 * The url generator to test.
39 * @var \Drupal\Core\Routing\UrlGenerator
46 * @var \Drupal\Core\Path\AliasManager|\PHPUnit_Framework_MockObject_MockObject
48 protected $aliasManager;
51 * The mock route processor manager.
53 * @var \Drupal\Core\RouteProcessor\RouteProcessorManager|\PHPUnit_Framework_MockObject_MockObject
55 protected $routeProcessorManager;
60 * @var \Symfony\Component\HttpFoundation\RequestStack
62 protected $requestStack;
65 * The request context.
67 * @var \Drupal\Core\Routing\RequestContext
74 * @var \Drupal\Core\PathProcessor\PathProcessorManager
76 protected $processorManager;
81 protected function setUp() {
82 $cache_contexts_manager = $this->getMockBuilder('Drupal\Core\Cache\Context\CacheContextsManager')
83 ->disableOriginalConstructor()
85 $cache_contexts_manager->method('assertValidTokens')->willReturn(TRUE);
86 $container = new ContainerBuilder();
87 $container->set('cache_contexts_manager', $cache_contexts_manager);
88 \Drupal::setContainer($container);
90 $routes = new RouteCollection();
91 $first_route = new Route('/test/one');
92 $second_route = new Route('/test/two/{narf}');
93 $third_route = new Route('/test/two/');
94 $fourth_route = new Route('/test/four', [], [], [], '', ['https']);
95 $none_route = new Route('', [], [], ['_no_path' => TRUE]);
97 $routes->add('test_1', $first_route);
98 $routes->add('test_2', $second_route);
99 $routes->add('test_3', $third_route);
100 $routes->add('test_4', $fourth_route);
101 $routes->add('<none>', $none_route);
103 // Create a route provider stub.
104 $provider = $this->getMockBuilder('Drupal\Core\Routing\RouteProvider')
105 ->disableOriginalConstructor()
107 // We need to set up return value maps for both the getRouteByName() and the
108 // getRoutesByNames() method calls on the route provider. The parameters
109 // are not passed in and default to an empty array.
110 $route_name_return_map = $routes_names_return_map = [];
111 $return_map_values = [
113 'route_name' => 'test_1',
114 'return' => $first_route,
117 'route_name' => 'test_2',
118 'return' => $second_route,
121 'route_name' => 'test_3',
122 'return' => $third_route,
125 'route_name' => 'test_4',
126 'return' => $fourth_route,
129 'route_name' => '<none>',
130 'return' => $none_route,
133 foreach ($return_map_values as $values) {
134 $route_name_return_map[] = [$values['route_name'], $values['return']];
135 $routes_names_return_map[] = [[$values['route_name']], $values['return']];
137 $this->provider = $provider;
138 $this->provider->expects($this->any())
139 ->method('getRouteByName')
140 ->will($this->returnValueMap($route_name_return_map));
141 $provider->expects($this->any())
142 ->method('getRoutesByNames')
143 ->will($this->returnValueMap($routes_names_return_map));
145 // Create an alias manager stub.
146 $alias_manager = $this->getMockBuilder('Drupal\Core\Path\AliasManager')
147 ->disableOriginalConstructor()
150 $alias_manager->expects($this->any())
151 ->method('getAliasByPath')
152 ->will($this->returnCallback([$this, 'aliasManagerCallback']));
154 $this->aliasManager = $alias_manager;
156 $this->requestStack = new RequestStack();
157 $request = Request::create('/some/path');
158 $this->requestStack->push($request);
160 $this->context = new RequestContext();
161 $this->context->fromRequestStack($this->requestStack);
163 $processor = new PathProcessorAlias($this->aliasManager);
164 $processor_manager = new PathProcessorManager();
165 $processor_manager->addOutbound($processor, 1000);
166 $this->processorManager = $processor_manager;
168 $this->routeProcessorManager = $this->getMockBuilder('Drupal\Core\RouteProcessor\RouteProcessorManager')
169 ->disableOriginalConstructor()
172 $generator = new UrlGenerator($this->provider, $processor_manager, $this->routeProcessorManager, $this->requestStack, ['http', 'https']);
173 $generator->setContext($this->context);
174 $this->generator = $generator;
178 * Return value callback for the getAliasByPath() method on the mock alias
181 * Ensures that by default the call to getAliasByPath() will return the first
182 * argument that was passed in. We special-case the paths for which we wish it
183 * to return an actual alias.
187 public function aliasManagerCallback() {
188 $args = func_get_args();
191 return '/hello/world';
193 return '/goodbye/cruel/world';
202 * Confirms that generated routes will have aliased paths.
204 public function testAliasGeneration() {
205 $url = $this->generator->generate('test_1');
206 $this->assertEquals('/hello/world', $url);
207 // No cacheability to test; UrlGenerator::generate() doesn't support
208 // collecting cacheability metadata.
210 $this->routeProcessorManager->expects($this->exactly(3))
211 ->method('processOutbound')
212 ->with($this->anything());
214 // Check that the two generate methods return the same result.
215 $this->assertGenerateFromRoute('test_1', [], [], $url, (new BubbleableMetadata())->setCacheMaxAge(Cache::PERMANENT));
217 $path = $this->generator->getPathFromRoute('test_1');
218 $this->assertEquals('test/one', $path);
222 * Confirms that generated routes will have aliased paths using interface constants.
224 public function testAliasGenerationUsingInterfaceConstants() {
225 $url = $this->generator->generate('test_1', [], UrlGenerator::ABSOLUTE_PATH);
226 $this->assertEquals('/hello/world', $url);
227 // No cacheability to test; UrlGenerator::generate() doesn't support
228 // collecting cacheability metadata.
230 $this->routeProcessorManager->expects($this->exactly(3))
231 ->method('processOutbound')
232 ->with($this->anything());
234 // Check that the two generate methods return the same result.
235 $this->assertGenerateFromRoute('test_1', [], [], $url, (new BubbleableMetadata())->setCacheMaxAge(Cache::PERMANENT));
237 $path = $this->generator->getPathFromRoute('test_1');
238 $this->assertEquals('test/one', $path);
242 * @covers ::generateFromRoute
244 public function testUrlGenerationWithDisabledPathProcessing() {
245 $path_processor = $this->prophesize(OutboundPathProcessorInterface::class);
246 $path_processor->processOutbound(Argument::cetera())->shouldNotBeCalled();
248 $generator = new UrlGenerator($this->provider, $path_processor->reveal(), $this->routeProcessorManager, $this->requestStack, ['http', 'https']);
249 $generator->setContext($this->context);
251 $url = $this->generator->generateFromRoute('test_1', [], ['path_processing' => FALSE]);
252 $this->assertEquals('/test/one', $url);
256 * @covers ::generateFromRoute
258 public function testUrlGenerationWithDisabledPathProcessingByRoute() {
259 $path_processor = $this->prophesize(OutboundPathProcessorInterface::class);
260 $path_processor->processOutbound(Argument::cetera())->shouldNotBeCalled();
262 $provider = $this->prophesize(RouteProviderInterface::class);
263 $provider->getRouteByName('test_1')->willReturn(new Route('/test/one', [], [], ['default_url_options' => ['path_processing' => FALSE]]));
265 $generator = new UrlGenerator($provider->reveal(), $path_processor->reveal(), $this->routeProcessorManager, $this->requestStack, ['http', 'https']);
266 $generator->setContext($this->context);
268 $url = $generator->generateFromRoute('test_1', []);
269 $this->assertEquals('/test/one', $url);
273 * @covers ::generateFromRoute
275 public function testUrlGenerationWithDisabledPathProcessingByRouteAndOptedInPathProcessing() {
276 $path_processor = $this->prophesize(OutboundPathProcessorInterface::class);
277 $path_processor->processOutbound('/test/one', Argument::cetera())->willReturn('/hello/world')->shouldBeCalled();
279 $provider = $this->prophesize(RouteProviderInterface::class);
280 $provider->getRouteByName('test_1')->willReturn(new Route('/test/one', [], [], ['default_url_options' => ['path_processing' => FALSE]]));
282 $generator = new UrlGenerator($provider->reveal(), $path_processor->reveal(), $this->routeProcessorManager, $this->requestStack, ['http', 'https']);
283 $generator->setContext($this->context);
285 $url = $generator->generateFromRoute('test_1', [], ['path_processing' => TRUE]);
286 $this->assertEquals('/hello/world', $url);
290 * Tests URL generation in a subdirectory.
292 public function testGetPathFromRouteWithSubdirectory() {
293 $this->routeProcessorManager->expects($this->once())
294 ->method('processOutbound');
296 $path = $this->generator->getPathFromRoute('test_1');
297 $this->assertEquals('test/one', $path);
301 * Confirms that generated routes will have aliased paths.
303 public function testAliasGenerationWithParameters() {
304 $url = $this->generator->generate('test_2', ['narf' => '5']);
305 $this->assertEquals('/goodbye/cruel/world', $url);
306 // No cacheability to test; UrlGenerator::generate() doesn't support
307 // collecting cacheability metadata.
309 $this->routeProcessorManager->expects($this->any())
310 ->method('processOutbound')
311 ->with($this->anything());
313 $options = ['fragment' => 'top'];
314 // Extra parameters should appear in the query string.
315 $this->assertGenerateFromRoute('test_1', ['zoo' => 5], $options, '/hello/world?zoo=5#top', (new BubbleableMetadata())->setCacheMaxAge(Cache::PERMANENT));
317 $options = ['query' => ['page' => '1'], 'fragment' => 'bottom'];
318 $this->assertGenerateFromRoute('test_2', ['narf' => 5], $options, '/goodbye/cruel/world?page=1#bottom', (new BubbleableMetadata())->setCacheMaxAge(Cache::PERMANENT));
320 // Changing the parameters, the route still matches but there is no alias.
321 $this->assertGenerateFromRoute('test_2', ['narf' => 7], $options, '/test/two/7?page=1#bottom', (new BubbleableMetadata())->setCacheMaxAge(Cache::PERMANENT));
323 $path = $this->generator->getPathFromRoute('test_2', ['narf' => '5']);
324 $this->assertEquals('test/two/5', $path);
326 // Specify a query parameter with NULL.
327 $options = ['query' => ['page' => NULL], 'fragment' => 'bottom'];
328 $this->assertGenerateFromRoute('test_2', ['narf' => 5], $options, '/goodbye/cruel/world?page#bottom', (new BubbleableMetadata())->setCacheMaxAge(Cache::PERMANENT));
332 * Confirms that generated routes will have aliased paths with options.
334 * @dataProvider providerTestAliasGenerationWithOptions
336 public function testAliasGenerationWithOptions($route_name, $route_parameters, $options, $expected) {
337 $this->assertGenerateFromRoute($route_name, $route_parameters, $options, $expected, (new BubbleableMetadata())->setCacheMaxAge(Cache::PERMANENT));
341 * Provides test data for testAliasGenerationWithOptions.
343 public function providerTestAliasGenerationWithOptions() {
345 // Extra parameters should appear in the query string.
349 ['fragment' => 'top'],
350 '/hello/world?zoo=5#top',
355 ['query' => ['page' => '1'], 'fragment' => 'bottom'],
356 '/goodbye/cruel/world?page=1#bottom',
358 // Changing the parameters, the route still matches but there is no alias.
362 ['query' => ['page' => '1'], 'fragment' => 'bottom'],
363 '/test/two/7?page=1#bottom',
365 // Query string values containing '/' should be decoded.
369 ['query' => ['page' => '1/2'], 'fragment' => 'bottom'],
370 '/test/two/7?page=1/2#bottom',
372 // A NULL query string.
373 $data['query-with-NULL'] = [
376 ['query' => NULL, 'fragment' => 'bottom'],
377 '/test/two/7#bottom',
383 * Tests URL generation from route with trailing start and end slashes.
385 public function testGetPathFromRouteTrailing() {
386 $this->routeProcessorManager->expects($this->once())
387 ->method('processOutbound');
389 $path = $this->generator->getPathFromRoute('test_3');
390 $this->assertEquals($path, 'test/two');
394 * Confirms that absolute URLs work with generated routes.
396 public function testAbsoluteURLGeneration() {
397 $url = $this->generator->generate('test_1', [], TRUE);
398 $this->assertEquals('http://localhost/hello/world', $url);
399 // No cacheability to test; UrlGenerator::generate() doesn't support
400 // collecting cacheability metadata.
402 $this->routeProcessorManager->expects($this->exactly(2))
403 ->method('processOutbound')
404 ->with($this->anything());
406 $options = ['absolute' => TRUE, 'fragment' => 'top'];
407 // Extra parameters should appear in the query string.
408 $this->assertGenerateFromRoute('test_1', ['zoo' => 5], $options, 'http://localhost/hello/world?zoo=5#top', (new BubbleableMetadata())->setCacheMaxAge(Cache::PERMANENT)->setCacheContexts(['url.site']));
412 * Confirms that absolute URLs work with generated routes using interface constants.
414 public function testAbsoluteURLGenerationUsingInterfaceConstants() {
415 $url = $this->generator->generate('test_1', [], UrlGenerator::ABSOLUTE_URL);
416 $this->assertEquals('http://localhost/hello/world', $url);
417 // No cacheability to test; UrlGenerator::generate() doesn't support
418 // collecting cacheability metadata.
420 $this->routeProcessorManager->expects($this->exactly(2))
421 ->method('processOutbound')
422 ->with($this->anything());
424 $options = ['absolute' => TRUE, 'fragment' => 'top'];
425 // Extra parameters should appear in the query string.
426 $this->assertGenerateFromRoute('test_1', ['zoo' => 5], $options, 'http://localhost/hello/world?zoo=5#top', (new BubbleableMetadata())->setCacheMaxAge(Cache::PERMANENT)->setCacheContexts(['url.site']));
430 * Confirms that explicitly setting the base_url works with generated routes
432 public function testBaseURLGeneration() {
433 $options = ['base_url' => 'http://www.example.com:8888'];
434 $this->assertGenerateFromRoute('test_1', [], $options, 'http://www.example.com:8888/hello/world', (new BubbleableMetadata())->setCacheMaxAge(Cache::PERMANENT));
436 $options = ['base_url' => 'http://www.example.com:8888', 'https' => TRUE];
437 $this->assertGenerateFromRoute('test_1', [], $options, 'https://www.example.com:8888/hello/world', (new BubbleableMetadata())->setCacheMaxAge(Cache::PERMANENT));
439 $options = ['base_url' => 'https://www.example.com:8888', 'https' => FALSE];
440 $this->assertGenerateFromRoute('test_1', [], $options, 'http://www.example.com:8888/hello/world', (new BubbleableMetadata())->setCacheMaxAge(Cache::PERMANENT));
442 $this->routeProcessorManager->expects($this->exactly(2))
443 ->method('processOutbound')
444 ->with($this->anything());
446 $options = ['base_url' => 'http://www.example.com:8888', 'fragment' => 'top'];
447 // Extra parameters should appear in the query string.
448 $this->assertGenerateFromRoute('test_1', ['zoo' => 5], $options, 'http://www.example.com:8888/hello/world?zoo=5#top', (new BubbleableMetadata())->setCacheMaxAge(Cache::PERMANENT));
452 * Test that the 'scheme' route requirement is respected during url generation.
454 public function testUrlGenerationWithHttpsRequirement() {
455 $url = $this->generator->generate('test_4', [], TRUE);
456 $this->assertEquals('https://localhost/test/four', $url);
457 // No cacheability to test; UrlGenerator::generate() doesn't support
458 // collecting cacheability metadata.
460 $this->routeProcessorManager->expects($this->exactly(2))
461 ->method('processOutbound')
462 ->with($this->anything());
464 $options = ['absolute' => TRUE, 'https' => TRUE];
465 $this->assertGenerateFromRoute('test_1', [], $options, 'https://localhost/hello/world', (new BubbleableMetadata())->setCacheMaxAge(Cache::PERMANENT)->setCacheContexts(['url.site']));
469 * Tests generating a relative URL with no path.
471 * @param array $options
472 * An array of URL options.
473 * @param string $expected_url
474 * The expected relative URL.
476 * @covers ::generateFromRoute
478 * @dataProvider providerTestNoPath
480 public function testNoPath($options, $expected_url) {
481 $url = $this->generator->generateFromRoute('<none>', [], $options);
482 $this->assertEquals($expected_url, $url);
486 * Data provider for ::testNoPath().
488 public function providerTestNoPath() {
492 // Query parameters only.
493 [['query' => ['foo' => 'bar']], '?foo=bar'],
494 // Multiple query parameters.
495 [['query' => ['foo' => 'bar', 'baz' => '']], '?foo=bar&baz='],
497 [['fragment' => 'foo'], '#foo'],
498 // Query parameters and fragment.
499 [['query' => ['bar' => 'baz'], 'fragment' => 'foo'], '?bar=baz#foo'],
500 // Multiple query parameters and fragment.
501 [['query' => ['bar' => 'baz', 'foo' => 'bar'], 'fragment' => 'foo'], '?bar=baz&foo=bar#foo'],
506 * @covers \Drupal\Core\Routing\UrlGenerator::generateFromRoute
508 * Note: We use absolute covers to let
509 * \Drupal\Tests\Core\Render\MetadataBubblingUrlGeneratorTest work.
511 public function testGenerateWithPathProcessorChangingQueryParameter() {
512 $path_processor = $this->getMock(OutboundPathProcessorInterface::CLASS);
513 $path_processor->expects($this->atLeastOnce())
514 ->method('processOutbound')
515 ->willReturnCallback(function ($path, &$options = [], Request $request = NULL, BubbleableMetadata $bubbleable_metadata = NULL) {
516 $options['query'] = ['zoo' => 5];
519 $this->processorManager->addOutbound($path_processor);
522 $this->assertGenerateFromRoute('test_2', ['narf' => 5], $options, '/goodbye/cruel/world?zoo=5', (new BubbleableMetadata())->setCacheMaxAge(Cache::PERMANENT));
526 * Asserts \Drupal\Core\Routing\UrlGenerator::generateFromRoute()'s output.
529 * The route name to test.
530 * @param array $route_parameters
531 * The route parameters to test.
532 * @param array $options
533 * The options to test.
534 * @param $expected_url
535 * The expected generated URL string.
536 * @param \Drupal\Core\Render\BubbleableMetadata $expected_bubbleable_metadata
537 * The expected generated bubbleable metadata.
539 protected function assertGenerateFromRoute($route_name, array $route_parameters, array $options, $expected_url, BubbleableMetadata $expected_bubbleable_metadata) {
540 // First, test with $collect_cacheability_metadata set to the default value.
541 $url = $this->generator->generateFromRoute($route_name, $route_parameters, $options);
542 $this->assertSame($expected_url, $url);
544 // Second, test with it set to TRUE.
545 $generated_url = $this->generator->generateFromRoute($route_name, $route_parameters, $options, TRUE);
546 $this->assertSame($expected_url, $generated_url->getGeneratedUrl());
547 $this->assertEquals($expected_bubbleable_metadata, BubbleableMetadata::createFromObject($generated_url));