Serve plugin updates from tagged Gitea releases
CI / Tests (PHP 8.1) (pull_request) Successful in 45s
CI / Tests (PHP 8.2) (pull_request) Successful in 44s
CI / No Debug Code (pull_request) Successful in 2s
CI / Coding Standards (pull_request) Successful in 2m43s
CI / PHPStan (pull_request) Successful in 2m51s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m34s
CI / Build Plugin Zip (pull_request) Skipped
CI / Tests (PHP 8.1) (pull_request) Successful in 45s
CI / Tests (PHP 8.2) (pull_request) Successful in 44s
CI / No Debug Code (pull_request) Successful in 2s
CI / Coding Standards (pull_request) Successful in 2m43s
CI / PHPStan (pull_request) Successful in 2m51s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m34s
CI / Build Plugin Zip (pull_request) Skipped
Closes #65 Declare an Update URI header and answer core's update_plugins_{hostname} filter from a new Update\UpdateChecker that offers the latest published Gitea release's zip asset when it is newer than the installed version, with transient caching and silent degradation on API failures. Add a release workflow that fires on v* tag pushes: verifies the tag matches the plugin Version header, runs the tests, builds the plugin zip, and attaches it to the release (reusing a UI-created release, flagging hyphenated versions as pre-release so /releases/latest skips them). Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Unsupervised\Schedular\Tests\Unit\Update;
|
||||
|
||||
use Brain\Monkey\Filters;
|
||||
use Brain\Monkey\Functions;
|
||||
use Unsupervised\Schedular\Tests\Unit\TestCase;
|
||||
use Unsupervised\Schedular\Update\UpdateChecker;
|
||||
|
||||
class UpdateCheckerTest extends TestCase
|
||||
{
|
||||
private const PLUGIN_FILE = 'unsupervised-schedular/unsupervised-schedular.php';
|
||||
private const PACKAGE_URL = 'https://git.unsupervised.ca/attachments/abc123';
|
||||
|
||||
/** Stub a successful Gitea API response with the given decoded body. */
|
||||
private function stubApiResponse(int $code, mixed $body): void
|
||||
{
|
||||
Functions\when('wp_remote_get')->justReturn(['response' => ['code' => $code]]);
|
||||
Functions\when('is_wp_error')->justReturn(false);
|
||||
Functions\when('wp_remote_retrieve_response_code')->justReturn($code);
|
||||
Functions\when('wp_remote_retrieve_body')->justReturn(json_encode($body));
|
||||
}
|
||||
|
||||
private function release(string $tag, array $assets): array
|
||||
{
|
||||
return ['tag_name' => $tag, 'assets' => $assets];
|
||||
}
|
||||
|
||||
private function zipAsset(string $name = 'unsupervised-schedular-9.9.9.zip'): array
|
||||
{
|
||||
return ['name' => $name, 'browser_download_url' => self::PACKAGE_URL];
|
||||
}
|
||||
|
||||
public function testRegisterHooksHostnameFilter(): void
|
||||
{
|
||||
Filters\expectAdded('update_plugins_git.unsupervised.ca')->once();
|
||||
|
||||
(new UpdateChecker())->register();
|
||||
}
|
||||
|
||||
public function testOffersUpdateWhenReleaseIsNewer(): void
|
||||
{
|
||||
Functions\when('plugin_basename')->justReturn(self::PLUGIN_FILE);
|
||||
Functions\when('get_transient')->justReturn(false);
|
||||
// USC_VERSION is 1.0.0 in the test bootstrap.
|
||||
$this->stubApiResponse(200, $this->release('v9.9.9', [$this->zipAsset()]));
|
||||
Functions\expect('set_transient')->once()->with(
|
||||
UpdateChecker::TRANSIENT,
|
||||
['version' => '9.9.9', 'package' => self::PACKAGE_URL],
|
||||
\Mockery::type('int')
|
||||
);
|
||||
|
||||
$result = (new UpdateChecker())->provideUpdate(false, [], self::PLUGIN_FILE);
|
||||
|
||||
self::assertSame(
|
||||
[
|
||||
'slug' => 'unsupervised-schedular',
|
||||
'version' => '9.9.9',
|
||||
'url' => UpdateChecker::REPO_URL,
|
||||
'package' => self::PACKAGE_URL,
|
||||
],
|
||||
$result
|
||||
);
|
||||
}
|
||||
|
||||
public function testReturnsUpdateUnchangedForOtherPlugins(): void
|
||||
{
|
||||
Functions\when('plugin_basename')->justReturn(self::PLUGIN_FILE);
|
||||
Functions\expect('wp_remote_get')->never();
|
||||
|
||||
$result = (new UpdateChecker())->provideUpdate(false, [], 'other-plugin/other-plugin.php');
|
||||
|
||||
self::assertFalse($result);
|
||||
}
|
||||
|
||||
public function testNoUpdateWhenReleaseIsNotNewer(): void
|
||||
{
|
||||
Functions\when('plugin_basename')->justReturn(self::PLUGIN_FILE);
|
||||
Functions\when('get_transient')->justReturn(false);
|
||||
Functions\when('set_transient')->justReturn(true);
|
||||
$this->stubApiResponse(200, $this->release('v1.0.0', [$this->zipAsset('unsupervised-schedular-1.0.0.zip')]));
|
||||
|
||||
$result = (new UpdateChecker())->provideUpdate(false, [], self::PLUGIN_FILE);
|
||||
|
||||
self::assertFalse($result);
|
||||
}
|
||||
|
||||
public function testUsesCachedReleaseWithoutHittingApi(): void
|
||||
{
|
||||
Functions\when('plugin_basename')->justReturn(self::PLUGIN_FILE);
|
||||
Functions\when('get_transient')->justReturn(['version' => '2.0.0', 'package' => self::PACKAGE_URL]);
|
||||
Functions\expect('wp_remote_get')->never();
|
||||
Functions\expect('set_transient')->never();
|
||||
|
||||
$result = (new UpdateChecker())->provideUpdate(false, [], self::PLUGIN_FILE);
|
||||
|
||||
self::assertIsArray($result);
|
||||
self::assertSame('2.0.0', $result['version']);
|
||||
}
|
||||
|
||||
public function testApiFailureIsCachedAndReturnsUpdateUnchanged(): void
|
||||
{
|
||||
Functions\when('plugin_basename')->justReturn(self::PLUGIN_FILE);
|
||||
Functions\when('get_transient')->justReturn(false);
|
||||
Functions\when('wp_remote_get')->justReturn('irrelevant');
|
||||
Functions\when('is_wp_error')->justReturn(true);
|
||||
// A failed lookup is cached too, so a down Gitea is not re-polled
|
||||
// on every admin page load.
|
||||
Functions\expect('set_transient')->once()->with(
|
||||
UpdateChecker::TRANSIENT,
|
||||
['version' => '', 'package' => ''],
|
||||
\Mockery::type('int')
|
||||
);
|
||||
|
||||
$result = (new UpdateChecker())->provideUpdate(false, [], self::PLUGIN_FILE);
|
||||
|
||||
self::assertFalse($result);
|
||||
}
|
||||
|
||||
public function testNon200ResponseReturnsUpdateUnchanged(): void
|
||||
{
|
||||
Functions\when('plugin_basename')->justReturn(self::PLUGIN_FILE);
|
||||
Functions\when('get_transient')->justReturn(false);
|
||||
Functions\when('set_transient')->justReturn(true);
|
||||
$this->stubApiResponse(404, ['message' => 'Not Found']);
|
||||
|
||||
$result = (new UpdateChecker())->provideUpdate(false, [], self::PLUGIN_FILE);
|
||||
|
||||
self::assertFalse($result);
|
||||
}
|
||||
|
||||
public function testPicksFirstZipAssetAndSkipsOthers(): void
|
||||
{
|
||||
Functions\when('plugin_basename')->justReturn(self::PLUGIN_FILE);
|
||||
Functions\when('get_transient')->justReturn(false);
|
||||
Functions\when('set_transient')->justReturn(true);
|
||||
$this->stubApiResponse(200, $this->release('v9.9.9', [
|
||||
['name' => 'release-notes.pdf', 'browser_download_url' => 'https://example.test/notes.pdf'],
|
||||
$this->zipAsset(),
|
||||
]));
|
||||
|
||||
$result = (new UpdateChecker())->provideUpdate(false, [], self::PLUGIN_FILE);
|
||||
|
||||
self::assertIsArray($result);
|
||||
self::assertSame(self::PACKAGE_URL, $result['package']);
|
||||
}
|
||||
|
||||
public function testReleaseWithoutZipAssetOffersNoUpdate(): void
|
||||
{
|
||||
Functions\when('plugin_basename')->justReturn(self::PLUGIN_FILE);
|
||||
Functions\when('get_transient')->justReturn(false);
|
||||
Functions\when('set_transient')->justReturn(true);
|
||||
$this->stubApiResponse(200, $this->release('v9.9.9', [
|
||||
['name' => 'release-notes.pdf', 'browser_download_url' => 'https://example.test/notes.pdf'],
|
||||
]));
|
||||
|
||||
$result = (new UpdateChecker())->provideUpdate(false, [], self::PLUGIN_FILE);
|
||||
|
||||
self::assertFalse($result);
|
||||
}
|
||||
|
||||
public function testMalformedApiBodyOffersNoUpdate(): void
|
||||
{
|
||||
Functions\when('plugin_basename')->justReturn(self::PLUGIN_FILE);
|
||||
Functions\when('get_transient')->justReturn(false);
|
||||
Functions\when('set_transient')->justReturn(true);
|
||||
Functions\when('wp_remote_get')->justReturn(['response' => ['code' => 200]]);
|
||||
Functions\when('is_wp_error')->justReturn(false);
|
||||
Functions\when('wp_remote_retrieve_response_code')->justReturn(200);
|
||||
Functions\when('wp_remote_retrieve_body')->justReturn('not json');
|
||||
|
||||
$result = (new UpdateChecker())->provideUpdate(false, [], self::PLUGIN_FILE);
|
||||
|
||||
self::assertFalse($result);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user