justReturn(true); $older = PresetBookTest::stored(); unset($older['module']['divi/button']['items']['bbbbbbbbbb']); $writer = Mockery::mock(DesignSystemWriter::class); $writer->allows('presets')->andReturn(PresetBookTest::stored()); $writer->allows('canUseVisualBuilder')->andReturn(true); $writer->expects('savePresets')->with(Mockery::type('array'), true)->andReturn($older); $snapshots = Mockery::mock(SnapshotStore::class); $snapshots->allows('get')->with('presets', 'presets-20261016T100000Z-aaaaaa')->andReturn(['id' => 'presets-20261016T100000Z-aaaaaa', 'data' => $older, 'created' => 'x', 'user_id' => 1, 'ability' => 'lit/divi-presets-set', 'log_id' => 2, 'size' => 1]); $snapshots->expects('save')->with('presets', Mockery::type('array'), 'lit/divi-design-restore')->andReturn('presets-3'); $snapshots->expects('attachLog'); $ability = new DiviDesignRestoreAbility( new DesignChange($snapshots, new FakeChangeLog(8)), $snapshots, new PresetRestorePlanner($writer) ); $dry = self::assertToolError($ability->execute(['snapshot_id' => 'presets-20261016T100000Z-aaaaaa']), 'confirm_required'); self::assertSame('removed', $dry['diff'][0]['action']); $result = $ability->execute(['snapshot_id' => 'presets-20261016T100000Z-aaaaaa', 'confirm' => true, 'design_rev' => $dry['design_rev']]); self::assertSame('presets-3', $result['snapshot_id']); } private static function snapshotOf(array $data): array { return ['id' => 'presets-20261016T100000Z-aaaaaa', 'data' => $data, 'created' => 'x', 'user_id' => 1, 'ability' => 'lit/divi-presets-set', 'log_id' => 2, 'size' => 1]; } private static function restoring(array $current, array $snapshot, bool $unfiltered): DiviDesignRestoreAbility { Functions\when('current_user_can')->justReturn(true); $writer = Mockery::mock(DesignSystemWriter::class); $writer->allows('presets')->andReturn($current); $writer->allows('canUseVisualBuilder')->andReturn(true); $snapshots = Mockery::mock(SnapshotStore::class); $snapshots->allows('get')->andReturn(self::snapshotOf($snapshot)); $snapshots->shouldNotReceive('save'); return new DiviDesignRestoreAbility( new DesignChange($snapshots, FakeChangeLog::forbidden()), $snapshots, new PresetRestorePlanner($writer, static fn (): bool => $unfiltered) ); } /** Final review I-2: markup in the snapshot or in the current inventory is lost for editors – refused. */ public function testEditorIsRefusedWhenTheSnapshotOrTheInventoryCarriesMarkup(): void { $plain = PresetBookTest::stored(); $markup = PresetBookTest::storedWithMarkup(); $older = $plain; unset($older['module']['divi/button']['items']['cccccccccc']); $input = ['snapshot_id' => 'presets-20261016T100000Z-aaaaaa']; $inSnapshot = self::assertToolError(self::restoring($plain, $markup, false)->execute($input), 'forbidden'); self::assertStringContainsString('unfiltered_html', $inSnapshot['message']); self::assertToolError(self::restoring($markup, $older, false)->execute($input), 'forbidden'); self::assertToolError(self::restoring($plain, $markup, true)->execute($input), 'confirm_required'); self::assertToolError(self::restoring($plain, $older, false)->execute($input), 'confirm_required'); } /** T13: restoring needs the preset rights. */ public function testNeedsEditThemeOptions(): void { Functions\when('current_user_can')->justReturn(false); $writer = Mockery::mock(DesignSystemWriter::class); $writer->allows('canUseVisualBuilder')->andReturn(true); $writer->shouldNotReceive('savePresets'); $snapshots = Mockery::mock(SnapshotStore::class); $snapshots->shouldNotReceive('get'); $ability = new DiviDesignRestoreAbility( new DesignChange($snapshots, FakeChangeLog::forbidden()), $snapshots, new PresetRestorePlanner($writer) ); $input = ['snapshot_id' => 'presets-20261016T100000Z-aaaaaa', 'confirm' => true, 'design_rev' => 'x']; self::assertToolError($ability->permission($input), 'forbidden'); self::assertToolError($ability->execute($input), 'forbidden'); } public function testUnknownSnapshots(): void { $snapshots = Mockery::mock(SnapshotStore::class); $snapshots->allows('get')->andReturn(null); $ability = self::allAreas($snapshots, self::colorStore([]), self::variableStore([])); self::assertToolError($ability->execute(['snapshot_id' => 'presets-20261016T100000Z-abcdef']), 'not_found'); self::assertToolError($ability->execute(['snapshot_id' => 'colors-20261016T100000Z-abcdef']), 'not_found'); self::assertToolError($ability->execute(['snapshot_id' => 'variables-20261016T100000Z-abcdef']), 'not_found'); self::assertToolError( $ability->execute(['snapshot_id' => 'theme_options-20261016T100000Z-abcdef']), 'not_found' ); self::assertToolError($ability->execute(['snapshot_id' => 'kaputt']), 'validation_failed'); } /** A snapshot area whose planner is not wired stays unsupported (W11) – never a silent no-op. */ public function testAnAreaWithoutAPlannerIsUnsupported(): void { Functions\when('current_user_can')->justReturn(true); $writer = Mockery::mock(DesignSystemWriter::class); $writer->allows('canUseVisualBuilder')->andReturn(true); $snapshots = Mockery::mock(SnapshotStore::class); $snapshots->shouldNotReceive('get'); $ability = new DiviDesignRestoreAbility( new DesignChange($snapshots, FakeChangeLog::forbidden()), $snapshots, new PresetRestorePlanner($writer) ); $error = self::assertToolError( $ability->execute(['snapshot_id' => ThemeOptionsRestorePlannerTest::SNAPSHOT_ID]), 'unsupported' ); self::assertStringNotContainsString('4d', $error['hint']); } /** The ability with the planners of all four areas; the user has every right unless a store says otherwise. */ private static function allAreas( SnapshotStore $snapshots, GlobalColorsAdapter $colors, GlobalVariablesAdapter $variables, ?FakeChangeLog $log = null, ?ThemeOptionsAdapter $options = null ): DiviDesignRestoreAbility { Functions\when('current_user_can')->justReturn(true); $writer = Mockery::mock(DesignSystemWriter::class); $writer->allows('canUseVisualBuilder')->andReturn(true); $writer->allows('presets')->andReturn(PresetBookTest::stored()); $writer->shouldNotReceive('savePresets'); $noOptions = []; return new DiviDesignRestoreAbility( new DesignChange($snapshots, $log ?? FakeChangeLog::forbidden()), $snapshots, new PresetRestorePlanner($writer), new ColorRestorePlanner($writer, $colors), new VariableRestorePlanner($writer, $variables), new ThemeOptionsRestorePlanner($options ?? self::optionStore($noOptions)) ); } /** * A theme options adapter that reads what it last wrote, starting with $stored (ThemeOptionsAreaTest::adapter()). * * @param array $stored */ private static function optionStore(array &$stored, bool $customize = true): ThemeOptionsAdapter { $writes = []; return ThemeOptionsAreaTest::adapter($stored, $writes, null, $customize); } /** * A colors adapter that reads what it last saved, starting with $stored. * * @param array> $stored */ private static function colorStore(array $stored): GlobalColorsAdapter&MockInterface { $colors = Mockery::mock(GlobalColorsAdapter::class); $colors->allows('read')->andReturnUsing(static function () use (&$stored): array { return $stored; }); $colors->allows('save')->andReturnUsing(static function (array $entries, bool $replace) use (&$stored): void { self::assertTrue($replace, 'a restore replaces every color (V4)'); $stored = $entries; }); return $colors; } /** * A variables adapter that reads what it last saved, starting with $stored. * * @param array>> $stored */ private static function variableStore(array $stored, bool $manage = true): GlobalVariablesAdapter&MockInterface { $variables = Mockery::mock(GlobalVariablesAdapter::class); $variables->allows('canManage')->andReturn($manage); $variables->allows('read')->andReturnUsing(static function () use (&$stored): array { return $stored; }); $variables->allows('save')->andReturnUsing(static function (array $data) use (&$stored): void { $stored = $data; }); return $variables; } /** @param array $data */ private static function snapshotIn(string $area, string $id, array $data): array { return ['id' => $id, 'data' => $data, 'created' => 'x', 'user_id' => 1, 'ability' => 'lit/divi-' . $area . '-set', 'log_id' => 2, 'size' => 1]; } public function testRestoresColorsWithDryRunAndASnapshotOfTheCurrentColors(): void { $id = ColorRestorePlannerTest::SNAPSHOT_ID; $colors = self::colorStore(ColorRestorePlannerTest::current()); $snapshots = Mockery::mock(SnapshotStore::class); $snapshots->allows('get')->with('colors', $id) ->andReturn(self::snapshotIn('colors', $id, ColorRestorePlannerTest::snapshot())); $snapshots->expects('save') ->with('colors', ColorBook::fromDivi(ColorRestorePlannerTest::current())->data(), 'lit/divi-design-restore') ->andReturn('colors-5'); $snapshots->expects('attachLog'); $ability = self::allAreas($snapshots, $colors, self::variableStore([]), new FakeChangeLog(8)); $dry = self::assertToolError($ability->execute(['snapshot_id' => $id]), 'confirm_required'); self::assertSame(['changed', 'removed'], array_column($dry['diff'], 'action')); self::assertStringContainsString('„Akzent“', $dry['warnings'][0]); $result = $ability->execute(['snapshot_id' => $id, 'confirm' => true, 'design_rev' => $dry['design_rev']]); self::assertSame('colors-5', $result['snapshot_id']); self::assertSame(ColorBook::fromDivi(ColorRestorePlannerTest::snapshot())->data(), $colors->read()); } public function testRestoresVariablesAndKeepsNewerOnesArchived(): void { $id = VariableRestorePlannerTest::SNAPSHOT_ID; $variables = self::variableStore(VariableRestorePlannerTest::current()); $snapshots = Mockery::mock(SnapshotStore::class); $snapshots->allows('get')->with('variables', $id) ->andReturn(self::snapshotIn('variables', $id, VariableRestorePlannerTest::snapshot())); $snapshots->expects('save') ->with('variables', Mockery::type('array'), 'lit/divi-design-restore') ->andReturn('variables-5'); $snapshots->expects('attachLog'); $ability = self::allAreas($snapshots, self::colorStore([]), $variables, new FakeChangeLog(8)); $dry = self::assertToolError($ability->execute(['snapshot_id' => $id]), 'confirm_required'); self::assertStringContainsString('„Neu“', $dry['warnings'][0]); $result = $ability->execute(['snapshot_id' => $id, 'confirm' => true, 'design_rev' => $dry['design_rev']]); self::assertSame('variables-5', $result['snapshot_id']); $stored = $variables->read(); self::assertSame('4px', $stored['numbers']['gvid-radius0001']['value']); self::assertSame('archived', $stored['numbers']['gvid-neu0000001']['status']); } public function testRestoresThemeOptionsAndResetsTheKeysTheSnapshotLacks(): void { $id = ThemeOptionsRestorePlannerTest::SNAPSHOT_ID; $stored = ThemeOptionsRestorePlannerTest::current(); $snapshots = Mockery::mock(SnapshotStore::class); $snapshots->allows('get')->with('theme_options', $id) ->andReturn(self::snapshotIn('theme_options', $id, ThemeOptionsRestorePlannerTest::snapshot())); $snapshots->expects('save') ->with( 'theme_options', ThemeOptionBook::fromDivi(ThemeOptionsRestorePlannerTest::current())->data(), 'lit/divi-design-restore' ) ->andReturn('theme_options-5'); $snapshots->expects('attachLog'); $ability = self::allAreas( $snapshots, self::colorStore([]), self::variableStore([]), new FakeChangeLog(8), self::optionStore($stored) ); $dry = self::assertToolError($ability->execute(['snapshot_id' => $id]), 'confirm_required'); self::assertSame(['added', 'changed', 'reset'], array_column($dry['diff'], 'action')); self::assertStringContainsString('Divi-Standard', $dry['warnings'][1]); $result = $ability->execute(['snapshot_id' => $id, 'confirm' => true, 'design_rev' => $dry['design_rev']]); self::assertSame('theme_options-5', $result['snapshot_id']); self::assertSame( ThemeOptionBook::fromDivi(ThemeOptionsRestorePlannerTest::snapshot())->data(), ThemeOptionBook::fromDivi($stored)->data() ); } /** Spec §10, V10: the rights are those of the snapshot's area. */ public function testRightsFollowTheSnapshotArea(): void { $snapshots = Mockery::mock(SnapshotStore::class); $snapshots->shouldNotReceive('get'); $options = []; $ability = self::allAreas( $snapshots, self::colorStore([]), self::variableStore([], false), null, self::optionStore($options, false) ); self::assertToolError( $ability->permission(['snapshot_id' => VariableRestorePlannerTest::SNAPSHOT_ID]), 'forbidden' ); self::assertToolError( $ability->execute(['snapshot_id' => VariableRestorePlannerTest::SNAPSHOT_ID]), 'forbidden' ); self::assertToolError( $ability->permission(['snapshot_id' => ThemeOptionsRestorePlannerTest::SNAPSHOT_ID]), 'forbidden' ); self::assertTrue($ability->permission(['snapshot_id' => ColorRestorePlannerTest::SNAPSHOT_ID])); self::assertTrue($ability->permission(['snapshot_id' => 'presets-20261016T100000Z-aaaaaa'])); self::assertToolError($ability->permission(['snapshot_id' => 'kaputt']), 'forbidden', 'fail closed'); } public function testFeaturesFollowTheSnapshotArea(): void { $ability = self::allAreas(Mockery::mock(SnapshotStore::class), self::colorStore([]), self::variableStore([])); self::assertSame( [DiviFeature::DesignRead, DiviFeature::ColorWrite], $ability->diviFeatures(['snapshot_id' => ColorRestorePlannerTest::SNAPSHOT_ID]) ); self::assertSame( [DiviFeature::DesignRead, DiviFeature::VariableWrite], $ability->diviFeatures(['snapshot_id' => VariableRestorePlannerTest::SNAPSHOT_ID]) ); self::assertSame( [DiviFeature::DesignRead, DiviFeature::PresetWrite], $ability->diviFeatures(['snapshot_id' => 'presets-20261016T100000Z-aaaaaa']) ); self::assertSame( [DiviFeature::ThemeOptionRead, DiviFeature::ThemeOptionWrite], $ability->diviFeatures(['snapshot_id' => ThemeOptionsRestorePlannerTest::SNAPSHOT_ID]) ); self::assertSame( [DiviFeature::DesignRead, DiviFeature::PresetWrite, DiviFeature::ColorWrite, DiviFeature::VariableWrite, DiviFeature::ThemeOptionRead, DiviFeature::ThemeOptionWrite], $ability->diviFeatures([]) ); } }