UrlGeneratorTest.php 17.3 KB
Newer Older
1
2
3
4
<?php

/**
 * @file
5
 * Contains \Drupal\Tests\Core\Routing\UrlGeneratorTest.
6
7
8
9
 */

namespace Drupal\Tests\Core\Routing;

10
11
use Drupal\Core\Cache\Cache;
use Drupal\Core\DependencyInjection\ContainerBuilder;
12
use Drupal\Core\PathProcessor\OutboundPathProcessorInterface;
13
14
use Drupal\Core\PathProcessor\PathProcessorAlias;
use Drupal\Core\PathProcessor\PathProcessorManager;
15
use Drupal\Core\Render\BubbleableMetadata;
16
use Drupal\Core\Routing\RequestContext;
17
use Drupal\Core\Routing\RouteProviderInterface;
18
19
use Drupal\Core\Routing\UrlGenerator;
use Drupal\Tests\UnitTestCase;
20
use Prophecy\Argument;
21
use Symfony\Component\HttpFoundation\Request;
22
use Symfony\Component\HttpFoundation\RequestStack;
23
24
25
26
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;

/**
27
 * Confirm that the UrlGenerator is functioning properly.
28
 *
29
 * @coversDefaultClass \Drupal\Core\Routing\UrlGenerator
30
31
32
33
 * @group Routing
 */
