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 |