Files
WP-LIT-MCP/tests/Unit/Integrations/Divi/ThemeBuilder/ThemeBuilderAccessTest.php
T
janniclas.loosenandClaude Opus 5.5 0975b2f69b feat: lit/tb-assign – conditions with dry run on live templates, reattach
- ThemeBuilderAccess::changeableTemplate(): rights (Q18 not_found), group gate,
  no TB draft, never the default template, current template_rev
- omitted exclude_from stays, [] clears (Q3); live dry run shows use_on and
  exclude_from before/after, overlaps and possible overlaps (B9)
- reattach only with published layouts (B10) and a live TB post; always
  disabled, unmarks template and layouts; failure restores everything

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

206 lines
11 KiB
PHP

<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\ThemeBuilder;
use Brain\Monkey\Functions;
use LIT\MCP\Integrations\Divi\ThemeBuilder\Template;
use LIT\MCP\Integrations\WordPress\PostGuard;
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');
}
/** Ruling Q15: Divi's Theme Builder page also needs edit_theme_options (theme-builder.php:2059-2063). */
public function testWithoutEditThemeOptionsTheThemeBuilderIsHidden(): void
{
LibraryFixtures::given([ThemeBuilderFixtures::layout(1062), ThemeBuilderFixtures::templatePost()], [
'edit_theme_options',
]);
$access = ThemeBuilderFixtures::access();
self::assertToolError($access->canUse(), 'forbidden');
self::assertStringContainsString('edit_theme_options', self::clientError($access->canUse())['message']);
self::assertFalse($access->visible());
self::assertFalse($access->canEditTemplate(ThemeBuilderFixtures::TEMPLATE));
self::assertToolError($access->canCreate(), 'forbidden');
$hidden = $access->layout(1062, false);
self::assertSame(self::clientError($hidden), self::clientError(PostGuard::notFound(1062)));
$template = self::assertToolError($access->template(ThemeBuilderFixtures::TEMPLATE, false), 'not_found');
self::assertSame('Theme-Builder-Vorlage 1066 gibt es nicht.', $template['message'], 'like a missing ID');
}
/** 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');
}
/** Ruling Q18: a template the user may not edit answers exactly like a missing one (like tb-list, Q8/B11). */
public function testATemplateTheUserMayNotEditReadsLikeAMissingOne(): void
{
LibraryFixtures::given([ThemeBuilderFixtures::templatePost()], ['edit_post']);
foreach ([false, true] as $forWrite) {
$hidden = ThemeBuilderFixtures::access()->template(ThemeBuilderFixtures::TEMPLATE, $forWrite);
$missing = ThemeBuilderFixtures::access()->template(1067, $forWrite);
self::assertToolError($hidden, 'not_found');
self::assertSame(
str_replace('1067', '1066', (string) json_encode(self::clientError($missing))),
(string) json_encode(self::clientError($hidden))
);
}
}
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');
}
public function testChangeableTemplateNeedsRightsNoDraftNoDefaultAndTheCurrentRev(): void
{
LibraryFixtures::given([ThemeBuilderFixtures::templatePost(1066), ThemeBuilderFixtures::templatePost(1065)]);
$store = ThemeBuilderFixtures::store([ThemeBuilderFixtures::template(1065), ThemeBuilderFixtures::template(1066)]);
$access = ThemeBuilderFixtures::access([], true, $store);
$rev = ThemeBuilderFixtures::template(1066)->rev();
self::assertInstanceOf(Template::class, $access->changeableTemplate(['template_id' => 1066, 'template_rev' => $rev]));
self::assertToolError($access->changeableTemplate(['template_id' => 1066, 'template_rev' => 'alt']), 'rev_mismatch');
self::assertToolError(
$access->changeableTemplate(['template_id' => 1065, 'template_rev' => ThemeBuilderFixtures::template(1065)->rev()]),
'unsupported'
);
$draft = ThemeBuilderFixtures::access([], true, ThemeBuilderFixtures::store([ThemeBuilderFixtures::template(1066)], 999));
self::assertToolError($draft->changeableTemplate(['template_id' => 1066, 'template_rev' => $rev]), 'unsupported');
self::assertToolError(
ThemeBuilderFixtures::access([], false, $store)->changeableTemplate(['template_id' => 1066, 'template_rev' => $rev]),
'forbidden'
);
}
/** Ruling Q18: a template the user may not edit answers like a missing one, also for writing. */
public function testChangeableTemplateHidesTemplatesTheUserMayNotEdit(): void
{
$store = ThemeBuilderFixtures::store([ThemeBuilderFixtures::template(1066)]);
$rev = ThemeBuilderFixtures::template(1066)->rev();
LibraryFixtures::given([ThemeBuilderFixtures::templatePost(1066)], ['edit_post']);
$hidden = ThemeBuilderFixtures::access([], true, $store)->changeableTemplate(['template_id' => 1066, 'template_rev' => $rev]);
LibraryFixtures::given([]);
$missing = ThemeBuilderFixtures::access([], true, $store)->changeableTemplate(['template_id' => 1066, 'template_rev' => $rev]);
self::assertToolError($hidden, 'not_found');
self::assertSame(self::clientError($missing), self::clientError($hidden));
}
}