class UrlGeneratorTest extends UnitTestCase {

34
35
36
37
38
39
40
  /**
   * The route provider.
   *
   * @var \Drupal\Core\Routing\RouteProviderInterface
   */
  protected $provider;

41
42
43
44
45
  /**
   * The url generator to test.
   *
   * @var \Drupal\Core\Routing\UrlGenerator
   */
46
47
  protected $generator;

48
49
50
51
52
  /**
   * The alias manager.
   *
   * @var \Drupal\Core\Path\AliasManager|\PHPUnit_Framework_MockObject_MockObject
   */
53
54
  protected $aliasManager;

55
56
57
58
59
60
61
  /**
   * The mock route processor manager.
   *
   * @var \Drupal\Core\RouteProcessor\RouteProcessorManager|\PHPUnit_Framework_MockObject_MockObject
   */
  protected $routeProcessorManager;

62
63
64
65
66
67
68
  /**
   * The request stack.
   *
   * @var \Symfony\Component\HttpFoundation\RequestStack
   */
  protected $requestStack;

69
70
71
72
73
74
75
  /**
   * The request context.
   *
   * @var \Drupal\Core\Routing\RequestContext
   */
  protected $context;

76
77
78
  /**
   * {@inheritdoc}
   */
79
  protected function setUp() {
80
    $cache_contexts_manager = $this->getMockBuilder('Drupal\Core\Cache\Context\CacheContextsManager')
81
82
      ->disableOriginalConstructor()
      ->getMock();
83
    $cache_contexts_manager->method('assertValidTokens')->willReturn(TRUE);
84
85
86
    $container = new ContainerBuilder();
    $container->set('cache_contexts_manager', $cache_contexts_manager);
    \Drupal::setContainer($container);
87
88
89
90

    $routes = new RouteCollection();
    $first_route = new Route('/test/one');
    $second_route = new Route('/test/two/{narf}');
91
    $third_route = new Route('/test/two/');
92
93
94
    $fourth_route = new Route('/test/four', [], [], [], '', ['https']);
    $none_route = new Route('', [], [], ['_no_path' => TRUE]);

95
96
    $routes->add('test_1', $first_route);
    $routes->add('test_2', $second_route);
97
98
    $routes->add('test_3', $third_route);
    $routes->add('test_4', $fourth_route);
99
    $routes->add('<none>', $none_route);
100
101
102
103
104

    // Create a route provider stub.
    $provider = $this->getMockBuilder('Drupal\Core\Routing\RouteProvider')
      ->disableOriginalConstructor()
      ->getMock();
105
106
    // We need to set up return value maps for both the getRouteByName() and the
    // getRoutesByNames() method calls on the route provider. The parameters
107
    // are not passed in and default to an empty array.
108
109
    $route_name_return_map = $routes_names_return_map = array();
    $return_map_values = array(
110
      [
111
112
        'route_name' => 'test_1',
        'return' => $first_route,
113
114
      ],
      [
115
116
        'route_name' => 'test_2',
        'return' => $second_route,
117
118
      ],
      [
119
120
        'route_name' => 'test_3',
        'return' => $third_route,
121
122
      ],
      [
123
124
        'route_name' => 'test_4',
        'return' => $fourth_route,
125
126
127
128
129
      ],
      [
        'route_name' => '<none>',
        'return' => $none_route,
      ],
130
    );
131
    foreach ($return_map_values as $values) {
132
133
      $route_name_return_map[] = array($values['route_name'], $values['return']);
      $routes_names_return_map[] = array(array($values['route_name']), $values['return']);
134
    }
135
136
    $this->provider = $provider;
    $this->provider->expects($this->any())
137
138
139
140
141
142
143
144
145
146
      ->method('getRouteByName')
      ->will($this->returnValueMap($route_name_return_map));
    $provider->expects($this->any())
      ->method('getRoutesByNames')
      ->will($this->returnValueMap($routes_names_return_map));

    // Create an alias manager stub.
    $alias_manager = $this->getMockBuilder('Drupal\Core\Path\AliasManager')
      ->disableOriginalConstructor()
      ->getMock();
147

148
    $alias_manager->expects($this->any())
149
      ->method('getAliasByPath')
150
      ->will($this->returnCallback(array($this, 'aliasManagerCallback')));
151
152
153

    $this->aliasManager = $alias_manager;

154
155
156
157
    $this->requestStack = new RequestStack();
    $request = Request::create('/some/path');
    $this->requestStack->push($request);

158
159
    $this->context = new RequestContext();
    $this->context->fromRequestStack($this->requestStack);
160
161
162
163
164

    $processor = new PathProcessorAlias($this->aliasManager);
    $processor_manager = new PathProcessorManager();
    $processor_manager->addOutbound($processor, 1000);

165
166
167
168
    $this->routeProcessorManager = $this->getMockBuilder('Drupal\Core\RouteProcessor\RouteProcessorManager')
      ->disableOriginalConstructor()
      ->getMock();

169
170
    $generator = new UrlGenerator($this->provider, $processor_manager, $this->routeProcessorManager, $this->requestStack, ['http', 'https']);
    $generator->setContext($this->context);
webchick's avatar
webchick committed
171
    $this->generator = $generator;
172
173
  }

174
  /**
175
176
   * Return value callback for the getAliasByPath() method on the mock alias
   * manager.
177
   *
178
179
180
   * Ensures that by default the call to getAliasByPath() will return the first
   * argument that was passed in. We special-case the paths for which we wish it
   * to return an actual alias.
181
182
183
184
185
186
   *
   * @return string
   */
  public function aliasManagerCallback() {
    $args = func_get_args();
    switch($args[0]) {
187
188
189
190
191
192
      case '/test/one':
        return '/hello/world';
      case '/test/two/5':
        return '/goodbye/cruel/world';
      case '/<front>':
        return '/';
193
194
195
196
197
      default:
        return $args[0];
    }
  }

198
199
200
201
202
203
  /**
   * Confirms that generated routes will have aliased paths.
   */
  public function testAliasGeneration() {
    $url = $this->generator->generate('test_1');
    $this->assertEquals('/hello/world', $url);
204
205
    // No cacheability to test; UrlGenerator::generate() doesn't support
    // collecting cacheability metadata.
206

207
    $this->routeProcessorManager->expects($this->exactly(3))
208
209
210
211
      ->method('processOutbound')
      ->with($this->anything());


212
    // Check that the two generate methods return the same result.
213
    $this->assertGenerateFromRoute('test_1', [], [], $url, (new BubbleableMetadata())->setCacheMaxAge(Cache::PERMANENT));
214

215
216
217
218
    $path = $this->generator->getPathFromRoute('test_1');
    $this->assertEquals('test/one', $path);
  }

219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
  /**
   * @covers ::generateFromRoute
   */
  public function testUrlGenerationWithDisabledPathProcessing() {
    $path_processor = $this->prophesize(OutboundPathProcessorInterface::class);
    $path_processor->processOutbound(Argument::cetera())->shouldNotBeCalled();

    $generator = new UrlGenerator($this->provider, $path_processor->reveal(), $this->routeProcessorManager, $this->requestStack, ['http', 'https']);
    $generator->setContext($this->context);

    $url = $this->generator->generateFromRoute('test_1', [], ['path_processing' => FALSE]);
    $this->assertEquals('/test/one', $url);
  }

