Problem/Motivation

Quoting Berdir from #2300677-153: JSON:API POST/PATCH support for fully validatable config entities:

But unless I'm blind (which is possible), we have no assertion that the title change was actually *persisted*. The test is happy as long as there is a 200 response, which makes sense because the patch() method isn't really doing anything.

Berdir is right, our test coverage should be stricter.

Proposed resolution

For both POST and PATCH requests, do the following:

  1. Decode the response body. This should be the updated/created entity's normalization.
  2. Load the updated/created entity from entity storage, unchanged. Normalize it. This should be identical to the response body.
  3. Assert that the decoded PATCH/POST request body is a strict subset of the full normalization of the entity, which we got from steps 1+2.

Remaining tasks

Roll patch.

User interface changes

None.

API changes

None.

Data model changes

None.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Wim Leers created an issue. See original summary.

Wim Leers’s picture

Assigned: Wim Leers » Unassigned
Status: Active » Needs review
FileSize
3 KB
Wim Leers’s picture

Title: EntityResourceTestBase: assert that PATCHed and POSTed entities are indeed updated » EntityResourceTestBase: assert that PATCHed and POSTed entities contain the intended values
Wim Leers’s picture

Status: Needs review » Needs work

The last submitted patch, 2: rest_test_patch_and_post-2882717-2.patch, failed testing.

Wim Leers’s picture

Every single hal+json format test is failing. Probably due to _links.

Also, the others that fail are:

  1. Comment
  2. EntityTestLabel
  3. EntityTest
  4. Shortcut
  5. Term
  6. User

Anybody interested in pushing this further?

dawehner’s picture

I might give it a try later ..., let's see :(

Wim Leers’s picture

Interesting that the number of failures is different on 8.3.x vs 8.4.x!

dawehner’s picture

I tried to do some research why it fails ... and well, it turns out the output format of an entity with an entity reference is different to its input for hal_json. Just compare

  protected function getNormalizedPostEntity() {
    return [
      'comment_type' => [
        [
          'target_id' => 'comment',
        ],
      ],
      'entity_type' => [
        [
          'value' => 'entity_test',
        ],
      ],
      'entity_id' => [
        [
          'target_id' => EntityTest::load(1)->id(),
        ],
      ],
      'field_name' => [
        [
          'value' => 'comment',
        ],
      ],
      'subject' => [
        [
          'value' => 'Dramallama',
        ],
      ],
      'comment_body' => [
        [
          'value' => 'Llamas are awesome.',
          'format' => 'plain_text',
        ],
      ],
    ];
  }

the entity_id entry with the bits produced in \Drupal\Tests\hal\Functional\EntityResource\Comment\CommentHalJsonTestBase::getExpectedNormalizedEntity

It feels like \Drupal\Tests\hal\Functional\EntityResource\Comment\CommentHalJsonTestBase::getNormalizedPostEntity is implemented in the wrong way, as it needs some transformation applied, OR the new testcode should call out to getExpectedNormalizedEntity instead.

Wim Leers’s picture

This fixes a whole bunch of the failures, which were happening because there is a discrepancy between what the denormalizers accept vs the normalizers generate: we accept ['type' => 'blah'] because Field API magic automatically maps this to ['type' => ['value' => 'blah']] for single-property field types. Some of the request bodies were using that "feature". Simply changing the request body to their "fully expressed" equivalents solves many of the failures (because it means we can actually check that the request body is a subset of the response body).

Wim Leers’s picture

Similar, yet different: UserResourceTestBase::getNormalizedPostEntity() was using a randomly generated name. This meant that every call to that method returned a different name. Which makes testing impossible. It's also the only entity REST test to do this. So, shifting to a hardcoded name, just like all other tests, is enough.

Wim Leers’s picture

The Term tests are failing in a different place: loading the entity from storage and then normalizing it yields a different result than the normalization we get from decoding the serialization in the response body.

Why?

Turns out that

$entity = $this->entityManager->getStorage($entity_type_id)->create($create_params);

causes $term->fields->parent to be initialized to have this value: 'target_id' => 0. So that's also what we see in the PATCH/POST response body: ['parent' => [['target_id' => 0]].

But if we load a stored term entity, $term->fields->parent does not exist at all, because there's no value set for it. Hence it's normalized to ['parent' => []].

Similar story for $term->description.

A potential work-around is for us to explicitly specify a target_id and a description in our PATCH/POST request body. But turns out that only solves the description case — apparently specifying target_id = 0 specifically still causes a ['parent' => []] normalization! Actually, even setting target_id = 1 causes this. Probably because is configured to use custom storage:

    $fields['parent'] = BaseFieldDefinition::create('entity_reference')
      ->setLabel(t('Term Parents'))
      ->setDescription(t('The parents of this term.'))
      ->setSetting('target_type', 'taxonomy_term')
      ->setCardinality(BaseFieldDefinition::CARDINALITY_UNLIMITED)
      ->setCustomStorage(TRUE);

But then I realized… we already have an issue for this: #2543726: Make $term->parent behave like any other entity reference field, to fix REST and Migrate support and de-customize its Views integration.

The last submitted patch, 10: rest_test_patch_and_post-2882717-10.patch, failed testing. View results

The last submitted patch, 11: rest_test_patch_and_post-2882717-11.patch, failed testing. View results

Wim Leers’s picture

The remaining problems with User are field-access related. The new assertions being added by this patch contain a simple but severe bug:

