diff --git a/CLAUDE.md b/CLAUDE.md index 1babb59..dc60e98 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -22,18 +22,19 @@ composer phpcs composer fix ``` -No phpunit.xml config exists yet — tests are fixture-based JSON files in `tests/fixtures/` representing API response shapes. +Tests use PHPUnit 9 with Brain Monkey for WordPress function mocking. Fixture-based JSON files in `tests/fixtures/` represent API response shapes. ## Architecture -**Entry point:** `Integration` is the main class — consumer plugins instantiate it with API URL, product file, product ID, and optional license/menu config. The constructor wires up the three subsystems: +**Entry point:** `Integration` is the main class — consumer plugins instantiate it with API URL, product file, product ID, and optional license/menu config. The constructor creates and stores all subsystem instances as public properties (`$client`, `$updater`, `$tracker`, `$admin`): -- **`Integration`** (`src/Integration.php`) — Holds all shared state (API URL, product info, license config). Provides `api_request()` for all remote API calls and license/transient management helpers. +- **`Integration`** (`src/Integration.php`) — Holds all shared state (API URL, product info, license config) and license/transient management helpers. Stores subsystem instances so classes can reach each other. +- **`Client`** (`src/Client.php`) — Handles all HTTP communication with the remote API. Public methods: `ping()`, `check_license($license)`, `updates()`, `details()`. Private `request()` method contains shared HTTP/response logic. - **`Updater`** (`src/Updater.php`) — Hooks into WordPress update system (`pre_set_site_transient_update_plugins`, `plugins_api`, `upgrader_package_options`, `upgrader_process_complete`) to inject update data from the remote API. - **`Admin`** (`src/Admin.php`) — Renders the license management admin page. Only instantiated when `license_enabled` and `display_menu` are both true. Handles license save/verify via POST with nonce verification. - **`Tracker`** (`src/Tracker.php`) — Handles plugin activation/deactivation hooks and hourly cron license sync via `sync_license_data()`. -All classes receive the `Integration` instance and use its public properties directly (no getters/setters pattern). +All classes receive the `Integration` instance and use its public properties directly (no getters/setters pattern). API calls go through `$integration->client->method()`. ## Code Conventions @@ -41,5 +42,5 @@ All classes receive the `Integration` instance and use its public properties dir - PHP 7.4+ with WordPress `ABSPATH` guard and `class_exists()` guard wrapping each class - Namespace: `Shazzad\PluginUpdater` with PSR-4 autoloading from `src/` - Uses tabs for indentation (WordPress standard) -- All API calls go through `Integration::api_request()` — returns associative array on success or `WP_Error` on failure +- All API calls go through `Client` methods (`ping()`, `check_license()`, `updates()`, `details()`) — each returns associative array on success or `WP_Error` on failure - WordPress capability required for admin: `delete_users` diff --git a/README.md b/README.md index a921097..fd959e0 100644 --- a/README.md +++ b/README.md @@ -43,8 +43,9 @@ new \Shazzad\PluginUpdater\Integration( ## File Structure ``` -/updater/ -├── Integration.php # Core functionality and API handling +/src/ +├── Integration.php # Core state, license helpers, and subsystem wiring +├── Client.php # API client with typed methods (ping, check_license, updates, details) ├── Updater.php # Update checks and WordPress integration ├── Admin.php # WordPress admin interface └── Tracker.php # Plugin tracking and license sync @@ -248,15 +249,6 @@ The updater includes comprehensive error handling: Errors are logged and displayed appropriately in the WordPress admin. -## Debugging - -Enable debugging with the included helper method: - -```php -$integration = new \Shazzad\PluginUpdater\Integration(/* ... */); -$integration->p($some_data); // Pretty print data -``` - ## Changelog ### Version 1.0 diff --git a/src/Admin.php b/src/Admin.php index bcc2460..aff5f84 100644 --- a/src/Admin.php +++ b/src/Admin.php @@ -78,10 +78,87 @@ public function admin_menu() { * @return void */ public function load_page() { + if ( isset( $_POST['wprepo_sync'] ) ) { + $this->handle_sync(); + return; + } + if ( ! isset( $_POST['wprepo_update'] ) ) { return; } + $this->handle_save(); + } + + /** + * Handle syncing/refreshing the existing license data. + * + * @since 1.1 + * @return void + */ + private function handle_sync() { + check_admin_referer( 'wprepo_license_update' ); + + $base_url = remove_query_arg( [ 'm' ] ); + $key = $this->integration->get_license_code(); + + if ( empty( $key ) ) { + wp_redirect( + add_query_arg( + 'error', + urlencode( 'No license key to sync' ), + $base_url + ) + ); + exit; + } + + $response = $this->integration->client->check_license( $key ); + + if ( is_wp_error( $response ) ) { + wp_redirect( + add_query_arg( + 'error', + urlencode( $response->get_error_message() ), + $base_url + ) + ); + exit; + } + + if ( ! empty( $response['license'] ) ) { + $this->integration->update_license_data( $response['license'] ); + $this->integration->refresh_updates_transient(); + wp_redirect( + add_query_arg( + 'message', + urlencode( 'License data synced' ), + $base_url + ) + ); + exit; + } + + $message = ! empty( $response['message'] ) + ? $response['message'] + : 'Unable to sync license data'; + wp_redirect( + add_query_arg( + 'error', + urlencode( $message ), + $base_url + ) + ); + exit; + } + + /** + * Handle saving/updating the license key. + * + * @since 1.1 + * @return void + */ + private function handle_save() { check_admin_referer( 'wprepo_license_update' ); $base_url = remove_query_arg( [ 'm' ] ); @@ -100,7 +177,7 @@ public function load_page() { } $key = sanitize_text_field( $_POST['wprepo_license'] ); - $response = $this->integration->api_request( 'check_license', $key ); + $response = $this->integration->client->check_license( $key ); if ( is_wp_error( $response ) ) { wp_redirect( @@ -179,6 +256,14 @@ public function admin_page() { aria-describedby="wprepo_license-description" value="integration->get_license_code() ); ?>" class="regular-text" /> + integration->has_license_code() ) : ?> + +
Enter your License Key to receive automatic Updates
@@ -193,7 +278,7 @@ class="regular-text" /> integration->api_request( 'details' ); + $response = $this->integration->client->details(); if ( is_wp_error( $response ) ) { \printf( diff --git a/src/Client.php b/src/Client.php new file mode 100644 index 0000000..8bf745f --- /dev/null +++ b/src/Client.php @@ -0,0 +1,174 @@ +integration = $integration; + } + + /** + * Ping the remote API server. + * + * @since 1.1 + * + * @return array|WP_Error Response data or WP_Error on failure. + */ + public function ping() { + return $this->request( 'ping', [], 2 ); + } + + /** + * Check a license against the remote API. + * + * @since 1.1 + * + * @param string $license License key. Uses stored license if empty. + * @return array|WP_Error Response data or WP_Error on failure. + */ + public function check_license( $license = '' ) { + if ( empty( $license ) ) { + $license = $this->integration->get_license_code(); + } + + $args = []; + if ( $license ) { + $args['license'] = $license; + } + + return $this->request( 'check_license', $args ); + } + + /** + * Fetch available updates from the remote API. + * + * @since 1.1 + * + * @return array|WP_Error Response data or WP_Error on failure. + */ + public function updates() { + $args = []; + if ( $this->integration->license_enabled ) { + $license = $this->integration->get_license_code(); + if ( $license ) { + $args['license'] = $license; + } + } + + return $this->request( 'updates', $args ); + } + + /** + * Fetch plugin details from the remote API. + * + * @since 1.1 + * + * @return array|WP_Error Response data or WP_Error on failure. + */ + public function details() { + $args = []; + if ( $this->integration->license_enabled ) { + $license = $this->integration->get_license_code(); + if ( $license ) { + $args['license'] = $license; + } + } + + return $this->request( 'details', $args ); + } + + /** + * Sends an API request to the remote server. + * + * @since 1.1 + * + * @param string $method The API endpoint method. + * @param array $args Additional query arguments. + * @param int $timeout Request timeout in seconds. + * @return array|WP_Error Response data or WP_Error on failure. + */ + private function request( $method, $args = [], $timeout = 5 ) { + $request_url = "{$this->integration->api_url}/products/{$this->integration->product_id}/$method"; + + $args = array_merge( + [ + 'product_version' => $this->integration->product_version, + 'product_status' => $this->integration->product_status, + 'wp_url' => esc_url( site_url( '', 'https' ) ), + 'wp_locale' => get_locale(), + 'wp_version' => get_bloginfo( 'version', 'display' ), + ], + $args + ); + + $request_url = add_query_arg( $args, $request_url ); + + $request = wp_remote_request( + $request_url, + [ 'timeout' => $timeout ] + ); + + if ( is_wp_error( $request ) ) { + return $request; + } + + $status_code = wp_remote_retrieve_response_code( $request ); + $body = wp_remote_retrieve_body( $request ); + $body = json_decode( $body, true ); + + if ( empty( $body ) ) { + return new WP_Error( + 'wprepo_api_fail', + 'No response from update server' + ); + } + + if ( $status_code >= 400 ) { + return new WP_Error( + ! empty( $body['code'] ) ? $body['code'] : 'wprepo_api_error', + ! empty( $body['message'] ) ? $body['message'] : 'API request failed' + ); + } + + return $body; + } + } + +endif; diff --git a/src/Integration.php b/src/Integration.php index 0fddcb0..da6a344 100644 --- a/src/Integration.php +++ b/src/Integration.php @@ -7,8 +7,6 @@ */ namespace Shazzad\PluginUpdater; -use WP_Error; - if ( ! \defined( 'ABSPATH' ) ) { exit; } @@ -116,6 +114,42 @@ class Integration { */ public $license_enabled; + /** + * API client instance. + * + * @since 1.1 + * + * @var Client + */ + public $client; + + /** + * Updater instance. + * + * @since 1.1 + * + * @var Updater + */ + public $updater; + + /** + * Tracker instance. + * + * @since 1.1 + * + * @var Tracker + */ + public $tracker; + + /** + * Admin instance. + * + * @since 1.1 + * + * @var Admin|null + */ + public $admin; + /** * Constructor. * @@ -157,11 +191,12 @@ public function __construct( $this->license_name = sanitize_key( "{$this->product_slug}{$this->product_id}" ); - new Updater( $this ); - new Tracker( $this ); + $this->client = new Client( $this ); + $this->updater = new Updater( $this ); + $this->tracker = new Tracker( $this ); if ( $this->display_menu ) { - new Admin( $this ); + $this->admin = new Admin( $this ); } } @@ -337,72 +372,6 @@ public function clear_updates_transient() { set_site_transient( 'update_plugins', $transient ); } - /** - * Sends API request to the remote server. - * - * @since 1.0 - * - * @param string $method The API endpoint method (e.g., 'ping', 'updates'). - * @param string $license (Optional) License key to check or validate. - * @return array|WP_Error Response data from the API or WP_Error on failure. - */ - public function api_request( $method, $license = '' ) { - $request_url = "{$this->api_url}/products/{$this->product_id}/$method"; - - $args = [ - 'product_version' => $this->product_version, - 'product_status' => $this->product_status, - 'wp_url' => esc_url( site_url( '', 'https' ) ), - 'wp_locale' => get_locale(), - 'wp_version' => get_bloginfo( 'version', 'display' ), - ]; - - if ( $this->license_enabled ) { - if ( ! $license ) { - $license = $this->get_license_code(); - } - if ( $license ) { - $args['license'] = $license; - } - } - - $request_url = add_query_arg( $args, $request_url ); - - $timeout = 5; - if ( 'ping' === $method ) { - $timeout = 2; - } - - $request = wp_remote_request( - $request_url, - [ 'timeout' => $timeout ] - ); - - if ( is_wp_error( $request ) ) { - return $request; - } - - $status_code = wp_remote_retrieve_response_code( $request ); - $body = wp_remote_retrieve_body( $request ); - $body = json_decode( $body, true ); - - if ( empty( $body ) ) { - return new WP_Error( - 'wprepo_api_fail', - 'No response from update server' - ); - } - - if ( $status_code >= 400 ) { - return new WP_Error( - ! empty( $body['code'] ) ? $body['code'] : 'wprepo_api_error', - ! empty( $body['message'] ) ? $body['message'] : 'API request failed' - ); - } - - return $body; - } - } endif; diff --git a/src/Tracker.php b/src/Tracker.php index b881b77..a108bc6 100644 --- a/src/Tracker.php +++ b/src/Tracker.php @@ -59,12 +59,12 @@ public function sync_license_data() { $license = $this->integration->get_license_code(); if ( empty( $license ) ) { // do ping to notify the installation. - $this->integration->api_request( 'ping' ); + $this->integration->client->ping(); return; } - $response = $this->integration->api_request( 'check_license' ); + $response = $this->integration->client->check_license(); if ( is_wp_error( $response ) ) { return; } @@ -83,7 +83,7 @@ public function sync_license_data() { public function product_activated() { $this->integration->product_status = 'active'; $this->integration->clear_updates_transient(); - $this->integration->api_request( 'ping' ); + $this->integration->client->ping(); } /** @@ -95,7 +95,7 @@ public function product_activated() { public function product_deactivated() { $this->integration->product_status = 'inactive'; $this->integration->clear_updates_transient(); - $this->integration->api_request( 'ping' ); + $this->integration->client->ping(); } } diff --git a/src/Updater.php b/src/Updater.php index 86b3f98..c22136f 100644 --- a/src/Updater.php +++ b/src/Updater.php @@ -63,7 +63,7 @@ public function init() { $this->integration->product_version = $plugin['Version']; $this->integration->product_name = $plugin['Name']; - // Schedule a cron job to update license hourly. + // Schedule a cron job to refresh license data hourly. $hook_name = "wprepo_sync_license_data_{$this->integration->license_name}"; if ( ! wp_next_scheduled( $hook_name ) ) { @@ -81,7 +81,7 @@ public function init() { */ public function pre_set_transient( $transient ) { if ( property_exists( $transient, 'checked' ) && ! empty( $transient->checked ) ) { - $response = $this->integration->api_request( 'updates' ); + $response = $this->integration->client->updates(); if ( ! is_wp_error( $response ) && ! empty( $response['updates'] ) ) { $updates = $response['updates']; @@ -142,7 +142,7 @@ public function plugins_api( $return, $action, $arg ) { } if ( ! empty( $arg ) && isset( $arg->slug ) && $arg->slug === $this->integration->product_slug ) { - $response = $this->integration->api_request( 'details' ); + $response = $this->integration->client->details(); if ( is_wp_error( $response ) ) { $return = new \stdClass(); @@ -227,7 +227,7 @@ public function upgrader_process_complete( $upgrader, $args ) { $this->integration->clear_updates_transient(); $this->integration->product_version = $plugin['Version']; - $this->integration->api_request( 'ping' ); + $this->integration->client->ping(); } } } diff --git a/tests/IntegrationApiRequestTest.php b/tests/ClientApiRequestTest.php similarity index 83% rename from tests/IntegrationApiRequestTest.php rename to tests/ClientApiRequestTest.php index c13a298..3452053 100644 --- a/tests/IntegrationApiRequestTest.php +++ b/tests/ClientApiRequestTest.php @@ -4,10 +4,10 @@ use Brain\Monkey\Functions; use WP_Error; -class IntegrationApiRequestTest extends TestCase { +class ClientApiRequestTest extends TestCase { /** - * Stub the common WP functions used by api_request(). + * Stub the common WP functions used by Client::request(). */ private function stub_api_dependencies(): void { Functions\when( 'esc_url' )->returnArg(); @@ -20,7 +20,7 @@ private function stub_api_dependencies(): void { } /** @test */ - public function success_returns_parsed_body() { + public function ping_returns_parsed_body() { $integration = $this->create_integration(); $this->stub_api_dependencies(); @@ -38,7 +38,7 @@ public function success_returns_parsed_body() { ->once() ->andReturn( $fixture ); - $result = $integration->api_request( 'ping' ); + $result = $integration->client->ping(); $this->assertIsArray( $result ); $this->assertSame( 'Ping successful', $result['message'] ); @@ -55,7 +55,7 @@ public function wp_error_from_wp_remote_request_is_returned() { ->once() ->andReturn( $error ); - $result = $integration->api_request( 'ping' ); + $result = $integration->client->ping(); $this->assertInstanceOf( WP_Error::class, $result ); $this->assertSame( 'http_error', $result->get_error_code() ); @@ -70,7 +70,7 @@ public function empty_body_returns_wp_error() { Functions\expect( 'wp_remote_retrieve_response_code' )->once()->andReturn( 200 ); Functions\expect( 'wp_remote_retrieve_body' )->once()->andReturn( '' ); - $result = $integration->api_request( 'updates' ); + $result = $integration->client->updates(); $this->assertInstanceOf( WP_Error::class, $result ); $this->assertSame( 'wprepo_api_fail', $result->get_error_code() ); @@ -87,7 +87,7 @@ public function error_404_with_error_body_returns_wp_error_with_api_code() { Functions\expect( 'wp_remote_retrieve_response_code' )->once()->andReturn( 404 ); Functions\expect( 'wp_remote_retrieve_body' )->once()->andReturn( $fixture ); - $result = $integration->api_request( 'updates' ); + $result = $integration->client->updates(); $this->assertInstanceOf( WP_Error::class, $result ); $this->assertSame( 'plugin_not_exists', $result->get_error_code() ); @@ -105,7 +105,7 @@ public function error_500_without_code_uses_fallback() { Functions\expect( 'wp_remote_retrieve_response_code' )->once()->andReturn( 500 ); Functions\expect( 'wp_remote_retrieve_body' )->once()->andReturn( $body ); - $result = $integration->api_request( 'updates' ); + $result = $integration->client->updates(); $this->assertInstanceOf( WP_Error::class, $result ); $this->assertSame( 'wprepo_api_error', $result->get_error_code() ); @@ -123,7 +123,7 @@ public function error_400_with_no_code_or_message_uses_both_fallbacks() { Functions\expect( 'wp_remote_retrieve_response_code' )->once()->andReturn( 400 ); Functions\expect( 'wp_remote_retrieve_body' )->once()->andReturn( $body ); - $result = $integration->api_request( 'updates' ); + $result = $integration->client->updates(); $this->assertInstanceOf( WP_Error::class, $result ); $this->assertSame( 'wprepo_api_error', $result->get_error_code() ); @@ -131,7 +131,7 @@ public function error_400_with_no_code_or_message_uses_both_fallbacks() { } /** @test */ - public function license_included_when_enabled() { + public function license_included_in_updates_when_enabled() { $integration = $this->create_integration( [ 'license_enabled' => true ] ); $this->stub_api_dependencies(); @@ -154,7 +154,7 @@ public function license_included_when_enabled() { Functions\expect( 'wp_remote_retrieve_response_code' )->once()->andReturn( 200 ); Functions\expect( 'wp_remote_retrieve_body' )->once()->andReturn( $fixture ); - $integration->api_request( 'ping' ); + $integration->client->updates(); $this->assertStringContainsString( 'license=MY-LICENSE-KEY', $captured_url ); } @@ -178,7 +178,7 @@ public function explicit_license_overrides_stored_value() { Functions\expect( 'wp_remote_retrieve_response_code' )->once()->andReturn( 200 ); Functions\expect( 'wp_remote_retrieve_body' )->once()->andReturn( $fixture ); - $integration->api_request( 'ping', 'EXPLICIT-KEY' ); + $integration->client->check_license( 'EXPLICIT-KEY' ); $this->assertStringContainsString( 'license=EXPLICIT-KEY', $captured_url ); $this->assertStringNotContainsString( 'stored-key', $captured_url ); @@ -189,13 +189,18 @@ public function check_license_returns_license_data() { $integration = $this->create_integration(); $this->stub_api_dependencies(); + Functions\expect( 'get_option' ) + ->once() + ->with( 'my-plugin42_code' ) + ->andReturn( false ); + $fixture = $this->load_fixture_raw( 'check-license-success.json' ); Functions\expect( 'wp_remote_request' )->once()->andReturn( [] ); Functions\expect( 'wp_remote_retrieve_response_code' )->once()->andReturn( 200 ); Functions\expect( 'wp_remote_retrieve_body' )->once()->andReturn( $fixture ); - $result = $integration->api_request( 'check_license' ); + $result = $integration->client->check_license(); $this->assertIsArray( $result ); $this->assertSame( 'active', $result['license']['status'] ); @@ -212,9 +217,26 @@ public function updates_returns_update_data() { Functions\expect( 'wp_remote_retrieve_response_code' )->once()->andReturn( 200 ); Functions\expect( 'wp_remote_retrieve_body' )->once()->andReturn( $fixture ); - $result = $integration->api_request( 'updates' ); + $result = $integration->client->updates(); $this->assertIsArray( $result ); $this->assertSame( '1.3.0', $result['updates']['new_version'] ); } + + /** @test */ + public function details_returns_plugin_details() { + $integration = $this->create_integration(); + $this->stub_api_dependencies(); + + $fixture = $this->load_fixture_raw( 'details-success.json' ); + + Functions\expect( 'wp_remote_request' )->once()->andReturn( [] ); + Functions\expect( 'wp_remote_retrieve_response_code' )->once()->andReturn( 200 ); + Functions\expect( 'wp_remote_retrieve_body' )->once()->andReturn( $fixture ); + + $result = $integration->client->details(); + + $this->assertIsArray( $result ); + $this->assertArrayHasKey( 'details', $result ); + } } diff --git a/tests/UpdaterPreSetTransientTest.php b/tests/UpdaterPreSetTransientTest.php index 1e4c1c8..ecad74a 100644 --- a/tests/UpdaterPreSetTransientTest.php +++ b/tests/UpdaterPreSetTransientTest.php @@ -8,7 +8,7 @@ class UpdaterPreSetTransientTest extends TestCase { /** - * Helper: stub the WP functions used by api_request() inside pre_set_transient(). + * Helper: stub the WP functions used by Client::request() inside pre_set_transient(). */ private function stub_api_dependencies(): void { Functions\when( 'esc_url' )->returnArg(); @@ -43,12 +43,7 @@ private function create_updater_with_api_response( ?string $fixture_name, int $s Functions\expect( 'wp_remote_retrieve_body' )->once()->andReturn( $fixture ); } - // Return the existing Updater created by Integration constructor. - // We need to access it — but Integration doesn't store a reference. - // Create a new Updater instance directly. - $updater = new Updater( $integration ); - - return $updater; + return $integration->updater; } /** @@ -66,12 +61,11 @@ private function make_transient( array $checked = [ 'my-plugin/my-plugin.php' => /** @test */ public function returns_unmodified_when_checked_is_empty() { $integration = $this->create_integration(); - $updater = new Updater( $integration ); $transient = new \stdClass(); $transient->checked = []; - $result = $updater->pre_set_transient( $transient ); + $result = $integration->updater->pre_set_transient( $transient ); $this->assertSame( $transient, $result ); $this->assertEmpty( $transient->checked ); @@ -80,11 +74,10 @@ public function returns_unmodified_when_checked_is_empty() { /** @test */ public function returns_unmodified_when_checked_is_missing() { $integration = $this->create_integration(); - $updater = new Updater( $integration ); $transient = new \stdClass(); - $result = $updater->pre_set_transient( $transient ); + $result = $integration->updater->pre_set_transient( $transient ); $this->assertSame( $transient, $result ); } @@ -115,10 +108,9 @@ public function adds_to_no_update_when_version_is_same() { Functions\expect( 'wp_remote_retrieve_response_code' )->once()->andReturn( 200 ); Functions\expect( 'wp_remote_retrieve_body' )->once()->andReturn( $fixture ); - $updater = new Updater( $integration ); $transient = $this->make_transient(); - $result = $updater->pre_set_transient( $transient ); + $result = $integration->updater->pre_set_transient( $transient ); $this->assertArrayNotHasKey( 'my-plugin/my-plugin.php', $result->response ); $this->assertArrayHasKey( 'my-plugin/my-plugin.php', $result->no_update ); @@ -136,10 +128,9 @@ public function adds_to_no_update_when_version_is_lower() { Functions\expect( 'wp_remote_retrieve_response_code' )->once()->andReturn( 200 ); Functions\expect( 'wp_remote_retrieve_body' )->once()->andReturn( $fixture ); - $updater = new Updater( $integration ); $transient = $this->make_transient(); - $result = $updater->pre_set_transient( $transient ); + $result = $integration->updater->pre_set_transient( $transient ); $this->assertArrayNotHasKey( 'my-plugin/my-plugin.php', $result->response ); $this->assertArrayHasKey( 'my-plugin/my-plugin.php', $result->no_update ); @@ -157,14 +148,12 @@ public function moves_existing_response_to_no_update_when_not_newer() { Functions\expect( 'wp_remote_retrieve_response_code' )->once()->andReturn( 200 ); Functions\expect( 'wp_remote_retrieve_body' )->once()->andReturn( $fixture ); - $updater = new Updater( $integration ); - $old_entry = (object) [ 'new_version' => '1.2.0', 'slug' => 'my-plugin' ]; $transient = $this->make_transient(); $transient->response = [ 'my-plugin/my-plugin.php' => $old_entry ]; - $result = $updater->pre_set_transient( $transient ); + $result = $integration->updater->pre_set_transient( $transient ); $this->assertArrayNotHasKey( 'my-plugin/my-plugin.php', $result->response ); $this->assertArrayHasKey( 'my-plugin/my-plugin.php', $result->no_update );