fix(audit L5): unter der Sperre Divis Laufzeit-Caches verwerfen (Options-Cache-Gruppe, GlobalData-/GlobalPreset-Static, et_theme_options) vor raw() und design_rev

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-10-20 00:26:40 +02:00
co-authored by Claude Sonnet 5.5
parent 6e84138979
commit 99c7aab238
28 changed files with 182 additions and 13 deletions
@@ -36,6 +36,11 @@ final class ColorArea implements DesignArea
return DiviFeature::ColorWrite; return DiviFeature::ColorWrite;
} }
public function refresh(): void
{
$this->colors->refresh();
}
public function raw(): array public function raw(): array
{ {
return ColorBook::fromDivi($this->colors->read())->data(); return ColorBook::fromDivi($this->colors->read())->data();
@@ -25,6 +25,12 @@ interface DesignArea
/** The Divi feature a write of this area needs (DesignChange asks ReleaseGate::assertFeature() for it). */ /** The Divi feature a write of this area needs (DesignChange asks ReleaseGate::assertFeature() for it). */
public function feature(): DiviFeature; public function feature(): DiviFeature;
/**
* Drops what this request cached of the inventory (Divi's static and option caches), called right after the
* lock is taken: another writer may have finished since the request began (audit L5).
*/
public function refresh(): void;
/** @return array<array-key, mixed> */ /** @return array<array-key, mixed> */
public function raw(): array; public function raw(): array;
@@ -93,6 +93,9 @@ final class DesignChange
); );
} }
try { try {
// Audit L5: drop Divi's request caches, so the inventory and design_rev below are the stored state.
$area->refresh();
return $this->attempt($ability, $area, $rev, $plan); return $this->attempt($ability, $area, $rev, $plan);
} finally { } finally {
$this->lock->release($area->name()); $this->lock->release($area->name());
+6 -5
View File
@@ -15,8 +15,8 @@ defined('ABSPATH') || exit;
* DiviCheckRoute (add_option() reads first and writes after, so two requests could both win; audit L6). A lock * DiviCheckRoute (add_option() reads first and writes after, so two requests could both win; audit L6). A lock
* older than STALE_AFTER seconds (a request that died) is taken over with a conditional UPDATE on the value just * older than STALE_AFTER seconds (a request that died) is taken over with a conditional UPDATE on the value just
* read, so only one taker wins; release() deletes the row only while it still holds this instance's token, so a * read, so only one taker wins; release() deletes the row only while it still holds this instance's token, so a
* holder that lost a stale lock cannot free the new holder's. A taken lock drops the cache of the autoloaded * holder that lost a stale lock cannot free the new holder's. A taken lock drops the options cache, so the
* options, so the inventory read under the lock (and design_rev) is the stored one. * inventory read under the lock (and design_rev) is the stored one.
*/ */
final class DesignLock final class DesignLock
{ {
@@ -66,9 +66,10 @@ final class DesignLock
} }
if ($taken) { if ($taken) {
$this->tokens[$option] = $token; $this->tokens[$option] = $token;
// Audit L5: what the holder reads and rewrites must be the stored state, not the request-start cache // Audit L5: what the holder reads and rewrites must be the stored state, not the request's option cache
// (autoloaded options like Divi's live in "alloptions"); another writer may have finished since. // ("alloptions" for autoloaded options, one entry each for the others); another writer may have
wp_cache_delete('alloptions', 'options'); // finished since. A cache without group flushing loses everything – rare, and only under the lock.
wp_cache_supports('flush_group') ? wp_cache_flush_group('options') : wp_cache_flush();
} }
return $taken; return $taken;
@@ -81,6 +81,19 @@ class DesignSystemWriter implements DiviAdapter
); );
} }
/**
* Audit L5: GlobalPreset::get_data() keeps a static copy that can be older than another writer's finished save
* (the option cache is dropped by DesignLock). Called under the lock, before reading.
*/
public function refresh(): void
{
try {
(new \ReflectionProperty(GlobalPreset::class, '_data'))->setValue(null, null);
} catch (\ReflectionException) {
// Divi without that property: its reads are not cached this way.
}
}
/** @return array<array-key, mixed> The stored inventory (GlobalPreset::get_data(), formats §3). */ /** @return array<array-key, mixed> The stored inventory (GlobalPreset::get_data(), formats §3). */
public function presets(): array public function presets(): array
{ {
@@ -84,7 +84,32 @@ class GlobalColorsAdapter implements DiviAdapter
} }
// Audit L5: set_global_colors() ends in et_update_option(), which writes Divi's request global back whole. // Audit L5: set_global_colors() ends in et_update_option(), which writes Divi's request global back whole.
$GLOBALS['et_theme_options'] = get_option('et_divi'); $this->refresh();
GlobalData::set_global_colors($clean, ! $replace); GlobalData::set_global_colors($clean, ! $replace);
} }
/**
* Audit L5: colors are read through et_get_option(), i.e. Divi's request global and GlobalData's static cache –
* both can be older than another writer's finished save. Called under the lock, before reading.
*/
public function refresh(): void
{
$GLOBALS['et_theme_options'] = get_option(self::OPTIONS);
self::forgetCached('colors');
}
/** Resets one entry of GlobalData's static runtime cache (private, so by reflection; gone = nothing to reset). */
private static function forgetCached(string $key): void
{
try {
$cache = new \ReflectionProperty(GlobalData::class, '_cached_data');
$data = $cache->getValue();
if (is_array($data) && array_key_exists($key, $data)) {
$data[$key] = null;
$cache->setValue(null, $data);
}
} catch (\ReflectionException) {
// Divi without that property: its reads are not cached this way.
}
}
} }
@@ -86,4 +86,28 @@ class GlobalVariablesAdapter implements DiviAdapter
{ {
return function_exists('et_pb_is_allowed') && (bool) et_pb_is_allowed('variables_manager'); return function_exists('et_pb_is_allowed') && (bool) et_pb_is_allowed('variables_manager');
} }
/**
* Audit L5: GlobalData keeps a static copy of the variables that can be older than another writer's finished
* save (the option cache is dropped by DesignLock). Called under the lock, before reading.
*/
public function refresh(): void
{
self::forgetCached('variables');
}
/** Resets one entry of GlobalData's static runtime cache (private, so by reflection; gone = nothing to reset). */
private static function forgetCached(string $key): void
{
try {
$cache = new \ReflectionProperty(GlobalData::class, '_cached_data');
$data = $cache->getValue();
if (is_array($data) && array_key_exists($key, $data)) {
$data[$key] = null;
$cache->setValue(null, $data);
}
} catch (\ReflectionException) {
// Divi without that property: its reads are not cached this way.
}
}
} }
@@ -34,6 +34,11 @@ final class PresetArea implements DesignArea
return DiviFeature::PresetWrite; return DiviFeature::PresetWrite;
} }
public function refresh(): void
{
$this->writer->refresh();
}
public function raw(): array public function raw(): array
{ {
return PresetBook::fromDivi($this->writer->presets())->data(); return PresetBook::fromDivi($this->writer->presets())->data();
@@ -40,6 +40,11 @@ final class ThemeOptionsArea implements DesignArea
return DiviFeature::ThemeOptionWrite; return DiviFeature::ThemeOptionWrite;
} }
public function refresh(): void
{
// read() asks get_option(), whose cache the lock drops; write() reloads Divi's request global itself.
}
public function raw(): array public function raw(): array
{ {
return ThemeOptionBook::fromDivi($this->options->read())->data(); return ThemeOptionBook::fromDivi($this->options->read())->data();
@@ -40,6 +40,11 @@ final class VariableArea implements DesignArea
return DiviFeature::VariableWrite; return DiviFeature::VariableWrite;
} }
public function refresh(): void
{
$this->variables->refresh();
}
public function raw(): array public function raw(): array
{ {
return VariableBook::fromDivi($this->variables->read())->data(); return VariableBook::fromDivi($this->variables->read())->data();
@@ -43,6 +43,7 @@ final class ColorAreaTest extends TestCase
public static function adapter(array &$stored, array &$saves): GlobalColorsAdapter public static function adapter(array &$stored, array &$saves): GlobalColorsAdapter
{ {
$colors = Mockery::mock(GlobalColorsAdapter::class); $colors = Mockery::mock(GlobalColorsAdapter::class);
$colors->allows('refresh');
$colors->allows('read')->andReturnUsing(static function () use (&$stored): array { $colors->allows('read')->andReturnUsing(static function () use (&$stored): array {
return $stored; return $stored;
}); });
@@ -47,6 +47,7 @@ final class ColorRestorePlannerTest extends TestCase
private static function planner(bool $visualBuilder = true, ?GlobalUsages $usages = null): ColorRestorePlanner private static function planner(bool $visualBuilder = true, ?GlobalUsages $usages = null): ColorRestorePlanner
{ {
$writer = Mockery::mock(DesignSystemWriter::class); $writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('canUseVisualBuilder')->andReturn($visualBuilder); $writer->allows('canUseVisualBuilder')->andReturn($visualBuilder);
return new ColorRestorePlanner( return new ColorRestorePlanner(
@@ -56,6 +56,9 @@ final class DesignChangeTest extends TestCase
public array $restores = []; public array $restores = [];
/** @var list<string> refresh()/raw() in the order they happened */
public array $calls = [];
public function __construct( public function __construct(
private array &$stored, private array &$stored,
private int $failWrites, private int $failWrites,
@@ -79,8 +82,15 @@ final class DesignChangeTest extends TestCase
return DiviFeature::PresetWrite; return DiviFeature::PresetWrite;
} }
public function refresh(): void
{
$this->calls[] = 'refresh';
}
public function raw(): array public function raw(): array
{ {
$this->calls[] = 'raw';
return $this->stored; return $this->stored;
} }
@@ -205,6 +215,25 @@ final class DesignChangeTest extends TestCase
self::assertSame([], $area->writes); self::assertSame([], $area->writes);
} }
/** Audit L5 (re-review): under the lock Divi's request caches are dropped before the inventory is read. */
public function testTheAreaDropsItsCachesUnderTheLockBeforeReading(): void
{
$stored = ['v' => 1];
$area = self::area($stored);
$snapshots = Mockery::mock(SnapshotStore::class);
$snapshots->allows('save')->andReturn('presets-1');
$snapshots->allows('attachLog');
(new DesignChange($snapshots, new FakeChangeLog(1), new DesignLock()))->run(
'lit/divi-presets-set',
$area,
['confirm' => true, 'design_rev' => $area->rev($stored)],
self::plan(['v' => 2])
);
self::assertSame(['refresh', 'raw'], array_slice($area->calls, 0, 2));
}
/** Final review I-5: a confirm while another change of the area runs writes nothing. */ /** Final review I-5: a confirm while another change of the area runs writes nothing. */
public function testAHeldLockRefusesTheConfirm(): void public function testAHeldLockRefusesTheConfirm(): void
{ {
@@ -102,26 +102,33 @@ final class DesignLockTest extends TestCase
public function testTheLockHolderReadsEtDiviFreshNotFromTheRequestStartCache(): void public function testTheLockHolderReadsEtDiviFreshNotFromTheRequestStartCache(): void
{ {
$db = $this->table(); $db = $this->table();
$stored = ['et_divi' => ['color' => 'alt']]; $stored = ['et_divi' => ['color' => 'alt'], 'et_divi_global_variables' => ['v' => 'alt']];
$cache = []; $cache = [];
// Like WordPress: every option is cached under its own key (not autoloaded ones) or in "alloptions".
Functions\when('get_option')->alias(static function (string $name) use (&$stored, &$cache) { Functions\when('get_option')->alias(static function (string $name) use (&$stored, &$cache) {
if (! isset($cache['options:' . $name]) && ! isset($cache['options:alloptions'][$name])) { $key = 'options:' . $name;
$cache['options:alloptions'][$name] = $stored[$name] ?? false; $cache[$key] ??= $stored[$name] ?? false;
}
return $cache['options:' . $name] ?? $cache['options:alloptions'][$name]; return $cache[$key];
}); });
Functions\when('wp_cache_delete')->alias(static function (string $key, string $group = '') use (&$cache): bool { Functions\when('wp_cache_flush_group')->alias(static function (string $group) use (&$cache): bool {
unset($cache[$group . ':' . $key]); foreach (array_keys($cache) as $key) {
if (str_starts_with($key, $group . ':')) {
unset($cache[$key]);
}
}
return true; return true;
}); });
self::assertSame(['color' => 'alt'], get_option('et_divi'), 'request B cached the old array'); self::assertSame(['color' => 'alt'], get_option('et_divi'), 'request B cached the old array');
self::assertSame(['v' => 'alt'], get_option('et_divi_global_variables'));
$stored['et_divi'] = ['color' => 'neu']; // request A wrote and released meanwhile $stored['et_divi'] = ['color' => 'neu']; // request A wrote and released meanwhile
$stored['et_divi_global_variables'] = ['v' => 'neu'];
self::assertTrue((new DesignLock())->acquire('colors')); self::assertTrue((new DesignLock())->acquire('colors'));
self::assertSame(['color' => 'neu'], get_option('et_divi')); self::assertSame(['color' => 'neu'], get_option('et_divi'));
self::assertSame(['v' => 'neu'], get_option('et_divi_global_variables'), 'also options that are not autoloaded');
self::assertNotSame([], $db->rows); self::assertNotSame([], $db->rows);
} }
} }
@@ -18,6 +18,7 @@ final class DesignRightsTest extends TestCase
public function testPresetsNeedEditThemeOptionsAndTheVisualBuilder(): void public function testPresetsNeedEditThemeOptionsAndTheVisualBuilder(): void
{ {
$writer = Mockery::mock(DesignSystemWriter::class); $writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('canUseVisualBuilder')->andReturn(true, false); $writer->allows('canUseVisualBuilder')->andReturn(true, false);
Functions\when('current_user_can')->alias(static fn (string $cap): bool => $cap === 'edit_theme_options'); Functions\when('current_user_can')->alias(static fn (string $cap): bool => $cap === 'edit_theme_options');
@@ -33,6 +34,7 @@ final class DesignRightsTest extends TestCase
public function testColorsNeedEditThemeOptionsAndTheVisualBuilder(): void public function testColorsNeedEditThemeOptionsAndTheVisualBuilder(): void
{ {
$writer = Mockery::mock(DesignSystemWriter::class); $writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('canUseVisualBuilder')->andReturn(true, false); $writer->allows('canUseVisualBuilder')->andReturn(true, false);
Functions\when('current_user_can')->alias(static fn (string $cap): bool => $cap === 'edit_theme_options'); Functions\when('current_user_can')->alias(static fn (string $cap): bool => $cap === 'edit_theme_options');
@@ -49,8 +51,10 @@ final class DesignRightsTest extends TestCase
public function testVariablesAlsoNeedTheVariablesManager(): void public function testVariablesAlsoNeedTheVariablesManager(): void
{ {
$writer = Mockery::mock(DesignSystemWriter::class); $writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('canUseVisualBuilder')->andReturn(true); $writer->allows('canUseVisualBuilder')->andReturn(true);
$variables = Mockery::mock(GlobalVariablesAdapter::class); $variables = Mockery::mock(GlobalVariablesAdapter::class);
$variables->allows('refresh');
$variables->allows('canManage')->andReturn(true, false); $variables->allows('canManage')->andReturn(true, false);
Functions\when('current_user_can')->alias(static fn (string $cap): bool => $cap === 'edit_theme_options'); Functions\when('current_user_can')->alias(static fn (string $cap): bool => $cap === 'edit_theme_options');
@@ -63,8 +67,10 @@ final class DesignRightsTest extends TestCase
public function testVariablesAskTheThemeRightAndTheVisualBuilderFirst(): void public function testVariablesAskTheThemeRightAndTheVisualBuilderFirst(): void
{ {
$writer = Mockery::mock(DesignSystemWriter::class); $writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('canUseVisualBuilder')->andReturn(false); $writer->allows('canUseVisualBuilder')->andReturn(false);
$variables = Mockery::mock(GlobalVariablesAdapter::class); $variables = Mockery::mock(GlobalVariablesAdapter::class);
$variables->allows('refresh');
$variables->allows('canManage')->andReturn(true); $variables->allows('canManage')->andReturn(true);
Functions\when('current_user_can')->justReturn(true); Functions\when('current_user_can')->justReturn(true);
@@ -53,6 +53,7 @@ final class DiviColorsSetAbilityTest extends TestCase
bool $visualBuilder = true bool $visualBuilder = true
): DiviColorsSetAbility { ): DiviColorsSetAbility {
$writer = Mockery::mock(DesignSystemWriter::class); $writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('canUseVisualBuilder')->andReturn($visualBuilder); $writer->allows('canUseVisualBuilder')->andReturn($visualBuilder);
$writer->allows('presets')->andReturn($presets === [] ? PresetBookTest::stored() : $presets); $writer->allows('presets')->andReturn($presets === [] ? PresetBookTest::stored() : $presets);
$counter = 0; $counter = 0;
@@ -297,6 +298,7 @@ final class DiviColorsSetAbilityTest extends TestCase
{ {
$calls = []; $calls = [];
$colors = Mockery::mock(GlobalColorsAdapter::class); $colors = Mockery::mock(GlobalColorsAdapter::class);
$colors->allows('refresh');
$colors->allows('read')->andReturn(ColorAreaTest::stored()); $colors->allows('read')->andReturn(ColorAreaTest::stored());
$colors->allows('save')->andReturnUsing(static function (array $entries, bool $replace) use (&$calls): void { $colors->allows('save')->andReturnUsing(static function (array $entries, bool $replace) use (&$calls): void {
$calls[] = $replace; $calls[] = $replace;
@@ -80,6 +80,7 @@ final class DiviDesignDetachAbilityTest extends TestCase
private static function readOnlyWriter(?array $stored = null): DesignSystemWriter&MockInterface private static function readOnlyWriter(?array $stored = null): DesignSystemWriter&MockInterface
{ {
$writer = Mockery::mock(DesignSystemWriter::class); $writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('presets')->andReturn($stored ?? PresetBookTest::stored()); $writer->allows('presets')->andReturn($stored ?? PresetBookTest::stored());
$writer->allows('canUseVisualBuilder')->andReturn(true); $writer->allows('canUseVisualBuilder')->andReturn(true);
$writer->expects('savePresets')->never(); $writer->expects('savePresets')->never();
@@ -91,6 +92,7 @@ final class DiviDesignDetachAbilityTest extends TestCase
private static function variableAdapter(bool $manage = true, string $radius = '8px'): GlobalVariablesAdapter&MockInterface private static function variableAdapter(bool $manage = true, string $radius = '8px'): GlobalVariablesAdapter&MockInterface
{ {
$variables = Mockery::mock(GlobalVariablesAdapter::class); $variables = Mockery::mock(GlobalVariablesAdapter::class);
$variables->allows('refresh');
$variables->allows('read')->andReturn(self::variables($radius)); $variables->allows('read')->andReturn(self::variables($radius));
$variables->allows('canManage')->andReturn($manage); $variables->allows('canManage')->andReturn($manage);
$variables->expects('save')->never(); $variables->expects('save')->never();
@@ -246,6 +248,7 @@ final class DiviDesignDetachAbilityTest extends TestCase
$calls = []; $calls = [];
$stored = self::storedWithVariable(); $stored = self::storedWithVariable();
$writer = Mockery::mock(DesignSystemWriter::class); $writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('presets')->andReturn($stored); $writer->allows('presets')->andReturn($stored);
$writer->allows('canUseVisualBuilder')->andReturn(true); $writer->allows('canUseVisualBuilder')->andReturn(true);
$writer->expects('savePresets') $writer->expects('savePresets')
@@ -451,6 +454,7 @@ final class DiviDesignDetachAbilityTest extends TestCase
{ {
$calls = []; $calls = [];
$writer = Mockery::mock(DesignSystemWriter::class); $writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('presets')->andReturn(PresetBookTest::stored()); $writer->allows('presets')->andReturn(PresetBookTest::stored());
$writer->allows('canUseVisualBuilder')->andReturn(true); $writer->allows('canUseVisualBuilder')->andReturn(true);
$writer->expects('savePresets') $writer->expects('savePresets')
@@ -502,6 +506,7 @@ final class DiviDesignDetachAbilityTest extends TestCase
public function testAPageErrorAfterThePresetStepSaysThePresetsAreChanged(): void public function testAPageErrorAfterThePresetStepSaysThePresetsAreChanged(): void
{ {
$writer = Mockery::mock(DesignSystemWriter::class); $writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('presets')->andReturn(PresetBookTest::stored()); $writer->allows('presets')->andReturn(PresetBookTest::stored());
$writer->allows('canUseVisualBuilder')->andReturn(true); $writer->allows('canUseVisualBuilder')->andReturn(true);
$writer->expects('savePresets') $writer->expects('savePresets')
@@ -633,6 +638,7 @@ final class DiviDesignDetachAbilityTest extends TestCase
private function abilityAfterThePresetStep(PageRewriter $pages): DiviDesignDetachAbility private function abilityAfterThePresetStep(PageRewriter $pages): DiviDesignDetachAbility
{ {
$writer = Mockery::mock(DesignSystemWriter::class); $writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('presets')->andReturn(PresetBookTest::stored()); $writer->allows('presets')->andReturn(PresetBookTest::stored());
$writer->allows('canUseVisualBuilder')->andReturn(true); $writer->allows('canUseVisualBuilder')->andReturn(true);
$writer->expects('savePresets') $writer->expects('savePresets')
@@ -42,6 +42,7 @@ final class DiviDesignRestoreAbilityTest extends TestCase
$older = PresetBookTest::stored(); $older = PresetBookTest::stored();
unset($older['module']['divi/button']['items']['bbbbbbbbbb']); unset($older['module']['divi/button']['items']['bbbbbbbbbb']);
$writer = Mockery::mock(DesignSystemWriter::class); $writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('presets')->andReturn(PresetBookTest::stored()); $writer->allows('presets')->andReturn(PresetBookTest::stored());
$writer->allows('canUseVisualBuilder')->andReturn(true); $writer->allows('canUseVisualBuilder')->andReturn(true);
$writer->expects('savePresets')->with(Mockery::type('array'), true)->andReturn($older); $writer->expects('savePresets')->with(Mockery::type('array'), true)->andReturn($older);
@@ -73,6 +74,7 @@ final class DiviDesignRestoreAbilityTest extends TestCase
{ {
Functions\when('current_user_can')->justReturn(true); Functions\when('current_user_can')->justReturn(true);
$writer = Mockery::mock(DesignSystemWriter::class); $writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('presets')->andReturn($current); $writer->allows('presets')->andReturn($current);
$writer->allows('canUseVisualBuilder')->andReturn(true); $writer->allows('canUseVisualBuilder')->andReturn(true);
$snapshots = Mockery::mock(SnapshotStore::class); $snapshots = Mockery::mock(SnapshotStore::class);
@@ -108,6 +110,7 @@ final class DiviDesignRestoreAbilityTest extends TestCase
{ {
Functions\when('current_user_can')->justReturn(false); Functions\when('current_user_can')->justReturn(false);
$writer = Mockery::mock(DesignSystemWriter::class); $writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('canUseVisualBuilder')->andReturn(true); $writer->allows('canUseVisualBuilder')->andReturn(true);
$writer->shouldNotReceive('savePresets'); $writer->shouldNotReceive('savePresets');
$snapshots = Mockery::mock(SnapshotStore::class); $snapshots = Mockery::mock(SnapshotStore::class);
@@ -144,6 +147,7 @@ final class DiviDesignRestoreAbilityTest extends TestCase
{ {
Functions\when('current_user_can')->justReturn(true); Functions\when('current_user_can')->justReturn(true);
$writer = Mockery::mock(DesignSystemWriter::class); $writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('canUseVisualBuilder')->andReturn(true); $writer->allows('canUseVisualBuilder')->andReturn(true);
$snapshots = Mockery::mock(SnapshotStore::class); $snapshots = Mockery::mock(SnapshotStore::class);
$snapshots->shouldNotReceive('get'); $snapshots->shouldNotReceive('get');
@@ -171,6 +175,7 @@ final class DiviDesignRestoreAbilityTest extends TestCase
): 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);
$writer->allows('refresh');
$writer->allows('canUseVisualBuilder')->andReturn(true); $writer->allows('canUseVisualBuilder')->andReturn(true);
$writer->allows('presets')->andReturn(PresetBookTest::stored()); $writer->allows('presets')->andReturn(PresetBookTest::stored());
$writer->shouldNotReceive('savePresets'); $writer->shouldNotReceive('savePresets');
@@ -206,6 +211,7 @@ final class DiviDesignRestoreAbilityTest extends TestCase
private static function colorStore(array $stored): GlobalColorsAdapter&MockInterface private static function colorStore(array $stored): GlobalColorsAdapter&MockInterface
{ {
$colors = Mockery::mock(GlobalColorsAdapter::class); $colors = Mockery::mock(GlobalColorsAdapter::class);
$colors->allows('refresh');
$colors->allows('read')->andReturnUsing(static function () use (&$stored): array { $colors->allows('read')->andReturnUsing(static function () use (&$stored): array {
return $stored; return $stored;
}); });
@@ -225,6 +231,7 @@ final class DiviDesignRestoreAbilityTest extends TestCase
private static function variableStore(array $stored, bool $manage = true): GlobalVariablesAdapter&MockInterface private static function variableStore(array $stored, bool $manage = true): GlobalVariablesAdapter&MockInterface
{ {
$variables = Mockery::mock(GlobalVariablesAdapter::class); $variables = Mockery::mock(GlobalVariablesAdapter::class);
$variables->allows('refresh');
$variables->allows('canManage')->andReturn($manage); $variables->allows('canManage')->andReturn($manage);
$variables->allows('read')->andReturnUsing(static function () use (&$stored): array { $variables->allows('read')->andReturnUsing(static function () use (&$stored): array {
return $stored; return $stored;
@@ -19,6 +19,7 @@ final class DiviPresetGetAbilityTest extends TestCase
private function ability(?GlobalUsages $usages = null): DiviPresetGetAbility private function ability(?GlobalUsages $usages = null): DiviPresetGetAbility
{ {
$writer = Mockery::mock(DesignSystemWriter::class); $writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('presets')->andReturn(PresetBookTest::stored()); $writer->allows('presets')->andReturn(PresetBookTest::stored());
$usages ??= Mockery::mock(GlobalUsages::class); $usages ??= Mockery::mock(GlobalUsages::class);
@@ -29,6 +29,7 @@ final class DiviPresetsAssignAbilityTest extends TestCase
private function ability(PageRewriter $pages): DiviPresetsAssignAbility private function ability(PageRewriter $pages): DiviPresetsAssignAbility
{ {
$writer = Mockery::mock(DesignSystemWriter::class); $writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('presets')->andReturn(PresetBookTest::stored()); $writer->allows('presets')->andReturn(PresetBookTest::stored());
$writer->allows('canUseVisualBuilder')->andReturn(true); $writer->allows('canUseVisualBuilder')->andReturn(true);
@@ -37,6 +37,7 @@ final class DiviPresetsDeleteAbilityTest extends TestCase
Functions\when('current_user_can')->justReturn(true); Functions\when('current_user_can')->justReturn(true);
if ($writer === null) { if ($writer === null) {
$writer = Mockery::mock(DesignSystemWriter::class); $writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('presets')->andReturn($stored === [] ? PresetBookTest::stored() : $stored); $writer->allows('presets')->andReturn($stored === [] ? PresetBookTest::stored() : $stored);
$writer->allows('canUseVisualBuilder')->andReturn(true); $writer->allows('canUseVisualBuilder')->andReturn(true);
} }
@@ -59,6 +60,7 @@ final class DiviPresetsDeleteAbilityTest extends TestCase
public function testUnusedPresetIsDeletedAfterConfirm(): void public function testUnusedPresetIsDeletedAfterConfirm(): void
{ {
$writer = Mockery::mock(DesignSystemWriter::class); $writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('presets')->andReturn(PresetBookTest::stored()); $writer->allows('presets')->andReturn(PresetBookTest::stored());
$writer->allows('canUseVisualBuilder')->andReturn(true); $writer->allows('canUseVisualBuilder')->andReturn(true);
$writer->expects('savePresets') $writer->expects('savePresets')
@@ -153,6 +155,7 @@ final class DiviPresetsDeleteAbilityTest extends TestCase
0, 1, [], 0, false); 0, 1, [], 0, false);
Functions\when('current_user_can')->justReturn(true); Functions\when('current_user_can')->justReturn(true);
$writer = Mockery::mock(DesignSystemWriter::class); $writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('presets')->andReturn(PresetBookTest::stored()); $writer->allows('presets')->andReturn(PresetBookTest::stored());
$writer->allows('canUseVisualBuilder')->andReturn(true); $writer->allows('canUseVisualBuilder')->andReturn(true);
$usages = Mockery::mock(GlobalUsages::class); $usages = Mockery::mock(GlobalUsages::class);
@@ -52,6 +52,7 @@ final class DiviPresetsSetAbilityTest extends TestCase
private function savingWriter(): DesignSystemWriter private function savingWriter(): DesignSystemWriter
{ {
$writer = Mockery::mock(DesignSystemWriter::class); $writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('presets')->andReturnUsing(fn (): array => $this->stored); $writer->allows('presets')->andReturnUsing(fn (): array => $this->stored);
$writer->allows('canUseVisualBuilder')->andReturn(true); $writer->allows('canUseVisualBuilder')->andReturn(true);
$writer->allows('savePresets')->andReturnUsing(function (array $rest, bool $mayRemove): array { $writer->allows('savePresets')->andReturnUsing(function (array $rest, bool $mayRemove): array {
@@ -291,6 +292,7 @@ final class DiviPresetsSetAbilityTest extends TestCase
public function testNothingIsWrittenWhenOneItemHasErrors(): void public function testNothingIsWrittenWhenOneItemHasErrors(): void
{ {
$writer = Mockery::mock(DesignSystemWriter::class); $writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('presets')->andReturnUsing(fn (): array => $this->stored); $writer->allows('presets')->andReturnUsing(fn (): array => $this->stored);
$writer->allows('canUseVisualBuilder')->andReturn(true); $writer->allows('canUseVisualBuilder')->andReturn(true);
$writer->shouldNotReceive('savePresets'); $writer->shouldNotReceive('savePresets');
@@ -53,6 +53,7 @@ final class DiviVariablesSetAbilityTest extends TestCase
?FontCatalog $fonts = null ?FontCatalog $fonts = null
): DiviVariablesSetAbility { ): DiviVariablesSetAbility {
$writer = Mockery::mock(DesignSystemWriter::class); $writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('canUseVisualBuilder')->andReturn(true); $writer->allows('canUseVisualBuilder')->andReturn(true);
$writer->allows('presets')->andReturn($presets === [] ? PresetBookTest::stored() : $presets); $writer->allows('presets')->andReturn($presets === [] ? PresetBookTest::stored() : $presets);
$counter = 0; $counter = 0;
@@ -313,6 +314,7 @@ final class DiviVariablesSetAbilityTest extends TestCase
{ {
$calls = []; $calls = [];
$variables = Mockery::mock(GlobalVariablesAdapter::class); $variables = Mockery::mock(GlobalVariablesAdapter::class);
$variables->allows('refresh');
$variables->allows('canManage')->andReturn(true); $variables->allows('canManage')->andReturn(true);
$variables->allows('read')->andReturn(VariableAreaTest::stored()); $variables->allows('read')->andReturn(VariableAreaTest::stored());
$variables->allows('save')->andReturnUsing(static function (array $data, array $expected) use (&$calls): void { $variables->allows('save')->andReturnUsing(static function (array $data, array $expected) use (&$calls): void {
@@ -16,6 +16,7 @@ final class PresetAreaTest extends TestCase
public function testWritesTheRestFormatAndReadsBack(): void public function testWritesTheRestFormatAndReadsBack(): void
{ {
$writer = Mockery::mock(DesignSystemWriter::class); $writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$stored = PresetBookTest::stored(); $stored = PresetBookTest::stored();
$writer->allows('presets')->andReturn($stored); $writer->allows('presets')->andReturn($stored);
$writer->expects('savePresets') $writer->expects('savePresets')
@@ -50,6 +51,7 @@ final class PresetAreaTest extends TestCase
public function testRestoreReplacesTheWholeInventory(): void public function testRestoreReplacesTheWholeInventory(): void
{ {
$writer = Mockery::mock(DesignSystemWriter::class); $writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$stored = PresetBookTest::stored(); $stored = PresetBookTest::stored();
$writer->expects('savePresets')->with(PresetBook::fromDivi($stored)->toRest(), true)->andReturn($stored); $writer->expects('savePresets')->with(PresetBook::fromDivi($stored)->toRest(), true)->andReturn($stored);
@@ -57,6 +57,7 @@ final class VariableAreaTest extends TestCase
public static function adapter(array &$stored, array &$saves, bool $manage = true): GlobalVariablesAdapter public static function adapter(array &$stored, array &$saves, bool $manage = true): GlobalVariablesAdapter
{ {
$variables = Mockery::mock(GlobalVariablesAdapter::class); $variables = Mockery::mock(GlobalVariablesAdapter::class);
$variables->allows('refresh');
$variables->allows('canManage')->andReturn($manage); $variables->allows('canManage')->andReturn($manage);
$variables->allows('read')->andReturnUsing(static function () use (&$stored): array { $variables->allows('read')->andReturnUsing(static function () use (&$stored): array {
return $stored; return $stored;
@@ -47,8 +47,10 @@ final class VariableRestorePlannerTest extends TestCase
private static function planner(bool $manage = true, ?GlobalUsages $usages = null): VariableRestorePlanner private static function planner(bool $manage = true, ?GlobalUsages $usages = null): VariableRestorePlanner
{ {
$writer = Mockery::mock(DesignSystemWriter::class); $writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('canUseVisualBuilder')->andReturn(true); $writer->allows('canUseVisualBuilder')->andReturn(true);
$variables = Mockery::mock(GlobalVariablesAdapter::class); $variables = Mockery::mock(GlobalVariablesAdapter::class);
$variables->allows('refresh');
$variables->allows('canManage')->andReturn($manage); $variables->allows('canManage')->andReturn($manage);
return new VariableRestorePlanner($writer, $variables, $usages ?? UsageFixture::none()); return new VariableRestorePlanner($writer, $variables, $usages ?? UsageFixture::none());
@@ -57,6 +57,7 @@ final class DiviCompositionTest extends TestCase
private function presetSource(int $reads = 1): DesignSystemWriter private function presetSource(int $reads = 1): DesignSystemWriter
{ {
$writer = Mockery::mock(DesignSystemWriter::class); $writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->expects('presets')->times($reads)->andReturn([ $writer->expects('presets')->times($reads)->andReturn([
'module' => ['divi/button' => ['default' => 'p-1', 'items' => [ 'module' => ['divi/button' => ['default' => 'p-1', 'items' => [
'p-1' => ['id' => 'p-1', 'name' => 'Standard', 'moduleName' => 'divi/button'], 'p-1' => ['id' => 'p-1', 'name' => 'Standard', 'moduleName' => 'divi/button'],
@@ -211,6 +212,7 @@ final class DiviCompositionTest extends TestCase
public function testThePresetBookIsNotReadWithoutAPresetInTheInput(): void public function testThePresetBookIsNotReadWithoutAPresetInTheInput(): void
{ {
$writer = Mockery::mock(DesignSystemWriter::class); $writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->shouldNotReceive('presets'); $writer->shouldNotReceive('presets');
$writer->shouldNotReceive('groupsOf'); $writer->shouldNotReceive('groupsOf');
+2
View File
@@ -34,6 +34,8 @@ abstract class TestCase extends BaseTestCase
'wp_slash' => static fn ($value) => $value, 'wp_slash' => static fn ($value) => $value,
'wp_unslash' => static fn ($value) => $value, 'wp_unslash' => static fn ($value) => $value,
'wp_cache_delete' => true, 'wp_cache_delete' => true,
'wp_cache_supports' => true,
'wp_cache_flush_group' => true,
'sanitize_text_field' => static fn ($value): string => trim(strip_tags((string) $value)), 'sanitize_text_field' => static fn ($value): string => trim(strip_tags((string) $value)),
]); ]);