From 1506b0af4dfd48f3fbd7c4681e7813fb9e8d08fc Mon Sep 17 00:00:00 2001
From: Nathaniel Catchpole <catch@35733.no-reply.drupal.org>
Date: Wed, 13 Mar 2019 17:04:47 +0000
Subject: [PATCH] Issue #3037970 by Berdir, Baysaa: Custom serialized field's
 data should be normalized even if it's empty

---
 .../hal/tests/src/Kernel/DenormalizeTest.php  | 49 ++++++++++
 .../src/Kernel/LinkItemSerializationTest.php  | 21 +++++
 .../src/Kernel/EntitySerializationTest.php    | 89 +++++++++++++++++++
 ...EntityReferenceFieldItemNormalizerTest.php |  2 +-
 .../TimestampItemNormalizerTest.php           |  2 +-
 .../src/Entity/EntitySerializedField.php      | 49 ++++++++++
 .../Plugin/Field/FieldType/SerializedItem.php | 48 ++++++++++
 .../FieldType/SerializedPropertyItem.php      | 50 +++++++++++
 8 files changed, 308 insertions(+), 2 deletions(-)
 create mode 100644 core/modules/system/tests/modules/entity_test/src/Entity/EntitySerializedField.php
 create mode 100644 core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/SerializedItem.php
 create mode 100644 core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/SerializedPropertyItem.php

diff --git a/core/modules/hal/tests/src/Kernel/DenormalizeTest.php b/core/modules/hal/tests/src/Kernel/DenormalizeTest.php
index 87eb97329f00..6c6fb731c98a 100644
--- a/core/modules/hal/tests/src/Kernel/DenormalizeTest.php
+++ b/core/modules/hal/tests/src/Kernel/DenormalizeTest.php
@@ -3,6 +3,7 @@
 namespace Drupal\Tests\hal\Kernel;
 
 use Drupal\Core\Url;
+use Drupal\entity_test\Entity\EntitySerializedField;
 use Drupal\field\Entity\FieldConfig;
 use Symfony\Component\Serializer\Exception\UnexpectedValueException;
 
@@ -139,4 +140,52 @@ public function testMarkFieldForDeletion() {
     $this->assertEqual($entity->field_test_text->count(), 0);
   }
 
+  /**
+   * Tests normalizing/denormalizing serialized columns.
+   */
+  public function testDenormalizeSerializedItem() {
+    $entity = EntitySerializedField::create(['serialized' => 'boo']);
+    $normalized = $this->serializer->normalize($entity, $this->format);
+    $this->setExpectedException(\LogicException::class, 'The generic FieldItemNormalizer cannot denormalize string values for "value" properties of the "serialized" field (field item class: Drupal\entity_test\Plugin\Field\FieldType\SerializedItem).');
+    $this->serializer->denormalize($normalized, EntitySerializedField::class, $this->format);
+  }
+
+  /**
+   * Tests normalizing/denormalizing invalid custom serialized fields.
+   */
+  public function testDenormalizeInvalidCustomSerializedField() {
+    $entity = EntitySerializedField::create(['serialized_long' => serialize(['Hello world!'])]);
+    $normalized = $this->serializer->normalize($entity);
+    $this->assertEquals($normalized['serialized_long'][0]['value'], ['Hello world!']);
+
+    $normalized['serialized_long'][0]['value'] = 'boo';
+    $this->setExpectedException(\LogicException::class, 'The generic FieldItemNormalizer cannot denormalize string values for "value" properties of the "serialized_long" field (field item class: Drupal\Core\Field\Plugin\Field\FieldType\StringLongItem).');
+    $this->serializer->denormalize($normalized, EntitySerializedField::class);
+  }
+
+  /**
+   * Tests normalizing/denormalizing empty custom serialized fields.
+   */
+  public function testDenormalizeEmptyCustomSerializedField() {
+    $entity = EntitySerializedField::create(['serialized_long' => serialize([])]);
+    $normalized = $this->serializer->normalize($entity);
+    $this->assertEquals([], $normalized['serialized_long'][0]['value']);
+
+    $entity = $this->serializer->denormalize($normalized, EntitySerializedField::class);
+    $this->assertEquals(serialize([]), $entity->get('serialized_long')->value);
+  }
+
+  /**
+   * Tests normalizing/denormalizing valid custom serialized fields.
+   */
+  public function testDenormalizeValidCustomSerializedField() {
+    $entity = EntitySerializedField::create(['serialized_long' => serialize(['key' => 'value'])]);
+    $normalized = $this->serializer->normalize($entity);
+    $this->assertEquals(['key' => 'value'], $normalized['serialized_long'][0]['value']);
+
+    $entity = $this->serializer->denormalize($normalized, EntitySerializedField::class);
+
+    $this->assertEquals(serialize(['key' => 'value']), $entity->get('serialized_long')->value);
+  }
+
 }
