Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
162 lines
6.6 KiB
PHP
162 lines
6.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Release;
|
|
|
|
use Brain\Monkey\Functions;
|
|
use LIT\MCP\Integrations\Divi\Release\CheckReport;
|
|
use LIT\MCP\Integrations\Divi\Release\CheckReportStore;
|
|
use LIT\MCP\Integrations\Divi\Release\CompatibilityCheck;
|
|
use LIT\MCP\Integrations\Divi\Release\DiviCheckRoute;
|
|
use LIT\MCP\Tests\Unit\TestCase;
|
|
use Mockery;
|
|
use RuntimeException;
|
|
|
|
final class DiviCheckRouteTest extends TestCase
|
|
{
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
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('get_current_user_id')->justReturn(1);
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
unset($GLOBALS['wpdb']);
|
|
parent::tearDown();
|
|
}
|
|
|
|
/** Mock of the global $wpdb, its INSERT IGNORE returning $inserted rows affected. */
|
|
private static function wpdb(int $inserted): \wpdb
|
|
{
|
|
$db = Mockery::mock(\wpdb::class)->makePartial();
|
|
$db->expects('query')->once()->with(Mockery::on(static function (string $sql): bool {
|
|
return str_starts_with(
|
|
$sql,
|
|
"INSERT IGNORE INTO wp_options (option_name, option_value, autoload) "
|
|
. "VALUES ('lit_mcp_divi_check_lock', '"
|
|
) && str_ends_with($sql, "', 'no')");
|
|
}))->andReturn($inserted);
|
|
$GLOBALS['wpdb'] = $db;
|
|
|
|
return $db;
|
|
}
|
|
|
|
public function testRegistersAPostRouteForAdministratorsOnly(): void
|
|
{
|
|
Functions\expect('register_rest_route')->once()->with(
|
|
'lit-mcp/v1',
|
|
'/divi-check',
|
|
Mockery::on(static function (array $args): bool {
|
|
Functions\when('current_user_can')->alias(static fn (string $cap): bool => $cap === 'manage_options');
|
|
$allowed = ($args['permission_callback'])();
|
|
Functions\when('current_user_can')->justReturn(false);
|
|
|
|
return $args['methods'] === 'POST' && $allowed === true && ($args['permission_callback'])() === false;
|
|
})
|
|
);
|
|
|
|
(new DiviCheckRoute(static fn () => null))->registerRoute();
|
|
}
|
|
|
|
public function testRunsTheCheckStoresTheReportAndRedirectsToTheTab(): void
|
|
{
|
|
self::wpdb(1);
|
|
$report = new CheckReport('r1', 'd', 1, '5.3.0', '0.6.0', 'k', []);
|
|
$check = Mockery::mock(CompatibilityCheck::class);
|
|
$check->expects('run')->with(1)->andReturn($report);
|
|
$reports = Mockery::mock(CheckReportStore::class);
|
|
$reports->expects('save')->with($report);
|
|
Functions\expect('delete_option')->once()->with(DiviCheckRoute::LOCK);
|
|
|
|
$response = (new DiviCheckRoute(static fn () => $check, $reports))->handle();
|
|
|
|
self::assertSame(303, $response->get_status());
|
|
self::assertStringContainsString('tab=divi', $response->get_headers()['Location']);
|
|
self::assertStringContainsString('lit_mcp_notice=divi_checked', $response->get_headers()['Location']);
|
|
}
|
|
|
|
public function testASecondCheckWaitsForTheFirst(): void
|
|
{
|
|
$db = self::wpdb(0);
|
|
$db->expects('get_var')->once()->andReturn((string) time());
|
|
Functions\expect('delete_option')->never();
|
|
$reports = Mockery::mock(CheckReportStore::class);
|
|
$reports->expects('save')->never();
|
|
|
|
$response = (new DiviCheckRoute(static fn () => self::fail('no second check'), $reports))->handle();
|
|
|
|
self::assertStringContainsString('lit_mcp_notice=divi_check_running', $response->get_headers()['Location']);
|
|
}
|
|
|
|
public function testAStaleLockIsTakenOver(): void
|
|
{
|
|
$db = self::wpdb(0);
|
|
$db->expects('get_var')->once()->andReturn('1');
|
|
$db->expects('query')->once()->with(Mockery::on(static function (string $sql): bool {
|
|
return str_starts_with($sql, "UPDATE wp_options SET option_value = '")
|
|
&& str_ends_with(
|
|
$sql,
|
|
"' WHERE option_name = 'lit_mcp_divi_check_lock' AND option_value = '1'"
|
|
);
|
|
}))->andReturn(1);
|
|
Functions\expect('delete_option')->once()->with(DiviCheckRoute::LOCK);
|
|
$check = Mockery::mock(CompatibilityCheck::class);
|
|
$check->expects('run')->andReturn(new CheckReport('r1', 'd', 1, '5.3.0', '0.6.0', 'k', []));
|
|
$reports = Mockery::mock(CheckReportStore::class);
|
|
$reports->expects('save');
|
|
|
|
$response = (new DiviCheckRoute(static fn () => $check, $reports))->handle();
|
|
|
|
self::assertStringContainsString('lit_mcp_notice=divi_checked', $response->get_headers()['Location']);
|
|
}
|
|
|
|
public function testATakeoverLostToAnotherRequestWaitsForIt(): void
|
|
{
|
|
$db = self::wpdb(0);
|
|
$db->expects('get_var')->once()->andReturn('1');
|
|
$db->expects('query')->once()->with(Mockery::on(static fn (string $sql): bool => str_starts_with(
|
|
$sql,
|
|
"UPDATE wp_options SET option_value = '"
|
|
)))->andReturn(0);
|
|
Functions\expect('delete_option')->never();
|
|
$reports = Mockery::mock(CheckReportStore::class);
|
|
$reports->expects('save')->never();
|
|
|
|
$response = (new DiviCheckRoute(static fn () => self::fail('no second check'), $reports))->handle();
|
|
|
|
self::assertStringContainsString('lit_mcp_notice=divi_check_running', $response->get_headers()['Location']);
|
|
}
|
|
|
|
public function testTheLockIsReleasedAndTheCheckMarkedFailedWhenItThrows(): void
|
|
{
|
|
self::wpdb(1);
|
|
Functions\expect('delete_option')->once()->with(DiviCheckRoute::LOCK);
|
|
$check = Mockery::mock(CompatibilityCheck::class);
|
|
$check->expects('run')->andThrow(new RuntimeException('divi kaputt'));
|
|
$reports = Mockery::mock(CheckReportStore::class);
|
|
$reports->expects('save')->never();
|
|
// error_log() is a PHP internal and cannot be stubbed; redirect it to a temp file instead.
|
|
$log = (string) tempnam(sys_get_temp_dir(), 'lit-mcp-log');
|
|
$previous = ini_set('error_log', $log);
|
|
|
|
try {
|
|
$response = (new DiviCheckRoute(static fn () => $check, $reports))->handle();
|
|
} finally {
|
|
ini_set('error_log', (string) $previous);
|
|
}
|
|
|
|
self::assertStringContainsString('lit_mcp_notice=divi_check_failed', $response->get_headers()['Location']);
|
|
self::assertStringContainsString(
|
|
'divi check failed: RuntimeException: divi kaputt',
|
|
(string) file_get_contents($log)
|
|
);
|
|
unlink($log);
|
|
}
|
|
}
|