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(); Filters\expectAdded('plugin_row_meta')->once(); (new UpdateChecker())->register(); } public function testFilterRowMetaReplacesViewDetailsLinkForThisPlugin(): void { Functions\when('plugin_basename')->justReturn(self::PLUGIN_FILE); $meta = [ 'Version 1.0.0', 'View details', ]; $result = (new UpdateChecker())->filterRowMeta($meta, self::PLUGIN_FILE); // Core's thickbox modal link is gone. foreach ($result as $item) { self::assertStringNotContainsString('open-plugin-details-modal', $item); } // Replaced with a new-tab link to the Gitea release tag for USC_VERSION (1.0.0 in tests). self::assertStringContainsString( UpdateChecker::REPO_URL . '/releases/tag/v1.0.0', end($result) ); self::assertStringContainsString('target="_blank"', end($result)); } public function testFilterRowMetaLeavesOtherPluginsUntouched(): void { Functions\when('plugin_basename')->justReturn(self::PLUGIN_FILE); $meta = ['View details']; $result = (new UpdateChecker())->filterRowMeta($meta, 'other-plugin/other-plugin.php'); self::assertSame($meta, $result); } 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); } }