feat(cache): WP Fastest Cache statt WP Super Cache

Die Cache-Integration war für WP Super Cache gebaut, eingesetzt wird aber
WP Fastest Cache. Profil (wpfc), Listener, Drosselung, lit/cache-clear und
Leeren-Probe laufen jetzt über die öffentlichen WPFC-Funktionen
wpfc_clear_post_cache_by_id/wpfc_clear_all_cache; die Probe leert nur die
Wegwerf-Seite (singleDeleteCache ohne Elternseiten) und prüft, dass WPFC
den Hook noch verarbeitet. Smoke fastest-cache-smoke.sh ersetzt den WPSC-Smoke.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-11-06 19:57:55 +01:00
co-authored by Claude Opus 5.5
parent e1fad3dffe
commit bee9da3a86
33 changed files with 1231 additions and 1441 deletions
@@ -2,15 +2,15 @@
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\SuperCache;
namespace LIT\MCP\Tests\Unit\Integrations\FastestCache;
use Brain\Monkey\Functions;
use LIT\MCP\Abilities\Ability;
use LIT\MCP\Abilities\Annotations;
use LIT\MCP\Abilities\DeclaresRequirements;
use LIT\MCP\Integrations\SuperCache\CacheClearAbility;
use LIT\MCP\Integrations\SuperCache\SuperCacheFeature;
use LIT\MCP\Integrations\SuperCache\SuperCachePurger;
use LIT\MCP\Integrations\FastestCache\CacheClearAbility;
use LIT\MCP\Integrations\FastestCache\FastestCacheFeature;
use LIT\MCP\Integrations\FastestCache\FastestCachePurger;
use LIT\MCP\Tests\Support\FakeChangeLog;
use LIT\MCP\Tests\Unit\TestCase;
use WP_Error;
@@ -51,20 +51,20 @@ final class CacheClearAbilityTest extends TestCase
protected function tearDown(): void
{
unset($GLOBALS['cache_enabled']);
unset($GLOBALS['wp_fastest_cache_options']);
parent::tearDown();
}
private function ability(?FakeChangeLog $log = null, bool $cacheOn = true, bool $usable = true): CacheClearAbility
{
$purger = new SuperCachePurger(
static fn (SuperCacheFeature $feature): bool => $usable,
$purger = new FastestCachePurger(
static fn (FastestCacheFeature $feature): bool => $usable,
static fn (): bool => $cacheOn,
function (int $id): void {
$this->purges[] = 'post:' . $id;
},
function (int $blog): void {
$this->purges[] = 'all:' . $blog;
function (): void {
$this->purges[] = 'all';
}
);
@@ -80,7 +80,7 @@ final class CacheClearAbilityTest extends TestCase
self::assertFalse($ability->isDirect());
self::assertSame(Annotations::write(true), $ability->annotations());
self::assertStringContainsString('confirm: true', $ability->description());
self::assertStringContainsString('WP Super Cache', $ability->description());
self::assertStringContainsString('WP Fastest Cache', $ability->description());
$schema = $ability->inputSchema();
self::assertSame(['scope'], $schema['required']);
@@ -95,8 +95,8 @@ final class CacheClearAbilityTest extends TestCase
$ability = $this->ability();
self::assertInstanceOf(DeclaresRequirements::class, $ability);
self::assertSame(['wpsc-purge-all'], $ability->requirements(['scope' => 'all']));
self::assertSame(['wpsc-purge-post'], $ability->requirements(['scope' => 'post', 'post_id' => 7]));
self::assertSame(['wpfc-purge-all'], $ability->requirements(['scope' => 'all']));
self::assertSame(['wpfc-purge-post'], $ability->requirements(['scope' => 'post', 'post_id' => 7]));
self::assertSame([], $ability->requirements(['scope' => 'x']));
self::assertSame([], $ability->requirements([]));
}
@@ -165,7 +165,7 @@ final class CacheClearAbilityTest extends TestCase
public function testTheDryRunClearsNothingAndReportsTheState(): void
{
$GLOBALS['cache_enabled'] = true;
$GLOBALS['wp_fastest_cache_options'] = (object) ['wpFastestCacheStatus' => 'on'];
$this->lastPurge = time() - 5;
$payload = self::assertToolError($this->ability()->execute(['scope' => 'all']), 'confirm_required');
@@ -202,17 +202,16 @@ final class CacheClearAbilityTest extends TestCase
self::assertTrue($result['ok']);
self::assertSame('purged', $result['outcome']);
self::assertSame(4, $result['log_id']);
$log->assertRecorded(['ability' => 'lit/cache-clear', 'object_type' => 'wpsc', 'object_id' => '7']);
$log->assertRecorded(['ability' => 'lit/cache-clear', 'object_type' => 'wpfc', 'object_id' => '7']);
}
public function testADraftHasNothingToClear(): void
/** WPFC finds the address of any post (a trashed one too), so no status is skipped. */
public function testADraftIsClearedToo(): void
{
$GLOBALS['cache_enabled'] = true;
$result = $this->ability()->execute(['scope' => 'post', 'post_id' => 8, 'confirm' => true]);
$result = $this->ability(new FakeChangeLog(2))->execute(['scope' => 'post', 'post_id' => 8, 'confirm' => true]);
self::assertSame([], $this->purges);
self::assertSame('nothing', $result['outcome']);
self::assertStringContainsString('Entwurf', $result['summary']);
self::assertSame(['post:8'], $this->purges);
self::assertSame('purged', $result['outcome']);
}
public function testConfirmClearsTheWholeCacheAtOnce(): void
@@ -221,9 +220,9 @@ final class CacheClearAbilityTest extends TestCase
$result = $this->ability($log)->execute(['scope' => 'all', 'confirm' => true]);
self::assertSame(['all:0'], $this->purges);
self::assertSame(['all'], $this->purges);
self::assertSame('purged', $result['outcome']);
$log->assertRecorded(['ability' => 'lit/cache-clear', 'object_type' => 'wpsc', 'object_id' => 'all']);
$log->assertRecorded(['ability' => 'lit/cache-clear', 'object_type' => 'wpfc', 'object_id' => 'all']);
}
public function testAWholePurgeInsideTheLockTimeIsThrottledWithATrailingPurge(): void
@@ -233,7 +232,7 @@ final class CacheClearAbilityTest extends TestCase
$result = $this->ability(new FakeChangeLog(1))->execute(['scope' => 'all', 'confirm' => true]);
self::assertSame([], $this->purges);
self::assertSame([SuperCachePurger::TRAILING_HOOK], $this->scheduled);
self::assertSame([FastestCachePurger::TRAILING_HOOK], $this->scheduled);
self::assertSame('throttled', $result['outcome']);
self::assertStringContainsString('Nachlauf', $result['summary']);
}
@@ -271,7 +270,7 @@ final class CacheClearAbilityTest extends TestCase
self::assertInstanceOf(WP_Error::class, $result);
self::assertSame('lit_compat_incompatible', $result->get_error_code());
self::assertStringContainsString(
'WP Super Cache ist gesperrt (siehe Reiter Kompatibilität). Es wurde nichts geleert.',
'WP Fastest Cache ist gesperrt (siehe Reiter Kompatibilität). Es wurde nichts geleert.',
$result->get_error_message()
);
self::assertSame([], $this->purges);
@@ -2,33 +2,33 @@
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\SuperCache;
namespace LIT\MCP\Tests\Unit\Integrations\FastestCache;
use LIT\MCP\Compatibility\CheckStep;
use LIT\MCP\Integrations\SuperCache\SuperCacheCheckSteps;
use LIT\MCP\Integrations\SuperCache\SuperCacheCompatibility;
use LIT\MCP\Integrations\FastestCache\FastestCacheCheckSteps;
use LIT\MCP\Integrations\FastestCache\FastestCacheCompatibility;
use LIT\MCP\Tests\Unit\TestCase;
final class SuperCacheCheckStepsTest extends TestCase
final class FastestCacheCheckStepsTest extends TestCase
{
public function testTheProbeIsSkippedWithARedManifestAndNotRun(): void
{
$ran = false;
$steps = SuperCacheCheckSteps::extra(static function () use (&$ran): array {
$steps = FastestCacheCheckSteps::extra(static function () use (&$ran): array {
$ran = true;
return [];
})(false);
self::assertCount(1, $steps);
self::assertSame(SuperCacheCheckSteps::PROBE, $steps[0]->key);
self::assertSame(FastestCacheCheckSteps::PROBE, $steps[0]->key);
self::assertTrue($steps[0]->skipped);
self::assertFalse($ran);
}
public function testTheStepCarriesTheProbeProblems(): void
{
$steps = SuperCacheCheckSteps::extra(static fn (): array => ['rot'])(true);
$steps = FastestCacheCheckSteps::extra(static fn (): array => ['rot'])(true);
self::assertSame(['rot'], $steps[0]->problems);
self::assertFalse($steps[0]->skipped);
@@ -38,7 +38,7 @@ final class SuperCacheCheckStepsTest extends TestCase
/** A switched-off cache is no red step: a skipped one would lock the purge of a site that simply caches nothing. */
public function testWithoutAnActiveCacheTheStepIsGreenAndSaysSo(): void
{
$steps = SuperCacheCheckSteps::extra(static fn (): ?array => null)(true);
$steps = FastestCacheCheckSteps::extra(static fn (): ?array => null)(true);
self::assertTrue($steps[0]->green());
self::assertStringContainsString('Cache ist ausgeschaltet, nicht geprüft', $steps[0]->label());
@@ -46,11 +46,11 @@ final class SuperCacheCheckStepsTest extends TestCase
public function testTheLabelOfAnOldStepComesFromTheKey(): void
{
self::assertSame('Leeren-Test an einer Wegwerf-Seite im Cache', SuperCacheCheckSteps::label(new CheckStep(SuperCacheCheckSteps::PROBE, [])));
self::assertSame('Leeren-Test an einer Wegwerf-Seite im Cache', FastestCacheCheckSteps::label(new CheckStep(FastestCacheCheckSteps::PROBE, [])));
self::assertSame(
SuperCacheCompatibility::LABEL . '-Funktionen, Klassen und Registrierungen',
SuperCacheCheckSteps::label(new CheckStep(CheckStep::MANIFEST, []))
FastestCacheCompatibility::LABEL . '-Funktionen, Klassen und Registrierungen',
FastestCacheCheckSteps::label(new CheckStep(CheckStep::MANIFEST, []))
);
self::assertSame('other', SuperCacheCheckSteps::label(new CheckStep('other', [])));
self::assertSame('other', FastestCacheCheckSteps::label(new CheckStep('other', [])));
}
}
@@ -0,0 +1,136 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\FastestCache;
use Brain\Monkey\Functions;
use LIT\MCP\Compatibility\Call;
use LIT\MCP\Compatibility\CheckStep;
use LIT\MCP\Compatibility\Compatibility;
use LIT\MCP\Compatibility\Tier;
use LIT\MCP\Integrations\Divi\Release\DiviProfile;
use LIT\MCP\Integrations\Pods\PodsCompatibility;
use LIT\MCP\Integrations\FastestCache\FastestCacheCheckSteps;
use LIT\MCP\Integrations\FastestCache\FastestCacheCompatibility;
use LIT\MCP\Integrations\FastestCache\FastestCacheFeature;
use LIT\MCP\Tests\Unit\TestCase;
final class FastestCacheCompatibilityTest extends TestCase
{
public function testDescribesFastestCacheAsALockOnFailurePlugin(): void
{
$profile = FastestCacheCompatibility::profile();
self::assertSame('wpfc', $profile->id);
self::assertSame('WP Fastest Cache', $profile->label);
self::assertSame(Tier::LockOnFailure, $profile->tier);
self::assertSame(['1.5.5'], $profile->testedVersions);
self::assertNull($profile->minVersion);
self::assertSame(FastestCacheFeature::cases(), $profile->capabilities);
}
/** Argument counts are those the purger and the probe pass. */
public function testTheManifestHoldsTheTwoPurgeFunctionsWithTheirArgumentCounts(): void
{
$manifest = FastestCacheCompatibility::profile()->manifests[0];
$calls = [];
foreach ($manifest->calls as $call) {
$calls[$call->symbol] = [$call->args, $call->features()];
}
self::assertSame([
'wpfc_clear_post_cache_by_id' => [1, [FastestCacheFeature::PurgePost]],
'wpfc_clear_all_cache' => [0, [FastestCacheFeature::PurgeAll]],
'WpFastestCache::singleDeleteCache' => [3, [FastestCacheFeature::PurgePost]],
'WpFastestCache::getWpContentDir' => [1, [FastestCacheFeature::PurgePost]],
], $calls);
self::assertContainsOnlyInstancesOf(Call::class, $manifest->calls);
}
/** The instance that hooks the purge actions and the decoded options with the cache switch. */
public function testTheGlobalsTheFunctionsReadAreRequired(): void
{
$manifest = FastestCacheCompatibility::profile()->manifests[0];
self::assertSame(['WPFC_MAIN_PATH'], array_map(static fn ($c) => $c->name, $manifest->constants));
$globals = [];
foreach ($manifest->globals as $global) {
$globals[$global->name] = $global->features();
}
self::assertSame(
['wp_fastest_cache' => FastestCacheFeature::cases(), 'wp_fastest_cache_options' => FastestCacheFeature::cases()],
$globals
);
}
/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
public function testActiveAndVersionFollowTheMainPathConstantAndThePluginHeader(): void
{
$profile = FastestCacheCompatibility::profile();
self::assertFalse(($profile->active)());
self::assertNull(($profile->version)());
define('WPFC_MAIN_PATH', '/plugins/wp-fastest-cache/');
Functions\expect('get_file_data')
->once()
->with('/plugins/wp-fastest-cache/wpFastestCache.php', ['Version' => 'Version'])
->andReturn(['Version' => '1.5.5']);
self::assertTrue(($profile->active)());
self::assertSame('1.5.5', ($profile->version)());
}
/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
public function testAnUnreadableHeaderIsAnUnknownVersion(): void
{
define('WPFC_MAIN_PATH', '/plugins/wp-fastest-cache/');
Functions\when('get_file_data')->justReturn(['Version' => '']);
self::assertNull((FastestCacheCompatibility::profile()->version)());
}
public function testTheCheckHasTheProbeAsExtraStep(): void
{
$profile = FastestCacheCompatibility::profile();
self::assertNotNull($profile->extraSteps);
$steps = ($profile->extraSteps)(false);
self::assertSame([FastestCacheCheckSteps::PROBE], array_map(static fn ($step): string => $step->key, $steps));
self::assertTrue($steps[0]->skipped);
self::assertNotNull($profile->stepLabel);
self::assertSame(
'Leeren-Test an einer Wegwerf-Seite im Cache',
($profile->stepLabel)(new CheckStep(FastestCacheCheckSteps::PROBE, []))
);
}
public function testTheCacheIsOnlyEnabledWhileTheSwitchInTheDecodedOptionsIsOn(): void
{
unset($GLOBALS['wp_fastest_cache_options']);
self::assertFalse(FastestCacheCompatibility::cacheEnabled());
$GLOBALS['wp_fastest_cache_options'] = [];
self::assertFalse(FastestCacheCompatibility::cacheEnabled(), 'WPFC stores [] when the option is missing');
$GLOBALS['wp_fastest_cache_options'] = (object) ['wpFastestCacheMinifyHtml' => 'on'];
self::assertFalse(FastestCacheCompatibility::cacheEnabled());
$GLOBALS['wp_fastest_cache_options'] = (object) ['wpFastestCacheStatus' => 'on'];
self::assertTrue(FastestCacheCompatibility::cacheEnabled());
unset($GLOBALS['wp_fastest_cache_options']);
}
public function testAllProfilesCoexistWithoutIdCollision(): void
{
$compatibility = new Compatibility(
[DiviProfile::profile(), PodsCompatibility::profile(), FastestCacheCompatibility::profile()]
);
self::assertSame(['divi', 'pods', 'wpfc'], array_map(static fn ($p) => $p->id, $compatibility->profiles()));
self::assertSame(FastestCacheFeature::PurgeAll, $compatibility->capability('wpfc-purge-all'));
}
}
@@ -2,25 +2,25 @@
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\SuperCache;
namespace LIT\MCP\Tests\Unit\Integrations\FastestCache;
use LIT\MCP\Compatibility\Capability;
use LIT\MCP\Integrations\SuperCache\SuperCacheFeature;
use LIT\MCP\Integrations\FastestCache\FastestCacheFeature;
use LIT\MCP\Tests\Unit\TestCase;
final class SuperCacheFeatureTest extends TestCase
final class FastestCacheFeatureTest extends TestCase
{
public function testEveryFeatureIsAWritingCapabilityWithAPrefixedId(): void
{
foreach (SuperCacheFeature::cases() as $feature) {
foreach (FastestCacheFeature::cases() as $feature) {
self::assertInstanceOf(Capability::class, $feature);
self::assertSame($feature->value, $feature->id());
self::assertNotSame('', $feature->label());
self::assertTrue($feature->isWrite());
}
self::assertSame(
['wpsc-purge-post', 'wpsc-purge-all'],
array_map(static fn (SuperCacheFeature $f): string => $f->id(), SuperCacheFeature::cases())
['wpfc-purge-post', 'wpfc-purge-all'],
array_map(static fn (FastestCacheFeature $f): string => $f->id(), FastestCacheFeature::cases())
);
}
}
@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\FastestCache;
use Brain\Monkey\Functions;
use LIT\MCP\Admin\CompatibilityTab;
use LIT\MCP\Compatibility\ProvidesCompatibility;
use LIT\MCP\Integrations\Bootable;
use LIT\MCP\Integrations\Integration;
use LIT\MCP\Integrations\FastestCache\FastestCacheCompatibility;
use LIT\MCP\Integrations\FastestCache\FastestCacheIntegration;
use LIT\MCP\Integrations\FastestCache\FastestCachePurger;
use LIT\MCP\Tests\Support\FakeChangeLog;
use LIT\MCP\Tests\Unit\TestCase;
final class FastestCacheIntegrationTest extends TestCase
{
private static function purger(): FastestCachePurger
{
return new FastestCachePurger(static fn (): bool => true);
}
public function testIsAnIntegrationThatProvidesItsProfileAndTheCacheClearTool(): void
{
$integration = new FastestCacheIntegration(null, self::purger(), FakeChangeLog::forbidden());
self::assertInstanceOf(Integration::class, $integration);
self::assertInstanceOf(ProvidesCompatibility::class, $integration);
self::assertSame('wpfc', $integration->id());
self::assertSame('WP Fastest Cache', $integration->label());
self::assertEquals(['lit/cache-clear'], array_map(static fn ($a): string => $a->name(), $integration->abilities()));
self::assertEquals([FastestCacheCompatibility::profile()], $integration->compatibilities());
}
/**
* The same constant the profile's active() reads: WP Fastest Cache defines it when its plugin file loads.
*
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
public function testIsAvailableOnlyWithFastestCache(): void
{
$integration = new FastestCacheIntegration(null, self::purger(), FakeChangeLog::forbidden());
self::assertFalse($integration->isAvailable());
define('WPFC_MAIN_PATH', '/plugins/wp-fastest-cache/');
self::assertTrue($integration->isAvailable());
self::assertTrue((FastestCacheCompatibility::profile()->active)());
}
public function testBootHooksTheOrphanCleanupIntoTheProfileLoad(): void
{
$integration = new FastestCacheIntegration(null, self::purger(), FakeChangeLog::forbidden());
$integration->boot();
self::assertInstanceOf(Bootable::class, $integration);
self::assertNotFalse(has_action(CompatibilityTab::LOAD_ACTION, [$integration, 'removeOrphans']));
}
public function testNoCleanupForAnotherProfileWithoutWpscOrWithoutTheCapability(): void
{
Functions\expect('get_posts')->never();
Functions\when('current_user_can')->justReturn(true);
(new FastestCacheIntegration(static fn (): bool => true, self::purger(), FakeChangeLog::forbidden()))->removeOrphans('pods');
(new FastestCacheIntegration(static fn (): bool => false, self::purger(), FakeChangeLog::forbidden()))->removeOrphans('wpfc');
Functions\when('current_user_can')->justReturn(false);
(new FastestCacheIntegration(static fn (): bool => true, self::purger(), FakeChangeLog::forbidden()))->removeOrphans('wpfc');
$this->addToAssertionCount(1);
}
public function testCleansUpPagesForTheAdministratorWithWpsc(): void
{
Functions\when('current_user_can')->justReturn(true);
Functions\expect('get_posts')->once()->andReturnUsing(static function (array $args): array {
self::assertSame('page', $args['post_type']);
self::assertSame(['draft', 'private'], $args['post_status']);
return [];
});
(new FastestCacheIntegration(static fn (): bool => true, self::purger(), FakeChangeLog::forbidden()))->removeOrphans('wpfc');
}
}
@@ -2,19 +2,19 @@
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\SuperCache;
namespace LIT\MCP\Tests\Unit\Integrations\FastestCache;
use Brain\Monkey\Functions;
use Closure;
use LIT\MCP\Admin\CompatibilityTab;
use LIT\MCP\Compatibility\Compatibility;
use LIT\MCP\Integrations\SuperCache\SuperCacheCompatibility;
use LIT\MCP\Integrations\SuperCache\SuperCacheFeature;
use LIT\MCP\Integrations\SuperCache\SuperCacheListener;
use LIT\MCP\Integrations\FastestCache\FastestCacheCompatibility;
use LIT\MCP\Integrations\FastestCache\FastestCacheFeature;
use LIT\MCP\Integrations\FastestCache\FastestCacheListener;
use LIT\MCP\Tests\Unit\TestCase;
use WP_Post;
final class SuperCacheListenerTest extends TestCase
final class FastestCacheListenerTest extends TestCase
{
/** @var list<string> */
private array $purges = [];
@@ -56,7 +56,7 @@ final class SuperCacheListenerTest extends TestCase
{
ini_set('error_log', (string) $this->previousErrorLog);
unlink($this->errorLog);
unset($GLOBALS['cache_enabled']);
unset($GLOBALS['wp_fastest_cache_options']);
parent::tearDown();
}
@@ -66,17 +66,17 @@ final class SuperCacheListenerTest extends TestCase
bool $usablePost = true,
bool $usableAll = true,
?Closure $postIdFor = null
): SuperCacheListener {
return new SuperCacheListener(
static fn (SuperCacheFeature $feature): bool => $feature === SuperCacheFeature::PurgePost
): FastestCacheListener {
return new FastestCacheListener(
static fn (FastestCacheFeature $feature): bool => $feature === FastestCacheFeature::PurgePost
? $usablePost
: $usableAll,
static fn (): bool => $cacheOn,
function (int $id): void {
$this->purges[] = 'post:' . $id;
},
function (int $blog): void {
$this->purges[] = 'all:' . $blog;
function (): void {
$this->purges[] = 'all';
},
$postIdFor ?? static fn (string $type, string $id): ?int => in_array($type, ['page', 'post'], true)
? (int) $id
@@ -120,7 +120,7 @@ final class SuperCacheListenerTest extends TestCase
$listener->onChange('lit/content-update', 'post', '14', 7);
$listener->flush();
self::assertSame(['all:0'], $this->purges);
self::assertSame(['all'], $this->purges);
}
public function testThePurgeIsDoneOncePerFlush(): void
@@ -143,7 +143,7 @@ final class SuperCacheListenerTest extends TestCase
$listener->flush();
$listener->onChange('lit/content-update', 'page', '14', 7);
$listener->flush();
self::assertSame(['all:0', 'post:14'], $this->purges);
self::assertSame(['all', 'post:14'], $this->purges);
}
/** @dataProvider siteWideChanges */
@@ -153,7 +153,7 @@ final class SuperCacheListenerTest extends TestCase
$listener->onChange($ability, $type, $id, null);
$listener->flush();
self::assertSame(['all:0'], $this->purges);
self::assertSame(['all'], $this->purges);
}
/** @return array<string, array{string, string, string}> */
@@ -175,7 +175,7 @@ final class SuperCacheListenerTest extends TestCase
$listener->onChange('lit/post-trash', 'post', '99', 1);
$listener->flush();
self::assertSame(['all:0'], $this->purges);
self::assertSame(['all'], $this->purges);
}
/** One whole-cache change among single posts: still one purge, no single ones on top. */
@@ -186,7 +186,7 @@ final class SuperCacheListenerTest extends TestCase
$listener->onChange('lit/wp-settings-update', 'option', 'a', 2);
$listener->flush();
self::assertSame(['all:0'], $this->purges);
self::assertSame(['all'], $this->purges);
}
/** Release and withdraw entries of the check workflow change nothing on the site (shared ability name). */
@@ -204,8 +204,8 @@ final class SuperCacheListenerTest extends TestCase
public function testTheCacheClearToolsOwnEntriesPurgeNothing(): void
{
$listener = $this->listener();
$listener->onChange('lit/cache-clear', 'wpsc', 'all', 1);
$listener->onChange('lit/cache-clear', 'wpsc', '12', 2);
$listener->onChange('lit/cache-clear', 'wpfc', 'all', 1);
$listener->onChange('lit/cache-clear', 'wpfc', '12', 2);
$listener->flush();
self::assertSame([], $this->purges);
@@ -233,7 +233,7 @@ final class SuperCacheListenerTest extends TestCase
$listener->flush();
self::assertSame([], $this->purges);
self::assertSame(1, substr_count($this->logged(), 'wpsc-purge-post'));
self::assertSame(1, substr_count($this->logged(), 'wpfc-purge-post'));
}
/** The capabilities lock separately: a gone purge-post does not stop the whole-cache purge. */
@@ -244,7 +244,7 @@ final class SuperCacheListenerTest extends TestCase
$listener->onChange('lit/wp-settings-update', 'option', 'blogname', 1);
$listener->flush();
self::assertSame(['all:0'], $this->purges);
self::assertSame(['all'], $this->purges);
}
public function testALockedWholeCachePurgeSchedulesNothing(): void
@@ -258,18 +258,6 @@ final class SuperCacheListenerTest extends TestCase
self::assertSame([], $this->scheduled);
}
public function testOnAMultisiteTheCurrentBlogIsPurged(): void
{
Functions\when('is_multisite')->justReturn(true);
Functions\when('get_current_blog_id')->justReturn(3);
$listener = $this->listener();
$listener->onChange('lit/wp-settings-update', 'option', 'blogname', 1);
$listener->flush();
self::assertSame(['all:3'], $this->purges);
}
public function testAWholeCachePurgeStampsTheTimeForThirtySeconds(): void
{
$listener = $this->listener();
@@ -277,7 +265,7 @@ final class SuperCacheListenerTest extends TestCase
$listener->flush();
self::assertCount(1, $this->stamped);
self::assertSame('lit_mcp_wpsc_purged_at', $this->stamped[0][0]);
self::assertSame('lit_mcp_wpfc_purged_at', $this->stamped[0][0]);
self::assertEqualsWithDelta(time(), $this->stamped[0][1], 2);
self::assertSame(30, $this->stamped[0][2]);
}
@@ -305,7 +293,7 @@ final class SuperCacheListenerTest extends TestCase
self::assertSame([], $this->purges);
self::assertSame([], $this->stamped);
self::assertSame([[$this->lastPurge + 30, 'lit_mcp_wpsc_trailing_purge']], $this->scheduled);
self::assertSame([[$this->lastPurge + 30, 'lit_mcp_wpfc_trailing_purge']], $this->scheduled);
}
/** Options-table transients come back as strings. */
@@ -337,7 +325,7 @@ final class SuperCacheListenerTest extends TestCase
$listener = $this->listener();
$listener->trailingPurge();
self::assertSame(['all:0'], $this->purges);
self::assertSame(['all'], $this->purges);
self::assertCount(1, $this->stamped);
}
@@ -351,7 +339,7 @@ final class SuperCacheListenerTest extends TestCase
public function testAThrowingTrailingPurgeIsLogged(): void
{
$listener = new SuperCacheListener(
$listener = new FastestCacheListener(
static fn (): bool => true,
static fn (): bool => true,
null,
@@ -367,13 +355,13 @@ final class SuperCacheListenerTest extends TestCase
public function testAThrowingPurgeIsLoggedAndNotRethrown(): void
{
$listener = new SuperCacheListener(
$listener = new FastestCacheListener(
static fn (): bool => true,
static fn (): bool => true,
static function (): void {
throw new \RuntimeException('disk full');
},
static fn (int $blog): null => null,
static fn (): null => null,
static fn (): int => 12
);
@@ -389,37 +377,37 @@ final class SuperCacheListenerTest extends TestCase
$listener->register();
self::assertSame(10, has_action('lit_mcp_changed', [$listener, 'onChange']));
self::assertSame(10, has_action('lit_mcp_wpsc_trailing_purge', [$listener, 'trailingPurge']));
self::assertSame(10, has_action('lit_mcp_wpfc_trailing_purge', [$listener, 'trailingPurge']));
}
/** Plugin wires create(): without WP Super Cache nothing is usable, and with the cache off nothing is asked. */
/** Plugin wires create(): without WP Fastest Cache nothing is usable, and with the cache off nothing is asked. */
public function testCreateAsksTheCompatibilityOnlyWhenTheCacheIsOn(): void
{
$compatibility = new Compatibility([SuperCacheCompatibility::profile()]);
$listener = SuperCacheListener::create($compatibility);
$compatibility = new Compatibility([FastestCacheCompatibility::profile()]);
$listener = FastestCacheListener::create($compatibility);
$listener->onChange('lit/wp-settings-update', 'option', 'blogname', 1);
$listener->flush();
self::assertSame('', $this->logged());
$GLOBALS['cache_enabled'] = true;
$GLOBALS['wp_fastest_cache_options'] = (object) ['wpFastestCacheStatus' => 'on'];
$listener->onChange('lit/wp-settings-update', 'option', 'blogname', 1);
$listener->flush();
self::assertStringContainsString('wpsc-purge-all not usable', $this->logged());
self::assertStringContainsString('wpfc-purge-all not usable', $this->logged());
}
public function testTheDefaultCacheSwitchIsTheWpscGlobal(): void
public function testTheDefaultCacheSwitchIsTheStatusOfWpfc(): void
{
$post = new WP_Post();
Functions\when('get_post')->justReturn($post);
Functions\when('get_post_type_object')->justReturn((object) ['public' => true]);
$listener = new SuperCacheListener(static fn (): bool => true);
$listener = new FastestCacheListener(static fn (): bool => true);
$GLOBALS['cache_enabled'] = false;
$GLOBALS['wp_fastest_cache_options'] = (object) [];
$listener->onChange('lit/content-update', 'page', '12', 5);
$listener->flush();
self::assertSame('', $this->logged(), 'unset or false cache_enabled: silent, nothing purged');
self::assertSame('', $this->logged(), 'cache switched off: silent, nothing purged');
self::assertFalse(has_action('shutdown', [$listener, 'flush']));
}
@@ -430,18 +418,18 @@ final class SuperCacheListenerTest extends TestCase
bool $public,
bool $exists
): void {
$GLOBALS['cache_enabled'] = true;
$GLOBALS['wp_fastest_cache_options'] = (object) ['wpFastestCacheStatus' => 'on'];
Functions\when('get_post_type_object')->justReturn((object) ['public' => $public]);
Functions\when('get_post')->justReturn($exists ? new WP_Post() : null);
$purged = [];
Functions\when('wp_cache_clear_cache')->alias(static function () use (&$purged): void {
Functions\when('wpfc_clear_all_cache')->alias(static function () use (&$purged): void {
$purged[] = 'all';
});
Functions\when('wp_cache_post_edit')->alias(static function () use (&$purged): void {
Functions\when('wpfc_clear_post_cache_by_id')->alias(static function () use (&$purged): void {
$purged[] = 'post';
});
$listener = new SuperCacheListener(static fn (): bool => true);
$listener = new FastestCacheListener(static fn (): bool => true);
$listener->onChange('lit/x', $type, $id, 1);
$listener->flush();
@@ -0,0 +1,421 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\FastestCache;
use Brain\Monkey\Functions;
use Closure;
use LIT\MCP\Compatibility\ThrowawayPost;
use LIT\MCP\Integrations\FastestCache\FastestCacheProbe;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
use RuntimeException;
use WP_Post;
final class FastestCacheProbeTest extends TestCase
{
private string $cacheRoot = '';
/** @var list<int> */
private array $deleted = [];
/** @var array<string, mixed>|null The wp_insert_post arguments. */
private ?array $inserted = null;
/** @var array<string, mixed> Globals the test changes (only those that existed). */
private array $savedGlobals = [];
private const GLOBALS_CHANGED = ['wp_fastest_cache', 'wp_fastest_cache_options', 'wpdb'];
protected function setUp(): void
{
parent::setUp();
$this->cacheRoot = sys_get_temp_dir() . '/lit-mcp-wpfc-' . bin2hex(random_bytes(4)) . '/';
mkdir($this->cacheRoot . 'cache/all/', 0777, true);
$this->deleted = [];
$this->inserted = null;
$this->savedGlobals = [];
foreach (self::GLOBALS_CHANGED as $name) {
if (array_key_exists($name, $GLOBALS)) {
$this->savedGlobals[$name] = $GLOBALS[$name];
}
}
Functions\when('wp_insert_post')->alias(function (array $post): int {
$this->inserted = $post;
return 77;
});
Functions\when('wp_delete_post')->alias(function (int $id, bool $force): WP_Post {
self::assertTrue($force, 'never to the trash');
$this->deleted[] = $id;
return new WP_Post(['ID' => $id]);
});
Functions\when('wp_mkdir_p')->alias(static fn (string $dir): bool => is_dir($dir) || @mkdir($dir, 0777, true));
}
protected function tearDown(): void
{
foreach (self::GLOBALS_CHANGED as $name) {
if (array_key_exists($name, $this->savedGlobals)) {
$GLOBALS[$name] = $this->savedGlobals[$name];
} else {
unset($GLOBALS[$name]);
}
}
self::removeTree($this->cacheRoot);
parent::tearDown();
}
private static function removeTree(string $path): void
{
foreach (glob(rtrim($path, '/') . '/{,.}*', GLOB_BRACE) ?: [] as $entry) {
if (in_array(basename($entry), ['.', '..'], true)) {
continue;
}
is_dir($entry) ? self::removeTree($entry) : unlink($entry);
}
@rmdir($path);
}
private function pageDir(): string
{
return $this->cacheRoot . 'cache/all/lit-mcp-check-77/';
}
/**
* @param (Closure(int): void)|null $purge
* @param (Closure(int): ?string)|null $cacheDir
*/
private function probe(
?Closure $purge = null,
?Closure $cacheDir = null,
bool $enabled = true,
bool $hooked = true,
bool $slugSet = true
): FastestCacheProbe {
return new FastestCacheProbe(
static fn (): bool => $enabled,
static fn (): bool => $hooked,
$cacheDir ?? fn (int $id): string => $this->pageDir(),
$purge ?? fn (int $id) => unlink($this->pageDir() . 'index.html'),
static fn (int $id): bool => $slugSet
);
}
/** Captures what Log writes to the PHP error log while $run runs. */
private function logged(Closure $run): string
{
$log = (string) tempnam(sys_get_temp_dir(), 'lit-mcp-log');
$previous = ini_set('error_log', $log);
try {
$run();
return (string) file_get_contents($log);
} finally {
ini_set('error_log', (string) $previous);
unlink($log);
}
}
public function testGreenWhenThePurgeRemovesTheCacheFileAndNothingIsLeftBehind(): void
{
$probe = $this->probe(function (int $id): void {
self::assertFileExists($this->pageDir() . 'index.html', 'the file is there before the purge');
unlink($this->pageDir() . 'index.html');
});
self::assertSame([], $probe->run());
self::assertDirectoryDoesNotExist($this->pageDir());
self::assertDirectoryExists($this->cacheRoot . 'cache/all/');
self::assertSame([77], $this->deleted);
}
public function testCreatesAMarkedDraftPage(): void
{
$this->probe()->run();
self::assertSame('page', $this->inserted['post_type']);
self::assertSame('draft', $this->inserted['post_status']);
self::assertStringStartsWith(ThrowawayPost::TITLE, $this->inserted['post_title']);
self::assertSame([ThrowawayPost::MARKER => '1'], $this->inserted['meta_input']);
}
/** WPFC moves the whole directory away (to cache/tmpWpfc). */
public function testADirectoryThePurgeMovedAwayIsGreen(): void
{
$problems = $this->probe(function (int $id): void {
rename($this->pageDir(), $this->cacheRoot . 'moved');
})->run();
self::assertSame([], $problems);
}
public function testACacheFileThatSurvivesThePurgeIsAFixedTextAndIsRemovedAnyway(): void
{
$problems = [];
$logged = $this->logged(function () use (&$problems): void {
$problems = $this->probe(static fn (int $id) => null)->run();
});
self::assertSame(['Die Testdatei im Cache blieb nach dem Leeren durch WP Fastest Cache bestehen.'], $problems);
self::assertStringContainsString('lit-mcp-check-77', $logged);
self::assertDirectoryDoesNotExist($this->pageDir());
self::assertSame([77], $this->deleted);
}
public function testAForeignFileInThePageDirectoryIsLeftAlone(): void
{
$problems = $this->probe(function (int $id): void {
unlink($this->pageDir() . 'index.html');
touch($this->pageDir() . 'fremd.txt');
})->run();
self::assertSame([], $problems);
self::assertFileExists($this->pageDir() . 'fremd.txt');
}
public function testAnExceptionOfThePurgeIsAFixedTextALogLineAndEverythingIsCleanedUp(): void
{
$problems = [];
$logged = $this->logged(function () use (&$problems): void {
$problems = $this->probe(static function (int $id): never {
throw new RuntimeException('WPFC kaputt');
})->run();
});
self::assertSame(['Fastest-Cache-Probe abgebrochen (Details im PHP-Error-Log).'], $problems);
self::assertStringContainsString('WPFC kaputt', $logged);
self::assertDirectoryDoesNotExist($this->pageDir());
self::assertSame([77], $this->deleted);
}
public function testNothingToProbeWhileTheCacheIsSwitchedOff(): void
{
self::assertNull($this->probe(enabled: false)->run());
self::assertNull($this->inserted);
}
/** Without the hook wpfc_clear_post_cache_by_id does nothing: the purge of a post would silently fail. */
public function testAMissingPurgeHookIsAProblemWithoutAnyPage(): void
{
self::assertSame(
['WP Fastest Cache verarbeitet wpfc_clear_post_cache_by_id nicht mehr; das Leeren eines Beitrags bliebe wirkungslos.'],
$this->probe(hooked: false)->run()
);
self::assertNull($this->inserted);
}
public function testNothingToProbeWithoutACachePathForThePage(): void
{
self::assertNull($this->probe(cacheDir: static fn (int $id): ?string => null)->run());
self::assertSame([77], $this->deleted);
}
public function testARefusedSlugIsItsOwnFixedProblemAndTheProbeNeverPurges(): void
{
$purged = false;
$problems = [];
$logged = $this->logged(function () use (&$problems, &$purged): void {
$problems = $this->probe(static function (int $id) use (&$purged): void {
$purged = true;
}, slugSet: false)->run();
});
self::assertSame(['Die Adresse der Testseite konnte nicht gesetzt werden (Details im PHP-Error-Log).'], $problems);
self::assertStringContainsString('77', $logged);
self::assertFalse($purged);
self::assertSame([77], $this->deleted);
}
public function testACacheDirectoryThatCannotBeCreatedIsAFixedText(): void
{
$blocker = $this->cacheRoot . 'blocker';
file_put_contents($blocker, 'x');
$problems = [];
$logged = $this->logged(function () use (&$problems, $blocker): void {
$problems = $this->probe(cacheDir: static fn (int $id): string => $blocker . '/seite/')->run();
});
self::assertSame(['Die Testdatei konnte nicht im Cache-Verzeichnis angelegt werden (Details im PHP-Error-Log).'], $problems);
self::assertStringContainsString('seite', $logged);
self::assertSame([77], $this->deleted);
}
// --- Defaults: the real WPFC and WordPress calls ----------------------------------------------------------------
/** Records calls; getWpContentDir like WPFC without WPML/multisite: content dir plus path. */
private function fakeWpfc(): object
{
return new class ($this->cacheRoot) {
/** @var list<array{mixed, mixed, mixed}> */
public array $calls = [];
public function __construct(private string $root)
{
}
public function singleDeleteCache(mixed $comment = false, mixed $post = false, mixed $parents = true): void
{
$this->calls[] = [$comment, $post, $parents];
}
public function getWpContentDir(string $path): string
{
return rtrim($this->root, '/') . $path;
}
};
}
public function testTheDefaultPurgeIsSingleDeleteCacheWithoutTheParentPages(): void
{
$GLOBALS['wp_fastest_cache'] = $wpfc = $this->fakeWpfc();
$probe = new FastestCacheProbe(
static fn (): bool => true,
static fn (): bool => true,
fn (int $id): string => $this->pageDir(),
null,
static fn (int $id): bool => true
);
$this->logged(static function () use ($probe): void {
$probe->run();
});
self::assertSame([[false, 77, false]], $wpfc->calls);
}
public function testTheDefaultHookCheckAsksForTheMethodOfTheWpfcInstance(): void
{
$GLOBALS['wp_fastest_cache'] = $wpfc = $this->fakeWpfc();
Functions\expect('has_action')->twice()
->with('wpfc_clear_post_cache_by_id', [$wpfc, 'singleDeleteCache'])
->andReturn(10, false);
$probe = new FastestCacheProbe(
static fn (): bool => true,
null,
static fn (int $id): ?string => null,
null,
static fn (int $id): bool => true
);
self::assertNull($probe->run(), 'hooked: on to the page, no path: not checked');
self::assertCount(1, $probe->run() ?? [], 'not hooked: one problem');
}
public function testTheDefaultEnabledCheckReadsTheWpfcSwitch(): void
{
$GLOBALS['wp_fastest_cache_options'] = (object) [];
self::assertNull((new FastestCacheProbe())->run());
$GLOBALS['wp_fastest_cache_options'] = [];
self::assertNull((new FastestCacheProbe())->run());
}
public function testTheDefaultSlugIsWrittenStraightToTheDatabaseWithoutHooks(): void
{
$wpdb = Mockery::mock();
$wpdb->posts = 'wp_posts';
$wpdb->expects('update')->with('wp_posts', ['post_name' => 'lit-mcp-check-77'], ['ID' => 77])->andReturn(1);
$GLOBALS['wpdb'] = $wpdb;
Functions\expect('clean_post_cache')->never();
$dropped = [];
Functions\when('wp_cache_delete')->alias(static function (int $id, string $group) use (&$dropped): bool {
$dropped[] = $group . ':' . $id;
return true;
});
$probe = new FastestCacheProbe(
static fn (): bool => true,
static fn (): bool => true,
fn (int $id): string => $this->pageDir(),
fn (int $id) => unlink($this->pageDir() . 'index.html')
);
self::assertSame([], $probe->run());
self::assertSame(['posts:77'], $dropped, 'the object cache must not keep the empty slug');
}
/** @return array<string, array{0: int|false}> */
public static function refusedUpdates(): array
{
return ['database error' => [false], 'no row changed' => [0]];
}
/** @dataProvider refusedUpdates */
public function testTheDefaultSlugWriterTreatsAFailedOrEmptyUpdateAsRefused(int|false $result): void
{
$wpdb = Mockery::mock();
$wpdb->posts = 'wp_posts';
$wpdb->shouldReceive('update')->andReturn($result);
$GLOBALS['wpdb'] = $wpdb;
Functions\when('wp_cache_delete')->justReturn(true);
$problems = [];
$this->logged(function () use (&$problems): void {
$problems = (new FastestCacheProbe(
static fn (): bool => true,
static fn (): bool => true,
fn (int $id): string => $this->pageDir(),
static fn (int $id) => self::fail('no purge without the slug')
))->run();
});
self::assertSame(['Die Adresse der Testseite konnte nicht gesetzt werden (Details im PHP-Error-Log).'], $problems);
}
private function permalink(string $address): void
{
$GLOBALS['wp_fastest_cache'] = $this->fakeWpfc();
Functions\when('get_post')->justReturn(new WP_Post(['ID' => 77, 'post_status' => 'draft']));
Functions\when('get_permalink')->alias(static function (WP_Post $post) use ($address): string {
self::assertSame('publish', $post->post_status, 'the address the draft would have once published');
return $address;
});
}
public function testTheDefaultCachePathIsTheProbePageDirectoryBelowCacheAll(): void
{
$this->permalink('https://example.test/lit-mcp-check-77/');
self::assertSame($this->pageDir(), FastestCacheProbe::cacheDirFor(77));
}
public function testTheDefaultCachePathKeepsTheSubdirectoryOfTheSite(): void
{
$this->permalink('https://example.test/wp/lit-mcp-check-77/');
self::assertSame($this->cacheRoot . 'cache/all/wp/lit-mcp-check-77/', FastestCacheProbe::cacheDirFor(77));
}
/** @return array<string, array{0: string}> */
public static function foreignAddresses(): array
{
return [
'plain address' => ['https://example.test/?page_id=77'],
'home page' => ['https://example.test/'],
'other slug' => ['https://example.test/seite/'],
'other probe' => ['https://example.test/lit-mcp-check-78/'],
'slug as suffix only' => ['https://example.test/x-lit-mcp-check-77/'],
'traversal' => ['https://example.test/../lit-mcp-check-77/'],
];
}
/** @dataProvider foreignAddresses */
public function testTheDefaultCachePathIsNullForAnythingButTheProbePage(string $address): void
{
$this->permalink($address);
self::assertNull(FastestCacheProbe::cacheDirFor(77));
}
public function testTheDefaultCachePathIsNullForAMissingPost(): void
{
Functions\when('get_post')->justReturn(null);
self::assertNull(FastestCacheProbe::cacheDirFor(77));
}
}
@@ -2,27 +2,27 @@
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\SuperCache;
namespace LIT\MCP\Tests\Unit\Integrations\FastestCache;
use Brain\Monkey\Functions;
use LIT\MCP\Integrations\SuperCache\SuperCacheFeature;
use LIT\MCP\Integrations\SuperCache\SuperCachePurger;
use LIT\MCP\Integrations\FastestCache\FastestCacheFeature;
use LIT\MCP\Integrations\FastestCache\FastestCachePurger;
use LIT\MCP\Tests\Unit\TestCase;
final class SuperCachePurgerTest extends TestCase
final class FastestCachePurgerTest extends TestCase
{
public function testALockedWholePurgeDoesNothingAndSaysSo(): void
{
$previous = ini_set('error_log', '/dev/null');
Functions\expect('get_transient')->never();
$purger = new SuperCachePurger(
static fn (SuperCacheFeature $feature): bool => false,
$purger = new FastestCachePurger(
static fn (FastestCacheFeature $feature): bool => false,
static fn (): bool => true,
static fn (int $id) => self::fail('no post purge'),
static fn (int $blog) => self::fail('no whole purge')
static fn () => self::fail('no whole purge')
);
self::assertSame(SuperCachePurger::LOCKED, $purger->everything());
self::assertSame(FastestCachePurger::LOCKED, $purger->everything());
self::assertFalse($purger->post(3));
ini_set('error_log', (string) $previous);
}
@@ -33,7 +33,7 @@ final class SuperCachePurgerTest extends TestCase
Functions\when('get_transient')->alias(static function () use (&$stamp) {
return $stamp;
});
$purger = new SuperCachePurger(static fn (): bool => true);
$purger = new FastestCachePurger(static fn (): bool => true);
self::assertFalse($purger->throttled());
$stamp = (string) (time() - 5);
@@ -56,9 +56,9 @@ final class SuperCachePurgerTest extends TestCase
return true;
});
$purger = new SuperCachePurger(static fn (): bool => true);
$purger = new FastestCachePurger(static fn (): bool => true);
self::assertSame(SuperCachePurger::THROTTLED, $purger->everything());
self::assertSame(FastestCachePurger::THROTTLED, $purger->everything());
self::assertSame(1, $reads);
self::assertCount(1, $scheduledAt);
@@ -1,121 +0,0 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\SuperCache;
use Brain\Monkey\Functions;
use LIT\MCP\Compatibility\Call;
use LIT\MCP\Compatibility\CheckStep;
use LIT\MCP\Compatibility\Compatibility;
use LIT\MCP\Compatibility\Tier;
use LIT\MCP\Integrations\Divi\Release\DiviProfile;
use LIT\MCP\Integrations\Pods\PodsCompatibility;
use LIT\MCP\Integrations\SuperCache\SuperCacheCheckSteps;
use LIT\MCP\Integrations\SuperCache\SuperCacheCompatibility;
use LIT\MCP\Integrations\SuperCache\SuperCacheFeature;
use LIT\MCP\Tests\Unit\TestCase;
final class SuperCacheCompatibilityTest extends TestCase
{
public function testDescribesSuperCacheAsALockOnFailurePlugin(): void
{
$profile = SuperCacheCompatibility::profile();
self::assertSame('wpsc', $profile->id);
self::assertSame('WP Super Cache', $profile->label);
self::assertSame(Tier::LockOnFailure, $profile->tier);
self::assertSame(['3.1.5'], $profile->testedVersions);
self::assertNull($profile->minVersion);
self::assertSame(SuperCacheFeature::cases(), $profile->capabilities);
}
/** Argument counts are those the listener passes; wp_cache_clear_cache($blog_id = 0) gets the blog. */
public function testTheManifestHoldsTheTwoPurgeFunctionsWithTheirArgumentCounts(): void
{
$manifest = SuperCacheCompatibility::profile()->manifests[0];
$calls = [];
foreach ($manifest->calls as $call) {
$calls[$call->symbol] = [$call->args, $call->features()];
}
self::assertSame([
'wp_cache_post_edit' => [1, [SuperCacheFeature::PurgePost]],
'wp_cache_clear_cache' => [1, [SuperCacheFeature::PurgeAll]],
], $calls);
self::assertContainsOnlyInstancesOf(Call::class, $manifest->calls);
}
/** Both purge functions read $cache_path; wp_cache_post_edit also $blog_cache_dir. */
public function testTheGlobalsTheFunctionsReadAreRequired(): void
{
$manifest = SuperCacheCompatibility::profile()->manifests[0];
self::assertSame(['WPCACHEHOME'], array_map(static fn ($c) => $c->name, $manifest->constants));
$globals = [];
foreach ($manifest->globals as $global) {
$globals[$global->name] = $global->features();
}
self::assertSame(
['cache_path' => SuperCacheFeature::cases(), 'blog_cache_dir' => [SuperCacheFeature::PurgePost]],
$globals
);
}
/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
public function testActiveAndVersionFollowTheCacheHomeConstantAndThePluginHeader(): void
{
$profile = SuperCacheCompatibility::profile();
self::assertFalse(($profile->active)());
self::assertNull(($profile->version)());
define('WPCACHEHOME', '/plugins/wp-super-cache/');
Functions\expect('get_file_data')
->once()
->with('/plugins/wp-super-cache/wp-cache.php', ['Version' => 'Version'])
->andReturn(['Version' => '3.1.5']);
self::assertTrue(($profile->active)());
self::assertSame('3.1.5', ($profile->version)());
}
/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
public function testAnUnreadableHeaderIsAnUnknownVersion(): void
{
define('WPCACHEHOME', '/plugins/wp-super-cache/');
Functions\when('get_file_data')->justReturn(['Version' => '']);
self::assertNull((SuperCacheCompatibility::profile()->version)());
}
public function testTheCheckHasTheProbeAsExtraStep(): void
{
$profile = SuperCacheCompatibility::profile();
self::assertNotNull($profile->extraSteps);
$steps = ($profile->extraSteps)(false);
self::assertSame([SuperCacheCheckSteps::PROBE], array_map(static fn ($step): string => $step->key, $steps));
self::assertTrue($steps[0]->skipped);
self::assertNotNull($profile->stepLabel);
self::assertSame(
'Leeren-Test an einer Wegwerf-Seite im Cache',
($profile->stepLabel)(new CheckStep(SuperCacheCheckSteps::PROBE, []))
);
}
public function testAllProfilesCoexistWithoutIdCollision(): void
{
$compatibility = new Compatibility(
[DiviProfile::profile(), PodsCompatibility::profile(), SuperCacheCompatibility::profile()]
);
self::assertSame(['divi', 'pods', 'wpsc'], array_map(static fn ($p) => $p->id, $compatibility->profiles()));
self::assertSame(SuperCacheFeature::PurgeAll, $compatibility->capability('wpsc-purge-all'));
}
}
@@ -1,75 +0,0 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\SuperCache;
use Brain\Monkey\Functions;
use LIT\MCP\Admin\CompatibilityTab;
use LIT\MCP\Compatibility\ProvidesCompatibility;
use LIT\MCP\Integrations\Bootable;
use LIT\MCP\Integrations\Integration;
use LIT\MCP\Integrations\SuperCache\SuperCacheCompatibility;
use LIT\MCP\Integrations\SuperCache\SuperCacheIntegration;
use LIT\MCP\Integrations\SuperCache\SuperCachePurger;
use LIT\MCP\Tests\Support\FakeChangeLog;
use LIT\MCP\Tests\Unit\TestCase;
final class SuperCacheIntegrationTest extends TestCase
{
private static function purger(): SuperCachePurger
{
return new SuperCachePurger(static fn (): bool => true);
}
public function testIsAnIntegrationThatProvidesItsProfileAndTheCacheClearTool(): void
{
$integration = new SuperCacheIntegration(null, self::purger(), FakeChangeLog::forbidden());
self::assertInstanceOf(Integration::class, $integration);
self::assertInstanceOf(ProvidesCompatibility::class, $integration);
self::assertSame('wpsc', $integration->id());
self::assertSame('WP Super Cache', $integration->label());
self::assertEquals(['lit/cache-clear'], array_map(static fn ($a): string => $a->name(), $integration->abilities()));
self::assertEquals([SuperCacheCompatibility::profile()], $integration->compatibilities());
}
public function testIsAvailableOnlyWithSuperCache(): void
{
self::assertSame(defined('WPCACHEHOME'), (new SuperCacheIntegration(null, self::purger(), FakeChangeLog::forbidden()))->isAvailable());
}
public function testBootHooksTheOrphanCleanupIntoTheProfileLoad(): void
{
$integration = new SuperCacheIntegration(null, self::purger(), FakeChangeLog::forbidden());
$integration->boot();
self::assertInstanceOf(Bootable::class, $integration);
self::assertNotFalse(has_action(CompatibilityTab::LOAD_ACTION, [$integration, 'removeOrphans']));
}
public function testNoCleanupForAnotherProfileWithoutWpscOrWithoutTheCapability(): void
{
Functions\expect('get_posts')->never();
Functions\when('current_user_can')->justReturn(true);
(new SuperCacheIntegration(static fn (): bool => true, self::purger(), FakeChangeLog::forbidden()))->removeOrphans('pods');
(new SuperCacheIntegration(static fn (): bool => false, self::purger(), FakeChangeLog::forbidden()))->removeOrphans('wpsc');
Functions\when('current_user_can')->justReturn(false);
(new SuperCacheIntegration(static fn (): bool => true, self::purger(), FakeChangeLog::forbidden()))->removeOrphans('wpsc');
$this->addToAssertionCount(1);
}
public function testCleansUpPagesForTheAdministratorWithWpsc(): void
{
Functions\when('current_user_can')->justReturn(true);
Functions\expect('get_posts')->once()->andReturnUsing(static function (array $args): array {
self::assertSame('page', $args['post_type']);
self::assertSame(['draft', 'private'], $args['post_status']);
return [];
});
(new SuperCacheIntegration(static fn (): bool => true, self::purger(), FakeChangeLog::forbidden()))->removeOrphans('wpsc');
}
}
@@ -1,525 +0,0 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\SuperCache;
use Brain\Monkey\Functions;
use Closure;
use LIT\MCP\Compatibility\ThrowawayPost;
use LIT\MCP\Integrations\SuperCache\SuperCacheProbe;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
use RuntimeException;
use WP_Post;
final class SuperCacheProbeTest extends TestCase
{
private string $cacheRoot = '';
/** @var list<int> */
private array $deleted = [];
/** @var list<string> "<id>:<status>" in call order. */
private array $statuses = [];
/** @var array<string, mixed>|null The wp_insert_post arguments. */
private ?array $inserted = null;
/** @var array<string, mixed> Globals the test changes (only those that existed). */
private array $savedGlobals = [];
private const GLOBALS_CHANGED = ['cache_enabled', 'cache_path', 'wpdb', 'wp_cache_clear_on_post_edit'];
/** The status the fake status switch refuses to set (returns false); null = none. */
private ?string $refusedStatus = null;
/** The status the fake status switch throws for; null = none. */
private ?string $throwingStatus = null;
protected function setUp(): void
{
parent::setUp();
$this->cacheRoot = sys_get_temp_dir() . '/lit-mcp-wpsc-' . bin2hex(random_bytes(4)) . '/';
mkdir($this->cacheRoot . 'supercache/example.test/', 0777, true);
$this->deleted = [];
$this->statuses = [];
$this->inserted = null;
$this->refusedStatus = null;
$this->throwingStatus = null;
$this->savedGlobals = [];
foreach (self::GLOBALS_CHANGED as $name) {
if (array_key_exists($name, $GLOBALS)) {
$this->savedGlobals[$name] = $GLOBALS[$name];
}
}
Functions\when('wp_insert_post')->alias(function (array $post): int {
$this->inserted = $post;
return 77;
});
Functions\when('wp_delete_post')->alias(function (int $id, bool $force): WP_Post {
self::assertTrue($force, 'never to the trash');
$this->deleted[] = $id;
return new WP_Post(['ID' => $id]);
});
Functions\when('wp_mkdir_p')->alias(static fn (string $dir): bool => is_dir($dir) || @mkdir($dir, 0777, true));
}
protected function tearDown(): void
{
foreach (self::GLOBALS_CHANGED as $name) {
if (array_key_exists($name, $this->savedGlobals)) {
$GLOBALS[$name] = $this->savedGlobals[$name];
} else {
unset($GLOBALS[$name]);
}
}
self::removeTree($this->cacheRoot);
parent::tearDown();
}
private static function removeTree(string $path): void
{
foreach (glob(rtrim($path, '/') . '/{,.}*', GLOB_BRACE) ?: [] as $entry) {
if (in_array(basename($entry), ['.', '..'], true)) {
continue;
}
is_dir($entry) ? self::removeTree($entry) : unlink($entry);
}
@rmdir($path);
}
private function pageDir(): string
{
return $this->cacheRoot . 'supercache/example.test/lit-mcp-check-77/';
}
/** @param (Closure(int): void)|null $purge */
private function probe(?Closure $purge = null, ?Closure $cacheDir = null, bool $enabled = true): SuperCacheProbe
{
return new SuperCacheProbe(
static fn (): bool => $enabled,
$cacheDir ?? fn (int $id): string => $this->pageDir(),
$purge ?? fn (int $id) => unlink($this->pageDir() . 'index.html'),
function (int $id, string $status): bool {
$this->statuses[] = $id . ':' . $status;
if ($status === $this->throwingStatus) {
throw new RuntimeException('Status kaputt');
}
return $status !== $this->refusedStatus;
}
);
}
/** Purge fake that does what WPSC does: the page's cache file disappears. */
private function purgingProbe(): SuperCacheProbe
{
return $this->probe(function (int $id): void {
self::assertFileExists($this->pageDir() . 'index.html', 'the file is there before the purge');
unlink($this->pageDir() . 'index.html');
});
}
/** Captures what Log writes to the PHP error log while $run runs. */
private function logged(Closure $run): string
{
$log = (string) tempnam(sys_get_temp_dir(), 'lit-mcp-log');
$previous = ini_set('error_log', $log);
try {
$run();
return (string) file_get_contents($log);
} finally {
ini_set('error_log', (string) $previous);
unlink($log);
}
}
public function testGreenWhenThePurgeRemovesTheCacheFileAndNothingIsLeftBehind(): void
{
self::assertSame([], $this->purgingProbe()->run());
self::assertDirectoryDoesNotExist($this->pageDir());
self::assertSame([77], $this->deleted);
}
public function testCreatesAMarkedDraftPageAndOnlyPassesThroughPrivateForThePurge(): void
{
$this->purgingProbe()->run();
self::assertSame('page', $this->inserted['post_type']);
self::assertSame('draft', $this->inserted['post_status']);
self::assertStringStartsWith(ThrowawayPost::TITLE, $this->inserted['post_title']);
self::assertSame([ThrowawayPost::MARKER => '1'], $this->inserted['meta_input']);
self::assertSame(['77:private', '77:draft'], $this->statuses);
}
public function testACacheFileThatSurvivesThePurgeIsAFixedTextAndIsRemovedAnyway(): void
{
$problems = [];
$logged = $this->logged(function () use (&$problems): void {
$problems = $this->probe(static fn (int $id) => null)->run();
});
self::assertSame(['Die Testdatei im Cache blieb nach dem Leeren durch WP Super Cache bestehen.'], $problems);
self::assertStringContainsString('lit-mcp-check-77', $logged);
self::assertDirectoryDoesNotExist($this->pageDir());
self::assertSame([77], $this->deleted);
}
public function testARenamedRebuildFileCountsAsPurgedAndIsCleanedUp(): void
{
$problems = $this->probe(function (int $id): void {
rename($this->pageDir() . 'index.html', $this->pageDir() . 'index.html.needs-rebuild');
})->run();
self::assertSame([], $problems);
self::assertDirectoryDoesNotExist($this->pageDir());
}
public function testADirectoryThePurgeAlreadyRemovedIsNoProblem(): void
{
$problems = $this->probe(function (int $id): void {
unlink($this->pageDir() . 'index.html');
rmdir($this->pageDir());
})->run();
self::assertSame([], $problems);
}
public function testAnExceptionOfThePurgeIsAFixedTextALogLineAndEverythingIsCleanedUp(): void
{
$problems = [];
$logged = $this->logged(function () use (&$problems): void {
$problems = $this->probe(static function (int $id): never {
throw new RuntimeException('WPSC kaputt');
})->run();
});
self::assertSame(['Super-Cache-Probe abgebrochen (Details im PHP-Error-Log).'], $problems);
self::assertStringContainsString('WPSC kaputt', $logged);
self::assertDirectoryDoesNotExist($this->pageDir());
self::assertSame([77], $this->deleted);
self::assertSame(['77:private', '77:draft'], $this->statuses);
}
public function testNothingToProbeWhileTheCacheIsSwitchedOff(): void
{
self::assertNull($this->probe(null, null, false)->run());
self::assertNull($this->inserted);
}
public function testNothingToProbeWithoutACachePathForThePage(): void
{
self::assertNull($this->probe(null, static fn (int $id): ?string => null)->run());
self::assertSame([77], $this->deleted);
}
public function testACacheDirectoryThatCannotBeCreatedIsAFixedText(): void
{
$blocker = $this->cacheRoot . 'blocker';
file_put_contents($blocker, 'x');
$problems = [];
$logged = $this->logged(function () use (&$problems, $blocker): void {
$problems = $this->probe(null, static fn (int $id): string => $blocker . '/seite/')->run();
});
self::assertSame(['Die Testdatei konnte nicht im Cache-Verzeichnis angelegt werden (Details im PHP-Error-Log).'], $problems);
self::assertStringContainsString('seite', $logged);
self::assertSame([77], $this->deleted);
}
public function testTheDefaultPurgeIsWpCachePostEdit(): void
{
Functions\expect('wp_cache_post_edit')->once()->with(77)->andReturnUsing(function (int $id): int {
unlink($this->pageDir() . 'index.html');
return $id;
});
$probe = new SuperCacheProbe(
static fn (): bool => true,
fn (int $id): string => $this->pageDir(),
null,
static fn (): bool => true
);
self::assertSame([], $probe->run());
}
public function testTheDefaultStatusSwitchWritesStatusAndSlugStraightToTheDatabaseWithoutHooks(): void
{
$wpdb = Mockery::mock();
$wpdb->posts = 'wp_posts';
$wpdb->expects('update')->with('wp_posts', ['post_status' => 'private', 'post_name' => 'lit-mcp-check-77'], ['ID' => 77])->andReturn(1);
$wpdb->expects('update')->with('wp_posts', ['post_status' => 'draft', 'post_name' => 'lit-mcp-check-77'], ['ID' => 77])->andReturn(1);
$GLOBALS['wpdb'] = $wpdb;
Functions\expect('clean_post_cache')->never();
$dropped = [];
Functions\when('wp_cache_delete')->alias(static function (int $id, string $group) use (&$dropped): bool {
$dropped[] = $group . ':' . $id;
return true;
});
$probe = new SuperCacheProbe(
static fn (): bool => true,
fn (int $id): string => $this->pageDir(),
fn (int $id) => unlink($this->pageDir() . 'index.html')
);
self::assertSame([], $probe->run());
self::assertSame(['posts:77', 'posts:77'], $dropped, 'the object cache must not keep the old status');
}
public function testTheDefaultEnabledCheckReadsTheWpscGlobal(): void
{
$GLOBALS['cache_enabled'] = false;
self::assertNull((new SuperCacheProbe())->run());
$GLOBALS['cache_enabled'] = null;
self::assertNull((new SuperCacheProbe())->run());
}
private function wpscDirs(string $home, string $current): void
{
Functions\when('get_supercache_dir')->justReturn($this->cacheRoot . 'supercache/' . $home . '/');
Functions\when('get_current_url_supercache_dir')->justReturn($this->cacheRoot . 'supercache/' . $current . '/');
}
public function testTheDefaultCachePathIsTheProbePageDirectoryBelowTheHomeHostDirectory(): void
{
$this->wpscDirs('example.test', 'example.test/lit-mcp-check-77');
self::assertSame($this->pageDir(), SuperCacheProbe::cacheDirFor(77));
}
/** WPSC derives the host from the request's Host header; the probe must not follow it somewhere else. */
public function testTheDefaultCachePathIsNullWhenWpscWouldUseAnotherHostThanTheHomeUrl(): void
{
$this->wpscDirs('example.test', 'www.example.test/lit-mcp-check-77');
self::assertNull(SuperCacheProbe::cacheDirFor(77));
}
public function testTheDefaultCachePathIsNullForAnythingButTheProbePageOfThisSite(): void
{
// A plain "?page_id=" address or an empty slug: WPSC resolves it to the home page's directory.
$this->wpscDirs('example.test', 'example.test');
self::assertNull(SuperCacheProbe::cacheDirFor(77));
$this->wpscDirs('example.test', 'example.test/seite');
self::assertNull(SuperCacheProbe::cacheDirFor(77));
$this->wpscDirs('example.test', 'example.test/lit-mcp-check-77/../../x');
self::assertNull(SuperCacheProbe::cacheDirFor(77));
$this->wpscDirs('example.test', 'example.test/lit-mcp-check-78');
self::assertNull(SuperCacheProbe::cacheDirFor(77));
Functions\when('get_current_url_supercache_dir')->justReturn('/var/www/html/lit-mcp-check-77/');
self::assertNull(SuperCacheProbe::cacheDirFor(77));
}
public function testTheDefaultCachePathIsNullWhenWpscKnowsNoDirectory(): void
{
Functions\when('get_supercache_dir')->justReturn(false);
Functions\when('get_current_url_supercache_dir')->justReturn(false);
self::assertNull(SuperCacheProbe::cacheDirFor(77));
}
public function testOnlyTheKnownCacheFilesOfTheProbePageAreRemoved(): void
{
$problems = $this->probe(function (int $id): void {
// What WPSC leaves after a rebuild-style purge, plus a file that is none of its names.
rename($this->pageDir() . 'index.html', $this->pageDir() . 'index.html.needs-rebuild');
touch($this->pageDir() . 'index-https.html');
touch($this->pageDir() . 'index-https.html.needs-rebuild');
touch($this->pageDir() . 'fremd.txt');
})->run();
self::assertSame([], $problems);
self::assertSame(['fremd.txt'], array_values(array_diff(scandir($this->pageDir()), ['.', '..'])));
}
public function testTheCacheDirectoryOfThePageIsRemovedWhenEmpty(): void
{
$this->purgingProbe()->run();
self::assertDirectoryDoesNotExist($this->pageDir());
self::assertDirectoryExists($this->cacheRoot . 'supercache/example.test/');
}
// --- P1: the purge must not empty the live cache ------------------------------------------------------------------
private function defaultPurgeProbe(): SuperCacheProbe
{
return new SuperCacheProbe(
static fn (): bool => true,
fn (int $id): string => $this->pageDir(),
null,
static fn (): bool => true
);
}
public function testTheDefaultPurgeRunsWithClearOnEditOffAndTheRelatedPagesFilterOffAndRestoresBoth(): void
{
$GLOBALS['wp_cache_clear_on_post_edit'] = 1;
$events = [];
$added = null;
Functions\expect('add_filter')->once()->with('wpsc_delete_related_pages_on_edit', Mockery::type(Closure::class), PHP_INT_MAX)
->andReturnUsing(static function (string $hook, Closure $callback) use (&$events, &$added): bool {
$events[] = 'add_filter returning ' . var_export($callback(true), true);
$added = $callback;
return true;
});
Functions\expect('remove_filter')->once()->with('wpsc_delete_related_pages_on_edit', Mockery::type(Closure::class), PHP_INT_MAX)
->andReturnUsing(static function (string $hook, Closure $callback) use (&$events, &$added): bool {
$events[] = $callback === $added ? 'remove_filter of the same callback' : 'remove_filter of ANOTHER callback';
return true;
});
Functions\expect('wp_cache_post_edit')->once()->with(77)->andReturnUsing(function (int $id) use (&$events): int {
$events[] = 'purge with clear_on_post_edit=' . var_export($GLOBALS['wp_cache_clear_on_post_edit'], true);
unlink($this->pageDir() . 'index.html');
return $id;
});
self::assertSame([], $this->defaultPurgeProbe()->run());
self::assertSame(
['add_filter returning false', 'purge with clear_on_post_edit=0', 'remove_filter of the same callback'],
$events
);
self::assertSame(1, $GLOBALS['wp_cache_clear_on_post_edit']);
}
public function testTheDefaultPurgeLeavesAnUnsetClearOnEditGlobalUnset(): void
{
unset($GLOBALS['wp_cache_clear_on_post_edit']);
Functions\when('add_filter')->justReturn(true);
Functions\when('remove_filter')->justReturn(true);
Functions\expect('wp_cache_post_edit')->once()->andReturnUsing(function (int $id): int {
self::assertSame(0, $GLOBALS['wp_cache_clear_on_post_edit']);
unlink($this->pageDir() . 'index.html');
return $id;
});
self::assertSame([], $this->defaultPurgeProbe()->run());
self::assertArrayNotHasKey('wp_cache_clear_on_post_edit', $GLOBALS);
}
public function testTheDefaultPurgeRestoresGlobalAndFilterWhenWpscThrows(): void
{
$GLOBALS['wp_cache_clear_on_post_edit'] = '1';
Functions\expect('add_filter')->once()->andReturn(true);
Functions\expect('remove_filter')->once()->with('wpsc_delete_related_pages_on_edit', Mockery::type(Closure::class), PHP_INT_MAX)->andReturn(true);
Functions\expect('wp_cache_post_edit')->once()->andThrow(new RuntimeException('WPSC kaputt'));
$problems = [];
$this->logged(function () use (&$problems): void {
$problems = $this->defaultPurgeProbe()->run();
});
self::assertSame(['Super-Cache-Probe abgebrochen (Details im PHP-Error-Log).'], $problems);
self::assertSame('1', $GLOBALS['wp_cache_clear_on_post_edit']);
}
// --- P2: a status the database refused is no purge result ---------------------------------------------------------
public function testARefusedPrivateStatusIsItsOwnFixedProblemAndTheProbeNeverPurges(): void
{
$this->refusedStatus = 'private';
$purged = false;
$problems = [];
$logged = $this->logged(function () use (&$problems, &$purged): void {
$problems = $this->probe(static function (int $id) use (&$purged): void {
$purged = true;
})->run();
});
self::assertSame(['Der Status der Testseite konnte nicht gesetzt werden (Details im PHP-Error-Log).'], $problems);
self::assertStringContainsString('77', $logged);
self::assertStringContainsString('private', $logged);
self::assertFalse($purged);
self::assertSame([77], $this->deleted);
self::assertSame(['77:private', '77:draft'], $this->statuses);
}
/** @return array<string, array{0: int|false}> */
public static function refusedUpdates(): array
{
return ['database error' => [false], 'no row changed' => [0]];
}
/** @dataProvider refusedUpdates */
public function testTheDefaultStatusSwitchTreatsAFailedOrEmptyUpdateAsRefused(int|false $result): void
{
$wpdb = Mockery::mock();
$wpdb->posts = 'wp_posts';
$wpdb->shouldReceive('update')->andReturn($result);
$GLOBALS['wpdb'] = $wpdb;
Functions\when('wp_cache_delete')->justReturn(true);
Functions\expect('wp_cache_post_edit')->never();
$problems = [];
$this->logged(function () use (&$problems): void {
$problems = (new SuperCacheProbe(
static fn (): bool => true,
fn (int $id): string => $this->pageDir(),
fn (int $id) => null
))->run();
});
self::assertSame(['Der Status der Testseite konnte nicht gesetzt werden (Details im PHP-Error-Log).'], $problems);
self::assertSame([77], $this->deleted);
}
public function testAFailingResetToDraftNeitherHidesTheResultNorEndsTheCheck(): void
{
$this->throwingStatus = 'draft';
$problems = [];
$logged = $this->logged(function () use (&$problems): void {
$problems = $this->purgingProbe()->run();
});
self::assertSame([], $problems);
self::assertStringContainsString('Status kaputt', $logged);
self::assertSame([77], $this->deleted);
self::assertDirectoryDoesNotExist($this->pageDir());
}
public function testAFailingResetToDraftDoesNotReplaceAnExceptionOfThePurge(): void
{
$this->throwingStatus = 'draft';
$problems = [];
$logged = $this->logged(function () use (&$problems): void {
$problems = $this->probe(static function (int $id): never {
throw new RuntimeException('WPSC kaputt');
})->run();
});
self::assertSame(['Super-Cache-Probe abgebrochen (Details im PHP-Error-Log).'], $problems);
self::assertStringContainsString('WPSC kaputt', $logged);
self::assertSame([77], $this->deleted);
}
public function testARefusedResetToDraftIsLoggedOnly(): void
{
$this->refusedStatus = 'draft';
$problems = [];
$logged = $this->logged(function () use (&$problems): void {
$problems = $this->purgingProbe()->run();
});
self::assertSame([], $problems);
self::assertStringContainsString('draft', $logged);
}
}