We would like to extend the output of #2930028: Comprehensive JSON API integration test coverage phase 1: for every entity type, individual resources only and #2932031: Comprehensive JSON API integration test coverage phase 2: for every entity type, complex use-cases to include tests for complex, JSON API specific use cases.

Theses tests should exercise collections, relationship resources, inclusion of related resources, filters, nested filters, sorts, nested sorts, pagination and sparse fieldsets.

CommentFileSizeAuthor
#76 interdiff-71-74.txt882 bytesgabesullice
#76 2945093-74.patch40.45 KBgabesullice
#71 interdiff-69-71.txt1.3 KBgabesullice
#71 2945093-71.patch40.69 KBgabesullice
#69 interdiff-66-69.txt1.8 KBgabesullice
#69 2945093-69.patch40.58 KBgabesullice
#66 2945093-66.patch39.53 KBgabesullice
#66 interdiff-63-66.txt1.6 KBgabesullice
#63 interdiff-61-63.txt8.07 KBgabesullice
#63 2945093-63.patch39.39 KBgabesullice
#61 2945093-61.patch37.68 KBgabesullice
#61 interdiff-60-61.txt45.02 KBgabesullice
#60 interdiff-58-60.txt25.96 KBgabesullice
#60 2945093-60.patch32.75 KBgabesullice
#58 2945093-58.patch26.58 KBgabesullice
#58 interdiff-57-58.txt18.98 KBgabesullice
#57 interdiff-53-56.txt9.05 KBgabesullice
#57 2945093-56.patch22.66 KBgabesullice
#53 2945093-53.patch23.02 KBgabesullice
#51 interdiff-commit.txt1.46 KBe0ipso
#41 2945093-41.patch15.67 KBgabesullice
#41 interdiff-39-41.txt1.88 KBgabesullice
#39 2945093-39.patch16.03 KBgabesullice
#39 interdiff-37-39.txt14.16 KBgabesullice
#37 interdiff-25-37.txt8.69 KBgabesullice
#37 2945093-37.patch15.37 KBgabesullice
#31 2945093-31.patch39.87 KBgabesullice
#31 interdiff-25-31.txt37.05 KBgabesullice
#25 2945093-25.patch18.43 KBgabesullice
#25 interdiff-24-25.patch1.43 KBgabesullice
#24 2945093-24.patch18.98 KBgabesullice
#24 interdiff-22-24.txt2.93 KBgabesullice
#22 2945093-22.patch18.51 KBgabesullice
#22 interdiff-21-22.txt4.22 KBgabesullice
#21 2945093-21.patch17.2 KBgabesullice
#19 interdiff-17-19.txt13.68 KBgabesullice
#19 2945093-19.patch17.18 KBgabesullice
#17 2945093-17.patch5.81 KBgabesullice
#17 interdiff-14-17.txt651 bytesgabesullice
#14 2945093-14.patch5.74 KBgabesullice
#14 interdiff-10-14.txt6.44 KBgabesullice
#10 2945093-9.patch2.14 KBgabesullice
#10 interdiff-2945093-9.txt533 bytesgabesullice
#9 2945093-8.patch2.44 KBgabesullice

Comments

Wim Leers created an issue. See original summary.

wim leers’s picture

Title: [PP-1] Comprehensive JSON API integration test coverage phase 3: test JSON API-specific use cases such as collections, includes, relationships, sparse fieldsets … » Comprehensive JSON API integration test coverage phase 3: test JSON API-specific use cases such as collections, includes, relationships, sparse fieldsets …
Status: Postponed » Active

#2945093: Comprehensive JSON API integration test coverage phase 3: test JSON API-specific use cases: related/relationship routes, includes and sparse field sets landed!

I think that a good starting point could be to move much of \Drupal\Tests\jsonapi\Functional\JsonApiFunctionalTest into ResourceTestBase, to e.g. do testGetCollection().

wim leers’s picture

Note that I do not intend to take this on for the time being. I think this is more in @e0ipso and @gabesullice's ball park — they're more deeply familiar with how these are supposed to work, which edge cases are worth testing, and so on.

e0ipso’s picture

The challenge here is the word comprehensive. There are just too many combinations to be able to do it for everything.

I'd choose some good representatives of each case and call it tested. We probably have much of these in the existing tests, we need to vet those and move them over first.

@gabesullice would you agree as a first step?

wim leers’s picture

Interpret comprehensive as as comprehensive as is reasonable and useful. It's impossible to test 100% of possible cases. For example, ResourceTestBase::testPatchIndividual() does this:

    // Multi-value field: remove item 0. Then item 1 becomes item 0.
    $doc_multi_value_tests = $this->getPatchDocument();
    $doc_multi_value_tests['data']['attributes']['field_rest_test_multivalue'] = $this->entity->get('field_rest_test_multivalue')->getValue();
    $doc_remove_item = $doc_multi_value_tests;
    unset($doc_remove_item['data']['attributes']['field_rest_test_multivalue'][0]);
    $request_options[RequestOptions::BODY] = Json::encode($doc_remove_item, 'api_json');
    $response = $this->request('PATCH', $url, $request_options);
    $this->assertResourceResponse(200, FALSE, $response, Cache::mergeTags($this->entity->getCacheTags(), ['http_response']), $this->getExpectedCacheContexts());
    $this->assertSame([0 => ['value' => 'Two']], $this->entityStorage->loadUnchanged($this->entity->id())->get('field_rest_test_multivalue')->getValue());

    // Multi-value field: add one item before the existing one, and one after.
    $doc_add_items = $doc_multi_value_tests;
    $doc_add_items['data']['attributes']['field_rest_test_multivalue'][2] = ['value' => 'Three'];
    $request_options[RequestOptions::BODY] = Json::encode($doc_add_items);
    $response = $this->request('PATCH', $url, $request_options);
    $this->assertResourceResponse(200, FALSE, $response, Cache::mergeTags($this->entity->getCacheTags(), ['http_response']), $this->getExpectedCacheContexts());
    $expected = [
      0 => ['value' => 'One'],
      1 => ['value' => 'Two'],
      2 => ['value' => 'Three'],
    ];
    $this->assertSame($expected, $this->entityStorage->loadUnchanged($this->entity->id())->get('field_rest_test_multivalue')->getValue());

