Doubles
1 week ago
Providers
1 week ago
BulkRearmOnGrowthTest.php
1 week ago
BulkReviewsUpdateStuckStateTest.php
1 week ago
ClearCacheRelayResetTest.php
1 week ago
DeleteSourceRelayFailureTest.php
1 week ago
ErrorHandlerFalsyOptionTest.php
1 week ago
FeedCacheUpdateServiceTest.php
1 week ago
FeedMalformedPayloadTest.php
1 week ago
ForceKeylessRefetchTest.php
1 week ago
LicenseDeactivateStaleStateTest.php
1 week ago
MediaFinderMemoTest.php
1 week ago
MultiSourceAggregationTest.php
1 week ago
ReconcileMigratedLicenseRoutineTest.php
1 week ago
ReconcileRemovalTest.php
1 week ago
RegisterWebsiteRoutineTest.php
1 week ago
RelaySlowEndpointsTest.php
1 week ago
RemoteRequestMemoTest.php
1 week ago
ReviewAlertHeaderTotalsTest.php
1 week ago
ReviewAlertPageTargetingTest.php
1 week ago
ReviewAlertStarFillTest.php
1 week ago
ShortcodeNeutralizationTest.php
1 week ago
SiteMigrationRecoveryTest.php
1 week ago
Smash1130UsageTrackingHardeningTest.php
1 week ago
Smash1130UsageTrackingHooksTest.php
1 week ago
Smash1583HeaderParityTest.php
1 week ago
Smash1631MultiLanguageBulkTest.php
1 week ago
Smash1631UpdateSingleLangScopeTest.php
1 week ago
Smash1706TripAdvisorPlaceIdTest.php
1 week ago
Smash1756SchemaServiceTest.php
1 week ago
Smash1785AvatarLocalUrlGuardTest.php
1 week ago
Smash1785AvatarReHealTest.php
1 week ago
Smash1795ReviewTextXssTest.php
1 week ago
Smash1835TripAdvisorKeyShapeTest.php
1 week ago
Smash1973WordpressOrgPlaceIdNullTest.php
1 week ago
Smash1987NestedSourceErrorShapeTest.php
1 week ago
Smash782BookingHeaderRatingTest.php
1 week ago
Smash782CountryFlagEmojiTest.php
1 week ago
Smash782ExternalRefreshCronTest.php
1 week ago
Smash782ExtrasTemplateTest.php
1 week ago
Smash782ReviewAlertProviderDataTest.php
1 week ago
SourceIdLookupTest.php
1 week ago
WpmlGetCurrentLanguageTest.php
1 week ago
WpmlLanguageMappingTest.php
1 week ago
Smash1987NestedSourceErrorShapeTest.php
220 lines
| 1 | <?php |
| 2 | |
| 3 | namespace SmashBalloon\Reviews\Tests\Unit; |
| 4 | |
| 5 | use PHPUnit\Framework\TestCase; |
| 6 | use SmashBalloon\Reviews\Common\Builder\SBR_Feed_Saver_Manager; |
| 7 | |
| 8 | /** |
| 9 | * SMASH-1987 — a relay source error can arrive as an object, and every check in |
| 10 | * process_source_apikey() compared it to a string. |
| 11 | * |
| 12 | * Captured verbatim from staging on 2026-08-25, adding a TripAdvisor source with an |
| 13 | * invalid key. Note the HTTP 200 and envelope `success: true`: the only failure signal |
| 14 | * is inside `info`. |
| 15 | * |
| 16 | * {"message":"OK","success":true,"data":{"info":{ |
| 17 | * "errorId":"sourceConnectionError", |
| 18 | * "error":{"provider":"tripadvisor","message":"Please make sure you have provided |
| 19 | * the right API Key.","reason":"Invalid API Key","error":"invalidKey"}}}} |
| 20 | * |
| 21 | * Consequences before the fix, all measured: the error gate missed it, `$checkValidKey` |
| 22 | * evaluated TRUE because an array is never equal to 'invalidKey' (so a bad key would be |
| 23 | * stored as valid), `apikey => invalid` was never set, and the response carried no |
| 24 | * message — indistinguishable from success to the modal. |
| 25 | */ |
| 26 | final class Smash1987NestedSourceErrorShapeTest extends TestCase |
| 27 | { |
| 28 | /** |
| 29 | * @return array<string,mixed> |
| 30 | */ |
| 31 | private static function nestedShape(): array |
| 32 | { |
| 33 | return [ |
| 34 | 'info' => [ |
| 35 | 'errorId' => 'sourceConnectionError', |
| 36 | 'error' => [ |
| 37 | 'provider' => 'tripadvisor', |
| 38 | 'message' => 'Please make sure you have provided the right API Key.', |
| 39 | 'status' => 'User is not authorized to access this resource with an explicit deny in an identity-based policy', |
| 40 | 'placeId' => '13871137', |
| 41 | 'reason' => 'Invalid API Key', |
| 42 | 'error' => 'invalidKey', |
| 43 | ], |
| 44 | ], |
| 45 | ]; |
| 46 | } |
| 47 | |
| 48 | public function test_the_code_is_read_out_of_the_nested_object(): void |
| 49 | { |
| 50 | $this->assertSame('invalidKey', SBR_Feed_Saver_Manager::get_source_error_code(self::nestedShape())); |
| 51 | } |
| 52 | |
| 53 | public function test_the_message_is_read_out_of_the_nested_object(): void |
| 54 | { |
| 55 | $this->assertSame( |
| 56 | 'Please make sure you have provided the right API Key.', |
| 57 | SBR_Feed_Saver_Manager::get_source_error_message(self::nestedShape()) |
| 58 | ); |
| 59 | } |
| 60 | |
| 61 | /** |
| 62 | * The inversion that made a bad key look good. Kept as an explicit assertion |
| 63 | * because it is the part with a write side effect. |
| 64 | */ |
| 65 | public function test_a_nested_invalid_key_is_not_treated_as_a_valid_key(): void |
| 66 | { |
| 67 | $info = self::nestedShape(); |
| 68 | $error_code = SBR_Feed_Saver_Manager::get_source_error_code($info); |
| 69 | |
| 70 | $checkValidKey = ( |
| 71 | $error_code !== null && $error_code !== 'invalidKey' |
| 72 | ) |
| 73 | || !empty($info['info']['id']) |
| 74 | || !empty($info['info']['successId']); |
| 75 | |
| 76 | $this->assertFalse($checkValidKey, 'an invalid key must never satisfy the store-the-key gate'); |
| 77 | |
| 78 | // The pre-fix expression, pinned so a revert fails here rather than in production. |
| 79 | $old = ( |
| 80 | !empty($info['info']['error']) && $info['info']['error'] !== 'invalidKey' |
| 81 | ) |
| 82 | || !empty($info['info']['id']); |
| 83 | $this->assertTrue($old, 'if this is false the shape changed and this test is moot'); |
| 84 | } |
| 85 | |
| 86 | /** |
| 87 | * BACKWARDS COMPATIBILITY (Rule 6) — the string shape other providers send must |
| 88 | * resolve exactly as it did, so this fix cannot alter their behaviour. |
| 89 | * |
| 90 | * @dataProvider stringShapeProvider |
| 91 | */ |
| 92 | public function test_string_shapes_are_unchanged(string $code): void |
| 93 | { |
| 94 | $this->assertSame( |
| 95 | $code, |
| 96 | SBR_Feed_Saver_Manager::get_source_error_code(['info' => ['error' => $code]]) |
| 97 | ); |
| 98 | } |
| 99 | |
| 100 | /** |
| 101 | * @return array<string,array{0:string}> |
| 102 | */ |
| 103 | public static function stringShapeProvider(): array |
| 104 | { |
| 105 | return [ |
| 106 | 'invalidKey' => ['invalidKey'], |
| 107 | 'invalidLocation' => ['invalidLocation'], |
| 108 | 'someFutureCode' => ['someFutureCode'], |
| 109 | ]; |
| 110 | } |
| 111 | |
| 112 | /** |
| 113 | * @dataProvider noErrorProvider |
| 114 | * @param array<string,mixed> $info |
| 115 | */ |
| 116 | public function test_a_successful_response_reports_no_error(array $info): void |
| 117 | { |
| 118 | $this->assertNull(SBR_Feed_Saver_Manager::get_source_error_code($info)); |
| 119 | } |
| 120 | |
| 121 | /** |
| 122 | * @return array<string,array{0:array<string,mixed>}> |
| 123 | */ |
| 124 | public static function noErrorProvider(): array |
| 125 | { |
| 126 | return [ |
| 127 | 'source added' => [['info' => ['id' => '13871137', 'name' => 'Hotel Colline de France']]], |
| 128 | 'empty error key' => [['info' => ['id' => '1', 'error' => '']]], |
| 129 | 'no info at all' => [[]], |
| 130 | 'info not array' => [['info' => 'unexpected']], |
| 131 | ]; |
| 132 | } |
| 133 | |
| 134 | /** |
| 135 | * An errorId alone is failure, but not a NAMED failure. |
| 136 | * |
| 137 | * The first version of this fix returned the errorId from get_source_error_code(), |
| 138 | * which fed the gate deciding whether to store a submitted key. An errorId is never |
| 139 | * literally 'invalidKey', so that gate opened and an invalid key was persisted as |
| 140 | * valid — worse than the bug being fixed, and a regression against the pre-fix code, |
| 141 | * where an absent info.error left the gate shut. |
| 142 | */ |
| 143 | public function test_an_error_id_alone_is_not_a_named_code(): void |
| 144 | { |
| 145 | $info = ['info' => ['errorId' => 'sourceConnectionError']]; |
| 146 | |
| 147 | $this->assertNull( |
| 148 | SBR_Feed_Saver_Manager::get_source_error_code($info), |
| 149 | 'an opaque errorId must not present itself as a specific error code' |
| 150 | ); |
| 151 | $this->assertSame( |
| 152 | 'sourceConnectionError', |
| 153 | SBR_Feed_Saver_Manager::get_source_error_id($info), |
| 154 | 'but it must still be reportable as a failure' |
| 155 | ); |
| 156 | } |
| 157 | |
| 158 | /** |
| 159 | * The gate itself, pinned. This is the expression in process_source_apikey() that |
| 160 | * decides whether update_provider_apikey() runs. |
| 161 | * |
| 162 | * @dataProvider gateProvider |
| 163 | * @param array<string,mixed> $info |
| 164 | */ |
| 165 | public function test_only_a_named_non_key_error_may_store_the_key(array $info, bool $expected, string $why): void |
| 166 | { |
| 167 | $error_code = SBR_Feed_Saver_Manager::get_source_error_code($info); |
| 168 | |
| 169 | $checkValidKey = ( |
| 170 | $error_code !== null && $error_code !== 'invalidKey' |
| 171 | ) |
| 172 | || !empty($info['info']['id']) |
| 173 | || !empty($info['info']['successId']); |
| 174 | |
| 175 | $this->assertSame($expected, $checkValidKey, $why); |
| 176 | } |
| 177 | |
| 178 | /** |
| 179 | * @return array<string,array{0:array<string,mixed>,1:bool,2:string}> |
| 180 | */ |
| 181 | public static function gateProvider(): array |
| 182 | { |
| 183 | return [ |
| 184 | 'errorId only' => [ |
| 185 | ['info' => ['errorId' => 'sourceConnectionError']], |
| 186 | false, |
| 187 | 'an unnamed failure is no evidence the key works', |
| 188 | ], |
| 189 | 'nested invalidKey' => [ |
| 190 | ['info' => ['errorId' => 'sourceConnectionError', 'error' => ['error' => 'invalidKey']]], |
| 191 | false, |
| 192 | 'an invalid key must never store', |
| 193 | ], |
| 194 | 'string invalidKey' => [ |
| 195 | ['info' => ['error' => 'invalidKey']], |
| 196 | false, |
| 197 | 'the string shape must behave as it always did', |
| 198 | ], |
| 199 | 'invalidLocation' => [ |
| 200 | ['info' => ['error' => 'invalidLocation']], |
| 201 | true, |
| 202 | 'a wrong location proves the key reached far enough to be told so', |
| 203 | ], |
| 204 | 'source returned' => [ |
| 205 | ['info' => ['id' => '13871137']], |
| 206 | true, |
| 207 | 'a created source is the plainest evidence of a working key', |
| 208 | ], |
| 209 | ]; |
| 210 | } |
| 211 | |
| 212 | public function test_the_message_falls_back_when_the_relay_sends_only_a_code(): void |
| 213 | { |
| 214 | $this->assertNotSame( |
| 215 | '', |
| 216 | SBR_Feed_Saver_Manager::get_source_error_message(['info' => ['error' => 'invalidKey']]) |
| 217 | ); |
| 218 | } |
| 219 | } |
| 220 |