+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
@@ -856,6 +856,18 @@ public function testPost() {
+    $created_entity_normalization = $this->serializer->normalize($created_entity, static::$format);

This is normalizing the given entity. Content entity normalization checks field access. Field access uses the current user by default. In this environment, there isn't a current user, so it falls back to the anonymous user. That explains why User*AnonTest are passing, but not basic_auth/cookie tests!

So, we need this to normalize with the same user account as our actual tests. That's all :)

The last submitted patch, 12: rest_test_patch_and_post-2882717-12.patch, failed testing. View results

Wim Leers’s picture

#12 should bring it down to 10 failures. #15 to 4.

#8:

Interesting that the number of failures is different on 8.3.x vs 8.4.x!

The reason is simple: \Drupal\Tests\datetime\Functional\EntityResource\EntityTest\EntityTestDateonlyTest and \Drupal\Tests\datetime\Functional\EntityResource\EntityTest\EntityTestDatetimeTest only exist in 8.4.x! And since #10 fixed the failures for EntityTest, those failures were also fixed.


That now only leaves CommentHalJson*Test and ItemHalJson*Test.

Status: Needs review » Needs work

The last submitted patch, 15: rest_test_patch_and_post-2882717-15.patch, failed testing. View results

Wim Leers’s picture

The failing CommentHalJson*Test: for some reason the entity_id field is missing in POST response bodies (so, @dawehner, thank you very much for #9, but AFAICT that's not what is happening :O). I tried reproducing this manually: POSTing a HAL+JSON comment, and stepping through the normalization process to observe why entity_id was missing… only to observe that it's actually generated just fine :/ I have no explanation for this. Other than: WTF?! — at least for now :)

Similarly, the failing ItemHalJson*Test the problem is similar: the fid field is missing in POST response bodies. I haven't done any step-by-step debugging there yet.

If somebody else could push this one over the finish line, that'd be wonderful!

Wim Leers’s picture

Priority: Normal » Major

This is blocking #2300677: JSON:API POST/PATCH support for fully validatable config entities, which is major, hence this is major too.

dawehner’s picture

The failing CommentHalJson*Test: for some reason the entity_id field is missing in POST response bodies (so, @dawehner, thank you very much for #9, but AFAICT that's not what is happening :O). I tried reproducing this manually: POSTing a HAL+JSON comment, and stepping through the normalization process to observe why entity_id was missing… only to observe that it's actually generated just fine :/ I have no explanation for this. Other than: WTF?! — at least for now :)

Well, if you have a look at \Drupal\hal\Normalizer\EntityReferenceItemNormalizer::normalize you'll not see the entity_id to be included, also see \Drupal\Tests\hal\Functional\EntityResource\HalEntityNormalizationTrait::applyHalFieldNormalization:

    // In the HAL normalization, reference fields are omitted, except for the
    // bundle field.
    $bundle_key = $this->entity->getEntityType()->getKey('bundle');
    $reference_fields = array_keys(array_filter($this->entity->getFields(), function (FieldItemListInterface $field) use ($bundle_key) {
      return $field instanceof EntityReferenceFieldItemListInterface && $field->getName() !== $bundle_key;
    }));
    foreach ($reference_fields as $field_name) {
      unset($normalization[$field_name]);
    }
Wim Leers’s picture

Assigned: Unassigned » Wim Leers
Wim Leers’s picture

Assigned: Wim Leers » Unassigned
Status: Needs work » Needs review
FileSize
3.25 KB
8.6 KB

#21 Turns out that's it. I wonder how the hell I managed to observe something else in my manual testing in #19 then… I think I might've confused entity_id and id? (That is quite confusing in Comment entities.)


With that confirmed, let's work on the fix. The root cause of this problem is:

+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
@@ -856,6 +856,18 @@ public function testPost() {
+    // Subset, not same, because we can e.g. send just the target_id for the
+    // bundle in a PATCH request, but the response will include more properties.
+    $this->assertArraySubset($this->getNormalizedPostEntity(), $created_entity_normalization, TRUE);

Using assertArraySubset() is simply a bad assumption: this assumption only holds for the simplest possible normalization, i.e. the default normalization that Drupal uses. But it breaks down for HAL+JSON, as well as for e.g. JSON API.

Then a better way to assert that the sent values are indeed present is by iterating over all fields in the normalization that's being POSTed and then comparing that with the actually stored values.

This reroll brings us to just 3 failures. The remaining 3 failures are in UserHal*Test::testPatch(). Investigating…

Wim Leers’s picture

Those 3 remaining failures had the same root cause as #15, I just only thought of fixing it for testPost() in #15. This then fixes it for testPatch() too.

This is ready for final review.

The last submitted patch, 23: rest_test_patch_and_post-2882717-23.patch, failed testing. View results

tedbow’s picture

Status: Needs review » Reviewed & tested by the community

This looks good to me.
Nice work @Wim Leers and @dawehner

dawehner’s picture

Nice!

Wim Leers’s picture

Whew, so glad to have this be RTBC! It'll be great to finally start pushing #2300677: JSON:API POST/PATCH support for fully validatable config entities forward again, after @dawehner's incredible effort to get that going again!

catch’s picture

+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
@@ -856,6 +856,25 @@ public function testPost() {
     }
     $this->assertFalse($response->hasHeader('X-Drupal-Cache'));
+    // Assert that the entity was indeed created, and that the response body
+    // the serialized created entity.

"and that the response body the". Is it missing 'contains'?

Otherwise looks good though.

Wim Leers’s picture

Indeed that word is missing. It's present for the PATCH test coverage. Fixed :)

catch’s picture

Status: Reviewed & tested by the community » Fixed

Committed/pushed to 8.4.x, thanks!

  • catch committed f3f18a2 on 8.4.x
    Issue #2882717 by Wim Leers, dawehner: EntityResourceTestBase: assert...
Wim Leers’s picture

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

Wim Leers’s picture