Files
WP-LIT-MCP/tests/Unit/Integrations/Divi/ThemeBuilder/TbActivateAbilityTest.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

247 lines
11 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\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']);
}
}