PluginProbe ʕ •ᴥ•ʔ
Reviews Feed – Add Testimonials and Customer Reviews From Google Reviews, Yelp, TripAdvisor, and More / 2.6.7
Reviews Feed – Add Testimonials and Customer Reviews From Google Reviews, Yelp, TripAdvisor, and More v2.6.7
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 2 months ago Providers 2 months ago BulkReviewsUpdateStuckStateTest.php 2 months ago ClearCacheRelayResetTest.php 2 months ago DeleteSourceRelayFailureTest.php 2 months ago ErrorHandlerFalsyOptionTest.php 2 months ago FeedCacheUpdateServiceTest.php 2 months ago FeedMalformedPayloadTest.php 2 months ago ForceKeylessRefetchTest.php 2 months ago LicenseDeactivateStaleStateTest.php 2 months ago MediaFinderMemoTest.php 2 months ago MultiSourceAggregationTest.php 2 months ago ReconcileMigratedLicenseRoutineTest.php 2 months ago ReconcileRemovalTest.php 2 months ago RegisterWebsiteRoutineTest.php 2 months ago RemoteRequestMemoTest.php 2 months ago ReviewAlertHeaderTotalsTest.php 2 months ago ReviewAlertPageTargetingTest.php 2 months ago ReviewAlertStarFillTest.php 2 months ago ShortcodeNeutralizationTest.php 2 months ago SiteMigrationRecoveryTest.php 2 months ago Smash1583HeaderParityTest.php 2 months ago SourceIdLookupTest.php 2 months ago WpmlGetCurrentLanguageTest.php 2 months ago WpmlLanguageMappingTest.php 2 months ago
BulkReviewsUpdateStuckStateTest.php
228 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 $bulk->settings = [];
81 $bulk->provider = ['info' => '{"id":"' . $account_id . '"}'];
82
83 // Run the same code path the cron tick fires.
84 $bulk->get_bulk_reviews();
85
86 $state = $this->getBulkSourcesState($account_id);
87 $this->assertTrue(
88 $state['is_done'],
89 'After second consecutive empty response, is_done MUST be true so the warning clears.'
90 );
91 $this->assertTrue(
92 $state['retry'],
93 'Retry flag stays true (preserves audit trail); the loop terminator is is_done.'
94 );
95 }
96
97 /**
98 * Regression pin: first-failure path STILL sets retry=true and reschedules.
99 * Pre-fix behavior must remain unchanged — only the new third branch was
100 * added.
101 */
102 public function test_first_empty_response_sets_retry_true(): void
103 {
104 $account_id = 'CHIJ_FRESH_FAIL';
105
106 $this->seed_bulk_sources([
107 $account_id => [
108 'account_id' => $account_id,
109 'provider' => 'google',
110 'retry' => false,
111 'is_done' => false,
112 'page' => 1,
113 ],
114 ]);
115
116 $bulk = $this->makeBulkInstance($account_id, 'google');
117 $bulk->relay = $this->stubRelayWithEmptyReviews();
118 $bulk->endpoint = 'reviews/google';
119 $bulk->settings = [];
120 $bulk->provider = ['info' => '{"id":"' . $account_id . '"}'];
121
122 $bulk->get_bulk_reviews();
123
124 $state = $this->getBulkSourcesState($account_id);
125 $this->assertTrue($state['retry'], 'First empty response must flip retry → true.');
126 $this->assertFalse($state['is_done'], 'First empty response must NOT set is_done — only retry.');
127 }
128
129 /**
130 * Pin the should_make_call gate so a fresh-state account_info gets seeded
131 * and the get_bulk_reviews loop can run. Defensive check: ensures
132 * subsequent fixes don't accidentally short-circuit on a stuck state.
133 */
134 public function test_should_make_call_returns_true_for_stuck_state(): void
135 {
136 $account_id = 'CHIJ_STUCK_ENTRY';
137
138 $this->seed_bulk_sources([
139 $account_id => [
140 'account_id' => $account_id,
141 'provider' => 'yelp',
142 'retry' => true,
143 'is_done' => false,
144 'page' => 1,
145 ],
146 ]);
147
148 $bulk = $this->makeBulkInstance($account_id, 'yelp');
149
150 $this->assertTrue(
151 $bulk->should_make_call(),
152 'Stuck-state entries must allow another call so the new branch can flip is_done.'
153 );
154 }
155
156 /**
157 * Pin the should_make_call gate when is_done is already true: subsequent
158 * cron ticks must short-circuit. Mirrors the existing contract — added
159 * here because the new fix RELIES on this short-circuit holding (after
160 * the new branch flips is_done, no further bulk calls fire).
161 */
162 public function test_should_make_call_returns_false_when_is_done_true(): void
163 {
164 $account_id = 'CHIJ_ALREADY_DONE';
165
166 $this->seed_bulk_sources([
167 $account_id => [
168 'account_id' => $account_id,
169 'provider' => 'google',
170 'retry' => false,
171 'is_done' => true,
172 'page' => 3,
173 ],
174 ]);
175
176 $bulk = $this->makeBulkInstance($account_id, 'google');
177
178 $this->assertFalse(
179 $bulk->should_make_call(),
180 'is_done=true must short-circuit further bulk-history calls.'
181 );
182 }
183
184 /* --- helpers --- */
185
186 private function seed_bulk_sources(array $state): void
187 {
188 global $wp_options_mock;
189 $wp_options_mock['sbr_bulk_sources'] = $state;
190 }
191
192 /**
193 * @return array<string, mixed>
194 */
195 private function getBulkSourcesState(string $account_id): array
196 {
197 global $wp_options_mock;
198 return $wp_options_mock['sbr_bulk_sources'][$account_id] ?? [];
199 }
200
201 private function makeBulkInstance(string $account_id, string $provider): Bulk_Reviews_Update
202 {
203 $bulk = new Bulk_Reviews_Update();
204 $bulk->account_id = $account_id;
205 $bulk->account_provider = $provider;
206 // Seed account_info so should_make_call's lookup matches what's in the option.
207 $bulk->should_make_call();
208 return $bulk;
209 }
210
211 /**
212 * Stub returning a relay-shaped envelope with empty reviews.
213 *
214 * @return mixed Anonymous-class instance — wider than \stdClass to keep
215 * the phpcs PHP-7.1 floor happy without losing the duck.
216 */
217 private function stubRelayWithEmptyReviews()
218 {
219 return new class {
220 public function call($endpoint, $args, $method, $auth)
221 {
222 return ['data' => ['reviews' => []]];
223 }
224 };
225 }
226
227 }
228