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
@@ -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;