*/ private array $purges = []; /** @var list */ private array $scheduled = []; private int|false $lastPurge = false; protected function setUp(): void { parent::setUp(); $this->purges = []; $this->scheduled = []; $this->lastPurge = false; Functions\when('is_multisite')->justReturn(false); Functions\when('get_transient')->alias(fn (): int|false => $this->lastPurge); Functions\when('set_transient')->justReturn(true); Functions\when('wp_next_scheduled')->justReturn(false); Functions\when('wp_schedule_single_event')->alias(function (int $at, string $hook): bool { $this->scheduled[] = $hook; return true; }); Functions\when('current_user_can')->justReturn(true); Functions\when('get_post')->alias( static fn (int $id): ?WP_Post => $id === 7 ? new WP_Post(['ID' => 7, 'post_type' => 'page', 'post_title' => 'Start', 'post_status' => 'publish']) : ($id === 8 ? new WP_Post(['ID' => 8, 'post_type' => 'page', 'post_title' => 'Idee', 'post_status' => 'draft']) : null) ); } protected function tearDown(): void { unset($GLOBALS['wp_fastest_cache_options']); parent::tearDown(); } private function ability(?FakeChangeLog $log = null, bool $cacheOn = true, bool $usable = true): CacheClearAbility { $purger = new FastestCachePurger( static fn (FastestCacheFeature $feature): bool => $usable, static fn (): bool => $cacheOn, function (int $id): void { $this->purges[] = 'post:' . $id; }, function (): void { $this->purges[] = 'all'; } ); return new CacheClearAbility($purger, $log ?? FakeChangeLog::forbidden()); } public function testDescribesItselfLikeAGuardedWriteTool(): void { $ability = $this->ability(); self::assertInstanceOf(Ability::class, $ability); self::assertSame('lit/cache-clear', $ability->name()); self::assertFalse($ability->isDirect()); self::assertSame(Annotations::write(true), $ability->annotations()); self::assertStringContainsString('confirm: true', $ability->description()); self::assertStringContainsString('WP Fastest Cache', $ability->description()); $schema = $ability->inputSchema(); self::assertSame(['scope'], $schema['required']); self::assertFalse($schema['additionalProperties']); self::assertSame(['all', 'post'], $schema['properties']['scope']['enum']); self::assertSame(['scope', 'post_id', 'confirm'], array_keys($schema['properties'])); self::assertArrayHasKey('outcome', $ability->outputSchema()['properties']); } public function testDeclaresTheCapabilityOfTheScope(): void { $ability = $this->ability(); self::assertInstanceOf(DeclaresRequirements::class, $ability); 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([])); } public function testAllNeedsManageOptions(): void { $asked = []; Functions\when('current_user_can')->alias(function (string $cap) use (&$asked): bool { $asked[] = $cap; return false; }); self::assertToolError($this->ability()->permission(['scope' => 'all']), 'forbidden'); self::assertSame(['manage_options'], $asked); Functions\when('current_user_can')->justReturn(true); self::assertTrue($this->ability()->permission(['scope' => 'all'])); } public function testPostNeedsEditPostForThatId(): void { $asked = []; Functions\when('current_user_can')->alias(function (string $cap, int $id) use (&$asked): bool { $asked[] = [$cap, $id]; return false; }); self::assertToolError($this->ability()->permission(['scope' => 'post', 'post_id' => 7]), 'forbidden'); self::assertSame([['edit_post', 7]], $asked); Functions\when('current_user_can')->justReturn(true); self::assertTrue($this->ability()->permission(['scope' => 'post', 'post_id' => 7])); } public function testAnUnknownPostIsNotFound(): void { self::assertToolError($this->ability()->permission(['scope' => 'post', 'post_id' => 99]), 'not_found'); self::assertToolError($this->ability()->execute(['scope' => 'post', 'post_id' => 99, 'confirm' => true]), 'not_found'); self::assertSame([], $this->purges); } /** @return array}> */ public static function invalidInputs(): array { return [ 'no scope' => [[]], 'unknown scope' => [['scope' => 'site']], 'post without id' => [['scope' => 'post']], 'post with zero' => [['scope' => 'post', 'post_id' => 0]], 'post with text' => [['scope' => 'post', 'post_id' => 'abc']], ]; } /** * @dataProvider invalidInputs * @param array $input */ public function testInvalidInputIsRefusedWithoutEffect(array $input): void { self::assertToolError($this->ability()->permission($input), 'validation_failed'); self::assertToolError($this->ability()->execute($input + ['confirm' => true]), 'validation_failed'); self::assertSame([], $this->purges); } public function testTheDryRunClearsNothingAndReportsTheState(): void { $GLOBALS['wp_fastest_cache_options'] = (object) ['wpFastestCacheStatus' => 'on']; $this->lastPurge = time() - 5; $payload = self::assertToolError($this->ability()->execute(['scope' => 'all']), 'confirm_required'); self::assertSame([], $this->purges); self::assertSame([], $this->scheduled); self::assertSame('all', $payload['scope']); self::assertTrue($payload['cache_enabled']); self::assertTrue($payload['throttled']); self::assertStringContainsString('Probelauf', $payload['message']); } public function testTheDryRunOfAPostNamesThePostAndIgnoresAConfirmThatIsNotTrue(): void { $payload = self::assertToolError( $this->ability(cacheOn: false)->execute(['scope' => 'post', 'post_id' => 7, 'confirm' => 'true']), 'confirm_required' ); self::assertSame([], $this->purges); self::assertSame(7, $payload['post_id']); self::assertFalse($payload['cache_enabled']); self::assertStringContainsString('Start', $payload['message']); self::assertStringContainsString('ausgeschaltet', $payload['message']); } public function testConfirmClearsThePageOfThePostAndLogsIt(): void { $log = new FakeChangeLog(4); $result = $this->ability($log)->execute(['scope' => 'post', 'post_id' => 7, 'confirm' => true]); self::assertSame(['post:7'], $this->purges); self::assertTrue($result['ok']); self::assertSame('purged', $result['outcome']); self::assertSame(4, $result['log_id']); $log->assertRecorded(['ability' => 'lit/cache-clear', 'object_type' => 'wpfc', 'object_id' => '7']); } /** WPFC finds the address of any post (a trashed one too), so no status is skipped. */ public function testADraftIsClearedToo(): void { $result = $this->ability(new FakeChangeLog(2))->execute(['scope' => 'post', 'post_id' => 8, 'confirm' => true]); self::assertSame(['post:8'], $this->purges); self::assertSame('purged', $result['outcome']); } public function testConfirmClearsTheWholeCacheAtOnce(): void { $log = new FakeChangeLog(1); $result = $this->ability($log)->execute(['scope' => 'all', 'confirm' => true]); self::assertSame(['all'], $this->purges); self::assertSame('purged', $result['outcome']); $log->assertRecorded(['ability' => 'lit/cache-clear', 'object_type' => 'wpfc', 'object_id' => 'all']); } public function testAWholePurgeInsideTheLockTimeIsThrottledWithATrailingPurge(): void { $this->lastPurge = time() - 5; $result = $this->ability(new FakeChangeLog(1))->execute(['scope' => 'all', 'confirm' => true]); self::assertSame([], $this->purges); self::assertSame([FastestCachePurger::TRAILING_HOOK], $this->scheduled); self::assertSame('throttled', $result['outcome']); self::assertStringContainsString('Nachlauf', $result['summary']); } /** @return array}> */ public static function scopes(): array { return ['all' => [['scope' => 'all']], 'post' => [['scope' => 'post', 'post_id' => 7]]]; } /** * @dataProvider scopes * @param array $input */ public function testWithTheCacheOffNothingHappensAndNothingIsLogged(array $input): void { $result = $this->ability(FakeChangeLog::forbidden(), cacheOn: false)->execute($input + ['confirm' => true]); self::assertSame([], $this->purges); self::assertSame('cache_off', $result['outcome']); self::assertNull($result['log_id']); } /** * @dataProvider confirmedScopes * @param array $input */ public function testALockedPurgeIsReportedAsAnErrorAndNeitherPurgesNorLogs(array $input): void { $previous = ini_set('error_log', '/dev/null'); $result = $this->ability(FakeChangeLog::forbidden(), usable: false)->execute($input); ini_set('error_log', (string) $previous); self::assertInstanceOf(WP_Error::class, $result); self::assertSame('lit_compat_incompatible', $result->get_error_code()); self::assertStringContainsString( 'WP Fastest Cache ist gesperrt (siehe Reiter Kompatibilität). Es wurde nichts geleert.', $result->get_error_message() ); self::assertSame([], $this->purges); } /** @return array}> */ public static function confirmedScopes(): array { return [ 'all' => [['scope' => 'all', 'confirm' => true]], 'post' => [['scope' => 'post', 'post_id' => 7, 'confirm' => true]], ]; } }