  /**
   * @covers ::generateFromRoute
   */
  public function testUrlGenerationWithDisabledPathProcessingByRoute() {
    $path_processor = $this->prophesize(OutboundPathProcessorInterface::class);
    $path_processor->processOutbound(Argument::cetera())->shouldNotBeCalled();

    $provider = $this->prophesize(RouteProviderInterface::class);
    $provider->getRouteByName('test_1')->willReturn(new Route('/test/one', [], [], ['default_url_options' => ['path_processing' => FALSE]]));

    $generator = new UrlGenerator($provider->reveal(), $path_processor->reveal(), $this->routeProcessorManager, $this->requestStack, ['http', 'https']);
    $generator->setContext($this->context);

    $url = $generator->generateFromRoute('test_1', []);
    $this->assertEquals('/test/one', $url);
  }

  /**
   * @covers ::generateFromRoute
   */
  public function testUrlGenerationWithDisabledPathProcessingByRouteAndOptedInPathProcessing() {
    $path_processor = $this->prophesize(OutboundPathProcessorInterface::class);
    $path_processor->processOutbound('/test/one', Argument::cetera())->willReturn('/hello/world')->shouldBeCalled();

    $provider = $this->prophesize(RouteProviderInterface::class);
    $provider->getRouteByName('test_1')->willReturn(new Route('/test/one', [], [], ['default_url_options' => ['path_processing' => FALSE]]));

    $generator = new UrlGenerator($provider->reveal(), $path_processor->reveal(), $this->routeProcessorManager, $this->requestStack, ['http', 'https']);
    $generator->setContext($this->context);

    $url = $generator->generateFromRoute('test_1', [], ['path_processing' => TRUE]);
    $this->assertEquals('/hello/world', $url);
  }

267
268
269
270
  /**
   * Tests URL generation in a subdirectory.
   */
  public function testGetPathFromRouteWithSubdirectory() {
271
    $this->routeProcessorManager->expects($this->once())
272
273
      ->method('processOutbound');

274
275
    $path = $this->generator->getPathFromRoute('test_1');
    $this->assertEquals('test/one', $path);
276
277
278
279
280
281
282
  }

  /**
   * Confirms that generated routes will have aliased paths.
   */
  public function testAliasGenerationWithParameters() {
    $url = $this->generator->generate('test_2', array('narf' => '5'));
283
    $this->assertEquals('/goodbye/cruel/world', $url);
284
285
    // No cacheability to test; UrlGenerator::generate() doesn't support
    // collecting cacheability metadata.
286

287
    $this->routeProcessorManager->expects($this->exactly(7))
288
289
290
      ->method('processOutbound')
      ->with($this->anything());

291
292
    $options = array('fragment' => 'top');
    // Extra parameters should appear in the query string.
293
    $this->assertGenerateFromRoute('test_1', ['zoo' => 5], $options, '/hello/world?zoo=5#top', (new BubbleableMetadata())->setCacheMaxAge(Cache::PERMANENT));
294
295

    $options = array('query' => array('page' => '1'), 'fragment' => 'bottom');
296
    $this->assertGenerateFromRoute('test_2', ['narf' => 5], $options, '/goodbye/cruel/world?page=1#bottom', (new BubbleableMetadata())->setCacheMaxAge(Cache::PERMANENT));
297
298

    // Changing the parameters, the route still matches but there is no alias.
299
    $this->assertGenerateFromRoute('test_2', ['narf' => 7], $options, '/test/two/7?page=1#bottom', (new BubbleableMetadata())->setCacheMaxAge(Cache::PERMANENT));
300
301
302
303
304

    $path = $this->generator->getPathFromRoute('test_2', array('narf' => '5'));
    $this->assertEquals('test/two/5', $path);
  }

305
306
307
308
309
310
  /**
   * Confirms that generated routes will have aliased paths with options.
   *
   * @dataProvider providerTestAliasGenerationWithOptions
   */
  public function testAliasGenerationWithOptions($route_name, $route_parameters, $options, $expected) {
311
    $this->assertGenerateFromRoute($route_name, $route_parameters, $options, $expected, (new BubbleableMetadata())->setCacheMaxAge(Cache::PERMANENT));
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
  }

