Files
WP-LIT-MCP/tests/Unit/Integrations/Divi/Design/DesignLockTest.php
T

135 lines
5.2 KiB
PHP

<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Design;
use Brain\Monkey\Functions;
use InvalidArgumentException;
use LIT\MCP\Integrations\Divi\Design\DesignLock;
use LIT\MCP\Tests\Support\LockTable;
use LIT\MCP\Tests\Unit\TestCase;
/** Audits L5/L6: one shared lock for colors and theme options; atomic acquire and token-checked release. */
final class DesignLockTest extends TestCase
{
private function table(): LockTable
{
return LockTable::install();
}
protected function tearDown(): void
{
unset($GLOBALS['wpdb']);
parent::tearDown();
}
public function testAcquireIsExclusiveUntilReleased(): void
{
$db = $this->table();
$lock = new DesignLock();
self::assertTrue($lock->acquire('presets'));
self::assertMatchesRegularExpression('/^\d+:[0-9a-f]{8}$/', $db->rows['lit_mcp_lock_presets']);
self::assertFalse((new DesignLock())->acquire('presets'));
self::assertTrue($lock->acquire('variables'), 'areas lock independently');
$lock->release('presets');
self::assertSame([], array_diff_key($db->rows, ['lit_mcp_lock_variables' => 1]));
self::assertTrue((new DesignLock())->acquire('presets'));
}
/** Audit L5: both areas rewrite the whole et_divi array, so they exclude each other. */
public function testColorsAndThemeOptionsShareOneLock(): void
{
$this->table();
self::assertSame(DesignLock::option('colors'), DesignLock::option('theme_options'));
self::assertTrue((new DesignLock())->acquire('colors'));
self::assertFalse((new DesignLock())->acquire('theme_options'));
self::assertTrue((new DesignLock())->acquire('variables'));
}
public function testAStaleLockIsTakenOverExactlyOnce(): void
{
$db = $this->table();
$db->rows['lit_mcp_lock_presets'] = (time() - DesignLock::STALE_AFTER - 1) . ':old';
self::assertTrue((new DesignLock())->acquire('presets'));
self::assertGreaterThan(time() - 5, (int) $db->rows['lit_mcp_lock_presets']);
self::assertFalse((new DesignLock())->acquire('presets'), 'the second taker sees a fresh lock');
}
/** Audit L6: a holder whose lock went stale and was taken over must not free the new holder's lock. */
public function testReleaseOnlyDeletesTheOwnToken(): void
{
$db = $this->table();
$old = new DesignLock();
self::assertTrue($old->acquire('presets'));
$db->rows['lit_mcp_lock_presets'] = (time() - DesignLock::STALE_AFTER - 1) . ':x';
$new = new DesignLock();
self::assertTrue($new->acquire('presets'));
$old->release('presets');
self::assertArrayHasKey('lit_mcp_lock_presets', $db->rows);
self::assertFalse((new DesignLock())->acquire('presets'));
$new->release('presets');
self::assertArrayNotHasKey('lit_mcp_lock_presets', $db->rows);
}
public function testAFreshLockIsNotTakenOver(): void
{
$db = $this->table();
$db->rows['lit_mcp_lock_presets'] = (time() - 10) . ':x';
self::assertFalse((new DesignLock())->acquire('presets'));
}
public function testOptionNamesAndUnknownAreas(): void
{
self::assertSame('lit_mcp_lock_divi_options', DesignLock::option('theme_options'));
self::assertSame('lit_mcp_lock_presets', DesignLock::option('presets'));
$this->expectException(InvalidArgumentException::class);
DesignLock::option('pages');
}
/**
* Audit L5 (re-review): get_option('et_divi') comes from the object cache loaded at the start of the request
* (autoload / alloptions). A writer that finished meanwhile is invisible until the lock holder drops that cache.
*/
public function testTheLockHolderReadsEtDiviFreshNotFromTheRequestStartCache(): void
{
$db = $this->table();
$stored = ['et_divi' => ['color' => 'alt'], 'et_divi_global_variables' => ['v' => 'alt']];
$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) {
$key = 'options:' . $name;
$cache[$key] ??= $stored[$name] ?? false;
return $cache[$key];
});
Functions\when('wp_cache_flush_group')->alias(static function (string $group) use (&$cache): bool {
foreach (array_keys($cache) as $key) {
if (str_starts_with($key, $group . ':')) {
unset($cache[$key]);
}
}
return true;
});
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_global_variables'] = ['v' => 'neu'];
self::assertTrue((new DesignLock())->acquire('colors'));
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);
}
}