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
Smash1631UpdateSingleLangScopeTest.php
230 lines
| 1 | <?php |
| 2 | |
| 3 | namespace SmashBalloon\Reviews\Tests\Unit; |
| 4 | |
| 5 | use PHPUnit\Framework\TestCase; |
| 6 | use SmashBalloon\Reviews\Common\SinglePostCache as CommonSinglePostCache; |
| 7 | use SmashBalloon\Reviews\Pro\SinglePostCache as ProSinglePostCache; |
| 8 | use SmashBalloon\Reviews\Pro\MediaFinder; |
| 9 | |
| 10 | /** |
| 11 | * SMASH-1631 regression: SinglePostCache::update_single() must scope its UPDATE by |
| 12 | * language. |
| 13 | * |
| 14 | * A Google review_id carries no language, so on a WPML multi-language site the same |
| 15 | * review is cached once per language (unique key: post_id, provider_id, lang). The |
| 16 | * update path matched on post_id ALONE, so one language's update overwrote every |
| 17 | * sibling-language row for that review — collapsing them all onto the last-written |
| 18 | * text (observed: en/fr/ro rows all became the Spanish original after a bulk re-run). |
| 19 | * db_record() — the existence check that gates update_single() — already keys on |
| 20 | * (post_id, lang, provider_id); these tests pin that update_single() now mirrors the |
| 21 | * lang dimension so an update only ever touches the row for its own language. |
| 22 | * |
| 23 | * Covers BOTH the Pro override (the runtime bulk-cron path) and the Common base. |
| 24 | */ |
| 25 | class Smash1631UpdateSingleLangScopeTest extends TestCase |
| 26 | { |
| 27 | /** @var mixed */ |
| 28 | private $previous_wpdb; |
| 29 | |
| 30 | protected function setUp(): void |
| 31 | { |
| 32 | parent::setUp(); |
| 33 | $this->previous_wpdb = $GLOBALS['wpdb'] ?? null; |
| 34 | |
| 35 | // $wpdb double that records the arguments handed to update() so we can assert |
| 36 | // the WHERE clause. Returns 1 (rows affected) so update_single() sees success. |
| 37 | $GLOBALS['wpdb'] = new class { |
| 38 | /** @var string */ |
| 39 | public $prefix = 'wp_'; |
| 40 | /** @var int */ |
| 41 | public $insert_id = 0; |
| 42 | /** @var array<string,mixed>|null */ |
| 43 | public $last_update = null; |
| 44 | |
| 45 | public function update($table, $data, $where, $format = null, $where_format = null) |
| 46 | { |
| 47 | $this->last_update = compact('table', 'data', 'where', 'format', 'where_format'); |
| 48 | return 1; |
| 49 | } |
| 50 | |
| 51 | public function prepare($query, ...$args) |
| 52 | { |
| 53 | return $query; |
| 54 | } |
| 55 | |
| 56 | public function get_results($query, $output = null) |
| 57 | { |
| 58 | return []; |
| 59 | } |
| 60 | }; |
| 61 | } |
| 62 | |
| 63 | protected function tearDown(): void |
| 64 | { |
| 65 | $GLOBALS['wpdb'] = $this->previous_wpdb; |
| 66 | parent::tearDown(); |
| 67 | } |
| 68 | |
| 69 | /** A minimal Google review payload (Google is the only lang-aware provider). */ |
| 70 | private function google_review(string $review_id = 'REVIEW_A'): array |
| 71 | { |
| 72 | return [ |
| 73 | 'review_id' => $review_id, |
| 74 | 'text' => 'Thank you Roya and Genta.', |
| 75 | 'rating' => 5, |
| 76 | 'time' => 1783106917, |
| 77 | 'reviewer' => ['name' => 'Judi Rawlings'], |
| 78 | 'provider' => ['name' => 'google'], |
| 79 | ]; |
| 80 | } |
| 81 | |
| 82 | private function make_pro(array $review): ProSinglePostCache |
| 83 | { |
| 84 | return new ProSinglePostCache($review, new MediaFinder($review)); |
| 85 | } |
| 86 | |
| 87 | public function test_pro_update_single_scopes_where_by_language(): void |
| 88 | { |
| 89 | $cache = $this->make_pro($this->google_review()); |
| 90 | $cache->set_provider_id('place_123'); |
| 91 | $cache->set_lang('es-419'); |
| 92 | $cache->update_single(); |
| 93 | |
| 94 | $where = $GLOBALS['wpdb']->last_update['where']; |
| 95 | $this->assertArrayHasKey('lang', $where, 'update_single() must include lang in its WHERE'); |
| 96 | $this->assertSame('es-419', $where['lang']); |
| 97 | $this->assertSame('REVIEW_A', $where['post_id']); |
| 98 | |
| 99 | // where_format must stay aligned with the where columns or $wpdb->update breaks. |
| 100 | $this->assertCount( |
| 101 | count($where), |
| 102 | $GLOBALS['wpdb']->last_update['where_format'], |
| 103 | 'where_format count must match the number of WHERE columns' |
| 104 | ); |
| 105 | } |
| 106 | |
| 107 | public function test_common_update_single_scopes_where_by_language(): void |
| 108 | { |
| 109 | $cache = new CommonSinglePostCache($this->google_review()); |
| 110 | $cache->set_provider_id('place_123'); |
| 111 | $cache->set_lang('es-419'); |
| 112 | $cache->update_single(); |
| 113 | |
| 114 | $where = $GLOBALS['wpdb']->last_update['where']; |
| 115 | $this->assertSame('es-419', $where['lang']); |
| 116 | $this->assertSame('REVIEW_A', $where['post_id']); |
| 117 | } |
| 118 | |
| 119 | /** |
| 120 | * The core regression: two languages of the SAME review must produce DIFFERENT |
| 121 | * WHERE clauses, so updating one language can never match (and overwrite) the other. |
| 122 | */ |
| 123 | public function test_two_languages_of_same_review_do_not_share_a_where(): void |
| 124 | { |
| 125 | $en = $this->make_pro($this->google_review('REVIEW_A')); |
| 126 | $en->set_provider_id('place_123'); |
| 127 | $en->set_lang('en'); |
| 128 | $en->update_single(); |
| 129 | $en_where = $GLOBALS['wpdb']->last_update['where']; |
| 130 | |
| 131 | $es = $this->make_pro($this->google_review('REVIEW_A')); |
| 132 | $es->set_provider_id('place_123'); |
| 133 | $es->set_lang('es-419'); |
| 134 | $es->update_single(); |
| 135 | $es_where = $GLOBALS['wpdb']->last_update['where']; |
| 136 | |
| 137 | $this->assertSame($en_where['post_id'], $es_where['post_id'], 'same review id'); |
| 138 | $this->assertNotSame( |
| 139 | $en_where['lang'], |
| 140 | $es_where['lang'], |
| 141 | 'the two languages must scope to different rows' |
| 142 | ); |
| 143 | $this->assertSame('en', $en_where['lang']); |
| 144 | $this->assertSame('es-419', $es_where['lang']); |
| 145 | } |
| 146 | |
| 147 | /** |
| 148 | * Backwards-compat: a non-lang provider never calls set_lang(), so lang stays the |
| 149 | * '' default. The WHERE still carries post_id (unchanged) plus lang='' — which |
| 150 | * matches the lang='' rows those providers have always stored, so their update |
| 151 | * behaviour is preserved. |
| 152 | */ |
| 153 | public function test_non_lang_provider_keeps_empty_lang_where(): void |
| 154 | { |
| 155 | $review = [ |
| 156 | 'review_id' => 'YELP_1', |
| 157 | 'text' => 'Great place.', |
| 158 | 'rating' => 4, |
| 159 | 'time' => 1783106917, |
| 160 | 'reviewer' => ['name' => 'Sam'], |
| 161 | 'provider' => ['name' => 'yelp'], |
| 162 | ]; |
| 163 | $cache = $this->make_pro($review); |
| 164 | $cache->set_provider_id('yelp_biz_1'); |
| 165 | // No set_lang() — mirrors the non-lang provider path. |
| 166 | $cache->update_single(); |
| 167 | |
| 168 | $where = $GLOBALS['wpdb']->last_update['where']; |
| 169 | $this->assertSame('YELP_1', $where['post_id']); |
| 170 | $this->assertSame('', $where['lang'], 'non-lang providers scope on the empty-string default'); |
| 171 | } |
| 172 | |
| 173 | /** |
| 174 | * The sibling update() method (used by the media-resize render path, |
| 175 | * Pro/Feed::find_and_resize_media) had the identical post_id-only WHERE and leaked |
| 176 | * the same cross-language overwrite. It must also scope by language. |
| 177 | */ |
| 178 | public function test_pro_update_scopes_where_by_language(): void |
| 179 | { |
| 180 | $cache = $this->make_pro($this->google_review()); |
| 181 | $cache->set_provider_id('place_123'); |
| 182 | $cache->set_lang('es-419'); |
| 183 | $cache->update([ |
| 184 | ['images_done', 1, '%d'], |
| 185 | ['post_content', 'Gracias Roya', '%s'], |
| 186 | ]); |
| 187 | |
| 188 | $where = $GLOBALS['wpdb']->last_update['where']; |
| 189 | $this->assertArrayHasKey('lang', $where, 'update() must include lang in its WHERE'); |
| 190 | $this->assertSame('es-419', $where['lang']); |
| 191 | $this->assertSame('REVIEW_A', $where['post_id']); |
| 192 | $this->assertCount(count($where), $GLOBALS['wpdb']->last_update['where_format']); |
| 193 | } |
| 194 | |
| 195 | public function test_common_update_scopes_where_by_language(): void |
| 196 | { |
| 197 | $cache = new CommonSinglePostCache($this->google_review()); |
| 198 | $cache->set_provider_id('place_123'); |
| 199 | $cache->set_lang('en'); |
| 200 | $cache->update([['images_done', 1, '%d']]); |
| 201 | |
| 202 | $where = $GLOBALS['wpdb']->last_update['where']; |
| 203 | $this->assertSame('en', $where['lang']); |
| 204 | $this->assertSame('REVIEW_A', $where['post_id']); |
| 205 | } |
| 206 | |
| 207 | /** |
| 208 | * Core regression for the render path: resizing the media of one language's row |
| 209 | * must not overwrite a sibling language's row. Two languages of the same review |
| 210 | * produce different update() WHERE clauses. |
| 211 | */ |
| 212 | public function test_update_two_languages_do_not_share_a_where(): void |
| 213 | { |
| 214 | $en = $this->make_pro($this->google_review('REVIEW_A')); |
| 215 | $en->set_provider_id('place_123'); |
| 216 | $en->set_lang('en'); |
| 217 | $en->update([['images_done', 1, '%d']]); |
| 218 | $en_where = $GLOBALS['wpdb']->last_update['where']; |
| 219 | |
| 220 | $es = $this->make_pro($this->google_review('REVIEW_A')); |
| 221 | $es->set_provider_id('place_123'); |
| 222 | $es->set_lang('es-419'); |
| 223 | $es->update([['images_done', 1, '%d']]); |
| 224 | $es_where = $GLOBALS['wpdb']->last_update['where']; |
| 225 | |
| 226 | $this->assertSame($en_where['post_id'], $es_where['post_id']); |
| 227 | $this->assertNotSame($en_where['lang'], $es_where['lang']); |
| 228 | } |
| 229 | } |
| 230 |