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
ForceKeylessRefetchTest.php
223 lines
| 1 | <?php |
| 2 | |
| 3 | namespace SmashBalloon\Reviews\Tests\Unit; |
| 4 | |
| 5 | use PHPUnit\Framework\TestCase; |
| 6 | use SmashBalloon\Reviews\Common\Utils\FreeRetriever as CommonFreeRetriever; |
| 7 | use SmashBalloon\Reviews\Pro\Utils\FreeRetriever as ProFreeRetriever; |
| 8 | |
| 9 | /** |
| 10 | * Pins the keyless "force one-time refetch on Clear All Caches" behaviour. |
| 11 | * |
| 12 | * Background: a keyless Google/Yelp/TripAdvisor/Trustpilot source that already |
| 13 | * has cached reviews is gated by the plugin's local "already fetched" belt |
| 14 | * (FreeRetriever::limit_review_api_call -> SBR_Sources::already_fetched / |
| 15 | * already_fetched_week). Clear All Caches resets the relay's SERVER-side weekly |
| 16 | * window but keeps the cached rows, so before this fix the immediate refresh |
| 17 | * still skipped the keyless relay call and the feed could never refetch without |
| 18 | * manual row deletion. |
| 19 | * |
| 20 | * The fix is a short-lived, TTL-bounded transient flag the belt honours (a |
| 21 | * time window, NOT a single-use token — it is not consumed per call; the |
| 22 | * relay's own weekly window is the real per-source cap). It is NON-destructive |
| 23 | * (cached reviews stay, so a failed refetch keeps the existing feed) and BC-safe |
| 24 | * (when the flag is unset, the belt behaves exactly as before). |
| 25 | * |
| 26 | * Behaviour tests exercise should_force_refetch() directly (it only reads the |
| 27 | * transient, so no $wpdb is needed). The full limit_review_api_call() path hits |
| 28 | * SBR_Sources static DB queries that the plain-PHPUnit suite can't provide, so |
| 29 | * the wiring of `&& ! self::should_force_refetch()` into both belts — and the |
| 30 | * flag-set in clear_all_caches() — is protected by source-level guards, matching |
| 31 | * the ClearCacheRelayResetTest convention. |
| 32 | */ |
| 33 | final class ForceKeylessRefetchTest extends TestCase |
| 34 | { |
| 35 | protected function setUp(): void |
| 36 | { |
| 37 | parent::setUp(); |
| 38 | // Isolate the transient mock between cases. |
| 39 | global $wp_transients_mock; |
| 40 | $wp_transients_mock = []; |
| 41 | } |
| 42 | |
| 43 | protected function tearDown(): void |
| 44 | { |
| 45 | global $wp_transients_mock; |
| 46 | $wp_transients_mock = []; |
| 47 | parent::tearDown(); |
| 48 | } |
| 49 | |
| 50 | private static function invokeShouldForceRefetch(string $class): bool |
| 51 | { |
| 52 | $m = new \ReflectionMethod($class, 'should_force_refetch'); |
| 53 | $m->setAccessible(true); |
| 54 | |
| 55 | return (bool) $m->invoke(null); |
| 56 | } |
| 57 | |
| 58 | private static function commonSource(): string |
| 59 | { |
| 60 | $path = __DIR__ . '/../../class/Common/Utils/FreeRetriever.php'; |
| 61 | self::assertFileExists($path); |
| 62 | |
| 63 | return (string) file_get_contents($path); |
| 64 | } |
| 65 | |
| 66 | private static function proSource(): string |
| 67 | { |
| 68 | $path = __DIR__ . '/../../class/Pro/Utils/FreeRetriever.php'; |
| 69 | self::assertFileExists($path); |
| 70 | |
| 71 | return (string) file_get_contents($path); |
| 72 | } |
| 73 | |
| 74 | private static function saverSource(): string |
| 75 | { |
| 76 | $path = __DIR__ . '/../../class/Common/Builder/SBR_Feed_Saver_Manager.php'; |
| 77 | self::assertFileExists($path); |
| 78 | |
| 79 | return (string) file_get_contents($path); |
| 80 | } |
| 81 | |
| 82 | // ---- Behaviour: should_force_refetch() reflects the transient ---- |
| 83 | |
| 84 | public function test_should_force_refetch_is_false_when_flag_unset(): void |
| 85 | { |
| 86 | $this->assertFalse( |
| 87 | self::invokeShouldForceRefetch(CommonFreeRetriever::class), |
| 88 | 'With no flag set the belt must stay closed (normal/BC behaviour).' |
| 89 | ); |
| 90 | } |
| 91 | |
| 92 | public function test_should_force_refetch_is_true_when_flag_set(): void |
| 93 | { |
| 94 | set_transient(CommonFreeRetriever::FORCE_REFETCH_FLAG, 1, 300); |
| 95 | |
| 96 | $this->assertTrue( |
| 97 | self::invokeShouldForceRefetch(CommonFreeRetriever::class), |
| 98 | 'Setting the flag must open the belt for a one-time refetch.' |
| 99 | ); |
| 100 | } |
| 101 | |
| 102 | public function test_should_force_refetch_is_false_after_flag_deleted(): void |
| 103 | { |
| 104 | set_transient(CommonFreeRetriever::FORCE_REFETCH_FLAG, 1, 300); |
| 105 | delete_transient(CommonFreeRetriever::FORCE_REFETCH_FLAG); |
| 106 | |
| 107 | $this->assertFalse( |
| 108 | self::invokeShouldForceRefetch(CommonFreeRetriever::class), |
| 109 | 'Once the flag expires or is cleared the belt must close again — no permanent bypass.' |
| 110 | ); |
| 111 | } |
| 112 | |
| 113 | public function test_flag_stays_open_for_multiple_reads_within_window(): void |
| 114 | { |
| 115 | // The flag is a TTL window, not a single-use token: it is NOT consumed on |
| 116 | // read, so every source in a multi-source refresh within the window gets |
| 117 | // the bypass. (Cost is bounded by the relay's per-source weekly re-close, |
| 118 | // not by consuming this flag.) Pin that contract so a future "make it |
| 119 | // one-shot" change can't silently break multi-source refetch. |
| 120 | set_transient(CommonFreeRetriever::FORCE_REFETCH_FLAG, 1, 300); |
| 121 | |
| 122 | $this->assertTrue(self::invokeShouldForceRefetch(CommonFreeRetriever::class), 'first read'); |
| 123 | $this->assertTrue(self::invokeShouldForceRefetch(CommonFreeRetriever::class), 'second read'); |
| 124 | $this->assertTrue(self::invokeShouldForceRefetch(ProFreeRetriever::class), 'third read (Pro)'); |
| 125 | } |
| 126 | |
| 127 | public function test_pro_inherits_should_force_refetch_and_the_same_flag(): void |
| 128 | { |
| 129 | // Pro extends Common; self::should_force_refetch() / self::FORCE_REFETCH_FLAG |
| 130 | // in the Pro belt must resolve to the inherited member, not a separate one. |
| 131 | set_transient(CommonFreeRetriever::FORCE_REFETCH_FLAG, 1, 300); |
| 132 | |
| 133 | $this->assertSame( |
| 134 | CommonFreeRetriever::FORCE_REFETCH_FLAG, |
| 135 | ProFreeRetriever::FORCE_REFETCH_FLAG, |
| 136 | 'Pro must share the exact same flag constant as Common.' |
| 137 | ); |
| 138 | $this->assertTrue( |
| 139 | self::invokeShouldForceRefetch(ProFreeRetriever::class), |
| 140 | 'Pro must honour the same flag through the inherited helper.' |
| 141 | ); |
| 142 | } |
| 143 | |
| 144 | // ---- Guard: the flag name survives clear_plugin_cache()'s transient purge ---- |
| 145 | |
| 146 | public function test_flag_uses_sbreviews_prefix_to_survive_cache_purge(): void |
| 147 | { |
| 148 | // clear_plugin_cache() deletes `_transient_sbr_%`. A `sbr_`-prefixed flag |
| 149 | // would be swept immediately; the `sbreviews_` prefix dodges that LIKE. |
| 150 | $flag = CommonFreeRetriever::FORCE_REFETCH_FLAG; |
| 151 | $this->assertStringStartsWith('sbreviews_', $flag); |
| 152 | $this->assertFalse( |
| 153 | str_starts_with($flag, 'sbr_'), |
| 154 | 'Flag must not match the `_transient_sbr_%` purge in clear_plugin_cache().' |
| 155 | ); |
| 156 | } |
| 157 | |
| 158 | // ---- Guards: the belts honour the flag ---- |
| 159 | |
| 160 | public function test_common_belt_honours_force_flag(): void |
| 161 | { |
| 162 | $this->assertMatchesRegularExpression( |
| 163 | '/already_fetched\([^)]*\);\s*\n\s*if \(\$limit_current && ! self::should_force_refetch\(\)\)/', |
| 164 | self::commonSource(), |
| 165 | 'Common limit_review_api_call() must bypass already_fetched when the flag is set.' |
| 166 | ); |
| 167 | } |
| 168 | |
| 169 | public function test_pro_belt_honours_force_flag(): void |
| 170 | { |
| 171 | $this->assertMatchesRegularExpression( |
| 172 | '/already_fetched_week\([^)]*\);\s*\n\s*if \(\$already_fetched && ! self::should_force_refetch\(\)\)/', |
| 173 | self::proSource(), |
| 174 | 'Pro limit_review_api_call() must bypass already_fetched_week when the flag is set.' |
| 175 | ); |
| 176 | } |
| 177 | |
| 178 | // ---- Guards: clear_all_caches() sets the flag, filtered, after the purge, bounded ---- |
| 179 | |
| 180 | public function test_clear_all_caches_sets_the_force_flag(): void |
| 181 | { |
| 182 | $this->assertStringContainsString( |
| 183 | 'FreeRetriever::FORCE_REFETCH_FLAG', |
| 184 | self::saverSource(), |
| 185 | 'Clear All Caches must set the keyless force-refetch flag.' |
| 186 | ); |
| 187 | } |
| 188 | |
| 189 | public function test_force_flag_set_is_filterable(): void |
| 190 | { |
| 191 | $this->assertStringContainsString( |
| 192 | "apply_filters('sbr_force_keyless_refetch_on_clear', true)", |
| 193 | self::saverSource(), |
| 194 | 'The force-refetch flag must be filterable so support can disable it without a redeploy.' |
| 195 | ); |
| 196 | } |
| 197 | |
| 198 | public function test_force_flag_is_set_after_clear_plugin_cache(): void |
| 199 | { |
| 200 | $src = self::saverSource(); |
| 201 | $purgePos = strpos($src, 'self::clear_plugin_cache()'); |
| 202 | $flagPos = strpos($src, 'FreeRetriever::FORCE_REFETCH_FLAG'); |
| 203 | $this->assertNotFalse($purgePos); |
| 204 | $this->assertNotFalse($flagPos); |
| 205 | $this->assertLessThan( |
| 206 | $flagPos, |
| 207 | $purgePos, |
| 208 | 'The flag must be set AFTER clear_plugin_cache() so its transient purge does not remove it.' |
| 209 | ); |
| 210 | } |
| 211 | |
| 212 | public function test_force_flag_has_a_bounded_short_ttl(): void |
| 213 | { |
| 214 | // A permanent (TTL 0 / no expiry) flag would keep the belt open and let |
| 215 | // every render re-call the relay — a cost/loop risk. Pin a bounded TTL. |
| 216 | $this->assertMatchesRegularExpression( |
| 217 | '/set_transient\(\s*\\\\SmashBalloon\\\\Reviews\\\\Common\\\\Utils\\\\FreeRetriever::FORCE_REFETCH_FLAG,\s*1,\s*\d+\s*\*\s*MINUTE_IN_SECONDS\s*\)/', |
| 218 | self::saverSource(), |
| 219 | 'The force-refetch flag must use a bounded MINUTE_IN_SECONDS TTL (one-shot, not permanent).' |
| 220 | ); |
| 221 | } |
| 222 | } |
| 223 |