Version 1
[yaffs-website] / web / core / tests / Drupal / Tests / Core / Menu / DefaultMenuLinkTreeManipulatorsTest.php
1 <?php
2
3 namespace Drupal\Tests\Core\Menu;
4
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;
14
15 /**
16  * Tests the default menu link tree manipulators.
17  *
18  * @group Menu
19  *
20  * @coversDefaultClass \Drupal\Core\Menu\DefaultMenuLinkTreeManipulators
21  */
22 class DefaultMenuLinkTreeManipulatorsTest extends UnitTestCase {
23
24   /**
25    * The mocked access manager.
26    *
27    * @var \Drupal\Core\Access\AccessManagerInterface|\PHPUnit_Framework_MockObject_MockObject
28    */
29   protected $accessManager;
30
31   /**
32    * The mocked current user.
33    *
34    * @var \Drupal\Core\Session\AccountInterface|\PHPUnit_Framework_MockObject_MockObject
35    */
36   protected $currentUser;
37
38   /**
39    * The mocked entity type manager.
40    *
41    * @var \Drupal\Core\Entity\EntityTypeManagerInterface|\PHPUnit_Framework_MockObject_MockObject
42    */
43   protected $entityTypeManager;
44
45   /**
46    * The default menu link tree manipulators.
47    *
48    * @var \Drupal\Core\Menu\DefaultMenuLinkTreeManipulators
49    */
50   protected $defaultMenuTreeManipulators;
51
52   /**
53    * The original menu tree build in mockTree().
54    *
55    * @var \Drupal\Core\Menu\MenuLinkTreeElement[]
56    */
57   protected $originalTree = [];
58
59   /**
60    * Array of menu link instances
61    *
62    * @var \Drupal\Core\Menu\MenuLinkInterface[]
63    */
64   protected $links = [];
65
66   /**
67    * {@inheritdoc}
68    */
69   protected function setUp() {
70     parent::setUp();
71
72     $this->accessManager = $this->getMock('\Drupal\Core\Access\AccessManagerInterface');
73     $this->currentUser = $this->getMock('Drupal\Core\Session\AccountInterface');
74     $this->currentUser->method('isAuthenticated')
75       ->willReturn(TRUE);
76     $this->entityTypeManager = $this->getMock(EntityTypeManagerInterface::class);
77
78     $this->defaultMenuTreeManipulators = new DefaultMenuLinkTreeManipulators($this->accessManager, $this->currentUser, $this->entityTypeManager);
79
80     $cache_contexts_manager = $this->prophesize(CacheContextsManager::class);
81     $cache_contexts_manager->assertValidTokens()->willReturn(TRUE);
82     $cache_contexts_manager->reveal();
83
84     $container = new Container();
85     $container->set('cache_contexts_manager', $cache_contexts_manager);
86     \Drupal::setContainer($container);
87   }
88
89   /**
90    * Creates a mock tree.
91    *
92    * This mocks a tree with the following structure:
93    * - 1
94    * - 2
95    *   - 3
96    *     - 4
97    * - 5
98    *   - 7
99    * - 6
100    * - 8
101    * - 9
102    *
103    * With link 6 being the only external link.
104    */
105   protected function mockTree() {
106     $this->links = [
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),
116     ];
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, []),
122       ]),
123     ]);
124     $this->originalTree[5] = new MenuLinkTreeElement($this->links[5], TRUE, 1, FALSE, [
125       7 => new MenuLinkTreeElement($this->links[7], FALSE, 2, FALSE, []),
126     ]);
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, []);
130   }
131
132   /**
133    * Tests the generateIndexAndSort() tree manipulator.
134    *
135    * @covers ::generateIndexAndSort
136    */
137   public function testGenerateIndexAndSort() {
138     $this->mockTree();
139     $tree = $this->originalTree;
140     $tree = $this->defaultMenuTreeManipulators->generateIndexAndSort($tree);
141
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());
148
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());
153   }
154
155   /**
156    * Tests the checkAccess() tree manipulator.
157    *
158    * @covers ::checkAccess
159    * @covers ::menuLinkCheckAccess
160    */
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()],
174       ]));
175
176     $this->mockTree();
177     $this->originalTree[5]->subtree[7]->access = AccessResult::neutral();
178     $this->originalTree[8]->access = AccessResult::allowed()->cachePerUser();
179
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);
184
185     // Menu link 1: route without parameters, access forbidden, but at level 0,
186     // hence kept.
187     $element = $tree[1];
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.
191     $element = $tree[2];
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.
205     $element = $tree[5];
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.
209     $element = $tree[6];
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
216     // already set.
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
221     // is not added.
222     $element = $tree[8];
223     $this->assertEquals(AccessResult::allowed()->cachePerUser(), $element->access);
224     $this->assertNotInstanceOf('\Drupal\Core\Menu\InaccessibleMenuLink', $element->link);
225   }
226
227   /**
228    * Tests checkAccess() tree manipulator with 'link to any page' permission.
229    *
230    * @covers ::checkAccess
231    * @covers ::menuLinkCheckAccess
232    */
233   public function testCheckAccessWithLinkToAnyPagePermission() {
234     $this->mockTree();
235     $this->currentUser->expects($this->exactly(9))
236       ->method('hasPermission')
237       ->with('link to any page')
238       ->willReturn(TRUE);
239
240     $this->mockTree();
241     $this->defaultMenuTreeManipulators->checkAccess($this->originalTree);
242
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);
252   }
253
254   /**
255    * Tests the flatten() tree manipulator.
256    *
257    * @covers ::flatten
258    */
259   public function testFlatten() {
260     $this->mockTree();
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));
264   }
265
266   /**
267    * Tests the optimized node access checking.
268    *
269    * @covers ::checkNodeAccess
270    * @covers ::collectNodeLinks
271    * @covers ::checkAccess
272    */
273   public function testCheckNodeAccess() {
274     $links = [
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']),
281     ];
282     $tree = [];
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, []),
287       ]),
288     ]);
289     $tree[5] = new MenuLinkTreeElement($links[5], TRUE, 1, FALSE, [
290       6 => new MenuLinkTreeElement($links[6], FALSE, 2, FALSE, []),
291     ]);
292
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())
301       ->method('execute')
302       ->willReturn([1, 2, 4]);
303     $storage = $this->getMock(EntityStorageInterface::class);
304     $storage->expects($this->once())
305       ->method('getQuery')
306       ->willReturn($query);
307     $this->entityTypeManager->expects($this->once())
308       ->method('getStorage')
309       ->with('node')
310       ->willReturn($storage);
311
312     $node_access_result = AccessResult::allowed()->cachePerPermissions()->addCacheContexts(['user.node_grants:view']);
313
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);
323
324     // On top of the node access checking now run the ordinary route based
325     // access checkers.
326
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);
337
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);
343   }
344
345 }