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
RemoteRequestMemoTest.php
186 lines
| 1 | <?php |
| 2 | |
| 3 | namespace SmashBalloon\Reviews\Tests\Unit; |
| 4 | |
| 5 | use PHPUnit\Framework\TestCase; |
| 6 | use SmashBalloon\Reviews\Common\RemoteRequest; |
| 7 | |
| 8 | /** |
| 9 | * Unit tests for the per-request memoization on RemoteRequest::fetch() |
| 10 | * (SMASH-1360 Phase 2). |
| 11 | * |
| 12 | * Pins the memo-key contract: same `(endpoint, provider, place_id, …)` |
| 13 | * regardless of `source_id` collapses to the same memo key. The wp_lhr_log |
| 14 | * captured on demo-wp2 2026-05-06 showed the customizer firing duplicate |
| 15 | * calls — one with `source_id=N&place_id=X`, one with `place_id=X` only. |
| 16 | * Without the memo, both would have hit the relay (and the relay-side cache |
| 17 | * absorbs the upstream cost but the WP host still pays the HTTP roundtrip). |
| 18 | * With the memo, the second call is served from in-process state. |
| 19 | * |
| 20 | * @group memo |
| 21 | * @group SMASH-1360 |
| 22 | */ |
| 23 | class RemoteRequestMemoTest extends TestCase |
| 24 | { |
| 25 | protected function setUp(): void |
| 26 | { |
| 27 | parent::setUp(); |
| 28 | // Static state — flush between tests so prior runs don't leak. |
| 29 | RemoteRequest::flush_memo(); |
| 30 | } |
| 31 | |
| 32 | private function makeRequest(string $place_id, ?int $relay_source_id = null): RemoteRequest |
| 33 | { |
| 34 | $args = [ |
| 35 | 'business' => $place_id, |
| 36 | 'info' => $relay_source_id !== null ? ['relay_source_id' => $relay_source_id] : [], |
| 37 | ]; |
| 38 | |
| 39 | return new RemoteRequest('google', $args, 'reviews'); |
| 40 | } |
| 41 | |
| 42 | public function test_memo_key_collapses_source_id_and_no_source_id_for_same_place(): void |
| 43 | { |
| 44 | // The two URL shapes captured in the M4 amplifier evidence: |
| 45 | // - /reviews/google?place_id=X&source_id=20 |
| 46 | // - /reviews/google?place_id=X |
| 47 | // Both must produce the SAME memo key — they fetch the same upstream data. |
| 48 | |
| 49 | $with_source_id = $this->makeRequest('CHIJ_TEST', 20); |
| 50 | $without_source_id = $this->makeRequest('CHIJ_TEST', null); |
| 51 | |
| 52 | $args_a = ['place_id' => 'CHIJ_TEST', 'source_id' => 20, 'api_key' => 'KEY']; |
| 53 | $args_b = ['place_id' => 'CHIJ_TEST', 'api_key' => 'KEY']; |
| 54 | |
| 55 | $key_a = $with_source_id->memo_key($args_a); |
| 56 | $key_b = $without_source_id->memo_key($args_b); |
| 57 | |
| 58 | $this->assertNotNull($key_a); |
| 59 | $this->assertNotNull($key_b); |
| 60 | $this->assertSame($key_a, $key_b, 'source_id presence/absence must collapse to same memo key'); |
| 61 | } |
| 62 | |
| 63 | public function test_memo_key_differs_for_different_place_ids(): void |
| 64 | { |
| 65 | $req = $this->makeRequest('CHIJ_AFI'); |
| 66 | |
| 67 | $key_afi = $req->memo_key(['place_id' => 'CHIJ_AFI', 'api_key' => 'K']); |
| 68 | $key_googleplex = $req->memo_key(['place_id' => 'CHIJ_GOOGLEPLEX', 'api_key' => 'K']); |
| 69 | |
| 70 | $this->assertNotSame($key_afi, $key_googleplex, 'Different place_ids MUST get different memo keys'); |
| 71 | } |
| 72 | |
| 73 | public function test_memo_key_differs_for_different_endpoints(): void |
| 74 | { |
| 75 | $reviews = new RemoteRequest('google', ['business' => 'X'], 'reviews'); |
| 76 | $sources = new RemoteRequest('google', ['business' => 'X'], 'sources'); |
| 77 | |
| 78 | $args = ['place_id' => 'X', 'api_key' => 'K']; |
| 79 | |
| 80 | $this->assertNotSame( |
| 81 | $reviews->memo_key($args), |
| 82 | $sources->memo_key($args), |
| 83 | '/reviews/google and /sources/google return different bytes — must NOT share memo' |
| 84 | ); |
| 85 | } |
| 86 | |
| 87 | public function test_memo_key_differs_for_different_providers(): void |
| 88 | { |
| 89 | $google = new RemoteRequest('google', ['business' => 'X'], 'reviews'); |
| 90 | $yelp = new RemoteRequest('yelp', ['business' => 'X'], 'reviews'); |
| 91 | |
| 92 | $args = ['place_id' => 'X', 'api_key' => 'K']; |
| 93 | |
| 94 | $this->assertNotSame( |
| 95 | $google->memo_key($args), |
| 96 | $yelp->memo_key($args), |
| 97 | 'Different providers MUST get different memo keys' |
| 98 | ); |
| 99 | } |
| 100 | |
| 101 | public function test_memo_key_differs_for_different_languages(): void |
| 102 | { |
| 103 | $req = $this->makeRequest('X'); |
| 104 | |
| 105 | $en = $req->memo_key(['place_id' => 'X', 'api_key' => 'K', 'language' => 'en']); |
| 106 | $ro = $req->memo_key(['place_id' => 'X', 'api_key' => 'K', 'language' => 'ro']); |
| 107 | |
| 108 | $this->assertNotSame($en, $ro, 'Language is part of upstream response shape — must differentiate'); |
| 109 | } |
| 110 | |
| 111 | public function test_memo_key_differs_for_different_stars_filters(): void |
| 112 | { |
| 113 | $req = $this->makeRequest('X'); |
| 114 | |
| 115 | $no_filter = $req->memo_key(['place_id' => 'X', 'api_key' => 'K']); |
| 116 | $filtered = $req->memo_key(['place_id' => 'X', 'api_key' => 'K', 'starsFilter' => '5']); |
| 117 | |
| 118 | $this->assertNotSame($no_filter, $filtered); |
| 119 | } |
| 120 | |
| 121 | public function test_memo_key_canonicalizes_param_order(): void |
| 122 | { |
| 123 | $req = $this->makeRequest('X'); |
| 124 | |
| 125 | $a = $req->memo_key(['place_id' => 'X', 'api_key' => 'K', 'language' => 'en']); |
| 126 | $b = $req->memo_key(['language' => 'en', 'api_key' => 'K', 'place_id' => 'X']); |
| 127 | |
| 128 | $this->assertSame($a, $b, 'Memo key derivation MUST be order-independent (ksort applied)'); |
| 129 | } |
| 130 | |
| 131 | public function test_memo_key_returns_null_for_array_typed_arg(): void |
| 132 | { |
| 133 | // Defensive: if anywhere in the args we get an array-typed value |
| 134 | // (e.g., a future caller passes downloads list as args directly), |
| 135 | // refuse to memoize — fall through to a real fetch instead of |
| 136 | // hashing unstably. |
| 137 | $req = $this->makeRequest('X'); |
| 138 | |
| 139 | $key = $req->memo_key([ |
| 140 | 'place_id' => 'X', |
| 141 | 'api_key' => 'K', |
| 142 | 'downloads' => [1, 2, 3], // array-typed value |
| 143 | ]); |
| 144 | |
| 145 | $this->assertNull($key, 'Array-typed args MUST bypass memo (mirrors relay-side EXCLUDED_PARAMS policy)'); |
| 146 | } |
| 147 | |
| 148 | public function test_memo_key_excludes_source_id_only_difference(): void |
| 149 | { |
| 150 | // Direct echo of the wp_lhr_log demo-wp2 evidence — rows 3+5 had |
| 151 | // source_id=20, rows 7+9 didn't. They MUST collapse. |
| 152 | $req = $this->makeRequest('CHIJ_M4'); |
| 153 | |
| 154 | $with_sid = $req->memo_key(['place_id' => 'CHIJ_M4', 'source_id' => 20, 'api_key' => 'K']); |
| 155 | $no_sid = $req->memo_key(['place_id' => 'CHIJ_M4', 'api_key' => 'K']); |
| 156 | |
| 157 | $this->assertSame($with_sid, $no_sid); |
| 158 | } |
| 159 | |
| 160 | public function test_memo_key_includes_api_key_difference(): void |
| 161 | { |
| 162 | // Different api_keys MUST produce different memo keys — this is the |
| 163 | // tenant isolation primitive on the plugin side. Mirrors the |
| 164 | // relay-side `sha256(api_key)` discriminator. |
| 165 | $req = $this->makeRequest('X'); |
| 166 | |
| 167 | $tenant_a = $req->memo_key(['place_id' => 'X', 'api_key' => 'AIza-tenant-a-key']); |
| 168 | $tenant_b = $req->memo_key(['place_id' => 'X', 'api_key' => 'AIza-tenant-b-key']); |
| 169 | |
| 170 | $this->assertNotSame($tenant_a, $tenant_b, 'Different api_keys MUST get different memo keys'); |
| 171 | } |
| 172 | |
| 173 | public function test_flush_memo_clears_state_for_test_isolation(): void |
| 174 | { |
| 175 | // The test-only flush helper must actually empty the memo. |
| 176 | // Verified indirectly via the protected setUp() — if flush didn't |
| 177 | // work, prior-test state would leak and these tests would interact. |
| 178 | // This test exists primarily as an explicit contract pin. |
| 179 | RemoteRequest::flush_memo(); |
| 180 | // No fluent state to check from outside; the assertion is just the |
| 181 | // observation that subsequent tests pass cleanly when they call |
| 182 | // flush_memo() in their own setUp. |
| 183 | $this->assertTrue(true); |
| 184 | } |
| 185 | } |
| 186 |