Added Entity and Entity Reference Revisions which got dropped somewhere along the...
[yaffs-website] / web / core / modules / node / tests / src / Functional / NodeRevisionsTest.php
1 <?php
2
3 namespace Drupal\Tests\node\Functional;
4
5 use Drupal\Core\Url;
6 use Drupal\field\Entity\FieldConfig;
7 use Drupal\field\Entity\FieldStorageConfig;
8 use Drupal\language\Entity\ConfigurableLanguage;
9 use Drupal\node\Entity\Node;
10 use Drupal\node\NodeInterface;
11 use Drupal\Component\Serialization\Json;
12
13 /**
14  * Create a node with revisions and test viewing, saving, reverting, and
15  * deleting revisions for users with access for this content type.
16  *
17  * @group node
18  */
19 class NodeRevisionsTest extends NodeTestBase {
20
21   /**
22    * An array of node revisions.
23    *
24    * @var \Drupal\node\NodeInterface[]
25    */
26   protected $nodes;
27
28   /**
29    * Revision log messages.
30    *
31    * @var array
32    */
33   protected $revisionLogs;
34
35   /**
36    * {@inheritdoc}
37    */
38   public static $modules = ['node', 'contextual', 'datetime', 'language', 'content_translation'];
39
40   /**
41    * {@inheritdoc}
42    */
43   protected function setUp() {
44     parent::setUp();
45
46     // Enable additional languages.
47     ConfigurableLanguage::createFromLangcode('de')->save();
48     ConfigurableLanguage::createFromLangcode('it')->save();
49
50     $field_storage_definition = [
51       'field_name' => 'untranslatable_string_field',
52       'entity_type' => 'node',
53       'type' => 'string',
54       'cardinality' => 1,
55       'translatable' => FALSE,
56     ];
57     $field_storage = FieldStorageConfig::create($field_storage_definition);
58     $field_storage->save();
59
60     $field_definition = [
61       'field_storage' => $field_storage,
62       'bundle' => 'page',
63     ];
64     $field = FieldConfig::create($field_definition);
65     $field->save();
66
67     // Enable translation for page nodes.
68     \Drupal::service('content_translation.manager')->setEnabled('node', 'page', TRUE);
69
70     // Create and log in user.
71     $web_user = $this->drupalCreateUser(
72       [
73         'view page revisions',
74         'revert page revisions',
75         'delete page revisions',
76         'edit any page content',
77         'delete any page content',
78         'access contextual links',
79         'translate any entity',
80         'administer content types',
81       ]
82     );
83
84     $this->drupalLogin($web_user);
85
86     // Create initial node.
87     $node = $this->drupalCreateNode();
88     $settings = get_object_vars($node);
89     $settings['revision'] = 1;
90     $settings['isDefaultRevision'] = TRUE;
91
92     $nodes = [];
93     $logs = [];
94
95     // Get original node.
96     $nodes[] = clone $node;
97
98     // Create three revisions.
99     $revision_count = 3;
100     for ($i = 0; $i < $revision_count; $i++) {
101       $logs[] = $node->revision_log = $this->randomMachineName(32);
102
103       // Create revision with a random title and body and update variables.
104       $node->title = $this->randomMachineName();
105       $node->body = [
106         'value' => $this->randomMachineName(32),
107         'format' => filter_default_format(),
108       ];
109       $node->untranslatable_string_field->value = $this->randomString();
110       $node->setNewRevision();
111
112       // Edit the 1st and 2nd revision with a different user.
113       if ($i < 2) {
114         $editor = $this->drupalCreateUser();
115         $node->setRevisionUserId($editor->id());
116       }
117       else {
118         $node->setRevisionUserId($web_user->id());
119       }
120
121       $node->save();
122
123       // Make sure we get revision information.
124       $node = Node::load($node->id());
125       $nodes[] = clone $node;
126     }
127
128     $this->nodes = $nodes;
129     $this->revisionLogs = $logs;
130   }
131
132   /**
133    * Checks node revision related operations.
134    */
135   public function testRevisions() {
136     $node_storage = $this->container->get('entity.manager')->getStorage('node');
137     $nodes = $this->nodes;
138     $logs = $this->revisionLogs;
139
140     // Get last node for simple checks.
141     $node = $nodes[3];
142
143     // Confirm the correct revision text appears on "view revisions" page.
144     $this->drupalGet("node/" . $node->id() . "/revisions/" . $node->getRevisionId() . "/view");
145     $this->assertText($node->body->value, 'Correct text displays for version.');
146
147     // Confirm the correct log message appears on "revisions overview" page.
148     $this->drupalGet("node/" . $node->id() . "/revisions");
149     foreach ($logs as $revision_log) {
150       $this->assertText($revision_log, 'Revision log message found.');
151     }
152     // Original author, and editor names should appear on revisions overview.
153     $web_user = $nodes[0]->revision_uid->entity;
154     $this->assertText(t('by @name', ['@name' => $web_user->getAccountName()]));
155     $editor = $nodes[2]->revision_uid->entity;
156     $this->assertText(t('by @name', ['@name' => $editor->getAccountName()]));
157
158     // Confirm that this is the default revision.
159     $this->assertTrue($node->isDefaultRevision(), 'Third node revision is the default one.');
160
161     // Confirm that revisions revert properly.
162     $this->drupalPostForm("node/" . $node->id() . "/revisions/" . $nodes[1]->getRevisionid() . "/revert", [], t('Revert'));
163     $this->assertRaw(t('@type %title has been reverted to the revision from %revision-date.', [
164       '@type' => 'Basic page',
165       '%title' => $nodes[1]->label(),
166       '%revision-date' => format_date($nodes[1]->getRevisionCreationTime()),
167     ]), 'Revision reverted.');
168     $node_storage->resetCache([$node->id()]);
169     $reverted_node = $node_storage->load($node->id());
170     $this->assertTrue(($nodes[1]->body->value == $reverted_node->body->value), 'Node reverted correctly.');
171     // Confirm the revision author is the user performing the revert.
172     $this->assertTrue($reverted_node->getRevisionUserId() == $this->loggedInUser->id(), 'Node revision author is user performing revert.');
173     // And that its not the revision author.
174     $this->assertTrue($reverted_node->getRevisionUserId() != $nodes[1]->getRevisionUserId(), 'Node revision author is not original revision author.');
175
176     // Confirm that this is not the default version.
177     $node = node_revision_load($node->getRevisionId());
178     $this->assertFalse($node->isDefaultRevision(), 'Third node revision is not the default one.');
179
180     // Confirm revisions delete properly.
181     $this->drupalPostForm("node/" . $node->id() . "/revisions/" . $nodes[1]->getRevisionId() . "/delete", [], t('Delete'));
182     $this->assertRaw(t('Revision from %revision-date of @type %title has been deleted.', [
183       '%revision-date' => format_date($nodes[1]->getRevisionCreationTime()),
184       '@type' => 'Basic page',
185       '%title' => $nodes[1]->label(),
186     ]), 'Revision deleted.');
187     $this->assertTrue(db_query('SELECT COUNT(vid) FROM {node_revision} WHERE nid = :nid and vid = :vid', [':nid' => $node->id(), ':vid' => $nodes[1]->getRevisionId()])->fetchField() == 0, 'Revision not found.');
188     $this->assertTrue(db_query('SELECT COUNT(vid) FROM {node_field_revision} WHERE nid = :nid and vid = :vid', [':nid' => $node->id(), ':vid' => $nodes[1]->getRevisionId()])->fetchField() == 0, 'Field revision not found.');
189
190     // Set the revision timestamp to an older date to make sure that the
191     // confirmation message correctly displays the stored revision date.
192     $old_revision_date = REQUEST_TIME - 86400;
193     db_update('node_revision')
194       ->condition('vid', $nodes[2]->getRevisionId())
195       ->fields([
196         'revision_timestamp' => $old_revision_date,
197       ])
198       ->execute();
199     $this->drupalPostForm("node/" . $node->id() . "/revisions/" . $nodes[2]->getRevisionId() . "/revert", [], t('Revert'));
200     $this->assertRaw(t('@type %title has been reverted to the revision from %revision-date.', [
201       '@type' => 'Basic page',
202       '%title' => $nodes[2]->label(),
203       '%revision-date' => format_date($old_revision_date),
204     ]));
205
206     // Make a new revision and set it to not be default.
207     // This will create a new revision that is not "front facing".
208     $new_node_revision = clone $node;
209     $new_body = $this->randomMachineName();
210     $new_node_revision->body->value = $new_body;
211     // Save this as a non-default revision.
212     $new_node_revision->setNewRevision();
213     $new_node_revision->isDefaultRevision = FALSE;
214     $new_node_revision->save();
215
216     $this->drupalGet('node/' . $node->id());
217     $this->assertNoText($new_body, 'Revision body text is not present on default version of node.');
218
219     // Verify that the new body text is present on the revision.
220     $this->drupalGet("node/" . $node->id() . "/revisions/" . $new_node_revision->getRevisionId() . "/view");
221     $this->assertText($new_body, 'Revision body text is present when loading specific revision.');
222
223     // Verify that the non-default revision vid is greater than the default
224     // revision vid.
225     $default_revision = db_select('node', 'n')
226       ->fields('n', ['vid'])
227       ->condition('nid', $node->id())
228       ->execute()
229       ->fetchCol();
230     $default_revision_vid = $default_revision[0];
231     $this->assertTrue($new_node_revision->getRevisionId() > $default_revision_vid, 'Revision vid is greater than default revision vid.');
232
233     // Create an 'EN' node with a revision log message.
234     $node = $this->drupalCreateNode();
235     $node->title = 'Node title in EN';
236     $node->revision_log = 'Simple revision message (EN)';
237     $node->save();
238
239     $this->drupalGet("node/" . $node->id() . "/revisions");
240     $this->assertResponse(403);
241
242     // Create a new revision and new log message.
243     $node = Node::load($node->id());
244     $node->body->value = 'New text (EN)';
245     $node->revision_log = 'New revision message (EN)';
246     $node->setNewRevision();
247     $node->save();
248
249     // Check both revisions are shown on the node revisions overview page.
250     $this->drupalGet("node/" . $node->id() . "/revisions");
251     $this->assertText('Simple revision message (EN)');
252     $this->assertText('New revision message (EN)');
253
254     // Create an 'EN' node with a revision log message.
255     $node = $this->drupalCreateNode();
256     $node->langcode = 'en';
257     $node->title = 'Node title in EN';
258     $node->revision_log = 'Simple revision message (EN)';
259     $node->save();
260
261     $this->drupalGet("node/" . $node->id() . "/revisions");
262     $this->assertResponse(403);
263
264     // Add a translation in 'DE' and create a new revision and new log message.
265     $translation = $node->addTranslation('de');
266     $translation->title->value = 'Node title in DE';
267     $translation->body->value = 'New text (DE)';
268     $translation->revision_log = 'New revision message (DE)';
269     $translation->setNewRevision();
270     $translation->save();
271
272     // View the revision UI in 'IT', only the original node revision is shown.
273     $this->drupalGet("it/node/" . $node->id() . "/revisions");
274     $this->assertText('Simple revision message (EN)');
275     $this->assertNoText('New revision message (DE)');
276
277     // View the revision UI in 'DE', only the translated node revision is shown.
278     $this->drupalGet("de/node/" . $node->id() . "/revisions");
279     $this->assertNoText('Simple revision message (EN)');
280     $this->assertText('New revision message (DE)');
281
282     // View the revision UI in 'EN', only the original node revision is shown.
283     $this->drupalGet("node/" . $node->id() . "/revisions");
284     $this->assertText('Simple revision message (EN)');
285     $this->assertNoText('New revision message (DE)');
286   }
287
288   /**
289    * Checks that revisions are correctly saved without log messages.
290    */
291   public function testNodeRevisionWithoutLogMessage() {
292     $node_storage = $this->container->get('entity.manager')->getStorage('node');
293     // Create a node with an initial log message.
294     $revision_log = $this->randomMachineName(10);
295     $node = $this->drupalCreateNode(['revision_log' => $revision_log]);
296
297     // Save over the same revision and explicitly provide an empty log message
298     // (for example, to mimic the case of a node form submitted with no text in
299     // the "log message" field), and check that the original log message is
300     // preserved.
301     $new_title = $this->randomMachineName(10) . 'testNodeRevisionWithoutLogMessage1';
302
303     $node = clone $node;
304     $node->title = $new_title;
305     $node->revision_log = '';
306     $node->setNewRevision(FALSE);
307
308     $node->save();
309     $this->drupalGet('node/' . $node->id());
310     $this->assertText($new_title, 'New node title appears on the page.');
311     $node_storage->resetCache([$node->id()]);
312     $node_revision = $node_storage->load($node->id());
313     $this->assertEqual($node_revision->revision_log->value, $revision_log, 'After an existing node revision is re-saved without a log message, the original log message is preserved.');
314
315     // Create another node with an initial revision log message.
316     $node = $this->drupalCreateNode(['revision_log' => $revision_log]);
317
318     // Save a new node revision without providing a log message, and check that
319     // this revision has an empty log message.
320     $new_title = $this->randomMachineName(10) . 'testNodeRevisionWithoutLogMessage2';
321
322     $node = clone $node;
323     $node->title = $new_title;
324     $node->setNewRevision();
325     $node->revision_log = NULL;
326
327     $node->save();
328     $this->drupalGet('node/' . $node->id());
329     $this->assertText($new_title, 'New node title appears on the page.');
330     $node_storage->resetCache([$node->id()]);
331     $node_revision = $node_storage->load($node->id());
332     $this->assertTrue(empty($node_revision->revision_log->value), 'After a new node revision is saved with an empty log message, the log message for the node is empty.');
333   }
334
335   /**
336    * Gets server-rendered contextual links for the given contextual links IDs.
337    *
338    * @param string[] $ids
339    *   An array of contextual link IDs.
340    * @param string $current_path
341    *   The Drupal path for the page for which the contextual links are rendered.
342    *
343    * @return string
344    *   The decoded JSON response body.
345    */
346   protected function renderContextualLinks(array $ids, $current_path) {
347     $post = [];
348     for ($i = 0; $i < count($ids); $i++) {
349       $post['ids[' . $i . ']'] = $ids[$i];
350     }
351     $response = $this->drupalPost('contextual/render', 'application/json', $post, ['query' => ['destination' => $current_path]]);
352
353     return Json::decode($response);
354   }
355
356   /**
357    * Tests the revision translations are correctly reverted.
358    */
359   public function testRevisionTranslationRevert() {
360     // Create a node and a few revisions.
361     $node = $this->drupalCreateNode(['langcode' => 'en']);
362
363     $initial_revision_id = $node->getRevisionId();
364     $initial_title = $node->label();
365     $this->createRevisions($node, 2);
366
367     // Translate the node and create a few translation revisions.
368     $translation = $node->addTranslation('it');
369     $this->createRevisions($translation, 3);
370     $revert_id = $node->getRevisionId();
371     $translated_title = $translation->label();
372     $untranslatable_string = $node->untranslatable_string_field->value;
373
374     // Create a new revision for the default translation in-between a series of
375     // translation revisions.
376     $this->createRevisions($node, 1);
377     $default_translation_title = $node->label();
378
379     // And create a few more translation revisions.
380     $this->createRevisions($translation, 2);
381     $translation_revision_id = $translation->getRevisionId();
382
383     // Now revert the a translation revision preceding the last default
384     // translation revision, and check that the desired value was reverted but
385     // the default translation value was preserved.
386     $revert_translation_url = Url::fromRoute('node.revision_revert_translation_confirm', [
387       'node' => $node->id(),
388       'node_revision' => $revert_id,
389       'langcode' => 'it',
390     ]);
391     $this->drupalPostForm($revert_translation_url, [], t('Revert'));
392     /** @var \Drupal\node\NodeStorage $node_storage */
393     $node_storage = $this->container->get('entity.manager')->getStorage('node');
394     $node_storage->resetCache();
395     /** @var \Drupal\node\NodeInterface $node */
396     $node = $node_storage->load($node->id());
397     $this->assertTrue($node->getRevisionId() > $translation_revision_id);
398     $this->assertEqual($node->label(), $default_translation_title);
399     $this->assertEqual($node->getTranslation('it')->label(), $translated_title);
400     $this->assertNotEqual($node->untranslatable_string_field->value, $untranslatable_string);
401
402     $latest_revision_id = $translation->getRevisionId();
403
404     // Now revert the a translation revision preceding the last default
405     // translation revision again, and check that the desired value was reverted
406     // but the default translation value was preserved. But in addition the
407     // untranslated field will be reverted as well.
408     $this->drupalPostForm($revert_translation_url, ['revert_untranslated_fields' => TRUE], t('Revert'));
409     $node_storage->resetCache();
410     /** @var \Drupal\node\NodeInterface $node */
411     $node = $node_storage->load($node->id());
412     $this->assertTrue($node->getRevisionId() > $latest_revision_id);
413     $this->assertEqual($node->label(), $default_translation_title);
414     $this->assertEqual($node->getTranslation('it')->label(), $translated_title);
415     $this->assertEqual($node->untranslatable_string_field->value, $untranslatable_string);
416
417     $latest_revision_id = $translation->getRevisionId();
418
419     // Now revert the entity revision to the initial one where the translation
420     // didn't exist.
421     $revert_url = Url::fromRoute('node.revision_revert_confirm', [
422       'node' => $node->id(),
423       'node_revision' => $initial_revision_id,
424     ]);
425     $this->drupalPostForm($revert_url, [], t('Revert'));
426     $node_storage->resetCache();
427     /** @var \Drupal\node\NodeInterface $node */
428     $node = $node_storage->load($node->id());
429     $this->assertTrue($node->getRevisionId() > $latest_revision_id);
430     $this->assertEqual($node->label(), $initial_title);
431     $this->assertFalse($node->hasTranslation('it'));
432   }
433
434   /**
435    * Creates a series of revisions for the specified node.
436    *
437    * @param \Drupal\node\NodeInterface $node
438    *   The node object.
439    * @param $count
440    *   The number of revisions to be created.
441    */
442   protected function createRevisions(NodeInterface $node, $count) {
443     for ($i = 0; $i < $count; $i++) {
444       $node->title = $this->randomString();
445       $node->untranslatable_string_field->value = $this->randomString();
446       $node->setNewRevision(TRUE);
447       $node->save();
448     }
449   }
450
451 }