diff core/modules/jsonapi/tests/src/Functional/JsonApiRegressionTest.php @ 18:af1871eacc83

Update to Drupal core 8.7.1
author Chris Cannam
date Thu, 09 May 2019 15:33:08 +0100
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/core/modules/jsonapi/tests/src/Functional/JsonApiRegressionTest.php	Thu May 09 15:33:08 2019 +0100
@@ -0,0 +1,908 @@
+<?php
+
+namespace Drupal\Tests\jsonapi\Functional;
+
+use Drupal\comment\Entity\Comment;
+use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
+use Drupal\comment\Tests\CommentTestTrait;
+use Drupal\Component\Serialization\Json;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\Url;
+use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem;
+use Drupal\entity_test\Entity\EntityTestMapField;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\node\Entity\Node;
+use Drupal\node\Entity\NodeType;
+use Drupal\shortcut\Entity\Shortcut;
+use Drupal\taxonomy\Entity\Term;
+use Drupal\taxonomy\Entity\Vocabulary;
+use Drupal\user\Entity\Role;
+use Drupal\user\Entity\User;
+use Drupal\user\RoleInterface;
+use GuzzleHttp\RequestOptions;
+
+/**
+ * JSON:API regression tests.
+ *
+ * @group jsonapi
+ * @group legacy
+ *
+ * @internal
+ */
+class JsonApiRegressionTest extends JsonApiFunctionalTestBase {
+
+  use CommentTestTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'basic_auth',
+  ];
+
+  /**
+   * Ensure filtering on relationships works with bundle-specific target types.
+   *
+   * @see https://www.drupal.org/project/jsonapi/issues/2953207
+   */
+  public function testBundleSpecificTargetEntityTypeFromIssue2953207() {
+    // Set up data model.
+    $this->assertTrue($this->container->get('module_installer')->install(['comment'], TRUE), 'Installed modules.');
+    $this->addDefaultCommentField('taxonomy_term', 'tags', 'comment', CommentItemInterface::OPEN, 'tcomment');
+    $this->rebuildAll();
+
+    // Create data.
+    Term::create([
+      'name' => 'foobar',
+      'vid' => 'tags',
+    ])->save();
+    Comment::create([
+      'subject' => 'Llama',
+      'entity_id' => 1,
+      'entity_type' => 'taxonomy_term',
+      'field_name' => 'comment',
+    ])->save();
+
+    // Test.
+    $user = $this->drupalCreateUser([
+      'access comments',
+    ]);
+    $response = $this->request('GET', Url::fromUri('internal:/jsonapi/comment/tcomment?include=entity_id&filter[entity_id.name]=foobar'), [
+      RequestOptions::AUTH => [
+        $user->getUsername(),
+        $user->pass_raw,
+      ],
+    ]);
+    $this->assertSame(200, $response->getStatusCode());
+  }
+
+  /**
+   * Ensure deep nested include works on multi target entity type field.
+   *
+   * @see https://www.drupal.org/project/jsonapi/issues/2973681
+   */
+  public function testDeepNestedIncludeMultiTargetEntityTypeFieldFromIssue2973681() {
+    // Set up data model.
+    $this->assertTrue($this->container->get('module_installer')->install(['comment'], TRUE), 'Installed modules.');
+    $this->addDefaultCommentField('node', 'article');
+    $this->addDefaultCommentField('taxonomy_term', 'tags', 'comment', CommentItemInterface::OPEN, 'tcomment');
+    $this->drupalCreateContentType(['type' => 'page']);
+    $this->createEntityReferenceField(
+      'node',
+      'page',
+      'field_comment',
+      NULL,
+      'comment',
+      'default',
+      [
+        'target_bundles' => [
+          'comment' => 'comment',
+          'tcomment' => 'tcomment',
+        ],
+      ],
+      FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
+    );
+    $this->rebuildAll();
+
+    // Create data.
+    $node = Node::create([
+      'title' => 'test article',
+      'type' => 'article',
+    ]);
+    $node->save();
+    $comment = Comment::create([
+      'subject' => 'Llama',
+      'entity_id' => 1,
+      'entity_type' => 'node',
+      'field_name' => 'comment',
+    ]);
+    $comment->save();
+    $page = Node::create([
+      'title' => 'test node',
+      'type' => 'page',
+      'field_comment' => [
+        'entity' => $comment,
+      ],
+    ]);
+    $page->save();
+
+    // Test.
+    $user = $this->drupalCreateUser([
+      'access content',
+      'access comments',
+    ]);
+    $response = $this->request('GET', Url::fromUri('internal:/jsonapi/node/page?include=field_comment,field_comment.entity_id,field_comment.entity_id.uid'), [
+      RequestOptions::AUTH => [
+        $user->getUsername(),
+        $user->pass_raw,
+      ],
+    ]);
+    $this->assertSame(200, $response->getStatusCode());
+  }
+
+  /**
+   * Ensure POST and PATCH works for bundle-less relationship routes.
+   *
+   * @see https://www.drupal.org/project/jsonapi/issues/2976371
+   */
+  public function testBundlelessRelationshipMutationFromIssue2973681() {
+    $this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
+
+    // Set up data model.
+    $this->drupalCreateContentType(['type' => 'page']);
+    $this->createEntityReferenceField(
+      'node',
+      'page',
+      'field_test',
+      NULL,
+      'user',
+      'default',
+      [
+        'target_bundles' => NULL,
+      ],
+      FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
+    );
+    $this->rebuildAll();
+
+    // Create data.
+    $node = Node::create([
+      'title' => 'test article',
+      'type' => 'page',
+    ]);
+    $node->save();
+    $target = $this->createUser();
+
+    // Test.
+    $user = $this->drupalCreateUser(['bypass node access']);
+    $url = Url::fromRoute('jsonapi.node--page.field_test.relationship.post', ['entity' => $node->uuid()]);
+    $request_options = [
+      RequestOptions::HEADERS => [
+        'Content-Type' => 'application/vnd.api+json',
+        'Accept' => 'application/vnd.api+json',
+      ],
+      RequestOptions::AUTH => [$user->getUsername(), $user->pass_raw],
+      RequestOptions::JSON => [
+        'data' => [
+          ['type' => 'user--user', 'id' => $target->uuid()],
+        ],
+      ],
+    ];
+    $response = $this->request('POST', $url, $request_options);
+    $this->assertSame(204, $response->getStatusCode(), (string) $response->getBody());
+  }
+
+  /**
+   * Ensures GETting terms works when multiple vocabularies exist.
+   *
+   * @see https://www.drupal.org/project/jsonapi/issues/2977879
+   */
+  public function testGetTermWhenMultipleVocabulariesExistFromIssue2977879() {
+    // Set up data model.
+    $this->assertTrue($this->container->get('module_installer')->install(['taxonomy'], TRUE), 'Installed modules.');
+    Vocabulary::create([
+      'name' => 'one',
+      'vid' => 'one',
+    ])->save();
+    Vocabulary::create([
+      'name' => 'two',
+      'vid' => 'two',
+    ])->save();
+    $this->rebuildAll();
+
+    // Create data.
+    Term::create(['vid' => 'one'])
+      ->setName('Test')
+      ->save();
+
+    // Test.
+    $user = $this->drupalCreateUser([
+      'access content',
+    ]);
+    $response = $this->request('GET', Url::fromUri('internal:/jsonapi/taxonomy_term/one'), [
+      RequestOptions::AUTH => [
+        $user->getUsername(),
+        $user->pass_raw,
+      ],
+    ]);
+    $this->assertSame(200, $response->getStatusCode());
+  }
+
+  /**
+   * Cannot PATCH an entity with dangling references in an ER field.
+   *
+   * @see https://www.drupal.org/project/jsonapi/issues/2968972
+   */
+  public function testDanglingReferencesInAnEntityReferenceFieldFromIssue2968972() {
+    $this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
+
+    // Set up data model.
+    $this->drupalCreateContentType(['type' => 'journal_issue']);
+    $this->drupalCreateContentType(['type' => 'journal_article']);
+    $this->createEntityReferenceField(
+      'node',
+      'journal_article',
+      'field_issue',
+      NULL,
+      'node',
+      'default',
+      [
+        'target_bundles' => [
+          'journal_issue' => 'journal_issue',
+        ],
+      ],
+      FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
+    );
+    $this->rebuildAll();
+
+    // Create data.
+    $issue_node = Node::create([
+      'title' => 'Test Journal Issue',
+      'type' => 'journal_issue',
+    ]);
+    $issue_node->save();
+
+    $user = $this->drupalCreateUser([
+      'access content',
+      'edit own journal_article content',
+    ]);
+    $article_node = Node::create([
+      'title' => 'Test Journal Article',
+      'type' => 'journal_article',
+      'field_issue' => [
+        'target_id' => $issue_node->id(),
+      ],
+    ]);
+    $article_node->setOwner($user);
+    $article_node->save();
+
+    // Test.
+    $url = Url::fromUri(sprintf('internal:/jsonapi/node/journal_article/%s', $article_node->uuid()));
+    $request_options = [
+      RequestOptions::HEADERS => [
+        'Content-Type' => 'application/vnd.api+json',
+        'Accept' => 'application/vnd.api+json',
+      ],
+      RequestOptions::AUTH => [$user->getUsername(), $user->pass_raw],
+      RequestOptions::JSON => [
+        'data' => [
+          'type' => 'node--journal_article',
+          'id' => $article_node->uuid(),
+          'attributes' => [
+            'title' => 'My New Article Title',
+          ],
+        ],
+      ],
+    ];
+    $issue_node->delete();
+    $response = $this->request('PATCH', $url, $request_options);
+    $this->assertSame(200, $response->getStatusCode(), (string) $response->getBody());
+  }
+
+  /**
+   * Ensures GETting node collection + hook_node_grants() implementations works.
+   *
+   * @see https://www.drupal.org/project/jsonapi/issues/2984964
+   */
+  public function testGetNodeCollectionWithHookNodeGrantsImplementationsFromIssue2984964() {
+    // Set up data model.
+    $this->assertTrue($this->container->get('module_installer')->install(['node_access_test'], TRUE), 'Installed modules.');
+    node_access_rebuild();
+    $this->rebuildAll();
+
+    // Create data.
+    Node::create([
+      'title' => 'test article',
+      'type' => 'article',
+    ])->save();
+
+    // Test.
+    $user = $this->drupalCreateUser([
+      'access content',
+    ]);
+    $response = $this->request('GET', Url::fromUri('internal:/jsonapi/node/article'), [
+      RequestOptions::AUTH => [
+        $user->getUsername(),
+        $user->pass_raw,
+      ],
+    ]);
+    $this->assertSame(200, $response->getStatusCode());
+    $this->assertTrue(in_array('user.node_grants:view', explode(' ', $response->getHeader('X-Drupal-Cache-Contexts')[0]), TRUE));
+  }
+
+  /**
+   * Cannot GET an entity with dangling references in an ER field.
+   *
+   * @see https://www.drupal.org/project/jsonapi/issues/2984647
+   */
+  public function testDanglingReferencesInAnEntityReferenceFieldFromIssue2984647() {
+    // Set up data model.
+    $this->drupalCreateContentType(['type' => 'journal_issue']);
+    $this->drupalCreateContentType(['type' => 'journal_conference']);
+    $this->drupalCreateContentType(['type' => 'journal_article']);
+    $this->createEntityReferenceField(
+      'node',
+      'journal_article',
+      'field_issue',
+      NULL,
+      'node',
+      'default',
+      [
+        'target_bundles' => [
+          'journal_issue' => 'journal_issue',
+        ],
+      ],
+      FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
+    );
+    $this->createEntityReferenceField(
+      'node',
+      'journal_article',
+      'field_mentioned_in',
+      NULL,
+      'node',
+      'default',
+      [
+        'target_bundles' => [
+          'journal_issue' => 'journal_issue',
+          'journal_conference' => 'journal_conference',
+        ],
+      ],
+      FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
+    );
+    $this->rebuildAll();
+
+    // Create data.
+    $issue_node = Node::create([
+      'title' => 'Test Journal Issue',
+      'type' => 'journal_issue',
+    ]);
+    $issue_node->save();
+    $conference_node = Node::create([
+      'title' => 'First Journal Conference!',
+      'type' => 'journal_conference',
+    ]);
+    $conference_node->save();
+
+    $user = $this->drupalCreateUser([
+      'access content',
+      'edit own journal_article content',
+    ]);
+    $article_node = Node::create([
+      'title' => 'Test Journal Article',
+      'type' => 'journal_article',
+      'field_issue' => [
+        ['target_id' => $issue_node->id()],
+      ],
+      'field_mentioned_in' => [
+        ['target_id' => $issue_node->id()],
+        ['target_id' => $conference_node->id()],
+      ],
+    ]);
+    $article_node->setOwner($user);
+    $article_node->save();
+
+    // Test.
+    $url = Url::fromUri(sprintf('internal:/jsonapi/node/journal_article/%s', $article_node->uuid()));
+    $request_options = [
+      RequestOptions::HEADERS => [
+        'Content-Type' => 'application/vnd.api+json',
+        'Accept' => 'application/vnd.api+json',
+      ],
+      RequestOptions::AUTH => [$user->getUsername(), $user->pass_raw],
+    ];
+    $issue_node->delete();
+    $response = $this->request('GET', $url, $request_options);
+    $this->assertSame(200, $response->getStatusCode());
+
+    // Entity reference field allowing a single bundle: dangling reference's
+    // resource type is deduced.
+    $this->assertSame([
+      [
+        'type' => 'node--journal_issue',
+        'id' => 'missing',
+        'meta' => [
+          'links' => [
+            'help' => [
+              'href' => 'https://www.drupal.org/docs/8/modules/json-api/core-concepts#missing',
+              'meta' => [
+                'about' => "Usage and meaning of the 'missing' resource identifier.",
+              ],
+            ],
+          ],
+        ],
+      ],
+    ], Json::decode((string) $response->getBody())['data']['relationships']['field_issue']['data']);
+
+    // Entity reference field allowing multiple bundles: dangling reference's
+    // resource type is NOT deduced.
+    $this->assertSame([
+      [
+        'type' => 'unknown',
+        'id' => 'missing',
+        'meta' => [
+          'links' => [
+            'help' => [
+              'href' => 'https://www.drupal.org/docs/8/modules/json-api/core-concepts#missing',
+              'meta' => [
+                'about' => "Usage and meaning of the 'missing' resource identifier.",
+              ],
+            ],
+          ],
+        ],
+      ],
+      [
+        'type' => 'node--journal_conference',
+        'id' => $conference_node->uuid(),
+      ],
+    ], Json::decode((string) $response->getBody())['data']['relationships']['field_mentioned_in']['data']);
+  }
+
+  /**
+   * Ensures that JSON:API routes are caches are dynamically rebuilt.
+   *
+   * Adding a new relationship field should cause new routes to be immediately
+   * regenerated. The site builder should not need to manually rebuild caches.
+   *
+   * @see https://www.drupal.org/project/jsonapi/issues/2984886
+   */
+  public function testThatRoutesAreRebuiltAfterDataModelChangesFromIssue2984886() {
+    $user = $this->drupalCreateUser(['access content']);
+    $request_options = [
+      RequestOptions::AUTH => [
+        $user->getUsername(),
+        $user->pass_raw,
+      ],
+    ];
+
+    $response = $this->request('GET', Url::fromUri('internal:/jsonapi/node/dog'), $request_options);
+    $this->assertSame(404, $response->getStatusCode());
+
+    $node_type_dog = NodeType::create(['type' => 'dog']);
+    $node_type_dog->save();
+    NodeType::create(['type' => 'cat'])->save();
+    \Drupal::service('router.builder')->rebuildIfNeeded();
+
+    $response = $this->request('GET', Url::fromUri('internal:/jsonapi/node/dog'), $request_options);
+    $this->assertSame(200, $response->getStatusCode());
+
+    $this->createEntityReferenceField('node', 'dog', 'field_test', NULL, 'node');
+    \Drupal::service('router.builder')->rebuildIfNeeded();
+
+    $dog = Node::create(['type' => 'dog', 'title' => 'Rosie P. Mosie']);
+    $dog->save();
+
+    $response = $this->request('GET', Url::fromUri('internal:/jsonapi/node/dog/' . $dog->uuid() . '/field_test'), $request_options);
+    $this->assertSame(200, $response->getStatusCode());
+
+    $this->createEntityReferenceField('node', 'cat', 'field_test', NULL, 'node');
+    \Drupal::service('router.builder')->rebuildIfNeeded();
+
+    $cat = Node::create(['type' => 'cat', 'title' => 'E. Napoleon']);
+    $cat->save();
+
+    $response = $this->request('GET', Url::fromUri('internal:/jsonapi/node/cat/' . $cat->uuid() . '/field_test'), $request_options);
+    $this->assertSame(200, $response->getStatusCode());
+
+    FieldConfig::loadByName('node', 'cat', 'field_test')->delete();
+    \Drupal::service('router.builder')->rebuildIfNeeded();
+
+    $response = $this->request('GET', Url::fromUri('internal:/jsonapi/node/cat/' . $cat->uuid() . '/field_test'), $request_options);
+    $this->assertSame(404, $response->getStatusCode());
+
+    $node_type_dog->delete();
+    \Drupal::service('router.builder')->rebuildIfNeeded();
+
+    $response = $this->request('GET', Url::fromUri('internal:/jsonapi/node/dog'), $request_options);
+    $this->assertSame(404, $response->getStatusCode());
+  }
+
+  /**
+   * Ensures denormalizing relationships with aliased field names works.
+   *
+   * @see https://www.drupal.org/project/jsonapi/issues/3007113
+   * @see https://www.drupal.org/project/jsonapi_extras/issues/3004582#comment-12817261
+   */
+  public function testDenormalizeAliasedRelationshipFromIssue2953207() {
+    $this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
+
+    // Since the JSON:API module does not have an explicit mechanism to set up
+    // field aliases, create a strange data model so that automatic aliasing
+    // allows us to test aliased relationships.
+    // @see \Drupal\jsonapi\ResourceType\ResourceTypeRepository::getFieldMapping()
+    $internal_relationship_field_name = 'type';
+    $public_relationship_field_name = 'taxonomy_term_' . $internal_relationship_field_name;
+
+    // Set up data model.
+    $this->createEntityReferenceField(
+      'taxonomy_term',
+      'tags',
+      $internal_relationship_field_name,
+      NULL,
+      'user'
+    );
+    $this->rebuildAll();
+
+    // Create data.
+    Term::create([
+      'name' => 'foobar',
+      'vid' => 'tags',
+      'type' => ['target_id' => 1],
+    ])->save();
+
+    // Test.
+    $user = $this->drupalCreateUser([
+      'edit terms in tags',
+    ]);
+    $body = [
+      'data' => [
+        'type' => 'user--user',
+        'id' => User::load(0)->uuid(),
+      ],
+    ];
+
+    // Test.
+    $response = $this->request('PATCH', Url::fromUri(sprintf('internal:/jsonapi/taxonomy_term/tags/%s/relationships/%s', Term::load(1)->uuid(), $public_relationship_field_name)), [
+      RequestOptions::AUTH => [
+        $user->getUsername(),
+        $user->pass_raw,
+      ],
+      RequestOptions::HEADERS => [
+        'Content-Type' => 'application/vnd.api+json',
+      ],
+      RequestOptions::BODY => Json::encode($body),
+    ]);
+    $this->assertSame(204, $response->getStatusCode());
+  }
+
+  /**
+   * Ensures that Drupal's page cache is effective.
+   *
+   * @see https://www.drupal.org/project/jsonapi/issues/3009596
+   */
+  public function testPageCacheFromIssue3009596() {
+    $anonymous_role = Role::load(RoleInterface::ANONYMOUS_ID);
+    $anonymous_role->grantPermission('access content');
+    $anonymous_role->trustData()->save();
+
+    NodeType::create(['type' => 'emu_fact'])->save();
+    \Drupal::service('router.builder')->rebuildIfNeeded();
+
+    $node = Node::create([
+      'type' => 'emu_fact',
+      'title' => "Emus don't say moo!",
+    ]);
+    $node->save();
+
+    $request_options = [
+      RequestOptions::HEADERS => ['Accept' => 'application/vnd.api+json'],
+    ];
+    $node_url = Url::fromUri('internal:/jsonapi/node/emu_fact/' . $node->uuid());
+
+    // The first request should be a cache MISS.
+    $response = $this->request('GET', $node_url, $request_options);
+    $this->assertSame(200, $response->getStatusCode());
+    $this->assertSame('MISS', $response->getHeader('X-Drupal-Cache')[0]);
+
+    // The second request should be a cache HIT.
+    $response = $this->request('GET', $node_url, $request_options);
+    $this->assertSame(200, $response->getStatusCode());
+    $this->assertSame('HIT', $response->getHeader('X-Drupal-Cache')[0]);
+  }
+
+  /**
+   * Ensures that filtering by a sequential internal ID named 'id' is possible.
+   *
+   * @see https://www.drupal.org/project/jsonapi/issues/3015759
+   */
+  public function testFilterByIdFromIssue3015759() {
+    // Set up data model.
+    $this->assertTrue($this->container->get('module_installer')->install(['shortcut'], TRUE), 'Installed modules.');
+    $this->rebuildAll();
+
+    // Create data.
+    $shortcut = Shortcut::create([
+      'shortcut_set' => 'default',
+      'title' => $this->randomMachineName(),
+      'weight' => -20,
+      'link' => [
+        'uri' => 'internal:/user/logout',
+      ],
+    ]);
+    $shortcut->save();
+
+    // Test.
+    $user = $this->drupalCreateUser([
+      'access shortcuts',
+      'customize shortcut links',
+    ]);
+    $response = $this->request('GET', Url::fromUri('internal:/jsonapi/shortcut/default?filter[drupal_internal__id]=' . $shortcut->id()), [
+      RequestOptions::AUTH => [
+        $user->getUsername(),
+        $user->pass_raw,
+      ],
+    ]);
+    $this->assertSame(200, $response->getStatusCode());
+    $doc = Json::decode((string) $response->getBody());
+    $this->assertNotEmpty($doc['data']);
+    $this->assertSame($doc['data'][0]['id'], $shortcut->uuid());
+    $this->assertSame($doc['data'][0]['attributes']['drupal_internal__id'], (int) $shortcut->id());
+    $this->assertSame($doc['data'][0]['attributes']['title'], $shortcut->label());
+  }
+
+  /**
+   * Ensures datetime fields are normalized using the correct timezone.
+   *
+   * @see https://www.drupal.org/project/jsonapi/issues/2999438
+   */
+  public function testPatchingDateTimeNormalizedWrongTimeZoneIssue3021194() {
+    // Set up data model.
+    $this->assertTrue($this->container->get('module_installer')->install(['datetime'], TRUE), 'Installed modules.');
+    $this->drupalCreateContentType(['type' => 'page']);
+    $this->rebuildAll();
+    FieldStorageConfig::create([
+      'field_name' => 'when',
+      'type' => 'datetime',
+      'entity_type' => 'node',
+      'settings' => ['datetime_type' => DateTimeItem::DATETIME_TYPE_DATETIME],
+    ])
+      ->save();
+    FieldConfig::create([
+      'field_name' => 'when',
+      'entity_type' => 'node',
+      'bundle' => 'page',
+    ])
+      ->save();
+
+    // Create data.
+    $page = Node::create([
+      'title' => 'Stegosaurus',
+      'type' => 'page',
+      'when' => [
+        'value' => '2018-09-16T12:00:00',
+      ],
+    ]);
+    $page->save();
+
+    // Test.
+    $user = $this->drupalCreateUser([
+      'access content',
+    ]);
+    $response = $this->request('GET', Url::fromUri('internal:/jsonapi/node/page/' . $page->uuid()), [
+      RequestOptions::AUTH => [
+        $user->getUsername(),
+        $user->pass_raw,
+      ],
+    ]);
+    $this->assertSame(200, $response->getStatusCode());
+    $doc = Json::decode((string) $response->getBody());
+    $this->assertSame('2018-09-16T22:00:00+10:00', $doc['data']['attributes']['when']);
+  }
+
+  /**
+   * Ensures PATCHing datetime (both date-only & date+time) fields is possible.
+   *
+   * @see https://www.drupal.org/project/jsonapi/issues/3021194
+   */
+  public function testPatchingDateTimeFieldsFromIssue3021194() {
+    $this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
+
+    // Set up data model.
+    $this->assertTrue($this->container->get('module_installer')->install(['datetime'], TRUE), 'Installed modules.');
+    $this->drupalCreateContentType(['type' => 'page']);
+    $this->rebuildAll();
+    FieldStorageConfig::create([
+      'field_name' => 'when',
+      'type' => 'datetime',
+      'entity_type' => 'node',
+      'settings' => ['datetime_type' => DateTimeItem::DATETIME_TYPE_DATE],
+    ])
+      ->save();
+    FieldConfig::create([
+      'field_name' => 'when',
+      'entity_type' => 'node',
+      'bundle' => 'page',
+    ])
+      ->save();
+    FieldStorageConfig::create([
+      'field_name' => 'when_exactly',
+      'type' => 'datetime',
+      'entity_type' => 'node',
+      'settings' => ['datetime_type' => DateTimeItem::DATETIME_TYPE_DATETIME],
+    ])
+      ->save();
+    FieldConfig::create([
+      'field_name' => 'when_exactly',
+      'entity_type' => 'node',
+      'bundle' => 'page',
+    ])
+      ->save();
+
+    // Create data.
+    $page = Node::create([
+      'title' => 'Stegosaurus',
+      'type' => 'page',
+      'when' => [
+        'value' => '2018-12-19',
+      ],
+      'when_exactly' => [
+        'value' => '2018-12-19T17:00:00',
+      ],
+    ]);
+    $page->save();
+
+    // Test.
+    $user = $this->drupalCreateUser([
+      'access content',
+      'edit any page content',
+    ]);
+    $request_options = [
+      RequestOptions::AUTH => [
+        $user->getUsername(),
+        $user->pass_raw,
+      ],
+      RequestOptions::HEADERS => [
+        'Content-Type' => 'application/vnd.api+json',
+        'Accept' => 'application/vnd.api+json',
+      ],
+    ];
+    $node_url = Url::fromUri('internal:/jsonapi/node/page/' . $page->uuid());
+    $response = $this->request('GET', $node_url, $request_options);
+    $this->assertSame(200, $response->getStatusCode());
+    $doc = Json::decode((string) $response->getBody());
+    $this->assertSame('2018-12-19', $doc['data']['attributes']['when']);
+    $this->assertSame('2018-12-20T04:00:00+11:00', $doc['data']['attributes']['when_exactly']);
+    $doc['data']['attributes']['when'] = '2018-12-20';
+    $doc['data']['attributes']['when_exactly'] = '2018-12-19T19:00:00+01:00';
+    $request_options = $request_options + [RequestOptions::JSON => $doc];
+    $response = $this->request('PATCH', $node_url, $request_options);
+    $this->assertSame(200, $response->getStatusCode());
+    $doc = Json::decode((string) $response->getBody());
+    $this->assertSame('2018-12-20', $doc['data']['attributes']['when']);
+    $this->assertSame('2018-12-20T05:00:00+11:00', $doc['data']['attributes']['when_exactly']);
+  }
+
+  /**
+   * Ensure includes are respected even when POSTing.
+   *
+   * @see https://www.drupal.org/project/jsonapi/issues/3026030
+   */
+  public function testPostToIncludeUrlDoesNotReturnIncludeFromIssue3026030() {
+    $this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
+
+    // Set up data model.
+    $this->drupalCreateContentType(['type' => 'page']);
+    $this->rebuildAll();
+
+    // Test.
+    $user = $this->drupalCreateUser(['bypass node access']);
+    $url = Url::fromUri('internal:/jsonapi/node/page?include=uid');
+    $request_options = [
+      RequestOptions::HEADERS => [
+        'Content-Type' => 'application/vnd.api+json',
+        'Accept' => 'application/vnd.api+json',
+      ],
+      RequestOptions::AUTH => [$user->getUsername(), $user->pass_raw],
+      RequestOptions::JSON => [
+        'data' => [
+          'type' => 'node--page',
+          'attributes' => [
+            'title' => 'test',
+          ],
+        ],
+      ],
+    ];
+    $response = $this->request('POST', $url, $request_options);
+    $this->assertSame(201, $response->getStatusCode());
+    $doc = Json::decode((string) $response->getBody());
+    $this->assertArrayHasKey('included', $doc);
+    $this->assertSame($user->label(), $doc['included'][0]['attributes']['name']);
+  }
+
+  /**
+   * Ensure includes are respected even when PATCHing.
+   *
+   * @see https://www.drupal.org/project/jsonapi/issues/3026030
+   */
+  public function testPatchToIncludeUrlDoesNotReturnIncludeFromIssue3026030() {
+    $this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
+
+    // Set up data model.
+    $this->drupalCreateContentType(['type' => 'page']);
+    $this->rebuildAll();
+
+    // Create data.
+    $user = $this->drupalCreateUser(['bypass node access']);
+    $page = Node::create([
+      'title' => 'original',
+      'type' => 'page',
+      'uid' => $user->id(),
+    ]);
+    $page->save();
+
+    // Test.
+    $url = Url::fromUri(sprintf('internal:/jsonapi/node/page/%s/?include=uid', $page->uuid()));
+    $request_options = [
+      RequestOptions::HEADERS => [
+        'Content-Type' => 'application/vnd.api+json',
+        'Accept' => 'application/vnd.api+json',
+      ],
+      RequestOptions::AUTH => [$user->getUsername(), $user->pass_raw],
+      RequestOptions::JSON => [
+        'data' => [
+          'type' => 'node--page',
+          'id' => $page->uuid(),
+          'attributes' => [
+            'title' => 'modified',
+          ],
+        ],
+      ],
+    ];
+    $response = $this->request('PATCH', $url, $request_options);
+    $this->assertSame(200, $response->getStatusCode());
+    $doc = Json::decode((string) $response->getBody());
+    $this->assertArrayHasKey('included', $doc);
+    $this->assertSame($user->label(), $doc['included'][0]['attributes']['name']);
+  }
+
+  /**
+   * Ensure `@FieldType=map` fields are normalized correctly.
+   *
+   * @see https://www.drupal.org/project/jsonapi/issues/3040590
+   */
+  public function testMapFieldTypeNormalizationFromIssue3040590() {
+    $this->assertTrue($this->container->get('module_installer')->install(['entity_test'], TRUE), 'Installed modules.');
+
+    // Create data.
+    $entity = EntityTestMapField::create([
+      'data' => [
+        'foo' => 'bar',
+        'baz' => 'qux',
+      ],
+    ]);
+    $entity->save();
+    $user = $this->drupalCreateUser([
+      'administer entity_test content',
+    ]);
+
+    // Test.
+    $url = Url::fromUri(sprintf('internal:/jsonapi/entity_test_map_field/entity_test_map_field', $entity->uuid()));
+    $request_options = [
+      RequestOptions::AUTH => [$user->getUsername(), $user->pass_raw],
+    ];
+    $response = $this->request('GET', $url, $request_options);
+    $this->assertSame(200, $response->getStatusCode());
+    $data = Json::decode((string) $response->getBody());
+    $this->assertSame([
+      'foo' => 'bar',
+      'baz' => 'qux',
+    ], $data['data'][0]['attributes']['data']);
+    $entity->set('data', [
+      'foo' => 'bar',
+    ])->save();
+    $response = $this->request('GET', $url, $request_options);
+    $this->assertSame(200, $response->getStatusCode());
+    $data = Json::decode((string) $response->getBody());
+    $this->assertSame(['foo' => 'bar'], $data['data'][0]['attributes']['data']);
+  }
+
+}