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,79 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
# Fires when a v* tag is pushed — including tags created through Gitea's
|
||||||
|
# "New Release" UI. Builds the distributable plugin zip and attaches it to
|
||||||
|
# the release for that tag (creating the release if only a bare tag was
|
||||||
|
# pushed). The attached zip is what UpdateChecker serves to WordPress
|
||||||
|
# sites as the update package.
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
name: Build and Publish Release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '8.3'
|
||||||
|
tools: composer:v2
|
||||||
|
|
||||||
|
# A tag that disagrees with the plugin header would make sites see a
|
||||||
|
# phantom update forever (or never see a real one), so fail fast.
|
||||||
|
- name: Verify tag matches plugin version
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
tag_version="${GITHUB_REF_NAME#v}"
|
||||||
|
header_version="$(sed -nE 's/^[[:space:]]*\*?[[:space:]]*Version:[[:space:]]*([^[:space:]]+).*/\1/p' unsupervised-schedular.php | head -1)"
|
||||||
|
if [ "$tag_version" != "$header_version" ]; then
|
||||||
|
echo "Tag ${GITHUB_REF_NAME} does not match plugin header Version: ${header_version}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "version=${header_version}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: composer install --prefer-dist --no-progress --no-interaction
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: composer test
|
||||||
|
|
||||||
|
- name: Build plugin zip
|
||||||
|
run: composer build
|
||||||
|
|
||||||
|
- name: Publish release with zip asset
|
||||||
|
env:
|
||||||
|
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
api="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
|
||||||
|
version="${{ steps.meta.outputs.version }}"
|
||||||
|
zip="dist/unsupervised-schedular-${version}.zip"
|
||||||
|
|
||||||
|
# Pre-release versions (1.2.3-rc.1) are flagged so Gitea's
|
||||||
|
# /releases/latest endpoint — and therefore the update checker —
|
||||||
|
# skips them.
|
||||||
|
prerelease=false
|
||||||
|
case "$version" in *-*) prerelease=true ;; esac
|
||||||
|
|
||||||
|
# Reuse the release if the tag was created via Gitea's release UI.
|
||||||
|
release_id="$(curl -sS -H "Authorization: token ${TOKEN}" \
|
||||||
|
"${api}/releases/tags/${GITHUB_REF_NAME}" | jq -r '.id // empty' || true)"
|
||||||
|
|
||||||
|
if [ -z "$release_id" ]; then
|
||||||
|
release_id="$(curl -fsS -X POST "${api}/releases" \
|
||||||
|
-H "Authorization: token ${TOKEN}" \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d "{\"tag_name\":\"${GITHUB_REF_NAME}\",\"name\":\"${GITHUB_REF_NAME}\",\"prerelease\":${prerelease}}" \
|
||||||
|
| jq -r '.id')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Attaching ${zip} to release ${release_id}"
|
||||||
|
curl -fsS -X POST \
|
||||||
|
"${api}/releases/${release_id}/assets?name=unsupervised-schedular-${version}.zip" \
|
||||||
|
-H "Authorization: token ${TOKEN}" \
|
||||||
|
-F "attachment=@${zip}" > /dev/null
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# Feature: Plugin Self-Update from Gitea Releases
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
WordPress sites running this plugin receive updates directly from the Gitea
|
||||||
|
repository's releases — no wordpress.org listing and no manual zip uploads.
|
||||||
|
Publishing a release is the whole deploy: bump the version, merge to `main`,
|
||||||
|
tag `vX.Y.Z` in Gitea. Every site sees the update on its next check and can
|
||||||
|
install it with one click, or unattended if the site admin enables
|
||||||
|
auto-updates for the plugin.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
### Release side (`.gitea/workflows/release.yml`)
|
||||||
|
Pushing a `v*` tag (including tags created through Gitea's "New Release" UI)
|
||||||
|
triggers the release workflow, which:
|
||||||
|
|
||||||
|
1. Fails if the tag does not match the `Version:` plugin header — a mismatch
|
||||||
|
would make sites see a phantom update forever, or never see a real one.
|
||||||
|
2. Runs the test suite.
|
||||||
|
3. Builds the distributable zip via `composer build` (`bin/build-zip.sh`):
|
||||||
|
a single top-level `unsupervised-schedular/` folder with a production
|
||||||
|
(no-dev) Composer autoloader.
|
||||||
|
4. Creates the release for the tag (or reuses one created via the UI) and
|
||||||
|
attaches the zip as a release asset. Versions containing a hyphen
|
||||||
|
(e.g. `1.2.3-rc.1`) are flagged as pre-releases.
|
||||||
|
|
||||||
|
The attached asset — not Gitea's auto-generated source archive — is the
|
||||||
|
update package. Source archives have the wrong top-level folder name and no
|
||||||
|
`vendor/` directory, so WordPress could not install them.
|
||||||
|
|
||||||
|
### Site side (`src/Update/UpdateChecker.php`)
|
||||||
|
The plugin header declares:
|
||||||
|
|
||||||
|
```
|
||||||
|
Update URI: https://git.unsupervised.ca/Unsupervised/unsupervised-scheduler
|
||||||
|
```
|
||||||
|
|
||||||
|
Since WP 5.8 that header both blocks wordpress.org from ever serving an
|
||||||
|
update for a same-slug plugin and makes core fire the
|
||||||
|
`update_plugins_git.unsupervised.ca` filter during update checks.
|
||||||
|
`UpdateChecker` (registered in `Plugin::boot()`) answers that filter:
|
||||||
|
|
||||||
|
1. Fetches `GET /api/v1/repos/Unsupervised/unsupervised-scheduler/releases/latest`
|
||||||
|
(anonymous — the repo is public). The `/latest` endpoint excludes drafts
|
||||||
|
and pre-releases, so `-rc` builds are never offered to sites.
|
||||||
|
2. Caches the result (including failures) in the
|
||||||
|
`us_schedular_latest_release` transient for 6 hours.
|
||||||
|
3. Strips the leading `v` from the tag and compares against `USC_VERSION`
|
||||||
|
with `version_compare`; PHP orders `1.0.0-rc.2 < 1.0.0` correctly.
|
||||||
|
4. When newer, returns the release's first `.zip` asset as the update
|
||||||
|
package. Core takes over from there: Plugins-screen notice, one-click
|
||||||
|
update, and WP-Cron auto-updates if enabled.
|
||||||
|
|
||||||
|
Any API failure, malformed response, or asset-less release degrades to
|
||||||
|
"no update available" — never an error surfaced to the site.
|
||||||
|
|
||||||
|
## Cutting a Release
|
||||||
|
1. Bump the version in `unsupervised-schedular.php` (both the `Version:`
|
||||||
|
header and the `USC_VERSION` constant) and merge to `main`.
|
||||||
|
2. Tag the merge commit `vX.Y.Z` — via Gitea's New Release UI or
|
||||||
|
`git tag vX.Y.Z && git push origin vX.Y.Z`.
|
||||||
|
3. The release workflow attaches the zip; sites pick the update up on their
|
||||||
|
next check (twice daily via cron, or immediately from
|
||||||
|
Dashboard → Updates → Check again).
|
||||||
|
|
||||||
|
## Classes
|
||||||
|
|
||||||
|
| Class | Responsibility |
|
||||||
|
|---|---|
|
||||||
|
| `Update\UpdateChecker` | Answers core's `update_plugins_{hostname}` filter from the Gitea releases API |
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
- `tests/Unit/Update/UpdateCheckerTest.php`
|
||||||
@@ -29,6 +29,7 @@ use Unsupervised\Schedular\Policy\PolicyVersionRepository;
|
|||||||
use Unsupervised\Schedular\Registration\AnswerRepository;
|
use Unsupervised\Schedular\Registration\AnswerRepository;
|
||||||
use Unsupervised\Schedular\Registration\QuestionRepository;
|
use Unsupervised\Schedular\Registration\QuestionRepository;
|
||||||
use Unsupervised\Schedular\Registration\RegistrationGate;
|
use Unsupervised\Schedular\Registration\RegistrationGate;
|
||||||
|
use Unsupervised\Schedular\Update\UpdateChecker;
|
||||||
|
|
||||||
class Plugin {
|
class Plugin {
|
||||||
|
|
||||||
@@ -74,6 +75,7 @@ class Plugin {
|
|||||||
$registrationPage = new RegistrationPage( $invites, $policies, $policyVersions, $acceptances, $settings, $registrationMailer );
|
$registrationPage = new RegistrationPage( $invites, $policies, $policyVersions, $acceptances, $settings, $registrationMailer );
|
||||||
$groupClassPage = new GroupClassPage();
|
$groupClassPage = new GroupClassPage();
|
||||||
|
|
||||||
|
( new UpdateChecker() )->register();
|
||||||
( new RoleManager() )->register();
|
( new RoleManager() )->register();
|
||||||
( new RegistrationLoginGate() )->register();
|
( new RegistrationLoginGate() )->register();
|
||||||
( new EmailConfirmationHandler( $settings, $registrationMailer ) )->register();
|
( new EmailConfirmationHandler( $settings, $registrationMailer ) )->register();
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Unsupervised\Schedular\Update;
|
||||||
|
|
||||||
|
use Unsupervised\Schedular\Val;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serves plugin updates from the Gitea repository's releases.
|
||||||
|
*
|
||||||
|
* Core reads the plugin's `Update URI` header and, during every update
|
||||||
|
* check, fires the `update_plugins_{hostname}` filter for that host. This
|
||||||
|
* class answers the filter by fetching the latest published release from
|
||||||
|
* the Gitea API and returning its zip asset when it is newer than the
|
||||||
|
* installed version. Everything downstream — the Plugins-screen notice,
|
||||||
|
* one-click updates, and opt-in auto-updates — is handled by core.
|
||||||
|
*
|
||||||
|
* Drafts and releases marked "pre-release" in Gitea are never offered:
|
||||||
|
* the `/releases/latest` endpoint excludes both.
|
||||||
|
*/
|
||||||
|
class UpdateChecker {
|
||||||
|
|
||||||
|
public const HOSTNAME = 'git.unsupervised.ca';
|
||||||
|
public const REPO_URL = 'https://git.unsupervised.ca/Unsupervised/unsupervised-scheduler';
|
||||||
|
public const API_URL = 'https://git.unsupervised.ca/api/v1/repos/Unsupervised/unsupervised-scheduler/releases/latest';
|
||||||
|
|
||||||
|
public const TRANSIENT = 'us_schedular_latest_release';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How long a release lookup (including a failed one) is cached. Core
|
||||||
|
* runs update checks on admin page loads as well as twice-daily cron,
|
||||||
|
* so the cache keeps the plugin from hammering the Gitea API.
|
||||||
|
*/
|
||||||
|
private const CACHE_TTL = 6 * 3600;
|
||||||
|
|
||||||
|
public function register(): void {
|
||||||
|
add_filter( 'update_plugins_' . self::HOSTNAME, [ $this, 'provideUpdate' ], 10, 3 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `update_plugins_{hostname}` filter callback. Returns the incoming
|
||||||
|
* value untouched unless a newer release with a zip asset exists, in
|
||||||
|
* which case it returns the update array core expects.
|
||||||
|
*/
|
||||||
|
public function provideUpdate( mixed $update, mixed $plugin_data, mixed $plugin_file ): mixed {
|
||||||
|
if ( plugin_basename( USC_PLUGIN_FILE ) !== $plugin_file ) {
|
||||||
|
return $update;
|
||||||
|
}
|
||||||
|
|
||||||
|
$release = $this->latestRelease();
|
||||||
|
|
||||||
|
if ( '' === $release['version'] || '' === $release['package'] ) {
|
||||||
|
return $update;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( version_compare( $release['version'], USC_VERSION, '<=' ) ) {
|
||||||
|
return $update;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'slug' => 'unsupervised-schedular',
|
||||||
|
'version' => $release['version'],
|
||||||
|
'url' => self::REPO_URL,
|
||||||
|
'package' => $release['package'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The latest published release, from the transient cache when fresh.
|
||||||
|
*
|
||||||
|
* @return array{version: string, package: string} Empty strings when no
|
||||||
|
* usable release exists.
|
||||||
|
*/
|
||||||
|
private function latestRelease(): array {
|
||||||
|
$cached = get_transient( self::TRANSIENT );
|
||||||
|
if ( is_array( $cached ) ) {
|
||||||
|
return [
|
||||||
|
'version' => Val::string( $cached['version'] ?? '' ),
|
||||||
|
'package' => Val::string( $cached['package'] ?? '' ),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$release = $this->fetchLatestRelease();
|
||||||
|
set_transient( self::TRANSIENT, $release, self::CACHE_TTL );
|
||||||
|
|
||||||
|
return $release;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ask the Gitea API for the latest published release's version and zip asset.
|
||||||
|
*
|
||||||
|
* @return array{version: string, package: string}
|
||||||
|
*/
|
||||||
|
private function fetchLatestRelease(): array {
|
||||||
|
$none = [
|
||||||
|
'version' => '',
|
||||||
|
'package' => '',
|
||||||
|
];
|
||||||
|
|
||||||
|
$response = wp_remote_get(
|
||||||
|
self::API_URL,
|
||||||
|
[
|
||||||
|
'timeout' => 10,
|
||||||
|
'headers' => [ 'Accept' => 'application/json' ],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
|
||||||
|
return $none;
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = json_decode( wp_remote_retrieve_body( $response ), true );
|
||||||
|
if ( ! is_array( $body ) ) {
|
||||||
|
return $none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release tags are named v1.2.3; the plugin header carries the bare version.
|
||||||
|
$version = preg_replace( '/^v/i', '', Val::string( $body['tag_name'] ?? '' ) ) ?? '';
|
||||||
|
|
||||||
|
// The release workflow attaches the built plugin zip (top-level
|
||||||
|
// unsupervised-schedular/ folder, production autoloader) as an asset.
|
||||||
|
// Gitea's auto-generated source archives are not usable packages.
|
||||||
|
$package = '';
|
||||||
|
$assets = $body['assets'] ?? null;
|
||||||
|
if ( is_array( $assets ) ) {
|
||||||
|
foreach ( $assets as $asset ) {
|
||||||
|
if ( ! is_array( $asset ) ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ( str_ends_with( strtolower( Val::string( $asset['name'] ?? '' ) ), '.zip' ) ) {
|
||||||
|
$package = Val::string( $asset['browser_download_url'] ?? '' );
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( '' === $version || '' === $package ) {
|
||||||
|
return $none;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'version' => $version,
|
||||||
|
'package' => $package,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,3 +16,4 @@ remove_role('us_instructor');
|
|||||||
remove_role('us_student');
|
remove_role('us_student');
|
||||||
|
|
||||||
delete_option('us_schedular_version');
|
delete_option('us_schedular_version');
|
||||||
|
delete_transient('us_schedular_latest_release');
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
* Author: Unsupervised
|
* Author: Unsupervised
|
||||||
* License: GPL-2.0-or-later
|
* License: GPL-2.0-or-later
|
||||||
* Text Domain: unsupervised-schedular
|
* Text Domain: unsupervised-schedular
|
||||||
|
* Update URI: https://git.unsupervised.ca/Unsupervised/unsupervised-scheduler
|
||||||
*/
|
*/
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|||||||
Reference in New Issue
Block a user