5 * Contains \Drupal\Tests\Core\Access\AccessManagerTest.
8 namespace Drupal\Tests\Core\Access;
10 use Drupal\Core\Access\AccessCheckInterface;
11 use Drupal\Core\Access\AccessException;
12 use Drupal\Core\Access\AccessResult;
13 use Drupal\Core\Access\CheckProvider;
14 use Drupal\Core\Cache\Context\CacheContextsManager;
15 use Drupal\Core\Routing\RouteMatch;
16 use Drupal\Core\Access\AccessManager;
17 use Drupal\Core\Access\DefaultAccessCheck;
18 use Drupal\Tests\UnitTestCase;
19 use Drupal\router_test\Access\DefinedTestAccessCheck;
20 use Symfony\Cmf\Component\Routing\RouteObjectInterface;
21 use Symfony\Component\DependencyInjection\ContainerBuilder;
22 use Symfony\Component\Routing\Exception\RouteNotFoundException;
23 use Symfony\Component\Routing\Route;
24 use Symfony\Component\Routing\RouteCollection;
27 * @coversDefaultClass \Drupal\Core\Access\AccessManager
30 class AccessManagerTest extends UnitTestCase {
33 * The dependency injection container.
35 * @var \Symfony\Component\DependencyInjection\ContainerBuilder
40 * The collection of routes, which are tested.
42 * @var \Symfony\Component\Routing\RouteCollection
44 protected $routeCollection;
47 * The access manager to test.
49 * @var \Drupal\Core\Access\AccessManager
51 protected $accessManager;
56 * @var \PHPUnit_Framework_MockObject_MockObject
58 protected $routeProvider;
61 * The parameter converter.
63 * @var \Drupal\Core\ParamConverter\ParamConverterManagerInterface|\PHPUnit_Framework_MockObject_MockObject
65 protected $paramConverter;
70 * @var \Drupal\Core\Session\AccountInterface|\PHPUnit_Framework_MockObject_MockObject
75 * The access arguments resolver.
77 * @var \Drupal\Core\Access\AccessArgumentsResolverFactoryInterface|\PHPUnit_Framework_MockObject_MockObject
79 protected $argumentsResolverFactory;
82 * @var \Drupal\Core\Session\AccountInterface|\PHPUnit_Framework_MockObject_MockObject
84 protected $currentUser;
87 * @var \Drupal\Core\Access\CheckProvider
89 protected $checkProvider;
94 protected function setUp() {
97 $this->container = new ContainerBuilder();
98 $cache_contexts_manager = $this->prophesize(CacheContextsManager::class)->reveal();
99 $this->container->set('cache_contexts_manager', $cache_contexts_manager);
100 \Drupal::setContainer($this->container);
102 $this->routeCollection = new RouteCollection();
103 $this->routeCollection->add('test_route_1', new Route('/test-route-1'));
104 $this->routeCollection->add('test_route_2', new Route('/test-route-2', [], ['_access' => 'TRUE']));
105 $this->routeCollection->add('test_route_3', new Route('/test-route-3', [], ['_access' => 'FALSE']));
106 $this->routeCollection->add('test_route_4', new Route('/test-route-4/{value}', [], ['_access' => 'TRUE']));
108 $this->routeProvider = $this->getMock('Drupal\Core\Routing\RouteProviderInterface');
110 foreach ($this->routeCollection->all() as $name => $route) {
111 $map[] = [$name, [], $route];
113 $map[] = ['test_route_4', ['value' => 'example'], $this->routeCollection->get('test_route_4')];
114 $this->routeProvider->expects($this->any())
115 ->method('getRouteByName')
116 ->will($this->returnValueMap($map));
119 $map[] = ['test_route_1', [], '/test-route-1'];
120 $map[] = ['test_route_2', [], '/test-route-2'];
121 $map[] = ['test_route_3', [], '/test-route-3'];
122 $map[] = ['test_route_4', ['value' => 'example'], '/test-route-4/example'];
124 $this->paramConverter = $this->getMock('Drupal\Core\ParamConverter\ParamConverterManagerInterface');
126 $this->account = $this->getMock('Drupal\Core\Session\AccountInterface');
127 $this->currentUser = $this->getMock('Drupal\Core\Session\AccountInterface');
128 $this->argumentsResolverFactory = $this->getMock('Drupal\Core\Access\AccessArgumentsResolverFactoryInterface');
129 $this->checkProvider = new CheckProvider();
130 $this->checkProvider->setContainer($this->container);
132 $this->accessManager = new AccessManager($this->routeProvider, $this->paramConverter, $this->argumentsResolverFactory, $this->currentUser, $this->checkProvider);
136 * Tests \Drupal\Core\Access\AccessManager::setChecks().
138 public function testSetChecks() {
139 // Check setChecks without any access checker defined yet.
140 $this->checkProvider->setChecks($this->routeCollection);
142 foreach ($this->routeCollection->all() as $route) {
143 $this->assertNull($route->getOption('_access_checks'));
146 $this->setupAccessChecker();
148 $this->checkProvider->setChecks($this->routeCollection);
150 $this->assertEquals($this->routeCollection->get('test_route_1')->getOption('_access_checks'), NULL);
151 $this->assertEquals($this->routeCollection->get('test_route_2')->getOption('_access_checks'), ['test_access_default']);
152 $this->assertEquals($this->routeCollection->get('test_route_3')->getOption('_access_checks'), ['test_access_default']);
156 * Tests setChecks with a dynamic access checker.
158 public function testSetChecksWithDynamicAccessChecker() {
159 // Setup the access manager.
160 $this->accessManager = new AccessManager($this->routeProvider, $this->paramConverter, $this->argumentsResolverFactory, $this->currentUser, $this->checkProvider);
162 // Setup the dynamic access checker.
163 $access_check = $this->getMock('Drupal\Tests\Core\Access\TestAccessCheckInterface');
164 $this->container->set('test_access', $access_check);
165 $this->checkProvider->addCheckService('test_access', 'access');
167 $route = new Route('/test-path', [], ['_foo' => '1', '_bar' => '1']);
168 $route2 = new Route('/test-path', [], ['_foo' => '1', '_bar' => '2']);
169 $collection = new RouteCollection();
170 $collection->add('test_route', $route);
171 $collection->add('test_route2', $route2);
173 $access_check->expects($this->exactly(2))
175 ->with($this->isInstanceOf('Symfony\Component\Routing\Route'))
176 ->will($this->returnCallback(function (Route $route) {
177 return $route->getRequirement('_bar') == 2;
180 $this->checkProvider->setChecks($collection);
181 $this->assertEmpty($route->getOption('_access_checks'));
182 $this->assertEquals(['test_access'], $route2->getOption('_access_checks'));
186 * Tests \Drupal\Core\Access\AccessManager::check().
188 public function testCheck() {
191 // Construct route match objects.
192 foreach ($this->routeCollection->all() as $route_name => $route) {
193 $route_matches[$route_name] = new RouteMatch($route_name, $route, [], []);
196 // Check route access without any access checker defined yet.
197 foreach ($route_matches as $route_match) {
198 $this->assertEquals(FALSE, $this->accessManager->check($route_match, $this->account));
199 $this->assertEquals(AccessResult::neutral(), $this->accessManager->check($route_match, $this->account, NULL, TRUE));
202 $this->setupAccessChecker();
204 // An access checker got setup, but the routes haven't been setup using
206 foreach ($route_matches as $route_match) {
207 $this->assertEquals(FALSE, $this->accessManager->check($route_match, $this->account));
208 $this->assertEquals(AccessResult::neutral(), $this->accessManager->check($route_match, $this->account, NULL, TRUE));
211 // Now applicable access checks have been saved on each route object.
212 $this->checkProvider->setChecks($this->routeCollection);
213 $this->setupAccessArgumentsResolverFactory();
215 $this->assertEquals(FALSE, $this->accessManager->check($route_matches['test_route_1'], $this->account));
216 $this->assertEquals(TRUE, $this->accessManager->check($route_matches['test_route_2'], $this->account));
217 $this->assertEquals(FALSE, $this->accessManager->check($route_matches['test_route_3'], $this->account));
218 $this->assertEquals(TRUE, $this->accessManager->check($route_matches['test_route_4'], $this->account));
219 $this->assertEquals(AccessResult::neutral(), $this->accessManager->check($route_matches['test_route_1'], $this->account, NULL, TRUE));
220 $this->assertEquals(AccessResult::allowed(), $this->accessManager->check($route_matches['test_route_2'], $this->account, NULL, TRUE));
221 $this->assertEquals(AccessResult::forbidden(), $this->accessManager->check($route_matches['test_route_3'], $this->account, NULL, TRUE));
222 $this->assertEquals(AccessResult::allowed(), $this->accessManager->check($route_matches['test_route_4'], $this->account, NULL, TRUE));
226 * Tests \Drupal\Core\Access\AccessManager::check() with no account specified.
230 public function testCheckWithNullAccount() {
231 $this->setupAccessChecker();
232 $this->checkProvider->setChecks($this->routeCollection);
234 $route = $this->routeCollection->get('test_route_2');
235 $route_match = new RouteMatch('test_route_2', $route, [], []);
237 // Asserts that the current user is passed to the access arguments resolver
239 $this->setupAccessArgumentsResolverFactory()
240 ->with($route_match, $this->currentUser, NULL);
242 $this->assertTrue($this->accessManager->check($route_match));
246 * Provides data for the conjunction test.
249 * An array of data for check conjunctions.
251 * @see \Drupal\Tests\Core\Access\AccessManagerTest::testCheckConjunctions()
253 public function providerTestCheckConjunctions() {
254 $access_allow = AccessResult::allowed();
255 $access_deny = AccessResult::neutral();
256 $access_kill = AccessResult::forbidden();
258 $access_configurations = [];
259 $access_configurations[] = [
260 'name' => 'test_route_4',
261 'condition_one' => 'TRUE',
262 'condition_two' => 'FALSE',
263 'expected' => $access_kill,
265 $access_configurations[] = [
266 'name' => 'test_route_5',
267 'condition_one' => 'TRUE',
268 'condition_two' => 'NULL',
269 'expected' => $access_deny,
271 $access_configurations[] = [
272 'name' => 'test_route_6',
273 'condition_one' => 'FALSE',
274 'condition_two' => 'NULL',
275 'expected' => $access_kill,
277 $access_configurations[] = [
278 'name' => 'test_route_7',
279 'condition_one' => 'TRUE',
280 'condition_two' => 'TRUE',
281 'expected' => $access_allow,
283 $access_configurations[] = [
284 'name' => 'test_route_8',
285 'condition_one' => 'FALSE',
286 'condition_two' => 'FALSE',
287 'expected' => $access_kill,
289 $access_configurations[] = [
290 'name' => 'test_route_9',
291 'condition_one' => 'NULL',
292 'condition_two' => 'NULL',
293 'expected' => $access_deny,
296 return $access_configurations;
300 * Test \Drupal\Core\Access\AccessManager::check() with conjunctions.
302 * @dataProvider providerTestCheckConjunctions
304 public function testCheckConjunctions($name, $condition_one, $condition_two, $expected_access) {
305 $this->setupAccessChecker();
306 $access_check = new DefinedTestAccessCheck();
307 $this->container->register('test_access_defined', $access_check);
308 $this->checkProvider->addCheckService('test_access_defined', 'access', ['_test_access']);
310 $route_collection = new RouteCollection();
311 // Setup a test route for each access configuration.
313 '_access' => $condition_one,
314 '_test_access' => $condition_two,
316 $route = new Route($name, [], $requirements);
317 $route_collection->add($name, $route);
319 $this->checkProvider->setChecks($route_collection);
320 $this->setupAccessArgumentsResolverFactory();
322 $route_match = new RouteMatch($name, $route, [], []);
323 $this->assertEquals($expected_access->isAllowed(), $this->accessManager->check($route_match, $this->account));
324 $this->assertEquals($expected_access, $this->accessManager->check($route_match, $this->account, NULL, TRUE));
328 * Tests the checkNamedRoute method.
330 * @see \Drupal\Core\Access\AccessManager::checkNamedRoute()
332 public function testCheckNamedRoute() {
333 $this->setupAccessChecker();
334 $this->checkProvider->setChecks($this->routeCollection);
335 $this->setupAccessArgumentsResolverFactory();
337 $this->paramConverter->expects($this->at(0))
339 ->with([RouteObjectInterface::ROUTE_NAME => 'test_route_2', RouteObjectInterface::ROUTE_OBJECT => $this->routeCollection->get('test_route_2')])
340 ->will($this->returnValue([]));
341 $this->paramConverter->expects($this->at(1))
343 ->with([RouteObjectInterface::ROUTE_NAME => 'test_route_2', RouteObjectInterface::ROUTE_OBJECT => $this->routeCollection->get('test_route_2')])
344 ->will($this->returnValue([]));
346 $this->paramConverter->expects($this->at(2))
348 ->with(['value' => 'example', RouteObjectInterface::ROUTE_NAME => 'test_route_4', RouteObjectInterface::ROUTE_OBJECT => $this->routeCollection->get('test_route_4')])
349 ->will($this->returnValue(['value' => 'example']));
350 $this->paramConverter->expects($this->at(3))
352 ->with(['value' => 'example', RouteObjectInterface::ROUTE_NAME => 'test_route_4', RouteObjectInterface::ROUTE_OBJECT => $this->routeCollection->get('test_route_4')])
353 ->will($this->returnValue(['value' => 'example']));
355 // Tests the access with routes with parameters without given request.
356 $this->assertEquals(TRUE, $this->accessManager->checkNamedRoute('test_route_2', [], $this->account));
357 $this->assertEquals(AccessResult::allowed(), $this->accessManager->checkNamedRoute('test_route_2', [], $this->account, TRUE));
358 $this->assertEquals(TRUE, $this->accessManager->checkNamedRoute('test_route_4', ['value' => 'example'], $this->account));
359 $this->assertEquals(AccessResult::allowed(), $this->accessManager->checkNamedRoute('test_route_4', ['value' => 'example'], $this->account, TRUE));
363 * Tests the checkNamedRoute with upcasted values.
365 * @see \Drupal\Core\Access\AccessManager::checkNamedRoute()
367 public function testCheckNamedRouteWithUpcastedValues() {
368 $this->routeCollection = new RouteCollection();
369 $route = new Route('/test-route-1/{value}', [], ['_test_access' => 'TRUE']);
370 $this->routeCollection->add('test_route_1', $route);
372 $this->routeProvider = $this->getMock('Drupal\Core\Routing\RouteProviderInterface');
373 $this->routeProvider->expects($this->any())
374 ->method('getRouteByName')
375 ->with('test_route_1', ['value' => 'example'])
376 ->will($this->returnValue($route));
379 $map[] = ['test_route_1', ['value' => 'example'], '/test-route-1/example'];
381 $this->paramConverter = $this->getMock('Drupal\Core\ParamConverter\ParamConverterManagerInterface');
382 $this->paramConverter->expects($this->atLeastOnce())
384 ->with(['value' => 'example', RouteObjectInterface::ROUTE_NAME => 'test_route_1', RouteObjectInterface::ROUTE_OBJECT => $route])
385 ->will($this->returnValue(['value' => 'upcasted_value']));
387 $this->setupAccessArgumentsResolverFactory($this->exactly(2))
388 ->with($this->callback(function ($route_match) {
389 return $route_match->getParameters()->get('value') == 'upcasted_value';
392 $this->accessManager = new AccessManager($this->routeProvider, $this->paramConverter, $this->argumentsResolverFactory, $this->currentUser, $this->checkProvider);
394 $access_check = $this->getMock('Drupal\Tests\Core\Access\TestAccessCheckInterface');
395 $access_check->expects($this->atLeastOnce())
397 ->will($this->returnValue(TRUE));
398 $access_check->expects($this->atLeastOnce())
400 ->will($this->returnValue(AccessResult::forbidden()));
402 $this->container->set('test_access', $access_check);
404 $this->checkProvider->addCheckService('test_access', 'access');
405 $this->checkProvider->setChecks($this->routeCollection);
407 $this->assertEquals(FALSE, $this->accessManager->checkNamedRoute('test_route_1', ['value' => 'example'], $this->account));
408 $this->assertEquals(AccessResult::forbidden(), $this->accessManager->checkNamedRoute('test_route_1', ['value' => 'example'], $this->account, TRUE));
412 * Tests the checkNamedRoute with default values.
414 * @covers ::checkNamedRoute
416 public function testCheckNamedRouteWithDefaultValue() {
417 $this->routeCollection = new RouteCollection();
418 $route = new Route('/test-route-1/{value}', ['value' => 'example'], ['_test_access' => 'TRUE']);
419 $this->routeCollection->add('test_route_1', $route);
421 $this->routeProvider = $this->getMock('Drupal\Core\Routing\RouteProviderInterface');
422 $this->routeProvider->expects($this->any())
423 ->method('getRouteByName')
424 ->with('test_route_1', [])
425 ->will($this->returnValue($route));
428 $map[] = ['test_route_1', ['value' => 'example'], '/test-route-1/example'];
430 $this->paramConverter = $this->getMock('Drupal\Core\ParamConverter\ParamConverterManagerInterface');
431 $this->paramConverter->expects($this->atLeastOnce())
433 ->with(['value' => 'example', RouteObjectInterface::ROUTE_NAME => 'test_route_1', RouteObjectInterface::ROUTE_OBJECT => $route])
434 ->will($this->returnValue(['value' => 'upcasted_value']));
436 $this->setupAccessArgumentsResolverFactory($this->exactly(2))
437 ->with($this->callback(function ($route_match) {
438 return $route_match->getParameters()->get('value') == 'upcasted_value';
441 $this->accessManager = new AccessManager($this->routeProvider, $this->paramConverter, $this->argumentsResolverFactory, $this->currentUser, $this->checkProvider);
443 $access_check = $this->getMock('Drupal\Tests\Core\Access\TestAccessCheckInterface');
444 $access_check->expects($this->atLeastOnce())
446 ->will($this->returnValue(TRUE));
447 $access_check->expects($this->atLeastOnce())
449 ->will($this->returnValue(AccessResult::forbidden()));
451 $this->container->set('test_access', $access_check);
453 $this->checkProvider->addCheckService('test_access', 'access');
454 $this->checkProvider->setChecks($this->routeCollection);
456 $this->assertEquals(FALSE, $this->accessManager->checkNamedRoute('test_route_1', [], $this->account));
457 $this->assertEquals(AccessResult::forbidden(), $this->accessManager->checkNamedRoute('test_route_1', [], $this->account, TRUE));
461 * Tests checkNamedRoute given an invalid/non existing route name.
463 public function testCheckNamedRouteWithNonExistingRoute() {
464 $this->routeProvider->expects($this->any())
465 ->method('getRouteByName')
466 ->will($this->throwException(new RouteNotFoundException()));
468 $this->setupAccessChecker();
470 $this->assertEquals(FALSE, $this->accessManager->checkNamedRoute('test_route_1', [], $this->account), 'A non existing route lead to access.');
471 $this->assertEquals(AccessResult::forbidden()->addCacheTags(['config:core.extension']), $this->accessManager->checkNamedRoute('test_route_1', [], $this->account, TRUE), 'A non existing route lead to access.');
475 * Tests that an access checker throws an exception for not allowed values.
477 * @dataProvider providerCheckException
479 public function testCheckException($return_value) {
480 $route_provider = $this->getMock('Drupal\Core\Routing\RouteProviderInterface');
482 // Setup a test route for each access configuration.
484 '_test_incorrect_value' => 'TRUE',
487 '_access_checks' => [
488 'test_incorrect_value',
491 $route = new Route('', [], $requirements, $options);
493 $route_provider->expects($this->any())
494 ->method('getRouteByName')
495 ->will($this->returnValue($route));
497 $this->paramConverter = $this->getMock('Drupal\Core\ParamConverter\ParamConverterManagerInterface');
498 $this->paramConverter->expects($this->any())
500 ->will($this->returnValue([]));
502 $this->setupAccessArgumentsResolverFactory();
504 $container = new ContainerBuilder();
506 // Register a service that will return an incorrect value.
507 $access_check = $this->getMock('Drupal\Tests\Core\Access\TestAccessCheckInterface');
508 $access_check->expects($this->any())
510 ->will($this->returnValue($return_value));
511 $container->set('test_incorrect_value', $access_check);
513 $access_manager = new AccessManager($route_provider, $this->paramConverter, $this->argumentsResolverFactory, $this->currentUser, $this->checkProvider);
514 $this->checkProvider->setContainer($container);
515 $this->checkProvider->addCheckService('test_incorrect_value', 'access');
517 $this->setExpectedException(AccessException::class);
518 $access_manager->checkNamedRoute('test_incorrect_value', [], $this->account);
522 * Data provider for testCheckException.
526 public function providerCheckException() {
536 * Adds a default access check service to the container and the access manager.
538 protected function setupAccessChecker() {
539 $access_check = new DefaultAccessCheck();
540 $this->container->register('test_access_default', $access_check);
541 $this->checkProvider->addCheckService('test_access_default', 'access', ['_access']);
545 * Add default expectations to the access arguments resolver factory.
547 protected function setupAccessArgumentsResolverFactory($constraint = NULL) {
548 if (!isset($constraint)) {
549 $constraint = $this->any();
551 return $this->argumentsResolverFactory->expects($constraint)
552 ->method('getArgumentsResolver')
553 ->will($this->returnCallback(function ($route_match, $account) {
554 $resolver = $this->getMock('Drupal\Component\Utility\ArgumentsResolverInterface');
555 $resolver->expects($this->any())
556 ->method('getArguments')
557 ->will($this->returnCallback(function ($callable) use ($route_match) {
558 return [$route_match->getRouteObject()];
567 * Defines an interface with a defined access() method for mocking.
569 interface TestAccessCheckInterface extends AccessCheckInterface {
570 public function access();