fix: global library edits check private and scheduled pages, name the backup on failure (P8a, P14)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ namespace LIT\MCP\Tests\Unit\Integrations\Divi\Library;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use LIT\MCP\Abilities\Rev;
|
||||
use LIT\MCP\Abilities\WriteFailed;
|
||||
use LIT\MCP\Integrations\Divi\Library\GlobalEdits;
|
||||
use LIT\MCP\Integrations\Divi\Library\GlobalUsages;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviDeleteAbility;
|
||||
@@ -17,6 +18,7 @@ use LIT\MCP\Integrations\Divi\Pages\DiviWriter;
|
||||
use LIT\MCP\Integrations\Divi\Pages\SaveScope;
|
||||
use LIT\MCP\Tests\Support\DiviFixtures;
|
||||
use LIT\MCP\Tests\Support\DiviWriterFixture;
|
||||
use LIT\MCP\Tests\Support\FakeChangeLog;
|
||||
use LIT\MCP\Tests\Support\LibraryFixtures;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use Mockery;
|
||||
@@ -30,8 +32,14 @@ final class GlobalEditsTest extends TestCase
|
||||
|
||||
private int $backups = 0;
|
||||
|
||||
/** @var list<int> Published pages that show the global entry (ruling P8). */
|
||||
private array $published = [1053];
|
||||
/** Change log of the writer; null = DiviWriterFixture::log() (records only). */
|
||||
private ?FakeChangeLog $log = null;
|
||||
|
||||
/** Thrown by the saver instead of saving, if set. */
|
||||
private ?\Throwable $failure = null;
|
||||
|
||||
/** @var list<int> Live (published, private, scheduled) posts that show the global entry (ruling P8a). */
|
||||
private array $live = [1053];
|
||||
|
||||
private bool $trash = true;
|
||||
|
||||
@@ -39,6 +47,7 @@ final class GlobalEditsTest extends TestCase
|
||||
{
|
||||
parent::setUp();
|
||||
Functions\when('wp_revisions_enabled')->justReturn(true);
|
||||
Functions\when('_prime_post_caches')->justReturn(null);
|
||||
}
|
||||
|
||||
private function usages(): GlobalUsages
|
||||
@@ -51,7 +60,7 @@ final class GlobalEditsTest extends TestCase
|
||||
],
|
||||
'count' => 2,
|
||||
]);
|
||||
$usages->allows('publishedIds')->andReturnUsing(fn (): array => $this->published);
|
||||
$usages->allows('liveIds')->andReturnUsing(fn (): array => $this->live);
|
||||
|
||||
return $usages;
|
||||
}
|
||||
@@ -61,6 +70,9 @@ final class GlobalEditsTest extends TestCase
|
||||
{
|
||||
$saver = Mockery::mock(DiviSaver::class);
|
||||
$saver->allows('save')->andReturnUsing(function (WP_Post $post, string $content, SaveScope $scope): array {
|
||||
if ($this->failure !== null) {
|
||||
throw $this->failure;
|
||||
}
|
||||
$this->saved[] = [$content, $scope];
|
||||
$post->post_content = $content;
|
||||
|
||||
@@ -82,7 +94,7 @@ final class GlobalEditsTest extends TestCase
|
||||
return new DiviWriter(
|
||||
LibraryFixtures::loader($access),
|
||||
$saver,
|
||||
DiviWriterFixture::log(),
|
||||
$this->log ?? DiviWriterFixture::log(),
|
||||
$design,
|
||||
DiviWriterFixture::composition($design),
|
||||
$globals
|
||||
@@ -209,20 +221,138 @@ final class GlobalEditsTest extends TestCase
|
||||
public function testConfirmIsRefusedWhenAPublishedPageShowingTheEntryIsNotEditable(): void
|
||||
{
|
||||
LibraryFixtures::given([LibraryFixtures::item(1059)]);
|
||||
$this->published = [1053, 1080];
|
||||
$this->live = [1053, 1080];
|
||||
self::denyOn('edit_post', [1080]);
|
||||
|
||||
$dryRun = (new DiviUpdateAbility($this->writer()))->execute(self::relabel(1059));
|
||||
$result = (new DiviUpdateAbility($this->writer()))->execute(self::relabel(1059, true));
|
||||
|
||||
self::assertToolError($dryRun, 'confirm_required', 'the dry run stays unchanged');
|
||||
self::assertToolError($dryRun, 'confirm_required', 'the dry run still lists the pages');
|
||||
self::assertToolError($result, 'forbidden');
|
||||
self::assertStringContainsString('1080', $result->get_error_message());
|
||||
self::assertStringContainsString('veröffentlichten, privaten oder geplanten', $result->get_error_message());
|
||||
self::assertStringNotContainsString('1053', $result->get_error_message());
|
||||
self::assertSame([], $this->saved);
|
||||
self::assertSame(0, $this->backups);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruling P8a: private and scheduled posts render (or will) – each blocks when the user may not
|
||||
* edit it; drafts are listed in the dry run but never block (they are not among liveIds()).
|
||||
*/
|
||||
public function testPrivateAndScheduledPagesBlockButDraftsDoNot(): void
|
||||
{
|
||||
LibraryFixtures::given([LibraryFixtures::item(1059)]);
|
||||
foreach ([1081, 1082] as $private) {
|
||||
$this->live = [1053, $private];
|
||||
self::denyOn('edit_post', [$private, 1070]);
|
||||
|
||||
$result = (new DiviUpdateAbility($this->writer()))->execute(self::relabel(1059, true));
|
||||
|
||||
self::assertToolError($result, 'forbidden');
|
||||
self::assertStringContainsString((string) $private, $result->get_error_message());
|
||||
}
|
||||
self::assertSame([], $this->saved);
|
||||
|
||||
$this->live = [1053];
|
||||
$result = (new DiviUpdateAbility($this->writer()))->execute(self::relabel(1059, true));
|
||||
self::assertTrue($result['ok'], 'draft 1070 (not editable) does not block');
|
||||
}
|
||||
|
||||
/** Ruling P14 (M-1): the dry run already says which posts would block the confirm. */
|
||||
public function testTheDryRunNamesThePostsThatWouldBlock(): void
|
||||
{
|
||||
LibraryFixtures::given([LibraryFixtures::item(1059)]);
|
||||
$this->live = [1053, 1081];
|
||||
self::denyOn('edit_post', [1081]);
|
||||
|
||||
$dryRun = (new DiviUpdateAbility($this->writer()))->execute(self::relabel(1059));
|
||||
|
||||
self::assertToolError($dryRun, 'confirm_required');
|
||||
self::assertSame([1081], $dryRun->get_error_data()['blocked_ids']);
|
||||
self::assertStringContainsString('1081', $dryRun->get_error_message());
|
||||
self::assertStringContainsString('abgelehnt', $dryRun->get_error_message());
|
||||
}
|
||||
|
||||
public function testADryRunWithoutBlockersHasNoBlockedIds(): void
|
||||
{
|
||||
LibraryFixtures::given([LibraryFixtures::item(1059)]);
|
||||
|
||||
$dryRun = (new DiviUpdateAbility($this->writer()))->execute(self::relabel(1059));
|
||||
|
||||
self::assertArrayNotHasKey('blocked_ids', $dryRun->get_error_data());
|
||||
}
|
||||
|
||||
/** Ruling P14 (I-2): a failed save after the backup names it. */
|
||||
public function testAFailedSaveAfterTheBackupNamesIt(): void
|
||||
{
|
||||
Functions\when('wp_revisions_enabled')->justReturn(false);
|
||||
LibraryFixtures::given([LibraryFixtures::item(1059)]);
|
||||
$this->failure = WriteFailed::rolledBack(DiviSaver::REASON, new \RuntimeException('hook'), postId: 1059);
|
||||
|
||||
try {
|
||||
(new DiviUpdateAbility($this->writer()))->execute(self::relabel(1059, true));
|
||||
self::fail('WriteFailed expected');
|
||||
} catch (WriteFailed $e) {
|
||||
$error = $e->toError();
|
||||
}
|
||||
|
||||
self::assertTrue($e->rolledBack);
|
||||
self::assertSame(1071, $error->get_error_data()['backup_id']);
|
||||
self::assertStringContainsString('Sicherung 1071 liegt im Papierkorb', $error->get_error_data()['hint']);
|
||||
self::assertStringNotContainsString('Nichts wurde gespeichert', $error->get_error_message());
|
||||
self::assertSame(1, $this->backups);
|
||||
}
|
||||
|
||||
public function testAPartialSaveAfterTheBackupNamesIt(): void
|
||||
{
|
||||
Functions\when('wp_revisions_enabled')->justReturn(false);
|
||||
LibraryFixtures::given([LibraryFixtures::item(1059)]);
|
||||
$this->failure = WriteFailed::partial(DiviSaver::REASON, new \RuntimeException('hook'), postId: 1059);
|
||||
$this->log = FakeChangeLog::partialsOnly();
|
||||
|
||||
try {
|
||||
(new DiviUpdateAbility($this->writer()))->execute(self::relabel(1059, true));
|
||||
self::fail('WriteFailed expected');
|
||||
} catch (WriteFailed $e) {
|
||||
$error = $e->toError();
|
||||
}
|
||||
|
||||
self::assertToolError($error, 'partial_write');
|
||||
self::assertSame(1071, $error->get_error_data()['backup_id']);
|
||||
self::assertSame(1059, $error->get_error_data()['post_id']);
|
||||
self::assertStringContainsString('Sicherung 1071 liegt im Papierkorb', $error->get_error_data()['hint']);
|
||||
self::assertCount(1, $this->log->partials);
|
||||
}
|
||||
|
||||
/** Ruling P14 (M-2): one page is "Seite". */
|
||||
public function testDeleteNoticeUsesTheSingularForOnePage(): void
|
||||
{
|
||||
LibraryFixtures::given([LibraryFixtures::item(1058)]);
|
||||
$usages = Mockery::mock(GlobalUsages::class);
|
||||
$usages->allows('of')->andReturn([
|
||||
'items' => [['post_id' => 1053, 'title' => 'Globale Instanzen', 'status' => 'publish', 'type' => 'page']],
|
||||
'count' => 1,
|
||||
]);
|
||||
$usages->allows('liveIds')->andReturn([1053]);
|
||||
$access = LibraryFixtures::access();
|
||||
$design = DiviWriterFixture::designReader();
|
||||
$writer = new DiviWriter(
|
||||
LibraryFixtures::loader($access),
|
||||
Mockery::mock(DiviSaver::class),
|
||||
DiviWriterFixture::log(),
|
||||
$design,
|
||||
DiviWriterFixture::composition($design),
|
||||
new GlobalEdits($access, $usages)
|
||||
);
|
||||
|
||||
$result = (new DiviDeleteAbility($writer))->execute(
|
||||
['post_id' => 1058, 'tree_rev' => self::rev(1058), 'ids' => ['s0.r0.c0.m1']]
|
||||
);
|
||||
|
||||
self::assertStringContainsString('wirkt auf 1 Seite (siehe affected)', $result->get_error_message());
|
||||
}
|
||||
|
||||
public function testRefusesToEmptyOrDoubleTheRoot(): void
|
||||
{
|
||||
LibraryFixtures::given([LibraryFixtures::item(1058)]);
|
||||
|
||||
Reference in New Issue
Block a user