Files
WP-LIT-MCP/tests/Unit/Integrations/Divi/Library/LibraryInsertAbilityTest.php
T

270 lines
12 KiB
PHP
Raw 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 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]);
}
}