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:
co-authored by
Claude Sonnet 5.5
parent
4a8a889f93
commit
bd80ab1394
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace LIT\MCP\Integrations\Divi\Design;
|
namespace LIT\MCP\Integrations\Divi\Design;
|
||||||
|
|
||||||
|
use LIT\MCP\Integrations\Divi\Library\GlobalUsages;
|
||||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||||
use WP_Error;
|
use WP_Error;
|
||||||
|
|
||||||
@@ -19,8 +20,11 @@ final class ColorRestorePlanner implements RestorePlanner
|
|||||||
private const REMOVED = 'Farben, die es im Snapshot nicht gibt, entfallen (%s); Seiten und Presets, die sie '
|
private const REMOVED = 'Farben, die es im Snapshot nicht gibt, entfallen (%s); Seiten und Presets, die sie '
|
||||||
. 'verwenden, behalten den Verweis ohne Wert.';
|
. 'verwenden, behalten den Verweis ohne Wert.';
|
||||||
|
|
||||||
public function __construct(private DesignSystemWriter $writer, private GlobalColorsAdapter $colors)
|
public function __construct(
|
||||||
{
|
private DesignSystemWriter $writer,
|
||||||
|
private GlobalColorsAdapter $colors,
|
||||||
|
private GlobalUsages $usages
|
||||||
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function area(): string
|
public function area(): string
|
||||||
@@ -64,6 +68,13 @@ final class ColorRestorePlanner implements RestorePlanner
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new DesignPlan($after->data(), $diff, $summary, null, $warnings, true);
|
return new DesignPlan(
|
||||||
|
$after->data(),
|
||||||
|
$diff,
|
||||||
|
$summary,
|
||||||
|
RestoreUsages::ofReferences($this->usages, $diff),
|
||||||
|
$warnings,
|
||||||
|
true
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ declare(strict_types=1);
|
|||||||
namespace LIT\MCP\Integrations\Divi\Design;
|
namespace LIT\MCP\Integrations\Divi\Design;
|
||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
|
use LIT\MCP\Integrations\Divi\Library\GlobalUsages;
|
||||||
|
use LIT\MCP\Integrations\Divi\Library\UsageReport;
|
||||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||||
use WP_Error;
|
use WP_Error;
|
||||||
|
|
||||||
@@ -21,8 +23,11 @@ final class PresetRestorePlanner implements RestorePlanner
|
|||||||
. 'ohne das Recht unfiltered_html verloren ginge; nichts wurde gespeichert.';
|
. 'ohne das Recht unfiltered_html verloren ginge; nichts wurde gespeichert.';
|
||||||
|
|
||||||
/** @param (Closure(): bool)|null $unfiltered Whether the user may save unfiltered HTML (DesignRights). */
|
/** @param (Closure(): bool)|null $unfiltered Whether the user may save unfiltered HTML (DesignRights). */
|
||||||
public function __construct(private DesignSystemWriter $writer, private ?Closure $unfiltered = null)
|
public function __construct(
|
||||||
{
|
private DesignSystemWriter $writer,
|
||||||
|
private GlobalUsages $usages,
|
||||||
|
private ?Closure $unfiltered = null
|
||||||
|
) {
|
||||||
$this->unfiltered ??= DesignRights::unfilteredHtml();
|
$this->unfiltered ??= DesignRights::unfilteredHtml();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,10 +70,32 @@ final class PresetRestorePlanner implements RestorePlanner
|
|||||||
$after->data(),
|
$after->data(),
|
||||||
$diff,
|
$diff,
|
||||||
sprintf('Snapshot %s wiederherstellen – %s', $snapshotId, PresetDiff::summary($diff)),
|
sprintf('Snapshot %s wiederherstellen – %s', $snapshotId, PresetDiff::summary($diff)),
|
||||||
null,
|
$this->usagesOf($before, $diff),
|
||||||
['Seiten, die Presets verwenden, die es im Snapshot nicht gibt, behalten den Verweis; das Modul fällt '
|
['Seiten, die Presets verwenden, die es im Snapshot nicht gibt, behalten den Verweis; das Modul fällt '
|
||||||
. 'dann auf das Standard-Preset zurück.'],
|
. 'dann auf das Standard-Preset zurück.'],
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Audit L4: the pages using a preset the restore changes or removes (spec §4 step 4, ruling P8a); a default
|
||||||
|
* preset also counts its implicit users (like lit/divi-presets-delete). The orphan warning stays (spec §5).
|
||||||
|
*
|
||||||
|
* @param list<array<string, mixed>> $diff
|
||||||
|
*/
|
||||||
|
private function usagesOf(PresetBook $before, array $diff): ?UsageReport
|
||||||
|
{
|
||||||
|
$targets = [];
|
||||||
|
foreach ($diff as $row) {
|
||||||
|
if (in_array($row['action'], RestoreUsages::TOUCHING, true)) {
|
||||||
|
$scope = new PresetScope((string) $row['kind'], (string) $row['scope']);
|
||||||
|
$targets[] = ['scope' => $scope, 'id' => (string) $row['id'],
|
||||||
|
'default' => $before->defaultId($scope) === $row['id']];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $targets === []
|
||||||
|
? null
|
||||||
|
: $this->usages->reportFor(PresetUsageQuery::withGroups($targets, $this->writer->groupsOf(...)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Integrations\Divi\Design;
|
||||||
|
|
||||||
|
use LIT\MCP\Integrations\Divi\Library\GlobalUsages;
|
||||||
|
use LIT\MCP\Integrations\Divi\Library\UsageReport;
|
||||||
|
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The usages a snapshot restore has to report (audit L4, spec §4 step 4): the pages that use an entry the restore
|
||||||
|
* changes, removes or deactivates – DesignChange then blocks the confirmed restore for pages the caller may not edit
|
||||||
|
* (ruling P8a) and the dry run lists them as "affected".
|
||||||
|
*/
|
||||||
|
final class RestoreUsages
|
||||||
|
{
|
||||||
|
/** Diff actions that change what a page shows (colors: deactivated, variables: archived). */
|
||||||
|
public const TOUCHING = ['changed', 'removed', 'deactivated', 'archived'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<array<string, mixed>> $diff Rows with "id" (gcid-/gvid-) and "action"
|
||||||
|
*/
|
||||||
|
public static function ofReferences(GlobalUsages $usages, array $diff): ?UsageReport
|
||||||
|
{
|
||||||
|
$ids = [];
|
||||||
|
foreach ($diff as $row) {
|
||||||
|
if (in_array($row['action'], self::TOUCHING, true)) {
|
||||||
|
$ids[] = (string) $row['id'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ids === [] ? null : $usages->reportFor(new ReferenceUsageQuery(array_values(array_unique($ids))));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace LIT\MCP\Integrations\Divi\Design;
|
namespace LIT\MCP\Integrations\Divi\Design;
|
||||||
|
|
||||||
|
use LIT\MCP\Integrations\Divi\Library\GlobalUsages;
|
||||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||||
use WP_Error;
|
use WP_Error;
|
||||||
|
|
||||||
@@ -19,8 +20,11 @@ final class VariableRestorePlanner implements RestorePlanner
|
|||||||
private const KEPT = 'Variable „%s“ gibt es im Snapshot nicht; sie bleibt archiviert erhalten (Divi löscht keine '
|
private const KEPT = 'Variable „%s“ gibt es im Snapshot nicht; sie bleibt archiviert erhalten (Divi löscht keine '
|
||||||
. 'Variablen).';
|
. 'Variablen).';
|
||||||
|
|
||||||
public function __construct(private DesignSystemWriter $writer, private GlobalVariablesAdapter $variables)
|
public function __construct(
|
||||||
{
|
private DesignSystemWriter $writer,
|
||||||
|
private GlobalVariablesAdapter $variables,
|
||||||
|
private GlobalUsages $usages
|
||||||
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function area(): string
|
public function area(): string
|
||||||
@@ -55,7 +59,7 @@ final class VariableRestorePlanner implements RestorePlanner
|
|||||||
$after->data(),
|
$after->data(),
|
||||||
$diff,
|
$diff,
|
||||||
sprintf('Snapshot %s wiederherstellen – %s', $snapshotId, VariableDiff::summary($diff)),
|
sprintf('Snapshot %s wiederherstellen – %s', $snapshotId, VariableDiff::summary($diff)),
|
||||||
null,
|
RestoreUsages::ofReferences($this->usages, $diff),
|
||||||
array_map(static fn (array $row): string => sprintf(self::KEPT, $row['label']), $before->onlyHere($older))
|
array_map(static fn (array $row): string => sprintf(self::KEPT, $row['label']), $before->onlyHere($older))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -301,9 +301,9 @@ final class DiviIntegration implements Integration, Bootable
|
|||||||
new DiviDesignRestoreAbility(
|
new DiviDesignRestoreAbility(
|
||||||
$this->designChange(),
|
$this->designChange(),
|
||||||
new SnapshotStore(),
|
new SnapshotStore(),
|
||||||
new PresetRestorePlanner($this->designWriter, DesignRights::unfilteredHtml()),
|
new PresetRestorePlanner($this->designWriter, $this->globalUsages(), DesignRights::unfilteredHtml()),
|
||||||
new ColorRestorePlanner($this->designWriter, $this->colorsAdapter),
|
new ColorRestorePlanner($this->designWriter, $this->colorsAdapter, $this->globalUsages()),
|
||||||
new VariableRestorePlanner($this->designWriter, $this->variablesAdapter),
|
new VariableRestorePlanner($this->designWriter, $this->variablesAdapter, $this->globalUsages()),
|
||||||
new ThemeOptionsRestorePlanner($this->themeOptionsAdapter)
|
new ThemeOptionsRestorePlanner($this->themeOptionsAdapter)
|
||||||
),
|
),
|
||||||
new DiviPresetsAssignAbility(
|
new DiviPresetsAssignAbility(
|
||||||
|
|||||||
@@ -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\DesignPlan;
|
||||||
use LIT\MCP\Integrations\Divi\Design\DesignSystemWriter;
|
use LIT\MCP\Integrations\Divi\Design\DesignSystemWriter;
|
||||||
use LIT\MCP\Integrations\Divi\Design\GlobalColorsAdapter;
|
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\Integrations\Divi\Release\DiviFeature;
|
||||||
|
use LIT\MCP\Tests\Support\UsageFixture;
|
||||||
use LIT\MCP\Tests\Unit\TestCase;
|
use LIT\MCP\Tests\Unit\TestCase;
|
||||||
use Mockery;
|
use Mockery;
|
||||||
|
|
||||||
@@ -41,12 +44,16 @@ final class ColorRestorePlannerTest extends TestCase
|
|||||||
return ['gcid-brand00001' => self::color('Marke', '#0000ff')];
|
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 = Mockery::mock(DesignSystemWriter::class);
|
||||||
$writer->allows('canUseVisualBuilder')->andReturn($visualBuilder);
|
$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
|
public function testAreaFeaturesAndDesignArea(): void
|
||||||
@@ -104,4 +111,32 @@ final class ColorRestorePlannerTest extends TestCase
|
|||||||
self::assertTrue($plan->isEmpty());
|
self::assertTrue($plan->isEmpty());
|
||||||
self::assertSame([], $plan->warnings);
|
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\ThemeOptionsAdapter;
|
||||||
use LIT\MCP\Integrations\Divi\Design\ThemeOptionsRestorePlanner;
|
use LIT\MCP\Integrations\Divi\Design\ThemeOptionsRestorePlanner;
|
||||||
use LIT\MCP\Integrations\Divi\Design\VariableRestorePlanner;
|
use LIT\MCP\Integrations\Divi\Design\VariableRestorePlanner;
|
||||||
|
use LIT\MCP\Integrations\Divi\Library\GlobalUsages;
|
||||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||||
use LIT\MCP\Tests\Support\FakeChangeLog;
|
use LIT\MCP\Tests\Support\FakeChangeLog;
|
||||||
use LIT\MCP\Tests\Support\LockTable;
|
use LIT\MCP\Tests\Support\LockTable;
|
||||||
|
use LIT\MCP\Tests\Support\UsageFixture;
|
||||||
use LIT\MCP\Tests\Unit\TestCase;
|
use LIT\MCP\Tests\Unit\TestCase;
|
||||||
use Mockery;
|
use Mockery;
|
||||||
use Mockery\MockInterface;
|
use Mockery\MockInterface;
|
||||||
@@ -51,7 +53,7 @@ final class DiviDesignRestoreAbilityTest extends TestCase
|
|||||||
$ability = new DiviDesignRestoreAbility(
|
$ability = new DiviDesignRestoreAbility(
|
||||||
new DesignChange($snapshots, new FakeChangeLog(8)),
|
new DesignChange($snapshots, new FakeChangeLog(8)),
|
||||||
$snapshots,
|
$snapshots,
|
||||||
new PresetRestorePlanner($writer)
|
new PresetRestorePlanner($writer, UsageFixture::none())
|
||||||
);
|
);
|
||||||
|
|
||||||
$dry = self::assertToolError($ability->execute(['snapshot_id' => 'presets-20261016T100000Z-aaaaaa']), 'confirm_required');
|
$dry = self::assertToolError($ability->execute(['snapshot_id' => 'presets-20261016T100000Z-aaaaaa']), 'confirm_required');
|
||||||
@@ -80,7 +82,7 @@ final class DiviDesignRestoreAbilityTest extends TestCase
|
|||||||
return new DiviDesignRestoreAbility(
|
return new DiviDesignRestoreAbility(
|
||||||
new DesignChange($snapshots, FakeChangeLog::forbidden()),
|
new DesignChange($snapshots, FakeChangeLog::forbidden()),
|
||||||
$snapshots,
|
$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(
|
$ability = new DiviDesignRestoreAbility(
|
||||||
new DesignChange($snapshots, FakeChangeLog::forbidden()),
|
new DesignChange($snapshots, FakeChangeLog::forbidden()),
|
||||||
$snapshots,
|
$snapshots,
|
||||||
new PresetRestorePlanner($writer)
|
new PresetRestorePlanner($writer, UsageFixture::none())
|
||||||
);
|
);
|
||||||
$input = ['snapshot_id' => 'presets-20261016T100000Z-aaaaaa', 'confirm' => true, 'design_rev' => 'x'];
|
$input = ['snapshot_id' => 'presets-20261016T100000Z-aaaaaa', 'confirm' => true, 'design_rev' => 'x'];
|
||||||
|
|
||||||
@@ -148,7 +150,7 @@ final class DiviDesignRestoreAbilityTest extends TestCase
|
|||||||
$ability = new DiviDesignRestoreAbility(
|
$ability = new DiviDesignRestoreAbility(
|
||||||
new DesignChange($snapshots, FakeChangeLog::forbidden()),
|
new DesignChange($snapshots, FakeChangeLog::forbidden()),
|
||||||
$snapshots,
|
$snapshots,
|
||||||
new PresetRestorePlanner($writer)
|
new PresetRestorePlanner($writer, UsageFixture::none())
|
||||||
);
|
);
|
||||||
|
|
||||||
$error = self::assertToolError(
|
$error = self::assertToolError(
|
||||||
@@ -164,7 +166,8 @@ final class DiviDesignRestoreAbilityTest extends TestCase
|
|||||||
GlobalColorsAdapter $colors,
|
GlobalColorsAdapter $colors,
|
||||||
GlobalVariablesAdapter $variables,
|
GlobalVariablesAdapter $variables,
|
||||||
?FakeChangeLog $log = null,
|
?FakeChangeLog $log = null,
|
||||||
?ThemeOptionsAdapter $options = null
|
?ThemeOptionsAdapter $options = null,
|
||||||
|
?GlobalUsages $usages = null
|
||||||
): DiviDesignRestoreAbility {
|
): DiviDesignRestoreAbility {
|
||||||
Functions\when('current_user_can')->justReturn(true);
|
Functions\when('current_user_can')->justReturn(true);
|
||||||
$writer = Mockery::mock(DesignSystemWriter::class);
|
$writer = Mockery::mock(DesignSystemWriter::class);
|
||||||
@@ -176,9 +179,9 @@ final class DiviDesignRestoreAbilityTest extends TestCase
|
|||||||
return new DiviDesignRestoreAbility(
|
return new DiviDesignRestoreAbility(
|
||||||
new DesignChange($snapshots, $log ?? FakeChangeLog::forbidden()),
|
new DesignChange($snapshots, $log ?? FakeChangeLog::forbidden()),
|
||||||
$snapshots,
|
$snapshots,
|
||||||
new PresetRestorePlanner($writer),
|
new PresetRestorePlanner($writer, $usages ?? UsageFixture::none()),
|
||||||
new ColorRestorePlanner($writer, $colors),
|
new ColorRestorePlanner($writer, $colors, $usages ?? UsageFixture::none()),
|
||||||
new VariableRestorePlanner($writer, $variables),
|
new VariableRestorePlanner($writer, $variables, $usages ?? UsageFixture::none()),
|
||||||
new ThemeOptionsRestorePlanner($options ?? self::optionStore($noOptions))
|
new ThemeOptionsRestorePlanner($options ?? self::optionStore($noOptions))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -240,6 +243,35 @@ final class DiviDesignRestoreAbilityTest extends TestCase
|
|||||||
'log_id' => 2, 'size' => 1];
|
'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
|
public function testRestoresColorsWithDryRunAndASnapshotOfTheCurrentColors(): void
|
||||||
{
|
{
|
||||||
$id = ColorRestorePlannerTest::SNAPSHOT_ID;
|
$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\DesignSystemWriter;
|
||||||
use LIT\MCP\Integrations\Divi\Design\PresetArea;
|
use LIT\MCP\Integrations\Divi\Design\PresetArea;
|
||||||
use LIT\MCP\Integrations\Divi\Design\PresetRestorePlanner;
|
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\Integrations\Divi\Release\DiviFeature;
|
||||||
|
use LIT\MCP\Tests\Support\UsageFixture;
|
||||||
use LIT\MCP\Tests\Unit\TestCase;
|
use LIT\MCP\Tests\Unit\TestCase;
|
||||||
use Mockery;
|
use Mockery;
|
||||||
|
|
||||||
final class PresetRestorePlannerTest extends TestCase
|
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
|
public function testAreaFeaturesAndDesignArea(): void
|
||||||
@@ -51,4 +57,36 @@ final class PresetRestorePlannerTest extends TestCase
|
|||||||
|
|
||||||
self::assertToolError($plan, 'forbidden');
|
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\VariableArea;
|
||||||
use LIT\MCP\Integrations\Divi\Design\VariableBook;
|
use LIT\MCP\Integrations\Divi\Design\VariableBook;
|
||||||
use LIT\MCP\Integrations\Divi\Design\VariableRestorePlanner;
|
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\Integrations\Divi\Release\DiviFeature;
|
||||||
|
use LIT\MCP\Tests\Support\UsageFixture;
|
||||||
use LIT\MCP\Tests\Unit\TestCase;
|
use LIT\MCP\Tests\Unit\TestCase;
|
||||||
use Mockery;
|
use Mockery;
|
||||||
|
|
||||||
@@ -41,14 +44,14 @@ final class VariableRestorePlannerTest extends TestCase
|
|||||||
return ['numbers' => ['gvid-radius0001' => self::number('gvid-radius0001', 'Eckenradius', '4px')]];
|
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 = Mockery::mock(DesignSystemWriter::class);
|
||||||
$writer->allows('canUseVisualBuilder')->andReturn(true);
|
$writer->allows('canUseVisualBuilder')->andReturn(true);
|
||||||
$variables = Mockery::mock(GlobalVariablesAdapter::class);
|
$variables = Mockery::mock(GlobalVariablesAdapter::class);
|
||||||
$variables->allows('canManage')->andReturn($manage);
|
$variables->allows('canManage')->andReturn($manage);
|
||||||
|
|
||||||
return new VariableRestorePlanner($writer, $variables);
|
return new VariableRestorePlanner($writer, $variables, $usages ?? UsageFixture::none());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testAreaFeaturesAndDesignArea(): void
|
public function testAreaFeaturesAndDesignArea(): void
|
||||||
@@ -89,4 +92,19 @@ final class VariableRestorePlannerTest extends TestCase
|
|||||||
$plan->warnings
|
$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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user