Files
WP-LIT-MCP/tests/Unit/Updater/GiteaUpdaterTest.php
T
Jan-Niclas LoosenandClaude Sonnet 5.5 c8648d5263 fix(updater): Paket-Adresse aus Tag und Dateiname bauen, Version 1.0.2
Gitea meldet browser_download_url hinter dem TLS-Proxy mit http://, der Updater lehnte die Adresse ab
und fand kein Update. Die Adresse wird jetzt aus dem geprüften Tag und dem Dateinamen gebaut; release-check.sh
prüft genauso.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 14:50:43 +01:00

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 . '/janniclas.loosen/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]],
]);
}
}