1<?php
2
3/**
4 * @file
5 * Contains \Drupal\Tests\system\Unit\Breadcrumbs\PathBasedBreadcrumbBuilderTest.
6 */
7
8namespace Drupal\Tests\system\Unit\Breadcrumbs;
9
10use Drupal\Core\Access\AccessResult;
11use Drupal\Core\Cache\Cache;
12use Drupal\Core\Link;
13use Drupal\Core\Access\AccessResultAllowed;
14use Drupal\Core\Path\PathMatcherInterface;
15use Drupal\Core\StringTranslation\TranslationInterface;
16use Drupal\Core\Url;
17use Drupal\Core\Utility\LinkGeneratorInterface;
18use Drupal\system\PathBasedBreadcrumbBuilder;
19use Drupal\Tests\UnitTestCase;
20use Symfony\Cmf\Component\Routing\RouteObjectInterface;
21use Symfony\Component\DependencyInjection\Container;
22use Symfony\Component\HttpFoundation\ParameterBag;
23use Symfony\Component\HttpFoundation\Request;
24use Symfony\Component\Routing\Route;
25
26/**
27 * @coversDefaultClass \Drupal\system\PathBasedBreadcrumbBuilder
28 * @group system
29 */
30class PathBasedBreadcrumbBuilderTest extends UnitTestCase {
31
32  /**
33   * The path based breadcrumb builder object to test.
34   *
35   * @var \Drupal\system\PathBasedBreadcrumbBuilder
36   */
37  protected $builder;
38
39  /**
40   * The mocked title resolver.
41   *
42   * @var \Drupal\Core\Controller\TitleResolverInterface|\PHPUnit\Framework\MockObject\MockObject
43   */
44  protected $titleResolver;
45
46  /**
47   * The mocked access manager.
48   *
49   * @var \Drupal\Core\Access\AccessManagerInterface|\PHPUnit\Framework\MockObject\MockObject
50   */
51  protected $accessManager;
52
53  /**
54   * The request matching mock object.
55   *
56   * @var \Symfony\Component\Routing\Matcher\RequestMatcherInterface|\PHPUnit\Framework\MockObject\MockObject
57   */
58  protected $requestMatcher;
59
60  /**
61   * The mocked route request context.
62   *
63   * @var \Drupal\Core\Routing\RequestContext|\PHPUnit\Framework\MockObject\MockObject
64   */
65  protected $context;
66
67  /**
68   * The mocked current user.
69   *
70   * @var \Drupal\Core\Session\AccountInterface|\PHPUnit\Framework\MockObject\MockObject
71   */
72  protected $currentUser;
73
74  /**
75   * The mocked path processor.
76   *
77   * @var \Drupal\Core\PathProcessor\InboundPathProcessorInterface|\PHPUnit\Framework\MockObject\MockObject
78   */
79  protected $pathProcessor;
80
81  /**
82   * The mocked current path.
83   *
84   * @var \Drupal\Core\Path\CurrentPathStack|\PHPUnit\Framework\MockObject\MockObject
85   */
86  protected $currentPath;
87
88  /**
89   * The mocked path matcher service.
90   *
91   * @var \Drupal\Core\Path\PathMatcherInterface|\PHPUnit\Framework\MockObject\MockObject
92   */
93  protected $pathMatcher;
94
95  /**
96   * {@inheritdoc}
97   *
98   * @covers ::__construct
99   */
100  protected function setUp() {
101    parent::setUp();
102
103    $this->requestMatcher = $this->createMock('\Symfony\Component\Routing\Matcher\RequestMatcherInterface');
104
105    $config_factory = $this->getConfigFactoryStub(['system.site' => ['front' => 'test_frontpage']]);
106
107    $this->pathProcessor = $this->createMock('\Drupal\Core\PathProcessor\InboundPathProcessorInterface');
108    $this->context = $this->createMock('\Drupal\Core\Routing\RequestContext');
109
110    $this->accessManager = $this->createMock('\Drupal\Core\Access\AccessManagerInterface');
111    $this->titleResolver = $this->createMock('\Drupal\Core\Controller\TitleResolverInterface');
112    $this->currentUser = $this->createMock('Drupal\Core\Session\AccountInterface');
113    $this->currentPath = $this->getMockBuilder('Drupal\Core\Path\CurrentPathStack')
114      ->disableOriginalConstructor()
115      ->getMock();
116
117    $this->pathMatcher = $this->createMock(PathMatcherInterface::class);
118
119    $this->builder = new TestPathBasedBreadcrumbBuilder(
120      $this->context,
121      $this->accessManager,
122      $this->requestMatcher,
123      $this->pathProcessor,
124      $config_factory,
125      $this->titleResolver,
126      $this->currentUser,
127      $this->currentPath,
128      $this->pathMatcher
129    );
130
131    $this->builder->setStringTranslation($this->getStringTranslationStub());
132
133    $cache_contexts_manager = $this->getMockBuilder('Drupal\Core\Cache\Context\CacheContextsManager')
134      ->disableOriginalConstructor()
135      ->getMock();
136    $cache_contexts_manager->method('assertValidTokens')->willReturn(TRUE);
137    $container = new Container();
138    $container->set('cache_contexts_manager', $cache_contexts_manager);
139    \Drupal::setContainer($container);
140  }
141
142  /**
143   * Tests the build method on the frontpage.
144   *
145   * @covers ::build
146   */
147  public function testBuildOnFrontpage() {
148    $this->pathMatcher->expects($this->once())
149      ->method('isFrontPage')
150      ->willReturn(TRUE);
151
152    $breadcrumb = $this->builder->build($this->createMock('Drupal\Core\Routing\RouteMatchInterface'));
153    $this->assertEquals([], $breadcrumb->getLinks());
154    $this->assertEquals(['url.path.is_front', 'url.path.parent'], $breadcrumb->getCacheContexts());
155    $this->assertEquals([], $breadcrumb->getCacheTags());
156    $this->assertEquals(Cache::PERMANENT, $breadcrumb->getCacheMaxAge());
157  }
158
159  /**
160   * Tests the build method with one path element.
161   *
162   * @covers ::build
163   */
164  public function testBuildWithOnePathElement() {
165    $this->context->expects($this->once())
166      ->method('getPathInfo')
167      ->will($this->returnValue('/example'));
168
169    $breadcrumb = $this->builder->build($this->createMock('Drupal\Core\Routing\RouteMatchInterface'));
170    $this->assertEquals([0 => new Link('Home', new Url('<front>'))], $breadcrumb->getLinks());
171    $this->assertEquals(['url.path.is_front', 'url.path.parent'], $breadcrumb->getCacheContexts());
172    $this->assertEquals([], $breadcrumb->getCacheTags());
173    $this->assertEquals(Cache::PERMANENT, $breadcrumb->getCacheMaxAge());
174  }
175
176  /**
177   * Tests the build method with two path elements.
178   *
179   * @covers ::build
180   * @covers ::getRequestForPath
181   */
182  public function testBuildWithTwoPathElements() {
183    $this->context->expects($this->once())
184      ->method('getPathInfo')
185      ->will($this->returnValue('/example/baz'));
186    $this->setupStubPathProcessor();
187
188    $route_1 = new Route('/example');
189
190    $this->requestMatcher->expects($this->exactly(1))
191      ->method('matchRequest')
192      ->will($this->returnCallback(function (Request $request) use ($route_1) {
193        if ($request->getPathInfo() == '/example') {
194          return [
195            RouteObjectInterface::ROUTE_NAME => 'example',
196            RouteObjectInterface::ROUTE_OBJECT => $route_1,
197            '_raw_variables' => new ParameterBag([]),
198          ];
199        }
200      }));
201
202    $this->setupAccessManagerToAllow();
203
204    $breadcrumb = $this->builder->build($this->createMock('Drupal\Core\Routing\RouteMatchInterface'));
205    $this->assertEquals([0 => new Link('Home', new Url('<front>')), 1 => new Link('Example', new Url('example'))], $breadcrumb->getLinks());
206    $this->assertEquals([
207      'url.path.is_front',
208      'url.path.parent',
209      'user.permissions',
210    ], $breadcrumb->getCacheContexts());
211    $this->assertEquals([], $breadcrumb->getCacheTags());
212    $this->assertEquals(Cache::PERMANENT, $breadcrumb->getCacheMaxAge());
213  }
214
215  /**
216   * Tests the build method with three path elements.
217   *
218   * @covers ::build
219   * @covers ::getRequestForPath
220   */
221  public function testBuildWithThreePathElements() {
222    $this->context->expects($this->once())
223      ->method('getPathInfo')
224      ->will($this->returnValue('/example/bar/baz'));
225    $this->setupStubPathProcessor();
226
227    $route_1 = new Route('/example/bar');
228    $route_2 = new Route('/example');
229
230    $this->requestMatcher->expects($this->exactly(2))
231      ->method('matchRequest')
232      ->will($this->returnCallback(function (Request $request) use ($route_1, $route_2) {
233        if ($request->getPathInfo() == '/example/bar') {
234          return [
235            RouteObjectInterface::ROUTE_NAME => 'example_bar',
236            RouteObjectInterface::ROUTE_OBJECT => $route_1,
237            '_raw_variables' => new ParameterBag([]),
238          ];
239        }
240        elseif ($request->getPathInfo() == '/example') {
241          return [
242            RouteObjectInterface::ROUTE_NAME => 'example',
243            RouteObjectInterface::ROUTE_OBJECT => $route_2,
244            '_raw_variables' => new ParameterBag([]),
245          ];
246        }
247      }));
248
249    $this->accessManager->expects($this->any())
250      ->method('check')
251      ->willReturnOnConsecutiveCalls(
252        AccessResult::allowed()->cachePerPermissions(),
253        AccessResult::allowed()->addCacheContexts(['bar'])->addCacheTags(['example'])
254      );
255    $breadcrumb = $this->builder->build($this->createMock('Drupal\Core\Routing\RouteMatchInterface'));
256    $this->assertEquals([
257      new Link('Home', new Url('<front>')),
258      new Link('Example', new Url('example')),
259      new Link('Bar', new Url('example_bar')),
260    ], $breadcrumb->getLinks());
261    $this->assertEquals([
262      'bar',
263      'url.path.is_front',
264      'url.path.parent',
265      'user.permissions',
266    ], $breadcrumb->getCacheContexts());
267    $this->assertEquals(['example'], $breadcrumb->getCacheTags());
268    $this->assertEquals(Cache::PERMANENT, $breadcrumb->getCacheMaxAge());
269  }
270
271  /**
272   * Tests that exceptions during request matching are caught.
273   *
274   * @covers ::build
275   * @covers ::getRequestForPath
276   *
277   * @dataProvider providerTestBuildWithException
278   */
279  public function testBuildWithException($exception_class, $exception_argument) {
280    $this->context->expects($this->once())
281      ->method('getPathInfo')
282      ->will($this->returnValue('/example/bar'));
283    $this->setupStubPathProcessor();
284
285    $this->requestMatcher->expects($this->any())
286      ->method('matchRequest')
287      ->will($this->throwException(new $exception_class($exception_argument)));
288
289    $breadcrumb = $this->builder->build($this->createMock('Drupal\Core\Routing\RouteMatchInterface'));
290
291    // No path matched, though at least the frontpage is displayed.
292    $this->assertEquals([0 => new Link('Home', new Url('<front>'))], $breadcrumb->getLinks());
293    $this->assertEquals(['url.path.is_front', 'url.path.parent'], $breadcrumb->getCacheContexts());
294    $this->assertEquals([], $breadcrumb->getCacheTags());
295    $this->assertEquals(Cache::PERMANENT, $breadcrumb->getCacheMaxAge());
296  }
297
298  /**
299   * Provides exception types for testBuildWithException.
300   *
301   * @return array
302   *   The list of exception test cases.
303   *
304   * @see \Drupal\Tests\system\Unit\Breadcrumbs\PathBasedBreadcrumbBuilderTest::testBuildWithException()
305   */
306  public function providerTestBuildWithException() {
307    return [
308      ['Drupal\Core\ParamConverter\ParamNotConvertedException', ''],
309      ['Symfony\Component\Routing\Exception\MethodNotAllowedException', []],
310      ['Symfony\Component\Routing\Exception\ResourceNotFoundException', ''],
311    ];
312  }
313
314  /**
315   * Tests the build method with a non processed path.
316   *
317   * @covers ::build
318   * @covers ::getRequestForPath
319   */
320  public function testBuildWithNonProcessedPath() {
321    $this->context->expects($this->once())
322      ->method('getPathInfo')
323      ->will($this->returnValue('/example/bar'));
324
325    $this->pathProcessor->expects($this->once())
326      ->method('processInbound')
327      ->will($this->returnValue(FALSE));
328
329    $this->requestMatcher->expects($this->any())
330      ->method('matchRequest')
331      ->will($this->returnValue([]));
332
333    $breadcrumb = $this->builder->build($this->createMock('Drupal\Core\Routing\RouteMatchInterface'));
334
335    // No path matched, though at least the frontpage is displayed.
336    $this->assertEquals([0 => new Link('Home', new Url('<front>'))], $breadcrumb->getLinks());
337    $this->assertEquals(['url.path.is_front', 'url.path.parent'], $breadcrumb->getCacheContexts());
338    $this->assertEquals([], $breadcrumb->getCacheTags());
339    $this->assertEquals(Cache::PERMANENT, $breadcrumb->getCacheMaxAge());
340  }
341
342  /**
343   * Tests the applied method.
344   *
345   * @covers ::applies
346   */
347  public function testApplies() {
348    $this->assertTrue($this->builder->applies($this->createMock('Drupal\Core\Routing\RouteMatchInterface')));
349  }
350
351  /**
352   * Tests the breadcrumb for a user path.
353   *
354   * @covers ::build
355   * @covers ::getRequestForPath
356   */
357  public function testBuildWithUserPath() {
358    $this->context->expects($this->once())
359      ->method('getPathInfo')
360      ->will($this->returnValue('/user/1/edit'));
361    $this->setupStubPathProcessor();
362
363    $route_1 = new Route('/user/1');
364
365    $this->requestMatcher->expects($this->exactly(1))
366      ->method('matchRequest')
367      ->will($this->returnCallback(function (Request $request) use ($route_1) {
368        if ($request->getPathInfo() == '/user/1') {
369          return [
370            RouteObjectInterface::ROUTE_NAME => 'user_page',
371            RouteObjectInterface::ROUTE_OBJECT => $route_1,
372            '_raw_variables' => new ParameterBag([]),
373          ];
374        }
375      }));
376
377    $this->setupAccessManagerToAllow();
378    $this->titleResolver->expects($this->once())
379      ->method('getTitle')
380      ->with($this->anything(), $route_1)
381      ->will($this->returnValue('Admin'));
382
383    $breadcrumb = $this->builder->build($this->createMock('Drupal\Core\Routing\RouteMatchInterface'));
384    $this->assertEquals([0 => new Link('Home', new Url('<front>')), 1 => new Link('Admin', new Url('user_page'))], $breadcrumb->getLinks());
385    $this->assertEquals([
386      'url.path.is_front',
387      'url.path.parent',
388      'user.permissions',
389    ], $breadcrumb->getCacheContexts());
390    $this->assertEquals([], $breadcrumb->getCacheTags());
391    $this->assertEquals(Cache::PERMANENT, $breadcrumb->getCacheMaxAge());
392  }
393
394  /**
395   * Setup the access manager to always allow access to routes.
396   */
397  public function setupAccessManagerToAllow() {
398    $this->accessManager->expects($this->any())
399      ->method('check')
400      ->willReturn((new AccessResultAllowed())->cachePerPermissions());
401  }
402
403  protected function setupStubPathProcessor() {
404    $this->pathProcessor->expects($this->any())
405      ->method('processInbound')
406      ->will($this->returnArgument(0));
407  }
408
409}
410
411/**
412 * Helper class for testing purposes only.
413 */
414class TestPathBasedBreadcrumbBuilder extends PathBasedBreadcrumbBuilder {
415
416  public function setStringTranslation(TranslationInterface $string_translation) {
417    $this->stringTranslation = $string_translation;
418  }
419
420  public function setLinkGenerator(LinkGeneratorInterface $link_generator) {
421    $this->linkGenerator = $link_generator;
422  }
423
424}
425