DuplicatePreventionTest.php
1 week ago
EddItemTemplateBcTest.php
1 week ago
EddProviderGateTest.php
1 week ago
EddTitleRenderingBcTest.php
1 week ago
Smash782AirbnbUrlListingIdTest.php
1 week ago
Smash782BookingUrlHotelIdTest.php
1 week ago
EddTitleRenderingBcTest.php
167 lines
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace SmashBalloon\Reviews\Tests\Unit\Providers; |
| 6 | |
| 7 | use PHPUnit\Framework\TestCase; |
| 8 | |
| 9 | /** |
| 10 | * SMASH-1553 — BC pin for the templates/frontend/post-elements/text.php |
| 11 | * provider-title gate. |
| 12 | * |
| 13 | * The PR adds a review-title render block to the default text template, |
| 14 | * gated by a provider allowlist. The gate exists because |
| 15 | * Util::parse_single_review() at class/Common/Util.php:1421 synthesizes |
| 16 | * a substring fallback title from the review text whenever the upstream |
| 17 | * provider omits one: |
| 18 | * |
| 19 | * 'title' => isset($review['title']) ? $review['title'] : substr($review['text'], 0, 40) |
| 20 | * |
| 21 | * That fallback fires for every review stored via Collection/Import paths |
| 22 | * (PostAggregator::insert_multiple_reviews at PostAggregator.php:458 and |
| 23 | * SBR_Feed_Saver_Manager::import_reviews_collection at the equivalent |
| 24 | * lines). Result: Google / Yelp / Facebook / TripAdvisor / Trustpilot / |
| 25 | * WP.org / WooCommerce / Airbnb / AliExpress reviews stored via Collection |
| 26 | * carry a synthesized "first 40 chars of review text" value in $post['title']. |
| 27 | * |
| 28 | * A naked `!empty($post['title'])` guard in the template would surface that |
| 29 | * synthesized substring as a bold title above the full review text — a |
| 30 | * visible BC regression for every existing Collection feed. |
| 31 | * |
| 32 | * These tests reproduce the gate logic from text.php exactly so future |
| 33 | * edits to the template must touch this helper in lockstep — intentionally |
| 34 | * tight coupling. |
| 35 | */ |
| 36 | class EddTitleRenderingBcTest extends TestCase { |
| 37 | |
| 38 | /** |
| 39 | * Reproduces the gate logic from templates/frontend/post-elements/text.php |
| 40 | * (the `$providers_with_review_titles` allowlist block introduced in |
| 41 | * SMASH-1553). If text.php changes, this helper must change in lockstep. |
| 42 | * |
| 43 | * @param array<string,mixed> $post Shape used by the default text template. |
| 44 | * @return bool Whether the title block should render. |
| 45 | */ |
| 46 | private static function shouldRenderTitle( array $post ): bool { |
| 47 | $providers_with_review_titles = array( 'edd' ); |
| 48 | $post_provider = ! empty( $post['provider']['name'] ) ? $post['provider']['name'] : ''; |
| 49 | return in_array( $post_provider, $providers_with_review_titles, true ) |
| 50 | && ! empty( $post['title'] ) |
| 51 | && is_string( $post['title'] ); |
| 52 | } |
| 53 | |
| 54 | public function test_edd_review_with_real_title_renders(): void { |
| 55 | // EDD's submission form makes the title field required, so every |
| 56 | // real EDD review carries an authoritative customer-typed title. |
| 57 | $post = array( |
| 58 | 'provider' => array( 'name' => 'edd' ), |
| 59 | 'title' => 'Excellent plugin — easy to set up', |
| 60 | 'text' => 'I bought this last week and it just works.', |
| 61 | ); |
| 62 | $this->assertTrue( |
| 63 | self::shouldRenderTitle( $post ), |
| 64 | 'EDD reviews with a real title MUST render the title block.' |
| 65 | ); |
| 66 | } |
| 67 | |
| 68 | public function test_google_review_with_synthesized_title_does_not_render(): void { |
| 69 | // Shape produced by Util::parse_single_review() at Util.php:1421 when |
| 70 | // a Google review is stored via the Collection / Import path: the |
| 71 | // `title` field is synthesized as the first 40 chars of `text`. |
| 72 | // Pre-SMASH-1553, the default template never read `title`, so this |
| 73 | // fake value was harmless. The new title block MUST exclude this |
| 74 | // case via the provider allowlist. |
| 75 | $post = array( |
| 76 | 'provider' => array( 'name' => 'google' ), |
| 77 | 'title' => 'Great service! Will definitely come ba', |
| 78 | 'text' => 'Great service! Will definitely come back next time I am in town.', |
| 79 | ); |
| 80 | $this->assertFalse( |
| 81 | self::shouldRenderTitle( $post ), |
| 82 | 'BC: Google reviews carrying a synthesized substring title (from Util.php:1421) MUST NOT render the title block — that would surface fake duplicated text above the review body.' |
| 83 | ); |
| 84 | } |
| 85 | |
| 86 | public function test_yelp_review_with_synthesized_title_does_not_render(): void { |
| 87 | $post = array( |
| 88 | 'provider' => array( 'name' => 'yelp' ), |
| 89 | 'title' => 'Service was a bit slow but the food m', |
| 90 | 'text' => 'Service was a bit slow but the food more than made up for it.', |
| 91 | ); |
| 92 | $this->assertFalse( self::shouldRenderTitle( $post ), 'BC: Yelp Collection reviews must not render the synthesized title.' ); |
| 93 | } |
| 94 | |
| 95 | public function test_facebook_review_with_synthesized_title_does_not_render(): void { |
| 96 | $post = array( |
| 97 | 'provider' => array( 'name' => 'facebook' ), |
| 98 | 'title' => 'Highly recommend this place — friendly', |
| 99 | 'text' => 'Highly recommend this place — friendly staff and great prices.', |
| 100 | ); |
| 101 | $this->assertFalse( self::shouldRenderTitle( $post ), 'BC: Facebook Collection reviews must not render the synthesized title.' ); |
| 102 | } |
| 103 | |
| 104 | public function test_woocommerce_review_with_empty_title_does_not_render(): void { |
| 105 | // WooCommerce direct-ingest sets `'title' => ''` (WooCommerce.php:298,427). |
| 106 | // Even if WooCommerce were on the allowlist, the empty-string check |
| 107 | // should prevent rendering. |
| 108 | $post = array( |
| 109 | 'provider' => array( 'name' => 'woocommerce' ), |
| 110 | 'title' => '', |
| 111 | 'text' => 'Decent product.', |
| 112 | ); |
| 113 | $this->assertFalse( self::shouldRenderTitle( $post ), 'WooCommerce empty-string title must not render.' ); |
| 114 | } |
| 115 | |
| 116 | public function test_edd_review_with_missing_title_does_not_render(): void { |
| 117 | // Defensive: EDD reviews predating the title field, or rows where |
| 118 | // the meta was somehow lost. Empty/missing title must skip the block. |
| 119 | $post = array( |
| 120 | 'provider' => array( 'name' => 'edd' ), |
| 121 | 'text' => 'No-title legacy EDD review.', |
| 122 | ); |
| 123 | $this->assertFalse( self::shouldRenderTitle( $post ), 'EDD reviews without a title field must not render an empty heading.' ); |
| 124 | |
| 125 | $post['title'] = null; |
| 126 | $this->assertFalse( self::shouldRenderTitle( $post ), 'EDD reviews with null title must not render.' ); |
| 127 | |
| 128 | $post['title'] = ''; |
| 129 | $this->assertFalse( self::shouldRenderTitle( $post ), 'EDD reviews with empty-string title must not render.' ); |
| 130 | } |
| 131 | |
| 132 | public function test_edd_review_with_non_string_title_does_not_render(): void { |
| 133 | // Defense in depth: if any future ingest path stuffs a non-string |
| 134 | // value into `title` (legacy data, dev fixture, schema drift), the |
| 135 | // `is_string()` guard prevents the template's `esc_html()` call from |
| 136 | // fatalling on PHP 8+ (which requires a string argument). |
| 137 | $post = array( |
| 138 | 'provider' => array( 'name' => 'edd' ), |
| 139 | 'title' => array( 'nested', 'shape' ), |
| 140 | 'text' => 'Review text.', |
| 141 | ); |
| 142 | $this->assertFalse( self::shouldRenderTitle( $post ), 'Array title must not render — would fatal on esc_html().' ); |
| 143 | |
| 144 | $post['title'] = (object) array( 'value' => 'x' ); |
| 145 | $this->assertFalse( self::shouldRenderTitle( $post ), 'Object title must not render.' ); |
| 146 | |
| 147 | $post['title'] = 12345; |
| 148 | $this->assertFalse( self::shouldRenderTitle( $post ), 'Integer title must not render — defends against silent string-cast surprises.' ); |
| 149 | } |
| 150 | |
| 151 | public function test_missing_provider_name_does_not_render(): void { |
| 152 | // Defensive: malformed $post with no provider name (unusual, but |
| 153 | // possible in legacy data or partial migrations). |
| 154 | $post = array( |
| 155 | 'title' => 'EDD review title', |
| 156 | 'text' => 'Some text.', |
| 157 | ); |
| 158 | $this->assertFalse( self::shouldRenderTitle( $post ), 'Missing provider.name must not render the title block.' ); |
| 159 | |
| 160 | $post['provider'] = array(); |
| 161 | $this->assertFalse( self::shouldRenderTitle( $post ), 'Empty provider array must not render.' ); |
| 162 | |
| 163 | $post['provider'] = array( 'name' => '' ); |
| 164 | $this->assertFalse( self::shouldRenderTitle( $post ), 'Empty provider name must not render.' ); |
| 165 | } |
| 166 | } |
| 167 |