diff --git a/core/modules/link/tests/src/Kernel/LinkItemSerializationTest.php b/core/modules/link/tests/src/Kernel/LinkItemSerializationTest.php
index dde7dc117fbd..66209eff6f35 100644
--- a/core/modules/link/tests/src/Kernel/LinkItemSerializationTest.php
+++ b/core/modules/link/tests/src/Kernel/LinkItemSerializationTest.php
@@ -80,4 +80,25 @@ public function testLinkSerialization() {
     $this->assertSame($options_expected, $deserialized->field_test->options);
   }
 
+  /**
+   * Tests the deserialization.
+   */
+  public function testLinkDeserialization() {
+    // Create entity.
+    $entity = EntityTest::create();
+    $url = 'https://www.drupal.org?test_param=test_value';
+    $parsed_url = UrlHelper::parse($url);
+    $title = $this->randomMachineName();
+    $entity->field_test->uri = $parsed_url['path'];
+    $entity->field_test->title = $title;
+    $entity->field_test->first()
+      ->get('options')
+      ->set('query', $parsed_url['query']);
+    $json = json_decode($this->serializer->serialize($entity, 'json'), TRUE);
+    $json['field_test'][0]['options'] = 'string data';
+    $serialized = json_encode($json, TRUE);
+    $this->setExpectedException(\LogicException::class, 'The generic FieldItemNormalizer cannot denormalize string values for "options" properties of the "field_test" field (field item class: Drupal\link\Plugin\Field\FieldType\LinkItem).');
+    $this->serializer->deserialize($serialized, EntityTest::class, 'json');
+  }
+
 }
diff --git a/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php b/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php
index c95a9d96e60f..eff749805a51 100644
--- a/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php
+++ b/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php
@@ -4,6 +4,7 @@
 
 use Drupal\Component\Serialization\Json;
 use Drupal\Component\Render\FormattableMarkup;
+use Drupal\entity_test\Entity\EntitySerializedField;
 use Drupal\entity_test\Entity\EntityTestMulRev;
 use Drupal\filter\Entity\FilterFormat;
 use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
@@ -260,4 +261,92 @@ public function testDenormalize() {
     }
   }
 
