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