Skip to content

Commit 3f4167d

Browse files
committed
Release: Fix empty addon list when request is throttled
1 parent 865955d commit 3f4167d

2 files changed

Lines changed: 68 additions & 13 deletions

File tree

src/Services/Addon.php

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,14 @@ public function __construct( Plugin $plugin, CacheInterface $cache, ApiFactory $
8282
* Get the list of addons for the host plugin.
8383
*
8484
* Returns an empty array (rather than a WP_Error) when:
85-
* - the host plugin does not declare `has_addons`,
86-
* - the API request fails, or
87-
* - the throttle window is still open.
85+
* - the host plugin does not declare `has_addons`, or
86+
* - the API request fails with no cached catalog to fall back on.
8887
*
8988
* Use `$force = true` to bypass the cache when the user explicitly
90-
* asks for a refresh from the host plugin's UI.
89+
* asks for a refresh from the host plugin's UI. A forced call that
90+
* the throttle blocks (or that the API rejects) transparently falls
91+
* back to the cached catalog — the host UI keeps showing the
92+
* last-known list instead of emptying out.
9193
*
9294
* @since 2.0.0
9395
*
@@ -100,16 +102,16 @@ public function get_addons( bool $force = false ): array {
100102
return array();
101103
}
102104

103-
if ( ! $force ) {
104-
$cached = $this->cache->get( 'addons' );
105-
if ( false !== $cached && is_array( $cached ) ) {
106-
return $cached;
107-
}
105+
$cached = $this->cache->get( 'addons' );
106+
$cached = ( false !== $cached && is_array( $cached ) ) ? $cached : array();
107+
108+
if ( ! $force && ! empty( $cached ) ) {
109+
return $cached;
108110
}
109111

110112
$addons = $this->get_remote_addons();
111113
if ( is_wp_error( $addons ) ) {
112-
return array();
114+
return $cached;
113115
}
114116

115117
$addons = array_map( array( $this, 'format_addon_data' ), $addons );

tests/Services/AddonTest.php

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,10 @@ public function test_returns_cached_addons_when_available(): void {
4646
public function test_force_bypasses_cache_and_persists_formatted_result(): void {
4747
$cache = $this->createMock( CacheInterface::class );
4848
$cache->method( 'is_throttled' )->willReturn( false );
49-
$cache->expects( $this->never() )->method( 'get' );
49+
// `get` is consulted even on a forced refresh so the cached
50+
// catalog can serve as a fallback when the API call fails or
51+
// the throttle kicks in. Returning `false` mimics "no cache".
52+
$cache->method( 'get' )->willReturn( false );
5053
$cache->expects( $this->once() )
5154
->method( 'set' )
5255
->with(
@@ -84,7 +87,7 @@ static function ( array $value ): bool {
8487
$this->assertTrue( $result[0]['is_premium'] );
8588
}
8689

87-
public function test_returns_empty_array_on_api_error(): void {
90+
public function test_returns_empty_when_api_errors_and_no_cache(): void {
8891
$cache = $this->createMock( CacheInterface::class );
8992
$cache->method( 'get' )->willReturn( false );
9093
$cache->method( 'is_throttled' )->willReturn( false );
@@ -98,7 +101,32 @@ public function test_returns_empty_array_on_api_error(): void {
98101
$this->assertSame( array(), ( new Addon( $this->plugin(), $cache, $factory ) )->get_addons() );
99102
}
100103

101-
public function test_throttled_request_returns_empty_array_and_does_not_call_api(): void {
104+
public function test_force_falls_back_to_cache_when_api_errors(): void {
105+
$cached = array(
106+
array(
107+
'id' => 42,
108+
'title' => 'Cached',
109+
),
110+
);
111+
112+
$cache = $this->createMock( CacheInterface::class );
113+
$cache->method( 'get' )->willReturn( $cached );
114+
$cache->method( 'is_throttled' )->willReturn( false );
115+
// API failed → catalog must not be overwritten.
116+
$cache->expects( $this->never() )->method( 'set' );
117+
118+
$api = $this->createMock( ApiClientInterface::class );
119+
$api->method( 'get' )->willReturn( new WP_Error( 'fail', 'no' ) );
120+
121+
$factory = $this->createMock( ApiFactory::class );
122+
$factory->method( 'make_for_plugin' )->willReturn( $api );
123+
124+
$result = ( new Addon( $this->plugin(), $cache, $factory ) )->get_addons( true );
125+
126+
$this->assertSame( $cached, $result );
127+
}
128+
129+
public function test_throttled_request_returns_empty_when_no_cache(): void {
102130
$cache = $this->createMock( CacheInterface::class );
103131
$cache->method( 'get' )->willReturn( false );
104132
$cache->method( 'is_throttled' )->with( 'addons_check' )->willReturn( true );
@@ -112,6 +140,31 @@ public function test_throttled_request_returns_empty_array_and_does_not_call_api
112140
$this->assertSame( array(), ( new Addon( $this->plugin(), $cache, $factory ) )->get_addons() );
113141
}
114142

143+
public function test_force_falls_back_to_cache_when_throttled(): void {
144+
$cached = array(
145+
array(
146+
'id' => 9,
147+
'title' => 'Stale but fine',
148+
),
149+
);
150+
151+
$cache = $this->createMock( CacheInterface::class );
152+
$cache->method( 'get' )->willReturn( $cached );
153+
$cache->method( 'is_throttled' )->with( 'addons_check' )->willReturn( true );
154+
// Throttle blocked the API call → catalog stays as it is.
155+
$cache->expects( $this->never() )->method( 'set' );
156+
157+
$api = $this->createMock( ApiClientInterface::class );
158+
$api->expects( $this->never() )->method( 'get' );
159+
160+
$factory = $this->createMock( ApiFactory::class );
161+
$factory->method( 'make_for_plugin' )->willReturn( $api );
162+
163+
$result = ( new Addon( $this->plugin(), $cache, $factory ) )->get_addons( true );
164+
165+
$this->assertSame( $cached, $result );
166+
}
167+
115168
public function test_mark_requested_called_after_request(): void {
116169
$cache = $this->createMock( CacheInterface::class );
117170
$cache->method( 'get' )->willReturn( false );

0 commit comments

Comments
 (0)