refactor: Divi-Reiter und divi-check in die Divi-Integration verschoben; CoreIndependenceTest prüft auch Cli, ChangeLog, Pods und die Wurzeldateien, erkennt Alias-Importe und kennt nur noch Plugin.php als Ausnahme
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d0d3488df8
commit
7f9a087b8b
@@ -0,0 +1,541 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Admin;
|
||||
|
||||
use Brain\Monkey\Actions;
|
||||
use Brain\Monkey\Functions;
|
||||
use LIT\MCP\Admin\AdminPostAction;
|
||||
use LIT\MCP\Integrations\Divi\Admin\DiviTab;
|
||||
use LIT\MCP\Integrations\Divi\Release\CheckReport;
|
||||
use LIT\MCP\Integrations\Divi\Release\CheckReportStore;
|
||||
use LIT\MCP\Integrations\Divi\Release\CheckStep;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\FeatureOverview;
|
||||
use LIT\MCP\Integrations\Divi\Release\FeatureStatus;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseState;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseStatus;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseStore;
|
||||
use LIT\MCP\Integrations\Divi\Release\WriteProbe;
|
||||
use LIT\MCP\Plugin;
|
||||
use LIT\MCP\Tests\Support\FakeChangeLog;
|
||||
use LIT\MCP\Tests\Support\FeatureFakeTool;
|
||||
use LIT\MCP\Tests\Support\MatrixFixtures;
|
||||
use LIT\MCP\Tests\Support\ReleaseMatrices;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use Mockery;
|
||||
use WP_Post;
|
||||
|
||||
final class DiviTabTest extends TestCase
|
||||
{
|
||||
private ?string $redirect = null;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->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)
|
||||
);
|
||||
Functions\when('sanitize_key')->alias(static fn (string $key): string => strtolower($key));
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
unset($_GET['tab']);
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
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,
|
||||
?FeatureOverview $overview = 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',
|
||||
$overview
|
||||
);
|
||||
}
|
||||
|
||||
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, ?DiviTab $tab = null): string
|
||||
{
|
||||
return $this->renderedTab($status, $tab ?? $this->tab($report, Mockery::mock(ReleaseStore::class)));
|
||||
}
|
||||
|
||||
private function renderedWith(ReleaseStatus $status, ?CheckReport $report, FeatureOverview $overview): string
|
||||
{
|
||||
return $this->renderedTab(
|
||||
$status,
|
||||
$this->tab($report, Mockery::mock(ReleaseStore::class), null, $overview)
|
||||
);
|
||||
}
|
||||
|
||||
private function renderedTab(ReleaseStatus $status, DiviTab $tab): string
|
||||
{
|
||||
ReleaseGate::useResolver(static fn (): ReleaseStatus => $status);
|
||||
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, string $type = 'primary'): void {
|
||||
echo '<button class="' . $type . '">' . $text . '</button>';
|
||||
});
|
||||
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();
|
||||
$tab->render();
|
||||
|
||||
return (string) ob_get_clean();
|
||||
}
|
||||
|
||||
public function testUntestedWithAFreshGreenReportOffersTheRelease(): void
|
||||
{
|
||||
$html = $this->rendered(ReleaseMatrices::uniform(ReleaseState::Untested), 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 testCheckButtonSaysWhatItDoesAndLeadsWhileNothingElseCanBeDone(): void
|
||||
{
|
||||
$stale = $this->rendered(ReleaseMatrices::uniform(ReleaseState::Untested), self::report(true, '5.2.9'));
|
||||
$fresh = $this->rendered(ReleaseMatrices::uniform(ReleaseState::Untested), self::report());
|
||||
$released = $this->rendered(ReleaseMatrices::released('5.3.0'), self::report());
|
||||
|
||||
foreach ([$stale, $fresh, $released] as $html) {
|
||||
self::assertStringContainsString('legt eine private Testseite an', $html);
|
||||
}
|
||||
self::assertStringContainsString('<button class="primary">Prüfung starten</button>', $stale);
|
||||
self::assertStringContainsString('<button class="secondary">Prüfung starten</button>', $fresh);
|
||||
self::assertStringContainsString('<button class="primary">Divi 5.3.0 freigeben</button>', $fresh);
|
||||
self::assertStringContainsString('<button class="secondary">Prüfung starten</button>', $released);
|
||||
}
|
||||
|
||||
public function testUntestedWithAStaleReportAsksForANewCheck(): void
|
||||
{
|
||||
$html = $this->rendered(ReleaseMatrices::uniform(ReleaseState::Untested), 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(ReleaseMatrices::released('5.3.0'), self::report());
|
||||
|
||||
self::assertStringContainsString('freigegeben', $html);
|
||||
self::assertStringContainsString('im Plugin als getestet hinterlegt', $html);
|
||||
self::assertStringNotContainsString('freigeben</button>', $html);
|
||||
self::assertStringNotContainsString('Freigabe zurückziehen', $html);
|
||||
}
|
||||
|
||||
public function testAnAutomaticReleaseSaysSoAndOffersNeitherReleaseNorWithdrawal(): void
|
||||
{
|
||||
$html = $this->rendered(ReleaseMatrices::released('5.99.0', ReleaseStatus::SOURCE_AUTO), self::report());
|
||||
|
||||
self::assertStringContainsString('Automatisch freigegeben', $html);
|
||||
self::assertStringNotContainsString('freigeben</button>', $html);
|
||||
self::assertStringNotContainsString('Freigabe zurückziehen', $html);
|
||||
}
|
||||
|
||||
public function testADatabaseReleaseCanBeWithdrawn(): void
|
||||
{
|
||||
$status = ReleaseMatrices::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(
|
||||
ReleaseMatrices::uniform(ReleaseState::Incompatible, '5.3.0', ['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);
|
||||
}
|
||||
|
||||
/** The tab removes the pages of a dead check in the settings page's load hook, never while rendering. */
|
||||
public function testRegistersTheCleanupOnTheSettingsPageLoadHook(): void
|
||||
{
|
||||
Actions\expectAdded('admin_post_lit_mcp_divi_release')->once();
|
||||
Actions\expectAdded('admin_post_lit_mcp_divi_withdraw')->once();
|
||||
Actions\expectAdded('load-settings_page_lit-mcp')->once()->with(Mockery::type('array'));
|
||||
|
||||
$this->tab(null, Mockery::mock(ReleaseStore::class))->registerActions();
|
||||
}
|
||||
|
||||
public function testTheLoadHookOnThisTabRemovesOrphansAndTheTabReportsThem(): void
|
||||
{
|
||||
$_GET['tab'] = 'divi';
|
||||
Functions\expect('get_posts')->once()->andReturn([
|
||||
new WP_Post(['ID' => 51, 'post_title' => WriteProbe::TITLE . ' 2026-10-18 08:00']),
|
||||
]);
|
||||
Functions\expect('wp_delete_post')->once()->with(51, true)->andReturn(new WP_Post(['ID' => 51]));
|
||||
$tab = $this->tab(self::report(), Mockery::mock(ReleaseStore::class));
|
||||
|
||||
$tab->removeOrphans();
|
||||
$html = $this->rendered(ReleaseMatrices::released('5.3.0'), self::report(), $tab);
|
||||
|
||||
self::assertStringContainsString('Liegengebliebene Prüfseiten gelöscht: 51', $html);
|
||||
}
|
||||
|
||||
public function testRenderingAloneDeletesNothing(): void
|
||||
{
|
||||
Functions\expect('get_posts')->never();
|
||||
Functions\expect('wp_delete_post')->never();
|
||||
|
||||
$html = $this->rendered(ReleaseMatrices::released('5.3.0'), self::report());
|
||||
|
||||
self::assertStringNotContainsString('Liegengebliebene Prüfseiten', $html);
|
||||
}
|
||||
|
||||
public function testTheLoadHookDeletesNothingOnOtherTabsWithoutTheRightOrWithoutDivi(): void
|
||||
{
|
||||
Functions\expect('get_posts')->never();
|
||||
$tab = $this->tab(null, Mockery::mock(ReleaseStore::class));
|
||||
|
||||
unset($_GET['tab']);
|
||||
$tab->removeOrphans();
|
||||
$_GET['tab'] = 'tools';
|
||||
$tab->removeOrphans();
|
||||
|
||||
$_GET['tab'] = 'divi';
|
||||
Functions\when('current_user_can')->justReturn(false);
|
||||
$tab->removeOrphans();
|
||||
|
||||
Functions\when('current_user_can')->justReturn(true);
|
||||
(new DiviTab(
|
||||
Mockery::mock(CheckReportStore::class),
|
||||
Mockery::mock(ReleaseStore::class),
|
||||
FakeChangeLog::forbidden(),
|
||||
null,
|
||||
static fn (): string => ''
|
||||
))->removeOrphans();
|
||||
}
|
||||
|
||||
private static function mixedStatus(): ReleaseStatus
|
||||
{
|
||||
$matrix = MatrixFixtures::released('auto', [
|
||||
new FeatureStatus(
|
||||
DiviFeature::PageWrite,
|
||||
ReleaseState::Incompatible,
|
||||
['Seiten: Signatur von et_pb_x() <geändert>'],
|
||||
null
|
||||
),
|
||||
new FeatureStatus(DiviFeature::Catalog, ReleaseState::Untested, [], null),
|
||||
]);
|
||||
|
||||
return ReleaseStatus::of('5.3.0', $matrix);
|
||||
}
|
||||
|
||||
public function testTheFeatureTableShowsStateSourceProblemsAndTools(): void
|
||||
{
|
||||
$overview = new FeatureOverview(static fn (): array => [
|
||||
new FeatureFakeTool('lit/divi-insert', true, [DiviFeature::PageRead, DiviFeature::PageWrite]),
|
||||
new FeatureFakeTool('lit/divi-presets-assign', false, [DiviFeature::PresetWrite]),
|
||||
]);
|
||||
|
||||
$html = $this->renderedWith(self::mixedStatus(), null, $overview);
|
||||
|
||||
self::assertStringContainsString('<th scope="col">Betroffene Werkzeuge</th>', $html);
|
||||
self::assertStringContainsString(
|
||||
'<td>Seiten schreiben</td><td><strong>inkompatibel</strong></td><td>–</td>',
|
||||
$html
|
||||
);
|
||||
self::assertStringContainsString('<td>Modulkatalog</td><td><strong>ungetestet</strong></td>', $html);
|
||||
self::assertStringContainsString(
|
||||
'<td>Seiten lesen</td><td><strong>freigegeben</strong></td><td>Automatik (auto)</td>',
|
||||
$html
|
||||
);
|
||||
self::assertStringContainsString('<li><code>lit-divi-insert</code></li>', $html);
|
||||
self::assertStringContainsString('<li><code>lit/divi-presets-assign</code></li>', $html);
|
||||
}
|
||||
|
||||
public function testFeatureProblemsAreEscaped(): void
|
||||
{
|
||||
$html = $this->rendered(self::mixedStatus(), null);
|
||||
|
||||
self::assertStringContainsString('Seiten: Signatur von et_pb_x() <geändert>', $html);
|
||||
self::assertStringNotContainsString('<geändert>', $html);
|
||||
}
|
||||
|
||||
public function testTheTableNamesEverySourceInWords(): void
|
||||
{
|
||||
$code = $this->rendered(ReleaseMatrices::released('5.3.0'), null);
|
||||
$database = $this->rendered(
|
||||
ReleaseStatus::of('5.3.0', MatrixFixtures::released('database'), [
|
||||
'released_at' => '2026-10-01T08:00:00+00:00', 'user_id' => 1,
|
||||
]),
|
||||
self::report()
|
||||
);
|
||||
|
||||
self::assertStringContainsString('Plugin (code)', $code);
|
||||
self::assertStringContainsString('Freigabe auf dieser Website (database)', $database);
|
||||
}
|
||||
|
||||
public function testTheTabExplainsTheAutomationAndTheFilter(): void
|
||||
{
|
||||
$html = $this->rendered(ReleaseMatrices::released('5.3.0'), null);
|
||||
|
||||
self::assertStringContainsString('gibt LIT MCP jede Fähigkeit einzeln frei', $html);
|
||||
self::assertStringContainsString('<code>lit_mcp_divi_auto_release</code>', $html);
|
||||
self::assertStringContainsString('als ungetestet', $html);
|
||||
self::assertStringContainsString('Aufrufe', $html);
|
||||
self::assertStringNotContainsString('Verhalten geprüft', $html);
|
||||
}
|
||||
|
||||
public function testTheTabNoLongerSaysWritingIsLockedAsAWhole(): void
|
||||
{
|
||||
$untested = $this->rendered(
|
||||
ReleaseStatus::of('5.3.0', MatrixFixtures::with(DiviFeature::Catalog, ReleaseState::Untested)),
|
||||
null
|
||||
);
|
||||
$incompatible = $this->rendered(self::mixedStatus(), null);
|
||||
|
||||
foreach ([$untested, $incompatible] as $html) {
|
||||
self::assertStringNotContainsString('Schreiben bleibt gesperrt', $html);
|
||||
self::assertStringNotContainsString('lesen nur', $html);
|
||||
self::assertStringContainsString('alle anderen laufen weiter', $html);
|
||||
}
|
||||
}
|
||||
|
||||
public function testADatabaseReleaseCanBeWithdrawnWhileOneFeatureIsIncompatible(): void
|
||||
{
|
||||
$matrix = MatrixFixtures::released('database', [
|
||||
new FeatureStatus(DiviFeature::PageWrite, ReleaseState::Incompatible, ['Seiten: x() fehlt'], null),
|
||||
]);
|
||||
$status = ReleaseStatus::of('5.3.0', $matrix, [
|
||||
'released_at' => '2026-10-01T08:00:00+00:00', 'user_id' => 1,
|
||||
]);
|
||||
|
||||
self::assertStringContainsString('Freigabe zurückziehen', $this->rendered($status, self::report()));
|
||||
}
|
||||
|
||||
public function testWithoutADatabaseReleaseNothingCanBeWithdrawnWhateverTheState(): void
|
||||
{
|
||||
self::assertStringNotContainsString('Freigabe zurückziehen', $this->rendered(self::mixedStatus(), null));
|
||||
}
|
||||
|
||||
/** @return string The admin notice of this request. */
|
||||
private function notice(ReleaseStatus $status, string $version = '5.3.0'): string
|
||||
{
|
||||
ReleaseGate::useResolver(static fn (): ReleaseStatus => $status);
|
||||
$tab = new DiviTab(
|
||||
Mockery::mock(CheckReportStore::class),
|
||||
Mockery::mock(ReleaseStore::class),
|
||||
FakeChangeLog::forbidden(),
|
||||
new AdminPostAction(static function (): void {
|
||||
}),
|
||||
static fn () => $version,
|
||||
static fn (string $v): string => 'key'
|
||||
);
|
||||
|
||||
ob_start();
|
||||
$tab->renderNotice();
|
||||
|
||||
return (string) ob_get_clean();
|
||||
}
|
||||
|
||||
public function testTheNoticeNamesTheUnusableFeaturesAndLinksToTheTab(): void
|
||||
{
|
||||
$html = $this->notice(self::mixedStatus());
|
||||
|
||||
self::assertStringContainsString('class="notice notice-warning"', $html);
|
||||
self::assertStringContainsString('Mit Divi 5.3.0 sind diese Fähigkeiten nicht nutzbar', $html);
|
||||
self::assertStringContainsString('Seiten schreiben, Modulkatalog', $html);
|
||||
self::assertStringContainsString('alle anderen laufen weiter', $html);
|
||||
self::assertStringContainsString('page=lit-mcp&tab=divi', $html);
|
||||
}
|
||||
|
||||
public function testNoNoticeWhileEveryFeatureIsUsable(): void
|
||||
{
|
||||
self::assertSame('', $this->notice(ReleaseMatrices::released('5.3.0')));
|
||||
}
|
||||
|
||||
public function testNoNoticeForUsersWithoutManageOptions(): void
|
||||
{
|
||||
Functions\when('current_user_can')->justReturn(false);
|
||||
|
||||
self::assertSame('', $this->notice(self::mixedStatus()));
|
||||
}
|
||||
|
||||
public function testNoNoticeWithoutDivi(): void
|
||||
{
|
||||
self::assertSame('', $this->notice(self::mixedStatus(), ''));
|
||||
}
|
||||
|
||||
public function testNoNoticeOnTheDiviTabItself(): void
|
||||
{
|
||||
Functions\when('get_current_screen')->justReturn((object) ['id' => 'settings_page_lit-mcp']);
|
||||
$_GET['tab'] = 'divi';
|
||||
|
||||
self::assertSame('', $this->notice(self::mixedStatus()));
|
||||
}
|
||||
|
||||
public function testTheNoticeStillShowsOnOtherTabsOfThePluginPage(): void
|
||||
{
|
||||
Functions\when('get_current_screen')->justReturn((object) ['id' => 'settings_page_lit-mcp']);
|
||||
$_GET['tab'] = 'tools';
|
||||
|
||||
self::assertStringContainsString('nicht nutzbar', $this->notice(self::mixedStatus()));
|
||||
}
|
||||
|
||||
public function testTheNoticeIsRegisteredOnAdminNotices(): void
|
||||
{
|
||||
$tab = $this->tab(null, Mockery::mock(ReleaseStore::class));
|
||||
Actions\expectAdded('admin_notices')->once()->with([$tab, 'renderNotice']);
|
||||
|
||||
$tab->registerNotice();
|
||||
}
|
||||
|
||||
public function testRegisteringTheTabsActionsAlsoRegistersTheNotice(): void
|
||||
{
|
||||
$tab = $this->tab(null, Mockery::mock(ReleaseStore::class));
|
||||
Actions\expectAdded('admin_notices')->once()->with([$tab, 'renderNotice']);
|
||||
|
||||
$tab->registerActions();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Cli;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use LIT\MCP\Integrations\Divi\Cli\DiviCheckCommand;
|
||||
use LIT\MCP\Integrations\Divi\Release\CheckReport;
|
||||
use LIT\MCP\Integrations\Divi\Release\CheckReportStore;
|
||||
use LIT\MCP\Integrations\Divi\Release\CheckStep;
|
||||
use LIT\MCP\Integrations\Divi\Release\CompatibilityCheck;
|
||||
use LIT\MCP\Integrations\Divi\Release\CompatibilityMatrix;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviReference;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseResolver;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseState;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseStatus;
|
||||
use LIT\MCP\Tests\Support\MatrixFixtures;
|
||||
use LIT\MCP\Tests\Support\ReleaseMatrices;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use Mockery;
|
||||
use RuntimeException;
|
||||
|
||||
final class DiviCheckCommandTest extends TestCase
|
||||
{
|
||||
/** @var list<string> */
|
||||
private array $lines = [];
|
||||
|
||||
private string $themeDir = '';
|
||||
private string $sourceDir = '';
|
||||
private string $expectedFile = '';
|
||||
|
||||
/** @var array<string, string> What the command reads as the signatures of the running Divi. */
|
||||
private array $signatures = [];
|
||||
|
||||
/** Set to true to make the signature read fail like a Divi call that is not loaded. */
|
||||
private bool $signaturesUnreadable = false;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->lines = [];
|
||||
$this->signatures = ['sig:et_pb_is_allowed' => str_repeat('a', 64), 'sig:A::b' => str_repeat('b', 64)];
|
||||
$this->signaturesUnreadable = false;
|
||||
$base = sys_get_temp_dir() . '/lit-cli-' . uniqid();
|
||||
$this->themeDir = $base . '/Divi';
|
||||
$this->sourceDir = $base . '/reference';
|
||||
mkdir($this->themeDir . '/includes/builder', 0777, true);
|
||||
mkdir($this->sourceDir, 0777, true);
|
||||
$this->expectedFile = $base . '/fingerprints.json';
|
||||
Functions\when('current_user_can')->justReturn(true);
|
||||
Functions\when('get_current_user_id')->justReturn(1);
|
||||
Functions\when('delete_transient')->justReturn(true);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
exec('rm -rf ' . escapeshellarg(dirname($this->themeDir)));
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, list<string>> $problems Step key => problems.
|
||||
* @param bool $expectSave True asserts save() is called exactly once (the green happy path); otherwise it is
|
||||
* merely allowed, since some tests (e.g. no admin) never reach the save() call.
|
||||
*/
|
||||
private function command(
|
||||
array $problems = [],
|
||||
bool $expectSave = false,
|
||||
bool $noSourceDir = false,
|
||||
?CompatibilityMatrix $matrix = null
|
||||
): DiviCheckCommand {
|
||||
$steps = array_map(
|
||||
static fn (string $key): CheckStep => new CheckStep($key, $problems[$key] ?? []),
|
||||
[CheckStep::MANIFEST, CheckStep::MIRRORS, CheckStep::CATALOG, CheckStep::FORMAT, CheckStep::WRITE]
|
||||
);
|
||||
$check = Mockery::mock(CompatibilityCheck::class);
|
||||
$check->allows('run')->andReturn(new CheckReport('r1', 'd', 1, '5.3.0', '0.6.0', 'k', $steps));
|
||||
$reports = Mockery::mock(CheckReportStore::class);
|
||||
if ($expectSave) {
|
||||
$reports->expects('save')->once();
|
||||
} else {
|
||||
$reports->allows('save');
|
||||
}
|
||||
|
||||
return new DiviCheckCommand(
|
||||
static fn () => $check,
|
||||
$reports,
|
||||
function (string $line): void {
|
||||
$this->lines[] = $line;
|
||||
},
|
||||
fn (): ?string => $this->themeDir,
|
||||
$this->expectedFile,
|
||||
$noSourceDir ? static fn (): ?string => null : fn (): ?string => $this->sourceDir,
|
||||
function (): array {
|
||||
if ($this->signaturesUnreadable) {
|
||||
throw new RuntimeException('signature of et_pb_is_allowed(): symbol is not loaded');
|
||||
}
|
||||
|
||||
return $this->signatures;
|
||||
},
|
||||
$matrix === null ? null : static fn (): CompatibilityMatrix => $matrix
|
||||
);
|
||||
}
|
||||
|
||||
public function testNeedsAnAdministrator(): void
|
||||
{
|
||||
Functions\when('current_user_can')->justReturn(false);
|
||||
|
||||
self::assertSame(1, $this->command()->run([]));
|
||||
self::assertStringContainsString('--user=', implode("\n", $this->lines));
|
||||
}
|
||||
|
||||
public function testPrintsTheStepsAndSucceedsWhenGreen(): void
|
||||
{
|
||||
self::assertSame(0, $this->command(expectSave: true)->run([]));
|
||||
self::assertContains('✓ Schreibtest auf einer Wegwerfseite', $this->lines);
|
||||
}
|
||||
|
||||
public function testFailsWhenAStepIsRed(): void
|
||||
{
|
||||
self::assertSame(1, $this->command([CheckStep::CATALOG => ['divi/text: unbekanntes Modul']])->run([]));
|
||||
self::assertContains('✗ Modulkatalog (Kurzform-Pfade)', $this->lines);
|
||||
self::assertContains(' divi/text: unbekanntes Modul', $this->lines);
|
||||
}
|
||||
|
||||
public function testUpdateReferenceRefusesWhenMoreThanFingerprintsIsRed(): void
|
||||
{
|
||||
$code = $this->command([CheckStep::FORMAT => ['kaputt']])->run(['update-reference' => true]);
|
||||
|
||||
self::assertSame(1, $code);
|
||||
self::assertFileDoesNotExist($this->expectedFile);
|
||||
}
|
||||
|
||||
public function testUpdateReferenceRefusesWithoutADevelopmentSourceDir(): void
|
||||
{
|
||||
$code = $this->command(noSourceDir: true)->run(['update-reference' => true]);
|
||||
|
||||
self::assertSame(1, $code);
|
||||
self::assertStringContainsString(
|
||||
'Referenz nicht aktualisiert: nur in einer Entwicklungskopie mit tests/ möglich.',
|
||||
implode("\n", $this->lines)
|
||||
);
|
||||
self::assertFileDoesNotExist($this->expectedFile);
|
||||
}
|
||||
|
||||
public function testDiffShowsWhatChangedInAMirroredFunction(): void
|
||||
{
|
||||
file_put_contents(
|
||||
$this->themeDir . '/includes/builder/shortcode-core.php',
|
||||
"<?php\nfunction et_pb_submit_layout( \$args ) {\n\t\$a = 1;\n\treturn 2;\n}\n"
|
||||
);
|
||||
DiviReference::writeExpected(['library-submit-layout' => str_repeat('0', 64)], $this->expectedFile);
|
||||
DiviReference::writeSource(
|
||||
'library-submit-layout',
|
||||
"function et_pb_submit_layout( \$args ) {\n\t\$a = 1;\n\treturn 1;\n}",
|
||||
$this->sourceDir
|
||||
);
|
||||
|
||||
$this->command([CheckStep::MIRRORS => ['Bibliothek: Nachbau von et_pb_submit_layout() – Terme']])
|
||||
->run(['diff' => true]);
|
||||
|
||||
$output = implode("\n", $this->lines);
|
||||
self::assertStringContainsString('library-submit-layout', $output);
|
||||
self::assertStringContainsString("- \treturn 1;", $output);
|
||||
self::assertStringContainsString("+ \treturn 2;", $output);
|
||||
}
|
||||
|
||||
public function testUpdateReferenceWritesNothingWhenAMirrorIsUnreadable(): void
|
||||
{
|
||||
file_put_contents(
|
||||
$this->themeDir . '/includes/builder/shortcode-core.php',
|
||||
"<?php\nfunction et_pb_submit_layout( \$args ) {\n\treturn 2;\n}\n"
|
||||
);
|
||||
|
||||
$code = $this->command([CheckStep::MIRRORS => ['geändert']])->run(['update-reference' => true]);
|
||||
|
||||
// The other mirrored Divi files are missing in the temp theme.
|
||||
self::assertSame(1, $code);
|
||||
self::assertStringContainsString('nicht lesbar', implode("\n", $this->lines));
|
||||
self::assertFileDoesNotExist($this->expectedFile, 'all or nothing');
|
||||
}
|
||||
|
||||
public function testUpdateReferenceWritesNothingWhenASourceIsUnwritable(): void
|
||||
{
|
||||
$this->writeDivi();
|
||||
chmod($this->sourceDir, 0500);
|
||||
|
||||
try {
|
||||
$code = $this->command([CheckStep::MIRRORS => ['geändert']])->run(['update-reference' => true]);
|
||||
|
||||
self::assertSame(1, $code);
|
||||
self::assertStringContainsString(
|
||||
'Referenz nicht vollständig aktualisiert:',
|
||||
implode("\n", $this->lines)
|
||||
);
|
||||
self::assertFileDoesNotExist($this->expectedFile, 'sources are written before fingerprints.json');
|
||||
} finally {
|
||||
chmod($this->sourceDir, 0777);
|
||||
}
|
||||
}
|
||||
|
||||
public function testUpdateReferenceWritesNothingWhenASignatureIsUnreadable(): void
|
||||
{
|
||||
$this->writeDivi();
|
||||
$this->signaturesUnreadable = true;
|
||||
|
||||
$code = $this->command([CheckStep::MIRRORS => ['geändert']])->run(['update-reference' => true]);
|
||||
|
||||
self::assertSame(1, $code);
|
||||
self::assertStringContainsString(
|
||||
'Referenz nicht aktualisiert – die Signatur eines Divi-Aufrufs ist nicht lesbar: '
|
||||
. 'signature of et_pb_is_allowed(): symbol is not loaded',
|
||||
implode("\n", $this->lines)
|
||||
);
|
||||
self::assertFileDoesNotExist($this->expectedFile, 'all or nothing');
|
||||
self::assertSame([], glob($this->sourceDir . '/*.txt'), 'no source written either');
|
||||
}
|
||||
|
||||
public function testUpdateReferenceWritesFingerprintsSignaturesAndSources(): void
|
||||
{
|
||||
$this->writeDivi();
|
||||
|
||||
$code = $this->command([CheckStep::MIRRORS => ['geändert']])->run(['update-reference' => true]);
|
||||
|
||||
self::assertSame(0, $code);
|
||||
self::assertSame(
|
||||
[
|
||||
'library-submit-layout', 'preset-sync', 'sig:A::b', 'sig:et_pb_is_allowed', 'tb-api-save',
|
||||
'tb-load-setting-options', 'tb-store-template', 'vb-sync-flags',
|
||||
],
|
||||
array_keys(DiviReference::expected($this->expectedFile))
|
||||
);
|
||||
self::assertSame(str_repeat('b', 64), DiviReference::expected($this->expectedFile)['sig:A::b']);
|
||||
self::assertContains(
|
||||
'Referenz aktualisiert (6 Fingerabdrücke, 2 Signaturen). Smoke-Skripte laufen lassen, dann committen.',
|
||||
$this->lines
|
||||
);
|
||||
self::assertStringStartsWith(
|
||||
'function et_pb_submit_layout(',
|
||||
(string) DiviReference::readSource('library-submit-layout', $this->sourceDir)
|
||||
);
|
||||
}
|
||||
|
||||
/** Minimal stand-ins for the six mirrored Divi functions at their real paths. */
|
||||
private function writeDivi(): void
|
||||
{
|
||||
$files = [
|
||||
'includes/builder/shortcode-core.php' => "function et_pb_submit_layout( \$args ) {\n\treturn 1;\n}",
|
||||
'includes/builder/frontend-builder/theme-builder/api.php' => "function et_theme_builder_api_save() {\n}",
|
||||
'includes/builder/frontend-builder/theme-builder/theme-builder.php' =>
|
||||
"function et_theme_builder_store_template( \$a, \$b, \$c ) {\n}\n"
|
||||
. "function et_theme_builder_load_template_setting_options( \$ids ) {\n}",
|
||||
'includes/builder-5/server/VisualBuilder/REST/SyncToServer/SyncToServerController.php' =>
|
||||
"class SyncToServerController {\n\tpublic static function update( \$request ) {\n\t}\n}",
|
||||
'includes/builder-5/server/Packages/GlobalData/GlobalPresetController.php' =>
|
||||
"class GlobalPresetController {\n\tpublic static function sync( \$request ) {\n\t}\n}",
|
||||
];
|
||||
foreach ($files as $path => $code) {
|
||||
$file = $this->themeDir . '/' . $path;
|
||||
if (! is_dir(dirname($file))) {
|
||||
mkdir(dirname($file), 0777, true);
|
||||
}
|
||||
file_put_contents($file, "<?php\n" . $code . "\n");
|
||||
}
|
||||
}
|
||||
|
||||
public function testPrintsTheMatrixAfterTheReport(): void
|
||||
{
|
||||
self::assertSame(0, $this->command()->run([]));
|
||||
|
||||
$schreibtest = array_search('✓ Schreibtest auf einer Wegwerfseite', $this->lines, true);
|
||||
$header = array_search('Fähigkeiten:', $this->lines, true);
|
||||
self::assertIsInt($schreibtest);
|
||||
self::assertIsInt($header);
|
||||
self::assertGreaterThan($schreibtest, $header);
|
||||
self::assertContains('✓ Seiten schreiben – freigegeben (code)', $this->lines);
|
||||
self::assertContains('✓ Theme Builder lesen – freigegeben (code)', $this->lines);
|
||||
}
|
||||
|
||||
public function testEveryFeatureGetsOneLine(): void
|
||||
{
|
||||
$this->command()->run([]);
|
||||
|
||||
$matrixLines = array_filter(
|
||||
$this->lines,
|
||||
static fn (string $line): bool => str_contains($line, ' – freigegeben')
|
||||
);
|
||||
self::assertCount(count(DiviFeature::cases()), $matrixLines);
|
||||
}
|
||||
|
||||
public function testAnIncompatibleFeatureFailsWithItsProblemsIndented(): void
|
||||
{
|
||||
$matrix = MatrixFixtures::with(
|
||||
DiviFeature::PageWrite,
|
||||
ReleaseState::Incompatible,
|
||||
'auto',
|
||||
'Seiten: et_pb_x() fehlt'
|
||||
);
|
||||
|
||||
$code = $this->command(matrix: $matrix)->run([]);
|
||||
|
||||
self::assertSame(2, $code);
|
||||
self::assertContains('✗ Seiten schreiben – inkompatibel', $this->lines);
|
||||
self::assertContains(' Seiten: et_pb_x() fehlt', $this->lines);
|
||||
self::assertContains('✓ Seiten lesen – freigegeben (auto)', $this->lines);
|
||||
}
|
||||
|
||||
public function testAnUntestedFeatureFailsWithExitTwoAlthoughTheReportIsGreen(): void
|
||||
{
|
||||
$matrix = MatrixFixtures::with(DiviFeature::Catalog, ReleaseState::Untested, 'auto');
|
||||
|
||||
$code = $this->command(expectSave: true, matrix: $matrix)->run([]);
|
||||
|
||||
self::assertSame(2, $code);
|
||||
self::assertContains('– Modulkatalog – ungetestet', $this->lines);
|
||||
}
|
||||
|
||||
public function testARedReportKeepsExitOneWhateverTheMatrixSays(): void
|
||||
{
|
||||
$red = [CheckStep::CATALOG => ['divi/text: unbekanntes Modul']];
|
||||
$incompatible = MatrixFixtures::with(DiviFeature::Catalog, ReleaseState::Incompatible, 'auto', 'x');
|
||||
|
||||
self::assertSame(1, $this->command($red)->run([]));
|
||||
self::assertSame(1, $this->command($red, matrix: $incompatible)->run([]));
|
||||
}
|
||||
|
||||
public function testUpdateReferenceKeepsItsOwnExitCodeAndStillPrintsTheMatrix(): void
|
||||
{
|
||||
$this->writeDivi();
|
||||
$matrix = MatrixFixtures::with(
|
||||
DiviFeature::PageWrite,
|
||||
ReleaseState::Incompatible,
|
||||
'auto',
|
||||
'Seiten: Nachbau geändert'
|
||||
);
|
||||
|
||||
$code = $this->command([CheckStep::MIRRORS => ['geändert']], matrix: $matrix)
|
||||
->run(['update-reference' => true]);
|
||||
|
||||
self::assertSame(0, $code);
|
||||
self::assertContains('✗ Seiten schreiben – inkompatibel', $this->lines);
|
||||
}
|
||||
|
||||
public function testWithoutAnInjectedMatrixTheStateIsComputedAfterTheCheck(): void
|
||||
{
|
||||
$order = [];
|
||||
Functions\when('delete_transient')->alias(static function (string $key) use (&$order): bool {
|
||||
$order[] = 'delete:' . $key;
|
||||
|
||||
return true;
|
||||
});
|
||||
ReleaseGate::useResolver(static function () use (&$order): ReleaseStatus {
|
||||
$order[] = 'resolve';
|
||||
|
||||
return ReleaseMatrices::released('5.3.0');
|
||||
});
|
||||
|
||||
$this->command()->run([]);
|
||||
|
||||
self::assertSame(['delete:' . ReleaseResolver::CACHE, 'resolve'], $order);
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ use LIT\MCP\Abilities\Ability;
|
||||
use LIT\MCP\Abilities\ExposurePolicy;
|
||||
use LIT\MCP\Abilities\ToolAvailability;
|
||||
use LIT\MCP\Admin\AdminTab;
|
||||
use LIT\MCP\Admin\DiviTab;
|
||||
use LIT\MCP\Integrations\Divi\Admin\DiviTab;
|
||||
use LIT\MCP\Admin\SettingsPage;
|
||||
use LIT\MCP\Integrations\Core\EnvironmentComponent;
|
||||
use LIT\MCP\Integrations\Divi\DiviBuilderDetector;
|
||||
|
||||
Reference in New Issue
Block a user