Files
WP-LIT-MCP/tests/Unit/Admin/CompatibilityTabTest.php
T
Jan-Niclas LoosenandClaude Opus 5.5 85f4138b36 feat(admin): Prüfroute je Profil und Reiter „Compatibility“
Die generische CheckRoute (POST lit-mcp/v1/<id>-check, für Divi weiter /divi-check) ersetzt DiviCheckRoute: Lock je Profil mit Besitzer-Token, Bericht je Profil, Rücksprung auf den neuen Reiter und Hook lit_mcp_compat_changed (Divi lädt darauf seinen Freigabe-Status neu). Der Reiter „Compatibility“ zeigt je Plugin Zustand (Wort und Icon), Hinweis zur Stufe, Fähigkeiten, getestete und freigegebene Versionen und den letzten Bericht, mit Prüfen, Freigeben und Zurückziehen (Recht manage_options, Allowlist der Profil-ID, Nonce je Profil). Freigeben serverseitig nur mit frischem grünem Bericht. Der Load-Hook lit_mcp_compat_profile_load bereitet den Abbau des DiviTab vor, der bis dahin parallel läuft.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 13:15:59 +01:00

743 lines
30 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Admin;
use Brain\Monkey\Actions;
use Brain\Monkey\Functions;
use Closure;
use LIT\MCP\Admin\AdminPostAction;
use LIT\MCP\Admin\AdminTab;
use LIT\MCP\Admin\CompatibilityTab;
use LIT\MCP\ChangeLog\ChangeLog;
use LIT\MCP\Compatibility\CheckReport;
use LIT\MCP\Compatibility\CheckReportStore;
use LIT\MCP\Compatibility\CheckStep;
use LIT\MCP\Compatibility\Compatibility;
use LIT\MCP\Compatibility\CompatibilityMatrix;
use LIT\MCP\Compatibility\Profile;
use LIT\MCP\Compatibility\ReleaseState;
use LIT\MCP\Compatibility\ReleaseStore;
use LIT\MCP\Compatibility\Tier;
use LIT\MCP\Plugin;
use LIT\MCP\Tests\Support\FakeCapability;
use LIT\MCP\Tests\Support\FakeChangeLog;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
final class CompatibilityTabTest extends TestCase
{
private const CAPS = [FakeCapability::Read, FakeCapability::Write];
private ?string $redirect = null;
/** @var list<string> The nonce actions check_admin_referer() was asked for. */
private array $nonces = [];
protected function setUp(): void
{
parent::setUp();
$this->redirect = null;
$this->nonces = [];
Functions\stubEscapeFunctions();
Functions\stubTranslationFunctions();
Functions\when('current_user_can')->justReturn(true);
Functions\when('check_admin_referer')->alias(function (string $action): int {
$this->nonces[] = $action;
return 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('rest_url')->alias(static fn (string $path): string => 'https://k.de/wp-json/' . $path);
Functions\when('wp_create_nonce')->alias(static fn (string $action): string => 'nonce-' . $action);
Functions\when('wp_nonce_field')->alias(static function (string $action): void {
echo '<input type="hidden" name="_wpnonce" value="nonce-' . $action . '">';
});
Functions\when('submit_button')->alias(
static function (
string $text,
string $type = 'primary',
string $name = 'submit',
bool $wrap = true,
array $attributes = []
): void {
$extra = '';
foreach ($attributes as $key => $value) {
$extra .= ' ' . $key . '="' . esc_attr($value) . '"'; // WordPress escapes the values itself
}
echo '<button class="button-' . $type . '" name="' . $name . '"' . $extra . '>' . $text . '</button>';
}
);
Functions\when('get_option')->alias(static fn (string $name): string => $name === 'date_format' ? 'Y-m-d' : '');
Functions\when('date_i18n')->alias(static fn (string $format, int $time): string => gmdate($format, $time));
Functions\when('get_userdata')->justReturn(false);
Functions\when('sanitize_key')->alias(
static fn (string $key): string => strtolower((string) preg_replace('/[^a-z0-9_\-]/i', '', $key))
);
Functions\when('wp_unslash')->returnArg();
}
protected function tearDown(): void
{
unset($_POST['profile'], $_GET['tab']);
parent::tearDown();
}
/** A profile of the invented plugin "fake"; $matrix only for ReleasePerVersion. */
private static function profile(
Tier $tier = Tier::LockOnFailure,
?CompatibilityMatrix $matrix = null,
bool $active = true,
?string $version = '1.2.0',
array $tested = [],
?Closure $stepLabel = null
): Profile {
return new Profile(
id: 'fake',
label: 'Fake <Plugin>',
tier: $tier,
capabilities: self::CAPS,
active: static fn (): bool => $active,
version: static fn (): ?string => $version,
matrix: $tier === Tier::ReleasePerVersion
? static fn (): CompatibilityMatrix => $matrix ?? self::fail('no matrix expected')
: null,
testedVersions: $tested,
stateKey: static fn (string $v): string => 'key' . $v,
stepLabel: $stepLabel
);
}
private static function matrix(ReleaseState $state, array $problems = []): CompatibilityMatrix
{
return CompatibilityMatrix::uniform(self::CAPS, $state, $problems, null);
}
private static function report(bool $green = true, string $version = '1.2.0', string $key = 'key1.2.0'): CheckReport
{
$steps = [
new CheckStep(CheckStep::MANIFEST, $green ? [] : ['Funktion <i>x</i> fehlt'], false, 'Manifest <b>Label</b>'),
new CheckStep('old', [], false),
];
return new CheckReport('r1', '2026-10-01T08:00:00+00:00', 1, $version, Plugin::VERSION, $key, $steps);
}
private function tab(
Profile $profile,
?CheckReport $report = null,
?ReleaseStore $releases = null,
?FakeChangeLog $log = null
): CompatibilityTab {
$reports = Mockery::mock(CheckReportStore::class);
$reports->allows('latest')->andReturn($report);
$store = $releases ?? Mockery::mock(ReleaseStore::class);
if ($releases === null) {
$store->allows('entries')->andReturn([]);
$store->allows('find')->andReturn(null);
}
return new CompatibilityTab(
new Compatibility([$profile]),
$log ?? FakeChangeLog::forbidden(),
new AdminPostAction(static function (): void {
}),
static fn (): CheckReportStore => $reports,
static fn (): ReleaseStore => $store
);
}
private static function html(CompatibilityTab $tab): string
{
ob_start();
$tab->render();
return (string) ob_get_clean();
}
public function testItIsTheTabCompatibility(): void
{
$tab = $this->tab(self::profile());
self::assertInstanceOf(AdminTab::class, $tab);
self::assertSame('compatibility', $tab->slug());
self::assertSame('Compatibility', $tab->label());
}
public function testAnInactiveProfileIsOneLineAndNeverAsksForTheMatrix(): void
{
Actions\expectDone('lit_mcp_compat_profile_extra')->never();
// ReleasePerVersion with a matrix closure that fails the test when called.
$html = self::html($this->tab(self::profile(Tier::ReleasePerVersion, null, active: false)));
self::assertStringContainsString('nicht installiert oder nicht aktiv', $html);
self::assertStringContainsString('Fake &lt;Plugin&gt;', $html);
self::assertStringNotContainsString('<form', $html);
self::assertStringNotContainsString('<table', $html);
}
/** @dataProvider states */
public function testTheOverallStateIsShownAsWordAndIcon(ReleaseState $state, string $word, string $icon): void
{
$html = self::html($this->tab(self::profile(Tier::ReleasePerVersion, self::matrix($state, ['Problem A']))));
self::assertStringContainsString('<strong>' . $word . '</strong>', $html);
self::assertStringContainsString('dashicons-' . $icon, $html);
self::assertStringContainsString('aria-hidden="true"', $html);
}
/** @return array<string, array{ReleaseState, string, string}> */
public static function states(): array
{
return [
'released' => [ReleaseState::Released, 'freigegeben', 'yes-alt'],
'untested' => [ReleaseState::Untested, 'ungetestet', 'warning'],
'incompatible' => [ReleaseState::Incompatible, 'inkompatibel', 'dismiss'],
];
}
public function testItShowsTheVersionAndTheProblemsAndTheCapabilityTable(): void
{
$html = self::html($this->tab(
self::profile(Tier::ReleasePerVersion, self::matrix(ReleaseState::Incompatible, ['Problem A']))
));
self::assertStringContainsString('1.2.0', $html);
self::assertStringContainsString('Problem A', $html);
self::assertStringContainsString('Fake fake-read', $html);
self::assertStringContainsString('Fake fake-write', $html);
}
/** @dataProvider tierTexts */
public function testTheTierHintSaysWhatTheReleaseMeans(Tier $tier, string $text): void
{
$matrix = $tier === Tier::ReleasePerVersion ? self::matrix(ReleaseState::Released) : null;
$html = self::html($this->tab(self::profile($tier, $matrix)));
self::assertStringContainsString($text, $html);
}
/** @return array<string, array{Tier, string}> */
public static function tierTexts(): array
{
return [
'release' => [Tier::ReleasePerVersion, 'Freigabe pro Version nötig'],
'lock' => [
Tier::LockOnFailure,
'Freigabe wird dokumentiert, Nutzung automatisch, solange die Prüfung grün ist',
],
'report' => [Tier::ReportOnly, 'nur Anzeige'],
];
}
public function testLockOnFailureIsLockedWhenTheProfileIsActiveButTooOld(): void
{
$profile = new Profile(
id: 'fake',
label: 'Fake',
tier: Tier::LockOnFailure,
capabilities: self::CAPS,
version: static fn (): string => '0.9',
minVersion: '1.0'
);
$tab = new CompatibilityTab(new Compatibility([$profile]), FakeChangeLog::forbidden());
Functions\when('get_option')->justReturn(false);
$html = self::html($tab);
self::assertStringContainsString('<strong>inkompatibel</strong>', $html);
self::assertStringContainsString('Mindestversion', $html);
}
public function testTestedAndReleasedVersionsAreListed(): void
{
$releases = Mockery::mock(ReleaseStore::class);
$releases->allows('entries')->andReturn([
['version' => '1.1.0', 'plugin_version' => '0.7.0', 'user_id' => 1, 'released_at' => 'x', 'report_id' => 'r'],
]);
$releases->allows('find')->andReturn(null);
$html = self::html($this->tab(self::profile(tested: ['1.0.0', '1.0.1']), null, $releases));
self::assertStringContainsString('1.0.0, 1.0.1', $html);
self::assertStringContainsString('1.1.0', $html);
self::assertStringContainsString('LIT MCP 0.7.0', $html);
}
public function testNoVersionListsShowADashWhenEmpty(): void
{
$html = self::html($this->tab(self::profile()));
self::assertStringContainsString('Getestete Versionen', $html);
self::assertStringContainsString('Freigegebene Versionen', $html);
self::assertSame(2, substr_count($html, '<td>–</td>'));
}
public function testTheLatestReportListsEveryStepWithIconAndLabel(): void
{
$profile = self::profile(stepLabel: static fn (CheckStep $s): string => $s->label ?? 'Alt:' . $s->key);
$html = self::html($this->tab($profile, self::report()));
self::assertStringContainsString('Letzte Prüfung', $html);
self::assertStringContainsString('Manifest &lt;b&gt;Label&lt;/b&gt;', $html);
self::assertStringContainsString('Alt:old', $html);
self::assertStringContainsString('<strong>bestanden</strong>', $html);
}
public function testAFailedStepSaysSoInVisibleText(): void
{
$html = self::html($this->tab(self::profile(), self::report(false)));
self::assertStringContainsString('<strong>fehlgeschlagen</strong>', $html);
}
public function testTheCapabilityTableHasACaptionNamingTheProfile(): void
{
$html = self::html($this->tab(self::profile()));
self::assertStringContainsString(
'<caption class="screen-reader-text">Fähigkeiten von Fake &lt;Plugin&gt;</caption>',
$html
);
}
public function testEveryButtonNamesItsProfileForAssistiveTechnology(): void
{
$profile = self::profile(Tier::ReleasePerVersion, self::matrix(ReleaseState::Untested));
$withdrawable = self::profile(Tier::ReleasePerVersion, self::matrix(ReleaseState::Released));
$html = self::html($this->tab($profile, self::report()))
. self::html($this->tab($withdrawable, self::report(), self::store(self::entry())));
self::assertStringContainsString('aria-label="Prüfung starten: Fake &lt;Plugin&gt;"', $html);
// The release label names the profile itself.
self::assertStringContainsString('>Fake <Plugin> 1.2.0 freigeben</button>', $html);
self::assertStringContainsString('aria-label="Freigabe zurückziehen: Fake &lt;Plugin&gt;"', $html);
}
public function testEachFormButtonHasAUniqueNameInsteadOfSubmit(): void
{
$profile = self::profile(Tier::ReleasePerVersion, self::matrix(ReleaseState::Untested));
$withdrawable = self::profile(Tier::ReleasePerVersion, self::matrix(ReleaseState::Released));
$html = self::html($this->tab($profile, self::report()))
. self::html($this->tab($withdrawable, self::report(), self::store(self::entry())));
self::assertStringNotContainsString('name="submit"', $html);
self::assertStringContainsString('name="lit-mcp-compat-release-fake"', $html);
self::assertStringContainsString('name="lit-mcp-compat-withdraw-fake"', $html);
self::assertStringContainsString('name="lit-mcp-compat-check-fake"', $html);
}
public function testTheLoadHookIsRegisteredOnTheSettingsPage(): void
{
$tab = $this->tab(self::profile());
Actions\expectAdded('load-settings_page_lit-mcp')->once()->with([$tab, 'loadProfiles']);
$tab->registerActions();
}
public function testTheLoadActionFiresPerProfileOnTheOwnTabOnly(): void
{
$_GET['tab'] = 'compatibility';
Actions\expectDone('lit_mcp_compat_profile_load')->once()->with('fake');
$this->tab(self::profile())->loadProfiles();
}
public function testTheLoadActionStaysQuietOnOtherTabsAndForOthersThanAdministrators(): void
{
Actions\expectDone('lit_mcp_compat_profile_load')->never();
foreach (['verbindung', '', ['compatibility']] as $tabParam) {
$_GET['tab'] = $tabParam;
$this->tab(self::profile())->loadProfiles();
}
unset($_GET['tab']);
$this->tab(self::profile())->loadProfiles();
$_GET['tab'] = 'compatibility';
Functions\when('current_user_can')->justReturn(false);
$this->tab(self::profile())->loadProfiles();
}
public function testRenderingDoesNotFireTheLoadAction(): void
{
$_GET['tab'] = 'compatibility';
Actions\expectDone('lit_mcp_compat_profile_load')->never();
self::html($this->tab(self::profile()));
}
public function testAnArrayAsProfileFieldIsRefusedWithoutAWarning(): void
{
$this->actionsDie();
$_POST['profile'] = ['fake'];
$this->expectException(\RuntimeException::class);
$this->tab(self::profile())->release();
}
public function testASkippedStepIsMarkedAsNotRun(): void
{
$report = new CheckReport('r1', '2026-10-01T08:00:00+00:00', 1, '1.2.0', Plugin::VERSION, 'k', [
new CheckStep('write', [], true, 'Schreibtest'),
]);
$html = self::html($this->tab(self::profile(), $report));
self::assertStringContainsString('Schreibtest', $html);
self::assertStringContainsString('<strong>übersprungen</strong>', $html);
self::assertStringNotContainsString('screen-reader-text">übersprungen', $html);
self::assertStringContainsString('nicht ausgeführt', $html);
}
public function testAStepWithoutLabelAndWithoutLabelSourceShowsItsKey(): void
{
$html = self::html($this->tab(self::profile(), self::report()));
self::assertStringContainsString('</strong>: old', $html);
}
public function testAFailedStepListsItsProblems(): void
{
$html = self::html($this->tab(self::profile(), self::report(false)));
self::assertStringContainsString('fehlgeschlagen', $html);
self::assertStringContainsString('Funktion &lt;i&gt;x&lt;/i&gt; fehlt', $html);
}
public function testWithoutAReportNothingIsListed(): void
{
self::assertStringNotContainsString('Letzte Prüfung', self::html($this->tab(self::profile())));
}
public function testTheProfileExtraHookRunsLastWithTheProfileId(): void
{
Actions\expectDone('lit_mcp_compat_profile_extra')->once()->with('fake');
self::html($this->tab(self::profile(), self::report()));
}
public function testEverythingFromThirdPartiesIsEscaped(): void
{
$profile = self::profile(
Tier::ReleasePerVersion,
self::matrix(ReleaseState::Incompatible, ['<script>alert(1)</script>']),
version: '<b>x</b>',
tested: ['<i>t</i>']
);
$html = self::html($this->tab($profile, self::report(false, '<b>x</b>')));
self::assertStringNotContainsString('<script>', $html);
self::assertStringNotContainsString('<b>x</b>', $html);
self::assertStringNotContainsString('<i>t</i>', $html);
self::assertStringNotContainsString('<i>x</i>', $html);
self::assertStringNotContainsString('<b>Label</b>', $html);
self::assertStringContainsString('&lt;b&gt;x&lt;/b&gt;', $html);
self::assertStringContainsString('&lt;script&gt;alert(1)&lt;/script&gt;', $html);
}
// --- 1.21: forms and actions -------------------------------------------------------------------------------
private function actionsDie(): void
{
Functions\when('wp_die')->alias(static function (): never {
throw new \RuntimeException('died');
});
}
private static function entry(string $version = '1.2.0'): array
{
return [
'version' => $version,
'plugin_version' => Plugin::VERSION,
'user_id' => 1,
'released_at' => '2026-10-01T08:00:00+00:00',
'report_id' => 'r1',
];
}
/** A store with the given entries for the installed state; add()/withdraw() expectations are the test's. */
private static function store(?array $entry = null): ReleaseStore
{
$releases = Mockery::mock(ReleaseStore::class);
$releases->allows('entries')->andReturn($entry === null ? [] : [$entry]);
$releases->allows('find')->andReturn($entry);
return $releases;
}
public function testRegistersTheAdminPostHandlers(): void
{
$tab = $this->tab(self::profile());
Actions\expectAdded('admin_post_lit_mcp_compat_release')->once()->with([$tab, 'release']);
Actions\expectAdded('admin_post_lit_mcp_compat_withdraw')->once()->with([$tab, 'withdraw']);
$tab->registerActions();
}
public function testTheCheckFormPostsToTheRestRouteWithTheRestNonce(): void
{
$html = self::html($this->tab(self::profile()));
self::assertStringContainsString('action="https://k.de/wp-json/lit-mcp/v1/fake-check"', $html);
self::assertStringContainsString('name="_wpnonce" value="nonce-wp_rest"', $html);
self::assertStringContainsString('Prüfung starten', $html);
}
public function testReleaseNeedsAFreshGreenReportForTheInstalledState(): void
{
$_POST['profile'] = 'fake';
$reports = [null, self::report(false), self::report(true, '1.1.0', 'key1.1.0'), self::report(true, '1.2.0', 'old')];
foreach ($reports as $report) {
$this->redirect = null;
$releases = self::store();
$releases->expects('add')->never();
Actions\expectDone('lit_mcp_compat_changed')->never();
$this->tab(self::profile(), $report, $releases)->release();
self::assertStringContainsString('lit_mcp_notice=compat_release_refused', (string) $this->redirect);
self::assertStringContainsString('lit_mcp_profile=fake', (string) $this->redirect);
}
}
public function testReleaseStoresTheEntryLogsItAndAnnouncesTheChange(): void
{
$_POST['profile'] = 'fake';
$releases = self::store();
$releases->expects('add')->once()->with('1.2.0', Plugin::VERSION, 1, 'r1');
$log = new FakeChangeLog();
Actions\expectDone('lit_mcp_compat_changed')->once()->with('fake');
$this->tab(self::profile(), self::report(), $releases, $log)->release();
self::assertSame(['lit_mcp_compat_release_fake'], $this->nonces);
self::assertStringContainsString('tab=compatibility', (string) $this->redirect);
self::assertStringContainsString('lit_mcp_notice=compat_released', (string) $this->redirect);
self::assertSame('lit/fake-release', $log->records[0]['ability']);
self::assertSame('fake', $log->records[0]['object_type']);
self::assertSame('1.2.0', $log->records[0]['object_id']);
self::assertStringContainsString('Fake <Plugin> 1.2.0', $log->records[0]['summary']);
}
/** @dataProvider refusedReleases */
public function testReleaseIsRefusedWhereTheTabOffersNone(Profile $profile, ?array $entry): void
{
$_POST['profile'] = 'fake';
$releases = self::store($entry);
$releases->expects('add')->never();
Actions\expectDone('lit_mcp_compat_changed')->never();
$this->tab($profile, self::report(), $releases)->release();
self::assertStringContainsString('lit_mcp_notice=compat_release_refused', (string) $this->redirect);
}
/** @return array<string, array{Profile, ?array}> */
public static function refusedReleases(): array
{
return [
'version tested in the plugin' => [self::profile(tested: ['1.2.0']), null],
'already released' => [self::profile(), self::entry()],
'report only' => [self::profile(Tier::ReportOnly), null],
'inactive' => [self::profile(active: false), null],
'version unknown' => [self::profile(version: null), null],
'per version, but already released' => [
self::profile(Tier::ReleasePerVersion, self::matrix(ReleaseState::Released)),
null,
],
'per version, but incompatible' => [
self::profile(Tier::ReleasePerVersion, self::matrix(ReleaseState::Incompatible, ['x'])),
null,
],
];
}
public function testAReleasePerVersionProfileIsReleasedWhileUntested(): void
{
$_POST['profile'] = 'fake';
$releases = self::store();
$releases->expects('add')->once();
Actions\expectDone('lit_mcp_compat_changed')->once();
$profile = self::profile(Tier::ReleasePerVersion, self::matrix(ReleaseState::Untested));
$this->tab($profile, self::report(), $releases, new FakeChangeLog())->release();
self::assertStringContainsString('lit_mcp_notice=compat_released', (string) $this->redirect);
}
public function testAnUnknownProfileIsRefusedBeforeTheNonceOrAnyStore(): void
{
$this->actionsDie();
Actions\expectDone('lit_mcp_compat_changed')->never();
$releases = self::store();
$releases->expects('add')->never();
$releases->expects('withdraw')->never();
foreach (['nope', '', "fake' OR 1=1"] as $id) {
$_POST['profile'] = $id;
foreach (['release', 'withdraw'] as $method) {
try {
$this->tab(self::profile(), self::report(), $releases)->$method();
self::fail('expected the request to stop');
} catch (\RuntimeException $e) {
self::assertSame('died', $e->getMessage());
}
}
}
self::assertNull($this->redirect);
self::assertSame([], $this->nonces);
}
public function testAMissingProfileFieldIsRefused(): void
{
$this->actionsDie();
unset($_POST['profile']);
$this->expectException(\RuntimeException::class);
$this->tab(self::profile())->release();
}
public function testActionsNeedManageOptionsAndTheNonceOfTheProfile(): void
{
$_POST['profile'] = 'fake';
$this->actionsDie();
Functions\when('current_user_can')->justReturn(false);
$releases = self::store();
$releases->expects('add')->never();
$releases->expects('withdraw')->never();
foreach (['release', 'withdraw'] as $method) {
try {
$this->tab(self::profile(), self::report(), $releases)->$method();
self::fail('expected 403');
} catch (\RuntimeException $e) {
self::assertSame('died', $e->getMessage());
}
}
// With the right but a failing nonce check (wrong nonce: WordPress dies) nothing is stored either.
Functions\when('current_user_can')->justReturn(true);
Functions\when('check_admin_referer')->alias(static function (): never {
throw new \RuntimeException('bad nonce');
});
foreach (['release', 'withdraw'] as $method) {
try {
$this->tab(self::profile(), self::report(), $releases)->$method();
self::fail('expected the nonce to stop');
} catch (\RuntimeException $e) {
self::assertSame('bad nonce', $e->getMessage());
}
}
}
public function testWithdrawRemovesTheEntryOfTheInstalledVersionLogsItAndAnnouncesTheChange(): void
{
$_POST['profile'] = 'fake';
$releases = self::store(self::entry());
$releases->expects('withdraw')->once()->with('1.2.0', Plugin::VERSION)->andReturn(true);
$log = new FakeChangeLog();
Actions\expectDone('lit_mcp_compat_changed')->once()->with('fake');
$this->tab(self::profile(), null, $releases, $log)->withdraw();
self::assertSame(['lit_mcp_compat_withdraw_fake'], $this->nonces);
self::assertStringContainsString('lit_mcp_notice=compat_withdrawn', (string) $this->redirect);
self::assertStringContainsString('lit_mcp_profile=fake', (string) $this->redirect);
self::assertSame('lit/fake-release', $log->records[0]['ability']);
}
public function testWithdrawWithoutAnEntryFails(): void
{
$_POST['profile'] = 'fake';
$releases = self::store();
$releases->expects('withdraw')->once()->andReturn(false);
Actions\expectDone('lit_mcp_compat_changed')->never();
$this->tab(self::profile(), null, $releases)->withdraw();
self::assertStringContainsString('lit_mcp_notice=compat_withdraw_failed', (string) $this->redirect);
}
public function testUntestedWithAFreshGreenReportOffersTheRelease(): void
{
$profile = self::profile(Tier::ReleasePerVersion, self::matrix(ReleaseState::Untested));
$html = self::html($this->tab($profile, self::report()));
self::assertStringContainsString('name="action" value="lit_mcp_compat_release"', $html);
self::assertStringContainsString('name="profile" value="fake"', $html);
self::assertStringContainsString('value="nonce-lit_mcp_compat_release_fake"', $html);
self::assertStringContainsString('1.2.0 freigeben', $html);
self::assertStringNotContainsString('lit_mcp_compat_withdraw', $html);
}
public function testUntestedWithoutAFreshReportAsksForACheckInsteadOfOfferingTheRelease(): void
{
$profile = self::profile(Tier::ReleasePerVersion, self::matrix(ReleaseState::Untested));
foreach ([null, self::report(false)] as $report) {
$html = self::html($this->tab($profile, $report));
self::assertStringNotContainsString('lit_mcp_compat_release', $html);
self::assertStringContainsString('Freigeben geht erst nach einer vollständig grünen Prüfung', $html);
}
}
public function testAReleasedPerVersionProfileOffersNeitherReleaseNorWithdrawal(): void
{
$profile = self::profile(Tier::ReleasePerVersion, self::matrix(ReleaseState::Released));
$html = self::html($this->tab($profile, self::report()));
self::assertStringNotContainsString('lit_mcp_compat_release', $html);
self::assertStringNotContainsString('lit_mcp_compat_withdraw', $html);
}
public function testLockOnFailureOffersTheReleaseForAVersionNotTestedInThePlugin(): void
{
$html = self::html($this->tab(self::profile(), self::report()));
self::assertStringContainsString('value="lit_mcp_compat_release"', $html);
self::assertStringNotContainsString('lit_mcp_compat_withdraw', $html);
}
public function testATestedVersionAndReportOnlyOfferNoRelease(): void
{
foreach ([self::profile(tested: ['1.2.0']), self::profile(Tier::ReportOnly)] as $profile) {
$html = self::html($this->tab($profile, self::report()));
self::assertStringNotContainsString('lit_mcp_compat_release', $html);
self::assertStringNotContainsString('Freigeben geht erst', $html);
}
}
public function testAnExistingReleaseCanBeWithdrawnWhateverTheState(): void
{
foreach ([ReleaseState::Released, ReleaseState::Incompatible] as $state) {
$profile = self::profile(Tier::ReleasePerVersion, self::matrix($state, $state === ReleaseState::Released ? [] : ['x']));
$html = self::html($this->tab($profile, self::report(), self::store(self::entry())));
self::assertStringContainsString('value="lit_mcp_compat_withdraw"', $html);
self::assertStringContainsString('value="nonce-lit_mcp_compat_withdraw_fake"', $html);
self::assertStringNotContainsString('value="lit_mcp_compat_release"', $html);
}
}
}