3 namespace Drupal\Tests\Core\Menu;
5 use Drupal\Core\Access\AccessResult;
6 use Drupal\Core\Cache\Context\CacheContextsManager;
7 use Drupal\Core\DependencyInjection\Container;
8 use Drupal\Core\Entity\EntityStorageInterface;
9 use Drupal\Core\Entity\EntityTypeManagerInterface;
10 use Drupal\Core\Menu\DefaultMenuLinkTreeManipulators;
11 use Drupal\Core\Menu\MenuLinkTreeElement;
12 use Drupal\Tests\UnitTestCase;
13 use Drupal\node\NodeInterface;
16 * Tests the default menu link tree manipulators.
20 * @coversDefaultClass \Drupal\Core\Menu\DefaultMenuLinkTreeManipulators
22 class DefaultMenuLinkTreeManipulatorsTest extends UnitTestCase {
25 * The mocked access manager.
27 * @var \Drupal\Core\Access\AccessManagerInterface|\PHPUnit_Framework_MockObject_MockObject
29 protected $accessManager;
32 * The mocked current user.
34 * @var \Drupal\Core\Session\AccountInterface|\PHPUnit_Framework_MockObject_MockObject
36 protected $currentUser;
39 * The mocked entity type manager.
41 * @var \Drupal\Core\Entity\EntityTypeManagerInterface|\PHPUnit_Framework_MockObject_MockObject
43 protected $entityTypeManager;
46 * The default menu link tree manipulators.
48 * @var \Drupal\Core\Menu\DefaultMenuLinkTreeManipulators
50 protected $defaultMenuTreeManipulators;
53 * The original menu tree build in mockTree().
55 * @var \Drupal\Core\Menu\MenuLinkTreeElement[]
57 protected $originalTree = [];
60 * Array of menu link instances
62 * @var \Drupal\Core\Menu\MenuLinkInterface[]
64 protected $links = [];
69 protected function setUp() {
72 $this->accessManager = $this->getMock('\Drupal\Core\Access\AccessManagerInterface');
73 $this->currentUser = $this->getMock('Drupal\Core\Session\AccountInterface');
74 $this->currentUser->method('isAuthenticated')
76 $this->entityTypeManager = $this->getMock(EntityTypeManagerInterface::class);
78 $this->defaultMenuTreeManipulators = new DefaultMenuLinkTreeManipulators($this->accessManager, $this->currentUser, $this->entityTypeManager);
80 $cache_contexts_manager = $this->prophesize(CacheContextsManager::class);
81 $cache_contexts_manager->assertValidTokens()->willReturn(TRUE);
82 $cache_contexts_manager->reveal();
84 $container = new Container();
85 $container->set('cache_contexts_manager', $cache_contexts_manager);
86 \Drupal::setContainer($container);
90 * Creates a mock tree.
92 * This mocks a tree with the following structure:
103 * With link 6 being the only external link.
105 protected function mockTree() {
107 1 => MenuLinkMock::create(['id' => 'test.example1', 'route_name' => 'example1', 'title' => 'foo', 'parent' => '']),
108 2 => MenuLinkMock::create(['id' => 'test.example2', 'route_name' => 'example2', 'title' => 'bar', 'parent' => 'test.example1', 'route_parameters' => ['foo' => 'bar']]),
109 3 => MenuLinkMock::create(['id' => 'test.example3', 'route_name' => 'example3', 'title' => 'baz', 'parent' => 'test.example2', 'route_parameters' => ['baz' => 'qux']]),
110 4 => MenuLinkMock::create(['id' => 'test.example4', 'route_name' => 'example4', 'title' => 'qux', 'parent' => 'test.example3']),
111 5 => MenuLinkMock::create(['id' => 'test.example5', 'route_name' => 'example5', 'title' => 'foofoo', 'parent' => '']),
112 6 => MenuLinkMock::create(['id' => 'test.example6', 'route_name' => '', 'url' => 'https://www.drupal.org/', 'title' => 'barbar', 'parent' => '']),
113 7 => MenuLinkMock::create(['id' => 'test.example7', 'route_name' => 'example7', 'title' => 'bazbaz', 'parent' => '']),
114 8 => MenuLinkMock::create(['id' => 'test.example8', 'route_name' => 'example8', 'title' => 'quxqux', 'parent' => '']),
115 9 => DynamicMenuLinkMock::create(['id' => 'test.example9', 'parent' => ''])->setCurrentUser($this->currentUser),
117 $this->originalTree = [];
118 $this->originalTree[1] = new MenuLinkTreeElement($this->links[1], FALSE, 1, FALSE, []);
119 $this->originalTree[2] = new MenuLinkTreeElement($this->links[2], TRUE, 1, FALSE, [
120 3 => new MenuLinkTreeElement($this->links[3], TRUE, 2, FALSE, [
121 4 => new MenuLinkTreeElement($this->links[4], FALSE, 3, FALSE, []),
124 $this->originalTree[5] = new MenuLinkTreeElement($this->links[5], TRUE, 1, FALSE, [
125 7 => new MenuLinkTreeElement($this->links[7], FALSE, 2, FALSE, []),
127 $this->originalTree[6] = new MenuLinkTreeElement($this->links[6], FALSE, 1, FALSE, []);
128 $this->originalTree[8] = new MenuLinkTreeElement($this->links[8], FALSE, 1, FALSE, []);
129 $this->originalTree[9] = new MenuLinkTreeElement($this->links[9], FALSE, 1, FALSE, []);
133 * Tests the generateIndexAndSort() tree manipulator.
135 * @covers ::generateIndexAndSort
137 public function testGenerateIndexAndSort() {
139 $tree = $this->originalTree;
140 $tree = $this->defaultMenuTreeManipulators->generateIndexAndSort($tree);
142 // Validate that parent elements #1, #2, #5 and #6 exist on the root level.
143 $this->assertEquals($this->links[1]->getPluginId(), $tree['50000 foo test.example1']->link->getPluginId());
144 $this->assertEquals($this->links[2]->getPluginId(), $tree['50000 bar test.example2']->link->getPluginId());
145 $this->assertEquals($this->links[5]->getPluginId(), $tree['50000 foofoo test.example5']->link->getPluginId());
146 $this->assertEquals($this->links[6]->getPluginId(), $tree['50000 barbar test.example6']->link->getPluginId());
147 $this->assertEquals($this->links[8]->getPluginId(), $tree['50000 quxqux test.example8']->link->getPluginId());
149 // Verify that child element #4 is at the correct location in the hierarchy.
150 $this->assertEquals($this->links[4]->getPluginId(), $tree['50000 bar test.example2']->subtree['50000 baz test.example3']->subtree['50000 qux test.example4']->link->getPluginId());
151 // Verify that child element #7 is at the correct location in the hierarchy.
152 $this->assertEquals($this->links[7]->getPluginId(), $tree['50000 foofoo test.example5']->subtree['50000 bazbaz test.example7']->link->getPluginId());
156 * Tests the checkAccess() tree manipulator.
158 * @covers ::checkAccess
159 * @covers ::menuLinkCheckAccess
161 public function testCheckAccess() {
162 // Those menu links that are non-external will have their access checks
163 // performed. 9 routes, but 1 is external, 2 already have their 'access'
164 // property set, and 1 is a child if an inaccessible menu link, so only 5
165 // calls will be made.
166 $this->accessManager->expects($this->exactly(5))
167 ->method('checkNamedRoute')
168 ->will($this->returnValueMap([
169 ['example1', [], $this->currentUser, TRUE, AccessResult::forbidden()],
170 ['example2', ['foo' => 'bar'], $this->currentUser, TRUE, AccessResult::allowed()->cachePerPermissions()],
171 ['example3', ['baz' => 'qux'], $this->currentUser, TRUE, AccessResult::neutral()],
172 ['example5', [], $this->currentUser, TRUE, AccessResult::allowed()],
173 ['user.logout', [], $this->currentUser, TRUE, AccessResult::allowed()],
177 $this->originalTree[5]->subtree[7]->access = AccessResult::neutral();
178 $this->originalTree[8]->access = AccessResult::allowed()->cachePerUser();
180 // Since \Drupal\Core\Menu\DefaultMenuLinkTreeManipulators::checkAccess()
181 // allows access to any link if the user has the 'link to any page'
182 // permission, *every* single access result is varied by permissions.
183 $tree = $this->defaultMenuTreeManipulators->checkAccess($this->originalTree);
185 // Menu link 1: route without parameters, access forbidden, but at level 0,
188 $this->assertEquals(AccessResult::forbidden()->cachePerPermissions(), $element->access);
189 $this->assertInstanceOf('\Drupal\Core\Menu\InaccessibleMenuLink', $element->link);
190 // Menu link 2: route with parameters, access granted.
192 $this->assertEquals(AccessResult::allowed()->cachePerPermissions(), $element->access);
193 $this->assertNotInstanceOf('\Drupal\Core\Menu\InaccessibleMenuLink', $element->link);
194 // Menu link 3: route with parameters, AccessResult::neutral(), top-level
195 // inaccessible link, hence kept for its cacheability metadata.
196 // Note that the permissions cache context is added automatically, because
197 // we always check the "link to any page" permission.
198 $element = $tree[2]->subtree[3];
199 $this->assertEquals(AccessResult::neutral()->cachePerPermissions(), $element->access);
200 $this->assertInstanceOf('\Drupal\Core\Menu\InaccessibleMenuLink', $element->link);
201 // Menu link 4: child of menu link 3, which was AccessResult::neutral(),
202 // hence menu link 3's subtree is removed, of which this menu link is one.
203 $this->assertFalse(array_key_exists(4, $tree[2]->subtree[3]->subtree));
204 // Menu link 5: no route name, treated as external, hence access granted.
206 $this->assertEquals(AccessResult::allowed()->cachePerPermissions(), $element->access);
207 $this->assertNotInstanceOf('\Drupal\Core\Menu\InaccessibleMenuLink', $element->link);
208 // Menu link 6: external URL, hence access granted.
210 $this->assertEquals(AccessResult::allowed()->cachePerPermissions(), $element->access);
211 $this->assertNotInstanceOf('\Drupal\Core\Menu\InaccessibleMenuLink', $element->link);
212 // Menu link 7: 'access' already set: AccessResult::neutral(), top-level
213 // inaccessible link, hence kept for its cacheability metadata.
214 // Note that unlike for menu link 3, the permission cache context is absent,
215 // because ::checkAccess() doesn't perform access checking when 'access' is
217 $element = $tree[5]->subtree[7];
218 $this->assertEquals(AccessResult::neutral(), $element->access);
219 $this->assertInstanceOf('\Drupal\Core\Menu\InaccessibleMenuLink', $element->link);
220 // Menu link 8: 'access' already set, note that 'per permissions' caching
223 $this->assertEquals(AccessResult::allowed()->cachePerUser(), $element->access);
224 $this->assertNotInstanceOf('\Drupal\Core\Menu\InaccessibleMenuLink', $element->link);
228 * Tests checkAccess() tree manipulator with 'link to any page' permission.
230 * @covers ::checkAccess
231 * @covers ::menuLinkCheckAccess
233 public function testCheckAccessWithLinkToAnyPagePermission() {
235 $this->currentUser->expects($this->exactly(9))
236 ->method('hasPermission')
237 ->with('link to any page')
241 $this->defaultMenuTreeManipulators->checkAccess($this->originalTree);
243 $expected_access_result = AccessResult::allowed()->cachePerPermissions();
244 $this->assertEquals($expected_access_result, $this->originalTree[1]->access);
245 $this->assertEquals($expected_access_result, $this->originalTree[2]->access);
246 $this->assertEquals($expected_access_result, $this->originalTree[2]->subtree[3]->access);
247 $this->assertEquals($expected_access_result, $this->originalTree[2]->subtree[3]->subtree[4]->access);
248 $this->assertEquals($expected_access_result, $this->originalTree[5]->subtree[7]->access);
249 $this->assertEquals($expected_access_result, $this->originalTree[6]->access);
250 $this->assertEquals($expected_access_result, $this->originalTree[8]->access);
251 $this->assertEquals($expected_access_result, $this->originalTree[9]->access);
255 * Tests the flatten() tree manipulator.
259 public function testFlatten() {
261 $tree = $this->defaultMenuTreeManipulators->flatten($this->originalTree);
262 $this->assertEquals([1, 2, 5, 6, 8, 9], array_keys($this->originalTree));
263 $this->assertEquals([1, 2, 5, 6, 8, 9, 3, 4, 7], array_keys($tree));
267 * Tests the optimized node access checking.
269 * @covers ::checkNodeAccess
270 * @covers ::collectNodeLinks
271 * @covers ::checkAccess
273 public function testCheckNodeAccess() {
275 1 => MenuLinkMock::create(['id' => 'node.1', 'route_name' => 'entity.node.canonical', 'title' => 'foo', 'parent' => '', 'route_parameters' => ['node' => 1]]),
276 2 => MenuLinkMock::create(['id' => 'node.2', 'route_name' => 'entity.node.canonical', 'title' => 'bar', 'parent' => '', 'route_parameters' => ['node' => 2]]),
277 3 => MenuLinkMock::create(['id' => 'node.3', 'route_name' => 'entity.node.canonical', 'title' => 'baz', 'parent' => 'node.2', 'route_parameters' => ['node' => 3]]),
278 4 => MenuLinkMock::create(['id' => 'node.4', 'route_name' => 'entity.node.canonical', 'title' => 'qux', 'parent' => 'node.3', 'route_parameters' => ['node' => 4]]),
279 5 => MenuLinkMock::create(['id' => 'test.1', 'route_name' => 'test_route', 'title' => 'qux', 'parent' => '']),
280 6 => MenuLinkMock::create(['id' => 'test.2', 'route_name' => 'test_route', 'title' => 'qux', 'parent' => 'test.1']),
283 $tree[1] = new MenuLinkTreeElement($links[1], FALSE, 1, FALSE, []);
284 $tree[2] = new MenuLinkTreeElement($links[2], TRUE, 1, FALSE, [
285 3 => new MenuLinkTreeElement($links[3], TRUE, 2, FALSE, [
286 4 => new MenuLinkTreeElement($links[4], FALSE, 3, FALSE, []),
289 $tree[5] = new MenuLinkTreeElement($links[5], TRUE, 1, FALSE, [
290 6 => new MenuLinkTreeElement($links[6], FALSE, 2, FALSE, []),
293 $query = $this->getMock('Drupal\Core\Entity\Query\QueryInterface');
294 $query->expects($this->at(0))
295 ->method('condition')
296 ->with('nid', [1, 2, 3, 4]);
297 $query->expects($this->at(1))
298 ->method('condition')
299 ->with('status', NodeInterface::PUBLISHED);
300 $query->expects($this->once())
302 ->willReturn([1, 2, 4]);
303 $storage = $this->getMock(EntityStorageInterface::class);
304 $storage->expects($this->once())
306 ->willReturn($query);
307 $this->entityTypeManager->expects($this->once())
308 ->method('getStorage')
310 ->willReturn($storage);
312 $node_access_result = AccessResult::allowed()->cachePerPermissions()->addCacheContexts(['user.node_grants:view']);
314 $tree = $this->defaultMenuTreeManipulators->checkNodeAccess($tree);
315 $this->assertEquals($node_access_result, $tree[1]->access);
316 $this->assertEquals($node_access_result, $tree[2]->access);
317 // Ensure that access denied is set.
318 $this->assertEquals(AccessResult::neutral(), $tree[2]->subtree[3]->access);
319 $this->assertEquals($node_access_result, $tree[2]->subtree[3]->subtree[4]->access);
320 // Ensure that other routes than entity.node.canonical are set as well.
321 $this->assertNull($tree[5]->access);
322 $this->assertNull($tree[5]->subtree[6]->access);
324 // On top of the node access checking now run the ordinary route based
327 // Ensure that the access manager is just called for the non-node routes.
328 $this->accessManager->expects($this->at(0))
329 ->method('checkNamedRoute')
330 ->with('test_route', [], $this->currentUser, TRUE)
331 ->willReturn(AccessResult::allowed());
332 $this->accessManager->expects($this->at(1))
333 ->method('checkNamedRoute')
334 ->with('test_route', [], $this->currentUser, TRUE)
335 ->willReturn(AccessResult::neutral());
336 $tree = $this->defaultMenuTreeManipulators->checkAccess($tree);
338 $this->assertEquals($node_access_result, $tree[1]->access);
339 $this->assertEquals($node_access_result, $tree[2]->access);
340 $this->assertEquals(AccessResult::neutral(), $tree[2]->subtree[3]->access);
341 $this->assertEquals(AccessResult::allowed()->cachePerPermissions(), $tree[5]->access);
342 $this->assertEquals(AccessResult::neutral()->cachePerPermissions(), $tree[5]->subtree[6]->access);