  /**
   * Provides test data for testAliasGenerationWithOptions.
   */
  public function providerTestAliasGenerationWithOptions() {
    $data = [];
    // Extra parameters should appear in the query string.
    $data[] = [
      'test_1',
      ['zoo' => '5'],
      ['fragment' => 'top'],
      '/hello/world?zoo=5#top',
    ];
    $data[] = [
      'test_2',
      ['narf' => '5'],
      ['query' => ['page' => '1'], 'fragment' => 'bottom'],
      '/goodbye/cruel/world?page=1#bottom',
    ];
    // Changing the parameters, the route still matches but there is no alias.
    $data[] = [
      'test_2',
      ['narf' => '7'],
      ['query' => ['page' => '1'], 'fragment' => 'bottom'],
      '/test/two/7?page=1#bottom',
    ];
    // Query string values containing '/' should be decoded.
    $data[] = [
      'test_2',
      ['narf' => '7'],
      ['query' => ['page' => '1/2'], 'fragment' => 'bottom'],
      '/test/two/7?page=1/2#bottom',
    ];
    return $data;
  }

349
350
351
352
  /**
   * Tests URL generation from route with trailing start and end slashes.
   */
  public function testGetPathFromRouteTrailing() {
353
    $this->routeProcessorManager->expects($this->once())
354
355
      ->method('processOutbound');

356
357
    $path = $this->generator->getPathFromRoute('test_3');
    $this->assertEquals($path, 'test/two');
358
359
360
361
362
363
364
365
  }

