diff --git a/jsonapi.api.php b/jsonapi.api.php index 92b8fc1..1a09422 100644 --- a/jsonapi.api.php +++ b/jsonapi.api.php @@ -71,6 +71,67 @@ * work automatically for both the JSON API module and core's REST module. * * + * @section revisions Revisions + * The JSON API module exposes entity revisions in a manner inspired by RFC5829: + * Link Relation Types for Simple Version Navigation between Web Resources. + * + * In doing so, JSON API module should be maximally compatible with other + * systems and should minimize the "Drupalisms" that a developer building + * against a JSON API implementation will be required to know. + * + * A "version" in the JSON API module is any revision that was previously, or is + * currently, a default revision. Not all revisions are considered to be a + * "version". Revisions that are not marked as a "default" revision are + * considered "working copies" since they are not usually publicly available + * and are the revisions to which most new work is applied. + * + * When the Content Moderation module is installed, it is possible that the + * most recent default revision is *not* the latest revision. + * + * Requesting a resource version is done via a URL query parameter. It has the + * following form: + * + * @code + * version-identifier + * __|__ + * / \ + * ?resource_version=foo:bar + * \_/ \_/ + * | | + * version-negotiator | + * version-argument + * @endcode + * + * A version identifier is a string with enough information to load a + * particular revision. The version negotiator component names the negotiation + * mechanism for loading a revision. Currently, this can be either @code id + * @endcode or @code rel @endcode. The @code id @endcode negotiator takes a + * version argument which is the desired revision ID. The @code rel @endcode + * negotiator takes a version argument which is either the string @code + * latest-version @encode or the string @code latest-version @encode. + * + * In future, other negotiators may be developed. For instance, a negotiator + * which is timestamp or workspace based. + * + * To illustrate how a particular entity revision is requested, imagine a node + * that has a "Published" revision and a subsequent "Draft" revision. + * + * Using JSON API, one could request the "Published" node by requesting + * @code /jsonapi/node/page/{{uuid}}?resource_version=rel:latest-version + * @endcode. + * + * To preview an entity that is still a work-in-progress (i.e. the "Draft" + * revision) one could request + * @code /jsonapi/node/page/{{uuid}}?resource_version=rel:working-copy + * @endcode. + * + * To request a specific revision ID, one can request + * @code /jsonapi/node/page/{{uuid}}?resource_version=id:{{revision_id}} + * @endcode. + * + * @see https://tools.ietf.org/html/rfc5829 + * + * * @section api API * The JSON API module provides an HTTP API that adheres to the JSON API * specification. @@ -129,7 +190,7 @@ * To help develop compatible clients, every response indicates the version of * the JSON API specification used under its "jsonapi" key. Future releases * *may* increment the minor version number if the module implements features of - * a later specification. Remember that he specification stipulates that future + * a later specification. Remember that the specification stipulates that future * versions *will* remain backwards compatible as only additions may be * released. * diff --git a/src/Plugin/VersionNegotiation/VersionByRel.php b/src/Plugin/VersionNegotiation/VersionByRel.php index 5d919d4..3499835 100644 --- a/src/Plugin/VersionNegotiation/VersionByRel.php +++ b/src/Plugin/VersionNegotiation/VersionByRel.php @@ -28,14 +28,25 @@ class VersionByRel extends PluginNegotiationBase implements ContainerFactoryPlug const NEGOTIATOR_NAME = 'rel'; /** - * Version argument which loads the latest revision. + * Version argument which loads the revision known to be the "working copy". + * + * In Drupal terms, a "working copy" is the latest revision. It may or may not + * be a "default" revision. This revision is the working copy because it is + * the revision to which new work will be applied. In other words, it denotes + * the most recent revision which might be considered a work-in-progress. * * @var string */ const WORKING_COPY = 'working-copy'; /** - * Version argument which loads the latest default revision. + * Version argument which loads the revision known to be the "latest version". + * + * In Drupal terms, the "latest version" is the latest "default" revision. It + * may or may not have later revisions after it, as long as none of them are + * "default" revisions. This revision is the latest version because it is the + * last revision where work was considered finished. Typically, this means + * that it is the most recent "published" revision. * * @var string */ @@ -50,7 +61,7 @@ class VersionByRel extends PluginNegotiationBase implements ContainerFactoryPlug case static::WORKING_COPY: /* @var \Drupal\Core\Entity\RevisionableStorageInterface $entity_storage */ $entity_storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId()); - return static::ensureVersionFound($entity_storage->getLatestRevisionId($entity->id())); + return static::ensureVersionExists($entity_storage->getLatestRevisionId($entity->id())); case static::LATEST_VERSION: // The already loaded revision will be the latest version by default. diff --git a/src/ResourceType/ResourceTypeRepository.php b/src/ResourceType/ResourceTypeRepository.php index 1fd68c5..3d29a68 100644 --- a/src/ResourceType/ResourceTypeRepository.php +++ b/src/ResourceType/ResourceTypeRepository.php @@ -13,7 +13,6 @@ use Drupal\Core\Entity\EntityTypeBundleInfoInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Entity\FieldableEntityInterface; -use Drupal\Core\Entity\RevisionableStorageInterface; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\TypedData\DataReferenceTargetDefinition; use Symfony\Component\HttpKernel\Exception\PreconditionFailedHttpException; @@ -300,8 +299,7 @@ class ResourceTypeRepository implements ResourceTypeRepositoryInterface { * TRUE if the entity type is versionable, FALSE otherwise. */ protected static function isVersionableResourceType(EntityTypeInterface $entity_type) { - $is_versionable_storage = is_subclass_of($entity_type->getStorageClass(), RevisionableStorageInterface::class); - return $is_versionable_storage && $entity_type->isRevisionable(); + return $entity_type->isRevisionable(); } /** diff --git a/src/Revisions/Annotation/VersionNegotiation.php b/src/Revisions/Annotation/VersionNegotiation.php index 6a48aae..9cade53 100644 --- a/src/Revisions/Annotation/VersionNegotiation.php +++ b/src/Revisions/Annotation/VersionNegotiation.php @@ -5,7 +5,7 @@ namespace Drupal\jsonapi\Revisions\Annotation; use Drupal\Component\Annotation\Plugin; /** - * Defines an revision ID negoriation annotation object. + * Defines an version negotiation plugin annotation object. * * Plugin Namespace: Plugin\VersionNegotiation. * diff --git a/src/Revisions/PluginNegotiationBase.php b/src/Revisions/PluginNegotiationBase.php index fd3017b..8cd60e5 100644 --- a/src/Revisions/PluginNegotiationBase.php +++ b/src/Revisions/PluginNegotiationBase.php @@ -9,7 +9,7 @@ use Drupal\Core\Plugin\PluginBase; use Symfony\Component\DependencyInjection\ContainerInterface; /** - * Base implementation for revision negotiator plugins. + * Base implementation for version negotiation plugins. * * @internal */ @@ -23,7 +23,7 @@ abstract class PluginNegotiationBase extends PluginBase implements ContainerFact protected $entityTypeManager; /** - * Constructs a Drupal\Component\Plugin\PluginBase object. + * Constructs a VersionNegotiation plugin. * * @param array $configuration * A configuration array containing information about the plugin instance. @@ -87,7 +87,7 @@ abstract class PluginNegotiationBase extends PluginBase implements ContainerFact */ protected function loadRevision(EntityInterface $entity, $revision_id) { $storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId()); - $revision = static::ensureVersionFound($storage->loadRevision($revision_id)); + $revision = static::ensureVersionExists($storage->loadRevision($revision_id)); if ($revision->id() !== $entity->id()) { throw new VersionNotFoundException(sprintf('The requested resource does not have a version with ID %s.', $revision_id)); } @@ -107,7 +107,7 @@ abstract class PluginNegotiationBase extends PluginBase implements ContainerFact * Thrown if the given value is NULL, meaning the requested version was not * found. */ - protected static function ensureVersionFound($revision) { + protected static function ensureVersionExists($revision) { if (is_null($revision)) { throw new VersionNotFoundException(); } diff --git a/src/Revisions/ResourceVersionRouteEnhancer.php b/src/Revisions/ResourceVersionRouteEnhancer.php index 15807e6..ca601a0 100644 --- a/src/Revisions/ResourceVersionRouteEnhancer.php +++ b/src/Revisions/ResourceVersionRouteEnhancer.php @@ -55,16 +55,16 @@ class ResourceVersionRouteEnhancer implements EnhancerInterface { * * @var \Drupal\jsonapi\Revisions\VersionNegotiationManager */ - protected $revisionNegotiatorManager; + protected $versionNegotiationManager; /** * ResourceVersionRouteEnhancer constructor. * - * @param \Drupal\jsonapi\Revisions\VersionNegotiationManager $revision_id_negotiation_manager - * The negotiator plugin manager. + * @param \Drupal\jsonapi\Revisions\VersionNegotiationManager $version_negotiation_manager + * The version negotiator. */ - public function __construct(VersionNegotiationManager $revision_id_negotiation_manager) { - $this->revisionNegotiatorManager = $revision_id_negotiation_manager; + public function __construct(VersionNegotiationManager $version_negotiation_manager) { + $this->versionNegotiationManager = $version_negotiation_manager; } /** @@ -89,7 +89,7 @@ class ResourceVersionRouteEnhancer implements EnhancerInterface { $resource_version_identifier = $request->query->get(static::RESOURCE_VERSION_QUERY_PARAMETER); - if (!static::validResourceVersion($resource_version_identifier)) { + if (!static::isValidResourceVersionIdentifier($resource_version_identifier)) { $cacheability = (new CacheableMetadata())->addCacheContexts(['url.query_args:' . ResourceVersionRouteEnhancer::RESOURCE_VERSION_QUERY_PARAMETER]); $message = sprintf('A resource version identifier was provided in an invalid format: `%s`', $resource_version_identifier); throw new CacheableBadRequestHttpException($cacheability, $message); @@ -97,6 +97,7 @@ class ResourceVersionRouteEnhancer implements EnhancerInterface { // Determine if the request is for a collection resource. if ($defaults[RouteObjectInterface::CONTROLLER_NAME] === Routes::CONTROLLER_SERVICE_NAME . ':getCollection') { + // @todo: If VersionNegotiation plugins become a public API, then this logic will need to be moved into the relevant plugins. $latest_version_identifier = VersionByRel::NEGOTIATOR_NAME . VersionNegotiationManager::SEPARATOR . VersionByRel::LATEST_VERSION; $working_copy_identifier = VersionByRel::NEGOTIATOR_NAME . VersionNegotiationManager::SEPARATOR . VersionByRel::WORKING_COPY; // 'latest-version' and 'working-copy' are the only acceptable version @@ -109,10 +110,7 @@ class ResourceVersionRouteEnhancer implements EnhancerInterface { ])); throw new CacheableBadRequestHttpException($cacheability, $message); } - // Whether the collection to be loaded should include only working-copies, - // which in Drupal terms, means that the latest revisions should be - // loaded. If the version identifier was 'latest-version', nothing needs - // to be done since that is the default behavior. + // Whether the collection to be loaded should include only working copies. $defaults[static::WORKING_COPIES_REQUESTED] = $resource_version_identifier === $working_copy_identifier; return $defaults; } @@ -121,7 +119,7 @@ class ResourceVersionRouteEnhancer implements EnhancerInterface { $entity = $defaults['entity']; /** @var \Drupal\jsonapi\Revisions\VersionNegotiationInterface $negotiator */ - $resolved_revision = $this->revisionNegotiatorManager->getRevision($entity, $resource_version_identifier); + $resolved_revision = $this->versionNegotiationManager->getRevision($entity, $resource_version_identifier); return ['entity' => $resolved_revision] + $defaults; } @@ -134,10 +132,8 @@ class ResourceVersionRouteEnhancer implements EnhancerInterface { * @return bool * TRUE if the received resource version value is valid, FALSE otherwise. */ - protected static function validResourceVersion($resource_version) { - $result = preg_match(static::RESOURCE_VERSION_PARAM_VALIDATOR, $resource_version); - assert($result || $result === 0, 'Regex failed.'); - return (bool) $result; + protected static function isValidResourceVersionIdentifier($resource_version) { + return preg_match(static::RESOURCE_VERSION_PARAM_VALIDATOR, $resource_version) === 1; } } diff --git a/src/Revisions/VersionNegotiationInterface.php b/src/Revisions/VersionNegotiationInterface.php index 24dac46..30d1de1 100644 --- a/src/Revisions/VersionNegotiationInterface.php +++ b/src/Revisions/VersionNegotiationInterface.php @@ -5,10 +5,10 @@ namespace Drupal\jsonapi\Revisions; use Drupal\Core\Entity\EntityInterface; /** - * Defines the common interface for all Revision ID negotiation classes. + * Defines the common interface for all version negotiation plugins. * + * @see \Drupal\jsonapi\Revisions\Annotation\VersionNegotiation * @see \Drupal\jsonapi\Revisions\VersionNegotiationManager - * @see \Drupal\jsonapi\Revisions\VersionNegotiation * @see plugin_api * @internal */ @@ -17,6 +17,17 @@ interface VersionNegotiationInterface { /** * Gets the identified revision. * + * The JSON API module exposes revisions in terms of RFC5829. As such, the + * public API always refers to "versions" and "working copies" instead of + * "revisions". There are multiple ways to request a specific revision. For + * example, one might like to load a particular revision by its ID. On the + * other hand, it may be useful if an HTTP consumer is able to always request + * the "latest version" regardless of its ID. It is possible to imagine other + * scenarios as well, like fetching a revision based on a date or time. + * + * Each VersionNegotiation plugin provides one of these strategies and is able + * to map a version argument to an existing revision. + * * @param \Drupal\Core\Entity\EntityInterface $entity * The entity for which a revision should be resolved. * @param string $version_argument diff --git a/src/Revisions/VersionNegotiationManager.php b/src/Revisions/VersionNegotiationManager.php index b58d710..0e67a57 100644 --- a/src/Revisions/VersionNegotiationManager.php +++ b/src/Revisions/VersionNegotiationManager.php @@ -10,9 +10,10 @@ use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Http\Exception\CacheableBadRequestHttpException; use Drupal\Core\Http\Exception\CacheableNotFoundHttpException; use Drupal\Core\Plugin\DefaultPluginManager; +use Drupal\jsonapi\Revisions\Annotation\VersionNegotiation; /** - * Provides an Revision ID negotiation plugin manager. + * Provides an version negotiation plugin manager. * * @see \Drupal\jsonapi\Revisions\Annotation\VersionNegotiation * @see \Drupal\jsonapi\Revisions\VersionNegotiationInterface @@ -39,14 +40,10 @@ class VersionNegotiationManager extends DefaultPluginManager { * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler * The module handler to invoke the alter hook with. */ - public function __construct( - \Traversable $namespaces, - CacheBackendInterface $cache_backend, - ModuleHandlerInterface $module_handler - ) { + public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) { // Only discover plugins provided by JSON API. This is an internal API. $internal = new \ArrayIterator(array_intersect_key(iterator_to_array($namespaces), array_flip(['Drupal\jsonapi']))); - parent::__construct('Plugin/VersionNegotiation', $internal, $module_handler, 'Drupal\jsonapi\Revisions\VersionNegotiationInterface', 'Drupal\jsonapi\Revisions\Annotation\VersionNegotiation'); + parent::__construct('Plugin/VersionNegotiation', $internal, $module_handler, VersionNegotiationInterface::class, VersionNegotiation::class); $this->setCacheBackend($cache_backend, 'revision_id_negotiation_info_plugins'); } @@ -69,8 +66,8 @@ class VersionNegotiationManager extends DefaultPluginManager { public function getRevision(EntityInterface $entity, $resource_version_identifier) { try { list($version_negotiator, $version_argument) = explode(VersionNegotiationManager::SEPARATOR, $resource_version_identifier, 2); - /* @var \Drupal\jsonapi\Revisions\VersionNegotiationInterface $negotiator */ $negotiator = $this->createInstance($version_negotiator); + assert($negotiator instanceof VersionNegotiationInterface); return $negotiator->getRevision($entity, $version_argument); } catch (VersionNotFoundException $exception) { diff --git a/tests/src/Functional/ResourceTestBase.php b/tests/src/Functional/ResourceTestBase.php index 87b6c60..be12c5f 100644 --- a/tests/src/Functional/ResourceTestBase.php +++ b/tests/src/Functional/ResourceTestBase.php @@ -2620,12 +2620,8 @@ abstract class ResourceTestBase extends BrowserTestBase { ])->setLabel('Revisionable text field')->setTranslatable(FALSE); $field_config->save(); - // Reload entity so that it has the new field. Some entity types are not - // stored, hence they cannot be reloaded. - if (!$entity = $this->entityStorage->loadUnchanged($this->entity->id())) { - assert(FALSE); - return; - } + // Reload entity so that it has the new field. + $entity = $this->entityStorage->loadUnchanged($this->entity->id()); // Set up test data. /* @var \Drupal\Core\Entity\FieldableEntityInterface $entity */ @@ -2641,14 +2637,14 @@ abstract class ResourceTestBase extends BrowserTestBase { // @todo Remove line below in favor of commented line in https://www.drupal.org/project/jsonapi/issues/2878463. $url = Url::fromRoute(sprintf('jsonapi.%s.individual', static::$resourceTypeName), ['entity' => $this->entity->uuid()]); /* $url = $this->entity->toUrl('jsonapi'); */ - $original_url = clone $url; - $original_url->setOption('query', ['resource_version' => "id:$original_revision_id"]); - $latest_url = clone $url; - $latest_url->setOption('query', ['resource_version' => "id:$latest_revision_id"]); - $latest_version_url = clone $url; - $latest_version_url->setOption('query', ['resource_version' => 'rel:latest-version']); - $working_copy_url = clone $url; - $working_copy_url->setOption('query', ['resource_version' => 'rel:working-copy']); + $original_revision_id_url = clone $url; + $original_revision_id_url->setOption('query', ['resource_version' => "id:$original_revision_id"]); + $latest_revision_id_url = clone $url; + $latest_revision_id_url->setOption('query', ['resource_version' => "id:$latest_revision_id"]); + $rel_latest_version_url = clone $url; + $rel_latest_version_url->setOption('query', ['resource_version' => 'rel:latest-version']); + $rel_working_copy_url = clone $url; + $rel_working_copy_url->setOption('query', ['resource_version' => 'rel:working-copy']); $revision_id_key = $this->entity->getEntityType()->getKey('revision'); $published_key = $this->entity->getEntityType()->getKey('published'); $revision_translation_affected_key = $this->entity->getEntityType()->getKey('revision_translation_affected'); @@ -2668,14 +2664,15 @@ abstract class ResourceTestBase extends BrowserTestBase { $this->assertResourceErrorResponse(403, $message, $url, $actual_response, '/data', $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), FALSE, 'MISS'); // Ensure that targeting a revision does not bypass access. - $actual_response = $this->request('GET', $original_url, $request_options); + $actual_response = $this->request('GET', $original_revision_id_url, $request_options); $expected_cacheability = $this->getExpectedUnauthorizedAccessCacheability(); $this->assertResourceErrorResponse(403, $message, $url, $actual_response, '/data', $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), FALSE, 'MISS'); $this->setUpAuthorization('GET'); - // Ensure the normal URL returns the default revision, which is always the - // latest revision when content_moderation is not installed. + // Ensure that the URL without a `resource_version` query parameter returns + // the default revision. This is always the latest revision when + // content_moderation is not installed. $actual_response = $this->request('GET', $url, $request_options); $expected_document = $this->getExpectedDocument(); // Resource objects always link to their specific revision by revision ID. @@ -2685,33 +2682,33 @@ abstract class ResourceTestBase extends BrowserTestBase { $expected_cache_contexts = $this->getExpectedCacheContexts(); $this->assertResourceResponse(200, $expected_document, $actual_response, $expected_cache_tags, $expected_cache_contexts, FALSE, 'MISS'); // Fetch the same revision using its revision ID. - $actual_response = $this->request('GET', $latest_url, $request_options); + $actual_response = $this->request('GET', $latest_revision_id_url, $request_options); // The top-level document object's `self` link should always link to the // request URL. - $expected_document['links']['self']['href'] = $latest_url->setAbsolute()->toString(); + $expected_document['links']['self']['href'] = $latest_revision_id_url->setAbsolute()->toString(); $this->assertResourceResponse(200, $expected_document, $actual_response, $expected_cache_tags, $expected_cache_contexts, FALSE, 'MISS'); // Ensure dynamic cache HIT on second request when using a version // negotiator. - $actual_response = $this->request('GET', $latest_url, $request_options); + $actual_response = $this->request('GET', $latest_revision_id_url, $request_options); $this->assertResourceResponse(200, $expected_document, $actual_response, $expected_cache_tags, $expected_cache_contexts, FALSE, 'HIT'); // Fetch the same revision using the `latest-version` link relation type // negotiator. Without content_moderation, this is always the most recent // revision. - $actual_response = $this->request('GET', $latest_version_url, $request_options); - $expected_document['links']['self']['href'] = $latest_version_url->setAbsolute()->toString(); + $actual_response = $this->request('GET', $rel_latest_version_url, $request_options); + $expected_document['links']['self']['href'] = $rel_latest_version_url->setAbsolute()->toString(); $this->assertResourceResponse(200, $expected_document, $actual_response, $expected_cache_tags, $expected_cache_contexts, FALSE, 'MISS'); // Fetch the same revision using the `working-copy` link relation type // negotiator. Without content_moderation, this is always the most recent // revision. - $actual_response = $this->request('GET', $working_copy_url, $request_options); - $expected_document['links']['self']['href'] = $working_copy_url->setAbsolute()->toString(); + $actual_response = $this->request('GET', $rel_working_copy_url, $request_options); + $expected_document['links']['self']['href'] = $rel_working_copy_url->setAbsolute()->toString(); $this->assertResourceResponse(200, $expected_document, $actual_response, $expected_cache_tags, $expected_cache_contexts, FALSE, 'MISS'); // Fetch the prior revision. - $actual_response = $this->request('GET', $original_url, $request_options); + $actual_response = $this->request('GET', $original_revision_id_url, $request_options); $expected_document['data']['attributes'][$revision_id_key] = $original_revision_id; $expected_document['data']['attributes']['field_revisionable_number'] = 42; - $expected_document['links']['self']['href'] = $original_url->setAbsolute()->toString(); + $expected_document['links']['self']['href'] = $original_revision_id_url->setAbsolute()->toString(); $this->assertResourceResponse(200, $expected_document, $actual_response, $expected_cache_tags, $expected_cache_contexts, FALSE, 'MISS'); // Install content_moderation module. @@ -2723,35 +2720,35 @@ abstract class ResourceTestBase extends BrowserTestBase { $workflow->save(); // Ensure the test entity has content_moderation fields attached to it. - /* @var \Drupal\Core\Entity\FieldableEntityInterface $entity */ - /* @var \Drupal\Core\Entity\TranslatableRevisionableInterface $entity */ + /* @var \Drupal\Core\Entity\FieldableEntityInterface|\Drupal\Core\Entity\TranslatableRevisionableInterface $entity */ $entity = $this->entityStorage->load($entity->id()); - // Set the 'published' moderation state on the test entity. + // Set the published moderation state on the test entity. $entity->set('moderation_state', 'published'); $entity->setNewRevision(); $entity->save(); $published_revision_id = (int) $entity->getRevisionId(); - // Fetch the 'published' revision by using the `latest-version` link - // relation type negotiator. With content_moderation, this is now the most - // recent revision where the moderation state was the 'default' one. - $actual_response = $this->request('GET', $latest_version_url, $request_options); + // Fetch the published revision by using the `rel` version negotiator and + // the `latest-version` version argument. With content_moderation, this is + // now the most recent revision where the moderation state was the 'default' + // one. + $actual_response = $this->request('GET', $rel_latest_version_url, $request_options); $expected_document['data']['attributes'][$revision_id_key] = $published_revision_id; $expected_document['data']['attributes']['moderation_state'] = 'published'; $expected_document['data']['attributes'][$published_key] = TRUE; $expected_document['data']['attributes']['field_revisionable_number'] = 99; - $expected_document['links']['self']['href'] = $latest_version_url->setAbsolute()->toString(); + $expected_document['links']['self']['href'] = $rel_latest_version_url->setAbsolute()->toString(); $expected_document['data']['attributes'][$revision_translation_affected_key] = $entity->isRevisionTranslationAffected(); $this->assertResourceResponse(200, $expected_document, $actual_response, $expected_cache_tags, $expected_cache_contexts, FALSE, 'MISS'); - // Fetch the 'published' revision by using the `working-copy` link - // relation type negotiator. With content_moderation, this is always the - // most recent revision regardless of moderation state. - $actual_response = $this->request('GET', $working_copy_url, $request_options); - $expected_document['links']['self']['href'] = $working_copy_url->setAbsolute()->toString(); + // Fetch the published revision by using the `working-copy` version + // argument. With content_moderation, this is always the most recent + // revision regardless of moderation state. + $actual_response = $this->request('GET', $rel_working_copy_url, $request_options); + $expected_document['links']['self']['href'] = $rel_working_copy_url->setAbsolute()->toString(); $this->assertResourceResponse(200, $expected_document, $actual_response, $expected_cache_tags, $expected_cache_contexts, FALSE, 'MISS'); - // Set the 'draft' moderation state on the test entity. + // Move the entity to its draft moderation state. $entity->set('field_revisionable_number', 42); $entity->set('moderation_state', 'draft'); $entity->setNewRevision(); @@ -2759,20 +2756,22 @@ abstract class ResourceTestBase extends BrowserTestBase { $draft_revision_id = (int) $entity->getRevisionId(); // The `latest-version` link should *still* reference the same revision - // since 'draft' is not a default revision. - $actual_response = $this->request('GET', $latest_version_url, $request_options); - $expected_document['links']['self']['href'] = $latest_version_url->setAbsolute()->toString(); + // since a draft is not a default revision. + $actual_response = $this->request('GET', $rel_latest_version_url, $request_options); + $expected_document['links']['self']['href'] = $rel_latest_version_url->setAbsolute()->toString(); $this->assertResourceResponse(200, $expected_document, $actual_response, $expected_cache_tags, $expected_cache_contexts, FALSE, 'MISS'); - // Now, the `working-copy` link should reference the 'draft' revision. This + // Now, the `working-copy` link should reference the draft revision. This // is significant because without content_moderation, the two responses - // have still been the same. Next, access is checked before any special - // permissions are granted. This asserts a 403 forbidden if the user is not - // allowed to see unpublished content. + // would still been the same. + // + // Access is checked before any special permissions are granted. This + // asserts a 403 forbidden if the user is not allowed to see unpublished + // content. $result = $entity->access('view', $this->account, TRUE); if (!$result->isAllowed()) { - $actual_response = $this->request('GET', $working_copy_url, $request_options); + $actual_response = $this->request('GET', $rel_working_copy_url, $request_options); $expected_cacheability = $this->getExpectedUnauthorizedAccessCacheability(); - $expected_cache_tags = array_unique(array_merge($expected_cacheability->getCacheTags(), $entity->getCacheTags())); + $expected_cache_tags = Cache::mergeTags($expected_cacheability->getCacheTags(), $entity->getCacheTags()); $expected_cache_contexts = $expected_cacheability->getCacheContexts(); $message = 'The current user is not allowed to GET the selected resource.'; if ($result instanceof AccessResultReasonInterface && ($reason = $result->getReason()) && !empty($reason)) { @@ -2785,12 +2784,12 @@ abstract class ResourceTestBase extends BrowserTestBase { } // Now, the `working-copy` link should be latest revision and be accessible. - $actual_response = $this->request('GET', $working_copy_url, $request_options); + $actual_response = $this->request('GET', $rel_working_copy_url, $request_options); $expected_document['data']['attributes'][$revision_id_key] = $draft_revision_id; $expected_document['data']['attributes']['moderation_state'] = 'draft'; $expected_document['data']['attributes'][$published_key] = FALSE; $expected_document['data']['attributes']['field_revisionable_number'] = 42; - $expected_document['links']['self']['href'] = $working_copy_url->setAbsolute()->toString(); + $expected_document['links']['self']['href'] = $rel_working_copy_url->setAbsolute()->toString(); $expected_document['data']['attributes'][$revision_translation_affected_key] = $entity->isRevisionTranslationAffected(); $expected_cache_tags = $this->getExpectedCacheTags(); $expected_cache_contexts = $this->getExpectedCacheContexts();