Doubles
2 months ago
Providers
2 months ago
BulkReviewsUpdateStuckStateTest.php
2 months ago
ClearCacheRelayResetTest.php
2 months ago
DeleteSourceRelayFailureTest.php
2 months ago
ErrorHandlerFalsyOptionTest.php
2 months ago
FeedCacheUpdateServiceTest.php
2 months ago
FeedMalformedPayloadTest.php
2 months ago
ForceKeylessRefetchTest.php
2 months ago
LicenseDeactivateStaleStateTest.php
2 months ago
MediaFinderMemoTest.php
2 months ago
MultiSourceAggregationTest.php
2 months ago
ReconcileMigratedLicenseRoutineTest.php
2 months ago
ReconcileRemovalTest.php
2 months ago
RegisterWebsiteRoutineTest.php
2 months ago
RemoteRequestMemoTest.php
2 months ago
ReviewAlertHeaderTotalsTest.php
2 months ago
ReviewAlertPageTargetingTest.php
2 months ago
ReviewAlertStarFillTest.php
2 months ago
ShortcodeNeutralizationTest.php
2 months ago
SiteMigrationRecoveryTest.php
2 months ago
Smash1583HeaderParityTest.php
2 months ago
SourceIdLookupTest.php
2 months ago
WpmlGetCurrentLanguageTest.php
2 months ago
WpmlLanguageMappingTest.php
2 months ago
FeedCacheUpdateServiceTest.php
166 lines
| 1 | <?php |
| 2 | |
| 3 | namespace SmashBalloon\Reviews\Tests\Unit; |
| 4 | |
| 5 | require_once __DIR__ . '/Doubles/FeedCacheWpdbDouble.php'; |
| 6 | |
| 7 | use PHPUnit\Framework\TestCase; |
| 8 | use SmashBalloon\Reviews\Common\Services\FeedCacheUpdateService; |
| 9 | use SmashBalloon\Reviews\Tests\Unit\Doubles\FeedCacheWpdbDouble; |
| 10 | |
| 11 | /** |
| 12 | * Regression coverage for the cron feed-cache batch query. |
| 13 | * |
| 14 | * Pins the dedupe + oldest-first behavior introduced for SMASH-1360 |
| 15 | * (cron Google Places API cost overhead). The same `feed_caches_query()` |
| 16 | * was previously a `SELECT * ... ORDER BY last_updated ASC LIMIT N` which |
| 17 | * returned one row per `cache_type` per feed — so a feed with both `posts` |
| 18 | * and `header` rows ≥12h-stale contributed two rows to the batch and got |
| 19 | * fully re-fetched twice in a single cron tick. The fix collapses the |
| 20 | * batch to one row per `feed_id` so the same `LIMIT N` budget now yields |
| 21 | * N unique feeds (instead of as few as N/2 when both rows were stale). |
| 22 | * |
| 23 | * The query is exercised through a `wpdb` test double that captures the |
| 24 | * prepared SQL + bound args without requiring a live database. |
| 25 | */ |
| 26 | class FeedCacheUpdateServiceTest extends TestCase |
| 27 | { |
| 28 | protected function setUp(): void |
| 29 | { |
| 30 | parent::setUp(); |
| 31 | global $wpdb; |
| 32 | $wpdb = new FeedCacheWpdbDouble(); |
| 33 | } |
| 34 | |
| 35 | /** GROUP BY feed_id is present and on `feed_id` (not `cache_type`). */ |
| 36 | public function test_query_groups_by_feed_id(): void |
| 37 | { |
| 38 | $service = new FeedCacheUpdateService(); |
| 39 | $service->feed_caches_query(['cron_update' => true]); |
| 40 | |
| 41 | global $wpdb; |
| 42 | $sql = $wpdb->last_prepared_sql; |
| 43 | |
| 44 | $this->assertStringContainsString('GROUP BY feed_id', $sql); |
| 45 | $this->assertStringNotContainsString('GROUP BY cache_type', $sql); |
| 46 | } |
| 47 | |
| 48 | /** Oldest-stale-first prioritization is preserved through the GROUP BY. */ |
| 49 | public function test_query_orders_by_last_updated_ascending(): void |
| 50 | { |
| 51 | $service = new FeedCacheUpdateService(); |
| 52 | $service->feed_caches_query(['cron_update' => true]); |
| 53 | |
| 54 | global $wpdb; |
| 55 | $sql = $wpdb->last_prepared_sql; |
| 56 | |
| 57 | $this->assertStringContainsString('ORDER BY last_updated ASC', $sql); |
| 58 | } |
| 59 | |
| 60 | /** |
| 61 | * MIN(id) / MIN(last_updated) keep the query strict-mode safe under |
| 62 | * `ONLY_FULL_GROUP_BY` (every non-aggregate column must appear in |
| 63 | * GROUP BY or be wrapped in an aggregate function). |
| 64 | */ |
| 65 | public function test_query_uses_aggregates_for_strict_sql_mode_safety(): void |
| 66 | { |
| 67 | $service = new FeedCacheUpdateService(); |
| 68 | $service->feed_caches_query(['cron_update' => true]); |
| 69 | |
| 70 | global $wpdb; |
| 71 | $sql = $wpdb->last_prepared_sql; |
| 72 | |
| 73 | $this->assertStringContainsString('MIN(id)', $sql); |
| 74 | $this->assertStringContainsString('MIN(last_updated)', $sql); |
| 75 | } |
| 76 | |
| 77 | /** SELECT * is gone — the previous shape is what produced the duplicates. */ |
| 78 | public function test_query_no_longer_uses_select_star(): void |
| 79 | { |
| 80 | $service = new FeedCacheUpdateService(); |
| 81 | $service->feed_caches_query(['cron_update' => true]); |
| 82 | |
| 83 | global $wpdb; |
| 84 | $sql = $wpdb->last_prepared_sql; |
| 85 | |
| 86 | $this->assertStringNotContainsString('SELECT *', $sql); |
| 87 | } |
| 88 | |
| 89 | /** 12h staleness gate (`last_updated < NOW - 12h`) is still bound. */ |
| 90 | public function test_query_includes_twelve_hour_staleness_filter(): void |
| 91 | { |
| 92 | $service = new FeedCacheUpdateService(); |
| 93 | $service->feed_caches_query(['cron_update' => true]); |
| 94 | |
| 95 | global $wpdb; |
| 96 | |
| 97 | $this->assertStringContainsString('last_updated < %s', $wpdb->last_prepared_sql); |
| 98 | // First bound arg is the 12h-ago timestamp string (gmdate format). |
| 99 | $this->assertMatchesRegularExpression( |
| 100 | '/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', |
| 101 | (string) $wpdb->last_prepared_args[0] |
| 102 | ); |
| 103 | } |
| 104 | |
| 105 | /** LIMIT is bound to RESULTS_PER_CRON_UPDATE — not a hard-coded literal. */ |
| 106 | public function test_query_limit_is_bound_to_results_per_cron_update(): void |
| 107 | { |
| 108 | $service = new FeedCacheUpdateService(); |
| 109 | $service->feed_caches_query(['cron_update' => true]); |
| 110 | |
| 111 | global $wpdb; |
| 112 | |
| 113 | $this->assertStringContainsString('LIMIT %d', $wpdb->last_prepared_sql); |
| 114 | $this->assertSame( |
| 115 | FeedCacheUpdateService::RESULTS_PER_CRON_UPDATE, |
| 116 | $wpdb->last_prepared_args[1] |
| 117 | ); |
| 118 | } |
| 119 | |
| 120 | /** `cron_update = 'yes'` filter is preserved (already passing pre-fix, regression-pin). */ |
| 121 | public function test_query_filters_to_cron_update_yes_rows(): void |
| 122 | { |
| 123 | $service = new FeedCacheUpdateService(); |
| 124 | $service->feed_caches_query(['cron_update' => true]); |
| 125 | |
| 126 | global $wpdb; |
| 127 | |
| 128 | $this->assertStringContainsString("cron_update = 'yes'", $wpdb->last_prepared_sql); |
| 129 | } |
| 130 | |
| 131 | /** |
| 132 | * End-to-end behavior: when wpdb returns the post-GROUP-BY shape (one |
| 133 | * row per feed_id with MIN-aliased columns), the service hands the |
| 134 | * same array back unchanged. FeedCacheUpdater::do_updates only reads |
| 135 | * `feed_id` from each row, so the alias projection is invisible to it. |
| 136 | */ |
| 137 | public function test_query_returns_aggregated_rows_unchanged(): void |
| 138 | { |
| 139 | global $wpdb; |
| 140 | $wpdb->next_results = [ |
| 141 | ['id' => 17, 'feed_id' => 4, 'last_updated' => '2026-05-04 02:00:00'], |
| 142 | ['id' => 23, 'feed_id' => 7, 'last_updated' => '2026-05-04 03:30:00'], |
| 143 | ]; |
| 144 | |
| 145 | $service = new FeedCacheUpdateService(); |
| 146 | $rows = $service->feed_caches_query(['cron_update' => true]); |
| 147 | |
| 148 | $this->assertCount(2, $rows); |
| 149 | $this->assertSame(4, $rows[0]['feed_id']); |
| 150 | $this->assertSame(7, $rows[1]['feed_id']); |
| 151 | } |
| 152 | |
| 153 | /** When `cron_update` arg is omitted the bare `SELECT *` is still issued (legacy shape). */ |
| 154 | public function test_query_without_cron_update_arg_keeps_select_star_legacy_shape(): void |
| 155 | { |
| 156 | $service = new FeedCacheUpdateService(); |
| 157 | $service->feed_caches_query([]); |
| 158 | |
| 159 | global $wpdb; |
| 160 | |
| 161 | // Legacy shape (no `cron_update` arg) does NOT go through prepare — |
| 162 | // the test double records the raw SQL on `last_get_results_sql`. |
| 163 | $this->assertStringContainsString('SELECT * FROM', $wpdb->last_get_results_sql); |
| 164 | } |
| 165 | } |
| 166 |