PluginProbe ʕ •ᴥ•ʔ
Reviews Feed – Add Testimonials and Customer Reviews From Google Reviews, Yelp, TripAdvisor, and More / 2.7.0
Reviews Feed – Add Testimonials and Customer Reviews From Google Reviews, Yelp, TripAdvisor, and More v2.7.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 / ReconcileMigratedLicenseRoutineTest.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 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
ReconcileMigratedLicenseRoutineTest.php
233 lines
1 <?php
2
3 namespace SmashBalloon\Reviews\Tests\Unit;
4
5 use PHPUnit\Framework\TestCase;
6 use SmashBalloon\Reviews\Common\Services\Upgrade\Routines\ReconcileMigratedLicenseRoutine;
7
8 /**
9 * SMASH-1585 — one-shot recovery for sites wedged by a pre-2.6.3 migration fork.
10 *
11 * Verifies the routine: (1) runs once, only for installs that think they are
12 * licensed; (2) treats only an explicit relay license_active:false (with a
13 * healthy pong) as the wedge; (3) on a confirmed wedge flips local state to
14 * surface Activate while preserving license_key; (4) never clears a healthy or
15 * unverifiable license; (5) is loop-safe (marks done before any work).
16 */
17 class ReconcileMigratedLicenseRoutineTest extends TestCase
18 {
19 protected function setUp(): void
20 {
21 parent::setUp();
22 global $wp_options_mock, $wp_home_url_mock, $wp_transients_mock;
23 $wp_options_mock = [];
24 $wp_home_url_mock = 'https://example.com';
25 $wp_transients_mock = [];
26 }
27
28 private function invokeWillRun(ReconcileMigratedLicenseRoutine $r): bool
29 {
30 $m = new \ReflectionMethod($r, 'will_run');
31 $m->setAccessible(true);
32
33 return (bool) $m->invoke($r);
34 }
35
36 private function invokeIsWedged(ReconcileMigratedLicenseRoutine $r, $ping): bool
37 {
38 $m = new \ReflectionMethod($r, 'is_wedged');
39 $m->setAccessible(true);
40
41 return (bool) $m->invoke($r, $ping);
42 }
43
44 /** Routine with a stubbed ping so run() can be tested without HTTP. */
45 private function routineWithPing($ping): ReconcileMigratedLicenseRoutine
46 {
47 return new class ($ping) extends ReconcileMigratedLicenseRoutine {
48 private $stub;
49
50 public function __construct($stub)
51 {
52 $this->stub = $stub;
53 }
54
55 protected function probe_ping()
56 {
57 return $this->stub;
58 }
59 };
60 }
61
62 private function licensedSettings(): array
63 {
64 return [
65 'access_token' => 'tok-forked',
66 'license_key' => 'ABC-123',
67 'license_status' => 'valid',
68 'license_info' => ['license' => 'valid'],
69 'website_url' => 'https://example.com',
70 ];
71 }
72
73 // ---- will_run ----------------------------------------------------------
74
75 public function test_will_run_true_for_licensed_install_not_yet_reconciled(): void
76 {
77 global $wp_options_mock;
78 $wp_options_mock['sbr_settings'] = $this->licensedSettings();
79
80 $this->assertTrue($this->invokeWillRun(new ReconcileMigratedLicenseRoutine()));
81 }
82
83 public function test_will_run_false_once_done_flag_set(): void
84 {
85 global $wp_options_mock;
86 $wp_options_mock['sbr_settings'] = $this->licensedSettings();
87 $wp_options_mock['sbr_license_reconcile_1585_done'] = time();
88
89 $this->assertFalse($this->invokeWillRun(new ReconcileMigratedLicenseRoutine()));
90 }
91
92 public function test_will_run_false_when_status_not_valid(): void
93 {
94 global $wp_options_mock;
95 $s = $this->licensedSettings();
96 $s['license_status'] = '';
97 $wp_options_mock['sbr_settings'] = $s;
98
99 $this->assertFalse($this->invokeWillRun(new ReconcileMigratedLicenseRoutine()));
100 }
101
102 public function test_will_run_false_without_key_or_token(): void
103 {
104 global $wp_options_mock;
105 $noKey = $this->licensedSettings();
106 $noKey['license_key'] = '';
107 $wp_options_mock['sbr_settings'] = $noKey;
108 $this->assertFalse($this->invokeWillRun(new ReconcileMigratedLicenseRoutine()));
109
110 $noToken = $this->licensedSettings();
111 $noToken['access_token'] = '';
112 $wp_options_mock['sbr_settings'] = $noToken;
113 $this->assertFalse($this->invokeWillRun(new ReconcileMigratedLicenseRoutine()));
114 }
115
116 // ---- is_wedged ---------------------------------------------------------
117
118 public function test_is_wedged_true_only_for_explicit_license_inactive_with_pong(): void
119 {
120 $r = new ReconcileMigratedLicenseRoutine();
121 $this->assertTrue($this->invokeIsWedged($r, [
122 'success' => true, 'pong' => true, 'license_active' => false,
123 ]));
124 }
125
126 public function test_is_wedged_false_for_healthy_or_unverifiable_signals(): void
127 {
128 $r = new ReconcileMigratedLicenseRoutine();
129 // Healthy.
130 $this->assertFalse($this->invokeIsWedged($r, ['success' => true, 'pong' => true, 'license_active' => true]));
131 // Flag missing (older relay) — must not clear.
132 $this->assertFalse($this->invokeIsWedged($r, ['success' => true, 'pong' => true]));
133 // Not reachable / no pong.
134 $this->assertFalse($this->invokeIsWedged($r, ['success' => true, 'license_active' => false]));
135 // Error / unreachable.
136 $this->assertFalse($this->invokeIsWedged($r, null));
137 $this->assertFalse($this->invokeIsWedged($r, ['success' => false, 'license_active' => false]));
138 // Dead/invalid token after migration — relay rejects it, so the call
139 // returns an error envelope (success false, no pong). Must NOT be treated
140 // as a wedge (the register/reverify path handles a dead token, not this).
141 $this->assertFalse($this->invokeIsWedged($r, ['success' => false, 'apiMessage' => 'invalid token']));
142 // Malformed relay response: license_active present but null (not a strict
143 // false) — must not clear on an ambiguous value.
144 $this->assertFalse($this->invokeIsWedged($r, ['success' => true, 'pong' => true, 'license_active' => null]));
145 // Garbage / wrong-type payloads must not throw or clear.
146 $this->assertFalse($this->invokeIsWedged($r, 'unexpected-string'));
147 $this->assertFalse($this->invokeIsWedged($r, ['success' => true, 'pong' => true, 'license_active' => 'false']));
148 }
149
150 // ---- run ---------------------------------------------------------------
151
152 public function test_run_flips_to_activate_on_confirmed_wedge_preserving_key(): void
153 {
154 global $wp_options_mock;
155 $wp_options_mock['sbr_settings'] = $this->licensedSettings();
156 $wp_options_mock['sbr_statuses'] = ['license_tier' => 3, 'license_info' => 'Pro Elite'];
157
158 $routine = $this->routineWithPing(['success' => true, 'pong' => true, 'license_active' => false]);
159 $routine->run();
160
161 $s = $wp_options_mock['sbr_settings'];
162 $this->assertSame('', $s['license_status'], 'license_status cleared -> panel surfaces Activate');
163 $this->assertSame('', $s['license_info']);
164 $this->assertSame('ABC-123', $s['license_key'], 'license_key preserved for one-click re-link');
165 $this->assertNotEmpty($wp_options_mock['sbr_license_reconcile_1585_done'], 'done flag set');
166 // #482 review: tier must also be cleared in sbr_statuses, else feature
167 // gating keeps treating the install as paid while Activate is surfaced.
168 $this->assertSame(0, $wp_options_mock['sbr_statuses']['license_tier'], 'license_tier reset so gating drops to free');
169 }
170
171 public function test_run_leaves_healthy_license_untouched(): void
172 {
173 global $wp_options_mock;
174 $wp_options_mock['sbr_settings'] = $this->licensedSettings();
175
176 $routine = $this->routineWithPing(['success' => true, 'pong' => true, 'license_active' => true]);
177 $routine->run();
178
179 $this->assertSame('valid', $wp_options_mock['sbr_settings']['license_status'], 'healthy license untouched');
180 $this->assertNotEmpty($wp_options_mock['sbr_license_reconcile_1585_done'], 'still one-shot: done flag set');
181 }
182
183 public function test_run_does_nothing_on_inconclusive_ping_but_still_marks_done(): void
184 {
185 global $wp_options_mock;
186 $wp_options_mock['sbr_settings'] = $this->licensedSettings();
187
188 $routine = $this->routineWithPing(null); // unreachable / error
189 $routine->run();
190
191 $this->assertSame('valid', $wp_options_mock['sbr_settings']['license_status'], 'never clear on a soft signal');
192 $this->assertNotEmpty($wp_options_mock['sbr_license_reconcile_1585_done'], 'done flag set so we do not retry forever');
193 }
194
195 /**
196 * Real-world migration case: the forked token was later deleted/revoked on
197 * the relay, so the ping is a 401 error envelope (success false, no pong).
198 * The routine must NOT clear a still-"valid" local license on a relay auth
199 * error — that would log the customer out on a transient/relay-side problem.
200 * A dead token is the register/reverify path's job, not this one.
201 */
202 public function test_run_does_not_clear_on_dead_or_revoked_token_error(): void
203 {
204 global $wp_options_mock;
205 $wp_options_mock['sbr_settings'] = $this->licensedSettings();
206 $wp_options_mock['sbr_statuses'] = ['license_tier' => 3];
207
208 $routine = $this->routineWithPing(['success' => false, 'errorId' => 'invalidToken', 'apiMessage' => 'Token is invalid']);
209 $routine->run();
210
211 $this->assertSame('valid', $wp_options_mock['sbr_settings']['license_status'], 'dead-token error must not clear a valid local license');
212 $this->assertSame(3, $wp_options_mock['sbr_statuses']['license_tier'], 'tier untouched on a relay auth error');
213 $this->assertNotEmpty($wp_options_mock['sbr_license_reconcile_1585_done'], 'still one-shot');
214 }
215
216 /**
217 * Real-world case: a backup/migration tool corrupted sbr_settings so it is
218 * no longer an array. Even on a confirmed wedge ping, run() must guard and
219 * no-op rather than fatal on string offset access.
220 */
221 public function test_run_survives_corrupted_non_array_settings_on_wedge(): void
222 {
223 global $wp_options_mock;
224 $wp_options_mock['sbr_settings'] = 'corrupted-not-an-array';
225
226 $routine = $this->routineWithPing(['success' => true, 'pong' => true, 'license_active' => false]);
227 $routine->run(); // must not throw
228
229 $this->assertSame('corrupted-not-an-array', $wp_options_mock['sbr_settings'], 'left untouched, no fatal');
230 $this->assertNotEmpty($wp_options_mock['sbr_license_reconcile_1585_done'], 'still marked done');
231 }
232 }
233