Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
170 lines
6.0 KiB
PHP
170 lines
6.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace LIT\MCP\Tests\Unit\Updater;
|
|
|
|
use Brain\Monkey\Filters;
|
|
use Brain\Monkey\Functions;
|
|
use LIT\MCP\Tests\Unit\TestCase;
|
|
use LIT\MCP\Updater\GiteaUpdater;
|
|
use Mockery;
|
|
use WP_Error;
|
|
|
|
final class GiteaUpdaterTest extends TestCase
|
|
{
|
|
private const BASENAME = 'wp-lit-mcp/wp-lit-mcp.php';
|
|
private const ZIP_NAME = 'wp-lit-mcp-1.0.0.zip';
|
|
private const ZIP = GiteaUpdater::REPOSITORY_URL . '/releases/download/v1.0.0/' . self::ZIP_NAME;
|
|
|
|
private string $logFile = '';
|
|
private string|false $previousLog = '';
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
// error_log() is a PHP internal and cannot be stubbed; redirect it to a temp file instead.
|
|
$this->logFile = (string) tempnam(sys_get_temp_dir(), 'lit-mcp-log');
|
|
$this->previousLog = ini_set('error_log', $this->logFile);
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
ini_set('error_log', (string) $this->previousLog);
|
|
unlink($this->logFile);
|
|
parent::tearDown();
|
|
}
|
|
|
|
public function testRegistersItselfForTheGiteaHost(): void
|
|
{
|
|
$updater = $this->updater();
|
|
Filters\expectAdded('update_plugins_' . GiteaUpdater::HOST)->once()->with([$updater, 'offer'], 10, 3);
|
|
|
|
$updater->register();
|
|
}
|
|
|
|
public function testLatestReleaseIsOfferedWithItsPackage(): void
|
|
{
|
|
$this->givenRelease(200, self::releaseBody('v1.0.0', self::ZIP_NAME, self::ZIP));
|
|
|
|
$update = $this->updater()->offer(false, [], self::BASENAME);
|
|
|
|
self::assertSame('1.0.0', $update['version']);
|
|
self::assertSame(self::ZIP, $update['package']);
|
|
self::assertSame('wp-lit-mcp', $update['slug']);
|
|
self::assertSame('', $this->log());
|
|
}
|
|
|
|
/** @dataProvider reportedAddresses */
|
|
public function testPackageAddressIsBuiltHereNotTakenFromTheAnswer(string $reported): void
|
|
{
|
|
$this->givenRelease(200, self::releaseBody('v1.0.0', self::ZIP_NAME, $reported));
|
|
|
|
$update = $this->updater()->offer(false, [], self::BASENAME);
|
|
|
|
self::assertSame(self::ZIP, $update['package']);
|
|
}
|
|
|
|
/** @return array<string, array{string}> */
|
|
public static function reportedAddresses(): array
|
|
{
|
|
return [
|
|
'http, as Gitea behind a TLS proxy reports it' => [
|
|
'http://' . GiteaUpdater::HOST . '/Loosen-IT/WP-LIT-MCP/releases/download/v1.0.0/'
|
|
. self::ZIP_NAME,
|
|
],
|
|
'another host' => ['https://evil.example/' . self::ZIP_NAME],
|
|
];
|
|
}
|
|
|
|
public function testOtherPluginsAreLeftAlone(): void
|
|
{
|
|
Functions\expect('wp_remote_get')->never();
|
|
|
|
self::assertFalse($this->updater()->offer(false, [], 'anderes/anderes.php'));
|
|
}
|
|
|
|
public function testGitCheckoutIsNeverOffered(): void
|
|
{
|
|
$dir = sys_get_temp_dir() . '/lit-mcp-checkout-' . uniqid();
|
|
mkdir($dir . '/.git', 0777, true);
|
|
Functions\expect('wp_remote_get')->never();
|
|
|
|
try {
|
|
self::assertFalse((new GiteaUpdater(self::BASENAME, $dir))->offer(false, [], self::BASENAME));
|
|
} finally {
|
|
rmdir($dir . '/.git');
|
|
rmdir($dir);
|
|
}
|
|
}
|
|
|
|
public function testUnreachableGiteaOffersNothingAndLogsWhy(): void
|
|
{
|
|
Functions\when('wp_remote_get')->justReturn(new WP_Error('http_request_failed', 'cURL error 28'));
|
|
|
|
self::assertFalse($this->updater()->offer(false, [], self::BASENAME));
|
|
self::assertStringContainsString('update check failed: cURL error 28', $this->log());
|
|
}
|
|
|
|
public function testHttpErrorOffersNothingAndLogsTheStatus(): void
|
|
{
|
|
$this->givenRelease(404, '{"message":"not found"}');
|
|
|
|
self::assertFalse($this->updater()->offer(false, [], self::BASENAME));
|
|
self::assertStringContainsString('update check failed: HTTP 404', $this->log());
|
|
}
|
|
|
|
/** @dataProvider unusableReleases */
|
|
public function testUnusableReleaseOffersNothingAndLogsIt(string $body): void
|
|
{
|
|
$this->givenRelease(200, $body);
|
|
|
|
self::assertFalse($this->updater()->offer(false, [], self::BASENAME));
|
|
self::assertStringContainsString('update check failed', $this->log());
|
|
}
|
|
|
|
/** @return array<string, array{string}> */
|
|
public static function unusableReleases(): array
|
|
{
|
|
return [
|
|
'no JSON' => ['<html>'],
|
|
'tag is no version' => [self::releaseBody('latest', self::ZIP_NAME, self::ZIP)],
|
|
'pre-release tag' => [self::releaseBody('v1.0.0-rc1', self::ZIP_NAME, self::ZIP)],
|
|
'tag with trailing newline' => [self::releaseBody("v1.0.0\n", self::ZIP_NAME, self::ZIP)],
|
|
'asset missing' => [self::releaseBody('v1.0.0', 'other.zip', self::ZIP)],
|
|
'release without assets' => ['{"tag_name":"v1.0.0"}'],
|
|
'asset of another version' => [self::releaseBody('v1.0.0', 'wp-lit-mcp-0.9.0.zip', self::ZIP)],
|
|
];
|
|
}
|
|
|
|
private function updater(): GiteaUpdater
|
|
{
|
|
// __DIR__ has no .git folder.
|
|
return new GiteaUpdater(self::BASENAME, __DIR__);
|
|
}
|
|
|
|
private function log(): string
|
|
{
|
|
return (string) file_get_contents($this->logFile);
|
|
}
|
|
|
|
private function givenRelease(int $status, string $body): void
|
|
{
|
|
$latestRelease = Mockery::pattern('#^https://git\.loosen-it\.de/api/v1/repos/.+/releases/latest$#');
|
|
Functions\expect('wp_remote_get')->once()
|
|
->with($latestRelease, Mockery::type('array'))
|
|
->andReturn(['body' => $body]);
|
|
Functions\when('wp_remote_retrieve_response_code')->justReturn($status);
|
|
Functions\when('wp_remote_retrieve_body')->justReturn($body);
|
|
}
|
|
|
|
private static function releaseBody(string $tag, string $assetName, string $url): string
|
|
{
|
|
// Data providers run before setUp(), so the wp_json_encode() stub does not exist yet.
|
|
return (string) json_encode([ // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode
|
|
'tag_name' => $tag,
|
|
'assets' => [['name' => $assetName, 'browser_download_url' => $url]],
|
|
]);
|
|
}
|
|
}
|