function EntityResourceTestBase::testGet

Same name in other branches
  1. 9 core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase::testGet()
  2. 8.9.x core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase::testGet()
  3. 10 core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase::testGet()

Tests a GET request for an entity, plus edge cases to ensure good DX.

1 method overrides EntityResourceTestBase::testGet()
MessageResourceTestBase::testGet in core/modules/contact/tests/src/Functional/Rest/MessageResourceTestBase.php
Tests a GET request for an entity, plus edge cases to ensure good DX.

File

core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php, line 409

Class

EntityResourceTestBase
Defines a base class for testing all entity resources.

Namespace

Drupal\Tests\rest\Functional\EntityResource

Code

public function testGet() : void {
    $this->initAuthentication();
    $has_canonical_url = $this->entity
        ->hasLinkTemplate('canonical');
    // The URL and Guzzle request options that will be used in this test. The
    // request options will be modified/expanded throughout this test:
    // - to first test all mistakes a developer might make, and assert that the
    //   error responses provide a good DX
    // - to eventually result in a well-formed request that succeeds.
    $url = $this->getEntityResourceUrl();
    $request_options = [];
    // DX: 404 when resource not provisioned, 403 if canonical route. HTML
    // response because missing ?_format query string.
    $response = $this->request('GET', $url, $request_options);
    $this->assertSame($has_canonical_url ? 403 : 404, $response->getStatusCode());
    $this->assertSame([
        'text/html; charset=UTF-8',
    ], $response->getHeader('Content-Type'));
    $url->setOption('query', [
        '_format' => static::$format,
    ]);
    // DX: 404 when resource not provisioned, 403 if canonical route. Non-HTML
    // response because ?_format query string is present.
    $response = $this->request('GET', $url, $request_options);
    if ($has_canonical_url) {
        $expected_cacheability = $this->getExpectedUnauthorizedAccessCacheability()
            ->addCacheTags([
            'config:user.role.anonymous',
        ]);
        $expected_cacheability->addCacheableDependency($this->getExpectedUnauthorizedEntityAccessCacheability(FALSE));
        // Mitigate https://www.drupal.org/project/drupal/issues/3451483 until
        // it gets resolved.
        $response = $response->withoutHeader('X-Drupal-Dynamic-Cache');
        $this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('GET'), $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), 'MISS', FALSE);
    }
    else {
        $this->assertResourceErrorResponse(404, 'No route found for "GET ' . $this->getEntityResourceUrl()
            ->setAbsolute()
            ->toString() . '"', $response);
    }
    $this->provisionEntityResource();
    // DX: forgetting authentication: authentication provider-specific error
    // response.
    if (static::$auth) {
        $response = $this->request('GET', $url, $request_options);
        // Mitigate https://www.drupal.org/project/drupal/issues/3451483 until
        // it gets resolved.
        $response = $response->withoutHeader('X-Drupal-Dynamic-Cache');
        $this->assertResponseWhenMissingAuthentication('GET', $response);
    }
    $request_options[RequestOptions::HEADERS]['REST-test-auth'] = '1';
    // DX: 403 when attempting to use disallowed authentication provider.
    $response = $this->request('GET', $url, $request_options);
    $this->assertResourceErrorResponse(403, 'The used authentication method is not allowed on this route.', $response);
    unset($request_options[RequestOptions::HEADERS]['REST-test-auth']);
    $request_options[RequestOptions::HEADERS]['REST-test-auth-global'] = '1';
    // DX: 403 when attempting to use disallowed global authentication provider.
    $response = $this->request('GET', $url, $request_options);
    $this->assertResourceErrorResponse(403, 'The used authentication method is not allowed on this route.', $response);
    unset($request_options[RequestOptions::HEADERS]['REST-test-auth-global']);
    $request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions('GET'));
    // First: single format. Drupal will automatically pick the only format.
    $this->provisionEntityResource(TRUE);
    $expected_403_cacheability = $this->getExpectedUnauthorizedAccessCacheability()
        ->addCacheableDependency($this->getExpectedUnauthorizedEntityAccessCacheability(static::$auth !== FALSE));
    // DX: 403 because unauthorized single-format route, ?_format is omittable.
    $url->setOption('query', []);
    $response = $this->request('GET', $url, $request_options);
    if ($has_canonical_url) {
        $this->assertSame(403, $response->getStatusCode());
        $this->assertSame([
            'text/html; charset=UTF-8',
        ], $response->getHeader('Content-Type'));
    }
    else {
        $this->assertResourceErrorResponse(403, FALSE, $response, $expected_403_cacheability->getCacheTags(), $expected_403_cacheability->getCacheContexts(), static::$auth ? FALSE : 'MISS', FALSE);
    }
    $this->assertSame(static::$auth ? [
        'UNCACHEABLE (request policy)',
    ] : [
        'MISS',
    ], $response->getHeader('X-Drupal-Cache'));
    // DX: 403 because unauthorized.
    $url->setOption('query', [
        '_format' => static::$format,
    ]);
    $response = $this->request('GET', $url, $request_options);
    // Mitigate https://www.drupal.org/project/drupal/issues/3451483 until
    // it gets resolved.
    $response = $response->withoutHeader('X-Drupal-Dynamic-Cache');
    $this->assertResourceErrorResponse(403, FALSE, $response, $expected_403_cacheability->getCacheTags(), $expected_403_cacheability->getCacheContexts(), static::$auth ? FALSE : 'MISS', FALSE);
    // Then, what we'll use for the remainder of the test: multiple formats.
    $this->provisionEntityResource();
    // DX: 406 because despite unauthorized, ?_format is not omittable.
    $url->setOption('query', []);
    $response = $this->request('GET', $url, $request_options);
    if ($has_canonical_url) {
        $this->assertSame(403, $response->getStatusCode());
        $dynamic_cache = str_starts_with($response->getHeader('X-Drupal-Cache-Max-Age')[0], '0') || !empty(array_intersect([
            'user',
            'session',
        ], explode(' ', $response->getHeader('X-Drupal-Cache-Contexts')[0]))) ? 'UNCACHEABLE (poor cacheability)' : 'MISS';
        $this->assertSame([
            $dynamic_cache,
        ], $response->getHeader('X-Drupal-Dynamic-Cache'));
    }
    else {
        $this->assertSame(406, $response->getStatusCode());
        $this->assertSame([
            'UNCACHEABLE (poor cacheability)',
        ], $response->getHeader('X-Drupal-Dynamic-Cache'));
    }
    $this->assertSame([
        'text/html; charset=UTF-8',
    ], $response->getHeader('Content-Type'));
    $this->assertSame(static::$auth ? [
        'UNCACHEABLE (request policy)',
    ] : [
        'MISS',
    ], $response->getHeader('X-Drupal-Cache'));
    // DX: 403 because unauthorized.
    $url->setOption('query', [
        '_format' => static::$format,
    ]);
    $response = $this->request('GET', $url, $request_options);
    // Mitigate https://www.drupal.org/project/drupal/issues/3451483 until
    // it gets resolved.
    $response = $response->withoutHeader('X-Drupal-Dynamic-Cache');
    $this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('GET'), $response, $expected_403_cacheability->getCacheTags(), $expected_403_cacheability->getCacheContexts(), static::$auth ? FALSE : 'MISS', FALSE);
    $this->assertArrayNotHasKey('Link', $response->getHeaders());
    $this->setUpAuthorization('GET');
    // 200 for well-formed HEAD request.
    $response = $this->request('HEAD', $url, $request_options);
    $is_cacheable_by_dynamic_page_cache = empty(array_intersect([
        'user',
        'session',
    ], $this->getExpectedCacheContexts()));
    $this->assertResourceResponse(200, '', $response, $this->getExpectedCacheTags(), $this->getExpectedCacheContexts(), static::$auth ? FALSE : 'MISS', $is_cacheable_by_dynamic_page_cache ? 'MISS' : 'UNCACHEABLE (poor cacheability)');
    $head_headers = $response->getHeaders();
    // 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, $this->getExpectedCacheTags(), $this->getExpectedCacheContexts(), static::$auth ? FALSE : 'HIT', $is_cacheable_by_dynamic_page_cache ? static::$auth ? 'HIT' : 'MISS' : 'UNCACHEABLE (poor cacheability)');
    // Assert that Dynamic Page Cache did not store a ResourceResponse object,
    // which needs serialization after every cache hit. Instead, it should
    // contain a flattened response. Otherwise performance suffers.
    // @see \Drupal\rest\EventSubscriber\ResourceResponseSubscriber::flattenResponse()
    $cache_items = $this->container
        ->get('database')
        ->select('cache_dynamic_page_cache', 'c')
        ->fields('c', [
        'data',
    ])
        ->condition('c.cid', '%[route]=rest.%', 'LIKE')
        ->execute()
        ->fetchAll();
    if (!$is_cacheable_by_dynamic_page_cache) {
        $this->assertCount(0, $cache_items);
    }
    else {
        $this->assertLessThanOrEqual(2, count($cache_items));
        $found_cached_200_response = FALSE;
        $other_cached_responses_are_4xx = TRUE;
        foreach ($cache_items as $cache_item) {
            $cached_response = unserialize($cache_item->data);
            if (!$cached_response instanceof CacheRedirect) {
                if ($cached_response->getStatusCode() === 200) {
                    $found_cached_200_response = TRUE;
                }
                elseif (!$cached_response->isClientError()) {
                    $other_cached_responses_are_4xx = FALSE;
                }
                $this->assertNotInstanceOf(ResourceResponseInterface::class, $cached_response);
                $this->assertInstanceOf(CacheableResponseInterface::class, $cached_response);
            }
        }
        $this->assertTrue($found_cached_200_response);
        $this->assertTrue($other_cached_responses_are_4xx);
    }
    // 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->getExpectedNormalizedEntity();
    static::recursiveKSort($expected);
    $actual = $this->serializer
        ->decode((string) $response->getBody(), static::$format);
    static::recursiveKSort($actual);
    $this->assertEqualsCanonicalizing($expected, $actual);
    // Not only assert the normalization, also assert deserialization of the
    // response results in the expected object.
    // Note: deserialization of the XML format is not supported, so only test
    // this for other formats.
    if (static::$format !== 'xml') {
        $unserialized = $this->serializer
            ->deserialize((string) $response->getBody(), get_class($this->entity), static::$format);
        $this->assertSame($unserialized->uuid(), $this->entity
            ->uuid());
    }
    // Finally, assert that the expected 'Link' headers are present.
    if ($this->entity
        ->getEntityType()
        ->getLinkTemplates()) {
        $this->assertArrayHasKey('Link', $response->getHeaders());
        $link_relation_type_manager = $this->container
            ->get('plugin.manager.link_relation_type');
        $expected_link_relation_headers = array_map(function ($relation_name) use ($link_relation_type_manager) {
            $link_relation_type = $link_relation_type_manager->createInstance($relation_name);
            return $link_relation_type->isRegistered() ? $link_relation_type->getRegisteredName() : $link_relation_type->getExtensionUri();
        }, array_keys($this->entity
            ->getEntityType()
            ->getLinkTemplates()));
        $parse_rel_from_link_header = function ($value) {
            $matches = [];
            if (preg_match('/rel="([^"]+)"/', $value, $matches) === 1) {
                return $matches[1];
            }
            return FALSE;
        };
        $this->assertSame($expected_link_relation_headers, array_map($parse_rel_from_link_header, $response->getHeader('Link')));
    }
    $get_headers = $response->getHeaders();
    // Verify that the GET and HEAD responses are the same. The only difference
    // is that there's no body. For this reason the 'Transfer-Encoding' and
    // 'Vary' headers are also added to the list of headers to ignore, as they
    // may be added to GET requests, depending on web server configuration. They
    // are usually 'Transfer-Encoding: chunked' and 'Vary: Accept-Encoding'.
    $ignored_headers = [
        'Date',
        'Content-Length',
        'X-Drupal-Cache',
        'X-Drupal-Dynamic-Cache',
        'Transfer-Encoding',
        'Vary',
    ];
    $header_cleaner = function ($headers) use ($ignored_headers) {
        foreach ($headers as $header => $value) {
            if (str_starts_with($header, 'X-Drupal-Assertion-') || in_array($header, $ignored_headers)) {
                unset($headers[$header]);
            }
        }
        return $headers;
    };
    $get_headers = $header_cleaner($get_headers);
    $head_headers = $header_cleaner($head_headers);
    $this->assertSame($get_headers, $head_headers);
    $this->resourceConfigStorage
        ->load(static::$resourceConfigId)
        ->disable()
        ->save();
    $this->refreshTestStateAfterRestConfigChange();
    // DX: upon disabling a resource, it's immediately no longer available.
    $this->assertResourceNotAvailable($url, $request_options);
    $this->resourceConfigStorage
        ->load(static::$resourceConfigId)
        ->enable()
        ->save();
    $this->refreshTestStateAfterRestConfigChange();
    // DX: upon re-enabling a resource, immediate 200.
    $response = $this->request('GET', $url, $request_options);
    $this->assertResourceResponse(200, FALSE, $response, $this->getExpectedCacheTags(), $this->getExpectedCacheContexts(), static::$auth ? FALSE : 'MISS', $is_cacheable_by_dynamic_page_cache ? 'MISS' : 'UNCACHEABLE (poor cacheability)');
    $this->resourceConfigStorage
        ->load(static::$resourceConfigId)
        ->delete();
    $this->refreshTestStateAfterRestConfigChange();
    // DX: upon deleting a resource, it's immediately no longer available.
    $this->assertResourceNotAvailable($url, $request_options);
    $this->provisionEntityResource();
    $url->setOption('query', [
        '_format' => 'non_existing_format',
    ]);
    // DX: 406 when requesting unsupported format.
    $response = $this->request('GET', $url, $request_options);
    $this->assert406Response($response);
    $this->assertSame([
        'text/plain; charset=UTF-8',
    ], $response->getHeader('Content-Type'));
    $request_options[RequestOptions::HEADERS]['Accept'] = static::$mimeType;
    // DX: 406 when requesting unsupported format but specifying Accept header:
    // should result in a text/plain response.
    $response = $this->request('GET', $url, $request_options);
    $this->assert406Response($response);
    $this->assertSame([
        'text/plain; charset=UTF-8',
    ], $response->getHeader('Content-Type'));
    $url = Url::fromRoute('rest.entity.' . static::$entityTypeId . '.GET');
    $url->setRouteParameter(static::$entityTypeId, 987654321);
    $url->setOption('query', [
        '_format' => static::$format,
    ]);
    // DX: 404 when GETting non-existing entity.
    $response = $this->request('GET', $url, $request_options);
    $path = str_replace('987654321', '{' . static::$entityTypeId . '}', $url->setAbsolute()
        ->setOptions([
        'base_url' => '',
        'query' => [],
    ])
        ->toString());
    $message = 'The "' . static::$entityTypeId . '" parameter was not converted for the path "' . $path . '" (route name: "rest.entity.' . static::$entityTypeId . '.GET")';
    $this->assertResourceErrorResponse(404, $message, $response);
}

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.