Files
janniclas.loosenandClaude Opus 5.5 c7ce924e5a fix: library IDs stay hidden without library access (ruling P13)
Outline, node-get, write outlines, the render summary and replace-text's
opaque-node match name no library ID unless the library group is visible
and the user has divi_library; read-only still names them.

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

212 lines
9.9 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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));
}
/** Inserting from the library (E3): canList(), Divi's add_library and a group that is not read-only. */
public function testInsertingNeedsAddLibrary(): void
{
LibraryFixtures::given([]);
self::assertNull(LibraryFixtures::access()->canInsert());
self::assertToolError(LibraryFixtures::access(['add_library'])->canInsert(), 'forbidden');
self::assertToolError(LibraryFixtures::access(['divi_library'])->canInsert(), 'forbidden');
self::assertToolError(LibraryFixtures::access([], false)->canInsert(), 'forbidden');
LibraryFixtures::given([], ['edit_posts']);
self::assertToolError(LibraryFixtures::access()->canInsert(), 'forbidden');
}
/** Ruling P13: library IDs are named only while the library is visible and the user has its right. */
public function testRevealsIdsOnlyWithTheVisibleLibraryAndItsRight(): void
{
LibraryFixtures::given([]);
self::assertTrue(LibraryFixtures::access()->revealsIds());
self::assertFalse(LibraryFixtures::access(['divi_library'])->revealsIds());
$hidden = new LibraryAccess(LibraryFixtures::store(), LibraryFixtures::roles(), null, static fn (): bool => false);
self::assertFalse($hidden->revealsIds());
LibraryFixtures::given([], ['edit_posts']);
self::assertFalse(LibraryFixtures::access()->revealsIds());
}
/** Ruling P13: "nur lesen" (group read-only) still names library IDs – only "deaktiviert" hides them. */
public function testAReadOnlyLibraryStillRevealsIds(): void
{
LibraryFixtures::given([]);
self::assertTrue(LibraryFixtures::access([], false)->revealsIds());
$readOnly = new LibraryAccess(
LibraryFixtures::store(),
LibraryFixtures::roles(),
static fn (): bool => false,
static fn (): bool => true
);
self::assertTrue($readOnly->revealsIds());
}
}