PluginProbe ʕ •ᴥ•ʔ
Reviews Feed – Add Testimonials and Customer Reviews From Google Reviews, Yelp, TripAdvisor, and More / 2.11.0
Reviews Feed – Add Testimonials and Customer Reviews From Google Reviews, Yelp, TripAdvisor, and More v2.11.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 / Smash1130UsageTrackingHooksTest.php
reviews-feed / tests / Unit Last commit date
Doubles 1 week ago Providers 1 week ago BulkRearmOnGrowthTest.php 1 week ago BulkReviewsUpdateStuckStateTest.php 1 week ago ClearCacheRelayResetTest.php 1 week ago DeleteSourceRelayFailureTest.php 1 week ago ErrorHandlerFalsyOptionTest.php 1 week ago FeedCacheUpdateServiceTest.php 1 week ago FeedMalformedPayloadTest.php 1 week ago ForceKeylessRefetchTest.php 1 week ago LicenseDeactivateStaleStateTest.php 1 week ago MediaFinderMemoTest.php 1 week ago MultiSourceAggregationTest.php 1 week ago ReconcileMigratedLicenseRoutineTest.php 1 week ago ReconcileRemovalTest.php 1 week ago RegisterWebsiteRoutineTest.php 1 week ago RelaySlowEndpointsTest.php 1 week ago RemoteRequestMemoTest.php 1 week ago ReviewAlertHeaderTotalsTest.php 1 week ago ReviewAlertPageTargetingTest.php 1 week ago ReviewAlertStarFillTest.php 1 week ago ShortcodeNeutralizationTest.php 1 week ago SiteMigrationRecoveryTest.php 1 week ago Smash1130UsageTrackingHardeningTest.php 1 week ago Smash1130UsageTrackingHooksTest.php 1 week ago Smash1583HeaderParityTest.php 1 week ago Smash1631MultiLanguageBulkTest.php 1 week ago Smash1631UpdateSingleLangScopeTest.php 1 week ago Smash1706TripAdvisorPlaceIdTest.php 1 week ago Smash1756SchemaServiceTest.php 1 week ago Smash1785AvatarLocalUrlGuardTest.php 1 week ago Smash1785AvatarReHealTest.php 1 week ago Smash1795ReviewTextXssTest.php 1 week ago Smash1835TripAdvisorKeyShapeTest.php 1 week ago Smash1973WordpressOrgPlaceIdNullTest.php 1 week ago Smash1987NestedSourceErrorShapeTest.php 1 week ago Smash782BookingHeaderRatingTest.php 1 week ago Smash782CountryFlagEmojiTest.php 1 week ago Smash782ExternalRefreshCronTest.php 1 week ago Smash782ExtrasTemplateTest.php 1 week ago Smash782ReviewAlertProviderDataTest.php 1 week ago SourceIdLookupTest.php 1 week ago WpmlGetCurrentLanguageTest.php 1 week ago WpmlLanguageMappingTest.php 1 week ago
Smash1130UsageTrackingHooksTest.php
255 lines
1 <?php
2
3 namespace SmashBalloon\Reviews\Tests\Unit;
4
5 use PHPUnit\Framework\TestCase;
6 use SmashBalloon\Reviews\Common\UsageTracking\Config;
7 use SmashBalloon\Reviews\Common\UsageTracking\EventRecorder;
8 use SmashBalloon\Reviews\Common\UsageTracking\SmashUsageTracking;
9
10 /**
11 * Regression tests for SMASH-1130 review findings.
12 *
13 * 1. The usage-tracking cron callback must be attached whenever the service
14 * container registers the tracker. On Pro, Pro\ServiceContainer::register()
15 * overrode Common's without calling parent::register() — the only place
16 * SmashUsageTracking is registered — so no Pro site ever attached
17 * Config::CRON_HOOK and zero reports were sent, while the settings toggle
18 * still scheduled a cron event with no callback. These tests pin
19 * register()'s hook wiring; the container test asserts the parent call.
20 *
21 * 2. The event listeners run at wp_ajax priority 5 — BEFORE the primary
22 * handler's nonce/capability checks — so they must replicate those checks
23 * (non-dying) before writing options, or any logged-in user can inflate
24 * event counters via bare admin-ajax POSTs.
25 *
26 * 3. Provider-specific source_connected events derive the provider from the
27 * hook name: most add-source endpoints never send $_POST['provider'].
28 *
29 * @group SMASH-1130
30 * @covers \SmashBalloon\Reviews\Common\UsageTracking\SmashUsageTracking
31 */
32 class Smash1130UsageTrackingHooksTest extends TestCase
33 {
34 protected function setUp(): void
35 {
36 parent::setUp();
37 global $wp_options_mock;
38 $wp_options_mock = ['sbr_settings' => ['usagetracking' => true]];
39 $GLOBALS['sbr_test_actions'] = [];
40 $GLOBALS['sbr_test_nonce_ok'] = false;
41 $GLOBALS['sbr_test_user_can'] = false;
42 $GLOBALS['sbr_test_current_action'] = '';
43 $GLOBALS['sbr_test_nonce_actions_checked'] = [];
44 $GLOBALS['sbr_test_caps_checked'] = [];
45 $_POST = [];
46 }
47
48 protected function tearDown(): void
49 {
50 global $wp_options_mock;
51 $wp_options_mock = [];
52 $GLOBALS['sbr_test_actions'] = [];
53 $_POST = [];
54 parent::tearDown();
55 }
56
57 public function testRegisterAttachesCronCallback(): void
58 {
59 (new SmashUsageTracking())->register();
60
61 $this->assertArrayHasKey(
62 Config::CRON_HOOK,
63 $GLOBALS['sbr_test_actions'],
64 'Cron hook must have a callback attached, or scheduled events fire into the void.'
65 );
66 $this->assertSame('send_checkin', $GLOBALS['sbr_test_actions'][Config::CRON_HOOK][0]['callback'][1]);
67 }
68
69 public function testSourceListenersAttachAtPriorityFive(): void
70 {
71 // The whole guard design depends on the listeners running BEFORE the
72 // primary handler (priority 10) — pin the priority explicitly.
73 (new SmashUsageTracking())->register();
74
75 $reg = $GLOBALS['sbr_test_actions']['wp_ajax_sbr_feed_saver_manager_add_source'][0];
76 $this->assertSame(5, $reg['priority']);
77 $this->assertSame('on_source_connected', $reg['callback'][1]);
78 }
79
80 public function testGuardChecksTheSbrAdminNonceAction(): void
81 {
82 $GLOBALS['sbr_test_nonce_ok'] = 1;
83 $GLOBALS['sbr_test_user_can'] = true;
84
85 $tracking = new SmashUsageTracking();
86 $tracking->on_feed_saved();
87
88 $this->assertContains(
89 'sbr-admin',
90 $GLOBALS['sbr_test_nonce_actions_checked'],
91 'The guard must verify the same nonce action the primary handlers use.'
92 );
93 }
94
95 public function testRegisterAttachesAllSourceConnectionHooks(): void
96 {
97 (new SmashUsageTracking())->register();
98
99 $expected = [
100 'wp_ajax_sbr_feed_saver_manager_add_source',
101 'wp_ajax_sbr_feed_saver_manager_add_facebook_source',
102 'wp_ajax_sbr_feed_saver_manager_add_facebook_souce',
103 'wp_ajax_sbr_feed_saver_manager_connect_manual_facebook',
104 'wp_ajax_sbr_add_woocommerce_source',
105 'wp_ajax_sbr_add_woocommerce_source_multi',
106 'wp_ajax_sbr_add_edd_source',
107 'wp_ajax_sbr_add_edd_source_multi',
108 'wp_ajax_sbr_add_airbnb_source',
109 'wp_ajax_sbr_add_booking_source',
110 'wp_ajax_sbr_add_aliexpress_source',
111 'wp_ajax_sbr_add_external_source',
112 ];
113 foreach ($expected as $hook) {
114 $this->assertArrayHasKey($hook, $GLOBALS['sbr_test_actions'], "Missing listener for {$hook}");
115 }
116 }
117
118 public function testProServiceContainerCallsParentRegister(): void
119 {
120 // Pro\ServiceContainer::register() must reach Common's register(),
121 // whose service loop registers SmashUsageTracking. Asserted structurally
122 // (source inspection) because registering the full container would
123 // boot ~40 services with un-stubbed WP dependencies.
124 $method = new \ReflectionMethod(\SmashBalloon\Reviews\Pro\ServiceContainer::class, 'register');
125 $file = $method->getFileName();
126 $lines = array_slice(
127 file($file),
128 $method->getStartLine() - 1,
129 $method->getEndLine() - $method->getStartLine() + 1
130 );
131 // Strip comments so a commented-out call cannot satisfy the assertion. Block
132 // comments go first and across lines: stripping `//` alone left
133 // `/* parent::register(); */` matching, which was measured passing while the
134 // real call was commented out.
135 $body = (string) preg_replace('#/\*.*?\*/#s', '', implode('', $lines));
136 $body = implode('', array_map(static function ($line) {
137 return preg_replace('/(\/\/|#).*$/', '', $line);
138 }, explode("\n", $body)));
139
140 $this->assertMatchesRegularExpression(
141 '/parent::register\(\)\s*;/',
142 $body,
143 'Pro\ServiceContainer::register() must call parent::register(); it is the only path that registers SmashUsageTracking.'
144 );
145 }
146
147 public function testListenerRejectsRequestWithoutValidNonce(): void
148 {
149 $GLOBALS['sbr_test_nonce_ok'] = false;
150 $GLOBALS['sbr_test_user_can'] = true;
151
152 $tracking = new SmashUsageTracking();
153 $tracking->on_feed_saved();
154 $tracking->on_source_connected();
155
156 global $wp_options_mock;
157 $this->assertArrayNotHasKey(
158 EventRecorder::OPTION_NAME,
159 $wp_options_mock,
160 'A request failing the nonce check must not write event options.'
161 );
162 }
163
164 public function testListenerRejectsRequestWithoutCapability(): void
165 {
166 $GLOBALS['sbr_test_nonce_ok'] = 1;
167 $GLOBALS['sbr_test_user_can'] = false;
168
169 $tracking = new SmashUsageTracking();
170 $tracking->on_feed_saved();
171
172 global $wp_options_mock;
173 $this->assertArrayNotHasKey(EventRecorder::OPTION_NAME, $wp_options_mock);
174 }
175
176 public function testVerifiedRequestRecordsFeedSaved(): void
177 {
178 $GLOBALS['sbr_test_nonce_ok'] = 1;
179 $GLOBALS['sbr_test_user_can'] = true;
180
181 $tracking = new SmashUsageTracking();
182 $tracking->on_feed_saved();
183
184 global $wp_options_mock;
185 $this->assertSame(1, $wp_options_mock[EventRecorder::OPTION_NAME]['feed_saved']['count']);
186 }
187
188 public function testProviderDerivedFromHookNameWithoutPostField(): void
189 {
190 $GLOBALS['sbr_test_nonce_ok'] = 1;
191 $GLOBALS['sbr_test_user_can'] = true;
192 $GLOBALS['sbr_test_current_action'] = 'wp_ajax_sbr_add_airbnb_source';
193
194 $tracking = new SmashUsageTracking();
195 $tracking->on_source_connected();
196
197 global $wp_options_mock;
198 $events = $wp_options_mock[EventRecorder::OPTION_NAME];
199 $this->assertSame(1, $events['source_connected']['count']);
200 $this->assertSame(1, $events['source_connected_airbnb']['count'], 'Airbnb sends no $_POST[provider]; the hook name must supply it.');
201 }
202
203 public function testResetAfterSendPreservesSessionsRecordedDuringSendWindow(): void
204 {
205 global $wp_options_mock;
206 // [10,20,30] existed when the payload was built; 99 arrived while the
207 // request was in flight. Only the reported three may be dropped.
208 $wp_options_mock[Config::OPTION_SESSION_DURATIONS] = [10, 20, 30, 99];
209
210 $tracking = new SmashUsageTracking();
211 $method = new \ReflectionMethod($tracking, 'reset_events_after_send');
212 $method->invoke($tracking, [], [10, 20, 30]);
213
214 $this->assertSame(
215 [99],
216 $wp_options_mock[Config::OPTION_SESSION_DURATIONS],
217 'Sessions recorded during the send window must roll over, not be wiped.'
218 );
219 }
220
221 public function testResetAfterSendSurvivesTheTenEntryCap(): void
222 {
223 global $wp_options_mock;
224 // 10 durations were reported; 3 more arrived during the send and the
225 // store's keep-last-10 cap displaced the 3 oldest reported entries.
226 // A count-based slice would wrongly delete the 3 new sessions here.
227 $reported = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
228 $wp_options_mock[Config::OPTION_SESSION_DURATIONS] = [4, 5, 6, 7, 8, 9, 10, 101, 102, 103];
229
230 $tracking = new SmashUsageTracking();
231 $method = new \ReflectionMethod($tracking, 'reset_events_after_send');
232 $method->invoke($tracking, [], $reported);
233
234 $this->assertSame(
235 [101, 102, 103],
236 $wp_options_mock[Config::OPTION_SESSION_DURATIONS],
237 'The multiset diff must keep unreported sessions even when the cap displaced reported ones.'
238 );
239 }
240
241 public function testGenericHookFallsBackToVerifiedPostProvider(): void
242 {
243 $GLOBALS['sbr_test_nonce_ok'] = 1;
244 $GLOBALS['sbr_test_user_can'] = true;
245 $GLOBALS['sbr_test_current_action'] = 'wp_ajax_sbr_feed_saver_manager_add_source';
246 $_POST['provider'] = 'google';
247
248 $tracking = new SmashUsageTracking();
249 $tracking->on_source_connected();
250
251 global $wp_options_mock;
252 $this->assertSame(1, $wp_options_mock[EventRecorder::OPTION_NAME]['source_connected_google']['count']);
253 }
254 }
255