DiviThemeBuilder wraps Divi's Theme Builder: reads through Divi's functions, writes layouts/templates through insert_layout/store_template behind a wp_die guard (edit_others_posts and function checks first, every handler filter and output buffer restored in finally), list/conditions/enabled mirrored from Divi's own save. Template: live = attached and enabled, whatever the post status (ruling Q8/B8). ThemeBuilderAccess: edit_others_posts plus Divi's theme_builder role, hidden IDs answer like missing ones, canEditTemplate (B11) and layoutsRefusal (B10). DiviCompatibility checks the Theme Builder functions and post types (E15). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
134 lines
6.6 KiB
PHP
134 lines
6.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace LIT\MCP\Tests\Unit\Integrations\Divi\ThemeBuilder;
|
|
|
|
use Brain\Monkey\Functions;
|
|
use LIT\MCP\Tests\Support\LibraryFixtures;
|
|
use LIT\MCP\Tests\Support\ThemeBuilderFixtures;
|
|
use LIT\MCP\Tests\Unit\TestCase;
|
|
use WP_Post;
|
|
|
|
final class ThemeBuilderAccessTest extends TestCase
|
|
{
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
Functions\when('get_post_type_object')->justReturn(null);
|
|
}
|
|
|
|
/** Spec §4.5 + F12/F23: edit_others_posts and Divi's role option "Theme Builder". */
|
|
public function testUsingTheThemeBuilderNeedsEditOthersPostsAndTheDiviRole(): void
|
|
{
|
|
LibraryFixtures::given([]);
|
|
self::assertNull(ThemeBuilderFixtures::access()->canUse());
|
|
self::assertToolError(ThemeBuilderFixtures::access(['theme_builder'])->canUse(), 'forbidden');
|
|
self::assertFalse(ThemeBuilderFixtures::access(['theme_builder'])->visible());
|
|
self::assertFalse(ThemeBuilderFixtures::access([], true, null, false)->visible());
|
|
|
|
LibraryFixtures::given([], ['edit_others_posts']);
|
|
self::assertToolError(ThemeBuilderFixtures::access()->canUse(), 'forbidden');
|
|
}
|
|
|
|
/** P12 pattern: without access a layout ID answers exactly like a missing one. */
|
|
public function testLayoutsAreHiddenLikeMissingIdsWithoutAccess(): void
|
|
{
|
|
$body = ThemeBuilderFixtures::layout(1062);
|
|
LibraryFixtures::given([$body, LibraryFixtures::page('')]);
|
|
|
|
self::assertInstanceOf(WP_Post::class, ThemeBuilderFixtures::access()->layout(1062, false));
|
|
self::assertNull(ThemeBuilderFixtures::access()->layout(7, false), 'a page is no layout');
|
|
$missing = ThemeBuilderFixtures::access(['theme_builder'])->layout(1062, false);
|
|
$hidden = ThemeBuilderFixtures::access([], true, null, false)->layout(1062, false);
|
|
self::assertToolError($missing, 'not_found');
|
|
self::assertSame(self::clientError($missing), self::clientError(\LIT\MCP\Integrations\WordPress\PostGuard::notFound(1062)));
|
|
self::assertToolError($hidden, 'not_found');
|
|
|
|
LibraryFixtures::given([$body], ['edit_post']);
|
|
self::assertToolError(ThemeBuilderFixtures::access()->layout(1062, false), 'forbidden');
|
|
}
|
|
|
|
public function testWritingALayoutNeedsTheGroupAndAPublishedLayout(): void
|
|
{
|
|
LibraryFixtures::given([ThemeBuilderFixtures::layout(1062), ThemeBuilderFixtures::layout(1063, ['post_status' => 'trash'])]);
|
|
|
|
self::assertInstanceOf(WP_Post::class, ThemeBuilderFixtures::access()->layout(1062, true));
|
|
self::assertToolError(ThemeBuilderFixtures::access([], false)->layout(1062, true), 'forbidden');
|
|
self::assertToolError(ThemeBuilderFixtures::access()->layout(1063, true), 'validation_failed');
|
|
}
|
|
|
|
public function testTemplatesAreTheOnlyTemplateIds(): void
|
|
{
|
|
LibraryFixtures::given([ThemeBuilderFixtures::templatePost(), ThemeBuilderFixtures::layout(1062)]);
|
|
|
|
self::assertInstanceOf(WP_Post::class, ThemeBuilderFixtures::access()->template(1066, true));
|
|
self::assertToolError(ThemeBuilderFixtures::access()->template(1062, false), 'not_found');
|
|
self::assertToolError(ThemeBuilderFixtures::access()->template(999, false), 'not_found');
|
|
self::assertToolError(ThemeBuilderFixtures::access()->template('x', false), 'validation_failed');
|
|
self::assertToolError(ThemeBuilderFixtures::access(['theme_builder'])->template(1066, false), 'not_found');
|
|
self::assertToolError(ThemeBuilderFixtures::access([], false)->template(1066, true), 'forbidden');
|
|
}
|
|
|
|
public function testCreatingNeedsPublishRightsAndTheGroup(): void
|
|
{
|
|
LibraryFixtures::given([]);
|
|
self::assertNull(ThemeBuilderFixtures::access()->canCreate());
|
|
self::assertToolError(ThemeBuilderFixtures::access([], false)->canCreate(), 'forbidden');
|
|
|
|
LibraryFixtures::given([], ['publish_posts']);
|
|
self::assertToolError(ThemeBuilderFixtures::access()->canCreate(), 'forbidden');
|
|
}
|
|
|
|
/** E7: an unsaved Theme Builder session would later replace the template list. */
|
|
public function testAThemeBuilderDraftBlocksWrites(): void
|
|
{
|
|
self::assertNull(ThemeBuilderFixtures::access()->draftRefusal());
|
|
$blocked = ThemeBuilderFixtures::access([], true, ThemeBuilderFixtures::store([], 999))->draftRefusal();
|
|
self::assertToolError($blocked, 'unsupported');
|
|
self::assertStringContainsString('Entwurf', self::clientError($blocked)['message']);
|
|
}
|
|
|
|
/** Ruling Q8/B11: tb-list names a template's ID only to a user who may edit it. */
|
|
public function testTemplateIdsAreNamedOnlyToEditors(): void
|
|
{
|
|
LibraryFixtures::given([], []);
|
|
self::assertTrue(ThemeBuilderFixtures::access()->canEditTemplate(1066));
|
|
self::assertFalse(ThemeBuilderFixtures::access(['theme_builder'])->canEditTemplate(1066));
|
|
self::assertFalse(ThemeBuilderFixtures::access([], true, null, false)->canEditTemplate(1066));
|
|
|
|
LibraryFixtures::given([], ['edit_post']);
|
|
self::assertFalse(ThemeBuilderFixtures::access()->canEditTemplate(1066));
|
|
}
|
|
|
|
/** Ruling Q8/B10: reattach/activate only with published layouts of the right area (Divi trashes marked ones). */
|
|
public function testLayoutsMustBePublishedBeforeTheTemplateGoesBackOrLive(): void
|
|
{
|
|
LibraryFixtures::given([
|
|
ThemeBuilderFixtures::layout(1061),
|
|
ThemeBuilderFixtures::layout(1062),
|
|
ThemeBuilderFixtures::layout(1063),
|
|
]);
|
|
self::assertNull(ThemeBuilderFixtures::access()->layoutsRefusal(ThemeBuilderFixtures::template()));
|
|
self::assertNull(ThemeBuilderFixtures::access()->layoutsRefusal(
|
|
ThemeBuilderFixtures::template(ThemeBuilderFixtures::DEFAULT_TEMPLATE)
|
|
), 'areas without a layout (ID 0) need nothing');
|
|
|
|
LibraryFixtures::given([
|
|
ThemeBuilderFixtures::layout(1061),
|
|
ThemeBuilderFixtures::layout(1062, ['post_status' => 'trash']),
|
|
]);
|
|
$refusal = ThemeBuilderFixtures::access()->layoutsRefusal(ThemeBuilderFixtures::template());
|
|
self::assertToolError($refusal, 'validation_failed');
|
|
self::assertSame(['layouts.body', 'layouts.footer'], array_keys(self::errorFields($refusal)));
|
|
|
|
LibraryFixtures::given([
|
|
ThemeBuilderFixtures::layout(1061),
|
|
ThemeBuilderFixtures::layout(1063, ['ID' => 1062]),
|
|
ThemeBuilderFixtures::layout(1063),
|
|
]);
|
|
$refusal = ThemeBuilderFixtures::access()->layoutsRefusal(ThemeBuilderFixtures::template());
|
|
self::assertSame(['layouts.body'], array_keys(self::errorFields($refusal)), 'a footer is no body');
|
|
}
|
|
}
|