redirect = null; Functions\stubEscapeFunctions(); Functions\stubTranslationFunctions(); Functions\when('current_user_can')->justReturn(true); Functions\when('check_admin_referer')->justReturn(1); Functions\when('get_current_user_id')->justReturn(1); Functions\when('wp_safe_redirect')->alias(function (string $url): bool { $this->redirect = $url; return true; }); Functions\when('admin_url')->alias(static fn (string $path = ''): string => 'https://k.de/wp-admin/' . $path); Functions\when('add_query_arg')->alias( static fn (array $args, string $url): string => $url . '?' . http_build_query($args) ); } private static function report(bool $green = true, string $version = '5.3.0', string $key = 'key'): CheckReport { $steps = array_map( static fn (string $step): CheckStep => new CheckStep($step, $green ? [] : ['rot']), [CheckStep::MANIFEST, CheckStep::MIRRORS, CheckStep::CATALOG, CheckStep::FORMAT, CheckStep::WRITE] ); return new CheckReport('r1', '2026-10-01T08:00:00+00:00', 1, $version, Plugin::VERSION, $key, $steps); } private function tab(?CheckReport $report, ReleaseStore $releases, ?FakeChangeLog $log = null): DiviTab { $reports = Mockery::mock(CheckReportStore::class); $reports->allows('latest')->andReturn($report); return new DiviTab( $reports, $releases, $log ?? FakeChangeLog::forbidden(), new AdminPostAction(static function (): void { }), static fn (): string => '5.3.0', static fn (string $version): string => 'key' ); } public function testReleaseNeedsAFreshGreenReport(): void { $reports = [null, self::report(false), self::report(true, '5.2.9'), self::report(true, '5.3.0', 'old')]; foreach ($reports as $report) { $this->redirect = null; $releases = Mockery::mock(ReleaseStore::class); $releases->expects('add')->never(); $this->tab($report, $releases)->release(); self::assertStringContainsString('lit_mcp_notice=divi_release_refused', (string) $this->redirect); } } public function testReleaseStoresTheEntryAndLogsIt(): void { $releases = Mockery::mock(ReleaseStore::class); $releases->expects('add')->once()->with('5.3.0', Plugin::VERSION, 1, 'r1'); $log = new FakeChangeLog(); $this->tab(self::report(), $releases, $log)->release(); self::assertStringContainsString('lit_mcp_notice=divi_released', (string) $this->redirect); self::assertSame('lit/divi-release', $log->records[0]['ability']); self::assertStringContainsString('Divi 5.3.0', $log->records[0]['summary']); } public function testWithdrawRemovesTheEntryOfTheInstalledVersion(): void { $releases = Mockery::mock(ReleaseStore::class); $releases->expects('withdraw')->once()->with('5.3.0', Plugin::VERSION)->andReturn(true); $log = new FakeChangeLog(); $this->tab(null, $releases, $log)->withdraw(); self::assertStringContainsString('lit_mcp_notice=divi_withdrawn', (string) $this->redirect); self::assertSame('lit/divi-release', $log->records[0]['ability']); } public function testActionsNeedNonceAndManageOptions(): void { Functions\when('current_user_can')->justReturn(false); Functions\expect('wp_die')->twice()->andReturnUsing(static function (): never { throw new \RuntimeException('died'); }); $releases = Mockery::mock(ReleaseStore::class); $releases->expects('add')->never(); $releases->expects('withdraw')->never(); $tab = $this->tab(self::report(), $releases); foreach (['release', 'withdraw'] as $method) { try { $tab->{$method}(); self::fail($method . ' without manage_options'); } catch (\RuntimeException $e) { self::assertSame('died', $e->getMessage()); } } } public function testReleaseChecksTheNonce(): void { Functions\when('check_admin_referer')->alias(static function (string $action): never { throw new \RuntimeException('bad nonce for ' . $action); }); $releases = Mockery::mock(ReleaseStore::class); $releases->expects('add')->never(); $this->expectExceptionMessage('bad nonce for lit_mcp_divi_release'); $this->tab(self::report(), $releases)->release(); } /** @return string The rendered tab. */ private function rendered(ReleaseStatus $status, ?CheckReport $report): string { ReleaseGate::useResolver(static fn (): ReleaseStatus => $status); Functions\when('get_posts')->justReturn([]); Functions\when('rest_url')->justReturn('https://k.de/wp-json/lit-mcp/v1/divi-check'); Functions\when('wp_create_nonce')->justReturn('n'); Functions\when('wp_nonce_field')->justReturn(''); Functions\when('submit_button')->alias(static function (string $text): void { echo ''; }); Functions\when('get_userdata')->justReturn((object) ['display_name' => 'Jan']); Functions\when('date_i18n')->justReturn('01.10.2026 10:00'); Functions\when('get_option')->justReturn('d.m.Y'); ob_start(); $this->tab($report, Mockery::mock(ReleaseStore::class))->render(); return (string) ob_get_clean(); } public function testUntestedWithAFreshGreenReportOffersTheRelease(): void { $html = $this->rendered(new ReleaseStatus(ReleaseState::Untested, '5.3.0'), self::report()); self::assertStringContainsString('ungetestet', $html); self::assertStringContainsString('Prüfung starten', $html); self::assertStringContainsString('Divi 5.3.0 freigeben', $html); self::assertStringContainsString('name="_wpnonce" value="n"', $html); } public function testUntestedWithAStaleReportAsksForANewCheck(): void { $html = $this->rendered(new ReleaseStatus(ReleaseState::Untested, '5.3.0'), self::report(true, '5.2.9')); self::assertStringNotContainsString('Divi 5.3.0 freigeben', $html); self::assertStringContainsString('Prüfung erneut ausführen', $html); } public function testReleasedFromCodeOffersNeitherReleaseNorWithdrawal(): void { $html = $this->rendered(ReleaseStatus::released('5.3.0'), self::report()); self::assertStringContainsString('freigegeben', $html); self::assertStringContainsString('im Plugin als getestet hinterlegt', $html); self::assertStringNotContainsString('freigeben', $html); self::assertStringNotContainsString('Freigabe zurückziehen', $html); } public function testADatabaseReleaseCanBeWithdrawn(): void { $status = new ReleaseStatus(ReleaseState::Released, '5.3.0', ReleaseStatus::SOURCE_DATABASE, [], [ 'released_at' => '2026-10-01T08:00:00+00:00', 'user_id' => 1, ]); self::assertStringContainsString('Freigabe zurückziehen', $this->rendered($status, self::report())); } public function testWithoutDiviShowsOnlyAShortNotice(): void { ReleaseGate::useResolver(static function (): ReleaseStatus { self::fail('resolver called although Divi is not active'); }); $reports = Mockery::mock(CheckReportStore::class); $reports->expects('latest')->never(); $tab = new DiviTab( $reports, Mockery::mock(ReleaseStore::class), FakeChangeLog::forbidden(), new AdminPostAction(static function (): void { }), static fn (): string => '', static fn (string $version): string => 'key' ); ob_start(); $tab->render(); $html = (string) ob_get_clean(); self::assertStringContainsString( 'Divi ist auf dieser Website nicht aktiv; die Divi-Freigabe ist ohne Bedeutung.', $html ); self::assertStringNotContainsString('Prüfung starten', $html); } public function testIncompatibleListsTheProblems(): void { $html = $this->rendered( new ReleaseStatus(ReleaseState::Incompatible, '5.3.0', null, ['Theme Builder: Nachbau von x() – y']), null ); self::assertStringContainsString('inkompatibel', $html); self::assertStringContainsString('Theme Builder: Nachbau von x() – y', $html); self::assertStringContainsString('Plugin-Update', $html); } }