Files
WP-LIT-MCP/tests/Unit/Integrations/Divi/ThemeBuilder/ThemeBuilderAccessTest.php
T
janniclas.loosenandClaude Opus 5.5 64fc439cb2 feat: lit/tb-activate – the explicit switch, always a dry run first (answer F1)
- only _et_enabled; dry run in both directions lists conditions, exclusions,
  layouts, overlaps and possible overlaps (B9); confirm: true switches
- enabling needs conditions and published layouts (B10); a detached
  template needs reattach first; the default template is refused
- the off-switch reaches an attached, enabled template outside publish,
  e.g. in the trash (ruling Q16); result names the fast way back
- every switch in the change log; failed switch rolled back

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

245 lines
13 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\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));
}
/**
* Ruling Q16: Divi renders an attached, enabled template whatever its post status (B8) – the off-switch
* (offSwitch: true) reaches it in the trash; everything else about it reads like a missing template.
*/
public function testTheOffSwitchReachesALiveTemplateOutsidePublish(): void
{
$trashed = ThemeBuilderFixtures::template(1066, ['enabled' => true, 'status' => 'trash']);
$input = ['template_id' => 1066, 'template_rev' => $trashed->rev()];
LibraryFixtures::given([ThemeBuilderFixtures::templatePost(1066, ['post_status' => 'trash'])]);
$access = ThemeBuilderFixtures::access([], true, ThemeBuilderFixtures::store([$trashed]));
self::assertSame($trashed, $access->changeableTemplate($input, true));
$missing = $access->changeableTemplate($input);
self::assertToolError($missing, 'not_found');
$notLive = [
ThemeBuilderFixtures::access([], true, ThemeBuilderFixtures::store([], 0, [], [$trashed])),
ThemeBuilderFixtures::access([], true, ThemeBuilderFixtures::store([
ThemeBuilderFixtures::template(1066, ['status' => 'trash']),
])),
ThemeBuilderFixtures::access(['theme_builder'], true, ThemeBuilderFixtures::store([$trashed])),
];
foreach ($notLive as $index => $other) {
$refused = $other->changeableTemplate($input, true);
self::assertToolError($refused, 'not_found', (string) $index);
self::assertSame(self::clientError($missing), self::clientError($refused), (string) $index);
}
self::assertToolError(
ThemeBuilderFixtures::access([], false, ThemeBuilderFixtures::store([$trashed]))->changeableTemplate($input, true),
'forbidden'
);
self::assertToolError(
ThemeBuilderFixtures::access([], true, ThemeBuilderFixtures::store([$trashed]))
->changeableTemplate(['template_id' => 1066, 'template_rev' => 'alt'], true),
'rev_mismatch'
);
}
}