447afbfa769cc20a112f2d6ac7e17129703410e2
[yaffs-website] / web / core / modules / system / tests / src / Functional / Entity / EntityCacheTagsTestBase.php
1 <?php
2
3 namespace Drupal\Tests\system\Functional\Entity;
4
5 use Drupal\Core\Cache\Cache;
6 use Drupal\Core\Entity\EntityInterface;
7 use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
8 use Drupal\Core\Field\FieldStorageDefinitionInterface;
9 use Drupal\Core\Language\LanguageInterface;
10 use Drupal\Core\Url;
11 use Drupal\field\Entity\FieldStorageConfig;
12 use Drupal\field\Entity\FieldConfig;
13 use Drupal\Tests\system\Functional\Cache\PageCacheTagsTestBase;
14 use Drupal\user\Entity\Role;
15 use Drupal\user\RoleInterface;
16
17 /**
18  * Provides helper methods for Entity cache tags tests.
19  */
20 abstract class EntityCacheTagsTestBase extends PageCacheTagsTestBase {
21
22   /**
23    * Modules to enable.
24    *
25    * @var array
26    */
27   public static $modules = ['entity_test', 'field_test'];
28
29   /**
30    * The main entity used for testing.
31    *
32    * @var \Drupal\Core\Entity\EntityInterface
33    */
34   protected $entity;
35
36   /**
37    * The entity instance referencing the main entity.
38    *
39    * @var \Drupal\Core\Entity\EntityInterface
40    */
41   protected $referencingEntity;
42
43   /**
44    * The entity instance not referencing the main entity.
45    *
46    * @var \Drupal\Core\Entity\EntityInterface
47    */
48   protected $nonReferencingEntity;
49
50   /**
51    * {@inheritdoc}
52    */
53   protected function setUp() {
54     parent::setUp();
55
56     // Give anonymous users permission to view test entities, so that we can
57     // verify the cache tags of cached versions of test entity pages.
58     $user_role = Role::load(RoleInterface::ANONYMOUS_ID);
59     $user_role->grantPermission('view test entity');
60     $user_role->save();
61
62     // Create an entity.
63     $this->entity = $this->createEntity();
64
65     // If this is an entity with field UI enabled, then add a configurable
66     // field. We will use this configurable field in later tests to ensure that
67     // field configuration invalidate render cache entries.
68     if ($this->entity->getEntityType()->get('field_ui_base_route')) {
69       // Add field, so we can modify the field storage and field entities to
70       // verify that changes to those indeed clear cache tags.
71       FieldStorageConfig::create([
72         'field_name' => 'configurable_field',
73         'entity_type' => $this->entity->getEntityTypeId(),
74         'type' => 'test_field',
75         'settings' => [],
76       ])->save();
77       FieldConfig::create([
78         'entity_type' => $this->entity->getEntityTypeId(),
79         'bundle' => $this->entity->bundle(),
80         'field_name' => 'configurable_field',
81         'label' => 'Configurable field',
82         'settings' => [],
83       ])->save();
84
85       // Reload the entity now that a new field has been added to it.
86       $storage = $this->container
87         ->get('entity.manager')
88         ->getStorage($this->entity->getEntityTypeId());
89       $storage->resetCache();
90       $this->entity = $storage->load($this->entity->id());
91     }
92
93     // Create a referencing and a non-referencing entity.
94     list(
95       $this->referencingEntity,
96       $this->nonReferencingEntity,
97     ) = $this->createReferenceTestEntities($this->entity);
98   }
99
100   /**
101    * Generates standardized entity cache tags test info.
102    *
103    * @param string $entity_type_label
104    *   The label of the entity type whose cache tags to test.
105    * @param string $group
106    *   The test group.
107    *
108    * @return array
109    *
110    * @see \Drupal\simpletest\TestBase::getInfo()
111    */
112   protected static function generateStandardizedInfo($entity_type_label, $group) {
113     return [
114       'name' => "$entity_type_label entity cache tags",
115       'description' => "Test the $entity_type_label entity's cache tags.",
116       'group' => $group,
117     ];
118   }
119
120   /**
121    * Creates the entity to be tested.
122    *
123    * @return \Drupal\Core\Entity\EntityInterface
124    *   The entity to be tested.
125    */
126   abstract protected function createEntity();
127
128   /**
129    * Returns the access cache contexts for the tested entity.
130    *
131    * Only list cache contexts that aren't part of the required cache contexts.
132    *
133    * @param \Drupal\Core\Entity\EntityInterface $entity
134    *   The entity to be tested, as created by createEntity().
135    *
136    * @return string[]
137    *   An array of the additional cache contexts.
138    *
139    * @see \Drupal\Core\Entity\EntityAccessControlHandlerInterface
140    */
141   protected function getAccessCacheContextsForEntity(EntityInterface $entity) {
142     return [];
143   }
144
145   /**
146    * Returns the additional (non-standard) cache contexts for the tested entity.
147    *
148    * Only list cache contexts that aren't part of the required cache contexts.
149    *
150    * @param \Drupal\Core\Entity\EntityInterface $entity
151    *   The entity to be tested, as created by createEntity().
152    *
153    * @return string[]
154    *   An array of the additional cache contexts.
155    *
156    * @see \Drupal\Tests\system\Functional\Entity\EntityCacheTagsTestBase::createEntity()
157    */
158   protected function getAdditionalCacheContextsForEntity(EntityInterface $entity) {
159     return [];
160   }
161
162   /**
163    * Returns the additional (non-standard) cache tags for the tested entity.
164    *
165    * @param \Drupal\Core\Entity\EntityInterface $entity
166    *   The entity to be tested, as created by createEntity().
167    * @return array
168    *   An array of the additional cache tags.
169    *
170    * @see \Drupal\Tests\system\Functional\Entity\EntityCacheTagsTestBase::createEntity()
171    */
172   protected function getAdditionalCacheTagsForEntity(EntityInterface $entity) {
173     return [];
174   }
175
176   /**
177    * Returns the additional cache tags for the tested entity's listing by type.
178    *
179    * @return string[]
180    *   An array of the additional cache contexts.
181    */
182   protected function getAdditionalCacheContextsForEntityListing() {
183     return [];
184   }
185
186   /**
187    * Returns the additional cache tags for the tested entity's listing by type.
188    *
189    * Necessary when there are unavoidable default entities of this type, e.g.
190    * the anonymous and administrator User entities always exist.
191    *
192    * @return array
193    *   An array of the additional cache tags.
194    */
195   protected function getAdditionalCacheTagsForEntityListing() {
196     return [];
197   }
198
199   /**
200    * Selects the preferred view mode for the given entity type.
201    *
202    * Prefers 'full', picks the first one otherwise, and if none are available,
203    * chooses 'default'.
204    */
205   protected function selectViewMode($entity_type) {
206     $view_modes = \Drupal::entityManager()
207       ->getStorage('entity_view_mode')
208       ->loadByProperties(['targetEntityType' => $entity_type]);
209
210     if (empty($view_modes)) {
211       return 'default';
212     }
213     else {
214       // Prefer the "full" display mode.
215       if (isset($view_modes[$entity_type . '.full'])) {
216         return 'full';
217       }
218       else {
219         $view_modes = array_keys($view_modes);
220         return substr($view_modes[0], strlen($entity_type) + 1);
221       }
222     }
223   }
224
225   /**
226    * Creates a referencing and a non-referencing entity for testing purposes.
227    *
228    * @param \Drupal\Core\Entity\EntityInterface $referenced_entity
229    *   The entity that the referencing entity should reference.
230    *
231    * @return \Drupal\Core\Entity\EntityInterface[]
232    *   An array containing a referencing entity and a non-referencing entity.
233    */
234   protected function createReferenceTestEntities($referenced_entity) {
235     // All referencing entities should be of the type 'entity_test'.
236     $entity_type = 'entity_test';
237
238     // Create a "foo" bundle for the given entity type.
239     $bundle = 'foo';
240     entity_test_create_bundle($bundle, NULL, $entity_type);
241
242     // Add a field of the given type to the given entity type's "foo" bundle.
243     $field_name = $referenced_entity->getEntityTypeId() . '_reference';
244     FieldStorageConfig::create([
245       'field_name' => $field_name,
246       'entity_type' => $entity_type,
247       'type' => 'entity_reference',
248       'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
249       'settings' => [
250         'target_type' => $referenced_entity->getEntityTypeId(),
251       ],
252     ])->save();
253     FieldConfig::create([
254       'field_name' => $field_name,
255       'entity_type' => $entity_type,
256       'bundle' => $bundle,
257       'settings' => [
258         'handler' => 'default',
259         'handler_settings' => [
260           'target_bundles' => [
261             $referenced_entity->bundle() => $referenced_entity->bundle(),
262           ],
263           'sort' => ['field' => '_none'],
264           'auto_create' => FALSE,
265         ],
266       ],
267     ])->save();
268     if (!$this->entity->getEntityType()->hasHandlerClass('view_builder')) {
269       entity_get_display($entity_type, $bundle, 'full')
270         ->setComponent($field_name, [
271           'type' => 'entity_reference_label',
272         ])
273         ->save();
274     }
275     else {
276       $referenced_entity_view_mode = $this->selectViewMode($this->entity->getEntityTypeId());
277       entity_get_display($entity_type, $bundle, 'full')
278         ->setComponent($field_name, [
279           'type' => 'entity_reference_entity_view',
280           'settings' => [
281             'view_mode' => $referenced_entity_view_mode,
282           ],
283         ])
284         ->save();
285     }
286
287     // Create an entity that does reference the entity being tested.
288     $label_key = \Drupal::entityManager()->getDefinition($entity_type)->getKey('label');
289     $referencing_entity = $this->container->get('entity_type.manager')
290       ->getStorage($entity_type)
291       ->create([
292         $label_key => 'Referencing ' . $entity_type,
293         'status' => 1,
294         'type' => $bundle,
295         $field_name => ['target_id' => $referenced_entity->id()],
296       ]);
297     $referencing_entity->save();
298
299     // Create an entity that does not reference the entity being tested.
300     $non_referencing_entity = $this->container->get('entity_type.manager')
301       ->getStorage($entity_type)
302       ->create([
303         $label_key => 'Non-referencing ' . $entity_type,
304         'status' => 1,
305         'type' => $bundle,
306       ]);
307     $non_referencing_entity->save();
308
309     return [
310       $referencing_entity,
311       $non_referencing_entity,
312     ];
313   }
314
315   /**
316    * Tests cache tags presence and invalidation of the entity when referenced.
317    *
318    * Tests the following cache tags:
319    * - entity type view cache tag: "<entity type>_view"
320    * - entity cache tag: "<entity type>:<entity ID>"
321    * - entity type list cache tag: "<entity type>_list"
322    * - referencing entity type view cache tag: "<referencing entity type>_view"
323    * - referencing entity type cache tag: "<referencing entity type>:<referencing entity ID>"
324    */
325   public function testReferencedEntity() {
326     $entity_type = $this->entity->getEntityTypeId();
327     $referencing_entity_url = $this->referencingEntity->urlInfo('canonical');
328     $non_referencing_entity_url = $this->nonReferencingEntity->urlInfo('canonical');
329     $listing_url = Url::fromRoute('entity.entity_test.collection_referencing_entities', [
330       'entity_reference_field_name' => $entity_type . '_reference',
331       'referenced_entity_type' => $entity_type,
332       'referenced_entity_id' => $this->entity->id(),
333     ]);
334     $empty_entity_listing_url = Url::fromRoute('entity.entity_test.collection_empty', ['entity_type_id' => $entity_type]);
335     $nonempty_entity_listing_url = Url::fromRoute('entity.entity_test.collection_labels_alphabetically', ['entity_type_id' => $entity_type]);
336
337     // The default cache contexts for rendered entities.
338     $default_cache_contexts = ['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'user.permissions'];
339     $entity_cache_contexts = $default_cache_contexts;
340     $page_cache_contexts = Cache::mergeContexts($default_cache_contexts, ['url.query_args:' . MainContentViewSubscriber::WRAPPER_FORMAT]);
341
342     // Cache tags present on every rendered page.
343     // 'user.permissions' is a required cache context, and responses that vary
344     // by this cache context when requested by anonymous users automatically
345     // also get this cache tag, to ensure correct invalidation.
346     $page_cache_tags = Cache::mergeTags(['http_response', 'rendered'], ['config:user.role.anonymous']);
347     // If the block module is used, the Block page display variant is used,
348     // which adds the block config entity type's list cache tags.
349     $page_cache_tags = Cache::mergeTags($page_cache_tags, \Drupal::moduleHandler()->moduleExists('block') ? ['config:block_list'] : []);
350
351     $page_cache_tags_referencing_entity = in_array('user.permissions', $this->getAccessCacheContextsForEntity($this->referencingEntity)) ? ['config:user.role.anonymous'] : [];
352
353     $view_cache_tag = [];
354     if ($this->entity->getEntityType()->hasHandlerClass('view_builder')) {
355       $view_cache_tag = \Drupal::entityManager()->getViewBuilder($entity_type)
356         ->getCacheTags();
357     }
358
359     $context_metadata = \Drupal::service('cache_contexts_manager')->convertTokensToKeys($entity_cache_contexts);
360     $cache_context_tags = $context_metadata->getCacheTags();
361
362     // Generate the cache tags for the (non) referencing entities.
363     $referencing_entity_cache_tags = Cache::mergeTags($this->referencingEntity->getCacheTags(), \Drupal::entityManager()->getViewBuilder('entity_test')->getCacheTags());
364     // Includes the main entity's cache tags, since this entity references it.
365     $referencing_entity_cache_tags = Cache::mergeTags($referencing_entity_cache_tags, $this->entity->getCacheTags());
366     $referencing_entity_cache_tags = Cache::mergeTags($referencing_entity_cache_tags, $this->getAdditionalCacheTagsForEntity($this->entity));
367     $referencing_entity_cache_tags = Cache::mergeTags($referencing_entity_cache_tags, $view_cache_tag);
368     $referencing_entity_cache_tags = Cache::mergeTags($referencing_entity_cache_tags, $cache_context_tags);
369     $referencing_entity_cache_tags = Cache::mergeTags($referencing_entity_cache_tags, ['rendered']);
370
371     $non_referencing_entity_cache_tags = Cache::mergeTags($this->nonReferencingEntity->getCacheTags(), \Drupal::entityManager()->getViewBuilder('entity_test')->getCacheTags());
372     $non_referencing_entity_cache_tags = Cache::mergeTags($non_referencing_entity_cache_tags, ['rendered']);
373
374     // Generate the cache tags for all two possible entity listing paths.
375     // 1. list cache tag only (listing query has no match)
376     // 2. list cache tag plus entity cache tag (listing query has a match)
377     $empty_entity_listing_cache_tags = Cache::mergeTags($this->entity->getEntityType()->getListCacheTags(), $page_cache_tags);
378
379     $nonempty_entity_listing_cache_tags = Cache::mergeTags($this->entity->getEntityType()->getListCacheTags(), $this->entity->getCacheTags());
380     $nonempty_entity_listing_cache_tags = Cache::mergeTags($nonempty_entity_listing_cache_tags, $this->getAdditionalCacheTagsForEntityListing($this->entity));
381     $nonempty_entity_listing_cache_tags = Cache::mergeTags($nonempty_entity_listing_cache_tags, $page_cache_tags);
382
383     $this->pass("Test referencing entity.", 'Debug');
384     $this->verifyPageCache($referencing_entity_url, 'MISS');
385
386     // Verify a cache hit, but also the presence of the correct cache tags.
387     $expected_tags = Cache::mergeTags($referencing_entity_cache_tags, $page_cache_tags);
388     $expected_tags = Cache::mergeTags($expected_tags, $page_cache_tags_referencing_entity);
389     $this->verifyPageCache($referencing_entity_url, 'HIT', $expected_tags);
390
391     // Also verify the existence of an entity render cache entry.
392     $cache_keys = ['entity_view', 'entity_test', $this->referencingEntity->id(), 'full'];
393     $cid = $this->createCacheId($cache_keys, $entity_cache_contexts);
394     $access_cache_contexts = $this->getAccessCacheContextsForEntity($this->entity);
395     $additional_cache_contexts = $this->getAdditionalCacheContextsForEntity($this->referencingEntity);
396     $redirected_cid = NULL;
397     if (count($access_cache_contexts) || count($additional_cache_contexts)) {
398       $cache_contexts = Cache::mergeContexts($entity_cache_contexts, $additional_cache_contexts);
399       $cache_contexts = Cache::mergeContexts($cache_contexts, $access_cache_contexts);
400       $redirected_cid = $this->createCacheId($cache_keys, $cache_contexts);
401       $context_metadata = \Drupal::service('cache_contexts_manager')->convertTokensToKeys($cache_contexts);
402       $referencing_entity_cache_tags = Cache::mergeTags($referencing_entity_cache_tags, $context_metadata->getCacheTags());
403     }
404     $this->verifyRenderCache($cid, $referencing_entity_cache_tags, $redirected_cid);
405
406     $this->pass("Test non-referencing entity.", 'Debug');
407     $this->verifyPageCache($non_referencing_entity_url, 'MISS');
408     // Verify a cache hit, but also the presence of the correct cache tags.
409     $this->verifyPageCache($non_referencing_entity_url, 'HIT', Cache::mergeTags($non_referencing_entity_cache_tags, $page_cache_tags));
410     // Also verify the existence of an entity render cache entry.
411     $cache_keys = ['entity_view', 'entity_test', $this->nonReferencingEntity->id(), 'full'];
412     $cid = $this->createCacheId($cache_keys, $entity_cache_contexts);
413     $this->verifyRenderCache($cid, $non_referencing_entity_cache_tags);
414
415     $this->pass("Test listing of referencing entities.", 'Debug');
416     // Prime the page cache for the listing of referencing entities.
417     $this->verifyPageCache($listing_url, 'MISS');
418
419     // Verify a cache hit, but also the presence of the correct cache tags.
420     $expected_tags = Cache::mergeTags($referencing_entity_cache_tags, $page_cache_tags);
421     $expected_tags = Cache::mergeTags($expected_tags, $page_cache_tags_referencing_entity);
422     $this->verifyPageCache($listing_url, 'HIT', $expected_tags);
423
424     $this->pass("Test empty listing.", 'Debug');
425     // Prime the page cache for the empty listing.
426     $this->verifyPageCache($empty_entity_listing_url, 'MISS');
427     // Verify a cache hit, but also the presence of the correct cache tags.
428     $this->verifyPageCache($empty_entity_listing_url, 'HIT', $empty_entity_listing_cache_tags);
429     // Verify the entity type's list cache contexts are present.
430     $contexts_in_header = $this->drupalGetHeader('X-Drupal-Cache-Contexts');
431     $this->assertEqual(Cache::mergeContexts($page_cache_contexts, $this->getAdditionalCacheContextsForEntityListing()), empty($contexts_in_header) ? [] : explode(' ', $contexts_in_header));
432
433     $this->pass("Test listing containing referenced entity.", 'Debug');
434     // Prime the page cache for the listing containing the referenced entity.
435     $this->verifyPageCache($nonempty_entity_listing_url, 'MISS', $nonempty_entity_listing_cache_tags);
436     // Verify a cache hit, but also the presence of the correct cache tags.
437     $this->verifyPageCache($nonempty_entity_listing_url, 'HIT', $nonempty_entity_listing_cache_tags);
438     // Verify the entity type's list cache contexts are present.
439     $contexts_in_header = $this->drupalGetHeader('X-Drupal-Cache-Contexts');
440     $this->assertEqual(Cache::mergeContexts($page_cache_contexts, $this->getAdditionalCacheContextsForEntityListing()), empty($contexts_in_header) ? [] : explode(' ', $contexts_in_header));
441
442     // Verify that after modifying the referenced entity, there is a cache miss
443     // for every route except the one for the non-referencing entity.
444     $this->pass("Test modification of referenced entity.", 'Debug');
445     $this->entity->save();
446     $this->verifyPageCache($referencing_entity_url, 'MISS');
447     $this->verifyPageCache($listing_url, 'MISS');
448     $this->verifyPageCache($empty_entity_listing_url, 'MISS');
449     $this->verifyPageCache($nonempty_entity_listing_url, 'MISS');
450     $this->verifyPageCache($non_referencing_entity_url, 'HIT');
451
452     // Verify cache hits.
453     $this->verifyPageCache($referencing_entity_url, 'HIT');
454     $this->verifyPageCache($listing_url, 'HIT');
455     $this->verifyPageCache($empty_entity_listing_url, 'HIT');
456     $this->verifyPageCache($nonempty_entity_listing_url, 'HIT');
457
458     // Verify that after modifying the referencing entity, there is a cache miss
459     // for every route except the ones for the non-referencing entity and the
460     // empty entity listing.
461     $this->pass("Test modification of referencing entity.", 'Debug');
462     $this->referencingEntity->save();
463     $this->verifyPageCache($referencing_entity_url, 'MISS');
464     $this->verifyPageCache($listing_url, 'MISS');
465     $this->verifyPageCache($nonempty_entity_listing_url, 'HIT');
466     $this->verifyPageCache($non_referencing_entity_url, 'HIT');
467     $this->verifyPageCache($empty_entity_listing_url, 'HIT');
468
469     // Verify cache hits.
470     $this->verifyPageCache($referencing_entity_url, 'HIT');
471     $this->verifyPageCache($listing_url, 'HIT');
472     $this->verifyPageCache($nonempty_entity_listing_url, 'HIT');
473
474     // Verify that after modifying the non-referencing entity, there is a cache
475     // miss only for the non-referencing entity route.
476     $this->pass("Test modification of non-referencing entity.", 'Debug');
477     $this->nonReferencingEntity->save();
478     $this->verifyPageCache($referencing_entity_url, 'HIT');
479     $this->verifyPageCache($listing_url, 'HIT');
480     $this->verifyPageCache($empty_entity_listing_url, 'HIT');
481     $this->verifyPageCache($nonempty_entity_listing_url, 'HIT');
482     $this->verifyPageCache($non_referencing_entity_url, 'MISS');
483
484     // Verify cache hits.
485     $this->verifyPageCache($non_referencing_entity_url, 'HIT');
486
487     if ($this->entity->getEntityType()->hasHandlerClass('view_builder')) {
488       // Verify that after modifying the entity's display, there is a cache miss
489       // for both the referencing entity, and the listing of referencing
490       // entities, but not for any other routes.
491       $referenced_entity_view_mode = $this->selectViewMode($this->entity->getEntityTypeId());
492       $this->pass("Test modification of referenced entity's '$referenced_entity_view_mode' display.", 'Debug');
493       $entity_display = entity_get_display($entity_type, $this->entity->bundle(), $referenced_entity_view_mode);
494       $entity_display->save();
495       $this->verifyPageCache($referencing_entity_url, 'MISS');
496       $this->verifyPageCache($listing_url, 'MISS');
497       $this->verifyPageCache($non_referencing_entity_url, 'HIT');
498       $this->verifyPageCache($empty_entity_listing_url, 'HIT');
499       $this->verifyPageCache($nonempty_entity_listing_url, 'HIT');
500
501       // Verify cache hits.
502       $this->verifyPageCache($referencing_entity_url, 'HIT');
503       $this->verifyPageCache($listing_url, 'HIT');
504     }
505
506     if ($bundle_entity_type_id = $this->entity->getEntityType()->getBundleEntityType()) {
507       // Verify that after modifying the corresponding bundle entity, there is a
508       // cache miss for both the referencing entity, and the listing of
509       // referencing entities, but not for any other routes.
510       $this->pass("Test modification of referenced entity's bundle entity.", 'Debug');
511       $bundle_entity = $this->container->get('entity_type.manager')
512         ->getStorage($bundle_entity_type_id)
513         ->load($this->entity->bundle());
514       $bundle_entity->save();
515       $this->verifyPageCache($referencing_entity_url, 'MISS');
516       $this->verifyPageCache($listing_url, 'MISS');
517       $this->verifyPageCache($non_referencing_entity_url, 'HIT');
518       // Special case: entity types may choose to use their bundle entity type
519       // cache tags, to avoid having excessively granular invalidation.
520       $is_special_case = $bundle_entity->getCacheTags() == $this->entity->getCacheTags() && $bundle_entity->getEntityType()->getListCacheTags() == $this->entity->getEntityType()->getListCacheTags();
521       if ($is_special_case) {
522         $this->verifyPageCache($empty_entity_listing_url, 'MISS');
523         $this->verifyPageCache($nonempty_entity_listing_url, 'MISS');
524       }
525       else {
526         $this->verifyPageCache($empty_entity_listing_url, 'HIT');
527         $this->verifyPageCache($nonempty_entity_listing_url, 'HIT');
528       }
529
530       // Verify cache hits.
531       $this->verifyPageCache($referencing_entity_url, 'HIT');
532       $this->verifyPageCache($listing_url, 'HIT');
533       if ($is_special_case) {
534         $this->verifyPageCache($empty_entity_listing_url, 'HIT');
535         $this->verifyPageCache($nonempty_entity_listing_url, 'HIT');
536       }
537     }
538
539     if ($this->entity->getEntityType()->get('field_ui_base_route')) {
540       // Verify that after modifying a configurable field on the entity, there
541       // is a cache miss.
542       $this->pass("Test modification of referenced entity's configurable field.", 'Debug');
543       $field_storage_name = $this->entity->getEntityTypeId() . '.configurable_field';
544       $field_storage = FieldStorageConfig::load($field_storage_name);
545       $field_storage->save();
546       $this->verifyPageCache($referencing_entity_url, 'MISS');
547       $this->verifyPageCache($listing_url, 'MISS');
548       $this->verifyPageCache($empty_entity_listing_url, 'HIT');
549       $this->verifyPageCache($nonempty_entity_listing_url, 'HIT');
550       $this->verifyPageCache($non_referencing_entity_url, 'HIT');
551
552       // Verify cache hits.
553       $this->verifyPageCache($referencing_entity_url, 'HIT');
554       $this->verifyPageCache($listing_url, 'HIT');
555
556       // Verify that after modifying a configurable field on the entity, there
557       // is a cache miss.
558       $this->pass("Test modification of referenced entity's configurable field.", 'Debug');
559       $field_name = $this->entity->getEntityTypeId() . '.' . $this->entity->bundle() . '.configurable_field';
560       $field = FieldConfig::load($field_name);
561       $field->save();
562       $this->verifyPageCache($referencing_entity_url, 'MISS');
563       $this->verifyPageCache($listing_url, 'MISS');
564       $this->verifyPageCache($empty_entity_listing_url, 'HIT');
565       $this->verifyPageCache($nonempty_entity_listing_url, 'HIT');
566       $this->verifyPageCache($non_referencing_entity_url, 'HIT');
567
568       // Verify cache hits.
569       $this->verifyPageCache($referencing_entity_url, 'HIT');
570       $this->verifyPageCache($listing_url, 'HIT');
571     }
572
573     // Verify that after invalidating the entity's cache tag directly, there is
574     // a cache miss for every route except the ones for the non-referencing
575     // entity and the empty entity listing.
576     $this->pass("Test invalidation of referenced entity's cache tag.", 'Debug');
577     Cache::invalidateTags($this->entity->getCacheTagsToInvalidate());
578     $this->verifyPageCache($referencing_entity_url, 'MISS');
579     $this->verifyPageCache($listing_url, 'MISS');
580     $this->verifyPageCache($nonempty_entity_listing_url, 'MISS');
581     $this->verifyPageCache($non_referencing_entity_url, 'HIT');
582     $this->verifyPageCache($empty_entity_listing_url, 'HIT');
583
584     // Verify cache hits.
585     $this->verifyPageCache($referencing_entity_url, 'HIT');
586     $this->verifyPageCache($listing_url, 'HIT');
587     $this->verifyPageCache($nonempty_entity_listing_url, 'HIT');
588
589     // Verify that after invalidating the entity's list cache tag directly,
590     // there is a cache miss for both the empty entity listing and the non-empty
591     // entity listing routes, but not for other routes.
592     $this->pass("Test invalidation of referenced entity's list cache tag.", 'Debug');
593     Cache::invalidateTags($this->entity->getEntityType()->getListCacheTags());
594     $this->verifyPageCache($empty_entity_listing_url, 'MISS');
595     $this->verifyPageCache($nonempty_entity_listing_url, 'MISS');
596     $this->verifyPageCache($referencing_entity_url, 'HIT');
597     $this->verifyPageCache($non_referencing_entity_url, 'HIT');
598     $this->verifyPageCache($listing_url, 'HIT');
599
600     // Verify cache hits.
601     $this->verifyPageCache($empty_entity_listing_url, 'HIT');
602     $this->verifyPageCache($nonempty_entity_listing_url, 'HIT');
603
604     if (!empty($view_cache_tag)) {
605       // Verify that after invalidating the generic entity type's view cache tag
606       // directly, there is a cache miss for both the referencing entity, and the
607       // listing of referencing entities, but not for other routes.
608       $this->pass("Test invalidation of referenced entity's 'view' cache tag.", 'Debug');
609       Cache::invalidateTags($view_cache_tag);
610       $this->verifyPageCache($referencing_entity_url, 'MISS');
611       $this->verifyPageCache($listing_url, 'MISS');
612       $this->verifyPageCache($non_referencing_entity_url, 'HIT');
613       $this->verifyPageCache($empty_entity_listing_url, 'HIT');
614       $this->verifyPageCache($nonempty_entity_listing_url, 'HIT');
615
616       // Verify cache hits.
617       $this->verifyPageCache($referencing_entity_url, 'HIT');
618       $this->verifyPageCache($listing_url, 'HIT');
619     }
620
621     // Verify that after deleting the entity, there is a cache miss for every
622     // route except for the non-referencing entity one.
623     $this->pass('Test deletion of referenced entity.', 'Debug');
624     $this->entity->delete();
625     $this->verifyPageCache($referencing_entity_url, 'MISS');
626     $this->verifyPageCache($listing_url, 'MISS');
627     $this->verifyPageCache($empty_entity_listing_url, 'MISS');
628     $this->verifyPageCache($nonempty_entity_listing_url, 'MISS');
629     $this->verifyPageCache($non_referencing_entity_url, 'HIT');
630
631     // Verify cache hits.
632     $referencing_entity_cache_tags = Cache::mergeTags($this->referencingEntity->getCacheTags(), \Drupal::entityManager()->getViewBuilder('entity_test')->getCacheTags());
633     $referencing_entity_cache_tags = Cache::mergeTags($referencing_entity_cache_tags, ['http_response', 'rendered']);
634
635     $nonempty_entity_listing_cache_tags = Cache::mergeTags($this->entity->getEntityType()->getListCacheTags(), $this->getAdditionalCacheTagsForEntityListing());
636     $nonempty_entity_listing_cache_tags = Cache::mergeTags($nonempty_entity_listing_cache_tags, $page_cache_tags);
637
638     $this->verifyPageCache($referencing_entity_url, 'HIT', Cache::mergeTags($referencing_entity_cache_tags, $page_cache_tags));
639     $this->verifyPageCache($listing_url, 'HIT', $page_cache_tags);
640     $this->verifyPageCache($empty_entity_listing_url, 'HIT', $empty_entity_listing_cache_tags);
641     $this->verifyPageCache($nonempty_entity_listing_url, 'HIT', $nonempty_entity_listing_cache_tags);
642   }
643
644   /**
645    * Creates a cache ID from a list of cache keys and a set of cache contexts.
646    *
647    * @param string[] $keys
648    *   A list of cache keys.
649    * @param string[] $contexts
650    *   A set of cache contexts.
651    *
652    * @return string
653    *   The cache ID string.
654    */
655   protected function createCacheId(array $keys, array $contexts) {
656     $cid_parts = $keys;
657
658     $contexts = \Drupal::service('cache_contexts_manager')->convertTokensToKeys($contexts);
659     $cid_parts = array_merge($cid_parts, $contexts->getKeys());
660
661     return implode(':', $cid_parts);
662   }
663
664   /**
665    * Verify that a given render cache entry exists, with the correct cache tags.
666    *
667    * @param string $cid
668    *   The render cache item ID.
669    * @param array $tags
670    *   An array of expected cache tags.
671    * @param string|null $redirected_cid
672    *   (optional) The redirected render cache item ID.
673    */
674   protected function verifyRenderCache($cid, array $tags, $redirected_cid = NULL) {
675     // Also verify the existence of an entity render cache entry.
676     $cache_entry = \Drupal::cache('render')->get($cid);
677     $this->assertTrue($cache_entry, 'A render cache entry exists.');
678     sort($cache_entry->tags);
679     sort($tags);
680     $this->assertIdentical($cache_entry->tags, $tags);
681     $is_redirecting_cache_item = isset($cache_entry->data['#cache_redirect']);
682     if ($redirected_cid === NULL) {
683       $this->assertFalse($is_redirecting_cache_item, 'Render cache entry is not a redirect.');
684       // If this is a redirecting cache item unlike we expected, log it.
685       if ($is_redirecting_cache_item) {
686         debug($cache_entry->data);
687       }
688     }
689     else {
690       // Verify that $cid contains a cache redirect.
691       $this->assertTrue($is_redirecting_cache_item, 'Render cache entry is a redirect.');
692       // If this is not a redirecting cache item unlike we expected, log it.
693       if (!$is_redirecting_cache_item) {
694         debug($cache_entry->data);
695       }
696       // Verify that the cache redirect points to the expected CID.
697       $redirect_cache_metadata = $cache_entry->data['#cache'];
698       $actual_redirection_cid = $this->createCacheId(
699         $redirect_cache_metadata['keys'],
700         $redirect_cache_metadata['contexts']
701       );
702       $this->assertIdentical($redirected_cid, $actual_redirection_cid);
703       // Finally, verify that the redirected CID exists and has the same cache
704       // tags.
705       $this->verifyRenderCache($redirected_cid, $tags);
706     }
707   }
708
709 }