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