From d756a5aaa8052f526332be68a73410c22084c255 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Gamez?= Date: Sat, 28 Jun 2025 13:49:03 +0200 Subject: [PATCH 1/2] Integrate `cuyz/valinor` for type-safe object mapping --- composer.json | 1 + phpstan.neon.dist | 1 + src/Firebase/Factory.php | 39 +++++++++ .../SnakeCaseToCamelCaseConverter.php | 36 ++++++++ src/Firebase/Valinor/Mapper.php | 71 ++++++++++++++++ src/Firebase/Valinor/Normalizer.php | 82 +++++++++++++++++++ src/Firebase/Valinor/Source.php | 52 ++++++++++++ .../CamelToSnakeCaseTransformer.php | 35 ++++++++ 8 files changed, 317 insertions(+) create mode 100644 src/Firebase/Valinor/Converter/SnakeCaseToCamelCaseConverter.php create mode 100644 src/Firebase/Valinor/Mapper.php create mode 100644 src/Firebase/Valinor/Normalizer.php create mode 100644 src/Firebase/Valinor/Source.php create mode 100644 src/Firebase/Valinor/Transformer/CamelToSnakeCaseTransformer.php diff --git a/composer.json b/composer.json index 872e23219..fb66500e1 100644 --- a/composer.json +++ b/composer.json @@ -31,6 +31,7 @@ "beste/clock": "^3.0", "beste/in-memory-cache": "^1.3.1", "beste/json": "^1.5.1", + "cuyz/valinor": "^2.0", "fig/http-message-util": "^1.1.5", "firebase/php-jwt": "^6.10.2", "google/auth": "^v1.45", diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 84e904c51..bec0913fe 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -28,4 +28,5 @@ parameters: reportPossiblyNonexistentConstantArrayOffset: true treatPhpDocTypesAsCertain: false includes: + - vendor/cuyz/valinor/qa/PHPStan/valinor-phpstan-configuration.php - vendor/phpstan/phpstan/conf/bleedingEdge.neon diff --git a/src/Firebase/Factory.php b/src/Firebase/Factory.php index 6cc5c4e20..f4c7f99b0 100644 --- a/src/Firebase/Factory.php +++ b/src/Firebase/Factory.php @@ -38,6 +38,9 @@ use Kreait\Firebase\JWT\SessionCookieVerifier; use Kreait\Firebase\Messaging\AppInstanceApiClient; use Kreait\Firebase\Messaging\RequestFactory; +use Kreait\Firebase\Valinor\Mapper; +use Kreait\Firebase\Valinor\Normalizer; +use Kreait\Firebase\Valinor\Source; use Psr\Cache\CacheItemPoolInterface; use Psr\Clock\ClockInterface; use Psr\Http\Message\UriInterface; @@ -134,6 +137,14 @@ final class Factory */ private array $firestoreClientConfig = []; + private mixed $mapperCache = null; + + private mixed $normalizerCache = null; + + private ?Mapper $mapper = null; + + private ?Normalizer $normalizer = null; + public function __construct() { $this->clock = SystemClock::create(); @@ -308,6 +319,24 @@ public function withKeySetCache(CacheItemPoolInterface $cache): self return $factory; } + public function withMapperCache(mixed $cache): self + { + $factory = clone $this; + $factory->mapperCache = $cache; + $factory->mapper = null; + + return $factory; + } + + public function withNormalizerCache(mixed $cache): self + { + $factory = clone $this; + $factory->normalizerCache = $cache; + $factory->normalizer = null; + + return $factory; + } + public function withHttpClientOptions(HttpClientOptions $options): self { $factory = clone $this; @@ -711,6 +740,16 @@ private function createIdTokenVerifier(): IdTokenVerifier return $verifier->withExpectedTenantId($this->tenantId); } + private function getMapper(): Mapper + { + return $this->mapper ??= new Mapper($this->mapperCache); + } + + private function getNormalizer(): Normalizer + { + return $this->normalizer ??= new Normalizer($this->normalizerCache); + } + private function createSessionCookieVerifier(): SessionCookieVerifier { return SessionCookieVerifier::createWithProjectIdAndCache($this->getProjectId(), $this->verifierCache ?? $this->defaultCache); diff --git a/src/Firebase/Valinor/Converter/SnakeCaseToCamelCaseConverter.php b/src/Firebase/Valinor/Converter/SnakeCaseToCamelCaseConverter.php new file mode 100644 index 000000000..a3673d16c --- /dev/null +++ b/src/Firebase/Valinor/Converter/SnakeCaseToCamelCaseConverter.php @@ -0,0 +1,36 @@ + lcfirst(str_replace('_', '', ucwords((string) $key, '_'))), + array_keys($values), + ), + $values, + ); + + return $next($camelCaseConverted); + } +} diff --git a/src/Firebase/Valinor/Mapper.php b/src/Firebase/Valinor/Mapper.php new file mode 100644 index 000000000..766721962 --- /dev/null +++ b/src/Firebase/Valinor/Mapper.php @@ -0,0 +1,71 @@ +withCache($this->cache); + } + + $this->mapperBuilder = $builder; + } + + public function withConverter(callable $converter): self + { + $mapperBuilder = $this->mapperBuilder->registerConverter($converter); // @phpstan-ignore-line argument.type + + return new self($this->cache, $mapperBuilder); + } + + public function snakeToCamelCase(): self + { + return $this->withConverter(new SnakeCaseToCamelCaseConverter()); + } + + public function allowSuperfluousKeys(): self + { + return new self($this->cache, $this->mapperBuilder->allowSuperfluousKeys()); + } + + /** + * @template T + * @param class-string $signature + * + * @throws InvalidArgumentException + * + * @return T + */ + public function map(string $signature, mixed $source): mixed + { + try { + return $this->mapperBuilder->mapper()->map($signature, $source); + } catch (MappingError $e) { + $errorMessages = []; + foreach ($e->messages() as $message) { + $errorMessages[] = '- `'.$message->path().'`: '.$message->toString(); + } + + $message = "Could not map type `$signature`:".PHP_EOL; + $message .= implode(PHP_EOL, $errorMessages); + + throw new InvalidArgumentException($message, 0, $e); + } + } +} diff --git a/src/Firebase/Valinor/Normalizer.php b/src/Firebase/Valinor/Normalizer.php new file mode 100644 index 000000000..a216bfb8d --- /dev/null +++ b/src/Firebase/Valinor/Normalizer.php @@ -0,0 +1,82 @@ +withCache($this->cache); + } + + $this->normalizerBuilder = $builder; + } + + public function withTransformer(callable $transformer): self + { + $builder = $this->normalizerBuilder->registerTransformer($transformer); // @phpstan-ignore-line argument.type + + return new self($this->cache, $builder); + } + + public function camelToSnakeCase(): self + { + return $this->withTransformer(new CamelToSnakeCaseTransformer()); + } + + /** + * @return array + */ + public function toArray(mixed $value): array + { + $this->arrayNormalizer ??= $this->normalizerBuilder->normalizer(Format::array()); + + $result = $this->arrayNormalizer->normalize($value); + assert(is_array($result)); + + return $result; + } + + /** + * @param int $options JSON encoding options + * + * @return non-empty-string + */ + public function toJson(mixed $value, ?int $options = null): string + { + $options ??= self::DEFAULT_JSON_OPTIONS; + + $this->jsonNormalizer ??= $this->normalizerBuilder->normalizer(Format::json())->withOptions($options); + + $result = $this->jsonNormalizer->normalize($value); + assert($result !== ''); + + return $result; + } +} diff --git a/src/Firebase/Valinor/Source.php b/src/Firebase/Valinor/Source.php new file mode 100644 index 000000000..4577ee122 --- /dev/null +++ b/src/Firebase/Valinor/Source.php @@ -0,0 +1,52 @@ + + */ +final class Source implements IteratorAggregate +{ + private function __construct( + /** @var iterable */ + private readonly iterable $delegate, + ) { + } + + public static function parse(mixed $value): self + { + if (is_iterable($value)) { + return new self(BaseSource::iterable($value)); + } + + if (str_starts_with((string) $value, '{')) { + try { + return new self(BaseSource::json($value)); + } catch (Throwable $e) { + throw new InvalidArgumentException(message: $e->getMessage(), previous: $e); + } + } + + try { + return new self(BaseSource::file(new SplFileObject($value))); + } catch (Throwable $e) { + throw new InvalidArgumentException(message: $e->getMessage(), previous: $e); + } + } + + public function getIterator(): Traversable + { + yield from $this->delegate; + } +} diff --git a/src/Firebase/Valinor/Transformer/CamelToSnakeCaseTransformer.php b/src/Firebase/Valinor/Transformer/CamelToSnakeCaseTransformer.php new file mode 100644 index 000000000..72ecc90ed --- /dev/null +++ b/src/Firebase/Valinor/Transformer/CamelToSnakeCaseTransformer.php @@ -0,0 +1,35 @@ + $value) { + $newKey = preg_replace('/[A-Z]/', '_$0', lcfirst($key)); + assert(is_string($newKey)); + + $newKey = strtolower($newKey); + + $snakeCased[$newKey] = $value; + } + + return $snakeCased; + } +} From f410f49f2209ccfb105d02cc39041d594bf89dbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Gamez?= Date: Mon, 21 Jul 2025 13:12:25 +0200 Subject: [PATCH 2/2] Introduce type-safe mapping of service account credentials --- CHANGELOG.md | 6 + src/Firebase/Factory.php | 139 ++++++------------ src/Firebase/ServiceAccount.php | 41 ++++++ .../CustomTokenViaGoogleCredentialsTest.php | 13 +- tests/Integration/ServiceAccountTest.php | 74 ++++++---- tests/IntegrationTestCase.php | 14 +- tests/Unit/FactoryTest.php | 80 ---------- 7 files changed, 150 insertions(+), 217 deletions(-) create mode 100644 src/Firebase/ServiceAccount.php delete mode 100644 tests/Unit/FactoryTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 2814c93ce..0f47aeed5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ Please read about the future of the Firebase Admin PHP SDK on the ## [Unreleased] +### Changed + +* This release introduces [Valinor](https://valinor.cuyz.io/) for type-safe object mapping. The first application is + mapping a given service account file, JSON, or array to the newly added internal `ServiceAccount` class, with more + to follow in future releases. + ## [7.20.0] - 2025-07-18 ### Added diff --git a/src/Firebase/Factory.php b/src/Firebase/Factory.php index f4c7f99b0..02248f8c2 100644 --- a/src/Firebase/Factory.php +++ b/src/Firebase/Factory.php @@ -7,7 +7,6 @@ use Beste\Cache\InMemoryCache; use Beste\Clock\SystemClock; use Beste\Clock\WrappingClock; -use Beste\Json; use Firebase\JWT\CachedKeySet; use Google\Auth\ApplicationDefaultCredentials; use Google\Auth\Credentials\ServiceAccountCredentials; @@ -48,21 +47,12 @@ use Psr\Log\LogLevel; use Stringable; use Throwable; -use UnexpectedValueException; use function array_filter; use function is_string; use function sprintf; use function trim; -/** - * @phpstan-type ServiceAccountShape array{ - * project_id: non-empty-string, - * client_email: non-empty-string, - * private_key: non-empty-string, - * type: 'service_account' - * } - */ final class Factory { public const API_CLIENT_SCOPES = [ @@ -86,10 +76,7 @@ final class Factory */ private ?string $defaultStorageBucket = null; - /** - * @var ServiceAccountShape|null - */ - private ?array $serviceAccount = null; + private ?ServiceAccount $serviceAccount = null; private ?FetchAuthTokenInterface $googleAuthTokenCredentials = null; @@ -155,40 +142,16 @@ public function __construct() } /** - * @param non-empty-string|ServiceAccountShape $value + * @param string|array $value + * + * @throws InvalidArgumentException */ public function withServiceAccount(string|array $value): self { - if (is_string($value) && str_starts_with($value, '{')) { - try { - /** @var ServiceAccountShape $serviceAccount */ - $serviceAccount = Json::decode($value, true); - } catch (UnexpectedValueException $e) { - throw new InvalidArgumentException('Invalid service account: '.$e->getMessage(), $e->getCode(), $e); - } - - $factory = clone $this; - $factory->serviceAccount = $serviceAccount; - - return $factory; - } - - if (is_string($value)) { - try { - /** @var ServiceAccountShape $serviceAccount */ - $serviceAccount = Json::decodeFile($value, true); - - $factory = clone $this; - $factory->serviceAccount = $serviceAccount; - - return $factory; - } catch (UnexpectedValueException $e) { - throw new InvalidArgumentException('Invalid service account: '.$e->getMessage(), $e->getCode(), $e); - } - } + $serviceAccount = $this->mapServiceAccount($value); $factory = clone $this; - $factory->serviceAccount = $value; + $factory->serviceAccount = $serviceAccount; return $factory; } @@ -414,8 +377,8 @@ public function createAppCheck(): Contract\AppCheck return new AppCheck( new AppCheck\ApiClient($http), new AppCheckTokenGenerator( - $serviceAccount['client_email'], - $serviceAccount['private_key'], + $serviceAccount->clientEmail, + $serviceAccount->privateKey, $this->clock, ), new AppCheckTokenVerifier($projectId, $keySet), @@ -548,16 +511,7 @@ public function createStorage(): Contract\Storage * @deprecated 7.20.0 * @codeCoverageIgnore * - * @return array{ - * credentialsType: string|null, - * databaseUrl: string, - * defaultStorageBucket: string|null, - * serviceAccount: string|array|null, - * projectId: string, - * tenantId: non-empty-string|null, - * tokenCacheType: class-string, - * verifierCacheType: class-string, - * } + * @return array */ public function getDebugInfo(): array { @@ -645,13 +599,7 @@ public function createApiClient(?array $config = null, ?array $middlewares = nul } /** - * @return array{ - * projectId: non-empty-string, - * authCache: CacheItemPoolInterface, - * credentialsFetcher?: FetchAuthTokenInterface, - * keyFile?: ServiceAccountShape, - * keyFilePath?: non-empty-string - * } + * @return array */ private function googleCloudClientConfig(): array { @@ -667,7 +615,7 @@ private function googleCloudClientConfig(): array $serviceAccount = $this->getServiceAccount(); if ($serviceAccount !== null) { - $config['keyFile'] = $serviceAccount; + $config['keyFile'] = $this->normalizeServiceAccount($serviceAccount); } return $config; @@ -687,6 +635,8 @@ private function getProjectId(): string ? $credentials->getProjectId() : Util::getenv('GOOGLE_CLOUD_PROJECT'); + $projectId ??= $this->getServiceAccount()?->projectId; + if (is_string($projectId) && $projectId !== '') { return $this->projectId = $projectId; } @@ -699,11 +649,7 @@ private function getProjectId(): string */ private function getDatabaseUrl(): string { - if ($this->databaseUrl === null) { - $this->databaseUrl = sprintf('https://%s.firebaseio.com', $this->getProjectId()); - } - - return $this->databaseUrl; + return $this->databaseUrl ??= sprintf('https://%s.firebaseio.com', $this->getProjectId()); } /** @@ -711,11 +657,7 @@ private function getDatabaseUrl(): string */ private function getStorageBucketName(): string { - if ($this->defaultStorageBucket === null) { - $this->defaultStorageBucket = sprintf('%s.appspot.com', $this->getProjectId()); - } - - return $this->defaultStorageBucket; + return $this->defaultStorageBucket ??= sprintf('%s.appspot.com', $this->getProjectId()); } private function createCustomTokenGenerator(): ?CustomTokenViaGoogleCredentials @@ -755,41 +697,34 @@ private function createSessionCookieVerifier(): SessionCookieVerifier return SessionCookieVerifier::createWithProjectIdAndCache($this->getProjectId(), $this->verifierCache ?? $this->defaultCache); } - /** - * @return ServiceAccountShape|null - */ - private function getServiceAccount(): ?array + private function getServiceAccount(): ?ServiceAccount { - if ($this->serviceAccount === null) { - $googleApplicationCredentials = Util::getenv('GOOGLE_APPLICATION_CREDENTIALS'); - - if ($googleApplicationCredentials === null) { - return null; - } - - if (!str_starts_with($googleApplicationCredentials, '{')) { - return null; - } + if ($this->serviceAccount !== null) { + return $this->serviceAccount; + } - /** @var ServiceAccountShape $serviceAccount */ - $serviceAccount = Json::decode($googleApplicationCredentials, true); + $googleApplicationCredentials = Util::getenv('GOOGLE_APPLICATION_CREDENTIALS'); - $this->serviceAccount = $serviceAccount; + if ($googleApplicationCredentials === null) { + return $this->serviceAccount; } - return $this->serviceAccount; + return $this->serviceAccount = $this->mapServiceAccount($googleApplicationCredentials); } private function getGoogleAuthTokenCredentials(): ?FetchAuthTokenInterface { - if ($this->googleAuthTokenCredentials !== null) { + if ($this->googleAuthTokenCredentials instanceof FetchAuthTokenInterface) { return $this->googleAuthTokenCredentials; } $serviceAccount = $this->getServiceAccount(); if ($serviceAccount !== null) { - return $this->googleAuthTokenCredentials = new ServiceAccountCredentials(self::API_CLIENT_SCOPES, $serviceAccount); + return $this->googleAuthTokenCredentials = new ServiceAccountCredentials( + self::API_CLIENT_SCOPES, + $this->normalizeServiceAccount($serviceAccount), + ); } try { @@ -798,4 +733,22 @@ private function getGoogleAuthTokenCredentials(): ?FetchAuthTokenInterface return null; } } + + private function mapServiceAccount(mixed $value): ServiceAccount + { + return $this->getMapper() + ->allowSuperfluousKeys() + ->snakeToCamelCase() + ->map(ServiceAccount::class, Source::parse($value)); + } + + /** + * @return array + */ + private function normalizeServiceAccount(ServiceAccount $serviceAccount): array + { + return $this->getNormalizer() + ->camelToSnakeCase() + ->toArray($serviceAccount); + } } diff --git a/src/Firebase/ServiceAccount.php b/src/Firebase/ServiceAccount.php new file mode 100644 index 000000000..46dd7e129 --- /dev/null +++ b/src/Firebase/ServiceAccount.php @@ -0,0 +1,41 @@ +allowSuperfluousKeys() + ->snakeToCamelCase() + ->map(ServiceAccount::class, Source::parse(self::$credentials)); + + $normalizedServiceAccount = (new Normalizer())->camelToSnakeCase()->toArray($serviceAccount); + + $credentials = new ServiceAccountCredentials(Factory::API_CLIENT_SCOPES, $normalizedServiceAccount); $this->generator = new CustomTokenViaGoogleCredentials($credentials); $this->auth = self::$factory->createAuth(); diff --git a/tests/Integration/ServiceAccountTest.php b/tests/Integration/ServiceAccountTest.php index dad8490eb..eb76e318f 100644 --- a/tests/Integration/ServiceAccountTest.php +++ b/tests/Integration/ServiceAccountTest.php @@ -4,60 +4,58 @@ namespace Kreait\Firebase\Tests\Integration; +use InvalidArgumentException; use Kreait\Firebase\Factory; +use Kreait\Firebase\ServiceAccount; use Kreait\Firebase\Tests\IntegrationTestCase; use Kreait\Firebase\Util; +use Kreait\Firebase\Valinor\Mapper; +use Kreait\Firebase\Valinor\Normalizer; +use Kreait\Firebase\Valinor\Source; use PHPUnit\Framework\Attributes\DoesNotPerformAssertions; use PHPUnit\Framework\Attributes\Test; -use function assert; - /** * @internal */ final class ServiceAccountTest extends IntegrationTestCase { - /** - * @var non-empty-string - */ - private static string $credentialsPath; + private ServiceAccount $serviceAccount; - private static ?string $originalCredentials; + private Normalizer $normalizer; - public static function setUpBeforeClass(): void + protected function setUp(): void { - parent::setUpBeforeClass(); + parent::setUp(); - self::$originalCredentials = Util::getenv('GOOGLE_APPLICATION_CREDENTIALS'); + $this->serviceAccount = (new Mapper()) + ->snakeToCamelCase() + ->allowSuperfluousKeys() + ->map(ServiceAccount::class, Source::parse(self::$credentials)); - self::$credentialsPath = sys_get_temp_dir().DIRECTORY_SEPARATOR.'test_credentials.json'; - file_put_contents(self::$credentialsPath, json_encode(self::$serviceAccount)); - Util::putenv('GOOGLE_APPLICATION_CREDENTIALS', self::$credentialsPath); - } - - public static function tearDownAfterClass(): void - { - unlink(self::$credentialsPath); - Util::putenv('GOOGLE_APPLICATION_CREDENTIALS', self::$originalCredentials); + $this->normalizer = (new Normalizer())->camelToSnakeCase(); } #[Test] #[DoesNotPerformAssertions] public function withPathToServiceAccount(): void { - $factory = (new Factory())->withServiceAccount(self::$credentialsPath); + $path = sys_get_temp_dir().DIRECTORY_SEPARATOR.__FUNCTION__.'.json'; + file_put_contents($path, $this->normalizer->toJson($this->serviceAccount)); - $this->assertFunctioningConnection($factory); + try { + $factory = (new Factory())->withServiceAccount($path); + $this->assertFunctioningConnection($factory); + } finally { + unlink($path); + } } #[Test] #[DoesNotPerformAssertions] public function withJsonString(): void { - $json = file_get_contents(self::$credentialsPath); - assert($json !== false && $json !== ''); - - $factory = (new Factory())->withServiceAccount($json); + $factory = (new Factory())->withServiceAccount($this->normalizer->toJson($this->serviceAccount)); $this->assertFunctioningConnection($factory); } @@ -66,7 +64,7 @@ public function withJsonString(): void #[DoesNotPerformAssertions] public function withArray(): void { - $factory = (new Factory())->withServiceAccount(self::$serviceAccount); + $factory = (new Factory())->withServiceAccount($this->normalizer->toArray($this->serviceAccount)); $this->assertFunctioningConnection($factory); } @@ -75,23 +73,35 @@ public function withArray(): void #[DoesNotPerformAssertions] public function withGoogleApplicationCredentialsAsFilePath(): void { - Util::putenv('GOOGLE_APPLICATION_CREDENTIALS', self::$credentialsPath); + $path = sys_get_temp_dir().DIRECTORY_SEPARATOR.__FUNCTION__.'.json'; + file_put_contents($path, $this->normalizer->toJson($this->serviceAccount)); - $this->assertFunctioningConnection(new Factory()); + Util::putenv('GOOGLE_APPLICATION_CREDENTIALS', $path); + + try { + $this->assertFunctioningConnection(new Factory()); + } finally { + unlink($path); + } } #[Test] #[DoesNotPerformAssertions] public function withGoogleApplicationCredentialsAsJsonString(): void { - $json = file_get_contents(self::$credentialsPath); - assert($json !== false && $json !== ''); - - Util::putenv('GOOGLE_APPLICATION_CREDENTIALS', $json); + Util::putenv('GOOGLE_APPLICATION_CREDENTIALS', $this->normalizer->toJson($this->serviceAccount)); $this->assertFunctioningConnection(new Factory()); } + #[Test] + public function withInvalidServiceAccount(): void + { + $this->expectException(InvalidArgumentException::class); + + (new Factory())->withServiceAccount(['invalid' => 'data']); + } + private function assertFunctioningConnection(Factory $factory): void { $auth = $factory->createAuth(); diff --git a/tests/IntegrationTestCase.php b/tests/IntegrationTestCase.php index 2cebc9311..417ff3f8e 100644 --- a/tests/IntegrationTestCase.php +++ b/tests/IntegrationTestCase.php @@ -15,17 +15,12 @@ /** * @internal - * - * @phpstan-import-type ServiceAccountShape from Factory */ abstract class IntegrationTestCase extends FirebaseTestCase { protected static Factory $factory; - /** - * @var ServiceAccountShape - */ - protected static array $serviceAccount; + protected static mixed $credentials; /** * @var non-empty-string|null @@ -57,11 +52,8 @@ public static function setUpBeforeClass(): void self::markTestSkipped('The integration tests require credentials'); } - /** @var ServiceAccountShape $serviceAccountArray */ - $serviceAccountArray = Json::decode($credentials, true); - self::$serviceAccount = $serviceAccountArray; - - self::$factory = (new Factory())->withServiceAccount(self::$serviceAccount); + self::$credentials = $credentials; + self::$factory = (new Factory())->withServiceAccount($credentials); self::$registrationTokens = self::registrationTokensFromEnvironment(); self::$rtdbUrl = Util::getenv('TEST_FIREBASE_RTDB_URI'); self::$tenantId = Util::getenv('TEST_FIREBASE_TENANT_ID'); diff --git a/tests/Unit/FactoryTest.php b/tests/Unit/FactoryTest.php deleted file mode 100644 index 618abf7d2..000000000 --- a/tests/Unit/FactoryTest.php +++ /dev/null @@ -1,80 +0,0 @@ -serviceAccountFilePath = self::$fixturesDir.'/ServiceAccount/valid.json'; - - /** @var ServiceAccountShape $serviceAccountArray */ - $serviceAccountArray = Json::decodeFile($this->serviceAccountFilePath, true); - - $this->serviceAccountArray = $serviceAccountArray; - } - - #[DoesNotPerformAssertions] - #[Test] - public function itUsesTheCredentialsFromTheGoogleApplicationCredentialsEnvironmentVariable(): void - { - putenv('GOOGLE_APPLICATION_CREDENTIALS='.$this->serviceAccountFilePath); - - $this->assertServices(new Factory()); - - putenv('GOOGLE_APPLICATION_CREDENTIALS'); - } - - #[DoesNotPerformAssertions] - #[Test] - public function itCanBeConfiguredWithThePathToAServiceAccount(): void - { - $factory = (new Factory())->withServiceAccount($this->serviceAccountFilePath); - - $this->assertServices($factory); - } - - #[DoesNotPerformAssertions] - #[Test] - public function itCanBeConfiguredWithAServiceAccountArray(): void - { - $factory = (new Factory())->withServiceAccount($this->serviceAccountArray); - - $this->assertServices($factory); - } - - private function assertServices(Factory $factory): void - { - $factory->createAuth(); - $factory->createDatabase(); - $factory->createFirestore(); - $factory->createMessaging(); - $factory->createRemoteConfig(); - $factory->createStorage(); - } -}