Files
WP-LIT-MCP/tests/Unit/Integrations/Divi/Library/LibraryAccessTest.php
T
janniclas.loosenandClaude Opus 5.5 5035092805 feat: lit/library-save creates library entries like Divi (E4)
Reuses NewDraft for insert and rollback (P6), requires edit_global_library
for scope global (P7), refuses opaque and global roots before wrapping (P11)
and checks the node with StoredNodeGuard. The source page is never written.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 02:12:50 +02:00

170 lines
8.0 KiB
PHP

<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Library;
use LIT\MCP\Integrations\Divi\Library\LibraryAccess;
use LIT\MCP\Integrations\WordPress\PostGuard;
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
use LIT\MCP\Tests\Support\LibraryFixtures;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
use WP_Error;
use WP_Post;
final class LibraryAccessTest extends TestCase
{
public function testReadsLibraryItemsAndLeavesOtherIdsToPostGuard(): void
{
$item = LibraryFixtures::item(1055);
LibraryFixtures::given([$item, LibraryFixtures::page('')]);
$access = LibraryFixtures::access();
self::assertSame($item, $access->item(1055, false));
self::assertNull($access->item(LibraryFixtures::PAGE, false), 'a page is PostGuard\'s business');
self::assertNull($access->item(9999, false));
self::assertNull($access->item('abc', false));
self::assertNull($access->canList());
}
/** Ruling P12: without the library right at all, library IDs read like missing IDs (no type oracle). */
public function testReadingNeedsEditPostsTheDiviLibraryRoleAndEditPost(): void
{
$item = LibraryFixtures::item(1055);
LibraryFixtures::given([$item], ['edit_posts']);
self::assertToolError(LibraryFixtures::access()->item(1055, false), 'not_found');
self::assertToolError(LibraryFixtures::access()->canList(), 'forbidden');
LibraryFixtures::given([$item]);
self::assertToolError(LibraryFixtures::access(['divi_library'])->item(1055, false), 'not_found');
self::assertToolError(LibraryFixtures::access(['divi_library'])->canList(), 'forbidden');
LibraryFixtures::given([$item], ['edit_post']);
$refusal = LibraryFixtures::access()->item(1055, false);
self::assertToolError($refusal, 'forbidden');
self::assertStringContainsString('Bibliothekseintrag 1055', $refusal->get_error_message());
}
public function testTrashedItemsCanBeRead(): void
{
$item = LibraryFixtures::item(1055, ['post_status' => 'trash']);
LibraryFixtures::given([$item]);
self::assertInstanceOf(WP_Post::class, LibraryFixtures::access()->item(1055, false));
}
public function testGlobalFromTheStore(): void
{
$access = LibraryFixtures::access();
self::assertTrue($access->isGlobal(LibraryFixtures::item(1058)));
self::assertFalse($access->isGlobal(LibraryFixtures::item(1055)));
}
public function testRefusalsNameNeitherTitleNorContent(): void
{
$item = LibraryFixtures::item(1055, ['post_status' => 'private', 'post_title' => 'Geheimer Entwurf']);
foreach ([['edit_posts'], ['edit_post']] as $denied) {
LibraryFixtures::given([$item], $denied);
$refusal = LibraryFixtures::access()->item(1055, false);
self::assertToolError($refusal, $denied === ['edit_post'] ? 'forbidden' : 'not_found');
self::assertStringNotContainsString('Geheimer Entwurf', $refusal->get_error_message());
self::assertStringNotContainsString('Geheimer Entwurf', (string) wp_json_encode($refusal->get_error_data()));
}
}
/** Ruling P9: with the group "Divi-Bibliothek" disabled, library IDs read like IDs that do not exist. */
public function testAHiddenLibraryAnswersNotFoundLikeAMissingEntry(): void
{
$item = LibraryFixtures::item(1055, ['post_title' => 'Geheimer Entwurf']);
LibraryFixtures::given([$item, LibraryFixtures::page('')], ['edit_post']);
$hidden = new LibraryAccess(LibraryFixtures::store(), LibraryFixtures::roles(), null, static fn (): bool => false);
foreach ([false, true] as $forWrite) {
$refusal = $hidden->item(1055, $forWrite);
self::assertToolError($refusal, 'not_found');
self::assertStringNotContainsString('Geheimer Entwurf', $refusal->get_error_message());
self::assertStringNotContainsString('Bibliothek', $refusal->get_error_message());
}
self::assertNull($hidden->item(LibraryFixtures::PAGE, false), 'pages stay PostGuard\'s business');
self::assertNull($hidden->item(9999, false));
}
/** Ruling P12: the refusal without the library right is byte-identical to a missing ID's. */
public function testWithoutTheLibraryRightAnEntryIsIndistinguishableFromAMissingId(): void
{
$schema = Mockery::mock(ContentSchema::class);
$schema->allows('isManaged')->andReturn(false);
foreach ([[['edit_posts'], []], [[], ['divi_library']]] as [$denied, $deniedRoles]) {
LibraryFixtures::given([LibraryFixtures::item(1055, ['post_title' => 'Geheimer Entwurf'])], $denied);
$refusal = LibraryFixtures::access($deniedRoles)->item(1055, false);
LibraryFixtures::given([], $denied);
$missing = PostGuard::editable(1055, $schema, 'post_id', true);
self::assertInstanceOf(WP_Error::class, $refusal);
self::assertInstanceOf(WP_Error::class, $missing);
self::assertSame($missing->get_error_code(), $refusal->get_error_code());
self::assertSame($missing->get_error_message(), $refusal->get_error_message());
self::assertSame($missing->get_error_data(), $refusal->get_error_data());
}
}
public function testEditingNeedsSaveLibraryAPublishedItemAWritableGroupAndForGlobalsEditGlobalLibrary(): void
{
LibraryFixtures::given([
LibraryFixtures::item(1057),
LibraryFixtures::item(1059),
LibraryFixtures::item(1055, ['post_status' => 'trash']),
]);
self::assertInstanceOf(WP_Post::class, LibraryFixtures::access()->item(1057, true));
self::assertToolError(LibraryFixtures::access(['save_library'])->item(1057, true), 'forbidden');
$readonly = LibraryFixtures::access([], false)->item(1057, true);
self::assertToolError($readonly, 'forbidden');
self::assertStringContainsString('Divi-Bibliothek', $readonly->get_error_message());
self::assertToolError(LibraryFixtures::access()->item(1055, true), 'validation_failed');
self::assertInstanceOf(WP_Post::class, LibraryFixtures::access()->item(1059, true));
self::assertToolError(LibraryFixtures::access(['edit_global_library'])->item(1059, true), 'forbidden');
self::assertInstanceOf(WP_Post::class, LibraryFixtures::access(['edit_global_library'])->item(1057, true));
}
public function testEditingNeedsEditPostOnTheEntryAndReadOnlyStillReads(): void
{
LibraryFixtures::given([LibraryFixtures::item(1057)], ['edit_post']);
self::assertToolError(LibraryFixtures::access()->item(1057, true), 'forbidden');
LibraryFixtures::given([LibraryFixtures::item(1057)]);
self::assertInstanceOf(WP_Post::class, LibraryFixtures::access([], false)->item(1057, false));
self::assertNull(LibraryFixtures::access()->writableGate());
}
public function testSavingNeedsPublishPostsSaveLibraryAndAWritableGroup(): void
{
LibraryFixtures::given([]);
self::assertNull(LibraryFixtures::access()->canSave());
LibraryFixtures::given([], ['publish_posts']);
self::assertToolError(LibraryFixtures::access()->canSave(), 'forbidden');
LibraryFixtures::given([]);
self::assertToolError(LibraryFixtures::access(['save_library'])->canSave(), 'forbidden');
self::assertToolError(LibraryFixtures::access([], false)->canSave(), 'forbidden');
self::assertToolError(LibraryFixtures::access(['divi_library'])->canSave(), 'forbidden');
}
/** Ruling P7: a global entry additionally needs Divi's edit_global_library. */
public function testSavingAGlobalEntryNeedsEditGlobalLibrary(): void
{
LibraryFixtures::given([]);
self::assertNull(LibraryFixtures::access(['edit_global_library'])->canSave());
self::assertToolError(LibraryFixtures::access(['edit_global_library'])->canSave(true), 'forbidden');
self::assertNull(LibraryFixtures::access()->canSave(true));
}
}