Prepare release engine
This commit is contained in:
@@ -8,7 +8,6 @@ use Brain\Monkey\Filters;
|
||||
use Brain\Monkey\Functions;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use LIT\MCP\Updater\GiteaUpdater;
|
||||
use LIT\MCP\Updater\PackageSignature;
|
||||
use Mockery;
|
||||
use WP_Error;
|
||||
|
||||
@@ -20,40 +19,26 @@ final class GiteaUpdaterTest extends TestCase
|
||||
|
||||
private string $logFile = '';
|
||||
private string|false $previousLog = '';
|
||||
private string $secretKey = '';
|
||||
private string $publicKey = '';
|
||||
/** @var list<string> */
|
||||
private array $tempFiles = [];
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
Functions\stubTranslationFunctions();
|
||||
// error_log() is a PHP internal and cannot be stubbed; redirect it to a temp file instead.
|
||||
$this->logFile = $this->tempFile();
|
||||
$this->logFile = (string) tempnam(sys_get_temp_dir(), 'lit-mcp-log');
|
||||
$this->previousLog = ini_set('error_log', $this->logFile);
|
||||
|
||||
$pair = sodium_crypto_sign_keypair();
|
||||
$this->secretKey = sodium_crypto_sign_secretkey($pair);
|
||||
$this->publicKey = base64_encode(sodium_crypto_sign_publickey($pair));
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
ini_set('error_log', (string) $this->previousLog);
|
||||
foreach ($this->tempFiles as $file) {
|
||||
if (file_exists($file)) {
|
||||
unlink($file);
|
||||
}
|
||||
}
|
||||
unlink($this->logFile);
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function testRegistersItsFilters(): void
|
||||
public function testRegistersItselfForTheGiteaHost(): void
|
||||
{
|
||||
$updater = $this->updater();
|
||||
Filters\expectAdded('update_plugins_' . GiteaUpdater::HOST)->once()->with([$updater, 'offer'], 10, 3);
|
||||
Filters\expectAdded('upgrader_pre_download')->once()->with([$updater, 'verifyDownload'], 10, 2);
|
||||
|
||||
$updater->register();
|
||||
}
|
||||
@@ -84,8 +69,7 @@ final class GiteaUpdaterTest extends TestCase
|
||||
Functions\expect('wp_remote_get')->never();
|
||||
|
||||
try {
|
||||
$updater = new GiteaUpdater(self::BASENAME, $dir, new PackageSignature($this->publicKey));
|
||||
self::assertFalse($updater->offer(false, [], self::BASENAME));
|
||||
self::assertFalse((new GiteaUpdater(self::BASENAME, $dir))->offer(false, [], self::BASENAME));
|
||||
} finally {
|
||||
rmdir($dir . '/.git');
|
||||
rmdir($dir);
|
||||
@@ -136,81 +120,10 @@ final class GiteaUpdaterTest extends TestCase
|
||||
];
|
||||
}
|
||||
|
||||
public function testDownloadWithAValidSignatureIsHandedToWordPress(): void
|
||||
{
|
||||
$zip = $this->tempFile('release bytes');
|
||||
$this->givenDownload($zip, $this->sign(self::ZIP_NAME, $zip));
|
||||
|
||||
self::assertSame($zip, $this->updater()->verifyDownload(false, self::ZIP));
|
||||
self::assertFileExists($zip);
|
||||
}
|
||||
|
||||
public function testDownloadWithAForeignSignatureIsDeletedAndRefused(): void
|
||||
{
|
||||
$zip = $this->tempFile('release bytes');
|
||||
$other = $this->tempFile('other bytes');
|
||||
$this->givenDownload($zip, $this->sign(self::ZIP_NAME, $other));
|
||||
|
||||
$result = $this->updater()->verifyDownload(false, self::ZIP);
|
||||
|
||||
self::assertInstanceOf(WP_Error::class, $result);
|
||||
self::assertFileDoesNotExist($zip);
|
||||
self::assertStringContainsString('update failed: signature of ' . self::ZIP_NAME, $this->log());
|
||||
}
|
||||
|
||||
public function testSignatureOfAnotherVersionDoesNotPassAsThisOne(): void
|
||||
{
|
||||
$zip = $this->tempFile('old release bytes');
|
||||
$this->givenDownload($zip, $this->sign('wp-lit-mcp-0.9.0.zip', $zip));
|
||||
|
||||
self::assertInstanceOf(WP_Error::class, $this->updater()->verifyDownload(false, self::ZIP));
|
||||
}
|
||||
|
||||
public function testDownloadWithoutSignatureFileIsRefused(): void
|
||||
{
|
||||
$zip = $this->tempFile('release bytes');
|
||||
$this->givenDownload($zip, '');
|
||||
|
||||
self::assertInstanceOf(WP_Error::class, $this->updater()->verifyDownload(false, self::ZIP));
|
||||
self::assertFileDoesNotExist($zip);
|
||||
}
|
||||
|
||||
public function testFailedDownloadIsReturnedAsIs(): void
|
||||
{
|
||||
$error = new WP_Error('download_failed', 'cURL error 28');
|
||||
Functions\expect('download_url')->once()->with(self::ZIP)->andReturn($error);
|
||||
|
||||
self::assertSame($error, $this->updater()->verifyDownload(false, self::ZIP));
|
||||
}
|
||||
|
||||
public function testOtherPackagesAndEarlierDecisionsAreNotTouched(): void
|
||||
{
|
||||
Functions\expect('download_url')->never();
|
||||
|
||||
self::assertFalse($this->updater()->verifyDownload(false, 'https://downloads.wordpress.org/plugin/x.zip'));
|
||||
self::assertSame('/tmp/earlier.zip', $this->updater()->verifyDownload('/tmp/earlier.zip', self::ZIP));
|
||||
}
|
||||
|
||||
private function updater(): GiteaUpdater
|
||||
{
|
||||
// __DIR__ has no .git folder.
|
||||
return new GiteaUpdater(self::BASENAME, __DIR__, new PackageSignature($this->publicKey));
|
||||
}
|
||||
|
||||
private function tempFile(string $contents = ''): string
|
||||
{
|
||||
$file = (string) tempnam(sys_get_temp_dir(), 'lit-mcp-test');
|
||||
file_put_contents($file, $contents);
|
||||
$this->tempFiles[] = $file;
|
||||
|
||||
return $file;
|
||||
}
|
||||
|
||||
private function sign(string $assetName, string $zipPath): string
|
||||
{
|
||||
$message = PackageSignature::message($assetName, $zipPath);
|
||||
|
||||
return base64_encode(sodium_crypto_sign_detached($message, $this->secretKey));
|
||||
return new GiteaUpdater(self::BASENAME, __DIR__);
|
||||
}
|
||||
|
||||
private function log(): string
|
||||
@@ -218,18 +131,6 @@ final class GiteaUpdaterTest extends TestCase
|
||||
return (string) file_get_contents($this->logFile);
|
||||
}
|
||||
|
||||
private function givenDownload(string $zip, string $signatureBody): void
|
||||
{
|
||||
Functions\expect('download_url')->once()->with(self::ZIP)->andReturn($zip);
|
||||
Functions\expect('wp_safe_remote_get')->once()
|
||||
->with(self::ZIP . '.sig', Mockery::type('array'))
|
||||
->andReturn(['body' => $signatureBody]);
|
||||
Functions\when('wp_remote_retrieve_body')->justReturn($signatureBody);
|
||||
Functions\when('wp_delete_file')->alias(static function (string $file): void {
|
||||
unlink($file);
|
||||
});
|
||||
}
|
||||
|
||||
private function givenRelease(int $status, string $body): void
|
||||
{
|
||||
$latestRelease = Mockery::pattern('#^https://git\.loosen-it\.de/api/v1/repos/.+/releases/latest$#');
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Updater;
|
||||
|
||||
use LIT\MCP\Updater\PackageSignature;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class PackageSignatureTest extends TestCase
|
||||
{
|
||||
private string $zip = '';
|
||||
private string $secretKey = '';
|
||||
private PackageSignature $signature;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$pair = sodium_crypto_sign_keypair();
|
||||
$this->secretKey = sodium_crypto_sign_secretkey($pair);
|
||||
$this->signature = new PackageSignature(base64_encode(sodium_crypto_sign_publickey($pair)));
|
||||
$this->zip = (string) tempnam(sys_get_temp_dir(), 'lit-mcp-sig');
|
||||
file_put_contents($this->zip, 'zip bytes');
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
unlink($this->zip);
|
||||
}
|
||||
|
||||
public function testASignatureOverNameAndContentVerifies(): void
|
||||
{
|
||||
$signature = $this->sign('wp-lit-mcp-1.0.0.zip');
|
||||
|
||||
self::assertTrue($this->signature->verifies('wp-lit-mcp-1.0.0.zip', $this->zip, $signature));
|
||||
}
|
||||
|
||||
public function testAnotherAssetNameDoesNotVerify(): void
|
||||
{
|
||||
$signature = $this->sign('wp-lit-mcp-1.0.0.zip');
|
||||
|
||||
self::assertFalse($this->signature->verifies('wp-lit-mcp-1.0.1.zip', $this->zip, $signature));
|
||||
}
|
||||
|
||||
public function testChangedContentDoesNotVerify(): void
|
||||
{
|
||||
$signature = $this->sign('x.zip');
|
||||
file_put_contents($this->zip, 'changed bytes');
|
||||
|
||||
self::assertFalse($this->signature->verifies('x.zip', $this->zip, $signature));
|
||||
}
|
||||
|
||||
public function testAnotherKeyDoesNotVerify(): void
|
||||
{
|
||||
$foreign = new PackageSignature(base64_encode(sodium_crypto_sign_publickey(sodium_crypto_sign_keypair())));
|
||||
|
||||
self::assertFalse($foreign->verifies('x.zip', $this->zip, $this->sign('x.zip')));
|
||||
}
|
||||
|
||||
/** @dataProvider malformedInput */
|
||||
public function testMalformedInputFailsClosed(string $signature, string $publicKey, string $zipPath): void
|
||||
{
|
||||
$subject = new PackageSignature($publicKey === 'real' ? $this->realKey() : $publicKey);
|
||||
|
||||
self::assertFalse($subject->verifies('x.zip', $zipPath === 'zip' ? $this->zip : $zipPath, $signature));
|
||||
}
|
||||
|
||||
/** @return array<string, array{string, string, string}> */
|
||||
public static function malformedInput(): array
|
||||
{
|
||||
return [
|
||||
'signature is no base64' => ['not base64 !!', 'real', 'zip'],
|
||||
'signature is empty' => ['', 'real', 'zip'],
|
||||
'signature has the wrong length' => [base64_encode('short'), 'real', 'zip'],
|
||||
'no key configured' => ['AAAA', '', 'zip'],
|
||||
'key has the wrong length' => ['AAAA', base64_encode('short'), 'zip'],
|
||||
'file is missing' => ['AAAA', 'real', '/nonexistent/x.zip'],
|
||||
];
|
||||
}
|
||||
|
||||
private function sign(string $assetName): string
|
||||
{
|
||||
$message = PackageSignature::message($assetName, $this->zip);
|
||||
|
||||
return base64_encode(sodium_crypto_sign_detached($message, $this->secretKey));
|
||||
}
|
||||
|
||||
private function realKey(): string
|
||||
{
|
||||
return base64_encode(sodium_crypto_sign_publickey(sodium_crypto_sign_keypair()));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user