From 188143d884d4bf32450d4d5a267e157ae6608910 Mon Sep 17 00:00:00 2001 From: James Drew Date: Thu, 27 Aug 2026 17:11:27 +0100 Subject: [PATCH 1/3] Add testable error handling to SDK version function See #90 --- src/ExpoPushConnector.php | 30 ++++++-- tests/Unit/ExpoPushConnectorTest.php | 106 +++++++++++++++++++++++++++ 2 files changed, 129 insertions(+), 7 deletions(-) diff --git a/src/ExpoPushConnector.php b/src/ExpoPushConnector.php index 959d208..45bde5a 100644 --- a/src/ExpoPushConnector.php +++ b/src/ExpoPushConnector.php @@ -25,6 +25,9 @@ class ExpoPushConnector extends Connector public const MAX_CONCURRENT_REQUESTS = 6; + private static bool $sdkVersionResolved = false; + private static ?string $sdkVersion = null; + public function __construct( protected ?string $authToken = null, ?RateLimitStore $rateLimitStore = null, @@ -83,17 +86,30 @@ protected function resolveRateLimitStore(): RateLimitStore */ public function sdkVersion(): string { - static $version = null; + if (!self::$sdkVersionResolved) { + self::$sdkVersion = self::resolveSdkVersion(dirname(__DIR__) . '/composer.json'); + self::$sdkVersionResolved = true; + } - if (!$version) { - $composer = json_decode( - file_get_contents(dirname(__DIR__) . '/composer.json'), - ); + return self::$sdkVersion; + } - $version = InstalledVersions::getPrettyVersion($composer->name); + /** + * Resolve the installed version from composer.json, falling back to 'unknown' if it can't be determined + */ + private static function resolveSdkVersion(string $composerJsonPath): string + { + if (!is_readable($composerJsonPath)) { + return 'unknown'; } - return (string) $version; + try { + $composer = json_decode(file_get_contents($composerJsonPath), flags: JSON_THROW_ON_ERROR); + + return InstalledVersions::getPrettyVersion($composer->name) ?? 'unknown'; + } catch (Throwable) { + return 'unknown'; + } } // Internals ---- diff --git a/tests/Unit/ExpoPushConnectorTest.php b/tests/Unit/ExpoPushConnectorTest.php index 354ef7b..33b30a6 100644 --- a/tests/Unit/ExpoPushConnectorTest.php +++ b/tests/Unit/ExpoPushConnectorTest.php @@ -4,10 +4,13 @@ namespace Dru1x\ExpoPush\Tests\Unit; +use Composer\InstalledVersions; use Dru1x\ExpoPush\Config\RetryConfig; use Dru1x\ExpoPush\ExpoPushConnector; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; +use ReflectionMethod; +use ReflectionProperty; use Saloon\Http\Auth\TokenAuthenticator; use Saloon\RateLimitPlugin\Exceptions\LimitException; use Saloon\RateLimitPlugin\Limit; @@ -15,6 +18,34 @@ class ExpoPushConnectorTest extends TestCase { + private ?bool $originalSdkVersionResolved = null; + private ?string $originalSdkVersion = null; + + /** @var list */ + private array $tempFiles = []; + + protected function setUp(): void + { + parent::setUp(); + + $this->originalSdkVersionResolved = (new ReflectionProperty(ExpoPushConnector::class, 'sdkVersionResolved'))->getValue(); + $this->originalSdkVersion = (new ReflectionProperty(ExpoPushConnector::class, 'sdkVersion'))->getValue(); + } + + protected function tearDown(): void + { + (new ReflectionProperty(ExpoPushConnector::class, 'sdkVersionResolved'))->setValue(null, $this->originalSdkVersionResolved); + (new ReflectionProperty(ExpoPushConnector::class, 'sdkVersion'))->setValue(null, $this->originalSdkVersion); + + foreach ($this->tempFiles as $tempFile) { + is_file($tempFile) && unlink($tempFile); + } + + $this->tempFiles = []; + + parent::tearDown(); + } + #[Test] public function resolves_the_expo_push_api_base_url(): void { @@ -113,8 +144,83 @@ public function custom_retry_configuration_is_used_when_supplied(): void $this->assertFalse($connector->throwOnMaxTries); } + // SDK Version ---- + + #[Test] + public function sdk_version_resolves_the_real_installed_version(): void + { + $composerJsonPath = dirname(__DIR__, 2) . '/composer.json'; + $composer = json_decode(file_get_contents($composerJsonPath)); + + $this->assertSame( + InstalledVersions::getPrettyVersion($composer->name), + $this->resolveSdkVersion($composerJsonPath), + ); + } + + #[Test] + public function sdk_version_falls_back_to_unknown_when_composer_json_is_missing(): void + { + $this->assertSame('unknown', $this->resolveSdkVersion('/nonexistent/composer.json')); + } + + #[Test] + public function sdk_version_falls_back_to_unknown_when_composer_json_is_malformed(): void + { + $path = $this->writeTempFile('{not valid json'); + + $this->assertSame('unknown', $this->resolveSdkVersion($path)); + } + + #[Test] + public function sdk_version_falls_back_to_unknown_when_the_package_is_not_installed(): void + { + $path = $this->writeTempFile(json_encode(['name' => 'not/a-real-package'])); + + $this->assertSame('unknown', $this->resolveSdkVersion($path)); + } + + #[Test] + public function sdk_version_does_not_recompute_once_resolved(): void + { + (new ReflectionProperty(ExpoPushConnector::class, 'sdkVersion'))->setValue(null, 'cached-test-value'); + (new ReflectionProperty(ExpoPushConnector::class, 'sdkVersionResolved'))->setValue(null, true); + + $connector = new ExpoPushConnector(); + + $this->assertSame('cached-test-value', $connector->sdkVersion()); + } + + #[Test] + public function sdk_version_caches_a_falsy_resolved_value_instead_of_recomputing_it(): void + { + (new ReflectionProperty(ExpoPushConnector::class, 'sdkVersion'))->setValue(null, ''); + (new ReflectionProperty(ExpoPushConnector::class, 'sdkVersionResolved'))->setValue(null, true); + + $connector = new ExpoPushConnector(); + + $this->assertSame('', $connector->sdkVersion()); + } + // Internals ---- + private function resolveSdkVersion(string $composerJsonPath): string + { + return (new ReflectionMethod(ExpoPushConnector::class, 'resolveSdkVersion')) + ->invoke(null, $composerJsonPath); + } + + private function writeTempFile(string $contents): string + { + $path = tempnam(sys_get_temp_dir(), 'expo-push-sdk-version-test-'); + + file_put_contents($path, $contents); + + $this->tempFiles[] = $path; + + return $path; + } + private function findLimit(ExpoPushConnector $connector, string $name): Limit { try { From 134bb73572025c786f22cf03dab96055d6e2148a Mon Sep 17 00:00:00 2001 From: James Drew Date: Fri, 4 Sep 2026 13:12:26 +0100 Subject: [PATCH 2/3] Remove redunant $sdkVersionResolved from ExpoPushConnector --- src/ExpoPushConnector.php | 6 ++---- tests/Unit/ExpoPushConnectorTest.php | 9 ++------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/ExpoPushConnector.php b/src/ExpoPushConnector.php index 45bde5a..d2261e7 100644 --- a/src/ExpoPushConnector.php +++ b/src/ExpoPushConnector.php @@ -25,7 +25,6 @@ class ExpoPushConnector extends Connector public const MAX_CONCURRENT_REQUESTS = 6; - private static bool $sdkVersionResolved = false; private static ?string $sdkVersion = null; public function __construct( @@ -86,9 +85,8 @@ protected function resolveRateLimitStore(): RateLimitStore */ public function sdkVersion(): string { - if (!self::$sdkVersionResolved) { - self::$sdkVersion = self::resolveSdkVersion(dirname(__DIR__) . '/composer.json'); - self::$sdkVersionResolved = true; + if (self::$sdkVersion === null) { + self::$sdkVersion = self::resolveSdkVersion(dirname(__DIR__) . '/composer.json'); } return self::$sdkVersion; diff --git a/tests/Unit/ExpoPushConnectorTest.php b/tests/Unit/ExpoPushConnectorTest.php index 33b30a6..25fd575 100644 --- a/tests/Unit/ExpoPushConnectorTest.php +++ b/tests/Unit/ExpoPushConnectorTest.php @@ -18,8 +18,7 @@ class ExpoPushConnectorTest extends TestCase { - private ?bool $originalSdkVersionResolved = null; - private ?string $originalSdkVersion = null; + private ?string $originalSdkVersion = null; /** @var list */ private array $tempFiles = []; @@ -28,13 +27,11 @@ protected function setUp(): void { parent::setUp(); - $this->originalSdkVersionResolved = (new ReflectionProperty(ExpoPushConnector::class, 'sdkVersionResolved'))->getValue(); - $this->originalSdkVersion = (new ReflectionProperty(ExpoPushConnector::class, 'sdkVersion'))->getValue(); + $this->originalSdkVersion = (new ReflectionProperty(ExpoPushConnector::class, 'sdkVersion'))->getValue(); } protected function tearDown(): void { - (new ReflectionProperty(ExpoPushConnector::class, 'sdkVersionResolved'))->setValue(null, $this->originalSdkVersionResolved); (new ReflectionProperty(ExpoPushConnector::class, 'sdkVersion'))->setValue(null, $this->originalSdkVersion); foreach ($this->tempFiles as $tempFile) { @@ -184,7 +181,6 @@ public function sdk_version_falls_back_to_unknown_when_the_package_is_not_instal public function sdk_version_does_not_recompute_once_resolved(): void { (new ReflectionProperty(ExpoPushConnector::class, 'sdkVersion'))->setValue(null, 'cached-test-value'); - (new ReflectionProperty(ExpoPushConnector::class, 'sdkVersionResolved'))->setValue(null, true); $connector = new ExpoPushConnector(); @@ -195,7 +191,6 @@ public function sdk_version_does_not_recompute_once_resolved(): void public function sdk_version_caches_a_falsy_resolved_value_instead_of_recomputing_it(): void { (new ReflectionProperty(ExpoPushConnector::class, 'sdkVersion'))->setValue(null, ''); - (new ReflectionProperty(ExpoPushConnector::class, 'sdkVersionResolved'))->setValue(null, true); $connector = new ExpoPushConnector(); From 1168484294193ef79139656e536edb327e84bb81 Mon Sep 17 00:00:00 2001 From: James Drew Date: Fri, 4 Sep 2026 13:13:19 +0100 Subject: [PATCH 3/3] Add smoke test for sdkVersion() method --- tests/Unit/ExpoPushConnectorTest.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/Unit/ExpoPushConnectorTest.php b/tests/Unit/ExpoPushConnectorTest.php index 25fd575..77c09d9 100644 --- a/tests/Unit/ExpoPushConnectorTest.php +++ b/tests/Unit/ExpoPushConnectorTest.php @@ -143,6 +143,14 @@ public function custom_retry_configuration_is_used_when_supplied(): void // SDK Version ---- + #[Test] + public function sdk_version_resolves(): void + { + $connector = new ExpoPushConnector(); + + $this->assertNotSame('unknown', $connector->sdkVersion()); + } + #[Test] public function sdk_version_resolves_the_real_installed_version(): void {