3 namespace Drupal\Tests\Core\Entity;
5 use Drupal\Core\DependencyInjection\ContainerBuilder;
6 use Drupal\Core\Entity\Entity;
7 use Drupal\Core\Entity\EntityMalformedException;
8 use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
9 use Drupal\Core\Entity\EntityTypeInterface;
10 use Drupal\Core\Entity\Exception\UndefinedLinkTemplateException;
11 use Drupal\Core\Entity\RevisionableInterface;
12 use Drupal\Core\GeneratedUrl;
13 use Drupal\Core\Routing\UrlGeneratorInterface;
15 use Drupal\Tests\UnitTestCase;
16 use Symfony\Component\Routing\Exception\MissingMandatoryParametersException;
19 * Tests URL handling of the \Drupal\Core\Entity\Entity class.
21 * @coversDefaultClass \Drupal\Core\Entity\Entity
25 class EntityUrlTest extends UnitTestCase {
28 * The entity type bundle info service mock used in this test.
30 * @var \Prophecy\Prophecy\ProphecyInterface|\Drupal\Core\Entity\EntityTypeBundleInfoInterface
32 protected $entityTypeBundleInfo;
35 * The ID of the entity type used in this test.
39 protected $entityTypeId = 'test_entity';
42 * The entity type mock used in this test.
44 * @var \Prophecy\Prophecy\ProphecyInterface|\Drupal\Core\Entity\EntityTypeInterface
46 protected $entityType;
49 * The ID of the entity used in this test.
53 protected $entityId = 1;
56 * The revision ID of the entity used in this test.
60 protected $revisionId = 2;
63 * The language code of the entity used in this test.
67 protected $langcode = 'en';
70 * Indicator for default revisions.
74 const DEFAULT_REVISION = TRUE;
77 * Indicator for non-default revisions.
81 const NON_DEFAULT_REVISION = FALSE;
84 * Indicator that the test entity type has no bundle key.
88 const HAS_NO_BUNDLE_KEY = FALSE;
91 * Indicator that the test entity type has a bundle key.
95 const HAS_BUNDLE_KEY = TRUE;
98 * Tests the toUrl() method without an entity ID.
102 public function testToUrlNoId() {
103 $entity = $this->getEntity(Entity::class, []);
105 $this->setExpectedException(EntityMalformedException::class, 'The "' . $this->entityTypeId . '" entity cannot have a URI as it does not have an ID');
110 * Tests the toUrl() method with simple link templates.
112 * @param string $link_template
113 * The link template to test.
114 * @param string $expected_route_name
115 * The expected route name of the generated URL.
117 * @dataProvider providerTestToUrlLinkTemplates
120 * @covers ::linkTemplates
121 * @covers ::urlRouteParameters
123 public function testToUrlLinkTemplates($link_template, $expected_route_name) {
124 $values = ['id' => $this->entityId, 'langcode' => $this->langcode];
125 $entity = $this->getEntity(Entity::class, $values);
126 $this->registerLinkTemplate($link_template);
128 /** @var \Drupal\Core\Url $url */
129 $url = $entity->toUrl($link_template);
130 // The entity ID is the sole route parameter for the link templates tested
132 $this->assertUrl($expected_route_name, ['test_entity' => $this->entityId], $entity, TRUE, $url);
136 * Provides data for testToUrlLinkTemplates().
139 * An array of test cases for testToUrlLinkTemplates().
141 public function providerTestToUrlLinkTemplates() {
144 $test_cases['canonical'] = ['canonical', 'entity.test_entity.canonical'];
145 $test_cases['version-history'] = ['version-history', 'entity.test_entity.version_history'];
146 $test_cases['edit-form'] = ['edit-form', 'entity.test_entity.edit_form'];
147 $test_cases['delete-form'] = ['delete-form', 'entity.test_entity.delete_form'];
148 $test_cases['revision'] = ['revision', 'entity.test_entity.revision'];
154 * Tests the toUrl() method with the 'revision' link template.
156 * @param bool $is_default_revision
157 * Whether or not the mock entity should be the default revision.
158 * @param string $link_template
159 * The link template to test.
160 * @param string $expected_route_name
161 * The expected route name of the generated URL.
162 * @param array $expected_route_parameters
163 * The expected route parameters of the generated URL.
165 * @dataProvider providerTestToUrlLinkTemplateRevision
168 * @covers ::linkTemplates
169 * @covers ::urlRouteParameters
171 public function testToUrlLinkTemplateRevision($is_default_revision, $link_template, $expected_route_name, array $expected_route_parameters) {
172 $values = ['id' => $this->entityId, 'langcode' => $this->langcode];
173 $entity = $this->getEntity(RevisionableEntity::class, $values);
174 $entity->method('getRevisionId')->willReturn($this->revisionId);
175 $entity->method('isDefaultRevision')->willReturn($is_default_revision);
176 $this->registerLinkTemplate($link_template);
177 // Even though this is tested with both the 'canonical' and the 'revision'
178 // template registered with the entity, we always ask for the 'revision'
179 // link template, to test that it falls back to the 'canonical' link
180 // template in case of the default revision.
181 /** @var \Drupal\Core\Url $url */
182 $url = $entity->toUrl('revision');
183 $this->assertUrl($expected_route_name, $expected_route_parameters, $entity, TRUE, $url);
188 * Provides data for testToUrlLinkTemplateRevision().
191 * An array of test cases for testToUrlLinkTemplateRevision().
193 public function providerTestToUrlLinkTemplateRevision() {
196 $route_parameters = ['test_entity' => $this->entityId];
197 $test_cases['default_revision'] = [static::DEFAULT_REVISION, 'canonical', 'entity.test_entity.canonical', $route_parameters];
198 // Add the revision ID to the expected route parameters.
199 $route_parameters['test_entity_revision'] = $this->revisionId;
200 $test_cases['non_default_revision'] = [static::NON_DEFAULT_REVISION, 'revision', 'entity.test_entity.revision', $route_parameters];
206 * Tests the toUrl() method with link templates without an entity ID.
208 * @param string $link_template
209 * The link template to test.
210 * @param string $expected_route_name
211 * The expected route name of the generated URL.
213 * @dataProvider providerTestToUrlLinkTemplateNoId
216 * @covers ::linkTemplates
217 * @covers ::urlRouteParameters
219 public function testToUrlLinkTemplateNoId($link_template, $expected_route_name) {
220 $entity = $this->getEntity(Entity::class, ['id' => $this->entityId]);
221 $this->registerLinkTemplate($link_template);
223 /** @var \Drupal\Core\Url $url */
224 $url = $entity->toUrl($link_template);
225 $this->assertUrl($expected_route_name, [], $entity, FALSE, $url);
229 * Provides data for testToUrlLinkTemplateNoId().
232 * An array of test cases for testToUrlLinkTemplateNoId().
234 public function providerTestToUrlLinkTemplateNoId() {
237 $test_cases['collection'] = ['collection', 'entity.test_entity.collection'];
238 $test_cases['add-page'] = ['add-page', 'entity.test_entity.add_page'];
244 * Tests the toUrl() method with the 'revision' link template.
246 * @param bool $has_bundle_key
247 * Whether or not the mock entity type should have a bundle key.
248 * @param string|null $bundle_entity_type
249 * The ID of the bundle entity type of the mock entity type, or NULL if the
250 * mock entity type should not have a bundle entity type.
251 * @param string $bundle_key
252 * The bundle key of the mock entity type or FALSE if the entity type should
253 * not have a bundle key.
254 * @param array $expected_route_parameters
255 * The expected route parameters of the generated URL.
257 * @dataProvider providerTestToUrlLinkTemplateAddForm
260 * @covers ::linkTemplates
261 * @covers ::urlRouteParameters
263 public function testToUrlLinkTemplateAddForm($has_bundle_key, $bundle_entity_type, $bundle_key, $expected_route_parameters) {
264 $values = ['id' => $this->entityId, 'langcode' => $this->langcode];
265 $entity = $this->getEntity(Entity::class, $values);
266 $this->entityType->hasKey('bundle')->willReturn($has_bundle_key);
267 $this->entityType->getBundleEntityType()->willReturn($bundle_entity_type);
268 $this->entityType->getKey('bundle')->willReturn($bundle_key);
269 $link_template = 'add-form';
270 $this->registerLinkTemplate($link_template);
272 /** @var \Drupal\Core\Url $url */
273 $url = $entity->toUrl($link_template);
274 $this->assertUrl('entity.test_entity.add_form', $expected_route_parameters, $entity, FALSE, $url);
278 * Provides data for testToUrlLinkTemplateAddForm().
281 * An array of test cases for testToUrlLinkTemplateAddForm().
283 public function providerTestToUrlLinkTemplateAddForm() {
286 $route_parameters = [];
287 $test_cases['no_bundle_key'] = [static::HAS_NO_BUNDLE_KEY, NULL, FALSE, $route_parameters];
289 $route_parameters = ['type' => $this->entityTypeId];
290 $test_cases['bundle_entity_type'] = [static::HAS_BUNDLE_KEY, 'type', FALSE, $route_parameters];
291 $test_cases['bundle_key'] = [static::HAS_BUNDLE_KEY, NULL, 'type', $route_parameters];
297 * Tests the toUrl() method with neither link templates nor a URI callback.
299 * @param array $bundle_info
300 * An array of bundle info to register.
301 * @param string $uri_callback
302 * The entity type URI callback to register.
304 * @dataProvider providerTestToUrlUriCallbackUndefined
307 * @covers ::linkTemplates
309 public function testToUrlUriCallbackUndefined(array $bundle_info, $uri_callback) {
310 $entity = $this->getEntity(Entity::class, ['id' => $this->entityId]);
312 $this->registerBundleInfo($bundle_info);
313 $this->entityType->getUriCallback()->willReturn($uri_callback);
315 $link_template = 'canonical';
316 $this->setExpectedException(UndefinedLinkTemplateException::class, "No link template '$link_template' found for the '$this->entityTypeId' entity type");
317 $entity->toUrl($link_template);
321 * Provides data for testToUrlUriCallbackUndefined().
324 * An array of test cases for testToUrlUriCallbackUndefined().
326 public function providerTestToUrlUriCallbackUndefined() {
329 $test_cases['no_callback'] = [[], NULL];
330 $test_cases['uri_callback'] = [[], 'not_a_callable'];
331 $test_cases['bundle_uri_callback'] = [['uri_callback' => 'not_a_callable'], NULL];
337 * Tests the toUrl() method with a URI callback.
339 * @param array $bundle_info
340 * An array of bundle info to register.
341 * @param string $uri_callback
342 * The entity type URI callback to register.
345 * @covers ::linkTemplates
347 * @dataProvider providerTestToUrlUriCallback
349 public function testToUrlUriCallback(array $bundle_info, $uri_callback) {
350 $entity = $this->getEntity(Entity::class, ['id' => $this->entityId, 'langcode' => $this->langcode]);
352 $this->registerBundleInfo($bundle_info);
353 $this->entityType->getUriCallback()->willReturn($uri_callback);
355 /** @var \Drupal\Core\Url $url */
356 $url = $entity->toUrl('canonical');
357 $this->assertUrl('<none>', [], $entity, TRUE, $url);
361 * Provides data for testToUrlUriCallback().
364 * An array of test cases for testToUrlUriCallback().
366 public function providerTestToUrlUriCallback() {
369 $uri_callback = function () {
370 return Url::fromRoute('<none>');
372 $test_cases['uri_callback'] = [[], $uri_callback];
373 $test_cases['bundle_uri_callback'] = [['uri_callback' => $uri_callback], NULL];
379 * Tests the urlInfo() method.
382 * The link relation to test.
383 * @param array $options
384 * An array of URL options to test with.
388 * @dataProvider providerTestUrlInfo
390 public function testUrlInfo($rel, $options) {
391 $entity = $this->getEntity(Entity::class, [], ['toUrl']);
392 $entity->expects($this->once())
394 ->with($rel, $options);
396 $entity->urlInfo($rel, $options);
400 * Provides data for testUrlInfo().
403 * An array of test cases for testUrlInfo().
405 public function providerTestUrlInfo() {
408 $test_cases['default'] = ['canonical', []];
409 $test_cases['with_option'] = ['canonical', ['absolute' => TRUE]];
410 $test_cases['revision'] = ['revision', []];
416 * Tests the url() method without an entity ID.
419 * The link relation to test.
422 * @covers ::hasLinkTemplate
423 * @covers ::linkTemplates
425 * @dataProvider providerTestUrl
427 public function testUrlEmpty($rel) {
428 $entity = $this->getEntity(Entity::class, []);
429 $this->assertEquals('', $entity->url($rel));
433 * Provides data for testUrlEmpty().
436 * An array of test cases for testUrlEmpty().
438 public function providerTestUrlEmpty() {
441 $test_cases['default'] = ['canonical', []];
442 $test_cases['revision'] = ['revision', []];
448 * Tests the url() method.
451 * The link relation to test.
452 * @param array $options
453 * An array of URL options to call url() with.
454 * @param array $default_options
455 * An array of URL options that toUrl() should generate.
456 * @param array $expected_options
457 * An array of combined URL options that should be set on the final URL.
460 * @covers ::hasLinkTemplate
461 * @covers ::linkTemplates
463 * @dataProvider providerTestUrl
465 public function testUrl($rel, $options, $default_options, $expected_options) {
466 $entity = $this->getEntity(Entity::class, ['id' => $this->entityId], ['toUrl']);
467 $this->registerLinkTemplate($rel);
469 $uri = $this->prophesize(Url::class);
470 $uri->getOptions()->willReturn($default_options);
471 $uri->setOptions($expected_options)->shouldBeCalled();
473 $url_string = "/test-entity/{$this->entityId}/$rel";
474 $uri->toString()->willReturn($url_string);
476 $entity->expects($this->once())
479 ->willReturn($uri->reveal());
481 $this->assertEquals($url_string, $entity->url($rel, $options));
485 * Provides data for testUrl().
488 * An array of test cases for testUrl().
490 public function providerTestUrl() {
493 $test_cases['default'] = ['canonical', [], [], []];
494 $test_cases['revision'] = ['revision', [], [], []];
495 $test_cases['option'] = ['canonical', ['absolute' => TRUE], [], ['absolute' => TRUE]];
496 $test_cases['default_option'] = ['canonical', [], ['absolute' => TRUE], ['absolute' => TRUE]];
497 $test_cases['option_merge'] = ['canonical', ['absolute' => TRUE], ['entity_type' => $this->entityTypeId], ['absolute' => TRUE, 'entity_type' => $this->entityTypeId]];
498 $test_cases['option_override'] = ['canonical', ['absolute' => TRUE], ['absolute' => FALSE], ['absolute' => TRUE]];
504 * Tests the uriRelationships() method.
506 * @covers ::uriRelationships
508 public function testUriRelationships() {
509 $entity = $this->getEntity(Entity::class, ['id' => $this->entityId]);
511 $container_builder = new ContainerBuilder();
512 $url_generator = $this->createMock(UrlGeneratorInterface::class);
513 $container_builder->set('url_generator', $url_generator);
514 \Drupal::setContainer($container_builder);
516 // Test route with no mandatory parameters.
517 $this->registerLinkTemplate('canonical');
518 $route_name_0 = 'entity.' . $this->entityTypeId . '.canonical';
519 $url_generator->expects($this->at(0))
520 ->method('generateFromRoute')
521 ->with($route_name_0)
522 ->willReturn((new GeneratedUrl())->setGeneratedUrl('/entity_test'));
523 $this->assertEquals(['canonical'], $entity->uriRelationships());
525 // Test route with non-default mandatory parameters.
526 $this->registerLinkTemplate('{non_default_parameter}');
527 $route_name_1 = 'entity.' . $this->entityTypeId . '.{non_default_parameter}';
528 $url_generator->expects($this->at(0))
529 ->method('generateFromRoute')
530 ->with($route_name_1)
531 ->willThrowException(new MissingMandatoryParametersException());
532 $this->assertEquals([], $entity->uriRelationships());
536 * Returns a mock entity for testing.
538 * @param string $class
539 * The class name to mock. Should be \Drupal\Core\Entity\Entity or a
541 * @param array $values
542 * An array of entity values to construct the mock entity with.
543 * @param array $methods
544 * (optional) An array of additional methods to mock on the entity object.
545 * The getEntityType() and entityManager() methods are always mocked.
547 * @return \Drupal\Core\Entity\Entity|\PHPUnit_Framework_MockObject_MockObject
549 protected function getEntity($class, array $values, array $methods = []) {
550 $methods = array_merge($methods, ['getEntityType', 'entityManager', 'entityTypeBundleInfo']);
552 // Prophecy does not allow prophesizing abstract classes while actually
553 // calling their code. We use Prophecy below because that allows us to
554 // add method prophecies later while still revealing the prophecy now.
555 $entity = $this->getMockBuilder($class)
556 ->setConstructorArgs([$values, $this->entityTypeId])
557 ->setMethods($methods)
558 ->getMockForAbstractClass();
560 $this->entityType = $this->prophesize(EntityTypeInterface::class);
561 $this->entityType->getLinkTemplates()->willReturn([]);
562 $this->entityType->getKey('langcode')->willReturn(FALSE);
563 $entity->method('getEntityType')->willReturn($this->entityType->reveal());
565 $this->entityTypeBundleInfo = $this->prophesize(EntityTypeBundleInfoInterface::class);
566 $entity->method('entityTypeBundleInfo')->willReturn($this->entityTypeBundleInfo->reveal());
572 * Asserts that a given URL object matches the expectations.
574 * @param string $expected_route_name
575 * The expected route name of the generated URL.
576 * @param array $expected_route_parameters
577 * The expected route parameters of the generated URL.
578 * @param \Drupal\Core\Entity\Entity|\PHPUnit_Framework_MockObject_MockObject $entity
579 * The entity that is expected to be set as a URL option.
580 * @param bool $has_language
581 * Whether or not the URL is expected to have a language option.
582 * @param \Drupal\Core\Url $url
583 * The URL option to make the assertions on.
585 protected function assertUrl($expected_route_name, array $expected_route_parameters, $entity, $has_language, Url $url) {
586 $this->assertEquals($expected_route_name, $url->getRouteName());
587 $this->assertEquals($expected_route_parameters, $url->getRouteParameters());
588 $this->assertEquals($this->entityTypeId, $url->getOption('entity_type'));
589 $this->assertEquals($entity, $url->getOption('entity'));
591 $this->assertEquals($this->langcode, $url->getOption('language')->getId());
594 $this->assertNull($url->getOption('language'));
599 * Registers a link template for the mock entity.
601 * @param string $link_template
602 * The link template to register.
604 protected function registerLinkTemplate($link_template) {
606 // The path is actually never used because we never invoke the URL
607 // generator but perform assertions on the URL object directly.
608 $link_template => "/test-entity/{test_entity}/$link_template",
610 $this->entityType->getLinkTemplates()->willReturn($link_templates);
614 * Registers bundle information for the mock entity type.
616 * @param array $bundle_info
617 * The bundle information to register.
619 protected function registerBundleInfo($bundle_info) {
620 $this->entityTypeBundleInfo
621 ->getBundleInfo($this->entityTypeId)
622 ->willReturn([$this->entityTypeId => $bundle_info]);
627 abstract class RevisionableEntity extends Entity implements RevisionableInterface {}