PluginProbe ʕ •ᴥ•ʔ
Reviews Feed – Add Testimonials and Customer Reviews From Google Reviews, Yelp, TripAdvisor, and More / 2.10.0
Reviews Feed – Add Testimonials and Customer Reviews From Google Reviews, Yelp, TripAdvisor, and More v2.10.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 3 weeks ago Providers 3 weeks ago BulkRearmOnGrowthTest.php 3 weeks ago BulkReviewsUpdateStuckStateTest.php 3 weeks ago ClearCacheRelayResetTest.php 3 weeks ago DeleteSourceRelayFailureTest.php 3 weeks ago ErrorHandlerFalsyOptionTest.php 3 weeks ago FeedCacheUpdateServiceTest.php 3 weeks ago FeedMalformedPayloadTest.php 3 weeks ago ForceKeylessRefetchTest.php 3 weeks ago LicenseDeactivateStaleStateTest.php 3 weeks ago MediaFinderMemoTest.php 3 weeks ago MultiSourceAggregationTest.php 3 weeks ago ReconcileMigratedLicenseRoutineTest.php 3 weeks ago ReconcileRemovalTest.php 3 weeks ago RegisterWebsiteRoutineTest.php 3 weeks ago RemoteRequestMemoTest.php 3 weeks ago ReviewAlertHeaderTotalsTest.php 3 weeks ago ReviewAlertPageTargetingTest.php 3 weeks ago ReviewAlertStarFillTest.php 3 weeks ago ShortcodeNeutralizationTest.php 3 weeks ago SiteMigrationRecoveryTest.php 3 weeks ago Smash1583HeaderParityTest.php 3 weeks ago Smash1631MultiLanguageBulkTest.php 3 weeks ago Smash1631UpdateSingleLangScopeTest.php 3 weeks ago Smash1706TripAdvisorPlaceIdTest.php 3 weeks ago Smash1756SchemaServiceTest.php 3 weeks ago Smash1785AvatarLocalUrlGuardTest.php 3 weeks ago Smash1785AvatarReHealTest.php 3 weeks ago Smash1795ReviewTextXssTest.php 3 weeks ago Smash782BookingHeaderRatingTest.php 3 weeks ago Smash782CountryFlagEmojiTest.php 3 weeks ago Smash782ExternalRefreshCronTest.php 3 weeks ago Smash782ExtrasTemplateTest.php 3 weeks ago Smash782ReviewAlertProviderDataTest.php 3 weeks ago SourceIdLookupTest.php 3 weeks ago WpmlGetCurrentLanguageTest.php 3 weeks ago WpmlLanguageMappingTest.php 3 weeks 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