Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions tests/behat/bootstrap/FeatureContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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'));
}
}

}
16 changes: 16 additions & 0 deletions tests/behat/features/content_moderation_publish.feature
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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());
}

Comment on lines +180 to +204

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Missing test coverage for media update-after-authoring.

Given the guard in ModerationPolicyHook::entityPresave() (Line 53-55 of that file) applies to both the node and media branches, consider adding a companion test asserting the expected behavior when an API-authored media entity's moderation_state is changed on a subsequent update — this exercises the concern raised in ModerationPolicyHook.php.

🧰 Tools
🪛 PHPMD (2.15.0)

[error] 189-193: Avoid using static access to class '\Drupal\node\Entity\Node' in method 'testApiPagePublishAfterAuthoringIsHonoured'. (undefined)

(StaticAccess)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@web/modules/custom/do_content_api/tests/src/Kernel/Hook/ModerationPolicyHookTest.php`
around lines 180 - 204, Add a companion kernel test in ModerationPolicyHookTest
next to testApiPagePublishAfterAuthoringIsHonoured that covers the media path in
ModerationPolicyHook::entityPresave(). Create an API user with the content
authoring permission, create a media entity in draft as an authored save, then
update the same entity to published and assert the later save keeps the
published moderation_state and published status. Use the existing entityPresave
node/media branching as the target behavior to verify.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d5ff521. Added testApiMediaUpdateAfterAuthoringIsHonoured, which authors a media (coerced to published on create) then updates it to draft and asserts the later change is honoured - covering the media branch of the isNew guard alongside the node test.

/**
* 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.
*/
Expand Down