fix(audit L4): Restore-Planer für Farben, Variablen und Presets berechnen die Fundstellen (affected, P8a blocked_ids) für geänderte, entfernte und deaktivierte Einträge

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-10-20 00:03:28 +02:00
co-authored by Claude Sonnet 5.5
parent 4a8a889f93
commit bd80ab1394
10 changed files with 274 additions and 26 deletions
+47
View File
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Support;
use LIT\MCP\Integrations\Divi\Library\GlobalUsages;
use LIT\MCP\Integrations\Divi\Library\UsageReport;
use Mockery;
use Mockery\MockInterface;
/** GlobalUsages stand-ins for the design planners (audit L4). */
final class UsageFixture
{
/** Nothing uses anything. */
public static function none(): GlobalUsages&MockInterface
{
return self::reporting(new UsageReport(0, [], 0, 0, [], 0, false));
}
/** Live post 7 uses everything and the caller may not edit it (ruling P8a). */
public static function blocked(): GlobalUsages&MockInterface
{
return self::reporting(self::blockedReport());
}
public static function blockedReport(): UsageReport
{
return new UsageReport(
1,
[['post_id' => 7, 'title' => 'Start', 'status' => 'publish', 'type' => 'page']],
0,
1,
[7],
0,
false
);
}
public static function reporting(UsageReport $report): GlobalUsages&MockInterface
{
$usages = Mockery::mock(GlobalUsages::class);
$usages->allows('reportFor')->andReturn($report);
return $usages;
}
}
@@ -11,7 +11,10 @@ use LIT\MCP\Integrations\Divi\Design\ColorRestorePlanner;
use LIT\MCP\Integrations\Divi\Design\DesignPlan;
use LIT\MCP\Integrations\Divi\Design\DesignSystemWriter;
use LIT\MCP\Integrations\Divi\Design\GlobalColorsAdapter;
use LIT\MCP\Integrations\Divi\Design\ReferenceUsageQuery;
use LIT\MCP\Integrations\Divi\Library\GlobalUsages;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Tests\Support\UsageFixture;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
@@ -41,12 +44,16 @@ final class ColorRestorePlannerTest extends TestCase
return ['gcid-brand00001' => self::color('Marke', '#0000ff')];
}
private static function planner(bool $visualBuilder = true): ColorRestorePlanner
private static function planner(bool $visualBuilder = true, ?GlobalUsages $usages = null): ColorRestorePlanner
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('canUseVisualBuilder')->andReturn($visualBuilder);
return new ColorRestorePlanner($writer, Mockery::mock(GlobalColorsAdapter::class));
return new ColorRestorePlanner(
$writer,
Mockery::mock(GlobalColorsAdapter::class),
$usages ?? UsageFixture::none()
);
}
public function testAreaFeaturesAndDesignArea(): void
@@ -104,4 +111,32 @@ final class ColorRestorePlannerTest extends TestCase
self::assertTrue($plan->isEmpty());
self::assertSame([], $plan->warnings);
}
/** Audit L4: the usages of changed and removed colors (not added ones) go into the plan, so P8a can block. */
public function testReportsThePagesUsingChangedOrRemovedColors(): void
{
$usages = Mockery::mock(GlobalUsages::class);
$usages->expects('reportFor')->once()->with(Mockery::on(
static fn (mixed $query): bool => $query instanceof ReferenceUsageQuery
&& $query->matches('gcid-brand00001') && $query->matches('gcid-akzent0001')
&& ! $query->matches('gcid-other00001')
))->andReturn(UsageFixture::blockedReport());
$plan = self::planner(true, $usages)->plan(self::current(), self::snapshot(), self::SNAPSHOT_ID);
self::assertInstanceOf(DesignPlan::class, $plan);
self::assertSame([7], $plan->usages?->blocked);
self::assertTrue($plan->usages->blocks());
}
public function testNoUsageLookupWithoutAChangedOrRemovedColor(): void
{
$usages = Mockery::mock(GlobalUsages::class);
$usages->expects('reportFor')->never();
$plan = self::planner(true, $usages)->plan(self::snapshot(), self::snapshot(), self::SNAPSHOT_ID);
self::assertInstanceOf(DesignPlan::class, $plan);
self::assertNull($plan->usages);
}
}
@@ -18,9 +18,11 @@ use LIT\MCP\Integrations\Divi\Design\ThemeOptionBook;
use LIT\MCP\Integrations\Divi\Design\ThemeOptionsAdapter;
use LIT\MCP\Integrations\Divi\Design\ThemeOptionsRestorePlanner;
use LIT\MCP\Integrations\Divi\Design\VariableRestorePlanner;
use LIT\MCP\Integrations\Divi\Library\GlobalUsages;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Tests\Support\FakeChangeLog;
use LIT\MCP\Tests\Support\LockTable;
use LIT\MCP\Tests\Support\UsageFixture;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
use Mockery\MockInterface;
@@ -51,7 +53,7 @@ final class DiviDesignRestoreAbilityTest extends TestCase
$ability = new DiviDesignRestoreAbility(
new DesignChange($snapshots, new FakeChangeLog(8)),
$snapshots,
new PresetRestorePlanner($writer)
new PresetRestorePlanner($writer, UsageFixture::none())
);
$dry = self::assertToolError($ability->execute(['snapshot_id' => 'presets-20261016T100000Z-aaaaaa']), 'confirm_required');
@@ -80,7 +82,7 @@ final class DiviDesignRestoreAbilityTest extends TestCase
return new DiviDesignRestoreAbility(
new DesignChange($snapshots, FakeChangeLog::forbidden()),
$snapshots,
new PresetRestorePlanner($writer, static fn (): bool => $unfiltered)
new PresetRestorePlanner($writer, UsageFixture::none(), static fn (): bool => $unfiltered)
);
}
@@ -113,7 +115,7 @@ final class DiviDesignRestoreAbilityTest extends TestCase
$ability = new DiviDesignRestoreAbility(
new DesignChange($snapshots, FakeChangeLog::forbidden()),
$snapshots,
new PresetRestorePlanner($writer)
new PresetRestorePlanner($writer, UsageFixture::none())
);
$input = ['snapshot_id' => 'presets-20261016T100000Z-aaaaaa', 'confirm' => true, 'design_rev' => 'x'];
@@ -148,7 +150,7 @@ final class DiviDesignRestoreAbilityTest extends TestCase
$ability = new DiviDesignRestoreAbility(
new DesignChange($snapshots, FakeChangeLog::forbidden()),
$snapshots,
new PresetRestorePlanner($writer)
new PresetRestorePlanner($writer, UsageFixture::none())
);
$error = self::assertToolError(
@@ -164,7 +166,8 @@ final class DiviDesignRestoreAbilityTest extends TestCase
GlobalColorsAdapter $colors,
GlobalVariablesAdapter $variables,
?FakeChangeLog $log = null,
?ThemeOptionsAdapter $options = null
?ThemeOptionsAdapter $options = null,
?GlobalUsages $usages = null
): DiviDesignRestoreAbility {
Functions\when('current_user_can')->justReturn(true);
$writer = Mockery::mock(DesignSystemWriter::class);
@@ -176,9 +179,9 @@ final class DiviDesignRestoreAbilityTest extends TestCase
return new DiviDesignRestoreAbility(
new DesignChange($snapshots, $log ?? FakeChangeLog::forbidden()),
$snapshots,
new PresetRestorePlanner($writer),
new ColorRestorePlanner($writer, $colors),
new VariableRestorePlanner($writer, $variables),
new PresetRestorePlanner($writer, $usages ?? UsageFixture::none()),
new ColorRestorePlanner($writer, $colors, $usages ?? UsageFixture::none()),
new VariableRestorePlanner($writer, $variables, $usages ?? UsageFixture::none()),
new ThemeOptionsRestorePlanner($options ?? self::optionStore($noOptions))
);
}
@@ -240,6 +243,35 @@ final class DiviDesignRestoreAbilityTest extends TestCase
'log_id' => 2, 'size' => 1];
}
/** Audit L4 / ruling P8a: a colors restore touching a color a live page the caller may not edit uses is refused. */
public function testRestoringAColorUsedOnAnUneditableLivePageIsForbidden(): void
{
$id = ColorRestorePlannerTest::SNAPSHOT_ID;
$colors = self::colorStore(ColorRestorePlannerTest::current());
$colors->shouldNotReceive('save');
$snapshots = Mockery::mock(SnapshotStore::class);
$snapshots->allows('get')->with('colors', $id)
->andReturn(self::snapshotIn('colors', $id, ColorRestorePlannerTest::snapshot()));
$snapshots->shouldNotReceive('save');
$ability = self::allAreas(
$snapshots,
$colors,
self::variableStore([]),
FakeChangeLog::forbidden(),
null,
UsageFixture::blocked()
);
$dry = self::assertToolError($ability->execute(['snapshot_id' => $id]), 'confirm_required');
self::assertSame(7, $dry['affected'][0]['post_id'] ?? null);
$error = self::assertToolError(
$ability->execute(['snapshot_id' => $id, 'confirm' => true, 'design_rev' => $dry['design_rev']]),
'forbidden'
);
self::assertSame([7], $error['blocked_ids']);
}
public function testRestoresColorsWithDryRunAndASnapshotOfTheCurrentColors(): void
{
$id = ColorRestorePlannerTest::SNAPSHOT_ID;
@@ -8,15 +8,21 @@ use LIT\MCP\Integrations\Divi\Design\DesignPlan;
use LIT\MCP\Integrations\Divi\Design\DesignSystemWriter;
use LIT\MCP\Integrations\Divi\Design\PresetArea;
use LIT\MCP\Integrations\Divi\Design\PresetRestorePlanner;
use LIT\MCP\Integrations\Divi\Design\PresetUsageQuery;
use LIT\MCP\Integrations\Divi\Library\GlobalUsages;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Tests\Support\UsageFixture;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
final class PresetRestorePlannerTest extends TestCase
{
private static function planner(bool $unfiltered = true): PresetRestorePlanner
private static function planner(bool $unfiltered = true, ?GlobalUsages $usages = null): PresetRestorePlanner
{
return new PresetRestorePlanner(Mockery::mock(DesignSystemWriter::class), static fn (): bool => $unfiltered);
return new PresetRestorePlanner(
Mockery::mock(DesignSystemWriter::class),
$usages ?? UsageFixture::none(),
static fn (): bool => $unfiltered);
}
public function testAreaFeaturesAndDesignArea(): void
@@ -51,4 +57,36 @@ final class PresetRestorePlannerTest extends TestCase
self::assertToolError($plan, 'forbidden');
}
/** Audit L4: removed and changed presets are looked up (spec §5 orphan warning stays); P8a can block. */
public function testReportsThePagesUsingRemovedPresets(): void
{
$older = PresetBookTest::stored();
unset($older['module']['divi/button']['items']['bbbbbbbbbb']);
$older['module']['divi/button']['items']['aaaaaaaaaa']['attrs'] = ['button' => ['x' => 2]];
$usages = Mockery::mock(GlobalUsages::class);
$usages->expects('reportFor')->once()->with(Mockery::on(
static fn (mixed $query): bool => $query instanceof PresetUsageQuery
&& $query->matches('<!-- wp:divi/button {"modulePreset":["bbbbbbbbbb"]} /-->')
&& $query->matches('<!-- wp:divi/button {"modulePreset":["aaaaaaaaaa"]} /-->')
&& ! $query->matches('<!-- wp:divi/button {"modulePreset":["cccccccccc"]} /-->')
))->andReturn(UsageFixture::blockedReport());
$plan = self::planner(true, $usages)->plan(PresetBookTest::stored(), $older, 'presets-1');
self::assertInstanceOf(DesignPlan::class, $plan);
self::assertTrue($plan->usages?->blocks());
self::assertStringContainsString('Standard-Preset', $plan->warnings[0]);
}
public function testNoUsageLookupWithoutChangedOrRemovedPresets(): void
{
$usages = Mockery::mock(GlobalUsages::class);
$usages->expects('reportFor')->never();
$plan = self::planner(true, $usages)->plan(PresetBookTest::stored(), PresetBookTest::stored(), 'presets-1');
self::assertInstanceOf(DesignPlan::class, $plan);
self::assertNull($plan->usages);
}
}
@@ -11,7 +11,10 @@ use LIT\MCP\Integrations\Divi\Design\GlobalVariablesAdapter;
use LIT\MCP\Integrations\Divi\Design\VariableArea;
use LIT\MCP\Integrations\Divi\Design\VariableBook;
use LIT\MCP\Integrations\Divi\Design\VariableRestorePlanner;
use LIT\MCP\Integrations\Divi\Design\ReferenceUsageQuery;
use LIT\MCP\Integrations\Divi\Library\GlobalUsages;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Tests\Support\UsageFixture;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
@@ -41,14 +44,14 @@ final class VariableRestorePlannerTest extends TestCase
return ['numbers' => ['gvid-radius0001' => self::number('gvid-radius0001', 'Eckenradius', '4px')]];
}
private static function planner(bool $manage = true): VariableRestorePlanner
private static function planner(bool $manage = true, ?GlobalUsages $usages = null): VariableRestorePlanner
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('canUseVisualBuilder')->andReturn(true);
$variables = Mockery::mock(GlobalVariablesAdapter::class);
$variables->allows('canManage')->andReturn($manage);
return new VariableRestorePlanner($writer, $variables);
return new VariableRestorePlanner($writer, $variables, $usages ?? UsageFixture::none());
}
public function testAreaFeaturesAndDesignArea(): void
@@ -89,4 +92,19 @@ final class VariableRestorePlannerTest extends TestCase
$plan->warnings
);
}
/** Audit L4: changed and archived variables are looked up; the plan carries the report for P8a. */
public function testReportsThePagesUsingChangedOrArchivedVariables(): void
{
$usages = Mockery::mock(GlobalUsages::class);
$usages->expects('reportFor')->once()->with(Mockery::on(
static fn (mixed $query): bool => $query instanceof ReferenceUsageQuery
&& $query->matches('gvid-radius0001') && $query->matches('gvid-neu0000001')
))->andReturn(UsageFixture::blockedReport());
$plan = self::planner(true, $usages)->plan(self::current(), self::snapshot(), self::SNAPSHOT_ID);
self::assertInstanceOf(DesignPlan::class, $plan);
self::assertTrue($plan->usages?->blocks());
}
}