+  /**
+   * Tests denormalizing serialized columns.
+   */
+  public function testDenormalizeSerializedItem() {
+    $this->setExpectedException(\LogicException::class, 'The generic FieldItemNormalizer cannot denormalize string values for "value" properties of the "serialized" field (field item class: Drupal\entity_test\Plugin\Field\FieldType\SerializedItem).');
+    $this->serializer->denormalize([
+      'serialized' => [
+        [
+          'value' => 'boo',
+        ],
+      ],
+      'type' => 'entity_test_serialized_field',
+    ], EntitySerializedField::class);
+  }
+
+  /**
+   * Tests normalizing/denormalizing custom serialized columns.
+   */
+  public function testDenormalizeCustomSerializedItem() {
+    $entity = EntitySerializedField::create(['serialized_text' => serialize(['Hello world!'])]);
+    $normalized = $this->serializer->normalize($entity);
+    $this->assertEquals($normalized['serialized_text'][0]['value'], ['Hello world!']);
+    $this->setExpectedException(\LogicException::class, 'The generic FieldItemNormalizer cannot denormalize string values for "value" properties of the "serialized_text" field (field item class: Drupal\entity_test\Plugin\Field\FieldType\SerializedPropertyItem).');
+    $this->serializer->denormalize([
+      'serialized_text' => [
+        [
+          'value' => 'boo',
+        ],
+      ],
+      'type' => 'entity_test_serialized_field',
+    ], EntitySerializedField::class);
+  }
+
+  /**
+   * Tests normalizing/denormalizing invalid custom serialized fields.
+   */
+  public function testDenormalizeInvalidCustomSerializedField() {
+    $entity = EntitySerializedField::create(['serialized_long' => serialize(['Hello world!'])]);
+    $normalized = $this->serializer->normalize($entity);
+    $this->assertEquals($normalized['serialized_long'][0]['value'], ['Hello world!']);
+    $this->setExpectedException(\LogicException::class, 'The generic FieldItemNormalizer cannot denormalize string values for "value" properties of the "serialized_long" field (field item class: Drupal\Core\Field\Plugin\Field\FieldType\StringLongItem).');
+    $this->serializer->denormalize([
+      'serialized_long' => [
+        [
+         'value' => 'boo',
+        ],
+      ],
+      'type' => 'entity_test_serialized_field',
+    ], EntitySerializedField::class);
+  }
+
+  /**
+   * Tests normalizing/denormalizing empty custom serialized fields.
+   */
+  public function testDenormalizeEmptyCustomSerializedField() {
+    $entity = EntitySerializedField::create(['serialized_long' => serialize([])]);
+    $normalized = $this->serializer->normalize($entity);
+    $this->assertEquals([], $normalized['serialized_long'][0]['value']);
+
+    $entity = $this->serializer->denormalize($normalized, EntitySerializedField::class);
+
+    $this->assertEquals(serialize([]), $entity->get('serialized_long')->value);
+  }
+
+  /**
+   * Tests normalizing/denormalizing valid custom serialized fields.
+   */
+  public function testDenormalizeValidCustomSerializedField() {
+    $entity = EntitySerializedField::create(['serialized_long' => serialize(['key' => 'value'])]);
+    $normalized = $this->serializer->normalize($entity);
+    $this->assertEquals(['key' => 'value'], $normalized['serialized_long'][0]['value']);
+
+    $entity = $this->serializer->denormalize($normalized, EntitySerializedField::class);
+
+    $this->assertEquals(serialize(['key' => 'value']), $entity->get('serialized_long')->value);
+  }
+
+  /**
+   * Tests normalizing/denormalizing using string values.
+   */
+  public function testDenormalizeStringValue() {
+    $this->setExpectedException(\LogicException::class, 'The generic FieldItemNormalizer cannot denormalize string values for "value" properties of the "serialized_long" field (field item class: Drupal\Core\Field\Plugin\Field\FieldType\StringLongItem).');
+    $this->serializer->denormalize([
+      'serialized_long' => ['boo'],
+      'type' => 'entity_test_serialized_field',
+    ], EntitySerializedField::class);
+  }
+
 }
diff --git a/core/modules/serialization/tests/src/Unit/Normalizer/EntityReferenceFieldItemNormalizerTest.php b/core/modules/serialization/tests/src/Unit/Normalizer/EntityReferenceFieldItemNormalizerTest.php
index a783d0928c63..581f43181d59 100644
--- a/core/modules/serialization/tests/src/Unit/Normalizer/EntityReferenceFieldItemNormalizerTest.php
+++ b/core/modules/serialization/tests/src/Unit/Normalizer/EntityReferenceFieldItemNormalizerTest.php
@@ -436,7 +436,7 @@ protected function assertDenormalize(array $data) {
         ->shouldBeCalled();
     }
 
-    // Avoid a static method call by returning dummy property data.
+    // Avoid a static method call by returning dummy serialized property data.
     $this->fieldDefinition
       ->getFieldStorageDefinition()
       ->willReturn()
