diff --git a/tests/behat/bootstrap/FeatureContext.php b/tests/behat/bootstrap/FeatureContext.php index 63639c70..70cb6576 100644 --- a/tests/behat/bootstrap/FeatureContext.php +++ b/tests/behat/bootstrap/FeatureContext.php @@ -37,6 +37,7 @@ use DrevOps\BehatSteps\ResponseTrait; use DrevOps\BehatSteps\WaitTrait; use Behat\Step\Given; +use Behat\Step\Then; use Drupal\DrupalExtension\Context\DrupalContext; use Drupal\node\NodeInterface; use Drupal\pathauto\PathautoState; @@ -109,4 +110,45 @@ public function contentSetPathAlias(string $content_type, string $title, string $node->save(); } + /** + * Assert that a content item is published. + * + * Checks both the published flag and the moderation state. The content is + * reloaded from storage because the UI action that published it ran in a + * separate web request, leaving this process's entity cache stale. + * + * @code + * Then the "civictheme_page" content "[TEST] Article" should be published + * @endcode + */ + #[Then('the :content_type content :title should be published')] + public function contentShouldBePublished(string $content_type, string $title): void { + // @phpstan-ignore globalDrupalDependencyInjection.useDependencyInjection + $storage = \Drupal::entityTypeManager()->getStorage('node'); + + $nids = $storage->getQuery() + ->accessCheck(FALSE) + ->condition('type', $content_type) + ->condition('title', $title) + ->execute(); + + if (count($nids) !== 1) { + throw new \RuntimeException(sprintf('Expected exactly one "%s" content item with the title "%s", but found %d.', $content_type, $title, count($nids))); + } + + $nid = (int) reset($nids); + $storage->resetCache([$nid]); + $node = $storage->load($nid); + + if (!$node instanceof NodeInterface) { + throw new \RuntimeException(sprintf('Unable to load "%s" content with the title "%s".', $content_type, $title)); + } + + $state = $node->get('moderation_state')->value; + + if ($state !== 'published' || !$node->isPublished()) { + throw new \RuntimeException(sprintf('Expected "%s" to be published, but its moderation state is "%s" and its published flag is %s.', $title, $state, $node->isPublished() ? 'true' : 'false')); + } + } + } diff --git a/tests/behat/features/content_moderation_publish.feature b/tests/behat/features/content_moderation_publish.feature new file mode 100644 index 00000000..6477c247 --- /dev/null +++ b/tests/behat/features/content_moderation_publish.feature @@ -0,0 +1,16 @@ +@api +Feature: Publishing API-authored content through the editorial UI + + As a reviewer who also holds the content authoring API permission + I want to publish a draft page through the editorial UI + So that content authored through the API can be reviewed and go live + + Scenario: An account with the content authoring API permission publishes a draft page + Given I am logged in as a user with the "access content, access administration pages, access content overview, edit any civictheme_page content, view any unpublished content, use content authoring api, use civictheme_editorial transition create_new_draft, use civictheme_editorial transition publish" permissions + And the following "civictheme_page" content: + | title | moderation_state | field_c_n_banner_type | field_c_n_banner_theme | field_c_n_banner_blend_mode | field_c_n_vertical_spacing | + | API draft to publish | draft | large | inherit | normal | both | + When I visit the "civictheme_page" content edit page with the title "API draft to publish" + And I select "Published" from "moderation_state[0][state]" + And I press "Save" + Then the "civictheme_page" content "API draft to publish" should be published diff --git a/web/modules/custom/do_content_api/src/Hook/ModerationPolicyHook.php b/web/modules/custom/do_content_api/src/Hook/ModerationPolicyHook.php index e301b5fb..05135184 100644 --- a/web/modules/custom/do_content_api/src/Hook/ModerationPolicyHook.php +++ b/web/modules/custom/do_content_api/src/Hook/ModerationPolicyHook.php @@ -46,8 +46,16 @@ public function entityPresave(EntityInterface $entity): void { return; } + // The policy governs authoring, which is entity creation. A later + // moderation change - a reviewer publishing the draft - is an update and + // must be left untouched, otherwise authored content could never be + // published. + if (!$entity->isNew()) { + return; + } + // Pages authored through the API never go live directly; a human reviews - // and publishes them, so they are always forced back to a draft. + // and publishes them, so they are forced to draft on creation. if ($entity->getEntityTypeId() === 'node') { $entity->set('moderation_state', 'draft'); diff --git a/web/modules/custom/do_content_api/tests/src/Kernel/Hook/ModerationPolicyHookTest.php b/web/modules/custom/do_content_api/tests/src/Kernel/Hook/ModerationPolicyHookTest.php index 7a8ad913..03bbc3c5 100644 --- a/web/modules/custom/do_content_api/tests/src/Kernel/Hook/ModerationPolicyHookTest.php +++ b/web/modules/custom/do_content_api/tests/src/Kernel/Hook/ModerationPolicyHookTest.php @@ -64,6 +64,7 @@ protected function setUp(): void { $this->installEntitySchema('file'); $this->installEntitySchema('content_moderation_state'); $this->installSchema('file', ['file_usage']); + $this->installSchema('node', ['node_access']); $this->installConfig(['filter']); NodeType::create(['type' => 'civictheme_page', 'name' => 'Page'])->save(); @@ -176,6 +177,55 @@ public function testNonApiPageUnaffected(): void { $this->assertTrue($node->isPublished()); } + /** + * Tests that an API actor can publish a page it previously authored. + */ + public function testApiPagePublishAfterAuthoringIsHonoured(): void { + $api_user = $this->createUser(['use content authoring api']); + $this->assertNotFalse($api_user); + $this->setCurrentUser($api_user); + + // Authoring (create) is forced to draft by the policy. + $node = Node::create([ + 'type' => 'civictheme_page', + 'title' => '[TEST] Authored then published', + 'moderation_state' => 'draft', + ]); + $node->save(); + $this->assertSame('draft', $node->get('moderation_state')->value); + + // A later publish is an update, not authoring, so it must be honoured. + $node->set('moderation_state', 'published'); + $node->save(); + + $this->assertSame('published', $node->get('moderation_state')->value); + $this->assertTrue($node->isPublished()); + } + + /** + * Tests that a later moderation change to an API-authored media is honoured. + */ + public function testApiMediaUpdateAfterAuthoringIsHonoured(): void { + $api_user = $this->createUser(['use content authoring api']); + $this->assertNotFalse($api_user); + $this->setCurrentUser($api_user); + + // Authoring (create) forces media to published. + $media = Media::create([ + 'bundle' => 'civictheme_image', + 'name' => '[TEST] Authored then updated', + 'moderation_state' => 'draft', + ]); + $media->save(); + $this->assertSame('published', $media->get('moderation_state')->value); + + // A later state change is an update, not authoring, so it is honoured. + $media->set('moderation_state', 'draft'); + $media->save(); + + $this->assertSame('draft', $media->get('moderation_state')->value); + } + /** * Tests that the superuser is treated as an ordinary administrator. */