Initial Drupal 11 with DDEV setup
This commit is contained in:
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\rest\Functional;
|
||||
|
||||
use Drupal\Core\Url;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
/**
|
||||
* Defines a trait for testing with no authentication provider.
|
||||
*
|
||||
* This is intended to be used with
|
||||
* \Drupal\Tests\rest\Functional\ResourceTestBase.
|
||||
*
|
||||
* Characteristics:
|
||||
* - When no authentication provider is being used, there also cannot be any
|
||||
* particular error response for missing authentication, since by definition
|
||||
* there is not any authentication.
|
||||
* - For the same reason, there are no authentication edge cases to test.
|
||||
* - Because no authentication is required, this is vulnerable to CSRF attacks
|
||||
* by design. Hence a REST resource should probably only allow for anonymous
|
||||
* for safe (GET/HEAD) HTTP methods, and only with extreme care should unsafe
|
||||
* (POST/PATCH/DELETE) HTTP methods be allowed for a REST resource that allows
|
||||
* anonymous access.
|
||||
*/
|
||||
trait AnonResourceTestTrait {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function assertResponseWhenMissingAuthentication($method, ResponseInterface $response) {
|
||||
throw new \LogicException('When testing for anonymous users, authentication cannot be missing.');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function assertAuthenticationEdgeCases($method, Url $url, array $request_options) {
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\rest\Functional;
|
||||
|
||||
use Drupal\Core\Url;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
/**
|
||||
* Trait for ResourceTestBase subclasses testing $auth=basic_auth.
|
||||
*
|
||||
* Characteristics:
|
||||
* - Every request must send an Authorization header.
|
||||
* - When accessing a URI that requires authentication without being
|
||||
* authenticated, a 401 response must be sent.
|
||||
* - Because every request must send an authorization, there is no danger of
|
||||
* CSRF attacks.
|
||||
*/
|
||||
trait BasicAuthResourceTestTrait {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getAuthenticationRequestOptions($method): array {
|
||||
return [
|
||||
'headers' => [
|
||||
'Authorization' => 'Basic ' . base64_encode($this->account->getAccountName() . ':' . $this->account->passRaw),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function assertResponseWhenMissingAuthentication($method, ResponseInterface $response) {
|
||||
if ($method !== 'GET') {
|
||||
return $this->assertResourceErrorResponse(401, 'No authentication credentials provided.', $response);
|
||||
}
|
||||
|
||||
$expected_page_cache_header_value = $method === 'GET' ? 'MISS' : FALSE;
|
||||
$expected_cacheability = $this->getExpectedUnauthorizedAccessCacheability()
|
||||
->addCacheableDependency($this->getExpectedUnauthorizedEntityAccessCacheability(FALSE))
|
||||
// @see \Drupal\basic_auth\Authentication\Provider\BasicAuth::challengeException()
|
||||
->addCacheableDependency($this->config('system.site'))
|
||||
// @see \Drupal\Core\EventSubscriber\AnonymousUserResponseSubscriber::onRespond()
|
||||
->addCacheTags(['config:user.role.anonymous']);
|
||||
// Only add the 'user.roles:anonymous' cache context if its parent cache
|
||||
// context is not already present.
|
||||
if (!in_array('user.roles', $expected_cacheability->getCacheContexts(), TRUE)) {
|
||||
$expected_cacheability->addCacheContexts(['user.roles:anonymous']);
|
||||
}
|
||||
$this->assertResourceErrorResponse(401, 'No authentication credentials provided.', $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), $expected_page_cache_header_value, FALSE);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function assertAuthenticationEdgeCases($method, Url $url, array $request_options) {
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\rest\Functional;
|
||||
|
||||
use Drupal\Core\Url;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
/**
|
||||
* Trait for ResourceTestBase subclasses testing $auth=cookie.
|
||||
*
|
||||
* Characteristics:
|
||||
* - After performing a valid "log in" request, the server responds with a 2xx
|
||||
* status code and a 'Set-Cookie' response header. This cookie is what
|
||||
* continues to identify the user in subsequent requests.
|
||||
* - When accessing a URI that requires authentication without being
|
||||
* authenticated, a standard 403 response must be sent.
|
||||
* - Because of the reliance on cookies, and the fact that user agents send
|
||||
* cookies with every request, this is vulnerable to CSRF attacks. To mitigate
|
||||
* this, the response for the "log in" request contains a CSRF token that must
|
||||
* be sent with every unsafe (POST/PATCH/DELETE) HTTP request.
|
||||
*/
|
||||
trait CookieResourceTestTrait {
|
||||
|
||||
/**
|
||||
* The session cookie.
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @see ::initAuthentication
|
||||
*/
|
||||
protected $sessionCookie;
|
||||
|
||||
/**
|
||||
* The CSRF token.
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @see ::initAuthentication
|
||||
*/
|
||||
protected $csrfToken;
|
||||
|
||||
/**
|
||||
* The logout token.
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @see ::initAuthentication
|
||||
*/
|
||||
protected $logoutToken;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function initAuthentication(): void {
|
||||
$user_login_url = Url::fromRoute('user.login.http')
|
||||
->setRouteParameter('_format', static::$format);
|
||||
|
||||
$request_body = [
|
||||
'name' => $this->account->name->value,
|
||||
'pass' => $this->account->passRaw,
|
||||
];
|
||||
|
||||
$request_options[RequestOptions::BODY] = $this->serializer->encode($request_body, static::$format);
|
||||
$request_options[RequestOptions::HEADERS] = [
|
||||
'Content-Type' => static::$mimeType,
|
||||
];
|
||||
$response = $this->request('POST', $user_login_url, $request_options);
|
||||
|
||||
// Parse and store the session cookie.
|
||||
$this->sessionCookie = explode(';', $response->getHeader('Set-Cookie')[0], 2)[0];
|
||||
|
||||
// Parse and store the CSRF token and logout token.
|
||||
$data = $this->serializer->decode((string) $response->getBody(), static::$format);
|
||||
$this->csrfToken = $data['csrf_token'];
|
||||
$this->logoutToken = $data['logout_token'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getAuthenticationRequestOptions($method) {
|
||||
$request_options[RequestOptions::HEADERS]['Cookie'] = $this->sessionCookie;
|
||||
// @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html
|
||||
if (!in_array($method, ['HEAD', 'GET', 'OPTIONS', 'TRACE'])) {
|
||||
$request_options[RequestOptions::HEADERS]['X-CSRF-Token'] = $this->csrfToken;
|
||||
}
|
||||
return $request_options;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function assertResponseWhenMissingAuthentication($method, ResponseInterface $response): void {
|
||||
// Requests needing cookie authentication but missing it results in a 403
|
||||
// response. The cookie authentication mechanism sets no response message.
|
||||
// Hence, effectively, this is just the 403 response that one gets as the
|
||||
// anonymous user trying to access a certain REST resource.
|
||||
// @see \Drupal\user\Authentication\Provider\Cookie
|
||||
// @todo https://www.drupal.org/node/2847623
|
||||
if ($method === 'GET') {
|
||||
$expected_cookie_403_cacheability = $this->getExpectedUnauthorizedAccessCacheability()
|
||||
// @see \Drupal\Core\EventSubscriber\AnonymousUserResponseSubscriber::onRespond()
|
||||
->addCacheableDependency($this->getExpectedUnauthorizedEntityAccessCacheability(FALSE));
|
||||
// - \Drupal\Core\EventSubscriber\AnonymousUserResponseSubscriber applies
|
||||
// to cacheable anonymous responses: it updates their cacheability.
|
||||
// - A 403 response to a GET request is cacheable.
|
||||
// Therefore we must update our cacheability expectations accordingly.
|
||||
if (in_array('user.permissions', $expected_cookie_403_cacheability->getCacheContexts(), TRUE)) {
|
||||
$expected_cookie_403_cacheability->addCacheTags(['config:user.role.anonymous']);
|
||||
}
|
||||
$this->assertResourceErrorResponse(403, FALSE, $response, $expected_cookie_403_cacheability->getCacheTags(), $expected_cookie_403_cacheability->getCacheContexts(), 'MISS', FALSE);
|
||||
}
|
||||
else {
|
||||
$this->assertResourceErrorResponse(403, FALSE, $response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function assertAuthenticationEdgeCases($method, Url $url, array $request_options): void {
|
||||
// X-CSRF-Token request header is unnecessary for safe and side effect-free
|
||||
// HTTP methods. No need for additional assertions.
|
||||
// @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html
|
||||
if (in_array($method, ['HEAD', 'GET', 'OPTIONS', 'TRACE'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
unset($request_options[RequestOptions::HEADERS]['X-CSRF-Token']);
|
||||
|
||||
// DX: 403 when missing X-CSRF-Token request header.
|
||||
$response = $this->request($method, $url, $request_options);
|
||||
$this->assertResourceErrorResponse(403, 'X-CSRF-Token request header is missing', $response);
|
||||
|
||||
$request_options[RequestOptions::HEADERS]['X-CSRF-Token'] = 'this-is-not-the-token-you-are-looking-for';
|
||||
|
||||
// DX: 403 when invalid X-CSRF-Token request header.
|
||||
$response = $this->request($method, $url, $request_options);
|
||||
$this->assertResourceErrorResponse(403, 'X-CSRF-Token request header is invalid', $response);
|
||||
|
||||
$request_options[RequestOptions::HEADERS]['X-CSRF-Token'] = $this->csrfToken;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\rest\Functional\EntityResource;
|
||||
|
||||
/**
|
||||
* Resource test base class for config entities.
|
||||
*
|
||||
* @todo Remove this in https://www.drupal.org/node/2300677.
|
||||
*/
|
||||
abstract class ConfigEntityResourceTestBase extends EntityResourceTestBase {
|
||||
|
||||
/**
|
||||
* A list of test methods to skip.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
const SKIP_METHODS = ['testPost', 'testPatch', 'testDelete'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
if (in_array($this->name(), static::SKIP_METHODS, TRUE)) {
|
||||
// Skip before installing Drupal to prevent unnecessary use of resources.
|
||||
$this->markTestSkipped("Not yet supported for config entities.");
|
||||
}
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\rest\Functional\EntityResource\ModeratedNode;
|
||||
|
||||
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
|
||||
|
||||
/**
|
||||
* @group rest
|
||||
*/
|
||||
class ModeratedNodeJsonAnonTest extends ModeratedNodeResourceTestBase {
|
||||
|
||||
use AnonResourceTestTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $format = 'json';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $mimeType = 'application/json';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\rest\Functional\EntityResource\ModeratedNode;
|
||||
|
||||
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
|
||||
|
||||
/**
|
||||
* @group rest
|
||||
*/
|
||||
class ModeratedNodeJsonBasicAuthTest extends ModeratedNodeResourceTestBase {
|
||||
|
||||
use BasicAuthResourceTestTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['basic_auth'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $format = 'json';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $mimeType = 'application/json';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $auth = 'basic_auth';
|
||||
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\rest\Functional\EntityResource\ModeratedNode;
|
||||
|
||||
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
|
||||
|
||||
/**
|
||||
* @group rest
|
||||
*/
|
||||
class ModeratedNodeJsonCookieTest extends ModeratedNodeResourceTestBase {
|
||||
|
||||
use CookieResourceTestTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $format = 'json';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $mimeType = 'application/json';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $auth = 'cookie';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
}
|
||||
@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\rest\Functional\EntityResource\ModeratedNode;
|
||||
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
|
||||
use Drupal\Tests\node\Functional\Rest\NodeResourceTestBase;
|
||||
|
||||
/**
|
||||
* Extend the Node resource test base and apply moderation to the entity.
|
||||
*/
|
||||
abstract class ModeratedNodeResourceTestBase extends NodeResourceTestBase {
|
||||
|
||||
use ContentModerationTestTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['content_moderation'];
|
||||
|
||||
/**
|
||||
* The test editorial workflow.
|
||||
*
|
||||
* @var \Drupal\workflows\WorkflowInterface
|
||||
*/
|
||||
protected $workflow;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method) {
|
||||
parent::setUpAuthorization($method);
|
||||
|
||||
switch ($method) {
|
||||
case 'POST':
|
||||
case 'PATCH':
|
||||
case 'DELETE':
|
||||
$this->grantPermissionsToTestedRole(['use editorial transition publish', 'use editorial transition create_new_draft']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity() {
|
||||
$entity = parent::createEntity();
|
||||
if (!$this->workflow) {
|
||||
$this->workflow = $this->createEditorialWorkflow();
|
||||
}
|
||||
$this->workflow->getTypePlugin()->addEntityTypeAndBundle($entity->getEntityTypeId(), $entity->bundle());
|
||||
$this->workflow->save();
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedNormalizedEntity() {
|
||||
return array_merge(parent::getExpectedNormalizedEntity(), [
|
||||
'moderation_state' => [
|
||||
[
|
||||
'value' => 'published',
|
||||
],
|
||||
],
|
||||
'vid' => [
|
||||
[
|
||||
'value' => (int) $this->entity->getRevisionId(),
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedCacheTags() {
|
||||
return Cache::mergeTags(parent::getExpectedCacheTags(), ['config:workflows.workflow.editorial']);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\rest\Functional\EntityResource\ModeratedNode;
|
||||
|
||||
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
|
||||
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
|
||||
|
||||
/**
|
||||
* @group rest
|
||||
*/
|
||||
class ModeratedNodeXmlAnonTest extends ModeratedNodeResourceTestBase {
|
||||
|
||||
use AnonResourceTestTrait;
|
||||
use XmlEntityNormalizationQuirksTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $format = 'xml';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $mimeType = 'text/xml; charset=UTF-8';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\rest\Functional\EntityResource\ModeratedNode;
|
||||
|
||||
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
|
||||
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
|
||||
|
||||
/**
|
||||
* @group rest
|
||||
*/
|
||||
class ModeratedNodeXmlBasicAuthTest extends ModeratedNodeResourceTestBase {
|
||||
|
||||
use BasicAuthResourceTestTrait;
|
||||
use XmlEntityNormalizationQuirksTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['basic_auth'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $format = 'xml';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $mimeType = 'text/xml; charset=UTF-8';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $auth = 'basic_auth';
|
||||
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\rest\Functional\EntityResource\ModeratedNode;
|
||||
|
||||
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
|
||||
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
|
||||
|
||||
/**
|
||||
* @group rest
|
||||
*/
|
||||
class ModeratedNodeXmlCookieTest extends ModeratedNodeResourceTestBase {
|
||||
|
||||
use CookieResourceTestTrait;
|
||||
use XmlEntityNormalizationQuirksTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $format = 'xml';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $mimeType = 'text/xml; charset=UTF-8';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $auth = 'cookie';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
}
|
||||
@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\rest\Functional\EntityResource;
|
||||
|
||||
use Drupal\Core\Entity\FieldableEntityInterface;
|
||||
use Drupal\Core\Field\Plugin\Field\FieldType\BooleanItem;
|
||||
use Drupal\Core\Field\Plugin\Field\FieldType\ChangedItem;
|
||||
use Drupal\Core\Field\Plugin\Field\FieldType\CreatedItem;
|
||||
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
|
||||
use Drupal\Core\Field\Plugin\Field\FieldType\IntegerItem;
|
||||
use Drupal\Core\Field\Plugin\Field\FieldType\TimestampItem;
|
||||
use Drupal\file\Plugin\Field\FieldType\FileItem;
|
||||
use Drupal\image\Plugin\Field\FieldType\ImageItem;
|
||||
use Drupal\options\Plugin\Field\FieldType\ListIntegerItem;
|
||||
use Drupal\path\Plugin\Field\FieldType\PathItem;
|
||||
use Drupal\Tests\rest\Functional\XmlNormalizationQuirksTrait;
|
||||
use Drupal\user\StatusItem;
|
||||
use PHPUnit\Framework\Attributes\Before;
|
||||
|
||||
/**
|
||||
* Trait for EntityResourceTestBase subclasses testing $format='xml'.
|
||||
*/
|
||||
trait XmlEntityNormalizationQuirksTrait {
|
||||
|
||||
use XmlNormalizationQuirksTrait;
|
||||
|
||||
/**
|
||||
* Marks some tests as skipped because XML cannot be deserialized.
|
||||
*/
|
||||
#[Before]
|
||||
public function xmlEntityNormalizationQuirksTraitSkipTests(): void {
|
||||
if (in_array($this->name(), ['testPatch', 'testPost'], TRUE)) {
|
||||
$this->markTestSkipped('Deserialization of the XML format is not supported.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedNormalizedEntity() {
|
||||
$default_normalization = parent::getExpectedNormalizedEntity();
|
||||
|
||||
if ($this->entity instanceof FieldableEntityInterface) {
|
||||
$normalization = $this->applyXmlFieldDecodingQuirks($default_normalization);
|
||||
}
|
||||
else {
|
||||
$normalization = $this->applyXmlConfigEntityDecodingQuirks($default_normalization);
|
||||
}
|
||||
$normalization = $this->applyXmlDecodingQuirks($normalization);
|
||||
|
||||
return $normalization;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the XML entity field encoding quirks that remain after decoding.
|
||||
*
|
||||
* The XML encoding:
|
||||
* - loses type data (int and bool become string)
|
||||
*
|
||||
* @param array $normalization
|
||||
* An entity normalization.
|
||||
*
|
||||
* @return array
|
||||
* The updated fieldable entity normalization.
|
||||
*
|
||||
* @see \Symfony\Component\Serializer\Encoder\XmlEncoder
|
||||
*/
|
||||
protected function applyXmlFieldDecodingQuirks(array $normalization): array {
|
||||
foreach ($this->entity->getFields(TRUE) as $field_name => $field) {
|
||||
// Not every field is accessible.
|
||||
if (!isset($normalization[$field_name])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for ($i = 0; $i < count($normalization[$field_name]); $i++) {
|
||||
switch ($field->getItemDefinition()->getClass()) {
|
||||
case BooleanItem::class:
|
||||
case StatusItem::class:
|
||||
// @todo Remove the StatusItem case in
|
||||
// https://www.drupal.org/project/drupal/issues/2936864.
|
||||
$value = &$normalization[$field_name][$i]['value'];
|
||||
$value = $value === TRUE ? '1' : '0';
|
||||
break;
|
||||
|
||||
case IntegerItem::class:
|
||||
case ListIntegerItem::class:
|
||||
$value = &$normalization[$field_name][$i]['value'];
|
||||
$value = (string) $value;
|
||||
break;
|
||||
|
||||
case PathItem::class:
|
||||
$pid = &$normalization[$field_name][$i]['pid'];
|
||||
$pid = (string) $pid;
|
||||
break;
|
||||
|
||||
case EntityReferenceItem::class:
|
||||
case FileItem::class:
|
||||
$target_id = &$normalization[$field_name][$i]['target_id'];
|
||||
$target_id = (string) $target_id;
|
||||
break;
|
||||
|
||||
case ChangedItem::class:
|
||||
case CreatedItem::class:
|
||||
case TimestampItem::class:
|
||||
$value = &$normalization[$field_name][$i]['value'];
|
||||
if (is_numeric($value)) {
|
||||
$value = (string) $value;
|
||||
}
|
||||
break;
|
||||
|
||||
case ImageItem::class:
|
||||
$height = &$normalization[$field_name][$i]['height'];
|
||||
$height = (string) $height;
|
||||
$width = &$normalization[$field_name][$i]['width'];
|
||||
$width = (string) $width;
|
||||
$target_id = &$normalization[$field_name][$i]['target_id'];
|
||||
$target_id = (string) $target_id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (count($normalization[$field_name]) === 1) {
|
||||
$normalization[$field_name] = $normalization[$field_name][0];
|
||||
}
|
||||
}
|
||||
|
||||
return $normalization;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the XML config entity encoding quirks that remain after decoding.
|
||||
*
|
||||
* The XML encoding:
|
||||
* - loses type data (int and bool become string)
|
||||
* - converts single-item arrays into single items (non-arrays)
|
||||
*
|
||||
* @param array $normalization
|
||||
* An entity normalization.
|
||||
*
|
||||
* @return array
|
||||
* The updated config entity normalization.
|
||||
*
|
||||
* @see \Symfony\Component\Serializer\Encoder\XmlEncoder
|
||||
*/
|
||||
protected function applyXmlConfigEntityDecodingQuirks(array $normalization) {
|
||||
$normalization = static::castToString($normalization);
|
||||
|
||||
// When a single dependency is listed, it's not decoded into an array.
|
||||
if (isset($normalization['dependencies'])) {
|
||||
foreach ($normalization['dependencies'] as $dependency_type => $dependency_list) {
|
||||
if (count($dependency_list) === 1) {
|
||||
$normalization['dependencies'][$dependency_type] = $dependency_list[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $normalization;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,834 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\rest\Functional;
|
||||
|
||||
use Drupal\Component\Render\PlainTextOutput;
|
||||
use Drupal\Component\Utility\NestedArray;
|
||||
use Drupal\Core\Cache\CacheableMetadata;
|
||||
use Drupal\Core\Field\FieldStorageDefinitionInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\entity_test\Entity\EntityTest;
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Drupal\field\Entity\FieldStorageConfig;
|
||||
use Drupal\file\Entity\File;
|
||||
use Drupal\file\FileInterface;
|
||||
use Drupal\rest\RestResourceConfigInterface;
|
||||
use Drupal\user\Entity\User;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
// cspell:ignore èxample msword
|
||||
|
||||
/**
|
||||
* Tests binary data file upload route.
|
||||
*/
|
||||
abstract class FileUploadResourceTestBase extends ResourceTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['rest_test', 'entity_test', 'file', 'user'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceConfigId = 'file.upload';
|
||||
|
||||
/**
|
||||
* The POST URI.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $postUri = 'file/upload/entity_test/entity_test/field_rest_file_test';
|
||||
|
||||
/**
|
||||
* Test file data.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $testFileData = 'Hares sit on chairs, and mules sit on stools.';
|
||||
|
||||
/**
|
||||
* The test field storage config.
|
||||
*
|
||||
* @var \Drupal\field\Entity\FieldStorageConfig
|
||||
*/
|
||||
protected $fieldStorage;
|
||||
|
||||
/**
|
||||
* The field config.
|
||||
*
|
||||
* @var \Drupal\field\Entity\FieldConfig
|
||||
*/
|
||||
protected $field;
|
||||
|
||||
/**
|
||||
* The parent entity.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* Created file entity.
|
||||
*
|
||||
* @var \Drupal\file\Entity\File
|
||||
*/
|
||||
protected $file;
|
||||
|
||||
/**
|
||||
* An authenticated user.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $user;
|
||||
|
||||
/**
|
||||
* The entity storage for the 'file' entity type.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityStorageInterface
|
||||
*/
|
||||
protected $fileStorage;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->fileStorage = $this->container->get('entity_type.manager')
|
||||
->getStorage('file');
|
||||
|
||||
// Add a file field.
|
||||
$this->fieldStorage = FieldStorageConfig::create([
|
||||
'entity_type' => 'entity_test',
|
||||
'field_name' => 'field_rest_file_test',
|
||||
'type' => 'file',
|
||||
'settings' => [
|
||||
'uri_scheme' => 'public',
|
||||
],
|
||||
])
|
||||
->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
|
||||
$this->fieldStorage->save();
|
||||
|
||||
$this->field = FieldConfig::create([
|
||||
'entity_type' => 'entity_test',
|
||||
'field_name' => 'field_rest_file_test',
|
||||
'bundle' => 'entity_test',
|
||||
'settings' => [
|
||||
'file_directory' => 'foobar',
|
||||
'file_extensions' => 'txt',
|
||||
'max_filesize' => '',
|
||||
],
|
||||
])
|
||||
->setLabel('Test file field')
|
||||
->setTranslatable(FALSE);
|
||||
$this->field->save();
|
||||
|
||||
// Create an entity that a file can be attached to.
|
||||
$this->entity = EntityTest::create([
|
||||
'name' => 'Llama',
|
||||
'type' => 'entity_test',
|
||||
]);
|
||||
$this->entity->setOwnerId(isset($this->account) ? $this->account->id() : 0);
|
||||
$this->entity->save();
|
||||
|
||||
// Provision entity_test resource.
|
||||
$this->resourceConfigStorage->create([
|
||||
'id' => 'entity.entity_test',
|
||||
'granularity' => RestResourceConfigInterface::RESOURCE_GRANULARITY,
|
||||
'configuration' => [
|
||||
'methods' => ['POST'],
|
||||
'formats' => [static::$format],
|
||||
'authentication' => [static::$auth],
|
||||
],
|
||||
'status' => TRUE,
|
||||
])->save();
|
||||
|
||||
// Provisioning the file upload REST resource without the File REST resource
|
||||
// does not make sense.
|
||||
$this->resourceConfigStorage->create([
|
||||
'id' => 'entity.file',
|
||||
'granularity' => RestResourceConfigInterface::RESOURCE_GRANULARITY,
|
||||
'configuration' => [
|
||||
'methods' => ['GET'],
|
||||
'formats' => [static::$format],
|
||||
'authentication' => isset(static::$auth) ? [static::$auth] : [],
|
||||
],
|
||||
'status' => TRUE,
|
||||
])->save();
|
||||
|
||||
$this->refreshTestStateAfterRestConfigChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests using the file upload POST route.
|
||||
*/
|
||||
public function testPostFileUpload(): void {
|
||||
$this->initAuthentication();
|
||||
|
||||
$this->provisionResource([static::$format], static::$auth ? [static::$auth] : [], ['POST']);
|
||||
|
||||
$uri = Url::fromUri('base:' . static::$postUri);
|
||||
|
||||
// DX: 403 when unauthorized.
|
||||
$response = $this->fileRequest($uri, $this->testFileData);
|
||||
$this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('POST'), $response);
|
||||
|
||||
$this->setUpAuthorization('POST');
|
||||
|
||||
// 404 when the field name is invalid.
|
||||
$invalid_uri = Url::fromUri('base:file/upload/entity_test/entity_test/field_rest_file_test_invalid');
|
||||
$response = $this->fileRequest($invalid_uri, $this->testFileData);
|
||||
$this->assertResourceErrorResponse(404, 'Field "field_rest_file_test_invalid" does not exist', $response);
|
||||
|
||||
// This request will have the default 'application/octet-stream' content
|
||||
// type header.
|
||||
$response = $this->fileRequest($uri, $this->testFileData);
|
||||
$this->assertSame(201, $response->getStatusCode());
|
||||
$expected = $this->getExpectedNormalizedEntity();
|
||||
$this->assertResponseData($expected, $response);
|
||||
|
||||
// Check the actual file data.
|
||||
$this->assertSame($this->testFileData, file_get_contents('public://foobar/example.txt'));
|
||||
|
||||
// Test the file again but using 'filename' in the Content-Disposition
|
||||
// header with no 'file' prefix.
|
||||
$response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'filename="example.txt"']);
|
||||
$this->assertSame(201, $response->getStatusCode());
|
||||
$expected = $this->getExpectedNormalizedEntity(2, 'example_0.txt', TRUE);
|
||||
$this->assertResponseData($expected, $response);
|
||||
|
||||
// Check the actual file data.
|
||||
$this->assertSame($this->testFileData, file_get_contents('public://foobar/example_0.txt'));
|
||||
$this->assertTrue($this->fileStorage->loadUnchanged(1)->isTemporary());
|
||||
|
||||
// Verify that we can create an entity that references the uploaded file.
|
||||
$entity_test_post_url = Url::fromRoute('rest.entity.entity_test.POST')
|
||||
->setOption('query', ['_format' => static::$format]);
|
||||
$request_options = [];
|
||||
$request_options[RequestOptions::HEADERS]['Content-Type'] = static::$mimeType;
|
||||
$request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions('POST'));
|
||||
|
||||
$request_options[RequestOptions::BODY] = $this->serializer->encode($this->getNormalizedPostEntity(), static::$format);
|
||||
$response = $this->request('POST', $entity_test_post_url, $request_options);
|
||||
$this->assertResourceResponse(201, FALSE, $response);
|
||||
$this->assertTrue($this->fileStorage->loadUnchanged(1)->isPermanent());
|
||||
$this->assertSame([
|
||||
[
|
||||
'target_id' => '1',
|
||||
'display' => NULL,
|
||||
'description' => "The most fascinating file ever!",
|
||||
],
|
||||
], EntityTest::load(2)->get('field_rest_file_test')->getValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the normalized POST entity referencing the uploaded file.
|
||||
*
|
||||
* @return array
|
||||
* The normalized POST entity.
|
||||
*
|
||||
* @see ::testPostFileUpload()
|
||||
* @see \Drupal\Tests\rest\Functional\EntityResource\EntityTest\EntityTestResourceTestBase::getNormalizedPostEntity()
|
||||
*/
|
||||
protected function getNormalizedPostEntity() {
|
||||
return [
|
||||
'type' => [
|
||||
[
|
||||
'value' => 'entity_test',
|
||||
],
|
||||
],
|
||||
'name' => [
|
||||
[
|
||||
'value' => 'Drama llama',
|
||||
],
|
||||
],
|
||||
'field_rest_file_test' => [
|
||||
[
|
||||
'target_id' => 1,
|
||||
'description' => 'The most fascinating file ever!',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests using the file upload POST route with invalid headers.
|
||||
*/
|
||||
public function testPostFileUploadInvalidHeaders(): void {
|
||||
$this->initAuthentication();
|
||||
|
||||
$this->provisionResource([static::$format], static::$auth ? [static::$auth] : [], ['POST']);
|
||||
|
||||
$this->setUpAuthorization('POST');
|
||||
|
||||
$uri = Url::fromUri('base:' . static::$postUri);
|
||||
|
||||
// The wrong content type header should return a 415 code.
|
||||
$response = $this->fileRequest($uri, $this->testFileData, ['Content-Type' => static::$mimeType]);
|
||||
$this->assertResourceErrorResponse(415, sprintf('No route found that matches "Content-Type: %s"', static::$mimeType), $response);
|
||||
|
||||
// An empty Content-Disposition header should return a 400.
|
||||
$response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => FALSE]);
|
||||
$this->assertResourceErrorResponse(400, '"Content-Disposition" header is required. A file name in the format "filename=FILENAME" must be provided.', $response);
|
||||
|
||||
// An empty filename with a context in the Content-Disposition header should
|
||||
// return a 400.
|
||||
$response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'file; filename=""']);
|
||||
$this->assertResourceErrorResponse(400, 'No filename found in "Content-Disposition" header. A file name in the format "filename=FILENAME" must be provided.', $response);
|
||||
|
||||
// An empty filename without a context in the Content-Disposition header
|
||||
// should return a 400.
|
||||
$response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'filename=""']);
|
||||
$this->assertResourceErrorResponse(400, 'No filename found in "Content-Disposition" header. A file name in the format "filename=FILENAME" must be provided.', $response);
|
||||
|
||||
// An invalid key-value pair in the Content-Disposition header should return
|
||||
// a 400.
|
||||
$response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'not_a_filename="example.txt"']);
|
||||
$this->assertResourceErrorResponse(400, 'No filename found in "Content-Disposition" header. A file name in the format "filename=FILENAME" must be provided.', $response);
|
||||
|
||||
// Using filename* extended format is not currently supported.
|
||||
$response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'filename*="UTF-8 \' \' example.txt"']);
|
||||
$this->assertResourceErrorResponse(400, 'The extended "filename*" format is currently not supported in the "Content-Disposition" header.', $response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests using the file upload POST route with a duplicate file name.
|
||||
*
|
||||
* A new file should be created with a suffixed name.
|
||||
*/
|
||||
public function testPostFileUploadDuplicateFile(): void {
|
||||
$this->initAuthentication();
|
||||
|
||||
$this->provisionResource([static::$format], static::$auth ? [static::$auth] : [], ['POST']);
|
||||
|
||||
$this->setUpAuthorization('POST');
|
||||
|
||||
$uri = Url::fromUri('base:' . static::$postUri);
|
||||
|
||||
// This request will have the default 'application/octet-stream' content
|
||||
// type header.
|
||||
$response = $this->fileRequest($uri, $this->testFileData);
|
||||
|
||||
$this->assertSame(201, $response->getStatusCode());
|
||||
|
||||
// Make the same request again. The file should be saved as a new file
|
||||
// entity that has the same file name but a suffixed file URI.
|
||||
$response = $this->fileRequest($uri, $this->testFileData);
|
||||
$this->assertSame(201, $response->getStatusCode());
|
||||
|
||||
// Loading expected normalized data for file 2, the duplicate file.
|
||||
$expected = $this->getExpectedNormalizedEntity(2, 'example_0.txt', TRUE);
|
||||
$this->assertResponseData($expected, $response);
|
||||
|
||||
// Check the actual file data.
|
||||
$this->assertSame($this->testFileData, file_get_contents('public://foobar/example_0.txt'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests using the file upload POST route twice, simulating a race condition.
|
||||
*
|
||||
* A validation error should occur when the filenames are not unique.
|
||||
*/
|
||||
public function testPostFileUploadDuplicateFileRaceCondition(): void {
|
||||
$this->initAuthentication();
|
||||
|
||||
$this->provisionResource([static::$format], static::$auth ? [static::$auth] : [], ['POST']);
|
||||
|
||||
$this->setUpAuthorization('POST');
|
||||
|
||||
$uri = Url::fromUri('base:' . static::$postUri);
|
||||
|
||||
// This request will have the default 'application/octet-stream' content
|
||||
// type header.
|
||||
$response = $this->fileRequest($uri, $this->testFileData);
|
||||
|
||||
$this->assertSame(201, $response->getStatusCode());
|
||||
|
||||
// Simulate a race condition where two files are uploaded at almost the same
|
||||
// time, by removing the first uploaded file from disk (leaving the entry in
|
||||
// the file_managed table) before trying to upload another file with the
|
||||
// same name.
|
||||
unlink(\Drupal::service('file_system')->realpath('public://foobar/example.txt'));
|
||||
|
||||
// Make the same request again. The upload should fail validation.
|
||||
$response = $this->fileRequest($uri, $this->testFileData);
|
||||
$this->assertResourceErrorResponse(422, PlainTextOutput::renderFromHtml("Unprocessable Entity: file validation failed.\nThe file public://foobar/example.txt already exists. Enter a unique file URI."), $response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests using the file upload route with any path prefixes being stripped.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition#Directives
|
||||
*/
|
||||
public function testFileUploadStrippedFilePath(): void {
|
||||
$this->initAuthentication();
|
||||
|
||||
$this->provisionResource([static::$format], static::$auth ? [static::$auth] : [], ['POST']);
|
||||
|
||||
$this->setUpAuthorization('POST');
|
||||
|
||||
$uri = Url::fromUri('base:' . static::$postUri);
|
||||
|
||||
$response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'file; filename="directory/example.txt"']);
|
||||
$this->assertSame(201, $response->getStatusCode());
|
||||
$expected = $this->getExpectedNormalizedEntity();
|
||||
$this->assertResponseData($expected, $response);
|
||||
|
||||
// Check the actual file data. It should have been written to the configured
|
||||
// directory, not /foobar/directory/example.txt.
|
||||
$this->assertSame($this->testFileData, file_get_contents('public://foobar/example.txt'));
|
||||
|
||||
$response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'file; filename="../../example_2.txt"']);
|
||||
$this->assertSame(201, $response->getStatusCode());
|
||||
$expected = $this->getExpectedNormalizedEntity(2, 'example_2.txt', TRUE);
|
||||
$this->assertResponseData($expected, $response);
|
||||
|
||||
// Check the actual file data. It should have been written to the configured
|
||||
// directory, not /foobar/directory/example.txt.
|
||||
$this->assertSame($this->testFileData, file_get_contents('public://foobar/example_2.txt'));
|
||||
$this->assertFileDoesNotExist('../../example_2.txt');
|
||||
|
||||
// Check a path from the root. Extensions have to be empty to allow a file
|
||||
// with no extension to pass validation.
|
||||
$this->field->setSetting('file_extensions', '')
|
||||
->save();
|
||||
$this->refreshTestStateAfterRestConfigChange();
|
||||
|
||||
$response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'file; filename="/etc/passwd"']);
|
||||
$this->assertSame(201, $response->getStatusCode());
|
||||
$expected = $this->getExpectedNormalizedEntity(3, 'passwd', TRUE);
|
||||
// This mime will be guessed as there is no extension.
|
||||
$expected['filemime'][0]['value'] = 'application/octet-stream';
|
||||
$this->assertResponseData($expected, $response);
|
||||
|
||||
// Check the actual file data. It should have been written to the configured
|
||||
// directory, not /foobar/directory/example.txt.
|
||||
$this->assertSame($this->testFileData, file_get_contents('public://foobar/passwd'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests using the file upload route with a unicode file name.
|
||||
*/
|
||||
public function testFileUploadUnicodeFilename(): void {
|
||||
$this->initAuthentication();
|
||||
|
||||
$this->provisionResource([static::$format], static::$auth ? [static::$auth] : [], ['POST']);
|
||||
|
||||
$this->setUpAuthorization('POST');
|
||||
|
||||
$uri = Url::fromUri('base:' . static::$postUri);
|
||||
|
||||
// It is important that the filename starts with a unicode character. See
|
||||
// https://bugs.php.net/bug.php?id=77239.
|
||||
$response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'file; filename="Èxample-✓.txt"']);
|
||||
$this->assertSame(201, $response->getStatusCode());
|
||||
$expected = $this->getExpectedNormalizedEntity(1, 'Èxample-✓.txt', TRUE);
|
||||
$this->assertResponseData($expected, $response);
|
||||
$this->assertSame($this->testFileData, file_get_contents('public://foobar/Èxample-✓.txt'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests using the file upload route with a zero byte file.
|
||||
*/
|
||||
public function testFileUploadZeroByteFile(): void {
|
||||
$this->initAuthentication();
|
||||
|
||||
$this->provisionResource([static::$format], static::$auth ? [static::$auth] : [], ['POST']);
|
||||
|
||||
$this->setUpAuthorization('POST');
|
||||
|
||||
$uri = Url::fromUri('base:' . static::$postUri);
|
||||
|
||||
// Test with a zero byte file.
|
||||
$response = $this->fileRequest($uri, NULL);
|
||||
$this->assertSame(201, $response->getStatusCode());
|
||||
$expected = $this->getExpectedNormalizedEntity();
|
||||
// Modify the default expected data to account for the 0 byte file.
|
||||
$expected['filesize'][0]['value'] = 0;
|
||||
$this->assertResponseData($expected, $response);
|
||||
|
||||
// Check the actual file data.
|
||||
$this->assertSame('', file_get_contents('public://foobar/example.txt'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests using the file upload route with an invalid file type.
|
||||
*/
|
||||
public function testFileUploadInvalidFileType(): void {
|
||||
$this->initAuthentication();
|
||||
|
||||
$this->provisionResource([static::$format], static::$auth ? [static::$auth] : [], ['POST']);
|
||||
|
||||
$this->setUpAuthorization('POST');
|
||||
|
||||
$uri = Url::fromUri('base:' . static::$postUri);
|
||||
|
||||
// Test with a JSON file.
|
||||
$response = $this->fileRequest($uri, '{"test":123}', ['Content-Disposition' => 'filename="example.json"']);
|
||||
$this->assertResourceErrorResponse(422, PlainTextOutput::renderFromHtml("Unprocessable Entity: file validation failed.\nOnly files with the following extensions are allowed: <em class=\"placeholder\">txt</em>."), $response);
|
||||
|
||||
// Make sure that no file was saved.
|
||||
$this->assertEmpty(File::load(1));
|
||||
$this->assertFileDoesNotExist('public://foobar/example.txt');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests using the file upload route with a file size larger than allowed.
|
||||
*/
|
||||
public function testFileUploadLargerFileSize(): void {
|
||||
// Set a limit of 50 bytes.
|
||||
$this->field->setSetting('max_filesize', 50)
|
||||
->save();
|
||||
$this->refreshTestStateAfterRestConfigChange();
|
||||
|
||||
$this->initAuthentication();
|
||||
|
||||
$this->provisionResource([static::$format], static::$auth ? [static::$auth] : [], ['POST']);
|
||||
|
||||
$this->setUpAuthorization('POST');
|
||||
|
||||
$uri = Url::fromUri('base:' . static::$postUri);
|
||||
|
||||
// Generate a string larger than the 50 byte limit set.
|
||||
$response = $this->fileRequest($uri, $this->randomString(100));
|
||||
$this->assertResourceErrorResponse(422, PlainTextOutput::renderFromHtml("Unprocessable Entity: file validation failed.\nThe file is <em class=\"placeholder\">100 bytes</em> exceeding the maximum file size of <em class=\"placeholder\">50 bytes</em>."), $response);
|
||||
|
||||
// Make sure that no file was saved.
|
||||
$this->assertEmpty(File::load(1));
|
||||
$this->assertFileDoesNotExist('public://foobar/example.txt');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests using the file upload POST route with malicious extensions.
|
||||
*/
|
||||
public function testFileUploadMaliciousExtension(): void {
|
||||
$this->initAuthentication();
|
||||
|
||||
$this->provisionResource([static::$format], static::$auth ? [static::$auth] : [], ['POST']);
|
||||
// Allow all file uploads but system.file::allow_insecure_uploads is set to
|
||||
// FALSE.
|
||||
$this->field->setSetting('file_extensions', '')->save();
|
||||
$this->refreshTestStateAfterRestConfigChange();
|
||||
|
||||
$this->setUpAuthorization('POST');
|
||||
|
||||
$uri = Url::fromUri('base:' . static::$postUri);
|
||||
|
||||
$php_string = '<?php print "Drupal"; ?>';
|
||||
|
||||
// Test using a masked exploit file.
|
||||
$response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example.php"']);
|
||||
// The filename is not munged because .txt is added and it is a known
|
||||
// extension to apache.
|
||||
$expected = $this->getExpectedNormalizedEntity(1, 'example.php_.txt', TRUE);
|
||||
// Override the expected filesize.
|
||||
$expected['filesize'][0]['value'] = strlen($php_string);
|
||||
$this->assertResponseData($expected, $response);
|
||||
$this->assertFileExists('public://foobar/example.php_.txt');
|
||||
|
||||
// Add .php and .txt as allowed extensions. Since 'allow_insecure_uploads'
|
||||
// is FALSE, .php files should be renamed to have a .txt extension.
|
||||
$this->field->setSetting('file_extensions', 'php txt')->save();
|
||||
$this->refreshTestStateAfterRestConfigChange();
|
||||
|
||||
$response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example_2.php"']);
|
||||
$expected = $this->getExpectedNormalizedEntity(2, 'example_2.php_.txt', TRUE);
|
||||
// Override the expected filesize.
|
||||
$expected['filesize'][0]['value'] = strlen($php_string);
|
||||
$this->assertResponseData($expected, $response);
|
||||
$this->assertFileExists('public://foobar/example_2.php_.txt');
|
||||
$this->assertFileDoesNotExist('public://foobar/example_2.php');
|
||||
|
||||
// Allow .doc file uploads and ensure even a mis-configured apache will not
|
||||
// fallback to php because the filename will be munged.
|
||||
$this->field->setSetting('file_extensions', 'doc')->save();
|
||||
$this->refreshTestStateAfterRestConfigChange();
|
||||
|
||||
// Test using a masked exploit file.
|
||||
$response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example_3.php.doc"']);
|
||||
// The filename is munged.
|
||||
$expected = $this->getExpectedNormalizedEntity(3, 'example_3.php_.doc', TRUE);
|
||||
// Override the expected filesize.
|
||||
$expected['filesize'][0]['value'] = strlen($php_string);
|
||||
// The file mime should be 'application/msword'.
|
||||
$expected['filemime'][0]['value'] = 'application/msword';
|
||||
$this->assertResponseData($expected, $response);
|
||||
$this->assertFileExists('public://foobar/example_3.php_.doc');
|
||||
$this->assertFileDoesNotExist('public://foobar/example_3.php.doc');
|
||||
|
||||
// Test that a dangerous extension such as .php is munged even if it is in
|
||||
// the list of allowed extensions.
|
||||
$this->field->setSetting('file_extensions', 'doc php')->save();
|
||||
$this->refreshTestStateAfterRestConfigChange();
|
||||
|
||||
// Test using a masked exploit file.
|
||||
$response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example_4.php.doc"']);
|
||||
// The filename is munged.
|
||||
$expected = $this->getExpectedNormalizedEntity(4, 'example_4.php_.doc', TRUE);
|
||||
// Override the expected filesize.
|
||||
$expected['filesize'][0]['value'] = strlen($php_string);
|
||||
// The file mime should be 'application/msword'.
|
||||
$expected['filemime'][0]['value'] = 'application/msword';
|
||||
$this->assertResponseData($expected, $response);
|
||||
$this->assertFileExists('public://foobar/example_4.php_.doc');
|
||||
$this->assertFileDoesNotExist('public://foobar/example_4.php.doc');
|
||||
|
||||
// Dangerous extensions are munged even when all extensions are allowed.
|
||||
$this->field->setSetting('file_extensions', '')->save();
|
||||
$this->rebuildAll();
|
||||
$response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example_5.php.png"']);
|
||||
$expected = $this->getExpectedNormalizedEntity(5, 'example_5.php_.png', TRUE);
|
||||
// Override the expected filesize.
|
||||
$expected['filesize'][0]['value'] = strlen($php_string);
|
||||
// The file mime should still see this as a PNG image.
|
||||
$expected['filemime'][0]['value'] = 'image/png';
|
||||
$this->assertResponseData($expected, $response);
|
||||
$this->assertFileExists('public://foobar/example_5.php_.png');
|
||||
|
||||
// Dangerous extensions are munged if is renamed to end in .txt.
|
||||
$response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example_6.cgi.png.txt"']);
|
||||
$expected = $this->getExpectedNormalizedEntity(6, 'example_6.cgi_.png_.txt', TRUE);
|
||||
// Override the expected filesize.
|
||||
$expected['filesize'][0]['value'] = strlen($php_string);
|
||||
// The file mime should also now be text.
|
||||
$expected['filemime'][0]['value'] = 'text/plain';
|
||||
$this->assertResponseData($expected, $response);
|
||||
$this->assertFileExists('public://foobar/example_6.cgi_.png_.txt');
|
||||
|
||||
// Add .php as an allowed extension without .txt. Since insecure uploads are
|
||||
// not allowed, .php files will be rejected.
|
||||
$this->field->setSetting('file_extensions', 'php')->save();
|
||||
$this->refreshTestStateAfterRestConfigChange();
|
||||
|
||||
$response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example_7.php"']);
|
||||
$this->assertResourceErrorResponse(422, "Unprocessable Entity: file validation failed.\nFor security reasons, your upload has been rejected.", $response);
|
||||
|
||||
// Make sure that no file was saved.
|
||||
$this->assertFileDoesNotExist('public://foobar/example_7.php');
|
||||
$this->assertFileDoesNotExist('public://foobar/example_7.php.txt');
|
||||
|
||||
// Now allow insecure uploads.
|
||||
\Drupal::configFactory()
|
||||
->getEditable('system.file')
|
||||
->set('allow_insecure_uploads', TRUE)
|
||||
->save();
|
||||
// Allow all file uploads. This is very insecure.
|
||||
$this->field->setSetting('file_extensions', '')->save();
|
||||
$this->refreshTestStateAfterRestConfigChange();
|
||||
|
||||
$response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example_7.php"']);
|
||||
$expected = $this->getExpectedNormalizedEntity(7, 'example_7.php', TRUE);
|
||||
// Override the expected filesize.
|
||||
$expected['filesize'][0]['value'] = strlen($php_string);
|
||||
// The file mime should also now be PHP.
|
||||
$expected['filemime'][0]['value'] = 'application/x-httpd-php';
|
||||
$this->assertResponseData($expected, $response);
|
||||
$this->assertFileExists('public://foobar/example_7.php');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests using the file upload POST route no extension configured.
|
||||
*/
|
||||
public function testFileUploadNoExtensionSetting(): void {
|
||||
$this->initAuthentication();
|
||||
|
||||
$this->provisionResource([static::$format], static::$auth ? [static::$auth] : [], ['POST']);
|
||||
|
||||
$this->setUpAuthorization('POST');
|
||||
|
||||
$uri = Url::fromUri('base:' . static::$postUri);
|
||||
|
||||
$this->field->setSetting('file_extensions', '')
|
||||
->save();
|
||||
$this->refreshTestStateAfterRestConfigChange();
|
||||
|
||||
$response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'filename="example.txt"']);
|
||||
$expected = $this->getExpectedNormalizedEntity(1, 'example.txt', TRUE);
|
||||
|
||||
$this->assertResponseData($expected, $response);
|
||||
$this->assertFileExists('public://foobar/example.txt');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function assertNormalizationEdgeCases($method, Url $url, array $request_options) {
|
||||
// The file upload resource only accepts binary data, so there are no
|
||||
// normalization edge cases to test, as there are no normalized entity
|
||||
// representations incoming.
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedUnauthorizedAccessMessage($method) {
|
||||
return "The following permissions are required: 'administer entity_test content' OR 'administer entity_test_with_bundle content' OR 'create entity_test entity_test_with_bundle entities'.";
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the expected file entity.
|
||||
*
|
||||
* @param int $fid
|
||||
* The file ID to load and create normalized data for.
|
||||
* @param string $expected_filename
|
||||
* The expected filename for the stored file.
|
||||
* @param bool $expected_as_filename
|
||||
* Whether the expected filename should be the filename property too.
|
||||
*
|
||||
* @return array
|
||||
* The expected normalized data array.
|
||||
*/
|
||||
protected function getExpectedNormalizedEntity($fid = 1, $expected_filename = 'example.txt', $expected_as_filename = FALSE) {
|
||||
$author = User::load(static::$auth ? $this->account->id() : 0);
|
||||
$file = File::load($fid);
|
||||
$this->assertInstanceOf(FileInterface::class, $file);
|
||||
|
||||
$expected_normalization = [
|
||||
'fid' => [
|
||||
[
|
||||
'value' => (int) $file->id(),
|
||||
],
|
||||
],
|
||||
'uuid' => [
|
||||
[
|
||||
'value' => $file->uuid(),
|
||||
],
|
||||
],
|
||||
'langcode' => [
|
||||
[
|
||||
'value' => 'en',
|
||||
],
|
||||
],
|
||||
'uid' => [
|
||||
[
|
||||
'target_id' => (int) $author->id(),
|
||||
'target_type' => 'user',
|
||||
'target_uuid' => $author->uuid(),
|
||||
'url' => base_path() . 'user/' . $author->id(),
|
||||
],
|
||||
],
|
||||
'filename' => [
|
||||
[
|
||||
'value' => $expected_as_filename ? $expected_filename : 'example.txt',
|
||||
],
|
||||
],
|
||||
'uri' => [
|
||||
[
|
||||
'value' => 'public://foobar/' . $expected_filename,
|
||||
'url' => base_path() . $this->siteDirectory . '/files/foobar/' . rawurlencode($expected_filename),
|
||||
],
|
||||
],
|
||||
'filemime' => [
|
||||
[
|
||||
'value' => 'text/plain',
|
||||
],
|
||||
],
|
||||
'filesize' => [
|
||||
[
|
||||
'value' => strlen($this->testFileData),
|
||||
],
|
||||
],
|
||||
'status' => [
|
||||
[
|
||||
'value' => FALSE,
|
||||
],
|
||||
],
|
||||
'created' => [
|
||||
[
|
||||
'value' => (new \DateTime())->setTimestamp($file->getCreatedTime())->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339),
|
||||
'format' => \DateTime::RFC3339,
|
||||
],
|
||||
],
|
||||
'changed' => [
|
||||
[
|
||||
'value' => (new \DateTime())->setTimestamp($file->getChangedTime())->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339),
|
||||
'format' => \DateTime::RFC3339,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
return $expected_normalization;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a file upload request. Wraps the Guzzle HTTP client.
|
||||
*
|
||||
* @param \Drupal\Core\Url $url
|
||||
* URL to request.
|
||||
* @param string $file_contents
|
||||
* The file contents to send as the request body.
|
||||
* @param array $headers
|
||||
* Additional headers to send with the request. Defaults will be added for
|
||||
* Content-Type and Content-Disposition. In order to remove the defaults set
|
||||
* the header value to FALSE.
|
||||
*
|
||||
* @return \Psr\Http\Message\ResponseInterface
|
||||
* The response object.
|
||||
*
|
||||
* @see \GuzzleHttp\ClientInterface::request()
|
||||
*/
|
||||
protected function fileRequest(Url $url, $file_contents, array $headers = []): ResponseInterface {
|
||||
// Set the format for the response.
|
||||
$url->setOption('query', ['_format' => static::$format]);
|
||||
|
||||
$request_options = [];
|
||||
$headers = $headers + [
|
||||
// Set the required (and only accepted) content type for the request.
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
// Set the required Content-Disposition header for the file name.
|
||||
'Content-Disposition' => 'file; filename="example.txt"',
|
||||
];
|
||||
$request_options[RequestOptions::HEADERS] = array_filter($headers, function ($value) {
|
||||
return $value !== FALSE;
|
||||
});
|
||||
$request_options[RequestOptions::BODY] = $file_contents;
|
||||
$request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions('POST'));
|
||||
|
||||
return $this->request('POST', $url, $request_options);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method) {
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
$this->grantPermissionsToTestedRole(['view test entity']);
|
||||
break;
|
||||
|
||||
case 'POST':
|
||||
$this->grantPermissionsToTestedRole(['create entity_test entity_test_with_bundle entities', 'access content']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts expected normalized data matches response data.
|
||||
*
|
||||
* @param array $expected
|
||||
* The expected data.
|
||||
* @param \Psr\Http\Message\ResponseInterface $response
|
||||
* The file upload response.
|
||||
*/
|
||||
protected function assertResponseData(array $expected, ResponseInterface $response) {
|
||||
static::recursiveKSort($expected);
|
||||
$actual = $this->serializer->decode((string) $response->getBody(), static::$format);
|
||||
static::recursiveKSort($actual);
|
||||
|
||||
$this->assertSame($expected, $actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedUnauthorizedAccessCacheability() {
|
||||
// There is cacheability metadata to check as file uploads only allows POST
|
||||
// requests, which will not return cacheable responses.
|
||||
return new CacheableMetadata();
|
||||
}
|
||||
|
||||
}
|
||||
14
web/core/modules/rest/tests/src/Functional/GenericTest.php
Normal file
14
web/core/modules/rest/tests/src/Functional/GenericTest.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\rest\Functional;
|
||||
|
||||
use Drupal\Tests\system\Functional\Module\GenericModuleTestBase;
|
||||
|
||||
/**
|
||||
* Generic module test for rest.
|
||||
*
|
||||
* @group rest
|
||||
*/
|
||||
class GenericTest extends GenericModuleTestBase {}
|
||||
165
web/core/modules/rest/tests/src/Functional/ResourceTest.php
Normal file
165
web/core/modules/rest/tests/src/Functional/ResourceTest.php
Normal file
@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\rest\Functional;
|
||||
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\entity_test\Entity\EntityTest;
|
||||
use Drupal\rest\Entity\RestResourceConfig;
|
||||
use Drupal\rest\RestResourceConfigInterface;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
use Drupal\user\Entity\Role;
|
||||
use Drupal\user\RoleInterface;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
|
||||
/**
|
||||
* Tests the structure of a REST resource.
|
||||
*
|
||||
* @group rest
|
||||
*/
|
||||
class ResourceTest extends BrowserTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['rest', 'entity_test', 'rest_test'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* The entity.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
// Create an entity programmatic.
|
||||
$this->entity = EntityTest::create([
|
||||
'name' => $this->randomMachineName(),
|
||||
'user_id' => 1,
|
||||
'field_test_text' => [
|
||||
0 => [
|
||||
'value' => $this->randomString(),
|
||||
'format' => 'plain_text',
|
||||
],
|
||||
],
|
||||
]);
|
||||
$this->entity->save();
|
||||
|
||||
Role::load(AccountInterface::ANONYMOUS_ROLE)
|
||||
->grantPermission('view test entity')
|
||||
->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that a resource without formats cannot be enabled.
|
||||
*/
|
||||
public function testFormats(): void {
|
||||
RestResourceConfig::create([
|
||||
'id' => 'entity.entity_test',
|
||||
'granularity' => RestResourceConfigInterface::METHOD_GRANULARITY,
|
||||
'configuration' => [
|
||||
'GET' => [
|
||||
'supported_auth' => [
|
||||
'basic_auth',
|
||||
],
|
||||
],
|
||||
],
|
||||
])->save();
|
||||
|
||||
// Verify that accessing the resource returns 406.
|
||||
$this->drupalGet($this->entity->toUrl()->setRouteParameter('_format', 'json'));
|
||||
// \Drupal\Core\Routing\RequestFormatRouteFilter considers the canonical,
|
||||
// non-REST route a match, but a lower quality one: no format restrictions
|
||||
// means there's always a match and hence when there is no matching REST
|
||||
// route, the non-REST route is used, but can't render into
|
||||
// application/json, so it returns a 406.
|
||||
$this->assertSession()->statusCodeEquals(406);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that a resource without authentication cannot be enabled.
|
||||
*/
|
||||
public function testAuthentication(): void {
|
||||
RestResourceConfig::create([
|
||||
'id' => 'entity.entity_test',
|
||||
'granularity' => RestResourceConfigInterface::METHOD_GRANULARITY,
|
||||
'configuration' => [
|
||||
'GET' => [
|
||||
'supported_formats' => [
|
||||
'json',
|
||||
],
|
||||
],
|
||||
],
|
||||
])->save();
|
||||
|
||||
// Verify that accessing the resource returns 401.
|
||||
$this->drupalGet($this->entity->toUrl()->setRouteParameter('_format', 'json'));
|
||||
// \Drupal\Core\Routing\RequestFormatRouteFilter considers the canonical,
|
||||
// non-REST route a match, but a lower quality one: no format restrictions
|
||||
// means there's always a match and hence when there is no matching REST
|
||||
// route, the non-REST route is used, but can't render into
|
||||
// application/json, so it returns a 406.
|
||||
$this->assertSession()->statusCodeEquals(406);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that serialization_class is optional.
|
||||
*/
|
||||
public function testSerializationClassIsOptional(): void {
|
||||
RestResourceConfig::create([
|
||||
'id' => 'serialization_test',
|
||||
'granularity' => RestResourceConfigInterface::METHOD_GRANULARITY,
|
||||
'configuration' => [
|
||||
'POST' => [
|
||||
'supported_formats' => [
|
||||
'json',
|
||||
],
|
||||
'supported_auth' => [
|
||||
'cookie',
|
||||
],
|
||||
],
|
||||
],
|
||||
])->save();
|
||||
\Drupal::service('router.builder')->rebuildIfNeeded();
|
||||
|
||||
Role::load(RoleInterface::ANONYMOUS_ID)
|
||||
->grantPermission('restful post serialization_test')
|
||||
->save();
|
||||
|
||||
$serialized = $this->container->get('serializer')->serialize(['foo', 'bar'], 'json');
|
||||
$request_options = [
|
||||
RequestOptions::HEADERS => ['Content-Type' => 'application/json'],
|
||||
RequestOptions::BODY => $serialized,
|
||||
];
|
||||
/** @var \GuzzleHttp\ClientInterface $client */
|
||||
$client = $this->getSession()->getDriver()->getClient()->getClient();
|
||||
$response = $client->request('POST', $this->buildUrl('serialization_test', ['query' => ['_format' => 'json']]), $request_options);
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
$this->assertSame('["foo","bar"]', (string) $response->getBody());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that resource URI paths are formatted properly.
|
||||
*/
|
||||
public function testUriPaths(): void {
|
||||
/** @var \Drupal\rest\Plugin\Type\ResourcePluginManager $manager */
|
||||
$manager = \Drupal::service('plugin.manager.rest');
|
||||
|
||||
foreach ($manager->getDefinitions() as $definition) {
|
||||
foreach ($definition['uri_paths'] as $uri_path) {
|
||||
$this->assertStringNotContainsString('//', $uri_path, 'The resource URI path does not have duplicate slashes.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
451
web/core/modules/rest/tests/src/Functional/ResourceTestBase.php
Normal file
451
web/core/modules/rest/tests/src/Functional/ResourceTestBase.php
Normal file
@ -0,0 +1,451 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\rest\Functional;
|
||||
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\rest\RestResourceConfigInterface;
|
||||
use Drupal\Tests\ApiRequestTrait;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
use Drupal\user\Entity\Role;
|
||||
use Drupal\user\RoleInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
/**
|
||||
* Subclass this for every REST resource, every format and every auth provider.
|
||||
*
|
||||
* For more guidance see
|
||||
* \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase
|
||||
* which has recommendations for testing the
|
||||
* \Drupal\rest\Plugin\rest\resource\EntityResource REST resource for every
|
||||
* format and every auth provider. It's a special case (because that single REST
|
||||
* resource generates supports not just one thing, but many things — multiple
|
||||
* entity types), but the same principles apply.
|
||||
*/
|
||||
abstract class ResourceTestBase extends BrowserTestBase {
|
||||
|
||||
use ApiRequestTrait {
|
||||
makeApiRequest as request;
|
||||
}
|
||||
|
||||
/**
|
||||
* The format to use in this test.
|
||||
*
|
||||
* A format is the combination of a certain normalizer and a certain
|
||||
* serializer.
|
||||
*
|
||||
* (The default is 'json' because that doesn't depend on any module.)
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @see https://www.drupal.org/developing/api/8/serialization
|
||||
*/
|
||||
protected static $format = 'json';
|
||||
|
||||
/**
|
||||
* The MIME type that corresponds to $format.
|
||||
*
|
||||
* (Sadly this cannot be computed automatically yet.)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $mimeType = 'application/json';
|
||||
|
||||
/**
|
||||
* The authentication mechanism to use in this test.
|
||||
*
|
||||
* (The default is 'cookie' because that doesn't depend on any module.)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $auth = FALSE;
|
||||
|
||||
/**
|
||||
* The REST Resource Config entity ID under test (i.e. a resource type).
|
||||
*
|
||||
* The REST Resource plugin ID can be calculated from this.
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @see \Drupal\rest\Entity\RestResourceConfig::__construct()
|
||||
*/
|
||||
protected static $resourceConfigId = NULL;
|
||||
|
||||
/**
|
||||
* The account to use for authentication, if any.
|
||||
*
|
||||
* @var null|\Drupal\Core\Session\AccountInterface
|
||||
*/
|
||||
protected $account = NULL;
|
||||
|
||||
/**
|
||||
* The REST resource config entity storage.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityStorageInterface
|
||||
*/
|
||||
protected $resourceConfigStorage;
|
||||
|
||||
/**
|
||||
* The serializer service.
|
||||
*
|
||||
* @var \Symfony\Component\Serializer\Serializer
|
||||
*/
|
||||
protected $serializer;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['rest'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->serializer = $this->container->get('serializer');
|
||||
|
||||
// Ensure the anonymous user role has no permissions at all.
|
||||
$user_role = Role::load(RoleInterface::ANONYMOUS_ID);
|
||||
foreach ($user_role->getPermissions() as $permission) {
|
||||
$user_role->revokePermission($permission);
|
||||
}
|
||||
$user_role->save();
|
||||
assert([] === $user_role->getPermissions(), 'The anonymous user role has no permissions at all.');
|
||||
|
||||
if (static::$auth !== FALSE) {
|
||||
// Ensure the authenticated user role has no permissions at all.
|
||||
$user_role = Role::load(RoleInterface::AUTHENTICATED_ID);
|
||||
foreach ($user_role->getPermissions() as $permission) {
|
||||
$user_role->revokePermission($permission);
|
||||
}
|
||||
$user_role->save();
|
||||
assert([] === $user_role->getPermissions(), 'The authenticated user role has no permissions at all.');
|
||||
|
||||
// Create an account.
|
||||
$this->account = $this->createUser();
|
||||
}
|
||||
else {
|
||||
// Otherwise, also create an account, so that any test involving User
|
||||
// entities will have the same user IDs regardless of authentication.
|
||||
$this->createUser();
|
||||
}
|
||||
|
||||
$this->resourceConfigStorage = $this->container->get('entity_type.manager')->getStorage('rest_resource_config');
|
||||
|
||||
// Ensure there's a clean slate: delete all REST resource config entities.
|
||||
$this->resourceConfigStorage->delete($this->resourceConfigStorage->loadMultiple());
|
||||
$this->refreshTestStateAfterRestConfigChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provisions the REST resource under test.
|
||||
*
|
||||
* @param string[] $formats
|
||||
* The allowed formats for this resource.
|
||||
* @param string[] $authentication
|
||||
* The allowed authentication providers for this resource.
|
||||
* @param string[] $methods
|
||||
* The allowed methods for this resource.
|
||||
*/
|
||||
protected function provisionResource($formats = [], $authentication = [], array $methods = ['GET', 'POST', 'PATCH', 'DELETE']) {
|
||||
$this->resourceConfigStorage->create([
|
||||
'id' => static::$resourceConfigId,
|
||||
'granularity' => RestResourceConfigInterface::RESOURCE_GRANULARITY,
|
||||
'configuration' => [
|
||||
'methods' => $methods,
|
||||
'formats' => $formats,
|
||||
'authentication' => $authentication,
|
||||
],
|
||||
'status' => TRUE,
|
||||
])->save();
|
||||
$this->refreshTestStateAfterRestConfigChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the state of the tester to be in sync with the testee.
|
||||
*
|
||||
* Should be called after every change made to:
|
||||
* - RestResourceConfig entities
|
||||
*/
|
||||
protected function refreshTestStateAfterRestConfigChange() {
|
||||
// Ensure that the cache tags invalidator has its internal values reset.
|
||||
// Otherwise the http_response cache tag invalidation won't work.
|
||||
$this->refreshVariables();
|
||||
|
||||
// Tests using this base class may trigger route rebuilds due to changes to
|
||||
// RestResourceConfig entities. Ensure the test generates routes using an
|
||||
// up-to-date router.
|
||||
\Drupal::service('router.builder')->rebuildIfNeeded();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the expected error message.
|
||||
*
|
||||
* @param string $method
|
||||
* The HTTP method (GET, POST, PATCH, DELETE).
|
||||
*
|
||||
* @return string
|
||||
* The error string.
|
||||
*/
|
||||
protected function getExpectedUnauthorizedAccessMessage($method) {
|
||||
$resource_plugin_id = str_replace('.', ':', static::$resourceConfigId);
|
||||
$permission = 'restful ' . strtolower($method) . ' ' . $resource_plugin_id;
|
||||
return "The '$permission' permission is required.";
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the necessary authorization.
|
||||
*
|
||||
* In case of a test verifying publicly accessible REST resources: grant
|
||||
* permissions to the anonymous user role.
|
||||
*
|
||||
* In case of a test verifying behavior when using a particular authentication
|
||||
* provider: create a user with a particular set of permissions.
|
||||
*
|
||||
* Because of the $method parameter, it's possible to first set up
|
||||
* authentication for only GET, then add POST, et cetera. This then also
|
||||
* allows for verifying a 403 in case of missing authorization.
|
||||
*
|
||||
* @param string $method
|
||||
* The HTTP method for which to set up authentication.
|
||||
*
|
||||
* @see ::grantPermissionsToAnonymousRole()
|
||||
* @see ::grantPermissionsToAuthenticatedRole()
|
||||
*/
|
||||
abstract protected function setUpAuthorization($method);
|
||||
|
||||
/**
|
||||
* Verifies the error response in case of missing authentication.
|
||||
*
|
||||
* @param string $method
|
||||
* HTTP method.
|
||||
* @param \Psr\Http\Message\ResponseInterface $response
|
||||
* The response to assert.
|
||||
*/
|
||||
abstract protected function assertResponseWhenMissingAuthentication($method, ResponseInterface $response);
|
||||
|
||||
/**
|
||||
* Asserts normalization-specific edge cases.
|
||||
*
|
||||
* (Should be called before sending a well-formed request.)
|
||||
*
|
||||
* @param string $method
|
||||
* HTTP method.
|
||||
* @param \Drupal\Core\Url $url
|
||||
* URL to request.
|
||||
* @param array $request_options
|
||||
* Request options to apply.
|
||||
*
|
||||
* @see \GuzzleHttp\ClientInterface::request()
|
||||
*/
|
||||
abstract protected function assertNormalizationEdgeCases($method, Url $url, array $request_options);
|
||||
|
||||
/**
|
||||
* Asserts authentication provider-specific edge cases.
|
||||
*
|
||||
* (Should be called before sending a well-formed request.)
|
||||
*
|
||||
* @param string $method
|
||||
* HTTP method.
|
||||
* @param \Drupal\Core\Url $url
|
||||
* URL to request.
|
||||
* @param array $request_options
|
||||
* Request options to apply.
|
||||
*
|
||||
* @see \GuzzleHttp\ClientInterface::request()
|
||||
*/
|
||||
abstract protected function assertAuthenticationEdgeCases($method, Url $url, array $request_options);
|
||||
|
||||
/**
|
||||
* Returns the expected cacheability of an unauthorized access response.
|
||||
*
|
||||
* @return \Drupal\Core\Cache\RefinableCacheableDependencyInterface
|
||||
* The expected cacheability.
|
||||
*/
|
||||
abstract protected function getExpectedUnauthorizedAccessCacheability();
|
||||
|
||||
/**
|
||||
* Initializes authentication.
|
||||
*
|
||||
* E.g. for cookie authentication, we first need to get a cookie.
|
||||
*/
|
||||
protected function initAuthentication() {}
|
||||
|
||||
/**
|
||||
* Returns Guzzle request options for authentication.
|
||||
*
|
||||
* @param string $method
|
||||
* The HTTP method for this authenticated request.
|
||||
*
|
||||
* @return array
|
||||
* Guzzle request options to use for authentication.
|
||||
*
|
||||
* @see \GuzzleHttp\ClientInterface::request()
|
||||
*/
|
||||
protected function getAuthenticationRequestOptions($method) {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Grants permissions to the anonymous role.
|
||||
*
|
||||
* @param string[] $permissions
|
||||
* Permissions to grant.
|
||||
*/
|
||||
protected function grantPermissionsToAnonymousRole(array $permissions) {
|
||||
$this->grantPermissions(Role::load(RoleInterface::ANONYMOUS_ID), $permissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Grants permissions to the authenticated role.
|
||||
*
|
||||
* @param string[] $permissions
|
||||
* Permissions to grant.
|
||||
*/
|
||||
protected function grantPermissionsToAuthenticatedRole(array $permissions) {
|
||||
$this->grantPermissions(Role::load(RoleInterface::AUTHENTICATED_ID), $permissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Grants permissions to the tested role: anonymous or authenticated.
|
||||
*
|
||||
* @param string[] $permissions
|
||||
* Permissions to grant.
|
||||
*
|
||||
* @see ::grantPermissionsToAuthenticatedRole()
|
||||
* @see ::grantPermissionsToAnonymousRole()
|
||||
*/
|
||||
protected function grantPermissionsToTestedRole(array $permissions) {
|
||||
if (static::$auth) {
|
||||
$this->grantPermissionsToAuthenticatedRole($permissions);
|
||||
}
|
||||
else {
|
||||
$this->grantPermissionsToAnonymousRole($permissions);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a resource response has the given status code and body.
|
||||
*
|
||||
* @param int $expected_status_code
|
||||
* The expected response status.
|
||||
* @param string|false $expected_body
|
||||
* The expected response body. FALSE in case this should not be asserted.
|
||||
* @param \Psr\Http\Message\ResponseInterface $response
|
||||
* The response to assert.
|
||||
* @param string[]|false $expected_cache_tags
|
||||
* (optional) The expected cache tags in the X-Drupal-Cache-Tags response
|
||||
* header, or FALSE if that header should be absent. Defaults to FALSE.
|
||||
* @param string[]|false $expected_cache_contexts
|
||||
* (optional) The expected cache contexts in the X-Drupal-Cache-Contexts
|
||||
* response header, or FALSE if that header should be absent. Defaults to
|
||||
* FALSE.
|
||||
* @param string|false $expected_page_cache_header_value
|
||||
* (optional) The expected X-Drupal-Cache response header value, or FALSE if
|
||||
* that header should be absent. Possible strings: 'MISS', 'HIT'. Defaults
|
||||
* to FALSE.
|
||||
* @param string|false $expected_dynamic_page_cache_header_value
|
||||
* (optional) The expected X-Drupal-Dynamic-Cache response header value, or
|
||||
* FALSE if that header should be absent. Possible strings: 'MISS', 'HIT'.
|
||||
* Defaults to FALSE.
|
||||
*/
|
||||
protected function assertResourceResponse($expected_status_code, $expected_body, ResponseInterface $response, $expected_cache_tags = FALSE, $expected_cache_contexts = FALSE, $expected_page_cache_header_value = FALSE, $expected_dynamic_page_cache_header_value = FALSE) {
|
||||
$this->assertSame($expected_status_code, $response->getStatusCode());
|
||||
if ($expected_status_code === 204) {
|
||||
// DELETE responses should not include a Content-Type header. But Apache
|
||||
// sets it to 'text/html' by default. We also cannot detect the presence
|
||||
// of Apache either here in the CLI. For now having this documented here
|
||||
// is all we can do.
|
||||
// $this->assertFalse($response->hasHeader('Content-Type'));
|
||||
$this->assertSame('', (string) $response->getBody());
|
||||
}
|
||||
else {
|
||||
$this->assertSame([static::$mimeType], $response->getHeader('Content-Type'));
|
||||
if ($expected_body !== FALSE) {
|
||||
$this->assertSame($expected_body, (string) $response->getBody());
|
||||
}
|
||||
}
|
||||
|
||||
// Expected cache tags: X-Drupal-Cache-Tags header.
|
||||
$this->assertSame($expected_cache_tags !== FALSE, $response->hasHeader('X-Drupal-Cache-Tags'));
|
||||
if (is_array($expected_cache_tags)) {
|
||||
$this->assertEqualsCanonicalizing($expected_cache_tags, explode(' ', $response->getHeader('X-Drupal-Cache-Tags')[0]));
|
||||
}
|
||||
|
||||
// Expected cache contexts: X-Drupal-Cache-Contexts header.
|
||||
$this->assertSame($expected_cache_contexts !== FALSE, $response->hasHeader('X-Drupal-Cache-Contexts'));
|
||||
if (is_array($expected_cache_contexts)) {
|
||||
$optimized_expected_cache_contexts = \Drupal::service('cache_contexts_manager')->optimizeTokens($expected_cache_contexts);
|
||||
$this->assertEqualsCanonicalizing($optimized_expected_cache_contexts, explode(' ', $response->getHeader('X-Drupal-Cache-Contexts')[0]));
|
||||
}
|
||||
|
||||
// Expected Page Cache header value: X-Drupal-Cache header.
|
||||
if ($expected_page_cache_header_value !== FALSE) {
|
||||
$this->assertTrue($response->hasHeader('X-Drupal-Cache'));
|
||||
$this->assertSame($expected_page_cache_header_value, $response->getHeader('X-Drupal-Cache')[0]);
|
||||
}
|
||||
elseif ($response->hasHeader('X-Drupal-Cache')) {
|
||||
$this->assertMatchesRegularExpression('#^UNCACHEABLE \((no cacheability|(request|response) policy)\)$#', $response->getHeader('X-Drupal-Cache')[0]);
|
||||
}
|
||||
|
||||
// Expected Dynamic Page Cache header value: X-Drupal-Dynamic-Cache header.
|
||||
if ($expected_dynamic_page_cache_header_value !== FALSE) {
|
||||
$this->assertTrue($response->hasHeader('X-Drupal-Dynamic-Cache'));
|
||||
$this->assertSame($expected_dynamic_page_cache_header_value, $response->getHeader('X-Drupal-Dynamic-Cache')[0]);
|
||||
}
|
||||
elseif ($response->hasHeader('X-Drupal-Dynamic-Cache')) {
|
||||
$this->assertMatchesRegularExpression('#^UNCACHEABLE \(((no|poor) cacheability|(request|response) policy)\)$#', $response->getHeader('X-Drupal-Dynamic-Cache')[0]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a resource error response has the given message.
|
||||
*
|
||||
* @param int $expected_status_code
|
||||
* The expected response status.
|
||||
* @param string|false $expected_message
|
||||
* The expected error message.
|
||||
* @param \Psr\Http\Message\ResponseInterface $response
|
||||
* The error response to assert.
|
||||
* @param string[]|false $expected_cache_tags
|
||||
* (optional) The expected cache tags in the X-Drupal-Cache-Tags response
|
||||
* header, or FALSE if that header should be absent. Defaults to FALSE.
|
||||
* @param string[]|false $expected_cache_contexts
|
||||
* (optional) The expected cache contexts in the X-Drupal-Cache-Contexts
|
||||
* response header, or FALSE if that header should be absent. Defaults to
|
||||
* FALSE.
|
||||
* @param string|false $expected_page_cache_header_value
|
||||
* (optional) The expected X-Drupal-Cache response header value, or FALSE if
|
||||
* that header should be absent. Possible strings: 'MISS', 'HIT'. Defaults
|
||||
* to FALSE.
|
||||
* @param string|false $expected_dynamic_page_cache_header_value
|
||||
* (optional) The expected X-Drupal-Dynamic-Cache response header value, or
|
||||
* FALSE if that header should be absent. Possible strings: 'MISS', 'HIT'.
|
||||
* Defaults to FALSE.
|
||||
*/
|
||||
protected function assertResourceErrorResponse($expected_status_code, $expected_message, ResponseInterface $response, $expected_cache_tags = FALSE, $expected_cache_contexts = FALSE, $expected_page_cache_header_value = FALSE, $expected_dynamic_page_cache_header_value = FALSE) {
|
||||
$expected_body = ($expected_message !== FALSE) ? $this->serializer->encode(['message' => $expected_message], static::$format) : FALSE;
|
||||
$this->assertResourceResponse($expected_status_code, $expected_body, $response, $expected_cache_tags, $expected_cache_contexts, $expected_page_cache_header_value, $expected_dynamic_page_cache_header_value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively sorts an array by key.
|
||||
*
|
||||
* @param array $array
|
||||
* An array to sort.
|
||||
*/
|
||||
protected static function recursiveKSort(array &$array) {
|
||||
// First, sort the main array.
|
||||
ksort($array);
|
||||
|
||||
// Then check for child arrays.
|
||||
foreach ($array as &$value) {
|
||||
if (is_array($value)) {
|
||||
static::recursiveKSort($value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\rest\Functional\Rest;
|
||||
|
||||
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
|
||||
|
||||
/**
|
||||
* @group rest
|
||||
*/
|
||||
class RestResourceConfigJsonAnonTest extends RestResourceConfigResourceTestBase {
|
||||
|
||||
use AnonResourceTestTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $format = 'json';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $mimeType = 'application/json';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\rest\Functional\Rest;
|
||||
|
||||
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
|
||||
|
||||
/**
|
||||
* @group rest
|
||||
*/
|
||||
class RestResourceConfigJsonBasicAuthTest extends RestResourceConfigResourceTestBase {
|
||||
|
||||
use BasicAuthResourceTestTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['basic_auth'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $format = 'json';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $mimeType = 'application/json';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $auth = 'basic_auth';
|
||||
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\rest\Functional\Rest;
|
||||
|
||||
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
|
||||
|
||||
/**
|
||||
* @group rest
|
||||
*/
|
||||
class RestResourceConfigJsonCookieTest extends RestResourceConfigResourceTestBase {
|
||||
|
||||
use CookieResourceTestTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $format = 'json';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $mimeType = 'application/json';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $auth = 'cookie';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
}
|
||||
@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\rest\Functional\Rest;
|
||||
|
||||
use Drupal\Tests\rest\Functional\EntityResource\ConfigEntityResourceTestBase;
|
||||
use Drupal\rest\Entity\RestResourceConfig;
|
||||
|
||||
/**
|
||||
* Resource test base for the RestResourceConfig entity.
|
||||
*/
|
||||
abstract class RestResourceConfigResourceTestBase extends ConfigEntityResourceTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['dblog'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $entityTypeId = 'rest_resource_config';
|
||||
|
||||
/**
|
||||
* @var \Drupal\rest\RestResourceConfigInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method) {
|
||||
$this->grantPermissionsToTestedRole(['administer rest resources']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity() {
|
||||
$rest_resource_config = RestResourceConfig::create([
|
||||
'id' => 'llama',
|
||||
'plugin_id' => 'dblog',
|
||||
'granularity' => 'method',
|
||||
'configuration' => [
|
||||
'GET' => [
|
||||
'supported_formats' => [
|
||||
'json',
|
||||
],
|
||||
'supported_auth' => [
|
||||
'cookie',
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
$rest_resource_config->save();
|
||||
|
||||
return $rest_resource_config;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedNormalizedEntity() {
|
||||
return [
|
||||
'uuid' => $this->entity->uuid(),
|
||||
'langcode' => 'en',
|
||||
'status' => TRUE,
|
||||
'dependencies' => [
|
||||
'module' => [
|
||||
'dblog',
|
||||
'serialization',
|
||||
'user',
|
||||
],
|
||||
],
|
||||
'id' => 'llama',
|
||||
'plugin_id' => 'dblog',
|
||||
'granularity' => 'method',
|
||||
'configuration' => [
|
||||
'GET' => [
|
||||
'supported_formats' => [
|
||||
'json',
|
||||
],
|
||||
'supported_auth' => [
|
||||
'cookie',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getNormalizedPostEntity() {
|
||||
// @todo Update in https://www.drupal.org/node/2300677.
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedCacheContexts() {
|
||||
return [
|
||||
'user.permissions',
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\rest\Functional\Rest;
|
||||
|
||||
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
|
||||
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
|
||||
|
||||
/**
|
||||
* @group rest
|
||||
*/
|
||||
class RestResourceConfigXmlAnonTest extends RestResourceConfigResourceTestBase {
|
||||
|
||||
use AnonResourceTestTrait;
|
||||
use XmlEntityNormalizationQuirksTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $format = 'xml';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $mimeType = 'text/xml; charset=UTF-8';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\rest\Functional\Rest;
|
||||
|
||||
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
|
||||
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
|
||||
|
||||
/**
|
||||
* @group rest
|
||||
*/
|
||||
class RestResourceConfigXmlBasicAuthTest extends RestResourceConfigResourceTestBase {
|
||||
|
||||
use BasicAuthResourceTestTrait;
|
||||
use XmlEntityNormalizationQuirksTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['basic_auth'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $format = 'xml';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $mimeType = 'text/xml; charset=UTF-8';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $auth = 'basic_auth';
|
||||
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\rest\Functional\Rest;
|
||||
|
||||
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
|
||||
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
|
||||
|
||||
/**
|
||||
* @group rest
|
||||
*/
|
||||
class RestResourceConfigXmlCookieTest extends RestResourceConfigResourceTestBase {
|
||||
|
||||
use CookieResourceTestTrait;
|
||||
use XmlEntityNormalizationQuirksTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $format = 'xml';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $mimeType = 'text/xml; charset=UTF-8';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $auth = 'cookie';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\rest\Functional\Views;
|
||||
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\Tests\views\Functional\ViewTestBase;
|
||||
use Drupal\views\Views;
|
||||
|
||||
/**
|
||||
* Tests the display of an excluded field that is used as a token.
|
||||
*
|
||||
* @group rest
|
||||
* @see \Drupal\rest\Plugin\views\display\RestExport
|
||||
* @see \Drupal\rest\Plugin\views\row\DataFieldRow
|
||||
*/
|
||||
class ExcludedFieldTokenTest extends ViewTestBase {
|
||||
|
||||
/**
|
||||
* @var \Drupal\views\ViewExecutable
|
||||
*/
|
||||
protected $view;
|
||||
|
||||
/**
|
||||
* The views that are used by this test.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $testViews = ['test_excluded_field_token_display'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* The modules that need to be installed for this test.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static $modules = [
|
||||
'entity_test',
|
||||
'rest_test_views',
|
||||
'node',
|
||||
'field',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp($import_test_views = TRUE, $modules = ['rest_test_views']): void {
|
||||
parent::setUp($import_test_views, $modules);
|
||||
|
||||
// Create some test content.
|
||||
for ($i = 1; $i <= 10; $i++) {
|
||||
Node::create([
|
||||
'type' => 'article',
|
||||
'title' => 'Article test ' . $i,
|
||||
])->save();
|
||||
}
|
||||
|
||||
$this->enableViewsTestModule();
|
||||
|
||||
$this->view = Views::getView('test_excluded_field_token_display');
|
||||
$this->view->setDisplay('rest_export_1');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the display of an excluded title field when used as a token.
|
||||
*/
|
||||
public function testExcludedTitleTokenDisplay(): void {
|
||||
$actual_json = $this->drupalGet($this->view->getPath(), ['query' => ['_format' => 'json']]);
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
|
||||
$expected = [
|
||||
['nothing' => 'Article test 10'],
|
||||
['nothing' => 'Article test 9'],
|
||||
['nothing' => 'Article test 8'],
|
||||
['nothing' => 'Article test 7'],
|
||||
['nothing' => 'Article test 6'],
|
||||
['nothing' => 'Article test 5'],
|
||||
['nothing' => 'Article test 4'],
|
||||
['nothing' => 'Article test 3'],
|
||||
['nothing' => 'Article test 2'],
|
||||
['nothing' => 'Article test 1'],
|
||||
];
|
||||
$this->assertSame(json_encode($expected), $actual_json);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\rest\Functional\Views;
|
||||
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\Tests\views\Functional\ViewTestBase;
|
||||
use Drupal\views\Views;
|
||||
|
||||
/**
|
||||
* Tests the display of counter field.
|
||||
*
|
||||
* @group rest
|
||||
* @see \Drupal\rest\Plugin\views\display\RestExport
|
||||
* @see \Drupal\rest\Plugin\views\row\DataFieldRow
|
||||
*/
|
||||
class FieldCounterTest extends ViewTestBase {
|
||||
|
||||
/**
|
||||
* @var \Drupal\views\ViewExecutable
|
||||
*/
|
||||
protected $view;
|
||||
|
||||
/**
|
||||
* The views that are used by this test.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $testViews = ['test_field_counter_display'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* The modules that need to be installed for this test.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static $modules = [
|
||||
'entity_test',
|
||||
'rest_test_views',
|
||||
'node',
|
||||
'field',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp($import_test_views = TRUE, $modules = ['rest_test_views']): void {
|
||||
parent::setUp($import_test_views, $modules);
|
||||
|
||||
// Create some test content.
|
||||
for ($i = 1; $i <= 10; $i++) {
|
||||
Node::create([
|
||||
'type' => 'article',
|
||||
'title' => 'Article test ' . $i,
|
||||
])->save();
|
||||
}
|
||||
|
||||
$this->enableViewsTestModule();
|
||||
|
||||
$this->view = Views::getView('test_field_counter_display');
|
||||
$this->view->setDisplay('rest_export_1');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the display of an excluded title field when used as a token.
|
||||
*/
|
||||
public function testExcludedTitleTokenDisplay(): void {
|
||||
$actual_json = $this->drupalGet($this->view->getPath(), ['query' => ['_format' => 'json']]);
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
|
||||
$expected = [
|
||||
['counter' => '1'],
|
||||
['counter' => '2'],
|
||||
['counter' => '3'],
|
||||
['counter' => '4'],
|
||||
['counter' => '5'],
|
||||
['counter' => '6'],
|
||||
['counter' => '7'],
|
||||
['counter' => '8'],
|
||||
['counter' => '9'],
|
||||
['counter' => '10'],
|
||||
];
|
||||
$this->assertSame(json_encode($expected), $actual_json);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\rest\Functional\Views;
|
||||
|
||||
use Drupal\Tests\views\Functional\ViewTestBase;
|
||||
use Drupal\views\Entity\View;
|
||||
|
||||
/**
|
||||
* Tests authentication for REST display.
|
||||
*
|
||||
* @group rest
|
||||
*/
|
||||
class RestExportAuthTest extends ViewTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['rest', 'views_ui', 'basic_auth'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp($import_test_views = TRUE, $modules = []): void {
|
||||
parent::setUp($import_test_views, $modules);
|
||||
|
||||
$this->drupalLogin($this->drupalCreateUser(['administer views']));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that correct authentication providers are available for choosing.
|
||||
*
|
||||
* @link https://www.drupal.org/node/2825204
|
||||
*/
|
||||
public function testAuthProvidersOptions(): void {
|
||||
$view_id = 'test_view_rest_export';
|
||||
$view_label = 'Test view (REST export)';
|
||||
$view_display = 'rest_export_1';
|
||||
$view_rest_path = 'test-view/rest-export';
|
||||
|
||||
// Create new view.
|
||||
$this->drupalGet('admin/structure/views/add');
|
||||
$this->submitForm([
|
||||
'id' => $view_id,
|
||||
'label' => $view_label,
|
||||
'show[wizard_key]' => 'users',
|
||||
'rest_export[path]' => $view_rest_path,
|
||||
'rest_export[create]' => TRUE,
|
||||
], 'Save and edit');
|
||||
|
||||
$this->drupalGet("admin/structure/views/nojs/display/$view_id/$view_display/auth");
|
||||
// The "basic_auth" will always be available since module,
|
||||
// providing it, has the same name.
|
||||
$this->assertSession()->fieldExists('edit-auth-basic-auth');
|
||||
// The "cookie" authentication provider defined by "user" module.
|
||||
$this->assertSession()->fieldExists('edit-auth-cookie');
|
||||
// Wrong behavior in "getAuthOptions()" method makes this option available
|
||||
// instead of "cookie".
|
||||
// @see \Drupal\rest\Plugin\views\display\RestExport::getAuthOptions()
|
||||
$this->assertSession()->fieldNotExists('edit-auth-user');
|
||||
|
||||
$this->submitForm(['auth[basic_auth]' => 1, 'auth[cookie]' => 1], 'Apply');
|
||||
$this->submitForm([], 'Save');
|
||||
|
||||
$view = View::load($view_id);
|
||||
$this->assertEquals(['basic_auth', 'cookie'], $view->getDisplay('rest_export_1')['display_options']['auth']);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,599 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\rest\Functional\Views;
|
||||
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
|
||||
use Drupal\entity_test\Entity\EntityTest;
|
||||
use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait;
|
||||
use Drupal\Tests\views\Functional\ViewTestBase;
|
||||
use Drupal\views\Entity\View;
|
||||
use Drupal\views\Plugin\views\display\DisplayPluginBase;
|
||||
use Drupal\views\Views;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Session\Session;
|
||||
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
|
||||
|
||||
/**
|
||||
* Tests the serializer style plugin.
|
||||
*
|
||||
* @group rest
|
||||
* @see \Drupal\rest\Plugin\views\display\RestExport
|
||||
* @see \Drupal\rest\Plugin\views\style\Serializer
|
||||
* @see \Drupal\rest\Plugin\views\row\DataEntityRow
|
||||
* @see \Drupal\rest\Plugin\views\row\DataFieldRow
|
||||
*/
|
||||
class StyleSerializerEntityTest extends ViewTestBase {
|
||||
|
||||
use AssertPageCacheContextsAndTagsTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'views_ui',
|
||||
'entity_test',
|
||||
'rest_test_views',
|
||||
'text',
|
||||
'field',
|
||||
'language',
|
||||
'basic_auth',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* Views used by this test.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $testViews = ['test_serializer_display_field', 'test_serializer_display_entity', 'test_serializer_display_entity_translated', 'test_serializer_node_display_field', 'test_serializer_node_exposed_filter', 'test_serializer_shared_path'];
|
||||
|
||||
/**
|
||||
* A user with permissions to look at test entity and configure views.
|
||||
*
|
||||
* @var \Drupal\user\Entity\User|false
|
||||
*
|
||||
* @see \Drupal\Tests\user\Traits\UserCreationTrait::createUser
|
||||
*/
|
||||
protected $adminUser;
|
||||
|
||||
/**
|
||||
* The renderer.
|
||||
*
|
||||
* @var \Drupal\Core\Render\RendererInterface
|
||||
*/
|
||||
protected $renderer;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp($import_test_views = TRUE, $modules = ['rest_test_views']): void {
|
||||
parent::setUp($import_test_views, $modules);
|
||||
|
||||
$this->adminUser = $this->drupalCreateUser([
|
||||
'administer views',
|
||||
'administer entity_test content',
|
||||
'access user profiles',
|
||||
'view test entity',
|
||||
]);
|
||||
|
||||
// Save some entity_test entities.
|
||||
for ($i = 1; $i <= 10; $i++) {
|
||||
EntityTest::create(['name' => 'test_' . $i, 'user_id' => $this->adminUser->id()])->save();
|
||||
}
|
||||
|
||||
$this->enableViewsTestModule();
|
||||
$this->renderer = \Drupal::service('renderer');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the behavior of the Serializer callback paths and row plugins.
|
||||
*/
|
||||
public function testSerializerResponses(): void {
|
||||
// Test the serialize callback.
|
||||
$view = Views::getView('test_serializer_display_field');
|
||||
$view->initDisplay();
|
||||
$this->executeView($view);
|
||||
|
||||
$actual_json = $this->drupalGet('test/serialize/field', ['query' => ['_format' => 'json']]);
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
$this->assertCacheTags($view->getCacheTags());
|
||||
$this->assertCacheContexts(['languages:language_interface', 'theme', 'request_format']);
|
||||
// @todo Due to https://www.drupal.org/node/2352009 we can't yet test the
|
||||
// propagation of cache max-age.
|
||||
|
||||
// Test the http Content-type.
|
||||
$headers = $this->getSession()->getResponseHeaders();
|
||||
$this->assertSame(['application/json'], $headers['Content-Type']);
|
||||
|
||||
$expected = [];
|
||||
foreach ($view->result as $row) {
|
||||
$expected_row = [];
|
||||
foreach ($view->field as $id => $field) {
|
||||
$expected_row[$id] = $field->render($row);
|
||||
}
|
||||
$expected[] = $expected_row;
|
||||
}
|
||||
|
||||
$this->assertSame(json_encode($expected), $actual_json, 'The expected JSON output was found.');
|
||||
|
||||
// Test that the rendered output and the preview output are the same.
|
||||
$view->destroy();
|
||||
$view->setDisplay('rest_export_1');
|
||||
// Mock the request content type by setting it on the display handler.
|
||||
$view->display_handler->setContentType('json');
|
||||
$output = $view->preview();
|
||||
$this->assertSame((string) $this->renderer->renderRoot($output), $actual_json, 'The expected JSON preview output was found.');
|
||||
|
||||
// Test a 403 callback.
|
||||
$this->drupalGet('test/serialize/denied', ['query' => ['_format' => 'json']]);
|
||||
$this->assertSession()->statusCodeEquals(403);
|
||||
|
||||
// Test the entity rows.
|
||||
$view = Views::getView('test_serializer_display_entity');
|
||||
$view->initDisplay();
|
||||
$this->executeView($view);
|
||||
|
||||
// Get the serializer service.
|
||||
$serializer = $this->container->get('serializer');
|
||||
|
||||
$entities = [];
|
||||
foreach ($view->result as $row) {
|
||||
$entities[] = $row->_entity;
|
||||
}
|
||||
|
||||
$expected = $serializer->serialize($entities, 'json');
|
||||
|
||||
$actual_json = $this->drupalGet('test/serialize/entity', ['query' => ['_format' => 'json']]);
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
$this->assertSame($expected, $actual_json, 'The expected JSON output was found.');
|
||||
$expected_cache_tags = $view->getCacheTags();
|
||||
$expected_cache_tags[] = 'entity_test_list';
|
||||
/** @var \Drupal\Core\Entity\EntityInterface $entity */
|
||||
foreach ($entities as $entity) {
|
||||
$expected_cache_tags = Cache::mergeTags($expected_cache_tags, $entity->getCacheTags());
|
||||
}
|
||||
$this->assertCacheTags($expected_cache_tags);
|
||||
$this->assertCacheContexts(['languages:language_interface', 'theme', 'entity_test_view_grants', 'request_format']);
|
||||
|
||||
// Change the format to xml.
|
||||
$view->setDisplay('rest_export_1');
|
||||
$view->getDisplay()->setOption('style', [
|
||||
'type' => 'serializer',
|
||||
'options' => [
|
||||
'uses_fields' => FALSE,
|
||||
'formats' => [
|
||||
'xml' => 'xml',
|
||||
],
|
||||
],
|
||||
]);
|
||||
$view->save();
|
||||
$expected = $serializer->serialize($entities, 'xml');
|
||||
$actual_xml = $this->drupalGet('test/serialize/entity', ['query' => ['_format' => 'xml']]);
|
||||
$this->assertSame(trim($expected), $actual_xml);
|
||||
$this->assertCacheContexts(['languages:language_interface', 'theme', 'entity_test_view_grants', 'request_format']);
|
||||
|
||||
// Allow multiple formats.
|
||||
$view->setDisplay('rest_export_1');
|
||||
$view->getDisplay()->setOption('style', [
|
||||
'type' => 'serializer',
|
||||
'options' => [
|
||||
'uses_fields' => FALSE,
|
||||
'formats' => [
|
||||
'xml' => 'xml',
|
||||
'json' => 'json',
|
||||
],
|
||||
],
|
||||
]);
|
||||
$view->save();
|
||||
$expected = $serializer->serialize($entities, 'json');
|
||||
$actual_json = $this->drupalGet('test/serialize/entity', ['query' => ['_format' => 'json']]);
|
||||
$this->assertSame($expected, $actual_json, 'The expected JSON output was found.');
|
||||
$expected = $serializer->serialize($entities, 'xml');
|
||||
$actual_xml = $this->drupalGet('test/serialize/entity', ['query' => ['_format' => 'xml']]);
|
||||
$this->assertSame(trim($expected), $actual_xml);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a request on the request stack with a specified format.
|
||||
*
|
||||
* @param string $format
|
||||
* The new request format.
|
||||
*/
|
||||
protected function addRequestWithFormat($format): void {
|
||||
$request = \Drupal::request();
|
||||
$request = clone $request;
|
||||
$request->setRequestFormat($format);
|
||||
|
||||
\Drupal::requestStack()->push($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests REST export with views render caching enabled.
|
||||
*/
|
||||
public function testRestRenderCaching(): void {
|
||||
$this->drupalLogin($this->adminUser);
|
||||
|
||||
/** @var \Drupal\Core\Cache\VariationCacheFactoryInterface $vc_factory */
|
||||
$variation_cache_factory = \Drupal::service('variation_cache_factory');
|
||||
$variation_cache = $variation_cache_factory->get('render');
|
||||
|
||||
/** @var \Drupal\Core\Render\RenderCacheInterface $render_cache */
|
||||
$render_cache = \Drupal::service('render_cache');
|
||||
|
||||
// Enable render caching for the views.
|
||||
/** @var \Drupal\views\ViewEntityInterface $storage */
|
||||
$storage = View::load('test_serializer_display_entity');
|
||||
$options = &$storage->getDisplay('default');
|
||||
$options['display_options']['cache'] = [
|
||||
'type' => 'tag',
|
||||
];
|
||||
$storage->save();
|
||||
|
||||
$original = DisplayPluginBase::buildBasicRenderable('test_serializer_display_entity', 'rest_export_1');
|
||||
|
||||
// Ensure that there is no corresponding render cache item yet.
|
||||
$original['#cache'] += ['contexts' => []];
|
||||
$original['#cache']['contexts'] = Cache::mergeContexts($original['#cache']['contexts'], $this->container->getParameter('renderer.config')['required_cache_contexts']);
|
||||
|
||||
$cache_tags = [
|
||||
'config:views.view.test_serializer_display_entity',
|
||||
'entity_test:1',
|
||||
'entity_test:10',
|
||||
'entity_test:2',
|
||||
'entity_test:3',
|
||||
'entity_test:4',
|
||||
'entity_test:5',
|
||||
'entity_test:6',
|
||||
'entity_test:7',
|
||||
'entity_test:8',
|
||||
'entity_test:9',
|
||||
'entity_test_list',
|
||||
];
|
||||
$cache_contexts = [
|
||||
'entity_test_view_grants',
|
||||
'languages:language_interface',
|
||||
'theme',
|
||||
'request_format',
|
||||
];
|
||||
|
||||
$this->assertFalse($render_cache->get($original));
|
||||
|
||||
// Request the page, once in XML and once in JSON to ensure that the caching
|
||||
// varies by it.
|
||||
$result1 = Json::decode($this->drupalGet('test/serialize/entity', ['query' => ['_format' => 'json']]));
|
||||
$this->addRequestWithFormat('json');
|
||||
$this->assertSession()->responseHeaderEquals('content-type', 'application/json');
|
||||
$this->assertCacheContexts($cache_contexts);
|
||||
$this->assertCacheTags($cache_tags);
|
||||
|
||||
// Because we warm caches in different requests, we do not properly populate
|
||||
// the internal properties of our variation cache. Reset it.
|
||||
$variation_cache->reset();
|
||||
$this->assertNotEmpty($render_cache->get($original));
|
||||
|
||||
$result_xml = $this->drupalGet('test/serialize/entity', ['query' => ['_format' => 'xml']]);
|
||||
$this->addRequestWithFormat('xml');
|
||||
$this->assertSession()->responseHeaderEquals('content-type', 'text/xml; charset=UTF-8');
|
||||
$this->assertCacheContexts($cache_contexts);
|
||||
$this->assertCacheTags($cache_tags);
|
||||
$this->assertNotEmpty($render_cache->get($original));
|
||||
|
||||
// Ensure that the XML output is different from the JSON one.
|
||||
$this->assertNotEquals($result1, $result_xml);
|
||||
|
||||
// Ensure that the cached page works.
|
||||
$result2 = Json::decode($this->drupalGet('test/serialize/entity', ['query' => ['_format' => 'json']]));
|
||||
$this->addRequestWithFormat('json');
|
||||
$this->assertSession()->responseHeaderEquals('content-type', 'application/json');
|
||||
$this->assertEquals($result1, $result2);
|
||||
$this->assertCacheContexts($cache_contexts);
|
||||
$this->assertCacheTags($cache_tags);
|
||||
$this->assertNotEmpty($render_cache->get($original));
|
||||
|
||||
// Create a new entity and ensure that the cache tags are taken over.
|
||||
EntityTest::create(['name' => 'test_11', 'user_id' => $this->adminUser->id()])->save();
|
||||
$result3 = Json::decode($this->drupalGet('test/serialize/entity', ['query' => ['_format' => 'json']]));
|
||||
$this->addRequestWithFormat('json');
|
||||
$this->assertSession()->responseHeaderEquals('content-type', 'application/json');
|
||||
$this->assertNotEquals($result2, $result3);
|
||||
|
||||
// Add the new entity cache tag and remove the first one, because we just
|
||||
// show 10 items in total.
|
||||
$cache_tags[] = 'entity_test:11';
|
||||
unset($cache_tags[array_search('entity_test:1', $cache_tags)]);
|
||||
|
||||
$this->assertCacheContexts($cache_contexts);
|
||||
$this->assertCacheTags($cache_tags);
|
||||
$this->assertNotEmpty($render_cache->get($original));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the response format configuration.
|
||||
*/
|
||||
public function testResponseFormatConfiguration(): void {
|
||||
$this->drupalLogin($this->adminUser);
|
||||
|
||||
$style_options = 'admin/structure/views/nojs/display/test_serializer_display_field/rest_export_1/style_options';
|
||||
|
||||
// Ensure a request with no format returns 406 Not Acceptable.
|
||||
$this->drupalGet('test/serialize/field');
|
||||
$this->assertSession()->responseHeaderEquals('content-type', 'text/html; charset=UTF-8');
|
||||
$this->assertSession()->statusCodeEquals(406);
|
||||
|
||||
// Select only 'xml' as an accepted format.
|
||||
$this->drupalGet($style_options);
|
||||
$this->submitForm(['style_options[formats][xml]' => 'xml'], 'Apply');
|
||||
$this->submitForm([], 'Save');
|
||||
|
||||
// Ensure a request for JSON returns 406 Not Acceptable.
|
||||
$this->drupalGet('test/serialize/field', ['query' => ['_format' => 'json']]);
|
||||
$this->assertSession()->responseHeaderEquals('content-type', 'application/json');
|
||||
$this->assertSession()->statusCodeEquals(406);
|
||||
// Ensure a request for XML returns 200 OK.
|
||||
$this->drupalGet('test/serialize/field', ['query' => ['_format' => 'xml']]);
|
||||
$this->assertSession()->responseHeaderEquals('content-type', 'text/xml; charset=UTF-8');
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
|
||||
// Add 'json' as an accepted format, so we have multiple.
|
||||
$this->drupalGet($style_options);
|
||||
$this->submitForm(['style_options[formats][json]' => 'json'], 'Apply');
|
||||
$this->submitForm([], 'Save');
|
||||
|
||||
// Should return a 406. Emulates a sample Firefox header.
|
||||
$this->drupalGet('test/serialize/field', [], ['Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8']);
|
||||
$this->assertSession()->responseHeaderEquals('content-type', 'text/html; charset=UTF-8');
|
||||
$this->assertSession()->statusCodeEquals(406);
|
||||
|
||||
// Ensure a request for HTML returns 406 Not Acceptable.
|
||||
$this->drupalGet('test/serialize/field', ['query' => ['_format' => 'html']]);
|
||||
$this->assertSession()->responseHeaderEquals('content-type', 'text/html; charset=UTF-8');
|
||||
$this->assertSession()->statusCodeEquals(406);
|
||||
|
||||
// Ensure a request for JSON returns 200 OK.
|
||||
$this->drupalGet('test/serialize/field', ['query' => ['_format' => 'json']]);
|
||||
$this->assertSession()->responseHeaderEquals('content-type', 'application/json');
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
|
||||
// Ensure a request XML returns 200 OK.
|
||||
$this->drupalGet('test/serialize/field', ['query' => ['_format' => 'xml']]);
|
||||
$this->assertSession()->responseHeaderEquals('content-type', 'text/xml; charset=UTF-8');
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
|
||||
// Now configure no format, so both serialization formats should be allowed.
|
||||
$this->drupalGet($style_options);
|
||||
$this->submitForm([
|
||||
'style_options[formats][json]' => '0',
|
||||
'style_options[formats][xml]' => '0',
|
||||
], 'Apply');
|
||||
|
||||
// Ensure a request for JSON returns 200 OK.
|
||||
$this->drupalGet('test/serialize/field', ['query' => ['_format' => 'json']]);
|
||||
$this->assertSession()->responseHeaderEquals('content-type', 'application/json');
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
|
||||
// Ensure a request for XML returns 200 OK.
|
||||
$this->drupalGet('test/serialize/field', ['query' => ['_format' => 'xml']]);
|
||||
$this->assertSession()->responseHeaderEquals('content-type', 'text/xml; charset=UTF-8');
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
|
||||
// Should return a 406 for HTML still.
|
||||
$this->drupalGet('test/serialize/field', ['query' => ['_format' => 'html']]);
|
||||
$this->assertSession()->responseHeaderEquals('content-type', 'text/html; charset=UTF-8');
|
||||
$this->assertSession()->statusCodeEquals(406);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the field ID alias functionality of the DataFieldRow plugin.
|
||||
*/
|
||||
public function testUIFieldAlias(): void {
|
||||
$this->drupalLogin($this->adminUser);
|
||||
|
||||
// Test the UI settings for adding field ID aliases.
|
||||
$this->drupalGet('admin/structure/views/view/test_serializer_display_field/edit/rest_export_1');
|
||||
$row_options = 'admin/structure/views/nojs/display/test_serializer_display_field/rest_export_1/row_options';
|
||||
$this->assertSession()->linkByHrefExists($row_options);
|
||||
|
||||
// Test an empty string for an alias, this should not be used. This also
|
||||
// tests that the form can be submitted with no aliases.
|
||||
$this->drupalGet($row_options);
|
||||
$this->submitForm(['row_options[field_options][name][alias]' => ''], 'Apply');
|
||||
$this->submitForm([], 'Save');
|
||||
|
||||
$view = Views::getView('test_serializer_display_field');
|
||||
$view->setDisplay('rest_export_1');
|
||||
$this->executeView($view);
|
||||
|
||||
$expected = [];
|
||||
foreach ($view->result as $row) {
|
||||
$expected_row = [];
|
||||
foreach ($view->field as $id => $field) {
|
||||
$expected_row[$id] = $field->render($row);
|
||||
}
|
||||
$expected[] = $expected_row;
|
||||
}
|
||||
|
||||
$this->assertEquals($expected, Json::decode($this->drupalGet('test/serialize/field', ['query' => ['_format' => 'json']])));
|
||||
|
||||
// Test a random aliases for fields, they should be replaced.
|
||||
$alias_map = [
|
||||
'name' => $this->randomMachineName(),
|
||||
// Use # to produce an invalid character for the validation.
|
||||
'nothing' => '#' . $this->randomMachineName(),
|
||||
'created' => 'created',
|
||||
];
|
||||
|
||||
$edit = ['row_options[field_options][name][alias]' => $alias_map['name'], 'row_options[field_options][nothing][alias]' => $alias_map['nothing']];
|
||||
$this->drupalGet($row_options);
|
||||
$this->submitForm($edit, 'Apply');
|
||||
$this->assertSession()->pageTextContains('The machine-readable name must contain only letters, numbers, dashes and underscores.');
|
||||
|
||||
// Change the map alias value to a valid one.
|
||||
$alias_map['nothing'] = $this->randomMachineName();
|
||||
|
||||
$edit = ['row_options[field_options][name][alias]' => $alias_map['name'], 'row_options[field_options][nothing][alias]' => $alias_map['nothing']];
|
||||
$this->drupalGet($row_options);
|
||||
$this->submitForm($edit, 'Apply');
|
||||
|
||||
$this->submitForm([], 'Save');
|
||||
|
||||
$view = Views::getView('test_serializer_display_field');
|
||||
$view->setDisplay('rest_export_1');
|
||||
$this->executeView($view);
|
||||
|
||||
$expected = [];
|
||||
foreach ($view->result as $row) {
|
||||
$expected_row = [];
|
||||
foreach ($view->field as $id => $field) {
|
||||
$expected_row[$alias_map[$id]] = $field->render($row);
|
||||
}
|
||||
$expected[] = $expected_row;
|
||||
}
|
||||
|
||||
$this->assertEquals($expected, Json::decode($this->drupalGet('test/serialize/field', ['query' => ['_format' => 'json']])));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the raw output options for row field rendering.
|
||||
*/
|
||||
public function testFieldRawOutput(): void {
|
||||
$this->drupalLogin($this->adminUser);
|
||||
|
||||
// Test the UI settings for adding field ID aliases.
|
||||
$this->drupalGet('admin/structure/views/view/test_serializer_display_field/edit/rest_export_1');
|
||||
$row_options = 'admin/structure/views/nojs/display/test_serializer_display_field/rest_export_1/row_options';
|
||||
$this->assertSession()->linkByHrefExists($row_options);
|
||||
|
||||
// Test an empty string for an alias, this should not be used. This also
|
||||
// tests that the form can be submitted with no aliases.
|
||||
$values = [
|
||||
'row_options[field_options][created][raw_output]' => '1',
|
||||
'row_options[field_options][name][raw_output]' => '1',
|
||||
];
|
||||
$this->drupalGet($row_options);
|
||||
$this->submitForm($values, 'Apply');
|
||||
$this->submitForm([], 'Save');
|
||||
|
||||
$view = Views::getView('test_serializer_display_field');
|
||||
$view->setDisplay('rest_export_1');
|
||||
$this->executeView($view);
|
||||
|
||||
$storage = $this->container->get('entity_type.manager')->getStorage('entity_test');
|
||||
|
||||
// Update the name for each to include a script tag.
|
||||
foreach ($storage->loadMultiple() as $entity_test) {
|
||||
$name = $entity_test->name->value;
|
||||
$entity_test->set('name', "<script>$name</script>");
|
||||
$entity_test->save();
|
||||
}
|
||||
|
||||
// Just test the raw 'created' value against each row.
|
||||
foreach (Json::decode($this->drupalGet('test/serialize/field', ['query' => ['_format' => 'json']])) as $index => $values) {
|
||||
$this->assertSame($view->result[$index]->views_test_data_created, $values['created'], 'Expected raw created value found.');
|
||||
$this->assertSame($view->result[$index]->views_test_data_name, $values['name'], 'Expected raw name value found.');
|
||||
}
|
||||
|
||||
// Test result with an excluded field.
|
||||
$view->setDisplay('rest_export_1');
|
||||
$view->displayHandlers->get('rest_export_1')->overrideOption('fields', [
|
||||
'name' => [
|
||||
'id' => 'name',
|
||||
'table' => 'views_test_data',
|
||||
'field' => 'name',
|
||||
'relationship' => 'none',
|
||||
],
|
||||
'created' => [
|
||||
'id' => 'created',
|
||||
'exclude' => TRUE,
|
||||
'table' => 'views_test_data',
|
||||
'field' => 'created',
|
||||
'relationship' => 'none',
|
||||
],
|
||||
]);
|
||||
$view->save();
|
||||
$this->executeView($view);
|
||||
foreach (Json::decode($this->drupalGet('test/serialize/field', ['query' => ['_format' => 'json']])) as $index => $values) {
|
||||
$this->assertTrue(!isset($values['created']), 'Excluded value not found.');
|
||||
}
|
||||
// Test that the excluded field is not shown in the row options.
|
||||
$this->drupalGet('admin/structure/views/nojs/display/test_serializer_display_field/rest_export_1/row_options');
|
||||
$this->assertSession()->pageTextNotContains('created');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the live preview output for json output.
|
||||
*/
|
||||
public function testLivePreview(): void {
|
||||
// We set up a request so it looks like a request in the live preview.
|
||||
$request = new Request();
|
||||
$request->query->add([MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']);
|
||||
$request->setSession(new Session(new MockArraySessionStorage()));
|
||||
/** @var \Symfony\Component\HttpFoundation\RequestStack $request_stack */
|
||||
$request_stack = \Drupal::service('request_stack');
|
||||
$request_stack->push($request);
|
||||
|
||||
$view = Views::getView('test_serializer_display_entity');
|
||||
$view->setDisplay('rest_export_1');
|
||||
$this->executeView($view);
|
||||
|
||||
// Get the serializer service.
|
||||
$serializer = $this->container->get('serializer');
|
||||
|
||||
$entities = [];
|
||||
foreach ($view->result as $row) {
|
||||
$entities[] = $row->_entity;
|
||||
}
|
||||
|
||||
$expected = $serializer->serialize($entities, 'json');
|
||||
|
||||
$view->live_preview = TRUE;
|
||||
|
||||
$build = $view->preview();
|
||||
$rendered_json = $build['#plain_text'];
|
||||
$this->assertArrayNotHasKey('#markup', $build);
|
||||
$this->assertSame($expected, $rendered_json, 'Ensure the previewed json is escaped.');
|
||||
$view->destroy();
|
||||
|
||||
$expected = $serializer->serialize($entities, 'xml');
|
||||
|
||||
// Change the request format to xml.
|
||||
$view->setDisplay('rest_export_1');
|
||||
$view->getDisplay()->setOption('style', [
|
||||
'type' => 'serializer',
|
||||
'options' => [
|
||||
'uses_fields' => FALSE,
|
||||
'formats' => [
|
||||
'xml' => 'xml',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->executeView($view);
|
||||
$build = $view->preview();
|
||||
$rendered_xml = $build['#plain_text'];
|
||||
$this->assertEquals($expected, $rendered_xml, 'Ensure we preview xml when we change the request format.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the views interface for REST export displays.
|
||||
*/
|
||||
public function testSerializerViewsUI(): void {
|
||||
$this->drupalLogin($this->adminUser);
|
||||
// Click the "Update preview button".
|
||||
$this->drupalGet('admin/structure/views/view/test_serializer_display_field/edit/rest_export_1');
|
||||
$this->submitForm($edit = [], 'Update preview');
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
// Check if we receive the expected result.
|
||||
$result = $this->assertSession()->elementExists('xpath', '//div[@id="views-live-preview"]/pre');
|
||||
$json_preview = $result->getText();
|
||||
$this->assertSame($json_preview, $this->drupalGet('test/serialize/field', ['query' => ['_format' => 'json']]), 'The expected JSON preview output was found.');
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,324 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\rest\Functional\Views;
|
||||
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\Core\Field\FieldStorageDefinitionInterface;
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Drupal\field\Entity\FieldStorageConfig;
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait;
|
||||
use Drupal\Tests\views\Functional\ViewTestBase;
|
||||
use Drupal\views\Views;
|
||||
|
||||
/**
|
||||
* Tests the serializer style plugin.
|
||||
*
|
||||
* @group rest
|
||||
* @see \Drupal\rest\Plugin\views\display\RestExport
|
||||
* @see \Drupal\rest\Plugin\views\style\Serializer
|
||||
* @see \Drupal\rest\Plugin\views\row\DataEntityRow
|
||||
* @see \Drupal\rest\Plugin\views\row\DataFieldRow
|
||||
*/
|
||||
class StyleSerializerTest extends ViewTestBase {
|
||||
|
||||
use AssertPageCacheContextsAndTagsTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'views_ui',
|
||||
'entity_test',
|
||||
'rest_test_views',
|
||||
'node',
|
||||
'text',
|
||||
'field',
|
||||
'language',
|
||||
'basic_auth',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* Views used by this test.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $testViews = ['test_serializer_display_field', 'test_serializer_display_entity', 'test_serializer_display_entity_translated', 'test_serializer_node_display_field', 'test_serializer_node_exposed_filter', 'test_serializer_shared_path'];
|
||||
|
||||
/**
|
||||
* A user with permissions to look at test entity and configure views.
|
||||
*
|
||||
* @var \Drupal\user\Entity\User|false
|
||||
*/
|
||||
protected $adminUser;
|
||||
|
||||
/**
|
||||
* The renderer.
|
||||
*
|
||||
* @var \Drupal\Core\Render\RendererInterface
|
||||
*/
|
||||
protected $renderer;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp($import_test_views = TRUE, $modules = ['rest_test_views']): void {
|
||||
parent::setUp($import_test_views, $modules);
|
||||
|
||||
$this->adminUser = $this->drupalCreateUser([
|
||||
'administer views',
|
||||
'administer entity_test content',
|
||||
'access user profiles',
|
||||
'view test entity',
|
||||
]);
|
||||
|
||||
$this->enableViewsTestModule();
|
||||
$this->renderer = \Drupal::service('renderer');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that the auth options restricts access to a REST views display.
|
||||
*/
|
||||
public function testRestViewsAuthentication(): void {
|
||||
// Assume the view is hidden behind a permission.
|
||||
$this->drupalGet('test/serialize/auth_with_perm', ['query' => ['_format' => 'json']]);
|
||||
$this->assertSession()->statusCodeEquals(401);
|
||||
|
||||
// Not even logging in would make it possible to see the view, because then
|
||||
// we are denied based on authentication method (cookie).
|
||||
$this->drupalLogin($this->adminUser);
|
||||
$this->drupalGet('test/serialize/auth_with_perm', ['query' => ['_format' => 'json']]);
|
||||
$this->assertSession()->statusCodeEquals(403);
|
||||
$this->drupalLogout();
|
||||
|
||||
// But if we use the basic auth authentication strategy, we should be able
|
||||
// to see the page.
|
||||
$url = $this->buildUrl('test/serialize/auth_with_perm');
|
||||
$response = \Drupal::httpClient()->get($url, [
|
||||
'auth' => [$this->adminUser->getAccountName(), $this->adminUser->pass_raw],
|
||||
'query' => [
|
||||
'_format' => 'json',
|
||||
],
|
||||
]);
|
||||
|
||||
// Ensure that any changes to variables in the other thread are picked up.
|
||||
$this->refreshVariables();
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies REST export views work on the same path as a page display.
|
||||
*/
|
||||
public function testSharedPagePath(): void {
|
||||
// Test with no format as well as html explicitly.
|
||||
$this->drupalGet('test/serialize/shared');
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
$this->assertSession()->responseHeaderEquals('content-type', 'text/html; charset=UTF-8');
|
||||
|
||||
$this->drupalGet('test/serialize/shared', ['query' => ['_format' => 'html']]);
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
$this->assertSession()->responseHeaderEquals('content-type', 'text/html; charset=UTF-8');
|
||||
|
||||
$this->drupalGet('test/serialize/shared', ['query' => ['_format' => 'json']]);
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
$this->assertSession()->responseHeaderEquals('content-type', 'application/json');
|
||||
|
||||
$this->drupalGet('test/serialize/shared', ['query' => ['_format' => 'xml']]);
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
$this->assertSession()->responseHeaderEquals('content-type', 'text/xml; charset=UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies site maintenance mode functionality.
|
||||
*/
|
||||
public function testSiteMaintenance(): void {
|
||||
$view = Views::getView('test_serializer_display_field');
|
||||
$view->initDisplay();
|
||||
$this->executeView($view);
|
||||
|
||||
// Set the site to maintenance mode.
|
||||
$this->container->get('state')->set('system.maintenance_mode', TRUE);
|
||||
|
||||
$this->drupalGet('test/serialize/entity', ['query' => ['_format' => 'json']]);
|
||||
// Verify that the endpoint is unavailable for anonymous users.
|
||||
$this->assertSession()->statusCodeEquals(503);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a request on the request stack with a specified format.
|
||||
*
|
||||
* @param string $format
|
||||
* The new request format.
|
||||
*/
|
||||
protected function addRequestWithFormat($format): void {
|
||||
$request = \Drupal::request();
|
||||
$request = clone $request;
|
||||
$request->setRequestFormat($format);
|
||||
|
||||
\Drupal::requestStack()->push($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the "Grouped rows" functionality.
|
||||
*/
|
||||
public function testGroupRows(): void {
|
||||
$this->drupalCreateContentType(['type' => 'page']);
|
||||
// Create a text field with cardinality set to unlimited.
|
||||
$field_name = 'field_group_rows';
|
||||
$field_storage = FieldStorageConfig::create([
|
||||
'field_name' => $field_name,
|
||||
'entity_type' => 'node',
|
||||
'type' => 'string',
|
||||
'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
|
||||
]);
|
||||
$field_storage->save();
|
||||
// Create an instance of the text field on the content type.
|
||||
$field = FieldConfig::create([
|
||||
'field_storage' => $field_storage,
|
||||
'bundle' => 'page',
|
||||
]);
|
||||
$field->save();
|
||||
$grouped_field_values = ['a', 'b', 'c'];
|
||||
$edit = [
|
||||
'title' => $this->randomMachineName(),
|
||||
$field_name => $grouped_field_values,
|
||||
];
|
||||
$this->drupalCreateNode($edit);
|
||||
$view = Views::getView('test_serializer_node_display_field');
|
||||
$view->setDisplay('rest_export_1');
|
||||
// Override the view's fields to include the field_group_rows field, set the
|
||||
// group_rows setting to true.
|
||||
$fields = [
|
||||
$field_name => [
|
||||
'id' => $field_name,
|
||||
'table' => 'node__' . $field_name,
|
||||
'field' => $field_name,
|
||||
'type' => 'string',
|
||||
'group_rows' => TRUE,
|
||||
],
|
||||
];
|
||||
$view->displayHandlers->get('default')->overrideOption('fields', $fields);
|
||||
$build = $view->preview();
|
||||
// Get the serializer service.
|
||||
$serializer = $this->container->get('serializer');
|
||||
// Check if the field_group_rows field is grouped.
|
||||
$expected = [];
|
||||
$expected[] = [$field_name => implode(', ', $grouped_field_values)];
|
||||
$this->assertEquals($serializer->serialize($expected, 'json'), (string) $this->renderer->renderRoot($build));
|
||||
// Set the group rows setting to false.
|
||||
$view = Views::getView('test_serializer_node_display_field');
|
||||
$view->setDisplay('rest_export_1');
|
||||
$fields[$field_name]['group_rows'] = FALSE;
|
||||
$view->displayHandlers->get('default')->overrideOption('fields', $fields);
|
||||
$build = $view->preview();
|
||||
// Check if the field_group_rows field is ungrouped and displayed per row.
|
||||
$expected = [];
|
||||
foreach ($grouped_field_values as $grouped_field_value) {
|
||||
$expected[] = [$field_name => $grouped_field_value];
|
||||
}
|
||||
$this->assertEquals($serializer->serialize($expected, 'json'), (string) $this->renderer->renderRoot($build));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the exposed filter works.
|
||||
*
|
||||
* There is an exposed filter on the title field which takes a title query
|
||||
* parameter. This is set to filter nodes by those whose title starts with
|
||||
* the value provided.
|
||||
*/
|
||||
public function testRestViewExposedFilter(): void {
|
||||
$this->drupalCreateContentType(['type' => 'page']);
|
||||
$node0 = $this->drupalCreateNode(['title' => 'Node 1']);
|
||||
$node1 = $this->drupalCreateNode(['title' => 'Node 11']);
|
||||
$node2 = $this->drupalCreateNode(['title' => 'Node 111']);
|
||||
|
||||
// Test that no filter brings back all three nodes.
|
||||
$result = Json::decode($this->drupalGet('test/serialize/node-exposed-filter', ['query' => ['_format' => 'json']]));
|
||||
|
||||
$expected = [
|
||||
0 => [
|
||||
'nid' => $node0->id(),
|
||||
'body' => (string) $node0->body->processed,
|
||||
],
|
||||
1 => [
|
||||
'nid' => $node1->id(),
|
||||
'body' => (string) $node1->body->processed,
|
||||
],
|
||||
2 => [
|
||||
'nid' => $node2->id(),
|
||||
'body' => (string) $node2->body->processed,
|
||||
],
|
||||
];
|
||||
|
||||
$this->assertSame($expected, $result, 'Querying a view with no exposed filter returns all nodes.');
|
||||
|
||||
// Test that title starts with 'Node 11' query finds 2 of the 3 nodes.
|
||||
$result = Json::decode($this->drupalGet('test/serialize/node-exposed-filter', ['query' => ['_format' => 'json', 'title' => 'Node 11']]));
|
||||
|
||||
$expected = [
|
||||
0 => [
|
||||
'nid' => $node1->id(),
|
||||
'body' => (string) $node1->body->processed,
|
||||
],
|
||||
1 => [
|
||||
'nid' => $node2->id(),
|
||||
'body' => (string) $node2->body->processed,
|
||||
],
|
||||
];
|
||||
|
||||
$cache_contexts = [
|
||||
'languages:language_content',
|
||||
'languages:language_interface',
|
||||
'theme',
|
||||
'request_format',
|
||||
'user.node_grants:view',
|
||||
'url',
|
||||
];
|
||||
|
||||
$this->assertSame($expected, $result, 'Querying a view with a starts with exposed filter on the title returns nodes whose title starts with value provided.');
|
||||
$this->assertCacheContexts($cache_contexts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests multilingual entity rows.
|
||||
*/
|
||||
public function testMulEntityRows(): void {
|
||||
// Create some languages.
|
||||
ConfigurableLanguage::createFromLangcode('l1')->save();
|
||||
ConfigurableLanguage::createFromLangcode('l2')->save();
|
||||
|
||||
// Create an entity with no translations.
|
||||
$storage = \Drupal::entityTypeManager()->getStorage('entity_test_mul');
|
||||
$storage->create(['langcode' => 'l1', 'name' => 'mul-none'])->save();
|
||||
|
||||
// Create some entities with translations.
|
||||
$entity = $storage->create(['langcode' => 'l1', 'name' => 'mul-l1-orig']);
|
||||
$entity->save();
|
||||
$entity->addTranslation('l2', ['name' => 'mul-l1-l2'])->save();
|
||||
$entity = $storage->create(['langcode' => 'l2', 'name' => 'mul-l2-orig']);
|
||||
$entity->save();
|
||||
$entity->addTranslation('l1', ['name' => 'mul-l2-l1'])->save();
|
||||
|
||||
// Get the names of the output.
|
||||
$json = $this->drupalGet('test/serialize/translated_entity', ['query' => ['_format' => 'json']]);
|
||||
$decoded = $this->container->get('serializer')->decode($json, 'json');
|
||||
$names = [];
|
||||
foreach ($decoded as $item) {
|
||||
$names[] = $item['name'][0]['value'];
|
||||
}
|
||||
sort($names);
|
||||
|
||||
// Check that the names are correct.
|
||||
$expected = ['mul-l1-l2', 'mul-l1-orig', 'mul-l2-l1', 'mul-l2-orig', 'mul-none'];
|
||||
$this->assertSame($expected, $names, 'The translated content was found in the JSON.');
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\rest\Functional;
|
||||
|
||||
/**
|
||||
* Trait for ResourceTestBase subclasses testing $format='xml'.
|
||||
*/
|
||||
trait XmlNormalizationQuirksTrait {
|
||||
|
||||
/**
|
||||
* Applies the XML encoding quirks that remain after decoding.
|
||||
*
|
||||
* The XML encoding:
|
||||
* - maps empty arrays to the empty string
|
||||
* - maps single-item arrays to just that single item
|
||||
* - restructures multiple-item arrays that lives in a single-item array
|
||||
*
|
||||
* @param array $normalization
|
||||
* A normalization.
|
||||
*
|
||||
* @return array
|
||||
* The updated normalization.
|
||||
*
|
||||
* @see \Symfony\Component\Serializer\Encoder\XmlEncoder
|
||||
*/
|
||||
protected function applyXmlDecodingQuirks(array $normalization): array {
|
||||
foreach ($normalization as $key => $value) {
|
||||
if ($value === [] || $value === NULL) {
|
||||
$normalization[$key] = '';
|
||||
}
|
||||
elseif (is_array($value)) {
|
||||
// Collapse single-item numeric arrays to just the single item.
|
||||
if (count($value) === 1 && is_numeric(array_keys($value)[0]) && is_scalar($value[0])) {
|
||||
$value = $value[0];
|
||||
}
|
||||
// Restructure multiple-item arrays inside a single-item numeric array.
|
||||
// @see \Symfony\Component\Serializer\Encoder\XmlEncoder::buildXml()
|
||||
elseif (count($value) === 1 && is_numeric(array_keys($value)[0]) && is_array(reset($value))) {
|
||||
$rewritten_value = [];
|
||||
foreach ($value[0] as $child_key => $child_value) {
|
||||
if (is_numeric(array_keys(reset($value))[0])) {
|
||||
$rewritten_value[$child_key] = ['@key' => $child_key] + $child_value;
|
||||
}
|
||||
else {
|
||||
$rewritten_value[$child_key] = $child_value;
|
||||
}
|
||||
}
|
||||
$value = $rewritten_value;
|
||||
}
|
||||
|
||||
// If the post-quirk value is still an array after the above, recurse.
|
||||
if (is_array($value)) {
|
||||
$value = $this->applyXmlDecodingQuirks($value);
|
||||
}
|
||||
|
||||
// Store post-quirk value.
|
||||
$normalization[$key] = $value;
|
||||
}
|
||||
}
|
||||
return $normalization;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,208 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\rest\Kernel\Entity;
|
||||
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Drupal\rest\Entity\ConfigDependencies;
|
||||
use Drupal\rest\Entity\RestResourceConfig;
|
||||
use Drupal\rest\RestResourceConfigInterface;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\rest\Entity\ConfigDependencies
|
||||
*
|
||||
* @group rest
|
||||
*/
|
||||
class ConfigDependenciesTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['rest', 'entity_test', 'serialization'];
|
||||
|
||||
/**
|
||||
* @covers ::calculateDependencies
|
||||
*
|
||||
* @dataProvider providerBasicDependencies
|
||||
*/
|
||||
public function testCalculateDependencies(array $configuration): void {
|
||||
$config_dependencies = new ConfigDependencies(['json' => 'serialization'], ['basic_auth' => 'basic_auth']);
|
||||
|
||||
$rest_config = RestResourceConfig::create($configuration);
|
||||
|
||||
$result = $config_dependencies->calculateDependencies($rest_config);
|
||||
$this->assertEquals([
|
||||
'module' => ['basic_auth', 'serialization'],
|
||||
], $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::onDependencyRemoval
|
||||
* @covers ::onDependencyRemovalForMethodGranularity
|
||||
* @covers ::onDependencyRemovalForResourceGranularity
|
||||
*
|
||||
* @dataProvider providerBasicDependencies
|
||||
*/
|
||||
public function testOnDependencyRemovalRemoveUnrelatedDependency(array $configuration): void {
|
||||
$config_dependencies = new ConfigDependencies(['json' => 'serialization'], ['basic_auth' => 'basic_auth']);
|
||||
|
||||
$rest_config = RestResourceConfig::create($configuration);
|
||||
|
||||
$this->assertFalse($config_dependencies->onDependencyRemoval($rest_config, ['module' => ['node']]));
|
||||
$this->assertEquals($configuration['configuration'], $rest_config->get('configuration'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* An array with numerical keys:
|
||||
* 0. The original REST resource configuration.
|
||||
*/
|
||||
public static function providerBasicDependencies() {
|
||||
return [
|
||||
'method' => [
|
||||
[
|
||||
'plugin_id' => 'entity:entity_test',
|
||||
'granularity' => RestResourceConfigInterface::METHOD_GRANULARITY,
|
||||
'configuration' => [
|
||||
'GET' => [
|
||||
'supported_auth' => ['basic_auth'],
|
||||
'supported_formats' => ['json'],
|
||||
],
|
||||
'POST' => [
|
||||
'supported_auth' => ['cookie'],
|
||||
'supported_formats' => ['xml'],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'resource' => [
|
||||
[
|
||||
'plugin_id' => 'entity:entity_test',
|
||||
'granularity' => RestResourceConfigInterface::RESOURCE_GRANULARITY,
|
||||
'configuration' => [
|
||||
'methods' => ['GET', 'POST'],
|
||||
'formats' => ['json'],
|
||||
'authentication' => ['cookie', 'basic_auth'],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::onDependencyRemoval
|
||||
* @covers ::onDependencyRemovalForMethodGranularity
|
||||
*/
|
||||
public function testOnDependencyRemovalRemoveAuth(): void {
|
||||
$config_dependencies = new ConfigDependencies(['json' => 'serialization'], ['basic_auth' => 'basic_auth']);
|
||||
|
||||
$rest_config = RestResourceConfig::create([
|
||||
'plugin_id' => 'entity:entity_test',
|
||||
'granularity' => RestResourceConfigInterface::METHOD_GRANULARITY,
|
||||
'configuration' => [
|
||||
'GET' => [
|
||||
'supported_auth' => ['cookie'],
|
||||
'supported_formats' => ['json'],
|
||||
],
|
||||
'POST' => [
|
||||
'supported_auth' => ['basic_auth'],
|
||||
'supported_formats' => ['json'],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertTrue($config_dependencies->onDependencyRemoval($rest_config, ['module' => ['basic_auth']]));
|
||||
$this->assertEquals(['cookie'], $rest_config->getAuthenticationProviders('GET'));
|
||||
$this->assertEquals([], $rest_config->getAuthenticationProviders('POST'));
|
||||
$this->assertEquals([
|
||||
'GET' => [
|
||||
'supported_auth' => ['cookie'],
|
||||
'supported_formats' => ['json'],
|
||||
],
|
||||
'POST' => [
|
||||
'supported_formats' => ['json'],
|
||||
],
|
||||
], $rest_config->get('configuration'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::onDependencyRemoval
|
||||
* @covers ::onDependencyRemovalForResourceGranularity
|
||||
*
|
||||
* @dataProvider providerOnDependencyRemovalForResourceGranularity
|
||||
*/
|
||||
public function testOnDependencyRemovalForResourceGranularity(array $configuration, $module, $expected_configuration): void {
|
||||
assert(is_string($module));
|
||||
assert($expected_configuration === FALSE || is_array($expected_configuration));
|
||||
|
||||
$config_dependencies = new ConfigDependencies(['json' => 'serialization'], ['basic_auth' => 'basic_auth']);
|
||||
|
||||
$rest_config = RestResourceConfig::create($configuration);
|
||||
|
||||
$this->assertSame(!empty($expected_configuration), $config_dependencies->onDependencyRemoval($rest_config, ['module' => [$module]]));
|
||||
if (!empty($expected_configuration)) {
|
||||
$this->assertEquals($expected_configuration, $rest_config->get('configuration'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* An array with numerical keys:
|
||||
* 0. The original REST resource configuration.
|
||||
* 1. The module to uninstall (the dependency that is about to be removed).
|
||||
* 2. The expected configuration after uninstalling this module.
|
||||
*/
|
||||
public static function providerOnDependencyRemovalForResourceGranularity() {
|
||||
return [
|
||||
'resource with multiple formats' => [
|
||||
[
|
||||
'plugin_id' => 'entity:entity_test',
|
||||
'granularity' => RestResourceConfigInterface::RESOURCE_GRANULARITY,
|
||||
'configuration' => [
|
||||
'methods' => ['GET', 'POST'],
|
||||
'formats' => ['xml', 'json'],
|
||||
'authentication' => ['cookie', 'basic_auth'],
|
||||
],
|
||||
],
|
||||
'serialization',
|
||||
[
|
||||
'methods' => ['GET', 'POST'],
|
||||
'formats' => ['xml'],
|
||||
'authentication' => ['cookie', 'basic_auth'],
|
||||
],
|
||||
],
|
||||
'resource with multiple authentication providers' => [
|
||||
[
|
||||
'plugin_id' => 'entity:entity_test',
|
||||
'granularity' => RestResourceConfigInterface::RESOURCE_GRANULARITY,
|
||||
'configuration' => [
|
||||
'methods' => ['GET', 'POST'],
|
||||
'formats' => ['json', 'xml'],
|
||||
'authentication' => ['cookie', 'basic_auth'],
|
||||
],
|
||||
],
|
||||
'basic_auth',
|
||||
[
|
||||
'methods' => ['GET', 'POST'],
|
||||
'formats' => ['json', 'xml'],
|
||||
'authentication' => ['cookie'],
|
||||
],
|
||||
],
|
||||
'resource with only basic_auth authentication' => [
|
||||
[
|
||||
'plugin_id' => 'entity:entity_test',
|
||||
'granularity' => RestResourceConfigInterface::RESOURCE_GRANULARITY,
|
||||
'configuration' => [
|
||||
'methods' => ['GET', 'POST'],
|
||||
'formats' => ['json', 'xml'],
|
||||
'authentication' => ['basic_auth'],
|
||||
],
|
||||
],
|
||||
'basic_auth',
|
||||
FALSE,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\rest\Kernel\Entity;
|
||||
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Drupal\rest\Entity\RestResourceConfig;
|
||||
use Drupal\rest\RestResourceConfigInterface;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\rest\RestPermissions
|
||||
*
|
||||
* @group rest
|
||||
*/
|
||||
class RestPermissionsTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'rest',
|
||||
'dblog',
|
||||
'serialization',
|
||||
'basic_auth',
|
||||
'user',
|
||||
];
|
||||
|
||||
/**
|
||||
* @covers ::permissions
|
||||
*/
|
||||
public function testPermissions(): void {
|
||||
RestResourceConfig::create([
|
||||
'id' => 'dblog',
|
||||
'plugin_id' => 'dblog',
|
||||
'granularity' => RestResourceConfigInterface::METHOD_GRANULARITY,
|
||||
'configuration' => [
|
||||
'GET' => [
|
||||
'supported_auth' => ['cookie'],
|
||||
'supported_formats' => ['json'],
|
||||
],
|
||||
],
|
||||
])->save();
|
||||
|
||||
$permissions = $this->container->get('user.permissions')->getPermissions();
|
||||
$this->assertArrayHasKey('restful get dblog', $permissions);
|
||||
$this->assertSame(['config' => ['rest.resource.dblog']], $permissions['restful get dblog']['dependencies']);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\rest\Kernel\Entity;
|
||||
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Drupal\rest\Entity\RestResourceConfig;
|
||||
use Drupal\rest\RestResourceConfigInterface;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\rest\Entity\RestResourceConfig
|
||||
*
|
||||
* @group rest
|
||||
*/
|
||||
class RestResourceConfigTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'rest',
|
||||
'entity_test',
|
||||
'serialization',
|
||||
'basic_auth',
|
||||
'user',
|
||||
];
|
||||
|
||||
/**
|
||||
* @covers ::calculateDependencies
|
||||
*/
|
||||
public function testCalculateDependencies(): void {
|
||||
$rest_config = RestResourceConfig::create([
|
||||
'plugin_id' => 'entity:entity_test',
|
||||
'granularity' => RestResourceConfigInterface::METHOD_GRANULARITY,
|
||||
'configuration' => [
|
||||
'GET' => [
|
||||
'supported_auth' => ['cookie'],
|
||||
'supported_formats' => ['json'],
|
||||
],
|
||||
'POST' => [
|
||||
'supported_auth' => ['basic_auth'],
|
||||
'supported_formats' => ['json'],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$rest_config->calculateDependencies();
|
||||
$this->assertEquals(['module' => ['basic_auth', 'entity_test', 'serialization', 'user']], $rest_config->getDependencies());
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\rest\Kernel\Entity;
|
||||
|
||||
use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
|
||||
use Drupal\rest\Entity\RestResourceConfig;
|
||||
use Drupal\rest\RestResourceConfigInterface;
|
||||
|
||||
/**
|
||||
* Tests validation of rest_resource_config entities.
|
||||
*
|
||||
* @group rest
|
||||
*/
|
||||
class RestResourceConfigValidationTest extends ConfigEntityValidationTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['rest', 'serialization'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected bool $hasLabel = FALSE;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->entity = RestResourceConfig::create([
|
||||
'id' => 'test',
|
||||
'plugin_id' => 'entity:date_format',
|
||||
'granularity' => RestResourceConfigInterface::METHOD_GRANULARITY,
|
||||
'configuration' => [],
|
||||
]);
|
||||
$this->entity->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that the resource plugin ID is validated.
|
||||
*/
|
||||
public function testInvalidPluginId(): void {
|
||||
$this->entity->set('plugin_id', 'non_existent');
|
||||
$this->assertValidationErrors([
|
||||
'plugin_id' => "The 'non_existent' plugin does not exist.",
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\rest\Kernel\EntityResource;
|
||||
|
||||
use Drupal\Core\Config\Entity\ConfigEntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Extension\ExtensionLifecycle;
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Drupal\Tests\rest\Functional\EntityResource\ConfigEntityResourceTestBase;
|
||||
|
||||
/**
|
||||
* Checks that all core content/config entity types have REST test coverage.
|
||||
*
|
||||
* Every entity type must have test coverage for:
|
||||
* - every format in core (json + xml)
|
||||
* - every authentication provider in core (anon, cookie, basic_auth)
|
||||
*
|
||||
* Additionally, every entity type must have the correct parent test class.
|
||||
*
|
||||
* @group rest
|
||||
* @group #slow
|
||||
*/
|
||||
class EntityResourceRestTestCoverageTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['system', 'user'];
|
||||
|
||||
/**
|
||||
* Entity definitions array.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $definitions;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$all_modules = $this->container->get('extension.list.module')->getList();
|
||||
$stable_core_modules = array_filter($all_modules, function ($module) {
|
||||
// Filter out contrib, hidden, testing, deprecated and experimental
|
||||
// modules. We also don't need to enable modules that are already enabled.
|
||||
return $module->origin === 'core' &&
|
||||
empty($module->info['hidden']) &&
|
||||
$module->status == FALSE &&
|
||||
$module->info['package'] !== 'Testing' &&
|
||||
$module->info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] !== ExtensionLifecycle::DEPRECATED &&
|
||||
$module->info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] !== ExtensionLifecycle::EXPERIMENTAL;
|
||||
});
|
||||
|
||||
$this->container->get('module_installer')->install(array_keys($stable_core_modules));
|
||||
|
||||
$this->definitions = $this->container->get('entity_type.manager')->getDefinitions();
|
||||
|
||||
// Entity types marked as "internal" are not exposed by the entity REST
|
||||
// resource plugin and hence also don't need test coverage.
|
||||
$this->definitions = array_filter($this->definitions, function (EntityTypeInterface $entity_type) {
|
||||
return !$entity_type->isInternal();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that all core content/config entity types have REST test coverage.
|
||||
*/
|
||||
public function testEntityTypeRestTestCoverage(): void {
|
||||
$tests = [
|
||||
// Test coverage for formats provided by the 'serialization' module.
|
||||
'serialization' => [
|
||||
'path' => '\Drupal\Tests\PROVIDER\Functional\Rest\CLASS',
|
||||
'class suffix' => [
|
||||
'JsonAnonTest',
|
||||
'JsonBasicAuthTest',
|
||||
'JsonCookieTest',
|
||||
'XmlAnonTest',
|
||||
'XmlBasicAuthTest',
|
||||
'XmlCookieTest',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$problems = [];
|
||||
foreach ($this->definitions as $entity_type_id => $info) {
|
||||
$class_name_full = $info->getClass();
|
||||
$parts = explode('\\', $class_name_full);
|
||||
$class_name = end($parts);
|
||||
$module_name = $parts[1];
|
||||
|
||||
foreach ($tests as $module => $info) {
|
||||
$path = $info['path'];
|
||||
$missing_tests = [];
|
||||
foreach ($info['class suffix'] as $postfix) {
|
||||
$class = str_replace(['PROVIDER', 'CLASS'], [$module_name, $class_name], $path . $postfix);
|
||||
$class_alternative = str_replace("\\Drupal\\Tests\\$module_name\\Functional", '\Drupal\FunctionalTests', $class);
|
||||
// For entities defined in the system module with Jsonapi tests in
|
||||
// another module.
|
||||
$class_entity_in_system_alternative = str_replace(['PROVIDER', 'CLASS'], [$entity_type_id, $class_name], $path . $postfix);
|
||||
if (class_exists($class) || class_exists($class_alternative) || class_exists($class_entity_in_system_alternative)) {
|
||||
continue;
|
||||
}
|
||||
$missing_tests[] = $postfix;
|
||||
}
|
||||
if (!empty($missing_tests)) {
|
||||
$missing_tests_list = implode(', ', array_map(function ($missing_test) use ($class_name) {
|
||||
return $class_name . $missing_test;
|
||||
}, $missing_tests));
|
||||
$which_normalization = $module === 'serialization' ? 'default' : $module;
|
||||
$problems[] = "$entity_type_id: $class_name ($class_name_full), $which_normalization normalization (expected tests: $missing_tests_list)";
|
||||
}
|
||||
}
|
||||
|
||||
$config_entity = is_subclass_of($class_name_full, ConfigEntityInterface::class);
|
||||
$config_test = is_subclass_of($class, ConfigEntityResourceTestBase::class)
|
||||
|| is_subclass_of($class_alternative, ConfigEntityResourceTestBase::class)
|
||||
|| is_subclass_of($class_entity_in_system_alternative, ConfigEntityResourceTestBase::class);
|
||||
if ($config_entity && !$config_test) {
|
||||
$problems[] = "$entity_type_id: $class_name is a config entity, but the test is for content entities.";
|
||||
}
|
||||
elseif (!$config_entity && $config_test) {
|
||||
$problems[] = "$entity_type_id: $class_name is a content entity, but the test is for config entities.";
|
||||
}
|
||||
}
|
||||
$this->assertSame([], $problems);
|
||||
}
|
||||
|
||||
}
|
||||
144
web/core/modules/rest/tests/src/Kernel/RequestHandlerTest.php
Normal file
144
web/core/modules/rest/tests/src/Kernel/RequestHandlerTest.php
Normal file
@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\rest\Kernel;
|
||||
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\Core\Routing\RouteMatch;
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Drupal\rest\Plugin\ResourceBase;
|
||||
use Drupal\rest\RequestHandler;
|
||||
use Drupal\rest\ResourceResponse;
|
||||
use Drupal\rest\RestResourceConfigInterface;
|
||||
use Prophecy\Argument;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Route;
|
||||
use Symfony\Component\Serializer\Encoder\DecoderInterface;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
/**
|
||||
* Test REST RequestHandler controller logic.
|
||||
*
|
||||
* @group rest
|
||||
* @coversDefaultClass \Drupal\rest\RequestHandler
|
||||
*/
|
||||
class RequestHandlerTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* @var \Drupal\rest\RequestHandler
|
||||
*/
|
||||
protected $requestHandler;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['serialization', 'rest'];
|
||||
|
||||
/**
|
||||
* The entity storage.
|
||||
*
|
||||
* @var \Prophecy\Prophecy\ObjectProphecy
|
||||
*/
|
||||
protected $entityStorage;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
$serializer = $this->prophesize(SerializerInterface::class);
|
||||
$serializer->willImplement(DecoderInterface::class);
|
||||
$serializer->decode(Json::encode(['this is an array']), 'json', Argument::type('array'))
|
||||
->willReturn(['this is an array']);
|
||||
$this->requestHandler = new RequestHandler($serializer->reveal());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::handle
|
||||
*/
|
||||
public function testHandle(): void {
|
||||
$request = new Request([], [], [], [], [], ['CONTENT_TYPE' => 'application/json'], Json::encode(['this is an array']));
|
||||
$route_match = new RouteMatch(
|
||||
'test',
|
||||
(new Route(
|
||||
'/rest/test',
|
||||
['_rest_resource_config' => 'rest_plugin', 'example' => ''],
|
||||
['_format' => 'json']
|
||||
))->setMethods(['GET'])
|
||||
);
|
||||
|
||||
$resource = $this->prophesize(StubRequestHandlerResourcePlugin::class);
|
||||
$resource->get('', $request)
|
||||
->shouldBeCalled();
|
||||
$resource->getPluginDefinition()
|
||||
->willReturn([])
|
||||
->shouldBeCalled();
|
||||
|
||||
// Setup the configuration.
|
||||
$config = $this->prophesize(RestResourceConfigInterface::class);
|
||||
$config->getResourcePlugin()->willReturn($resource->reveal());
|
||||
$config->getCacheContexts()->willReturn([]);
|
||||
$config->getCacheTags()->willReturn([]);
|
||||
$config->getCacheMaxAge()->willReturn(12);
|
||||
|
||||
// Response returns NULL this time because response from plugin is not
|
||||
// a ResourceResponse so it is passed through directly.
|
||||
$response = $this->requestHandler->handle($route_match, $request, $config->reveal());
|
||||
$this->assertEquals(NULL, $response);
|
||||
|
||||
// Response will return a ResourceResponse this time.
|
||||
$response = new ResourceResponse([]);
|
||||
$resource->get(NULL, $request)
|
||||
->willReturn($response);
|
||||
$handler_response = $this->requestHandler->handle($route_match, $request, $config->reveal());
|
||||
$this->assertEquals($response, $handler_response);
|
||||
|
||||
// We will call the patch method this time.
|
||||
$route_match = new RouteMatch(
|
||||
'test',
|
||||
(new Route(
|
||||
'/rest/test',
|
||||
[
|
||||
'_rest_resource_config' => 'rest_plugin',
|
||||
'example_original' => '',
|
||||
],
|
||||
['_content_type_format' => 'json']
|
||||
))->setMethods(['PATCH']));
|
||||
$request->setMethod('PATCH');
|
||||
$response = new ResourceResponse([]);
|
||||
$resource->patch(['this is an array'], $request)
|
||||
->shouldBeCalledTimes(1)
|
||||
->willReturn($response);
|
||||
$handler_response = $this->requestHandler->handle($route_match, $request, $config->reveal());
|
||||
$this->assertEquals($response, $handler_response);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Stub class where we can prophesize methods.
|
||||
*/
|
||||
class StubRequestHandlerResourcePlugin extends ResourceBase {
|
||||
|
||||
/**
|
||||
* Handles a GET request.
|
||||
*/
|
||||
public function get($example = NULL, ?Request $request = NULL) {}
|
||||
|
||||
/**
|
||||
* Handles a POST request.
|
||||
*/
|
||||
public function post() {}
|
||||
|
||||
/**
|
||||
* Handles a PATCH request.
|
||||
*/
|
||||
public function patch($data, Request $request) {}
|
||||
|
||||
/**
|
||||
* Handles a DELETE request.
|
||||
*/
|
||||
public function delete() {}
|
||||
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\rest\Kernel\Views;
|
||||
|
||||
use Drupal\rest\Plugin\views\display\RestExport;
|
||||
use Drupal\Tests\views\Kernel\ViewsKernelTestBase;
|
||||
use Drupal\views\Entity\View;
|
||||
use Drupal\views\Tests\ViewTestData;
|
||||
|
||||
/**
|
||||
* Tests the REST export view display plugin.
|
||||
*
|
||||
* @coversDefaultClass \Drupal\rest\Plugin\views\display\RestExport
|
||||
*
|
||||
* @group rest
|
||||
*/
|
||||
class RestExportTest extends ViewsKernelTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $testViews = ['test_serializer_display_entity'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'rest_test_views',
|
||||
'serialization',
|
||||
'rest',
|
||||
'entity_test',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp($import_test_views = TRUE): void {
|
||||
parent::setUp($import_test_views);
|
||||
|
||||
ViewTestData::createTestViews(static::class, ['rest_test_views']);
|
||||
$this->installEntitySchema('entity_test');
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::buildResponse
|
||||
*/
|
||||
public function testBuildResponse(): void {
|
||||
/** @var \Drupal\views\Entity\View $view */
|
||||
$view = View::load('test_serializer_display_entity');
|
||||
$display = &$view->getDisplay('rest_export_1');
|
||||
|
||||
$display['display_options']['defaults']['style'] = FALSE;
|
||||
$display['display_options']['style']['type'] = 'serializer';
|
||||
$display['display_options']['style']['options']['formats'] = ['json', 'xml'];
|
||||
$view->save();
|
||||
|
||||
// No custom header should be set yet.
|
||||
$response = RestExport::buildResponse('test_serializer_display_entity', 'rest_export_1', []);
|
||||
$this->assertEmpty($response->headers->get('Custom-Header'));
|
||||
|
||||
// Clear render cache.
|
||||
/** @var \Drupal\Core\Cache\MemoryBackend $render_cache */
|
||||
$render_cache = $this->container->get('cache_factory')->get('render');
|
||||
$render_cache->deleteAll();
|
||||
|
||||
// A custom header should now be added.
|
||||
// @see rest_test_views_views_post_execute()
|
||||
$header = $this->randomString();
|
||||
$this->container->get('state')->set('rest_test_views_set_header', $header);
|
||||
$response = RestExport::buildResponse('test_serializer_display_entity', 'rest_export_1', []);
|
||||
$this->assertEquals($header, $response->headers->get('Custom-Header'));
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\rest\Kernel\Views;
|
||||
|
||||
use Drupal\Tests\views\Kernel\ViewsKernelTestBase;
|
||||
use Drupal\views\Entity\View;
|
||||
use Drupal\views\Tests\ViewTestData;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\rest\Plugin\views\style\Serializer
|
||||
* @group views
|
||||
*/
|
||||
class StyleSerializerKernelTest extends ViewsKernelTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $testViews = ['test_serializer_display_entity'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['rest_test_views', 'serialization', 'rest'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp($import_test_views = TRUE): void {
|
||||
parent::setUp($import_test_views);
|
||||
|
||||
ViewTestData::createTestViews(static::class, ['rest_test_views']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::calculateDependencies
|
||||
*/
|
||||
public function testCalculateDependencies(): void {
|
||||
/** @var \Drupal\views\Entity\View $view */
|
||||
$view = View::load('test_serializer_display_entity');
|
||||
$display = &$view->getDisplay('rest_export_1');
|
||||
|
||||
$display['display_options']['defaults']['style'] = FALSE;
|
||||
$display['display_options']['style']['type'] = 'serializer';
|
||||
$display['display_options']['style']['options']['formats'] = ['json', 'xml'];
|
||||
$view->save();
|
||||
|
||||
$view->calculateDependencies();
|
||||
$this->assertEquals(['module' => ['rest', 'serialization', 'user']], $view->getDependencies());
|
||||
}
|
||||
|
||||
}
|
||||
158
web/core/modules/rest/tests/src/Unit/CollectRoutesTest.php
Normal file
158
web/core/modules/rest/tests/src/Unit/CollectRoutesTest.php
Normal file
@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\rest\Unit;
|
||||
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
use Drupal\Core\DependencyInjection\ContainerBuilder;
|
||||
use Drupal\rest\Plugin\views\display\RestExport;
|
||||
use Drupal\views\Entity\View;
|
||||
use Symfony\Component\Routing\Route;
|
||||
use Symfony\Component\Routing\RouteCollection;
|
||||
|
||||
/**
|
||||
* Tests the REST export view plugin.
|
||||
*
|
||||
* @group rest
|
||||
*/
|
||||
class CollectRoutesTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* The REST export instance.
|
||||
*
|
||||
* @var \Drupal\rest\Plugin\views\display\RestExport
|
||||
*/
|
||||
protected $restExport;
|
||||
|
||||
/**
|
||||
* The RouteCollection.
|
||||
*
|
||||
* @var \Symfony\Component\Routing\RouteCollection
|
||||
*/
|
||||
protected $routes;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$container = new ContainerBuilder();
|
||||
$view = new View(['id' => 'test_view'], 'view');
|
||||
|
||||
$view_executable = $this->getMockBuilder('\Drupal\views\ViewExecutable')
|
||||
->onlyMethods(['initHandlers', 'getTitle'])
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$view_executable->expects($this->any())
|
||||
->method('getTitle')
|
||||
->willReturn('View title');
|
||||
|
||||
$view_executable->storage = $view;
|
||||
$view_executable->argument = [];
|
||||
|
||||
$display_manager = $this->getMockBuilder('\Drupal\views\Plugin\ViewsPluginManager')
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$container->set('plugin.manager.views.display', $display_manager);
|
||||
|
||||
$access_manager = $this->getMockBuilder('\Drupal\views\Plugin\ViewsPluginManager')
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$container->set('plugin.manager.views.access', $access_manager);
|
||||
|
||||
$route_provider = $this->getMockBuilder('\Drupal\Core\Routing\RouteProviderInterface')
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$container->set('router.route_provider', $route_provider);
|
||||
|
||||
$container->setParameter('authentication_providers', ['basic_auth' => 'basic_auth']);
|
||||
|
||||
$state = $this->createMock('\Drupal\Core\State\StateInterface');
|
||||
$container->set('state', $state);
|
||||
|
||||
$style_manager = $this->getMockBuilder('\Drupal\views\Plugin\ViewsPluginManager')
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$container->set('plugin.manager.views.style', $style_manager);
|
||||
$container->set('renderer', $this->createMock('Drupal\Core\Render\RendererInterface'));
|
||||
|
||||
$authentication_collector = $this->createMock('\Drupal\Core\Authentication\AuthenticationCollectorInterface');
|
||||
$container->set('authentication_collector', $authentication_collector);
|
||||
$authentication_collector->expects($this->any())
|
||||
->method('getSortedProviders')
|
||||
->willReturn(['basic_auth' => 'data', 'cookie' => 'data']);
|
||||
|
||||
$container->setParameter('serializer.format_providers', ['json']);
|
||||
|
||||
\Drupal::setContainer($container);
|
||||
|
||||
$this->restExport = RestExport::create($container, [], "test_routes", []);
|
||||
$this->restExport->view = $view_executable;
|
||||
|
||||
// Initialize a display.
|
||||
$this->restExport->display = ['id' => 'page_1'];
|
||||
|
||||
// Set the style option.
|
||||
$this->restExport->setOption('style', ['type' => 'serializer']);
|
||||
|
||||
// Set the auth option.
|
||||
$this->restExport->setOption('auth', ['basic_auth']);
|
||||
|
||||
$display_manager->expects($this->once())
|
||||
->method('getDefinition')
|
||||
->willReturn(['id' => 'test', 'provider' => 'test']);
|
||||
|
||||
$none = $this->getMockBuilder('\Drupal\views\Plugin\views\access\None')
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
|
||||
$access_manager->expects($this->once())
|
||||
->method('createInstance')
|
||||
->willReturn($none);
|
||||
|
||||
$style_plugin = $this->getMockBuilder('\Drupal\rest\Plugin\views\style\Serializer')
|
||||
->onlyMethods(['getFormats', 'init'])
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
|
||||
$style_plugin->expects($this->once())
|
||||
->method('getFormats')
|
||||
->willReturn(['json']);
|
||||
|
||||
$style_plugin->expects($this->once())
|
||||
->method('init')
|
||||
->with($view_executable)
|
||||
->willReturn(TRUE);
|
||||
|
||||
$style_manager->expects($this->once())
|
||||
->method('createInstance')
|
||||
->willReturn($style_plugin);
|
||||
|
||||
$this->routes = new RouteCollection();
|
||||
$this->routes->add('test_1', new Route('/test/1'));
|
||||
$this->routes->add('view.test_view.page_1', new Route('/test/2'));
|
||||
|
||||
$view->addDisplay('page', NULL, 'page_1');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if adding a requirement to a route only modify one route.
|
||||
*/
|
||||
public function testRoutesRequirements(): void {
|
||||
$this->restExport->collectRoutes($this->routes);
|
||||
|
||||
$requirements_1 = $this->routes->get('test_1')->getRequirements();
|
||||
$requirements_2 = $this->routes->get('view.test_view.page_1')->getRequirements();
|
||||
|
||||
$this->assertCount(0, $requirements_1, 'First route has no requirement.');
|
||||
$this->assertCount(1, $requirements_2, 'Views route with rest export had the format requirement added.');
|
||||
|
||||
// Check auth options.
|
||||
$auth = $this->routes->get('view.test_view.page_1')->getOption('_auth');
|
||||
$this->assertCount(1, $auth, 'View route with rest export has an auth option added');
|
||||
$this->assertEquals('basic_auth', $auth[0], 'View route with rest export has the correct auth option added');
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\rest\Unit\Entity;
|
||||
|
||||
use Drupal\rest\Entity\RestResourceConfig;
|
||||
use Drupal\rest\RestResourceConfigInterface;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\rest\Entity\RestResourceConfig
|
||||
*
|
||||
* @group rest
|
||||
*/
|
||||
class RestResourceConfigTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* Asserts that rest methods are normalized to upper case.
|
||||
*
|
||||
* This also tests that no exceptions are thrown during that method so that
|
||||
* alternate methods such as OPTIONS and PUT are supported.
|
||||
*/
|
||||
public function testNormalizeRestMethod(): void {
|
||||
$expected = ['GET', 'PUT', 'POST', 'PATCH', 'DELETE', 'OPTIONS', 'FOO'];
|
||||
$methods = ['get', 'put', 'post', 'patch', 'delete', 'options', 'foo'];
|
||||
$configuration = [];
|
||||
foreach ($methods as $method) {
|
||||
$configuration[$method] = [
|
||||
'supported_auth' => ['cookie'],
|
||||
'supported_formats' => ['json'],
|
||||
];
|
||||
}
|
||||
|
||||
$entity = new RestResourceConfig([
|
||||
'plugin_id' => 'entity:entity_test',
|
||||
'granularity' => RestResourceConfigInterface::METHOD_GRANULARITY,
|
||||
'configuration' => $configuration,
|
||||
], 'rest_resource_config');
|
||||
|
||||
$this->assertEquals($expected, $entity->getMethods());
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\rest\Unit;
|
||||
|
||||
use Drupal\Core\Entity\EntityConstraintViolationList;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\rest\Plugin\rest\resource\EntityResourceValidationTrait;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
use Drupal\user\Entity\User;
|
||||
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
|
||||
use Symfony\Component\Validator\ConstraintViolationInterface;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\rest\Plugin\rest\resource\EntityResourceValidationTrait
|
||||
* @group rest
|
||||
*/
|
||||
class EntityResourceValidationTraitTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* @covers ::validate
|
||||
*/
|
||||
public function testValidate(): void {
|
||||
$trait = new EntityResourceValidationTraitTestClass();
|
||||
|
||||
$method = new \ReflectionMethod($trait, 'validate');
|
||||
|
||||
$violations = $this->prophesize(EntityConstraintViolationList::class);
|
||||
$violations->filterByFieldAccess()->shouldBeCalled()->willReturn([]);
|
||||
$violations->count()->shouldBeCalled()->willReturn(0);
|
||||
|
||||
$entity = $this->prophesize(Node::class);
|
||||
$entity->validate()->shouldBeCalled()->willReturn($violations->reveal());
|
||||
|
||||
$method->invoke($trait, $entity->reveal());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::validate
|
||||
*/
|
||||
public function testFailedValidate(): void {
|
||||
$violation1 = $this->prophesize(ConstraintViolationInterface::class);
|
||||
$violation1->getPropertyPath()->willReturn('property_path');
|
||||
$violation1->getMessage()->willReturn('message');
|
||||
|
||||
$violation2 = $this->prophesize(ConstraintViolationInterface::class);
|
||||
$violation2->getPropertyPath()->willReturn('property_path');
|
||||
$violation2->getMessage()->willReturn('message');
|
||||
|
||||
$entity = $this->prophesize(User::class);
|
||||
|
||||
$violations = $this->getMockBuilder(EntityConstraintViolationList::class)
|
||||
->setConstructorArgs([$entity->reveal(), [$violation1->reveal(), $violation2->reveal()]])
|
||||
->onlyMethods(['filterByFieldAccess'])
|
||||
->getMock();
|
||||
|
||||
$violations->expects($this->once())
|
||||
->method('filterByFieldAccess')
|
||||
->willReturn([]);
|
||||
|
||||
$entity->validate()->willReturn($violations);
|
||||
|
||||
$trait = new EntityResourceValidationTraitTestClass();
|
||||
|
||||
$method = new \ReflectionMethod($trait, 'validate');
|
||||
|
||||
$this->expectException(UnprocessableEntityHttpException::class);
|
||||
|
||||
$method->invoke($trait, $entity->reveal());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A test class to use to test EntityResourceValidationTrait.
|
||||
*
|
||||
* Because the mock doesn't use the \Drupal namespace, the Symfony 4+ class
|
||||
* loader will throw a deprecation error.
|
||||
*/
|
||||
class EntityResourceValidationTraitTestClass {
|
||||
use EntityResourceValidationTrait;
|
||||
|
||||
}
|
||||
@ -0,0 +1,433 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\rest\Unit\EventSubscriber;
|
||||
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\Core\Cache\CacheableResponseInterface;
|
||||
use Drupal\Core\Render\RenderContext;
|
||||
use Drupal\Core\Render\RendererInterface;
|
||||
use Drupal\Core\Routing\RouteMatch;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
use Drupal\rest\EventSubscriber\ResourceResponseSubscriber;
|
||||
use Drupal\rest\ModifiedResourceResponse;
|
||||
use Drupal\rest\ResourceResponse;
|
||||
use Drupal\rest\ResourceResponseInterface;
|
||||
use Drupal\serialization\Encoder\JsonEncoder;
|
||||
use Drupal\serialization\Encoder\XmlEncoder;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
use Prophecy\Argument;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Event\ResponseEvent;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
use Symfony\Component\Routing\Route;
|
||||
use Symfony\Component\Serializer\Serializer;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\rest\EventSubscriber\ResourceResponseSubscriber
|
||||
* @group rest
|
||||
*/
|
||||
class ResourceResponseSubscriberTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* @covers ::onResponse
|
||||
* @dataProvider providerTestSerialization
|
||||
*/
|
||||
public function testSerialization($data, $expected_response = FALSE): void {
|
||||
$request = new Request();
|
||||
$route_match = new RouteMatch('test', new Route('/rest/test', ['_rest_resource_config' => 'rest_plugin'], ['_format' => 'json']));
|
||||
|
||||
$handler_response = new ResourceResponse($data);
|
||||
$resource_response_subscriber = $this->getFunctioningResourceResponseSubscriber($route_match);
|
||||
$event = new ResponseEvent(
|
||||
$this->prophesize(HttpKernelInterface::class)->reveal(),
|
||||
$request,
|
||||
HttpKernelInterface::MAIN_REQUEST,
|
||||
$handler_response
|
||||
);
|
||||
$resource_response_subscriber->onResponse($event);
|
||||
|
||||
// Content is a serialized version of the data we provided.
|
||||
$this->assertEquals($expected_response !== FALSE ? $expected_response : Json::encode($data), $event->getResponse()->getContent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides data to testSerialization().
|
||||
*/
|
||||
public static function providerTestSerialization() {
|
||||
return [
|
||||
// The default data for \Drupal\rest\ResourceResponse.
|
||||
'default' => [NULL, ''],
|
||||
'empty string' => [''],
|
||||
'simple string' => ['string'],
|
||||
// cSpell:disable-next-line
|
||||
'complex string' => ['Complex \ string $%^&@ with unicode ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΣὨ'],
|
||||
'empty array' => [[]],
|
||||
'numeric array' => [['test']],
|
||||
'associative array' => [['test' => 'foobar']],
|
||||
'boolean true' => [TRUE],
|
||||
'boolean false' => [FALSE],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the response format.
|
||||
*
|
||||
* Note this does *not* need to test formats being requested that are not
|
||||
* accepted by the server, because the routing system would have already
|
||||
* prevented those from reaching the controller.
|
||||
*
|
||||
* @covers ::getResponseFormat
|
||||
* @dataProvider providerTestResponseFormat
|
||||
*/
|
||||
public function testResponseFormat($methods, array $supported_response_formats, array $supported_request_formats, $request_format, array $request_headers, $request_body, $expected_response_format, $expected_response_content_type, $expected_response_content): void {
|
||||
foreach ($request_headers as $key => $value) {
|
||||
unset($request_headers[$key]);
|
||||
$key = strtoupper(str_replace('-', '_', $key));
|
||||
$request_headers[$key] = $value;
|
||||
}
|
||||
|
||||
foreach ($methods as $method) {
|
||||
$request = Request::create('/rest/test', $method, [], [], [], $request_headers, $request_body);
|
||||
// \Drupal\Core\StackMiddleware\NegotiationMiddleware normally takes care
|
||||
// of this so we'll hard code it here.
|
||||
if ($request_format) {
|
||||
$request->setRequestFormat($request_format);
|
||||
}
|
||||
|
||||
$route_requirements = $this->generateRouteRequirements($supported_response_formats, $supported_request_formats);
|
||||
|
||||
$route_match = new RouteMatch('test', new Route('/rest/test', ['_rest_resource_config' => $this->randomMachineName()], $route_requirements));
|
||||
|
||||
$resource_response_subscriber = new ResourceResponseSubscriber(
|
||||
$this->prophesize(SerializerInterface::class)->reveal(),
|
||||
$this->prophesize(RendererInterface::class)->reveal(),
|
||||
$route_match
|
||||
);
|
||||
|
||||
$this->assertSame($expected_response_format, $resource_response_subscriber->getResponseFormat($route_match, $request));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::onResponse
|
||||
* @covers ::getResponseFormat
|
||||
* @covers ::renderResponseBody
|
||||
* @covers ::flattenResponse
|
||||
*
|
||||
* @dataProvider providerTestResponseFormat
|
||||
*/
|
||||
public function testOnResponseWithCacheableResponse($methods, array $supported_response_formats, array $supported_request_formats, $request_format, array $request_headers, $request_body, $expected_response_format, $expected_response_content_type, $expected_response_content): void {
|
||||
foreach ($request_headers as $key => $value) {
|
||||
unset($request_headers[$key]);
|
||||
$key = strtoupper(str_replace('-', '_', $key));
|
||||
$request_headers[$key] = $value;
|
||||
}
|
||||
|
||||
foreach ($methods as $method) {
|
||||
$request = Request::create('/rest/test', $method, [], [], [], $request_headers, $request_body);
|
||||
// \Drupal\Core\StackMiddleware\NegotiationMiddleware normally takes care
|
||||
// of this so we'll hard code it here.
|
||||
if ($request_format) {
|
||||
$request->setRequestFormat($request_format);
|
||||
}
|
||||
|
||||
$route_requirements = $this->generateRouteRequirements($supported_response_formats, $supported_request_formats);
|
||||
|
||||
$route_match = new RouteMatch('test', new Route('/rest/test', ['_rest_resource_config' => $this->randomMachineName()], $route_requirements));
|
||||
|
||||
// The RequestHandler must return a ResourceResponseInterface object.
|
||||
$handler_response = new ResourceResponse(['REST' => 'Drupal']);
|
||||
$this->assertInstanceOf(ResourceResponseInterface::class, $handler_response);
|
||||
$this->assertInstanceOf(CacheableResponseInterface::class, $handler_response);
|
||||
|
||||
// The ResourceResponseSubscriber must then generate a response body and
|
||||
// transform it to a plain CacheableResponse object.
|
||||
$resource_response_subscriber = $this->getFunctioningResourceResponseSubscriber($route_match);
|
||||
$event = new ResponseEvent(
|
||||
$this->prophesize(HttpKernelInterface::class)->reveal(),
|
||||
$request,
|
||||
HttpKernelInterface::MAIN_REQUEST,
|
||||
$handler_response
|
||||
);
|
||||
$resource_response_subscriber->onResponse($event);
|
||||
$final_response = $event->getResponse();
|
||||
$this->assertNotInstanceOf(ResourceResponseInterface::class, $final_response);
|
||||
$this->assertInstanceOf(CacheableResponseInterface::class, $final_response);
|
||||
$this->assertSame($expected_response_content_type, $final_response->headers->get('Content-Type'));
|
||||
$this->assertEquals($expected_response_content, $final_response->getContent());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::onResponse
|
||||
* @covers ::getResponseFormat
|
||||
* @covers ::renderResponseBody
|
||||
* @covers ::flattenResponse
|
||||
*
|
||||
* @dataProvider providerTestResponseFormat
|
||||
*/
|
||||
public function testOnResponseWithUncacheableResponse($methods, array $supported_response_formats, array $supported_request_formats, $request_format, array $request_headers, $request_body, $expected_response_format, $expected_response_content_type, $expected_response_content): void {
|
||||
foreach ($request_headers as $key => $value) {
|
||||
unset($request_headers[$key]);
|
||||
$key = strtoupper(str_replace('-', '_', $key));
|
||||
$request_headers[$key] = $value;
|
||||
}
|
||||
|
||||
foreach ($methods as $method) {
|
||||
$request = Request::create('/rest/test', $method, [], [], [], $request_headers, $request_body);
|
||||
// \Drupal\Core\StackMiddleware\NegotiationMiddleware normally takes care
|
||||
// of this so we'll hard code it here.
|
||||
if ($request_format) {
|
||||
$request->setRequestFormat($request_format);
|
||||
}
|
||||
|
||||
$route_requirements = $this->generateRouteRequirements($supported_response_formats, $supported_request_formats);
|
||||
|
||||
$route_match = new RouteMatch('test', new Route('/rest/test', ['_rest_resource_config' => $this->randomMachineName()], $route_requirements));
|
||||
|
||||
// The RequestHandler must return a ResourceResponseInterface object.
|
||||
$handler_response = new ModifiedResourceResponse(['REST' => 'Drupal']);
|
||||
$this->assertInstanceOf(ResourceResponseInterface::class, $handler_response);
|
||||
$this->assertNotInstanceOf(CacheableResponseInterface::class, $handler_response);
|
||||
|
||||
// The ResourceResponseSubscriber must then generate a response body and
|
||||
// transform it to a plain Response object.
|
||||
$resource_response_subscriber = $this->getFunctioningResourceResponseSubscriber($route_match);
|
||||
$event = new ResponseEvent(
|
||||
$this->prophesize(HttpKernelInterface::class)->reveal(),
|
||||
$request,
|
||||
HttpKernelInterface::MAIN_REQUEST,
|
||||
$handler_response
|
||||
);
|
||||
$resource_response_subscriber->onResponse($event);
|
||||
$final_response = $event->getResponse();
|
||||
$this->assertNotInstanceOf(ResourceResponseInterface::class, $final_response);
|
||||
$this->assertNotInstanceOf(CacheableResponseInterface::class, $final_response);
|
||||
$this->assertSame($expected_response_content_type, $final_response->headers->get('Content-Type'));
|
||||
$this->assertEquals($expected_response_content, $final_response->getContent());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides data for testing the response format.
|
||||
*
|
||||
* @return array
|
||||
* 0. methods to test
|
||||
* 1. supported formats for route requirements
|
||||
* 2. request format
|
||||
* 3. request headers
|
||||
* 4. request body
|
||||
* 5. expected response format
|
||||
* 6. expected response content type
|
||||
* 7. expected response body
|
||||
*/
|
||||
public static function providerTestResponseFormat() {
|
||||
$json_encoded = Json::encode(['REST' => 'Drupal']);
|
||||
$xml_encoded = "<?xml version=\"1.0\"?>\n<response><REST>Drupal</REST></response>\n";
|
||||
|
||||
$safe_method_test_cases = [
|
||||
'safe methods: client requested format (JSON)' => [
|
||||
['GET', 'HEAD'],
|
||||
['xml', 'json'],
|
||||
[],
|
||||
'json',
|
||||
[],
|
||||
NULL,
|
||||
'json',
|
||||
'application/json',
|
||||
$json_encoded,
|
||||
],
|
||||
'safe methods: client requested format (XML)' => [
|
||||
['GET', 'HEAD'],
|
||||
['xml', 'json'],
|
||||
[],
|
||||
'xml',
|
||||
[],
|
||||
NULL,
|
||||
'xml',
|
||||
'text/xml',
|
||||
$xml_encoded,
|
||||
],
|
||||
'safe methods: client requested no format: response should use the first configured format (JSON)' => [
|
||||
['GET', 'HEAD'],
|
||||
['json', 'xml'],
|
||||
[],
|
||||
FALSE,
|
||||
[],
|
||||
NULL,
|
||||
'json',
|
||||
'application/json',
|
||||
$json_encoded,
|
||||
],
|
||||
'safe methods: client requested no format: response should use the first configured format (XML)' => [
|
||||
['GET', 'HEAD'],
|
||||
['xml', 'json'],
|
||||
[],
|
||||
FALSE,
|
||||
[],
|
||||
NULL,
|
||||
'xml',
|
||||
'text/xml',
|
||||
$xml_encoded,
|
||||
],
|
||||
];
|
||||
|
||||
$unsafe_method_bodied_test_cases = [
|
||||
'unsafe methods with response (POST, PATCH): client requested no format, response should use request body format (JSON)' => [
|
||||
['POST', 'PATCH'],
|
||||
['xml', 'json'],
|
||||
['xml', 'json'],
|
||||
FALSE,
|
||||
['Content-Type' => 'application/json'],
|
||||
$json_encoded,
|
||||
'json',
|
||||
'application/json',
|
||||
$json_encoded,
|
||||
],
|
||||
'unsafe methods with response (POST, PATCH): client requested no format, response should use request body format (XML)' => [
|
||||
['POST', 'PATCH'],
|
||||
['xml', 'json'],
|
||||
['xml', 'json'],
|
||||
FALSE,
|
||||
['Content-Type' => 'text/xml'],
|
||||
$xml_encoded,
|
||||
'xml',
|
||||
'text/xml',
|
||||
$xml_encoded,
|
||||
],
|
||||
'unsafe methods with response (POST, PATCH): client requested format other than request body format (JSON): response format should use requested format (XML)' => [
|
||||
['POST', 'PATCH'],
|
||||
['xml', 'json'],
|
||||
['xml', 'json'],
|
||||
'xml',
|
||||
['Content-Type' => 'application/json'],
|
||||
$json_encoded,
|
||||
'xml',
|
||||
'text/xml',
|
||||
$xml_encoded,
|
||||
],
|
||||
'unsafe methods with response (POST, PATCH): client requested format other than request body format (XML), but is allowed for the request body (JSON)' => [
|
||||
['POST', 'PATCH'],
|
||||
['xml', 'json'],
|
||||
['xml', 'json'],
|
||||
'json',
|
||||
['Content-Type' => 'text/xml'],
|
||||
$xml_encoded,
|
||||
'json',
|
||||
'application/json',
|
||||
$json_encoded,
|
||||
],
|
||||
'unsafe methods with response (POST, PATCH): client requested format other than request body format when only XML is allowed as a content type format' => [
|
||||
['POST', 'PATCH'],
|
||||
['xml'],
|
||||
['json'],
|
||||
'json',
|
||||
['Content-Type' => 'text/xml'],
|
||||
$xml_encoded,
|
||||
'json',
|
||||
'application/json',
|
||||
$json_encoded,
|
||||
],
|
||||
'unsafe methods with response (POST, PATCH): client requested format other than request body format when only JSON is allowed as a content type format' => [
|
||||
['POST', 'PATCH'],
|
||||
['json'],
|
||||
['xml'],
|
||||
'xml',
|
||||
['Content-Type' => 'application/json'],
|
||||
$json_encoded,
|
||||
'xml',
|
||||
'text/xml',
|
||||
$xml_encoded,
|
||||
],
|
||||
];
|
||||
|
||||
$unsafe_method_no_body_test_cases = [
|
||||
'unsafe methods without request bodies (DELETE): client requested no format, response should have the first acceptable format' => [
|
||||
['DELETE'],
|
||||
['xml', 'json'],
|
||||
['xml', 'json'],
|
||||
FALSE,
|
||||
['Content-Type' => 'application/json'],
|
||||
NULL,
|
||||
'xml',
|
||||
'text/xml',
|
||||
$xml_encoded,
|
||||
],
|
||||
'unsafe methods without request bodies (DELETE): client requested format (XML), response should have xml format' => [
|
||||
['DELETE'],
|
||||
['xml', 'json'],
|
||||
['xml', 'json'],
|
||||
'xml',
|
||||
['Content-Type' => 'application/json'],
|
||||
NULL,
|
||||
'xml',
|
||||
'text/xml',
|
||||
$xml_encoded,
|
||||
],
|
||||
'unsafe methods without request bodies (DELETE): client requested format (JSON), response should have json format' => [
|
||||
['DELETE'],
|
||||
['xml', 'json'],
|
||||
['xml', 'json'],
|
||||
'json',
|
||||
['Content-Type' => 'application/json'],
|
||||
NULL,
|
||||
'json',
|
||||
'application/json',
|
||||
$json_encoded,
|
||||
],
|
||||
];
|
||||
|
||||
return $safe_method_test_cases + $unsafe_method_bodied_test_cases + $unsafe_method_no_body_test_cases;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the resource response subscriber.
|
||||
*
|
||||
* @return \Drupal\rest\EventSubscriber\ResourceResponseSubscriber
|
||||
* A functioning ResourceResponseSubscriber.
|
||||
*/
|
||||
protected function getFunctioningResourceResponseSubscriber(RouteMatchInterface $route_match) {
|
||||
// Create a dummy of the renderer service.
|
||||
$renderer = $this->prophesize(RendererInterface::class);
|
||||
$renderer->executeInRenderContext(Argument::type(RenderContext::class), Argument::type('callable'))
|
||||
->will(function ($args) {
|
||||
$callable = $args[1];
|
||||
return $callable();
|
||||
});
|
||||
|
||||
// Instantiate the ResourceResponseSubscriber we will test.
|
||||
$resource_response_subscriber = new ResourceResponseSubscriber(
|
||||
new Serializer([], [new JsonEncoder(), new XmlEncoder()]),
|
||||
$renderer->reveal(),
|
||||
$route_match
|
||||
);
|
||||
|
||||
return $resource_response_subscriber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates route requirements based on supported formats.
|
||||
*
|
||||
* @param array $supported_response_formats
|
||||
* The supported response formats to add to the route requirements.
|
||||
* @param array $supported_request_formats
|
||||
* The supported request formats to add to the route requirements.
|
||||
*
|
||||
* @return array
|
||||
* An array of route requirements.
|
||||
*/
|
||||
protected function generateRouteRequirements(array $supported_response_formats, array $supported_request_formats): array {
|
||||
$route_requirements = [
|
||||
'_format' => implode('|', $supported_response_formats),
|
||||
];
|
||||
if (!empty($supported_request_formats)) {
|
||||
$route_requirements['_content_type_format'] = implode('|', $supported_request_formats);
|
||||
}
|
||||
|
||||
return $route_requirements;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\rest\Unit\Plugin\views\style;
|
||||
|
||||
use Drupal\rest\Plugin\views\display\RestExport;
|
||||
use Drupal\rest\Plugin\views\style\Serializer;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
use Drupal\views\ViewExecutable;
|
||||
use Prophecy\Argument;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\rest\Plugin\views\style\Serializer
|
||||
* @group rest
|
||||
*/
|
||||
class SerializerTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* The View instance.
|
||||
*
|
||||
* @var \Drupal\views\ViewExecutable|\PHPUnit\Framework\MockObject\MockObject
|
||||
*/
|
||||
protected $view;
|
||||
|
||||
/**
|
||||
* The RestExport display handler.
|
||||
*
|
||||
* @var \Drupal\rest\Plugin\views\display\RestExport|\PHPUnit\Framework\MockObject\MockObject
|
||||
*/
|
||||
protected $displayHandler;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->view = $this->getMockBuilder(ViewExecutable::class)
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
|
||||
// Make the view result empty so we don't have to mock the row plugin render
|
||||
// call.
|
||||
$this->view->result = [];
|
||||
|
||||
$this->displayHandler = $this->getMockBuilder(RestExport::class)
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
|
||||
$this->displayHandler->expects($this->any())
|
||||
->method('getContentType')
|
||||
->willReturn('json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that the symfony serializer receives style plugin from the render() method.
|
||||
*
|
||||
* @covers ::render
|
||||
*/
|
||||
public function testSerializerReceivesOptions(): void {
|
||||
$mock_serializer = $this->prophesize(SerializerInterface::class);
|
||||
|
||||
// This is the main expectation of the test. We want to make sure the
|
||||
// serializer options are passed to the SerializerInterface object.
|
||||
$mock_serializer->serialize([], 'json', Argument::that(function ($argument) {
|
||||
return isset($argument['views_style_plugin'])
|
||||
&& $argument['views_style_plugin'] instanceof Serializer;
|
||||
}))
|
||||
->willReturn('')
|
||||
->shouldBeCalled();
|
||||
|
||||
$view_serializer_style = new Serializer(
|
||||
[],
|
||||
'dummy_serializer',
|
||||
[],
|
||||
$mock_serializer->reveal(),
|
||||
['json', 'xml'],
|
||||
['json' => 'serialization', 'xml' => 'serialization']);
|
||||
$view_serializer_style->options = ['formats' => ['xml', 'json']];
|
||||
$view_serializer_style->view = $this->view;
|
||||
$view_serializer_style->displayHandler = $this->displayHandler;
|
||||
$view_serializer_style->render();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user