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>
This commit is contained in:
@@ -78,7 +78,7 @@ final class DiviIntegrationTest extends TestCase
|
||||
self::assertSame('Divi Theme Builder', $group->label());
|
||||
self::assertFalse($group->isAvailable(), 'no Divi classes in unit tests');
|
||||
self::assertSame(
|
||||
['lit/tb-list', 'lit/tb-create-template', 'lit/tb-assign'],
|
||||
['lit/tb-list', 'lit/tb-create-template', 'lit/tb-assign', 'lit/tb-activate'],
|
||||
array_map(static fn (Ability $ability): string => $ability->name(), $group->abilities())
|
||||
);
|
||||
foreach ($group->abilities() as $ability) {
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\ThemeBuilder;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use LIT\MCP\Abilities\WriteFailed;
|
||||
use LIT\MCP\Integrations\Divi\ThemeBuilder\DiviThemeBuilder;
|
||||
use LIT\MCP\Integrations\Divi\ThemeBuilder\TbActivateAbility;
|
||||
use LIT\MCP\Integrations\Divi\ThemeBuilder\Template;
|
||||
use LIT\MCP\Integrations\Divi\ThemeBuilder\TemplateConditions;
|
||||
use LIT\MCP\Tests\Support\DiviWriterFixture;
|
||||
use LIT\MCP\Tests\Support\FakeChangeLog;
|
||||
use LIT\MCP\Tests\Support\LibraryFixtures;
|
||||
use LIT\MCP\Tests\Support\ThemeBuilderFixtures;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use RuntimeException;
|
||||
|
||||
final class TbActivateAbilityTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
self::givenPosts();
|
||||
Functions\when('admin_url')->alias(static fn (string $path): string => 'https://kunde.test/wp-admin/' . $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $templateFields
|
||||
* @param array<string, mixed> $bodyFields
|
||||
*/
|
||||
private static function givenPosts(array $templateFields = [], array $bodyFields = []): void
|
||||
{
|
||||
LibraryFixtures::given([
|
||||
ThemeBuilderFixtures::templatePost(1066, $templateFields),
|
||||
ThemeBuilderFixtures::templatePost(1065),
|
||||
ThemeBuilderFixtures::layout(1061),
|
||||
ThemeBuilderFixtures::layout(1062, $bodyFields),
|
||||
ThemeBuilderFixtures::layout(1063),
|
||||
]);
|
||||
}
|
||||
|
||||
private static function ability(DiviThemeBuilder $store, ?FakeChangeLog $log = null): TbActivateAbility
|
||||
{
|
||||
return new TbActivateAbility(
|
||||
ThemeBuilderFixtures::access([], true, $store),
|
||||
new TemplateConditions($store),
|
||||
$log ?? DiviWriterFixture::log()
|
||||
);
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private static function input(Template $template, bool $enabled, bool $confirm = false): array
|
||||
{
|
||||
return array_filter([
|
||||
'template_id' => $template->id,
|
||||
'template_rev' => $template->rev(),
|
||||
'enabled' => $enabled,
|
||||
'confirm' => $confirm ?: null,
|
||||
], static fn (mixed $value): bool => $value !== null);
|
||||
}
|
||||
|
||||
public function testActivationIsAlwaysADryRunFirst(): void
|
||||
{
|
||||
$template = ThemeBuilderFixtures::template(1066);
|
||||
$other = ThemeBuilderFixtures::template(1070, [
|
||||
'enabled' => true,
|
||||
'title' => 'Seite 1053',
|
||||
'use_on' => ['singular:post_type:page:id:1053'],
|
||||
]);
|
||||
$pages = ThemeBuilderFixtures::template(1071, [
|
||||
'enabled' => true,
|
||||
'title' => 'Alle Seiten',
|
||||
'use_on' => ['singular:post_type:page:all'],
|
||||
]);
|
||||
$store = ThemeBuilderFixtures::store([$template, $other, $pages]);
|
||||
$store->shouldReceive('setEnabled')->never();
|
||||
|
||||
$dry = self::ability($store, FakeChangeLog::forbidden())->execute(self::input($template, true));
|
||||
|
||||
self::assertToolError($dry, 'confirm_required');
|
||||
self::assertStringContainsString('Aktivieren', self::clientError($dry)['message']);
|
||||
self::assertSame(['Bestimmte Seiten: 1053'], $dry->get_error_data()['use_on']);
|
||||
self::assertSame([], $dry->get_error_data()['exclude_from']);
|
||||
self::assertSame(1070, $dry->get_error_data()['overlaps'][0]['id']);
|
||||
self::assertSame(1071, $dry->get_error_data()['possible_overlaps'][0]['id']);
|
||||
self::assertSame(1062, $dry->get_error_data()['layouts']['body']['id']);
|
||||
}
|
||||
|
||||
public function testConfirmedActivationSwitchesOnlyTheFlag(): void
|
||||
{
|
||||
$template = ThemeBuilderFixtures::template(1066);
|
||||
$store = ThemeBuilderFixtures::store([$template]);
|
||||
$store->shouldReceive('setEnabled')->once()->with(1066, true);
|
||||
$store->shouldReceive('clearCaches')->once();
|
||||
$store->shouldReceive('setConditions')->never();
|
||||
$store->shouldReceive('attach')->never();
|
||||
$log = DiviWriterFixture::log();
|
||||
|
||||
$result = self::ability($store, $log)->execute(self::input($template, true, true));
|
||||
|
||||
self::assertTrue($result['ok']);
|
||||
self::assertTrue($result['enabled']);
|
||||
self::assertTrue($result['live']);
|
||||
self::assertStringContainsString('aktiviert', $result['summary']);
|
||||
self::assertStringContainsString('enabled: false', $result['summary'], 'the way back is named');
|
||||
self::assertSame(4711, $result['log_id']);
|
||||
self::assertStringContainsString('aktiviert', $log->summaries()[0]);
|
||||
}
|
||||
|
||||
public function testDeactivationAlsoNeedsConfirm(): void
|
||||
{
|
||||
$template = ThemeBuilderFixtures::template(1066, ['enabled' => true]);
|
||||
$store = ThemeBuilderFixtures::store([$template]);
|
||||
$store->shouldReceive('setEnabled')->once()->with(1066, false);
|
||||
$store->shouldReceive('clearCaches')->once();
|
||||
|
||||
$dry = self::ability($store)->execute(self::input($template, false));
|
||||
self::assertToolError($dry, 'confirm_required');
|
||||
self::assertStringContainsString('Deaktivieren', self::clientError($dry)['message']);
|
||||
self::assertSame([], $dry->get_error_data()['overlaps']);
|
||||
|
||||
$result = self::ability($store)->execute(self::input($template, false, true));
|
||||
self::assertTrue($result['ok']);
|
||||
self::assertFalse($result['enabled']);
|
||||
self::assertFalse($result['live']);
|
||||
}
|
||||
|
||||
/** Ruling Q16: an attached, enabled template in the trash is still rendered – the off-switch reaches it. */
|
||||
public function testDeactivatesALiveTemplateInTheTrash(): void
|
||||
{
|
||||
self::givenPosts(['post_status' => 'trash']);
|
||||
$trashed = ThemeBuilderFixtures::template(1066, ['enabled' => true, 'status' => 'trash']);
|
||||
$store = ThemeBuilderFixtures::store([$trashed]);
|
||||
$store->shouldReceive('setEnabled')->once()->with(1066, false);
|
||||
$store->shouldReceive('clearCaches')->once();
|
||||
|
||||
self::assertTrue(self::ability($store)->permission(self::input($trashed, false)));
|
||||
self::assertToolError(self::ability($store)->permission(self::input($trashed, true)), 'not_found');
|
||||
self::assertToolError(self::ability($store)->execute(self::input($trashed, true, true)), 'not_found');
|
||||
$dry = self::ability($store)->execute(self::input($trashed, false));
|
||||
self::assertToolError($dry, 'confirm_required');
|
||||
self::assertStringContainsString('Papierkorb', self::clientError($dry)['message']);
|
||||
self::assertTrue(self::ability($store)->execute(self::input($trashed, false, true))['ok']);
|
||||
}
|
||||
|
||||
/** Ruling Q8/B10: enabling needs published layouts; switching off never does. */
|
||||
public function testActivationNeedsPublishedLayoutsDeactivationDoesNot(): void
|
||||
{
|
||||
self::givenPosts([], ['post_status' => 'trash']);
|
||||
$off = ThemeBuilderFixtures::template(1066);
|
||||
$store = ThemeBuilderFixtures::store([$off]);
|
||||
$store->shouldReceive('setEnabled')->never();
|
||||
$refused = self::ability($store, FakeChangeLog::forbidden())->execute(self::input($off, true));
|
||||
self::assertToolError($refused, 'validation_failed');
|
||||
self::assertSame(['layouts.body'], array_keys(self::errorFields($refused)));
|
||||
|
||||
$on = ThemeBuilderFixtures::template(1066, ['enabled' => true]);
|
||||
$store = ThemeBuilderFixtures::store([$on]);
|
||||
$store->shouldReceive('setEnabled')->once()->with(1066, false);
|
||||
$store->shouldReceive('clearCaches');
|
||||
self::assertTrue(self::ability($store)->execute(self::input($on, false, true))['ok']);
|
||||
}
|
||||
|
||||
public function testRefusesTemplatesWithoutConditionsOrDetached(): void
|
||||
{
|
||||
$empty = ThemeBuilderFixtures::template(1066, ['use_on' => []]);
|
||||
$store = ThemeBuilderFixtures::store([$empty]);
|
||||
$store->shouldReceive('setEnabled')->never();
|
||||
self::assertToolError(self::ability($store)->execute(self::input($empty, true, true)), 'validation_failed');
|
||||
|
||||
$detached = ThemeBuilderFixtures::template(1066, ['attached' => false]);
|
||||
$store = ThemeBuilderFixtures::store([], 0, [$detached]);
|
||||
$store->shouldReceive('setEnabled')->never();
|
||||
$refused = self::ability($store)->execute(self::input($detached, true, true));
|
||||
self::assertToolError($refused, 'validation_failed');
|
||||
self::assertStringContainsString('reattach', (string) json_encode(self::errorFields($refused)));
|
||||
}
|
||||
|
||||
public function testNoChangeWhenAlreadyInThatState(): void
|
||||
{
|
||||
$template = ThemeBuilderFixtures::template(1066);
|
||||
$store = ThemeBuilderFixtures::store([$template]);
|
||||
$store->shouldReceive('setEnabled')->never();
|
||||
|
||||
$result = self::ability($store, FakeChangeLog::forbidden())->execute(self::input($template, false));
|
||||
|
||||
self::assertStringContainsString('Keine Änderung', $result['summary']);
|
||||
self::assertTrue(self::ability($store)->annotations()['destructive']);
|
||||
self::assertFalse(self::ability($store)->isDirect());
|
||||
}
|
||||
|
||||
public function testMissingEnabledAndTheDefaultTemplateAreRefused(): void
|
||||
{
|
||||
$template = ThemeBuilderFixtures::template(1066);
|
||||
$store = ThemeBuilderFixtures::store([ThemeBuilderFixtures::template(1065), $template]);
|
||||
$store->shouldReceive('setEnabled')->never();
|
||||
$input = self::input($template, true, true);
|
||||
unset($input['enabled']);
|
||||
|
||||
self::assertToolError(self::ability($store)->execute($input), 'validation_failed');
|
||||
self::assertToolError(
|
||||
self::ability($store)->execute(self::input(ThemeBuilderFixtures::template(1065), true, true)),
|
||||
'unsupported'
|
||||
);
|
||||
}
|
||||
|
||||
public function testAFailedSwitchIsRolledBack(): void
|
||||
{
|
||||
$template = ThemeBuilderFixtures::template(1066);
|
||||
$store = ThemeBuilderFixtures::store([$template]);
|
||||
$calls = [];
|
||||
$store->shouldReceive('setEnabled')->andReturnUsing(static function (int $id, bool $on) use (&$calls): void {
|
||||
$calls[] = $on;
|
||||
if ($on) {
|
||||
throw new RuntimeException('_et_enabled not stored');
|
||||
}
|
||||
});
|
||||
$store->shouldReceive('clearCaches')->never();
|
||||
|
||||
try {
|
||||
self::ability($store, FakeChangeLog::forbidden())->execute(self::input($template, true, true));
|
||||
self::fail('no WriteFailed');
|
||||
} catch (WriteFailed $e) {
|
||||
self::assertTrue($e->rolledBack);
|
||||
}
|
||||
self::assertSame([true, false], $calls);
|
||||
}
|
||||
|
||||
public function testAFailedRollbackIsAPartialWrite(): void
|
||||
{
|
||||
$template = ThemeBuilderFixtures::template(1066);
|
||||
$store = ThemeBuilderFixtures::store([$template]);
|
||||
$store->shouldReceive('setEnabled')->andThrow(new RuntimeException('database gone'));
|
||||
$log = FakeChangeLog::partialsOnly();
|
||||
|
||||
try {
|
||||
self::ability($store, $log)->execute(self::input($template, true, true));
|
||||
self::fail('no WriteFailed');
|
||||
} catch (WriteFailed $e) {
|
||||
self::assertFalse($e->rolledBack);
|
||||
}
|
||||
$log->assertPartials(['ability' => 'lit/tb-activate', 'object_type' => 'et_template', 'object_id' => '1066']);
|
||||
}
|
||||
}
|
||||
@@ -202,4 +202,43 @@ final class ThemeBuilderAccessTest extends TestCase
|
||||
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'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user