  /**
   * Confirms that absolute URLs work with generated routes.
   */
  public function testAbsoluteURLGeneration() {
    $url = $this->generator->generate('test_1', array(), TRUE);
    $this->assertEquals('http://localhost/hello/world', $url);
366
367
    // No cacheability to test; UrlGenerator::generate() doesn't support
    // collecting cacheability metadata.
368

369
    $this->routeProcessorManager->expects($this->exactly(2))
370
371
372
      ->method('processOutbound')
      ->with($this->anything());

373
374
    $options = array('absolute' => TRUE, 'fragment' => 'top');
    // Extra parameters should appear in the query string.
375
    $this->assertGenerateFromRoute('test_1', ['zoo' => 5], $options, 'http://localhost/hello/world?zoo=5#top', (new BubbleableMetadata())->setCacheMaxAge(Cache::PERMANENT)->setCacheContexts(['url.site']));
376
377
  }

378
379
380
381
382
  /**
   * Confirms that explicitly setting the base_url works with generated routes
   */
  public function testBaseURLGeneration() {
    $options = array('base_url' => 'http://www.example.com:8888');
383
    $this->assertGenerateFromRoute('test_1', [], $options, 'http://www.example.com:8888/hello/world', (new BubbleableMetadata())->setCacheMaxAge(Cache::PERMANENT));
384
385

    $options = array('base_url' => 'http://www.example.com:8888', 'https' => TRUE);
386
    $this->assertGenerateFromRoute('test_1', [], $options, 'https://www.example.com:8888/hello/world', (new BubbleableMetadata())->setCacheMaxAge(Cache::PERMANENT));
387
388

    $options = array('base_url' => 'https://www.example.com:8888', 'https' => FALSE);
389
    $this->assertGenerateFromRoute('test_1', [], $options, 'http://www.example.com:8888/hello/world', (new BubbleableMetadata())->setCacheMaxAge(Cache::PERMANENT));
390

391
    $this->routeProcessorManager->expects($this->exactly(2))
392
393
394
395
396
      ->method('processOutbound')
      ->with($this->anything());

    $options = array('base_url' => 'http://www.example.com:8888', 'fragment' => 'top');
    // Extra parameters should appear in the query string.
397
    $this->assertGenerateFromRoute('test_1', ['zoo' => 5], $options, 'http://www.example.com:8888/hello/world?zoo=5#top', (new BubbleableMetadata())->setCacheMaxAge(Cache::PERMANENT));
398
399
  }

400
401
402
403
404
405
  /**
   * Test that the 'scheme' route requirement is respected during url generation.
   */
  public function testUrlGenerationWithHttpsRequirement() {
    $url = $this->generator->generate('test_4', array(), TRUE);
    $this->assertEquals('https://localhost/test/four', $url);
406
407
    // No cacheability to test; UrlGenerator::generate() doesn't support
    // collecting cacheability metadata.
408

409
    $this->routeProcessorManager->expects($this->exactly(2))
410
411
412
      ->method('processOutbound')
      ->with($this->anything());

413
    $options = array('absolute' => TRUE, 'https' => TRUE);
414
    $this->assertGenerateFromRoute('test_1', [], $options, 'https://localhost/hello/world', (new BubbleableMetadata())->setCacheMaxAge(Cache::PERMANENT)->setCacheContexts(['url.site']));
415
416
  }

417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
  /**
   * Tests generating a relative URL with no path.
   *
   * @param array $options
   *   An array of URL options.
   * @param string $expected_url
   *   The expected relative URL.
   *
   * @covers ::generateFromRoute
   *
   * @dataProvider providerTestNoPath
   */
  public function testNoPath($options, $expected_url) {
    $url = $this->generator->generateFromRoute('<none>', [], $options);
    $this->assertEquals($expected_url, $url);
  }

  /**
   * Data provider for ::testNoPath().
   */
  public function providerTestNoPath() {
    return [
      // Empty options.
      [[], ''],
      // Query parameters only.
      [['query' => ['foo' => 'bar']], '?foo=bar'],
      // Multiple query parameters.
      [['query' => ['foo' => 'bar', 'baz' => '']], '?foo=bar&baz='],
      // Fragment only.
      [['fragment' => 'foo'], '#foo'],
      // Query parameters and fragment.
      [['query' => ['bar' => 'baz'], 'fragment' => 'foo'], '?bar=baz#foo'],
      // Multiple query parameters and fragment.
      [['query' => ['bar' => 'baz', 'foo' => 'bar'], 'fragment' => 'foo'], '?bar=baz&foo=bar#foo'],
    ];
  }

454
455
456
457
458
459
460
461
462
463
464
  /**
   * Asserts \Drupal\Core\Routing\UrlGenerator::generateFromRoute()'s output.
   *
   * @param $route_name
   *   The route name to test.
   * @param array $route_parameters
   *   The route parameters to test.
   * @param array $options
   *   The options to test.
   * @param $expected_url
   *   The expected generated URL string.
465
466
   * @param \Drupal\Core\Render\BubbleableMetadata $expected_bubbleable_metadata
   *   The expected generated bubbleable metadata.
467
   */
468
  protected function assertGenerateFromRoute($route_name, array $route_parameters, array $options, $expected_url, BubbleableMetadata $expected_bubbleable_metadata) {
469
470
471
472
473
474
475
    // First, test with $collect_cacheability_metadata set to the default value.
    $url = $this->generator->generateFromRoute($route_name, $route_parameters, $options);
    $this->assertSame($expected_url, $url);

    // Second, test with it set to TRUE.
    $generated_url = $this->generator->generateFromRoute($route_name, $route_parameters, $options, TRUE);
    $this->assertSame($expected_url, $generated_url->getGeneratedUrl());
476
    $this->assertEquals($expected_bubbleable_metadata, BubbleableMetadata::createFromObject($generated_url));
477
478
  }

479
}