feat: lit-library-insert as copy or global instance in the builder's format (E5)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -166,4 +166,18 @@ final class LibraryAccessTest extends TestCase
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Library;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use LIT\MCP\Abilities\Rev;
|
||||
use LIT\MCP\Integrations\Divi\Library\LibraryInsertAbility;
|
||||
use LIT\MCP\Tests\Support\DiviFixtures;
|
||||
use LIT\MCP\Tests\Support\DiviWriterFixture;
|
||||
use LIT\MCP\Tests\Support\LibraryFixtures;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use WP_Post;
|
||||
|
||||
final class LibraryInsertAbilityTest extends TestCase
|
||||
{
|
||||
private WP_Post $page;
|
||||
|
||||
/** @var list<string> */
|
||||
private array $saved = [];
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->page = LibraryFixtures::page(DiviFixtures::markup('synthetic-basic.html'));
|
||||
LibraryFixtures::given(array_merge(
|
||||
[$this->page],
|
||||
array_map(static fn (int $id): WP_Post => LibraryFixtures::item($id), array_keys(LibraryFixtures::ITEMS))
|
||||
));
|
||||
}
|
||||
|
||||
/** @param list<string> $deniedRoles */
|
||||
private function ability(array $deniedRoles = [], bool $diviWritable = true, bool $unfiltered = false): LibraryInsertAbility
|
||||
{
|
||||
$access = LibraryFixtures::access($deniedRoles);
|
||||
$failures = [];
|
||||
|
||||
return new LibraryInsertAbility(
|
||||
DiviWriterFixture::writer($this->saved, $failures, $unfiltered, LibraryFixtures::loader($access)),
|
||||
$access,
|
||||
static fn (): bool => $diviWritable
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $extra
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function input(array $extra): array
|
||||
{
|
||||
return array_merge(['post_id' => 7, 'tree_rev' => Rev::of((string) $this->page->post_content)], $extra);
|
||||
}
|
||||
|
||||
private static function fixtureInstance(int $id, string $end): string
|
||||
{
|
||||
$page = DiviFixtures::markup('stage3/page--globale-instanzen.html');
|
||||
$start = (int) strpos($page, '<!-- wp:divi/global-layout {"globalModule":"' . $id . '"');
|
||||
|
||||
return substr($page, $start, (int) strpos($page, $end, $start) + strlen($end) - $start);
|
||||
}
|
||||
|
||||
/** Replaces library entry $id by one with $content (still global/normal as in ITEMS). */
|
||||
private function givenItem(int $id, string $content, array $fields = []): void
|
||||
{
|
||||
LibraryFixtures::given([
|
||||
$this->page,
|
||||
LibraryFixtures::item($id, array_merge(['post_content' => $content], $fields)),
|
||||
]);
|
||||
}
|
||||
|
||||
public function testInsertsAGlobalSectionExactlyLikeTheBuilder(): void
|
||||
{
|
||||
$result = $this->ability()->execute($this->input(['library_id' => 1058, 'after' => 's0']));
|
||||
|
||||
self::assertTrue($result['ok']);
|
||||
self::assertStringContainsString(self::fixtureInstance(1058, '<!-- /wp:divi/global-layout -->'), $this->saved[0]);
|
||||
self::assertSame('g0', $result['changed'][0]['id']);
|
||||
self::assertStringContainsString('Bibliothekseintrag 1058', implode(' ', $result['warnings']));
|
||||
}
|
||||
|
||||
public function testInsertsAGlobalModuleIntoAColumn(): void
|
||||
{
|
||||
$result = $this->ability()->execute($this->input(['library_id' => 1059, 'parent' => 's0.r0.c1']));
|
||||
|
||||
self::assertTrue($result['ok']);
|
||||
self::assertStringContainsString(self::fixtureInstance(1059, '/-->'), $this->saved[0]);
|
||||
}
|
||||
|
||||
public function testUntouchedContentKeepsItsBytes(): void
|
||||
{
|
||||
$before = (string) $this->page->post_content;
|
||||
|
||||
$this->ability()->execute($this->input(['library_id' => 1059, 'parent' => 's0.r0.c1']));
|
||||
|
||||
$instance = self::fixtureInstance(1059, '/-->');
|
||||
$at = (int) strpos($this->saved[0], $instance);
|
||||
self::assertSame($before, substr($this->saved[0], 0, $at) . substr($this->saved[0], $at + strlen($instance)));
|
||||
}
|
||||
|
||||
public function testCopyInsertsTheRootsWithoutFrameAndCrlf(): void
|
||||
{
|
||||
$result = $this->ability()->execute($this->input(['library_id' => 1055]));
|
||||
|
||||
$section = str_replace(
|
||||
['<!-- wp:divi/placeholder -->', '<!-- /wp:divi/placeholder -->', "\r\n"],
|
||||
['', '', "\n"],
|
||||
DiviFixtures::markup('stage3/library--library-abschnitt.html')
|
||||
);
|
||||
self::assertTrue($result['ok']);
|
||||
self::assertStringContainsString($section, $this->saved[0]);
|
||||
self::assertStringNotContainsString("\r", $this->saved[0]);
|
||||
}
|
||||
|
||||
public function testLayoutCopyInsertsEverySection(): void
|
||||
{
|
||||
$result = $this->ability()->execute($this->input(['library_id' => 1054, 'mode' => 'copy']));
|
||||
|
||||
self::assertCount(3, $result['changed']);
|
||||
self::assertStringContainsString('Library-Blurb', $this->saved[0]);
|
||||
}
|
||||
|
||||
/** F10: a copy of a global entry is deglobalized like Divi's /load – its root bytes, no globalParent. */
|
||||
public function testCopyOfAGlobalEntryIsItsPlainRoot(): void
|
||||
{
|
||||
$result = $this->ability()->execute($this->input(['library_id' => 1058, 'mode' => 'copy']));
|
||||
|
||||
$section = str_replace(
|
||||
['<!-- wp:divi/placeholder -->', '<!-- /wp:divi/placeholder -->', "\r\n"],
|
||||
['', '', "\n"],
|
||||
DiviFixtures::markup('stage3/library-global--globaler-abschnitt.html')
|
||||
);
|
||||
self::assertTrue($result['ok']);
|
||||
self::assertStringContainsString($section, $this->saved[0]);
|
||||
self::assertStringNotContainsString('globalParent', $this->saved[0]);
|
||||
self::assertStringNotContainsString('global-layout', $this->saved[0]);
|
||||
self::assertSame([], $result['warnings']);
|
||||
}
|
||||
|
||||
/** F10: nested instances in a (stored) global entry become plain nodes in the copy. */
|
||||
public function testCopyDeglobalizesNestedInstances(): void
|
||||
{
|
||||
$instance = self::fixtureInstance(1059, '/-->');
|
||||
$this->givenItem(1058, '<!-- wp:divi/placeholder --><!-- wp:divi/section {"builderVersion":"5.2.1"} -->'
|
||||
. '<!-- wp:divi/row {"builderVersion":"5.2.1"} --><!-- wp:divi/column {"builderVersion":"5.2.1"} -->'
|
||||
. $instance . '<!-- /wp:divi/column --><!-- /wp:divi/row --><!-- /wp:divi/section -->'
|
||||
. '<!-- /wp:divi/placeholder -->');
|
||||
|
||||
$result = $this->ability()->execute($this->input(['library_id' => 1058, 'mode' => 'copy']));
|
||||
|
||||
self::assertTrue($result['ok']);
|
||||
self::assertStringNotContainsString('globalModule', $this->saved[0]);
|
||||
self::assertStringContainsString('<!-- wp:divi/text {"builderVersion":"5.2.1","content"', $this->saved[0]);
|
||||
}
|
||||
|
||||
/** Ruling P11: never an instance of an entry whose root is (or holds) an instance or is unreadable. */
|
||||
public function testNoNestedInstances(): void
|
||||
{
|
||||
$instance = self::fixtureInstance(1059, '/-->');
|
||||
foreach (
|
||||
[
|
||||
'<!-- wp:divi/placeholder -->' . $instance . '<!-- /wp:divi/placeholder -->',
|
||||
'<!-- wp:divi/placeholder --><!-- wp:divi/section {"builderVersion":"5.2.1"} -->'
|
||||
. '<!-- wp:divi/row {"builderVersion":"5.2.1"} --><!-- wp:divi/column {"builderVersion":"5.2.1"} -->'
|
||||
. $instance . '<!-- /wp:divi/column --><!-- /wp:divi/row --><!-- /wp:divi/section -->'
|
||||
. '<!-- /wp:divi/placeholder -->',
|
||||
'<!-- wp:divi/placeholder --><!-- wp:divi/text {"globalModule":"1057"} /--><!-- /wp:divi/placeholder -->',
|
||||
'<!-- wp:divi/placeholder --><!-- wp:divi/text {"a":1,} /--><!-- /wp:divi/placeholder -->',
|
||||
] as $content
|
||||
) {
|
||||
$this->givenItem(1059, $content);
|
||||
|
||||
$result = $this->ability([], true, true)->execute($this->input(['library_id' => 1059]));
|
||||
|
||||
self::assertToolError($result, 'validation_failed');
|
||||
self::assertStringContainsString('Verschachtelung', $result->get_error_message());
|
||||
}
|
||||
self::assertSame([], $this->saved);
|
||||
}
|
||||
|
||||
public function testGlobalModeNeedsAGlobalEntryAndUnknownEntriesAreNotFound(): void
|
||||
{
|
||||
self::assertToolError($this->ability()->execute($this->input(['library_id' => 1055, 'mode' => 'global'])), 'validation_failed');
|
||||
self::assertToolError($this->ability()->execute($this->input(['library_id' => 9999])), 'not_found');
|
||||
self::assertToolError($this->ability()->execute($this->input(['library_id' => 7])), 'not_found');
|
||||
self::assertToolError($this->ability()->execute($this->input(['library_id' => 1055, 'mode' => 'link'])), 'validation_failed');
|
||||
self::assertSame([], $this->saved);
|
||||
}
|
||||
|
||||
public function testATrashedEntryIsNotFound(): void
|
||||
{
|
||||
$this->givenItem(1055, DiviFixtures::markup('stage3/library--library-abschnitt.html'), ['post_status' => 'trash']);
|
||||
|
||||
self::assertToolError($this->ability()->execute($this->input(['library_id' => 1055])), 'not_found');
|
||||
self::assertSame([], $this->saved);
|
||||
}
|
||||
|
||||
/** Ruling P5: the entry is read through LibraryAccess – edit_post on it, not read_post. */
|
||||
public function testNeedsEditPostOnTheEntry(): void
|
||||
{
|
||||
Functions\when('current_user_can')->alias(
|
||||
static fn (string $cap, mixed ...$args): bool => ! ($cap === 'edit_post' && (int) ($args[0] ?? 0) === 1058)
|
||||
);
|
||||
|
||||
$result = $this->ability()->execute($this->input(['library_id' => 1058]));
|
||||
|
||||
self::assertToolError($result, 'forbidden');
|
||||
self::assertStringNotContainsString('[Fixture]', $result->get_error_message());
|
||||
self::assertSame([], $this->saved);
|
||||
}
|
||||
|
||||
public function testNeedsAddLibraryAndAWritableDiviGroup(): void
|
||||
{
|
||||
self::assertToolError($this->ability(['add_library'])->permission($this->input(['library_id' => 1058])), 'forbidden');
|
||||
self::assertToolError($this->ability(['add_library'])->execute($this->input(['library_id' => 1058])), 'forbidden');
|
||||
self::assertToolError($this->ability([], false)->permission($this->input(['library_id' => 1058])), 'unsupported');
|
||||
self::assertToolError($this->ability([], false)->execute($this->input(['library_id' => 1058])), 'unsupported');
|
||||
self::assertTrue($this->ability()->permission($this->input(['library_id' => 1058])));
|
||||
self::assertTrue($this->ability()->isDirect());
|
||||
self::assertSame([], $this->saved);
|
||||
}
|
||||
|
||||
public function testTheTargetKeepsTheTreeRevLockAndStructureRules(): void
|
||||
{
|
||||
self::assertToolError(
|
||||
$this->ability()->execute(['post_id' => 7, 'tree_rev' => 'alt', 'library_id' => 1058]),
|
||||
'rev_mismatch'
|
||||
);
|
||||
self::assertToolError(
|
||||
$this->ability()->execute($this->input(['library_id' => 1058, 'parent' => 's0.r0.c1'])),
|
||||
'validation_failed'
|
||||
);
|
||||
self::assertToolError(
|
||||
$this->ability()->execute($this->input(['library_id' => 1059, 'parent' => 's9'])),
|
||||
'validation_failed'
|
||||
);
|
||||
self::assertSame([], $this->saved);
|
||||
}
|
||||
|
||||
public function testEditorCannotInsertAnItemWithCustomCss(): void
|
||||
{
|
||||
$custom = LibraryFixtures::item(1055, [
|
||||
'ID' => 1080,
|
||||
'post_content' => '<!-- wp:divi/placeholder --><!-- wp:divi/section {"css":{"desktop":{"value":'
|
||||
. '{"freeForm":"body{display:none}"}}}} --><!-- /wp:divi/section --><!-- /wp:divi/placeholder -->',
|
||||
]);
|
||||
LibraryFixtures::given([$this->page, $custom]);
|
||||
|
||||
$result = $this->ability()->execute($this->input(['library_id' => 1080]));
|
||||
|
||||
self::assertToolError($result, 'validation_failed');
|
||||
self::assertSame([], $this->saved);
|
||||
}
|
||||
|
||||
public function testAdminMayInsertAnItemWithCustomCss(): void
|
||||
{
|
||||
$custom = LibraryFixtures::item(1055, [
|
||||
'ID' => 1080,
|
||||
'post_content' => '<!-- wp:divi/placeholder --><!-- wp:divi/section {"css":{"desktop":{"value":'
|
||||
. '{"freeForm":"body{display:none}"}}}} --><!-- /wp:divi/section --><!-- /wp:divi/placeholder -->',
|
||||
]);
|
||||
LibraryFixtures::given([$this->page, $custom]);
|
||||
|
||||
$result = $this->ability([], true, true)->execute($this->input(['library_id' => 1080]));
|
||||
|
||||
self::assertTrue($result['ok']);
|
||||
self::assertStringContainsString('body{display:none}', $this->saved[0]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user