diff --git a/core/modules/serialization/tests/src/Unit/Normalizer/TimestampItemNormalizerTest.php b/core/modules/serialization/tests/src/Unit/Normalizer/TimestampItemNormalizerTest.php
index af19ba339dc8..3d74c7cf771b 100644
--- a/core/modules/serialization/tests/src/Unit/Normalizer/TimestampItemNormalizerTest.php
+++ b/core/modules/serialization/tests/src/Unit/Normalizer/TimestampItemNormalizerTest.php
@@ -123,7 +123,7 @@ public function testDenormalize() {
     $timestamp_item->setValue(['value' => $timestamp_data_denormalization])
       ->shouldBeCalled();
 
-    // Avoid a static method call by returning dummy property data.
+    // Avoid a static method call by returning dummy serialized property data.
     $field_definition = $this->prophesize(FieldDefinitionInterface::class);
     $timestamp_item
       ->getFieldDefinition()
diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntitySerializedField.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntitySerializedField.php
new file mode 100644
index 000000000000..0d8bb8bd554a
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntitySerializedField.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace Drupal\entity_test\Entity;
+
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Field\BaseFieldDefinition;
+
+/**
+ * Defines a test class for testing fields with a serialized column.
+ *
+ * @ContentEntityType(
+ *   id = "entity_test_serialized_field",
+ *   label = @Translation("Test serialized fields"),
+ *   entity_keys = {
+ *     "id" = "id",
+ *     "uuid" = "uuid",
+ *     "bundle" = "type",
+ *     "label" = "name"
+ *   },
+ *   base_table = "entity_test_serialized_fields",
+ *   persistent_cache = FALSE,
+ *   serialized_field_property_names = {
+ *     "serialized_long" = {
+ *       "value"
+ *     }
+ *   }
+ * )
+ */
+class EntitySerializedField extends EntityTest {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
+    $fields = parent::baseFieldDefinitions($entity_type);
+
+    $fields['serialized'] = BaseFieldDefinition::create('serialized_item_test')
+      ->setLabel(t('Serialized'));
+
+    $fields['serialized_text'] = BaseFieldDefinition::create('serialized_property_item_test')
+      ->setLabel(t('Serialized text'));
+
+    $fields['serialized_long'] = BaseFieldDefinition::create('string_long')
+      ->setLabel(t('Serialized long string'));
+
+    return $fields;
+  }
+
+}
diff --git a/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/SerializedItem.php b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/SerializedItem.php
new file mode 100644
index 000000000000..d460cb2026aa
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/SerializedItem.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace Drupal\entity_test\Plugin\Field\FieldType;
+
+use Drupal\Core\Field\FieldItemBase;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\TypedData\DataDefinition;
+
+/**
+ * Defines the 'serialized_item' entity field type.
+ *
+ * @FieldType(
+ *   id = "serialized_item_test",
+ *   label = @Translation("Test serialized field item"),
+ *   description = @Translation("A field containing a serialized string value."),
+ *   category = @Translation("Field"),
+ * )
+ */
+class SerializedItem extends FieldItemBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
+    $properties['value'] = DataDefinition::create('string')
+      ->setLabel(new TranslatableMarkup('Test serialized value'))
+      ->setRequired(TRUE);
+
+    return $properties;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function schema(FieldStorageDefinitionInterface $field_definition) {
+    return [
+      'columns' => [
+        'value' => [
+          'type' => 'blob',
+          'size' => 'big',
+          'serialize' => TRUE,
+        ],
+      ],
+    ];
+  }
+
+}
diff --git a/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/SerializedPropertyItem.php b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/SerializedPropertyItem.php
new file mode 100644
index 000000000000..c9b067fe421f
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/SerializedPropertyItem.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace Drupal\entity_test\Plugin\Field\FieldType;
+
+use Drupal\Core\Field\FieldItemBase;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\TypedData\DataDefinition;
+
+/**
+ * Defines the 'serialized_property_item_test' entity field type.
+ *
+ * @FieldType(
+ *   id = "serialized_property_item_test",
+ *   label = @Translation("Test serialized property field item"),
+ *   description = @Translation("A field containing a string representing serialized data."),
+ *   category = @Translation("Field"),
+ *   serialized_property_names = {
+ *     "value"
+ *   }
+ * )
+ */
+class SerializedPropertyItem extends FieldItemBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
+    $properties['value'] = DataDefinition::create('string')
+      ->setLabel(new TranslatableMarkup('Test serialized value'))
+      ->setRequired(TRUE);
+
+    return $properties;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function schema(FieldStorageDefinitionInterface $field_definition) {
+    return [
+      'columns' => [
+        'value' => [
+          'type' => 'text',
+          'size' => 'big',
+        ],
+      ],
+    ];
+  }
+
+}
-- 
GitLab