PluginProbe ʕ •ᴥ•ʔ
Reviews Feed – Add Testimonials and Customer Reviews From Google Reviews, Yelp, TripAdvisor, and More / 2.9.0
Reviews Feed – Add Testimonials and Customer Reviews From Google Reviews, Yelp, TripAdvisor, and More v2.9.0
2.11.0 2.10.0 2.9.0 2.8.0 2.7.0 2.6.7 2.6.8 2.6.5 2.6.4 2.6.3 2.6.2 2.6.0 2.5.5 2.5.4 2.5.3 2.5.2 trunk 1.0 1.0.1 1.0.2 1.0.3 1.1 1.1.1 1.1.2 1.2.0 2.0 2.1.0 2.1.1 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.4.5 2.4.6 2.5.0 2.5.1
reviews-feed / tests / Unit / BulkReviewsUpdateStuckStateTest.php
reviews-feed / tests / Unit Last commit date
Doubles 1 month ago Providers 1 month ago BulkRearmOnGrowthTest.php 1 month ago BulkReviewsUpdateStuckStateTest.php 1 month ago ClearCacheRelayResetTest.php 1 month ago DeleteSourceRelayFailureTest.php 1 month ago ErrorHandlerFalsyOptionTest.php 1 month ago FeedCacheUpdateServiceTest.php 1 month ago FeedMalformedPayloadTest.php 1 month ago ForceKeylessRefetchTest.php 1 month ago LicenseDeactivateStaleStateTest.php 1 month ago MediaFinderMemoTest.php 1 month ago MultiSourceAggregationTest.php 1 month ago ReconcileMigratedLicenseRoutineTest.php 1 month ago ReconcileRemovalTest.php 1 month ago RegisterWebsiteRoutineTest.php 1 month ago RemoteRequestMemoTest.php 1 month ago ReviewAlertHeaderTotalsTest.php 1 month ago ReviewAlertPageTargetingTest.php 1 month ago ReviewAlertStarFillTest.php 1 month ago ShortcodeNeutralizationTest.php 1 month ago SiteMigrationRecoveryTest.php 1 month ago Smash1583HeaderParityTest.php 1 month ago Smash1631MultiLanguageBulkTest.php 1 month ago Smash1631UpdateSingleLangScopeTest.php 1 month ago Smash1706TripAdvisorPlaceIdTest.php 1 month ago Smash782BookingHeaderRatingTest.php 1 month ago Smash782CountryFlagEmojiTest.php 1 month ago Smash782ExternalRefreshCronTest.php 1 month ago Smash782ExtrasTemplateTest.php 1 month ago Smash782ReviewAlertProviderDataTest.php 1 month ago SourceIdLookupTest.php 1 month ago WpmlGetCurrentLanguageTest.php 1 month ago WpmlLanguageMappingTest.php 1 month ago
BulkReviewsUpdateStuckStateTest.php
236 lines
1 <?php
2
3 namespace SmashBalloon\Reviews\Tests\Unit;
4
5 use PHPUnit\Framework\TestCase;
6 use SmashBalloon\Reviews\Pro\Services\BulkUpdate\Bulk_Reviews_Update;
7
8 /**
9 * Pins the bulk-history retry state machine and the new "give up cleanly"
10 * branch that closes the customer-facing "Unable to retrieve reviews history"
11 * stuck state.
12 *
13 * Pre-fix, `Bulk_Reviews_Update::get_bulk_reviews()` had only two branches —
14 * happy-path (reviews returned) and first-failure (retry === false). When the
15 * relay returned empty reviews on a SECOND consecutive call (retry already
16 * true), neither branch fired and the option froze at
17 * `{retry: true, is_done: false, page: 1}` indefinitely. No UI button or
18 * license action in v2.5.4 / v2.5.5 ever cleared that state. The customer
19 * saw a permanent "Unable to retrieve reviews history" warning.
20 *
21 * This test asserts the missing third branch now writes `is_done: true`,
22 * which both ends the bulk loop AND collapses the customizer warning trigger
23 * (`retry === true && is_done === false` → false once is_done flips).
24 *
25 * @group bulk-history
26 * @group customer-bug-2026-05-08
27 */
28 class BulkReviewsUpdateStuckStateTest extends TestCase
29 {
30 protected function setUp(): void
31 {
32 parent::setUp();
33 // Reset the test option store between tests. Cron-API stubs
34 // (wp_schedule_single_event, wp_next_scheduled, wp_clear_scheduled_hook)
35 // live in tests/bootstrap.php in the global namespace so the bulk-
36 // update class can find them via PHP's namespace-fallback resolution.
37 global $wp_options_mock;
38 $wp_options_mock = [];
39 }
40
41 /**
42 * Regression pin for the test-harness wiring: the namespaced bulk-update
43 * class must be able to resolve `wp_schedule_single_event` via PHP's
44 * fallback lookup. Pre-fix the stub was eval'd inside the test namespace,
45 * which Bulk_Reviews_Update couldn't see — first-failure tests would
46 * fatal in environments without WordPress loaded.
47 */
48 public function test_cron_stubs_visible_to_bulk_update_namespace(): void
49 {
50 // function_exists with unqualified name checks the global namespace
51 // specifically — exactly the path PHP's fallback resolution uses.
52 $this->assertTrue(function_exists('wp_schedule_single_event'));
53 $this->assertTrue(function_exists('wp_next_scheduled'));
54 $this->assertTrue(function_exists('wp_clear_scheduled_hook'));
55 }
56
57 /**
58 * THE FIX: retry already true + empty reviews response → is_done flips
59 * to true so the source no longer shows the warning indefinitely.
60 */
61 public function test_second_consecutive_empty_response_marks_is_done(): void
62 {
63 $account_id = 'CHIJ_TEST_STUCK';
64
65 // Pre-populate the stuck state: retry already true, page still 1.
66 // This is the exact shape the customer's site was frozen in.
67 $this->seed_bulk_sources([
68 $account_id => [
69 'account_id' => $account_id,
70 'provider' => 'google',
71 'retry' => true,
72 'is_done' => false,
73 'page' => 1,
74 ],
75 ]);
76
77 $bulk = $this->makeBulkInstance($account_id, 'google');
78 $bulk->relay = $this->stubRelayWithEmptyReviews();
79 $bulk->endpoint = 'reviews/google';
80 // A real feed always has a 'localization'; supply one so the bulk job's
81 // language resolver (Util::get_api_call_language, added in SMASH-1631)
82 // takes the direct path instead of the global-defaults fallback. Value
83 // is irrelevant to this test's retry/is_done assertions.
84 $bulk->settings = ['localization' => 'en'];
85 $bulk->provider = ['info' => '{"id":"' . $account_id . '"}'];
86
87 // Run the same code path the cron tick fires.
88 $bulk->get_bulk_reviews();
89
90 $state = $this->getBulkSourcesState($account_id);
91 $this->assertTrue(
92 $state['is_done'],
93 'After second consecutive empty response, is_done MUST be true so the warning clears.'
94 );
95 $this->assertTrue(
96 $state['retry'],
97 'Retry flag stays true (preserves audit trail); the loop terminator is is_done.'
98 );
99 }
100
101 /**
102 * Regression pin: first-failure path STILL sets retry=true and reschedules.
103 * Pre-fix behavior must remain unchanged — only the new third branch was
104 * added.
105 */
106 public function test_first_empty_response_sets_retry_true(): void
107 {
108 $account_id = 'CHIJ_FRESH_FAIL';
109
110 $this->seed_bulk_sources([
111 $account_id => [
112 'account_id' => $account_id,
113 'provider' => 'google',
114 'retry' => false,
115 'is_done' => false,
116 'page' => 1,
117 ],
118 ]);
119
120 $bulk = $this->makeBulkInstance($account_id, 'google');
121 $bulk->relay = $this->stubRelayWithEmptyReviews();
122 $bulk->endpoint = 'reviews/google';
123 // A real feed always has a 'localization'; supply one so the bulk job's
124 // language resolver (Util::get_api_call_language, added in SMASH-1631)
125 // takes the direct path instead of the global-defaults fallback. Value
126 // is irrelevant to this test's retry/is_done assertions.
127 $bulk->settings = ['localization' => 'en'];
128 $bulk->provider = ['info' => '{"id":"' . $account_id . '"}'];
129
130 $bulk->get_bulk_reviews();
131
132 $state = $this->getBulkSourcesState($account_id);
133 $this->assertTrue($state['retry'], 'First empty response must flip retry → true.');
134 $this->assertFalse($state['is_done'], 'First empty response must NOT set is_done — only retry.');
135 }
136
137 /**
138 * Pin the should_make_call gate so a fresh-state account_info gets seeded
139 * and the get_bulk_reviews loop can run. Defensive check: ensures
140 * subsequent fixes don't accidentally short-circuit on a stuck state.
141 */
142 public function test_should_make_call_returns_true_for_stuck_state(): void
143 {
144 $account_id = 'CHIJ_STUCK_ENTRY';
145
146 $this->seed_bulk_sources([
147 $account_id => [
148 'account_id' => $account_id,
149 'provider' => 'yelp',
150 'retry' => true,
151 'is_done' => false,
152 'page' => 1,
153 ],
154 ]);
155
156 $bulk = $this->makeBulkInstance($account_id, 'yelp');
157
158 $this->assertTrue(
159 $bulk->should_make_call(),
160 'Stuck-state entries must allow another call so the new branch can flip is_done.'
161 );
162 }
163
164 /**
165 * Pin the should_make_call gate when is_done is already true: subsequent
166 * cron ticks must short-circuit. Mirrors the existing contract — added
167 * here because the new fix RELIES on this short-circuit holding (after
168 * the new branch flips is_done, no further bulk calls fire).
169 */
170 public function test_should_make_call_returns_false_when_is_done_true(): void
171 {
172 $account_id = 'CHIJ_ALREADY_DONE';
173
174 $this->seed_bulk_sources([
175 $account_id => [
176 'account_id' => $account_id,
177 'provider' => 'google',
178 'retry' => false,
179 'is_done' => true,
180 'page' => 3,
181 ],
182 ]);
183
184 $bulk = $this->makeBulkInstance($account_id, 'google');
185
186 $this->assertFalse(
187 $bulk->should_make_call(),
188 'is_done=true must short-circuit further bulk-history calls.'
189 );
190 }
191
192 /* --- helpers --- */
193
194 private function seed_bulk_sources(array $state): void
195 {
196 global $wp_options_mock;
197 $wp_options_mock['sbr_bulk_sources'] = $state;
198 }
199
200 /**
201 * @return array<string, mixed>
202 */
203 private function getBulkSourcesState(string $account_id): array
204 {
205 global $wp_options_mock;
206 return $wp_options_mock['sbr_bulk_sources'][$account_id] ?? [];
207 }
208
209 private function makeBulkInstance(string $account_id, string $provider): Bulk_Reviews_Update
210 {
211 $bulk = new Bulk_Reviews_Update();
212 $bulk->account_id = $account_id;
213 $bulk->account_provider = $provider;
214 // Seed account_info so should_make_call's lookup matches what's in the option.
215 $bulk->should_make_call();
216 return $bulk;
217 }
218
219 /**
220 * Stub returning a relay-shaped envelope with empty reviews.
221 *
222 * @return mixed Anonymous-class instance — wider than \stdClass to keep
223 * the phpcs PHP-7.1 floor happy without losing the duck.
224 */
225 private function stubRelayWithEmptyReviews()
226 {
227 return new class {
228 public function call($endpoint, $args, $method, $auth)
229 {
230 return ['data' => ['reviews' => []]];
231 }
232 };
233 }
234
235 }
236