1<?php
2
3namespace Drupal\Tests\Core\Entity;
4
5use Drupal\Core\Access\AccessResult;
6use Drupal\Core\Cache\Cache;
7use Drupal\Core\DependencyInjection\ContainerBuilder;
8use Drupal\Core\Entity\EntityStorageInterface;
9use Drupal\Core\Entity\EntityTypeManagerInterface;
10use Drupal\Core\Entity\EntityTypeRepositoryInterface;
11use Drupal\Core\Language\Language;
12use Drupal\entity_test\Entity\EntityTestMul;
13use Drupal\Tests\Traits\ExpectDeprecationTrait;
14use Drupal\Tests\UnitTestCase;
15
16/**
17 * @coversDefaultClass \Drupal\Core\Entity\Entity
18 * @group Entity
19 * @group Access
20 */
21class EntityUnitTest extends UnitTestCase {
22
23  use ExpectDeprecationTrait;
24
25  /**
26   * The entity under test.
27   *
28   * @var \Drupal\Core\Entity\Entity|\PHPUnit\Framework\MockObject\MockObject
29   */
30  protected $entity;
31
32  /**
33   * The entity type used for testing.
34   *
35   * @var \Drupal\Core\Entity\EntityTypeInterface|\PHPUnit\Framework\MockObject\MockObject
36   */
37  protected $entityType;
38
39  /**
40   * The entity type manager used for testing.
41   *
42   * @var \Drupal\Core\Entity\EntityTypeManagerInterface|\PHPUnit\Framework\MockObject\MockObject
43   */
44  protected $entityTypeManager;
45
46  /**
47   * The ID of the type of the entity under test.
48   *
49   * @var string
50   */
51  protected $entityTypeId;
52
53  /**
54   * The route provider used for testing.
55   *
56   * @var \Drupal\Core\Routing\RouteProvider|\PHPUnit\Framework\MockObject\MockObject
57   */
58  protected $routeProvider;
59
60  /**
61   * The UUID generator used for testing.
62   *
63   * @var \Drupal\Component\Uuid\UuidInterface|\PHPUnit\Framework\MockObject\MockObject
64   */
65  protected $uuid;
66
67  /**
68   * The language manager.
69   *
70   * @var \Drupal\Core\Language\LanguageManagerInterface|\PHPUnit\Framework\MockObject\MockObject
71   */
72  protected $languageManager;
73
74  /**
75   * The mocked cache tags invalidator.
76   *
77   * @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface|\PHPUnit\Framework\MockObject\MockObject
78   */
79  protected $cacheTagsInvalidator;
80
81  /**
82   * The entity values.
83   *
84   * @var array
85   */
86  protected $values;
87
88  /**
89   * {@inheritdoc}
90   */
91  protected function setUp() {
92    $this->values = [
93      'id' => 1,
94      'langcode' => 'en',
95      'uuid' => '3bb9ee60-bea5-4622-b89b-a63319d10b3a',
96    ];
97    $this->entityTypeId = $this->randomMachineName();
98
99    $this->entityType = $this->createMock('\Drupal\Core\Entity\EntityTypeInterface');
100    $this->entityType->expects($this->any())
101      ->method('getListCacheTags')
102      ->willReturn([$this->entityTypeId . '_list']);
103
104    $this->entityTypeManager = $this->getMockForAbstractClass(EntityTypeManagerInterface::class);
105    $this->entityTypeManager->expects($this->any())
106      ->method('getDefinition')
107      ->with($this->entityTypeId)
108      ->will($this->returnValue($this->entityType));
109
110    $this->uuid = $this->createMock('\Drupal\Component\Uuid\UuidInterface');
111
112    $this->languageManager = $this->createMock('\Drupal\Core\Language\LanguageManagerInterface');
113    $this->languageManager->expects($this->any())
114      ->method('getLanguage')
115      ->with('en')
116      ->will($this->returnValue(new Language(['id' => 'en'])));
117
118    $this->cacheTagsInvalidator = $this->createMock('Drupal\Core\Cache\CacheTagsInvalidator');
119
120    $container = new ContainerBuilder();
121    // Ensure that Entity doesn't use the deprecated entity.manager service.
122    $container->set('entity.manager', NULL);
123    $container->set('entity_type.manager', $this->entityTypeManager);
124    $container->set('uuid', $this->uuid);
125    $container->set('language_manager', $this->languageManager);
126    $container->set('cache_tags.invalidator', $this->cacheTagsInvalidator);
127    \Drupal::setContainer($container);
128
129    $this->entity = $this->getMockForAbstractClass('\Drupal\Core\Entity\EntityBase', [$this->values, $this->entityTypeId]);
130  }
131
132  /**
133   * @covers ::id
134   */
135  public function testId() {
136    $this->assertSame($this->values['id'], $this->entity->id());
137  }
138
139  /**
140   * @covers ::uuid
141   */
142  public function testUuid() {
143    $this->assertSame($this->values['uuid'], $this->entity->uuid());
144  }
145
146  /**
147   * @covers ::isNew
148   * @covers ::enforceIsNew
149   */
150  public function testIsNew() {
151    // We provided an ID, so the entity is not new.
152    $this->assertFalse($this->entity->isNew());
153    // Force it to be new.
154    $this->assertSame($this->entity, $this->entity->enforceIsNew());
155    $this->assertTrue($this->entity->isNew());
156  }
157
158  /**
159   * @covers ::getEntityType
160   */
161  public function testGetEntityType() {
162    $this->assertSame($this->entityType, $this->entity->getEntityType());
163  }
164
165  /**
166   * @covers ::bundle
167   */
168  public function testBundle() {
169    $this->assertSame($this->entityTypeId, $this->entity->bundle());
170  }
171
172  /**
173   * @covers ::label
174   * @group legacy
175   */
176  public function testLabel() {
177
178    $this->addExpectedDeprecationMessage('Entity type ' . $this->entityTypeId . ' defines a label callback. Support for that is deprecated in drupal:8.0.0 and will be removed in drupal:9.0.0. Override the EntityInterface::label() method instead. See https://www.drupal.org/node/3050794');
179
180    // Make a mock with one method that we use as the entity's uri_callback. We
181    // check that it is called, and that the entity's label is the callback's
182    // return value.
183    $callback_label = $this->randomMachineName();
184    $property_label = $this->randomMachineName();
185    $callback_container = $this->createMock(get_class());
186    $callback_container->expects($this->once())
187      ->method(__FUNCTION__)
188      ->will($this->returnValue($callback_label));
189    $this->entityType->expects($this->at(0))
190      ->method('get')
191      ->with('label_callback')
192      ->will($this->returnValue([$callback_container, __FUNCTION__]));
193    $this->entityType->expects($this->at(2))
194      ->method('getKey')
195      ->with('label')
196      ->will($this->returnValue('label'));
197
198    // Set a dummy property on the entity under test to test that the label can
199    // be returned form a property if there is no callback.
200    $this->entityTypeManager->expects($this->at(1))
201      ->method('getDefinition')
202      ->with($this->entityTypeId)
203      ->will($this->returnValue([
204        'entity_keys' => [
205          'label' => 'label',
206        ],
207      ]));
208    $this->entity->label = $property_label;
209
210    $this->assertSame($callback_label, $this->entity->label());
211    $this->assertSame($property_label, $this->entity->label());
212  }
213
214  /**
215   * @covers ::access
216   */
217  public function testAccess() {
218    $access = $this->createMock('\Drupal\Core\Entity\EntityAccessControlHandlerInterface');
219    $operation = $this->randomMachineName();
220    $access->expects($this->at(0))
221      ->method('access')
222      ->with($this->entity, $operation)
223      ->will($this->returnValue(AccessResult::allowed()));
224    $access->expects($this->at(1))
225      ->method('createAccess')
226      ->will($this->returnValue(AccessResult::allowed()));
227    $this->entityTypeManager->expects($this->exactly(2))
228      ->method('getAccessControlHandler')
229      ->will($this->returnValue($access));
230
231    $this->assertEquals(AccessResult::allowed(), $this->entity->access($operation));
232    $this->assertEquals(AccessResult::allowed(), $this->entity->access('create'));
233  }
234
235  /**
236   * @covers ::language
237   */
238  public function testLanguage() {
239    $this->entityType->expects($this->any())
240      ->method('getKey')
241      ->will($this->returnValueMap([
242        ['langcode', 'langcode'],
243      ]));
244    $this->assertSame('en', $this->entity->language()->getId());
245  }
246
247  /**
248   * Setup for the tests of the ::load() method.
249   */
250  public function setupTestLoad() {
251    // Base our mocked entity on a real entity class so we can test if calling
252    // Entity::load() on the base class will bubble up to an actual entity.
253    $this->entityTypeId = 'entity_test_mul';
254    $methods = get_class_methods(EntityTestMul::class);
255    unset($methods[array_search('load', $methods)]);
256    unset($methods[array_search('loadMultiple', $methods)]);
257    unset($methods[array_search('create', $methods)]);
258    $this->entity = $this->getMockBuilder(EntityTestMul::class)
259      ->disableOriginalConstructor()
260      ->setMethods($methods)
261      ->getMock();
262
263  }
264
265  /**
266   * @covers ::load
267   *
268   * Tests Entity::load() when called statically on a subclass of Entity.
269   */
270  public function testLoad() {
271    $this->setupTestLoad();
272
273    $class_name = get_class($this->entity);
274
275    $entity_type_repository = $this->getMockForAbstractClass(EntityTypeRepositoryInterface::class);
276    $entity_type_repository->expects($this->once())
277      ->method('getEntityTypeFromClass')
278      ->with($class_name)
279      ->willReturn($this->entityTypeId);
280
281    $storage = $this->createMock(EntityStorageInterface::class);
282    $storage->expects($this->once())
283      ->method('load')
284      ->with(1)
285      ->will($this->returnValue($this->entity));
286
287    $this->entityTypeManager->expects($this->once())
288      ->method('getStorage')
289      ->with($this->entityTypeId)
290      ->will($this->returnValue($storage));
291
292    \Drupal::getContainer()->set('entity_type.repository', $entity_type_repository);
293
294    // Call Entity::load statically and check that it returns the mock entity.
295    $this->assertSame($this->entity, $class_name::load(1));
296  }
297
298  /**
299   * @covers ::loadMultiple
300   *
301   * Tests Entity::loadMultiple() when called statically on a subclass of
302   * Entity.
303   */
304  public function testLoadMultiple() {
305    $this->setupTestLoad();
306
307    $class_name = get_class($this->entity);
308
309    $entity_type_repository = $this->getMockForAbstractClass(EntityTypeRepositoryInterface::class);
310    $entity_type_repository->expects($this->once())
311      ->method('getEntityTypeFromClass')
312      ->with($class_name)
313      ->willReturn($this->entityTypeId);
314
315    $storage = $this->createMock(EntityStorageInterface::class);
316    $storage->expects($this->once())
317      ->method('loadMultiple')
318      ->with([1])
319      ->will($this->returnValue([1 => $this->entity]));
320
321    $this->entityTypeManager->expects($this->once())
322      ->method('getStorage')
323      ->with($this->entityTypeId)
324      ->will($this->returnValue($storage));
325
326    \Drupal::getContainer()->set('entity_type.repository', $entity_type_repository);
327
328    // Call Entity::loadMultiple statically and check that it returns the mock
329    // entity.
330    $this->assertSame([1 => $this->entity], $class_name::loadMultiple([1]));
331  }
332
333  /**
334   * @covers ::create
335   */
336  public function testCreate() {
337    $this->setupTestLoad();
338
339    $class_name = get_class($this->entity);
340
341    $entity_type_repository = $this->getMockForAbstractClass(EntityTypeRepositoryInterface::class);
342    $entity_type_repository->expects($this->once())
343      ->method('getEntityTypeFromClass')
344      ->with($class_name)
345      ->willReturn($this->entityTypeId);
346
347    $storage = $this->createMock(EntityStorageInterface::class);
348    $storage->expects($this->once())
349      ->method('create')
350      ->with([])
351      ->will($this->returnValue($this->entity));
352
353    $this->entityTypeManager->expects($this->once())
354      ->method('getStorage')
355      ->with($this->entityTypeId)
356      ->will($this->returnValue($storage));
357
358    \Drupal::getContainer()->set('entity_type.repository', $entity_type_repository);
359
360    // Call Entity::create() statically and check that it returns the mock
361    // entity.
362    $this->assertSame($this->entity, $class_name::create([]));
363  }
364
365  /**
366   * @covers ::save
367   */
368  public function testSave() {
369    $storage = $this->createMock('\Drupal\Core\Entity\EntityStorageInterface');
370    $storage->expects($this->once())
371      ->method('save')
372      ->with($this->entity);
373
374    $this->entityTypeManager->expects($this->once())
375      ->method('getStorage')
376      ->with($this->entityTypeId)
377      ->will($this->returnValue($storage));
378
379    $this->entity->save();
380  }
381
382  /**
383   * @covers ::delete
384   */
385  public function testDelete() {
386    $this->entity->id = $this->randomMachineName();
387    $storage = $this->createMock('\Drupal\Core\Entity\EntityStorageInterface');
388    // Testing the argument of the delete() method consumes too much memory.
389    $storage->expects($this->once())
390      ->method('delete');
391
392    $this->entityTypeManager->expects($this->once())
393      ->method('getStorage')
394      ->with($this->entityTypeId)
395      ->will($this->returnValue($storage));
396
397    $this->entity->delete();
398  }
399
400  /**
401   * @covers ::getEntityTypeId
402   */
403  public function testGetEntityTypeId() {
404    $this->assertSame($this->entityTypeId, $this->entity->getEntityTypeId());
405  }
406
407  /**
408   * @covers ::preSave
409   */
410  public function testPreSave() {
411    // This method is internal, so check for errors on calling it only.
412    $storage = $this->createMock('\Drupal\Core\Entity\EntityStorageInterface');
413    // Our mocked entity->preSave() returns NULL, so assert that.
414    $this->assertNull($this->entity->preSave($storage));
415  }
416
417  /**
418   * @covers ::postSave
419   */
420  public function testPostSave() {
421    $this->cacheTagsInvalidator->expects($this->at(0))
422      ->method('invalidateTags')
423      ->with([
424        // List cache tag.
425        $this->entityTypeId . '_list',
426      ]);
427    $this->cacheTagsInvalidator->expects($this->at(1))
428      ->method('invalidateTags')
429      ->with([
430        // Own cache tag.
431        $this->entityTypeId . ':' . $this->values['id'],
432        // List cache tag.
433        $this->entityTypeId . '_list',
434      ]);
435
436    // This method is internal, so check for errors on calling it only.
437    $storage = $this->createMock('\Drupal\Core\Entity\EntityStorageInterface');
438
439    // A creation should trigger the invalidation of the "list" cache tag.
440    $this->entity->postSave($storage, FALSE);
441    // An update should trigger the invalidation of both the "list" and the
442    // "own" cache tags.
443    $this->entity->postSave($storage, TRUE);
444  }
445
446  /**
447   * @covers ::postSave
448   */
449  public function testPostSaveBundle() {
450    $this->cacheTagsInvalidator->expects($this->at(0))
451      ->method('invalidateTags')
452      ->with([
453        // List cache tag.
454        $this->entityTypeId . '_list',
455        $this->entityTypeId . '_list:' . $this->entity->bundle(),
456      ]);
457    $this->cacheTagsInvalidator->expects($this->at(1))
458      ->method('invalidateTags')
459      ->with([
460        // Own cache tag.
461        $this->entityTypeId . ':' . $this->values['id'],
462        // List cache tag.
463        $this->entityTypeId . '_list',
464        $this->entityTypeId . '_list:' . $this->entity->bundle(),
465      ]);
466
467    $this->entityType->expects($this->atLeastOnce())
468      ->method('hasKey')
469      ->with('bundle')
470      ->willReturn(TRUE);
471
472    // This method is internal, so check for errors on calling it only.
473    $storage = $this->createMock('\Drupal\Core\Entity\EntityStorageInterface');
474
475    // A creation should trigger the invalidation of the global list cache tag
476    // and the one for the bundle.
477    $this->entity->postSave($storage, FALSE);
478    // An update should trigger the invalidation of the "list", bundle list and
479    // the "own" cache tags.
480    $this->entity->postSave($storage, TRUE);
481  }
482
483  /**
484   * @covers ::preCreate
485   */
486  public function testPreCreate() {
487    // This method is internal, so check for errors on calling it only.
488    $storage = $this->createMock('\Drupal\Core\Entity\EntityStorageInterface');
489    $values = [];
490    // Our mocked entity->preCreate() returns NULL, so assert that.
491    $this->assertNull($this->entity->preCreate($storage, $values));
492  }
493
494  /**
495   * @covers ::postCreate
496   */
497  public function testPostCreate() {
498    // This method is internal, so check for errors on calling it only.
499    $storage = $this->createMock('\Drupal\Core\Entity\EntityStorageInterface');
500    // Our mocked entity->postCreate() returns NULL, so assert that.
501    $this->assertNull($this->entity->postCreate($storage));
502  }
503
504  /**
505   * @covers ::preDelete
506   */
507  public function testPreDelete() {
508    // This method is internal, so check for errors on calling it only.
509    $storage = $this->createMock('\Drupal\Core\Entity\EntityStorageInterface');
510    // Our mocked entity->preDelete() returns NULL, so assert that.
511    $this->assertNull($this->entity->preDelete($storage, [$this->entity]));
512  }
513
514  /**
515   * @covers ::postDelete
516   */
517  public function testPostDelete() {
518    $this->cacheTagsInvalidator->expects($this->once())
519      ->method('invalidateTags')
520      ->with([
521        $this->entityTypeId . ':' . $this->values['id'],
522        $this->entityTypeId . '_list',
523      ]);
524    $storage = $this->createMock('\Drupal\Core\Entity\EntityStorageInterface');
525    $storage->expects($this->once())
526      ->method('getEntityType')
527      ->willReturn($this->entityType);
528
529    $entities = [$this->values['id'] => $this->entity];
530    $this->entity->postDelete($storage, $entities);
531  }
532
533  /**
534   * @covers ::postDelete
535   */
536  public function testPostDeleteBundle() {
537    $this->cacheTagsInvalidator->expects($this->once())
538      ->method('invalidateTags')
539      ->with([
540        $this->entityTypeId . ':' . $this->values['id'],
541        $this->entityTypeId . '_list',
542        $this->entityTypeId . '_list:' . $this->entity->bundle(),
543      ]);
544    $this->entityType->expects($this->atLeastOnce())
545      ->method('hasKey')
546      ->with('bundle')
547      ->willReturn(TRUE);
548    $storage = $this->createMock('\Drupal\Core\Entity\EntityStorageInterface');
549    $storage->expects($this->once())
550      ->method('getEntityType')
551      ->willReturn($this->entityType);
552
553    $entities = [$this->values['id'] => $this->entity];
554    $this->entity->postDelete($storage, $entities);
555  }
556
557  /**
558   * @covers ::postLoad
559   */
560  public function testPostLoad() {
561    // This method is internal, so check for errors on calling it only.
562    $storage = $this->createMock('\Drupal\Core\Entity\EntityStorageInterface');
563    $entities = [$this->entity];
564    // Our mocked entity->postLoad() returns NULL, so assert that.
565    $this->assertNull($this->entity->postLoad($storage, $entities));
566  }
567
568  /**
569   * @covers ::referencedEntities
570   */
571  public function testReferencedEntities() {
572    $this->assertSame([], $this->entity->referencedEntities());
573  }
574
575  /**
576   * @covers ::getCacheTags
577   * @covers ::getCacheTagsToInvalidate
578   * @covers ::addCacheTags
579   */
580  public function testCacheTags() {
581    // Ensure that both methods return the same by default.
582    $this->assertEquals([$this->entityTypeId . ':' . 1], $this->entity->getCacheTags());
583    $this->assertEquals([$this->entityTypeId . ':' . 1], $this->entity->getCacheTagsToInvalidate());
584
585    // Add an additional cache tag and make sure only getCacheTags() returns
586    // that.
587    $this->entity->addCacheTags(['additional_cache_tag']);
588
589    // EntityTypeId is random so it can shift order. We need to duplicate the
590    // sort from \Drupal\Core\Cache\Cache::mergeTags().
591    $tags = ['additional_cache_tag', $this->entityTypeId . ':' . 1];
592    sort($tags);
593    $this->assertEquals($tags, $this->entity->getCacheTags());
594    $this->assertEquals([$this->entityTypeId . ':' . 1], $this->entity->getCacheTagsToInvalidate());
595  }
596
597  /**
598   * @covers ::getCacheContexts
599   * @covers ::addCacheContexts
600   */
601  public function testCacheContexts() {
602    $cache_contexts_manager = $this->getMockBuilder('Drupal\Core\Cache\Context\CacheContextsManager')
603      ->disableOriginalConstructor()
604      ->getMock();
605    $cache_contexts_manager->method('assertValidTokens')->willReturn(TRUE);
606
607    $container = new ContainerBuilder();
608    $container->set('cache_contexts_manager', $cache_contexts_manager);
609    \Drupal::setContainer($container);
610
611    // There are no cache contexts by default.
612    $this->assertEquals([], $this->entity->getCacheContexts());
613
614    // Add an additional cache context.
615    $this->entity->addCacheContexts(['user']);
616    $this->assertEquals(['user'], $this->entity->getCacheContexts());
617  }
618
619  /**
620   * @covers ::getCacheMaxAge
621   * @covers ::mergeCacheMaxAge
622   */
623  public function testCacheMaxAge() {
624    // Cache max age is permanent by default.
625    $this->assertEquals(Cache::PERMANENT, $this->entity->getCacheMaxAge());
626
627    // Set two cache max ages, the lower value is the one that needs to be
628    // returned.
629    $this->entity->mergeCacheMaxAge(600);
630    $this->entity->mergeCacheMaxAge(1800);
631    $this->assertEquals(600, $this->entity->getCacheMaxAge());
632  }
633
634}
635