That's testing an artificial field by first removing a particular item, and then adding one item before and item after. It's not doing this for every possible field. It's not doing this for an actual field. It's testing the mechanics.

I think that for the JSON API-specific capabilities, testing the mechanics is the most important thing.


I'd choose some good representatives of each case and call it tested.

Exactly :) This sounds perfect!

gabesullice’s picture

@e0ipso, I really like the design pattern @Wim Leers came up with for testing all available types (by extending from a base class). If we could replicate that pattern to ensure that we're not missing subtle edge cases, that would be ideal. But I agree that any first step must necessarily start with just one resource type as a representative case.

gabesullice’s picture

Assigned: Unassigned » gabesullice

yoink!

wim leers’s picture

Go Gabe! 🤘

gabesullice’s picture

Status: Active » Needs review
StatusFileSize
new2.44 KB

Here is a really small start, very simple testing of sparse field sets. Not sure how it'll play with all the types, but it works for nodes :P so we'll see!

gabesullice’s picture

StatusFileSize
new533 bytes
new2.14 KB

Whoops, that wasn't supposed to be in there.

The last submitted patch, 9: 2945093-8.patch, failed testing. View results

Status: Needs review » Needs work

The last submitted patch, 10: 2945093-9.patch, failed testing. View results

wim leers’s picture

  1. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1422,4 +1425,31 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +  protected function doTestSparseFieldSets($request_options) {
    ...
    +      $url = Url::fromRoute(
    +        sprintf('jsonapi.%s.individual', static::$resourceTypeName),
    +        [static::$entityTypeId => $this->entity->uuid()],
    +        ['query' => ['fields[' . static::$resourceTypeName . ']' => implode(',', $field_set)]]
    +      );
    

    Rather than regenerating a URL from scratch, you can pass the $url that the caller was already working with.

    Then you can just do $url->setOption('query', …);.

    Less code!

  2. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1422,4 +1425,31 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +      $this->setUpAuthorization('GET');
    

    This is not necessary: it already happened int he caller's code.

  3. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1422,4 +1425,31 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +      // Cache MISS because cache should vary based on the 'field' query param.
    +      $this->assertResourceResponse(200, FALSE, $response, $this->getExpectedCacheTags(), $this->getExpectedCacheContexts(), FALSE, 'MISS');
    

    😍

gabesullice’s picture

Status: Needs work » Needs review
StatusFileSize
new6.44 KB
new5.74 KB

This simplifies document assertions by recursively sorting the inputs. This means object key order cannot be guaranteed (but JSON object order isn't guaranteed anyway and arrays aren't affected).

It also makes the suggestions @Wim Leers had in #13.

This is still a WIP... Needs Review is just for tests for now.

wim leers’s picture

+++ b/tests/src/Functional/ResourceTestBase.php
@@ -463,6 +466,19 @@ abstract class ResourceTestBase extends BrowserTestBase {
+  protected function assertDocumentResponse(array $document, ResponseInterface $response) {

Rather than adding this, I think we should update assertResourceResponse(), shouldn't we?

You're also of course right that assertResourceResponse() is a misnomer, it should be renamed to assertDocumentResponse(). Same for the error one.

wim leers’s picture

(FYI: I think #15 can also happen in a follow-up — probably that's even better, because then this patch is only adding test coverage rather than refactoring existing test coverage. Just add a @todo to the new assertDocumentResponse()!)

gabesullice’s picture

StatusFileSize
new651 bytes
new5.81 KB
+++ b/tests/src/Functional/TermTest.php
@@ -385,11 +385,7 @@ class TermTest extends ResourceTestBase {
-    $expected = $this->getExpectedDocument();
-    static::recursiveKSort($expected);
-    $actual = Json::decode((string) $response->getBody());
-    static::recursiveKSort($actual);
-    $this->assertSame($expected, $actual);
+    $this->assertDocumentResponse($this->getExpectedDocument(), $response);

#16: I did try that for a while, but we have things like this outside assertResourceResponse() that I was trying to simplify. However, I agree there's probably a simpler generalization here, I just haven't been able to find it yet. Added a todo, to do that :P

Status: Needs review » Needs work

The last submitted patch, 17: 2945093-17.patch, failed testing. View results

gabesullice’s picture

Status: Needs work » Needs review
StatusFileSize
new17.18 KB
new13.68 KB

Posting some more progress now that other things have landed. This adds testing of the include query parameter. This patch does not yet cover "nested" includes.

This has a one-line change to prevent adding the `included` member when the `included` array would be empty. This is only possible when/if the included resource was removed for access reasons.

--- a/src/Normalizer/JsonApiDocumentTopLevelNormalizer.php
+++ b/src/Normalizer/JsonApiDocumentTopLevelNormalizer.php
@@ -203,7 +203,6 @@ class JsonApiDocumentTopLevelNormalizer extends NormalizerBase implements Denorm
     $normalized = $value_extractor->rasterizeValue();
     $included = array_filter($value_extractor->rasterizeIncludes());
     if (!empty($included)) {
-      $normalized['included'] = [];
       foreach ($included as $included_item) {
         if ($included_item['data'] === FALSE) {
           unset($included_item['data']);

Status: Needs review » Needs work

The last submitted patch, 19: 2945093-19.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

gabesullice’s picture

StatusFileSize
new17.2 KB

I haven't been able to reproduce any of those errors. Trying to upload another patch, because WTH.

gabesullice’s picture

StatusFileSize
new4.22 KB
new18.51 KB
  1. +++ b/src/Normalizer/Value/JsonApiDocumentTopLevelNormalizerValue.php
    @@ -168,10 +168,13 @@ class JsonApiDocumentTopLevelNormalizerValue implements ValueExtractorInterface,
    -      $unique_key = $rasterized_include['data'] === FALSE ?
    -        $rasterized_include['meta']['errors'][0]['detail'] :
    -        $rasterized_include['data']['type'] . ':' . $rasterized_include['data']['id'];
    -      $unique_includes[$unique_key] = $include;
    +      if ($rasterized_include['data'] === FALSE) {
    +        $unique_includes[] = $include;
    +      }
    +      else {
    +        $unique_key = $rasterized_include['data']['type'] . ':' . $rasterized_include['data']['id'];
    +        $unique_includes[$unique_key] = $include;
    +      }
    

    By "de-duping" on the error detail, we lose error information when the same error happens for multiple included entities. This makes sure that we keep them.

    FWIW, I actually think we should be combining a lot of these into more informative single error messages, but we should make that decision explicitly in #2943176: Spec Compliance: when error object 'id' key exists, it contains the individual resource URI, instead of "a unique identifier for this particular occurrence of the problem". This just resolves the inconsistent error behavior between included and related routes (which should mirror each other).

  2. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1712,18 +1712,21 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +      // @todo uncomment this assertion in https://www.drupal.org/project/jsonapi/issues/2929428
    ...
    -      $this->assertResourceResponse(
    ...
    +      // $this->assertResourceResponse(
    

    Many of the previous errors are due to the fact that the related endpoint does not throw/return cacheable exceptions. However, cache data is kept for included items.

    These tests operate on the basis of included being a mirror of related routes. Therefore, when related routes don't return cache metadata, we can't assert that includes have correct cache metadata (even though they do!).

wim leers’s picture

Nice progress! And fewer failures :)

gabesullice’s picture

Status: Needs work » Needs review
StatusFileSize
new2.93 KB
new18.98 KB

Alright, I finally was able to replicate those errors locally (when xdebug.show_error_trace = 1 in php.ini, exceptions get caught before they make it to the functional tests!).

That helped me narrow the cause down to 2 known issues:

  1. #2940339: Port reference field support for non-empty entity reference fields not pointing to an entity from #2543726
  2. #2930217: getRelated() not working when related entity field is empty

Once those are resolved, we'll be able to remove the try catch block and add include test coverage for BlockContent and Comment entities.

If all tests pass. Next up, `related` route test coverage!

gabesullice’s picture

StatusFileSize
new1.43 KB
new18.43 KB

Whoops, left a few debugging things in there.

The last submitted patch, 24: 2945093-24.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

The last submitted patch, 25: interdiff-24-25.patch, failed testing. View results

wim leers’s picture

This looks GREAT!

  1. +++ b/src/Normalizer/Value/JsonApiDocumentTopLevelNormalizerValue.php
    @@ -168,10 +168,13 @@ class JsonApiDocumentTopLevelNormalizerValue implements ValueExtractorInterface,
    -      $unique_key = $rasterized_include['data'] === FALSE ?
    -        $rasterized_include['meta']['errors'][0]['detail'] :
    -        $rasterized_include['data']['type'] . ':' . $rasterized_include['data']['id'];
    -      $unique_includes[$unique_key] = $include;
    +      if ($rasterized_include['data'] === FALSE) {
    +        $unique_includes[] = $include;
    +      }
    +      else {
    +        $unique_key = $rasterized_include['data']['type'] . ':' . $rasterized_include['data']['id'];
    +        $unique_includes[$unique_key] = $include;
    +      }
    

    Is this a pure simplification or a bugfix?

  2. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -461,7 +461,10 @@ abstract class ResourceTestBase extends BrowserTestBase {
    -        $this->assertSame($expected_body, (string) $response->getBody());
    +        $this->assertSame(
    +          static::sortJsonString($expected_body),
    +          static::sortJsonString((string) $response->getBody())
    +        );
    

    The intent here was that if you need to do sorting/deep analysis of the response body, that you pass FALSE to skip testing of the response body here (i.e. skip this assertion).

    Then you can just write your own assertions after doing $this->assertResourceResponse():

    $doc = Json::decode((string) $response->getBody());
    // assert things in $doc
    

    Would you rather have it not work that way? I can live with that.

  3. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -474,7 +477,8 @@ abstract class ResourceTestBase extends BrowserTestBase {
    -      $this->assertSame($expected_cache_contexts, explode(' ', $response->getHeader('X-Drupal-Cache-Contexts')[0]));
    +      $optimized_expected_cache_contexts = \Drupal::service('cache_contexts_manager')->optimizeTokens($expected_cache_contexts);
    +      $this->assertSame($optimized_expected_cache_contexts, explode(' ', $response->getHeader('X-Drupal-Cache-Contexts')[0]));
    

    This means that the expected cache contexts may be a superset of what you get back. I'd much prefer to update getExpectedCacheContexts(), so that we can again do the simple straight comparison, so that what a specific test is expecting, is also actually what the response contains!

  4. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -715,11 +734,7 @@ abstract class ResourceTestBase extends BrowserTestBase {
         // Sort the serialization data first so we can do an identical comparison
         // for the keys with the array order the same (it needs to match with
         // identical comparison).
    -    $expected = $this->getExpectedDocument();
    -    static::recursiveKsort($expected);
    -    $actual = Json::decode((string) $response->getBody());
    -    static::recursiveKsort($actual);
    -    $this->assertSame($expected, $actual);
    

    But then we should just remove this altogether, and update

        // 200 for well-formed GET request. Page Cache hit because of HEAD request.
        // Same for Dynamic Page Cache hit.
        $response = $this->request('GET', $url, $request_options);
        $this->assertResourceResponse(200, FALSE, $response, …
    

    to:

        // 200 for well-formed GET request. Page Cache hit because of HEAD request.
        // Same for Dynamic Page Cache hit.
        $response = $this->request('GET', $url, $request_options);
        $this->assertResourceResponse(200, Json::encode($this->getExpectedDocument()), $response, …
    
  5. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -797,6 +812,19 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +    catch (\Exception $e) {
    ...
    +      $this->markTestSkipped("Remove this try/catch in https://www.drupal.org/project/jsonapi/issues/2930217 and/or https://www.drupal.org/project/jsonapi/issues/2940339");
    

    Can we catch a more specific exception and then not mark this test as skipped, but rather complete the remainder of the test coverage?

  6. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1573,4 +1615,286 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +  protected function doTestSparseFieldSets(Url $url, array $request_options) {
    ...
    +  public function testIncluded() {
    

    Why does one have its own test method, and the other is just tested as part of testGetIndividual()?

  7. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1573,4 +1615,286 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +      'some' => array_slice($field_names, floor(count($field_names) / 2)),
    

    This means that if content entity types get more base fields, the test coverage would also be testing different things. Probably okay, but not ideal. Of course, testing every possible combination makes even less sense.

    However, wouldn't a more particularly selected subset make more sense? E.g. not listing the uuid field here should still result in the id entry per the JSON API standard, but then would probably have to omit the attributes.uuid entry?

  8. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1573,4 +1615,286 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +      // 'self' link should include the 'fields' query param.
    +      $expected_body['links']['self'] = $url->setAbsolute()->toString();
    

    ❤️

  9. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1573,4 +1615,286 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +      // Cache MISS because cache should vary based on the 'field' query param.
    ...
    +    // Test cache HIT for a query with the same field set.
    

    ❤️

  10. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1573,4 +1615,286 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +    $field_names = isset($individual_response_document['data']['relationships'])
    

    Nit: $relationship_field_names.

  11. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1573,4 +1615,286 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +  protected static function decorateExpectedResponseForIncludedFields(array &$expected_response, array $related_responses) {
    ...
    +        // If any of the related response documents had top-level errors, we
    +        // should later expect the document to have 'meta' errors too.
    ...
    +        if (array_key_exists('type', $related_data) && array_key_exists('id', $related_data)) {
    

    ❤️

  12. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1573,4 +1615,286 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +  protected function getRelatedResponses(array $links, array $request_options) {
    

    ❤️

  13. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1573,4 +1615,286 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +      $cacheable_metadata = new CacheableMetadata();
    

    Nit: s/cacheable_metadata/cacheability/

gabesullice’s picture

1. Bugfix. See #22.

2. That's absolutely fine. I think I misinterpreted your remark in #15 "Rather than adding this, I think we should update assertResourceResponse(), shouldn't we?"

3. That's fine.

4. Yeah, as I said in #17 the whole document assertion thing requires me to take some time to think about it. I'll do that in the next patch.

5. Unfortunately, no. It's a PHP fatal error and it's just a raw exception. I agree that I should have kept the tests going. I intended to but then forgot. BUT... it shouldn't matter anymore because #2930217: getRelated() not working when related entity field is empty is already RTBC and will fix these tests!

6. See #25, that was a leftover.

7. You're correct, changing the base fields will change these tests. But that's okay. The "empty" and "all" tests make sure we never lose coverage for any particular field and that the absence of a field (like UUID) doesn't break the output. I just wanted to ensure that one could select a subset of fields. I'm not following your remark about UUID and ID (I think you might be forgetting that the ID lives outside the attributes key).

10/13. Will fix!

wim leers’s picture

#29.7: I'm saying that if you specify the sparse fieldset title,field_tags, that you should get:

{
  id: …,
  attributes: {
    title: …,
    field_tags: …
  },
  …
}

Note how attributes.uuid is absent (because not listed in the sparse fieldset), even though id is/must be always included. So my point was that that is probably an edge case worth testing.

gabesullice’s picture

StatusFileSize
new37.05 KB
new39.87 KB

Alright, this refactors the assertDocument thing. I did do recursive sorting in assertResourceResponse. It makes sense to have it just run for every assertion as long as you can assert an _empty_ body too (which I added). One should only care about order for JSON arrays, but they are not affected by recursiveKSort because PHP decodes them as indexed arrays (without string keys). Thus, their order will be preserved.

I think this should also address all other nits and concerns from #28 too.

Status: Needs review » Needs work

The last submitted patch, 31: 2945093-31.patch, failed testing. View results

gabesullice’s picture

+++ b/tests/src/Functional/ResourceTestBase.php
@@ -428,8 +428,9 @@ abstract class ResourceTestBase extends BrowserTestBase {
-   * @param string|false $expected_body
-   *   The expected response body. FALSE in case this should not be asserted.
+   * @param array|null|false $expected_document
+   *   The expected document or NULL if there should be not be a response body.
+   *   FALSE in case this should not be asserted.

This is the primary difference here.

wim leers’s picture

  1. +++ b/tests/src/Functional/NodeTest.php
    @@ -313,7 +313,7 @@ class NodeTest extends ResourceTestBase {
    -    $expected = [
    +    $expected_document = [
    
    @@ -330,7 +330,7 @@ class NodeTest extends ResourceTestBase {
    -    $this->assertResourceResponse(403, Json::encode($expected), $response);
    +    $this->assertResourceResponse(403, $expected_document, $response);
    

    This is a bit too much of a refactor to include in this patch IMO: it's touching pretty much all existing integration test coverage! Let's split this off into a separate issue, and we can get it committed very quickly there.

  2. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -428,8 +428,9 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +   *   The expected document or NULL if there should be not be a response body.
    

    Nit: s/be not be/not be/ :)

    Other than that: ❤️

  3. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -460,11 +461,14 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +          $this->assertTrue(is_null($response_document));
    

    Nit: assertNull()

  4. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -503,16 +506,15 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +  protected function assertSameDocument(array $expected_document, array $actual_document) {
    

    ❤️

  5. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1793,6 +1762,22 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +   * Returns an array of sparse fields sets to test.
    ...
    +  protected function getSparseFieldSets() {
    

    This means every subclass can test different sets of sparse field sets. Handy for regression tests. Cool!

wim leers’s picture

Title: Comprehensive JSON API integration test coverage phase 3: test JSON API-specific use cases such as collections, includes, relationships, sparse fieldsets … » [PP-1] Comprehensive JSON API integration test coverage phase 3: test JSON API-specific use cases such as collections, includes, relationships, sparse fieldsets …
Related issues: +#2949136: Refactor assertResourceResponse test method to be more flexible.
wim leers’s picture

Title: [PP-1] Comprehensive JSON API integration test coverage phase 3: test JSON API-specific use cases such as collections, includes, relationships, sparse fieldsets … » Comprehensive JSON API integration test coverage phase 3: test JSON API-specific use cases such as collections, includes, relationships, sparse fieldsets …

#2949136: Refactor assertResourceResponse test method to be more flexible. is in!

That should make this patch quite a bit smaller and simpler!

gabesullice’s picture

StatusFileSize
new15.37 KB
new8.69 KB

This is the updated patch with #2949136: Refactor assertResourceResponse test method to be more flexible. in.

The interdiff is from #25 now, and ignores #31.

Tests should pass when #2930217: getRelated() not working when related entity field is empty lands.

After that, I think we should commit this if @Wim Leers and @e0ipso agree. That will unblock #2940342: Cacheability metadata on an entity fields' properties is lost.

Then I can add coverage for related routes and collections.

wim leers’s picture

After that, I think we should commit this if @Wim Leers and @e0ipso agree. That will unblock #2940342: Cacheability metadata on an entity fields' properties is lost.

Then I can add coverage for related routes and collections.

👍


Final review time!

  1. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1586,4 +1592,292 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +      // Cache MISS because cache should vary based on the 'field' query param.
    ...
    +      // Cache MISS because cache should vary based on the 'include' query
    

    Nit: s/Cache MISS/Dynamic Page Cache miss/

  2. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1586,4 +1592,292 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +    // Test cache HIT for a query with the same field set.
    

    Nit: s/cache HIT/Dynamic Page Cache hit/

  3. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1586,4 +1592,292 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +    $relationship_field_names = isset($individual_response_document['data']['relationships'])
    

    Note that this will only return the accessible relationship fields!

    For example, UserTest does not list roles in \Drupal\Tests\jsonapi\Functional\UserTest::getExpectedDocument() because the GET test coverage can be performed with just the access user profiles permission. To view the roles field, one must have administer users permission.

    This is fine, but it does mean we're not testing everything. Wanted you to be aware of that.

    Theoretically, that also means this should be renamed to $accessible_relationship_field_names.

  4. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1586,4 +1592,292 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +    // If there are no relationships, we can't include anything.
    

    no accessible relationships

  5. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1586,4 +1592,292 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +      $about_half_the_fields = floor(count($relationship_field_names) / 2);
    

    Love this variable name :P

  6. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1586,4 +1592,292 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +      // The expected response is based of the expected individual response.
    

    s/of/on/
    s/individual response/individual responses/

    I think?

  7. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1586,4 +1592,292 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +      $expected_response = [
    +        'document' => $this->getExpectedDocument(),
    +        'cacheability' => (new CacheableMetadata())
    +          ->setCacheContexts($this->getExpectedCacheContexts())
    +          ->setCacheTags($this->getExpectedCacheTags()),
    +      ];
    ...
    +   *   The expected response. This should be an array with a 'document' key and
    +   *   corresponding JSON API document array and a 'cacheability' key with a
    

    Rather than this work-around for the fact that PHP is a limited language where you don't have the equivalent of pair, you could also use the \Drupal\jsonapi\ResourceResponse value object which allows you to pass exactly this kind of data :)

  8. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1586,4 +1592,292 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +      // param. It's not possible to know the order of the included resources,
    +      // thus it is not possible to assert the expected body.
    ...
    +        $this->assertSame(
    +          Json::encode(static::sortResourceCollection($expected_response['document']['included'])),
    +          Json::encode(static::sortResourceCollection($response_document['included']))
    +        );
    ...
    +  protected static function sortResourceCollection(array &$resources) {
    

    Can this now not use \Drupal\Tests\jsonapi\Functional\ResourceTestBase::assertSameDocument()?

  9. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1586,4 +1592,292 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +   * This adds the expected included to the expected document and also builds
    

    s/included/includes/

  10. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1586,4 +1592,292 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +   * the expected cache metadata. It does so based of responses from the related
    

    s/cache metadata/cacheability/

  11. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1586,4 +1592,292 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +   *   corresponding cacheable metadata object.
    

    s/cacheable metadata/CacheableMetadata/

  12. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1586,4 +1592,292 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +      /* @var \Drupal\Core\Cache\CacheableMetadata $related_cacheability */
    +      $related_cacheability = $related_response['cacheability'];
    

    The annotation does not match.

  13. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1586,4 +1592,292 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +  protected static function extractRelatedLinks(array $field_names, array $document) {
    

    static 👍

  14. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1586,4 +1592,292 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +      $related_response = $this->request('GET', Url::fromUri($links[$field_name]), $request_options);
    

    Nice!

  15. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1586,4 +1592,292 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +      $cacheability = new CacheableMetadata();
    ...
    +      $related_responses[$field_name] = [
    +        'document' => Json::decode($related_response->getBody()),
    +        'cacheability' => $cacheability,
    +      ];
    

    Same as above: you could use \Drupal\jsonapi\ResourceResponse to make it easier to pass this around!

gabesullice’s picture

Status: Needs work » Needs review
StatusFileSize
new14.16 KB
new16.03 KB

1. ✔️
2. ✔️
3. Yes, that's an excellent point. I updated the field names and added a @todo to add explicit coverage for that. I just did a manual test of this to be sure that we are currently doing this right. That won't stop a regression of course.
4. ✔️
5. 😀
6.
s/of/on/ ✔️
s/individual response/individual responses/ Nope, but I've updated the comment to be more clear (hopefully).
7. This was an excellent suggestion. ✔️
8. Yep! Good thought. We still need to do some "prep" before hand, like updating the self link, but we weren't even checking that before! Now we are :)
9. ✔️
10. ✔️
11. ✔️
12. ✔️
13. :)
14. :)
15. ✔️

Status: Needs review » Needs work

The last submitted patch, 39: 2945093-39.patch, failed testing. View results

gabesullice’s picture

StatusFileSize
new1.88 KB
new15.67 KB

Forgot to update some annotation comments.

wim leers’s picture

This looks lovely now!

  1. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1586,4 +1592,305 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +  /**
    +   * Tests sparse field sets.
    +   *
    +   * @param \Drupal\Core\Url $url
    +   *   The base URL with which to test includes.
    +   * @param array $request_options
    +   *   Request options to apply.
    +   */
    ...
    +  /**
    +   * Tests included resources.
    +   *
    +   * @param \Drupal\Core\Url $url
    +   *   The base URL with which to test includes.
    +   * @param array $request_options
    +   *   Request options to apply.
    +   */
    ...
    +   * @param array $request_options
    +   *   Request options to apply.
    

    ❓ In ::request() we have a

    @see \GuzzleHttp\ClientInterface::request()
    

    Perhaps we want the same here? Not sure.

  2. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1586,4 +1592,305 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +      $this->assertResourceResponse(
    +        200,
    +        $expected_document,
    +        $response,
    +        $this->getExpectedCacheTags(),
    +        $this->getExpectedCacheContexts(),
    +        FALSE,
    +        'MISS'
    +      );
    

    🤐 Supernit: this is technically complying with the coding standards, but is hugely distracting. We put this on a single line everywhere else. I think we should do the same here.

wim leers’s picture

Tests should pass when #2930217: getRelated() not working when related entity field is empty lands.

After that, I think we should commit this if @Wim Leers and @e0ipso agree. That will unblock #2940342: Cacheability metadata on an entity fields' properties is lost.

Then I can add coverage for related routes and collections.

That means we need a phase 4 issue and an issue title+summary update. The phase 4 issue should also be added to #2931785: The path for JSON API to core.

gabesullice’s picture

That means we need a phase 4 issue and an issue title+summary update.

That seems burdensome. I think we can just have multiple commits to the same issue. I would not classify my work here as a complete "phase".

I spoke with @Wim Leers in chat about this and he said:

That’s interesting... multiple commits per issue. [1 commit per issue] makes it much easier to find the discussion associated with a commit but I don’t feel strongly about it

We can add comment numbers to our commits to help track down history :) so let's save ourselves a little overhead.

wim leers’s picture

Title: Comprehensive JSON API integration test coverage phase 3: test JSON API-specific use cases such as collections, includes, relationships, sparse fieldsets … » [PP-1] Comprehensive JSON API integration test coverage phase 3: test JSON API-specific use cases such as collections, includes, relationships, sparse fieldsets …
Issue tags: +blocker
e0ipso’s picture

e0ipso’s picture

Title: [PP-1] Comprehensive JSON API integration test coverage phase 3: test JSON API-specific use cases such as collections, includes, relationships, sparse fieldsets … » Comprehensive JSON API integration test coverage phase 3: test JSON API-specific use cases such as collections, includes, relationships, sparse fieldsets …
gabesullice’s picture

Status: Needs work » Needs review

@e0ipso, thanks! Since you're active, will you take a look at this one too? If tests pass on #41, perhaps #42 could be addressed on commit if you have no other concerns.

The last submitted patch, 37: 2945093-37.patch, failed testing. View results

  • e0ipso committed 89566f5 on 8.x-1.x authored by gabesullice
    Issue #2945093 by gabesullice, Wim Leers, e0ipso: Comprehensive JSON API...
e0ipso’s picture

Status: Needs review » Fixed
StatusFileSize
new1.46 KB

Getting really close! Thanks for all the hard work gentlemen 🎊

wim leers’s picture

Status: Fixed » Needs work

Nice, thanks! :)

Moving back to NW per #44 — @gabesullice is planning more work here :)

gabesullice’s picture

Title: Comprehensive JSON API integration test coverage phase 3: test JSON API-specific use cases such as collections, includes, relationships, sparse fieldsets … » [PP-1] Comprehensive JSON API integration test coverage phase 3: test JSON API-specific use cases such as collections, includes, relationships, sparse fieldsets …
Status: Needs work » Postponed
Issue tags: -blocker +blocked
Related issues: +#2940339: Port reference field support for non-empty entity reference fields not pointing to an entity from #2543726
StatusFileSize
new23.02 KB

The attached covers related routes. It should pass for all test classes except BlockContent and Comments. That is due to a known bug: #2940339: Port reference field support for non-empty entity reference fields not pointing to an entity from #2543726

Therefore, moving this to postponed and blocked. I'll manually queue tests. Feel free to review, however :)

wim leers’s picture

Looking great!

  1. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -659,7 +659,7 @@ abstract class ResourceTestBase extends BrowserTestBase {
    -    $request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions('GET'));
    +    $request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions());
    

    Seems like an accidental change?

  2. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -832,6 +832,193 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +    // Builds an array of expected responses, keyed by relationship field name.
    +    $expected_related_responses = $this->getExpectedRelatedResponses($url, $individual_response, $request_options, $accessible_relationship_field_names);
    +    // Fetches actual responses as an array keyed by relationship field name.
    +    $related_responses = $this->getRelatedResponses($individual_response, $request_options, $accessible_relationship_field_names);
    +    foreach ($accessible_relationship_field_names as $accessible_relationship_field_name) {
    ...
    +  protected function getExpectedRelatedResponses(Url $individual_url, ResourceResponse $individual_response, array $request_options, array $relationship_field_names) {
    

    ❤️

    This is of course the central part of this patch, and it takes some reading, but then it totally makes sense! 👏

  3. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -832,6 +832,193 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +        // Relationship data is accessible if the field is accessible, but the
    +        // individual targeted resource still may be inaccessible.
    

    Glad to see this being called out explicitly!

    But I think you meant to add something like "So we must request the individual resource for each resource related identifier, so that we can construct the expected 'related' response."

  4. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -832,6 +832,193 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +      // We need to extract the document so that we can make some modifcations.
    

    s/modifcations/modifications/

    Also: which modifications?

  5. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -832,6 +832,193 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +  protected static function mergeResourceResponses(array $responses, $self_link) {
    

    It seems that this method along with the toResourceResponse() etc helper method really would benefit from being moved out of this base test class.

    Because this class is growing to an enormous size.

    What do you think about moving them all to a ResourceResponseTestTrait?

  6. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -832,6 +832,193 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +    // 'self' link since it implies support for the route.
    

    "since it implies support for the route" -> not sure how to interpret this. Could you try to clarify that comment?

  7. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1788,63 +1967,231 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +   * @todo Remove URL and request options parameters and derived fields from the
    +   *   entity instead of a request.
    

    Nit: Missing a verb. "and get derived", I think?

  8. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1788,63 +1967,231 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +   *   The type of link to build. Either 'relationship' or 'related'.
    

    s/build/get/

  9. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1788,63 +1967,231 @@ abstract class ResourceTestBase extends BrowserTestBase {
    -  protected static function extractRelatedLinks(array $field_names, array $document) {
    ...
    +  protected static function extractLinks(array $link_paths, array $document) {
    

    ❤️

  10. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1788,63 +1967,231 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +  protected function getRelatedResourceResponses(ResourceResponse $individual_response, array $request_options, array $relationship_field_names) {
    +    return array_map([static::class, 'toResourceResponse'], $this->getRelatedResponses($individual_response, $request_options, $relationship_field_names));
    +  }
    ...
    +  protected function getResourceResponses(array $links, array $request_options) {
    +    return array_map([$this, 'toResourceResponse'], $this->getResponses($links, $request_options));
    +  }
    

    It seems these could use one and the same helper: a

    static function toResourceResponses(ResponseInterface[] $responses) {
      return array_map([static::class,
     'toResourceResponse'],
     $responses);
    }

    ?

  11. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1788,63 +1967,231 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +   * @return array
    ...
    +    return $this->getResponses($links, $request_options);
    ...
    +   * @return array
    ...
    +    return $this->getResourceResponses($links, $request_options);
    

    Could document a more specific type.

    Also, why does one return ResponseInterface[] and the other ResourceResponse[]?

  12. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1788,63 +1967,231 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +  protected function getRelatedResponses(ResourceResponse $individual_response, array $request_options, array $relationship_field_names) {
    ...
    +  protected function getRelationshipResponses(ResourceResponse $individual_response, array $request_options, array $relationship_field_names) {
    

    ❤️

  13. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1788,63 +1967,231 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +   * Maps a response object to a JSON API ResourceResponse.
    

    s/response/PSR response/

  14. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1788,63 +1967,231 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +   * cacheability data and to have easier access to the JSON API document as an
    

    s/cacheability data/cacheable responses/

  15. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1788,63 +1967,231 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +  protected static function toResourceResponse(ResponseInterface $response) {
    

    ❤️

wim leers’s picture

+++ b/tests/src/Functional/ResourceTestBase.php
@@ -832,6 +832,193 @@ abstract class ResourceTestBase extends BrowserTestBase {
+  public function testRelated() {

For consistency with testGetIndividual(), this should be renamed to testGetRelated().

In the future, we'll also want to add testPostRelated(), testPatchRelated() and testDeleteRelated().

gabesullice’s picture

In the future, we'll also want to add testPostRelated(), testPatchRelated() and testDeleteRelated().

Nope, the spec only lets you modify the "relationships" not the "related" resources themselves.

"related" routes are essentially just collection routes filtered by a referencing resource. They're read-only.

"relationship" routes let you add/update/remove the relationships between resources (in Drupal-speak, this means updating the entity reference field).

gabesullice’s picture

StatusFileSize
new22.66 KB
new9.05 KB

1. This is actually fixing a typo in the existing test coverage getAuthenticationRequestOptions doesn't take an argument.
2. 🤘
3. This comment is actually just a note to help whoever implements the "todo" not explain the code. I've moved it around.
4. Whoops, leftover comment. Removed.
5. I agree. I'll do that in the following comment so that the interdiff makes more sense.
6. Done.
7. Added more commentary.
8. Done.
9. 👍
10 + 11. Moved things around and I think it's much clearer now.
12. :)
13. Done
14. Some day, I'll get these wordings right.. some day.
15. :)

gabesullice’s picture

StatusFileSize
new18.98 KB
new26.58 KB

Moves all the static helpers into a ResourceResponseTestTrait per @Wim Leers suggestion in #54.5.

wim leers’s picture

#56: 🤦‍♂️ — of course, I misread \Drupal\jsonapi\Routing\Routes::routes(), sorry!
#57++
#58++

gabesullice’s picture

StatusFileSize
new32.75 KB
new25.96 KB

The attached applies some minor refactoring and determines which relationship field names to test the actual entity under test rather that relying on the output of the API we're testing to tell us which fields to test!

This is prep work for GET coverage of `relationship` routes.

gabesullice’s picture

StatusFileSize
new45.02 KB
new37.68 KB

This adds relationship testing. Still blocked on #2940339: Port reference field support for non-empty entity reference fields not pointing to an entity from #2543726. After that lands, we can fill in some details.

wim leers’s picture

  1. +++ b/tests/src/Functional/ResourceResponseTestTrait.php
    @@ -0,0 +1,335 @@
    + * Trait ResourceResponseTestTrait.
    ...
    + * @package Drupal\Tests\jsonapi\Functional
    

    Nit: these lines can be removed.

  2. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -840,6 +846,255 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +  public function testRelated() {
    +    $request_options = [];
    +    $request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json';
    +    $request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions());
    +    $this->doTestRelated($request_options);
    +    $this->setUpAuthorization('GET');
    +    $this->doTestRelated($request_options);
    +  }
    

    😍 — so elegant!

  3. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -840,6 +846,255 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +   * Unlike the "related" routes, relationship routes only return information
    +   * about the "relationship" itself, not the targeted resources. For JSON API
    +   * with Drupal, relationship routes are like looking at an entity reference
    +   * field without loading the entities. It only reveals the type of the
    +   * targeted resource and the target resource IDs. These type+ID combos are
    +   * referred to as "resource identifiers."
    

    This is SUCH A GOOD EXPLANATION! We should have this in jsonapi.api.php, and do it for every non-trivial route ("individual" and "collection" seem trivial).

  4. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -840,6 +846,255 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +  protected function getExpectedGetRelationshipDocument($relationship_field_name) {
    

    s/getExpectedGetRel…/getExpectedRel…/, no?

  5. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -840,6 +846,255 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +  protected function getExpectedGetRelationshipDocumentData($relationship_field_name) {
    

    😍 — so elegant!

  6. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -840,6 +846,255 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +        // Empty to-one relationships should be NULL and empty to-many
    +        // relationships should be an empty array.
    ...
    +            'data' => is_null($relationship_document['data']) ? NULL : [],
    

    These are strangely far apart.

  7. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1801,108 +2048,101 @@ abstract class ResourceTestBase extends BrowserTestBase {
    -  protected static function extractRelatedLinks(array $field_names, array $document) {
    -    return array_reduce($field_names, function ($links, $field_name) use ($document) {
    -      if ($link = array_reduce(
    -        ['data', 'relationships', $field_name, 'links', 'related'],
    -        'array_column',
    -        [$document]
    ...
    +  protected function getRelationshipFieldNames() {
    +    // Only content entity types can have relationships.
    +    $fields = $this->entity instanceof ContentEntityInterface
    +      ? iterator_to_array($this->entity)
    +      : [];
    +    return array_reduce($fields, function ($field_names, $field) {
    +      /* @var \Drupal\Core\Field\FieldItemListInterface $field */
    +      if ($this->isReferenceFieldDefinition($field->getFieldDefinition())) {
    

    Getting the list of relationship field names switched from introspecting JSON API responses to introspecting an entity in PHP. The latter is better for test coverage, because it would uncover anything that's consistently missing in JSON API. So: 👍

  8. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1801,108 +2048,101 @@ abstract class ResourceTestBase extends BrowserTestBase {
    -  protected function getRelatedResponses(array $links, array $request_options) {
    -    return array_reduce(array_keys($links), function ($related_responses, $field_name) use ($links, $request_options) {
    -      $related_response = $this->request('GET', Url::fromUri($links[$field_name]), $request_options);
    -      $cacheability = new CacheableMetadata();
    -      if ($cache_tags = $related_response->getHeader('X-Drupal-Cache-Tags')) {
    -        $cacheability->addCacheTags(explode(' ', $cache_tags[0]));
    -      }
    -      if ($cache_contexts = $related_response->getHeader('X-Drupal-Cache-Contexts')) {
    -        $cacheability->addCacheContexts(explode(' ', $cache_contexts[0]));
    -      }
    -      if ($dynamic_cache = $related_response->getHeader('X-Drupal-Dynamic-Cache')) {
    -        $cacheability->setCacheMaxAge(($dynamic_cache[0] === 'UNCACHEABLE') ? 0 : Cache::PERMANENT);
    -      }
    -      $related_document = Json::decode($related_response->getBody());
    -      $related_responses[$field_name] = (new ResourceResponse($related_document))
    -        ->addCacheableDependency($cacheability);
    -      return $related_responses;
    -    }, []);
    +  protected function getRelatedResponses(array $relationship_field_names, array $request_options) {
    +    $links = array_map(function ($relationship_field_name) {
    +      return static::getRelatedLink(static::toResourceIdentifier($this->entity), $relationship_field_name);
    +    }, array_combine($relationship_field_names, $relationship_field_names));
    +    return $this->getResponses($links, $request_options);
       }
    

    👏 — again, so elegant!

  9. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1801,108 +2048,101 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +  protected function getRelationshipResponses(array $relationship_field_names, array $request_options) {
    ...
    +  protected function getResponses(array $links, array $request_options) {
    

    Don't these also belong on the ResourceResponseTestTrait?

gabesullice’s picture

Status: Postponed » Needs review
StatusFileSize
new39.39 KB
new8.07 KB

1. Done.
2. Thanks!
3. Absolutely!
4. I thought about the same thing, but I think we'll want something for DELETE.
5. 🤘
6. Fixed.
7. 👍
8. 🔥
9. Moved.

I also skipped the the tests that are blocked.

I added another skip for an issue I just opened: #2952506: User role relationships route returns NULL

Let's see what the testbot thinks, finally.

gabesullice’s picture

Title: [PP-1] Comprehensive JSON API integration test coverage phase 3: test JSON API-specific use cases such as collections, includes, relationships, sparse fieldsets … » Comprehensive JSON API integration test coverage phase 3: test JSON API-specific use cases such as collections, includes, relationships, sparse fieldsets …
Issue tags: -blocked

Since we added skipped tests and have linked issues, this is no longer blocked.

Status: Needs review » Needs work

The last submitted patch, 63: 2945093-63.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

gabesullice’s picture

Status: Needs work » Needs review
StatusFileSize
new1.6 KB
new39.53 KB

I forgot and put a skip in the wrong class. Fixing.

Testbot did turn up a valid failure. For some reason the file relationship is serializing metadata that I think should be limited to 'related' routes.

Status: Needs review » Needs work

The last submitted patch, 66: 2945093-66.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

wim leers’s picture

#63:

  1. Ok, let's keep this for now then, we can still change it anyway
+++ b/tests/src/Functional/BlockContentTypeTest.php
@@ -102,4 +102,11 @@ class BlockContentTypeTest extends ResourceTestBase {
+  public function testRelated() {
+    $this->markTestSkipped('Remove this in https://www.drupal.org/project/jsonapi/issues/2940339');
+  }

+++ b/tests/src/Functional/CommentTest.php
@@ -394,4 +394,11 @@ class CommentTest extends ResourceTestBase {
+  public function testRelated() {
+    $this->markTestSkipped('Remove this in https://www.drupal.org/project/jsonapi/issues/2940339');
+  }

+++ b/tests/src/Functional/UserTest.php
@@ -444,4 +444,11 @@ class UserTest extends ResourceTestBase {
+  public function testGetRelationships() {
+    $this->markTestSkipped('Remove this in https://www.drupal.org/project/jsonapi/issues/2952506');
+  }

Excellent! This actually also makes #2940339: Port reference field support for non-empty entity reference fields not pointing to an entity from #2543726 simpler, because simply removing these overrides then gives us the necessary test coverage there!

Since we added skipped tests and have linked issues, this is no longer blocked.

EXCELLENT!

gabesullice’s picture

Status: Needs work » Needs review
StatusFileSize
new40.58 KB
new1.8 KB

This should pass! :)

Status: Needs review » Needs work

The last submitted patch, 69: 2945093-69.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

gabesullice’s picture

Status: Needs work » Needs review
StatusFileSize
new40.69 KB
new1.3 KB

This should pass! :)

Don't ever say this, because it guarantees that it will never be true.

Fixed... hopefully!

wim leers’s picture

Status: Needs review » Reviewed & tested by the community

There are still 2 CS violations… :P

Other than that, I think this looks great. Ideally, we get @e0ipso's +1. But because it doesn't change any logic, that's not a hard requirement I think?

At the very least, this is a huge step forward! :)

The last submitted patch, 57: 2945093-56.patch, failed testing. View results

The last submitted patch, 60: 2945093-60.patch, failed testing. View results

The last submitted patch, 61: 2945093-61.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

gabesullice’s picture

StatusFileSize
new40.45 KB
new882 bytes

Whoops, just some leftovers.

  • e0ipso committed edaeb69 on 8.x-1.x authored by gabesullice
    Issue #2945093 by gabesullice, e0ipso, Wim Leers: Comprehensive JSON API...
e0ipso’s picture

Status: Reviewed & tested by the community » Fixed

This was merged. I could not do a proper review, but I decided to merge anyways because: 1) it was RTBC-ed by a maintainer, 2) this is mostly adding test coverage (hence the possibility to break things is thin) and 3) so we can unblock things and keep momentum.

What I could review was as fantastic as usual. Thanks @gabesullice!

gabesullice’s picture

Title: Comprehensive JSON API integration test coverage phase 3: test JSON API-specific use cases such as collections, includes, relationships, sparse fieldsets … » Comprehensive JSON API integration test coverage phase 3: test JSON API-specific use cases: related/relationship routes, includes and sparse field sets

I think this issue has had enough activity. I'll do collections in a separate issue and nested includes/relationships in another.

Great work everybody, thank you!

Status: Fixed » Closed (fixed)

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