feat: group Divi-Bibliothek with lit/library-list

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-27 01:29:37 +02:00
co-authored by Claude Opus 5.5
parent 520a639411
commit b7c714939d
7 changed files with 528 additions and 4 deletions
@@ -0,0 +1,200 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Library;
use Brain\Monkey\Functions;
use LIT\MCP\Abilities\Cursor;
use LIT\MCP\Integrations\Divi\Library\GlobalUsages;
use LIT\MCP\Integrations\Divi\Library\LibraryListAbility;
use LIT\MCP\Integrations\WordPress\PostGuard;
use LIT\MCP\Tests\Support\DiviWriterFixture;
use LIT\MCP\Tests\Support\LibraryFixtures;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
use WP_Error;
final class LibraryListAbilityTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
Functions\when('get_the_terms')->justReturn(false);
Functions\when('get_post_meta')->justReturn('');
}
/**
* @param list<string> $deniedRoles
* @param list<int> $ids Entries the store lists (in this order).
*/
private function ability(array $deniedRoles = [], array $ids = [1055, 1058, 1059]): LibraryListAbility
{
$store = LibraryFixtures::store();
$store->allows('find')->andReturnUsing(static fn (array $filters, int $limit, int $offset): array => array_slice(
array_map(static fn (int $id) => LibraryFixtures::item($id), $ids),
$offset,
$limit + 1
));
$access = LibraryFixtures::access($deniedRoles, true, $store);
$usages = Mockery::mock(GlobalUsages::class);
$usages->allows('of')->with(1058)->andReturn([
'items' => [['post_id' => 1053, 'title' => 'Globale Instanzen', 'status' => 'draft', 'type' => 'page']],
'count' => 1,
]);
return new LibraryListAbility($access, LibraryFixtures::loader($access), DiviWriterFixture::designReader(), $usages);
}
/** A user with every capability but edit_post on the entries $hidden. */
private static function withoutEditPostOn(array $hidden): void
{
Functions\when('current_user_can')->alias(
static fn (string $cap, mixed ...$args): bool => ! ($cap === 'edit_post' && in_array((int) ($args[0] ?? 0), $hidden, true))
);
}
public function testListsAPageWithCursor(): void
{
LibraryFixtures::given([]);
$first = $this->ability()->execute(['limit' => 2]);
$second = $this->ability()->execute(['limit' => 2, 'cursor' => $first['next_cursor']]);
self::assertSame([1055, 1058], array_column($first['items'], 'id'));
self::assertSame(['normal', 'global'], array_column($first['items'], 'scope'));
self::assertSame('section', $first['items'][0]['layout_type']);
self::assertSame(Cursor::encode(2), $first['next_cursor']);
self::assertSame([1059], array_column($second['items'], 'id'));
self::assertNull($second['next_cursor']);
}
public function testPassesTheFilters(): void
{
LibraryFixtures::given([]);
$store = LibraryFixtures::store();
$store->expects('find')->with(['layout_type' => 'module', 'scope' => 'global', 'search' => 'Zeiten'], 20, 0)
->andReturn([]);
$access = LibraryFixtures::access([], true, $store);
$ability = new LibraryListAbility(
$access,
LibraryFixtures::loader($access),
DiviWriterFixture::designReader(),
Mockery::mock(GlobalUsages::class)
);
$result = $ability->execute(['layout_type' => 'module', 'scope' => 'global', 'search' => 'Zeiten', 'category' => '']);
self::assertSame(['items' => [], 'next_cursor' => null], $result);
}
public function testDetailOfAGlobalEntryWithOutlineAndAffectedPages(): void
{
LibraryFixtures::given([LibraryFixtures::item(1058)]);
$result = $this->ability()->execute(['id' => 1058]);
self::assertSame(1058, $result['items'][0]['id']);
self::assertNull($result['next_cursor']);
self::assertStringStartsWith('s0 section "Global Abschnitt"', $result['outline']);
self::assertSame(12, strlen($result['tree_rev']));
self::assertSame(1, $result['affected_count']);
self::assertSame('https://kunde.test/wp-admin/post.php?post=1058&action=edit', $result['edit_url']);
}
public function testDetailOfSomethingElseIsNotFoundAndTheRoleIsNeeded(): void
{
LibraryFixtures::given([LibraryFixtures::page('')]);
self::assertToolError($this->ability()->execute(['id' => LibraryFixtures::PAGE]), 'not_found');
self::assertToolError($this->ability(['divi_library'])->permission([]), 'forbidden');
self::assertToolError($this->ability()->execute(['cursor' => '!!']), 'validation_failed');
self::assertTrue($this->ability()->annotations()['readonly']);
self::assertFalse($this->ability()->isDirect());
}
/** Output ruling: entries without edit_post are left out – no title, no count, the cursor still pages right. */
public function testLeavesOutEntriesTheUserMayNotEdit(): void
{
LibraryFixtures::given([]);
self::withoutEditPostOn([1055, 1057]);
$ids = [1054, 1055, 1056, 1057, 1058, 1059];
$first = $this->ability([], $ids)->execute(['limit' => 2]);
$second = $this->ability([], $ids)->execute(['limit' => 2, 'cursor' => $first['next_cursor']]);
self::assertSame([1054, 1056], array_column($first['items'], 'id'));
self::assertNotNull($first['next_cursor']);
self::assertSame([1058, 1059], array_column($second['items'], 'id'));
self::assertNull($second['next_cursor']);
$json = (string) json_encode([$first, $second]);
self::assertStringNotContainsString('[Fixture] 1055', $json);
self::assertStringNotContainsString('[Fixture] 1057', $json);
self::assertSame(['items', 'next_cursor'], array_keys($first));
}
public function testNoNextCursorWhenOnlyHiddenEntriesFollow(): void
{
LibraryFixtures::given([]);
self::withoutEditPostOn([1058, 1059]);
$result = $this->ability()->execute(['limit' => 1]);
self::assertSame([1055], array_column($result['items'], 'id'));
self::assertNull($result['next_cursor']);
}
/** Ruling P12: without the library right, an id reads exactly like a missing ID; the list says forbidden. */
public function testWithoutTheLibraryRightAnIdIsNotFoundLikeAMissingOne(): void
{
$missing = PostGuard::notFound(1058);
$cases = [
'no divi_library' => [[], ['divi_library']],
'no edit_posts' => [['edit_posts'], []],
];
foreach ($cases as $case => [$deniedCaps, $deniedRoles]) {
LibraryFixtures::given([LibraryFixtures::item(1058), LibraryFixtures::page('')], $deniedCaps);
$ability = $this->ability($deniedRoles);
foreach ([$ability->permission(['id' => 1058]), $ability->execute(['id' => 1058])] as $result) {
self::assertInstanceOf(WP_Error::class, $result, $case);
self::assertSame($missing->get_error_code(), $result->get_error_code(), $case);
self::assertSame($missing->get_error_message(), $result->get_error_message(), $case);
self::assertSame($missing->get_error_data(), $result->get_error_data(), $case);
}
self::assertToolError($ability->execute(['id' => 99999]), 'not_found');
self::assertToolError($ability->execute(['id' => LibraryFixtures::PAGE]), 'not_found');
self::assertToolError($ability->execute([]), 'forbidden');
self::assertToolError($ability->permission([]), 'forbidden');
}
}
public function testAnInvalidIdIsAValidationError(): void
{
LibraryFixtures::given([]);
self::assertToolError($this->ability()->execute(['id' => 'abc']), 'validation_failed');
}
public function testDetailWithoutEditPostIsForbiddenWithoutTheTitle(): void
{
LibraryFixtures::given([LibraryFixtures::item(1058, ['post_title' => 'Geheim'])]);
self::withoutEditPostOn([1058]);
$result = $this->ability()->execute(['id' => 1058]);
self::assertToolError($result, 'forbidden');
self::assertStringNotContainsString('Geheim', (string) json_encode([$result->get_error_message(), $result->get_error_data()]));
self::assertToolError($this->ability()->permission(['id' => 1058]), 'forbidden');
}
public function testDetailOfANormalEntryHasNoAffectedPages(): void
{
LibraryFixtures::given([LibraryFixtures::item(1055)]);
$result = $this->ability()->execute(['id' => 1055]);
self::assertSame('normal', $result['items'][0]['scope']);
self::assertArrayNotHasKey('affected', $result);
self::assertTrue($this->ability()->permission(['id' => 1055]));
}
}