From 89d34dad5414aa9f3e7145a2f99419be6ef3ad3e Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 11 Sep 2025 11:02:34 -0700 Subject: [PATCH] chore: run all phpunit tests on windows --- .github/workflows/tests.yml | 7 +- CHANGELOG.md | 32 ++++ README.md | 14 +- VERSION | 2 +- autoload.php | 34 ---- composer.json | 5 +- src/Cache/FileSystemCacheItemPool.php | 6 +- src/Cache/Item.php | 175 ------------------ src/Cache/SysVCacheItemPool.php | 150 ++++++++++++--- .../ExternalAccountCredentials.php | 9 + .../ImpersonatedServiceAccountCredentials.php | 25 ++- src/CredentialsLoader.php | 44 +++-- src/Logging/LoggingTrait.php | 1 + tests/ApplicationDefaultCredentialsTest.php | 50 ++--- tests/Cache/FileSystemCacheItemPoolTest.php | 29 ++- tests/Cache/RaceConditionTest.php | 114 ++++++++++++ tests/Cache/SysVCacheItemPoolTest.php | 46 ++++- .../sysv_cache_race_condition_writer.php | 26 +++ .../ExternalAccountCredentialsTest.php | 26 ++- ...ersonatedServiceAccountCredentialsTest.php | 66 +++++++ .../ServiceAccountCredentialsTest.php | 4 +- .../UserRefreshCredentialsTest.php | 6 +- tests/CredentialsLoaderTest.php | 12 +- .../ExecutableHandlerTest.php | 10 +- tests/Logging/LoggingTraitTest.php | 12 ++ tests/bootstrap.php | 12 ++ tests/fixtures/.config/gcloud | 1 + .../application_default_credentials.json | 0 tests/fixtures2/.config/gcloud | 1 + .../application_default_credentials.json | 0 tests/fixtures5/.config/gcloud | 1 + .../application_default_credentials.json | 0 tests/mocks/TestFileCacheItemPool.php | 4 +- 33 files changed, 593 insertions(+), 331 deletions(-) delete mode 100644 autoload.php delete mode 100644 src/Cache/Item.php create mode 100644 tests/Cache/RaceConditionTest.php create mode 100644 tests/Cache/sysv_cache_race_condition_writer.php create mode 120000 tests/fixtures/.config/gcloud rename tests/fixtures/{.config => }/gcloud/application_default_credentials.json (100%) create mode 120000 tests/fixtures2/.config/gcloud rename tests/fixtures2/{.config => }/gcloud/application_default_credentials.json (100%) create mode 120000 tests/fixtures5/.config/gcloud rename tests/fixtures5/{.config => }/gcloud/application_default_credentials.json (100%) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e474760893..53878351b4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,11 +10,8 @@ jobs: test: strategy: matrix: + os: [ "ubuntu-latest", "windows-latest" ] php: [ "8.1", "8.2", "8.3", "8.4" ] - os: [ ubuntu-latest ] - include: - - os: windows-latest - php: "8.1" runs-on: ${{ matrix.os }} name: PHP ${{ matrix.php }} Unit Test${{ matrix.os == 'windows-latest' && ' on Windows' || '' }} steps: @@ -31,7 +28,7 @@ jobs: max_attempts: 3 command: composer install - name: Run Script - run: vendor/bin/phpunit ${{ matrix.os == 'windows-latest' && '--filter GCECredentialsTest' || '' }} + run: vendor/bin/phpunit test_lowest: runs-on: ubuntu-latest name: Test Prefer Lowest diff --git a/CHANGELOG.md b/CHANGELOG.md index 17d3efef8f..02107bce87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,38 @@ * [feat]: add support for Firebase v6.0 (#391) +## [1.49.0](https://github.com/googleapis/google-auth-library-php/compare/v1.48.1...v1.49.0) (2025-11-06) + + +### Features + +* Add semaphore locking to Sysv cache ([#640](https://github.com/googleapis/google-auth-library-php/issues/640)) ([38ea069](https://github.com/googleapis/google-auth-library-php/commit/38ea069652278928f55335fc6c4ed92be866cf0f)) +* Json key scopes in ImpersonatedServiceAccountCredentials ([#638](https://github.com/googleapis/google-auth-library-php/issues/638)) ([b6b6966](https://github.com/googleapis/google-auth-library-php/commit/b6b696696245519bbf50222514189dc7a1010bf7)) + + +### Bug Fixes + +* Filecache race condition ([#637](https://github.com/googleapis/google-auth-library-php/issues/637)) ([09042be](https://github.com/googleapis/google-auth-library-php/commit/09042be363a275b5055dce28e8c7ce10455376d9)) + +## [1.48.1](https://github.com/googleapis/google-auth-library-php/compare/v1.48.0...v1.48.1) (2025-09-29) + + +### Bug Fixes + +* Remove deprecated Item class for the CacheItemPool ([#631](https://github.com/googleapis/google-auth-library-php/issues/631)) ([7ec42c6](https://github.com/googleapis/google-auth-library-php/commit/7ec42c6ccc678865958766a32e888ae986a13608)) + +## [1.48.0](https://github.com/googleapis/google-auth-library-php/compare/v1.47.1...v1.48.0) (2025-09-16) + + +### Features + +* Add the rpcName to the logged event ([#630](https://github.com/googleapis/google-auth-library-php/issues/630)) ([d1d9e21](https://github.com/googleapis/google-auth-library-php/commit/d1d9e214af6a67bba4f06a2906be1be7da469419)) + + +### Bug Fixes + +* Deprecate Credentials::makeCredentials ([#624](https://github.com/googleapis/google-auth-library-php/issues/624)) ([12bb6e8](https://github.com/googleapis/google-auth-library-php/commit/12bb6e8a137f0dce5e2f1c193d59df8596fde3e4)) + ## [1.47.1](https://github.com/googleapis/google-auth-library-php/compare/v1.47.0...v1.47.1) (2025-07-08) diff --git a/README.md b/README.md index 3331aebf58..a8d77d692e 100644 --- a/README.md +++ b/README.md @@ -171,11 +171,19 @@ $jsonKey = ['key' => 'value']; // define the scopes for your API call $scopes = ['https://www.googleapis.com/auth/drive.readonly']; -// Load credentials -$creds = CredentialsLoader::makeCredentials($scopes, $jsonKey); +// Load credentials from JSON containing service account credentials. +$creds = new ServiceAccountCredentials($scopes, $jsonKey), + +// For other credentials types, create those classes explicitly using the +// "type" field in the JSON key, for example: +$creds = match ($jsonKey['type']) { + 'service_account' => new ServiceAccountCredentials($scope, $jsonKey), + 'authorized_user' => new UserRefreshCredentials($scope, $jsonKey), + default => throw new InvalidArgumentException('This application only supports service account and user account credentials'), +}; // optional caching -// $creds = new FetchAuthTokenCache($creds, $cacheConfig, $cache); +$creds = new FetchAuthTokenCache($creds, $cacheConfig, $cache); // create middleware $middleware = new AuthTokenMiddleware($creds); diff --git a/VERSION b/VERSION index f805cd6ed2..7f3a46a841 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.47.1 +1.49.0 diff --git a/autoload.php b/autoload.php deleted file mode 100644 index 1e13827f58..0000000000 --- a/autoload.php +++ /dev/null @@ -1,34 +0,0 @@ - 3) { - // Maximum class file path depth in this project is 3. - $classPath = array_slice($classPath, 0, 3); - } - $filePath = dirname(__FILE__) . '/src/' . implode('/', $classPath) . '.php'; - if (file_exists($filePath)) { - require_once $filePath; - } -} - -spl_autoload_register('oauth2client_php_autoload'); diff --git a/composer.json b/composer.json index 0a175e8437..2afdcdeb47 100644 --- a/composer.json +++ b/composer.json @@ -19,14 +19,15 @@ }, "require-dev": { "guzzlehttp/promises": "^2.0", - "squizlabs/php_codesniffer": "^3.5", + "squizlabs/php_codesniffer": "^4.0", "phpunit/phpunit": "^9.6", "phpspec/prophecy-phpunit": "^2.1", "sebastian/comparator": ">=1.2.3", "phpseclib/phpseclib": "^3.0.35", "kelvinmo/simplejwt": "0.7.1", "webmozart/assert": "^1.11", - "symfony/process": "^6.0||^7.0" + "symfony/process": "^6.0||^7.0", + "symfony/filesystem": "^6.3||^7.3" }, "suggest": { "phpseclib/phpseclib": "May be used in place of OpenSSL for signing strings or for token management. Please require version ^2." diff --git a/src/Cache/FileSystemCacheItemPool.php b/src/Cache/FileSystemCacheItemPool.php index ee0651a4e2..fb8a045b2f 100644 --- a/src/Cache/FileSystemCacheItemPool.php +++ b/src/Cache/FileSystemCacheItemPool.php @@ -46,7 +46,9 @@ public function __construct(string $path) return; } - if (!mkdir($this->cachePath)) { + // Suppress the error for when the directory already exists because of a + // race condition + if (!@mkdir($this->cachePath, 0777, true) && !is_dir($this->cachePath)) { throw new ErrorException("Cache folder couldn't be created."); } } @@ -111,7 +113,7 @@ public function save(CacheItemInterface $item): bool $itemPath = $this->cacheFilePath($item->getKey()); $serializedItem = serialize($item->get()); - $result = file_put_contents($itemPath, $serializedItem); + $result = file_put_contents($itemPath, $serializedItem, LOCK_EX); // 0 bytes write is considered a successful operation if ($result === false) { diff --git a/src/Cache/Item.php b/src/Cache/Item.php deleted file mode 100644 index ff85afa71f..0000000000 --- a/src/Cache/Item.php +++ /dev/null @@ -1,175 +0,0 @@ -key = $key; - } - - /** - * {@inheritdoc} - */ - public function getKey() - { - return $this->key; - } - - /** - * {@inheritdoc} - */ - public function get() - { - return $this->isHit() ? $this->value : null; - } - - /** - * {@inheritdoc} - */ - public function isHit() - { - if (!$this->isHit) { - return false; - } - - if ($this->expiration === null) { - return true; - } - - return $this->currentTime()->getTimestamp() < $this->expiration->getTimestamp(); - } - - /** - * {@inheritdoc} - */ - public function set($value) - { - $this->isHit = true; - $this->value = $value; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function expiresAt($expiration) - { - if ($this->isValidExpiration($expiration)) { - $this->expiration = $expiration; - - return $this; - } - - $error = sprintf( - 'Argument 1 passed to %s::expiresAt() must implement interface DateTimeInterface, %s given', - get_class($this), - gettype($expiration) - ); - - throw new TypeError($error); - } - - /** - * {@inheritdoc} - */ - public function expiresAfter($time) - { - if (is_int($time)) { - $this->expiration = $this->currentTime()->add(new \DateInterval("PT{$time}S")); - } elseif ($time instanceof \DateInterval) { - $this->expiration = $this->currentTime()->add($time); - } elseif ($time === null) { - $this->expiration = $time; - } else { - $message = 'Argument 1 passed to %s::expiresAfter() must be an ' . - 'instance of DateInterval or of the type integer, %s given'; - $error = sprintf($message, get_class($this), gettype($time)); - - throw new TypeError($error); - } - - return $this; - } - - /** - * Determines if an expiration is valid based on the rules defined by PSR6. - * - * @param mixed $expiration - * @return bool - */ - private function isValidExpiration($expiration) - { - if ($expiration === null) { - return true; - } - - if ($expiration instanceof DateTimeInterface) { - return true; - } - - return false; - } - - /** - * @return DateTime - */ - protected function currentTime() - { - return new DateTime('now', new DateTimeZone('UTC')); - } -} diff --git a/src/Cache/SysVCacheItemPool.php b/src/Cache/SysVCacheItemPool.php index 7821d6b097..3b0a2488bf 100644 --- a/src/Cache/SysVCacheItemPool.php +++ b/src/Cache/SysVCacheItemPool.php @@ -18,6 +18,8 @@ use Psr\Cache\CacheItemInterface; use Psr\Cache\CacheItemPoolInterface; +use SysvSemaphore; +use SysvSharedMemory; /** * SystemV shared memory based CacheItemPool implementation. @@ -32,6 +34,8 @@ class SysVCacheItemPool implements CacheItemPoolInterface const DEFAULT_PROJ = 'A'; + const DEFAULT_SEM_PROJ = 'B'; + const DEFAULT_MEMSIZE = 10000; const DEFAULT_PERM = 0600; @@ -61,6 +65,18 @@ class SysVCacheItemPool implements CacheItemPoolInterface */ private $hasLoadedItems = false; + /** + * @var SysvSemaphore|false + */ + private SysvSemaphore|false $semId = false; + + /** + * Maintain the process which is currently holding the semaphore to prevent deadlock. + * + * @var int|null + */ + private ?int $lockOwnerPid = null; + /** * Create a SystemV shared memory based CacheItemPool. * @@ -70,13 +86,16 @@ class SysVCacheItemPool implements CacheItemPoolInterface * @type int $variableKey The variable key for getting the data from the shared memory. **Defaults to** 1. * @type string $proj The project identifier for ftok. This needs to be a one character string. * **Defaults to** 'A'. + * @type string $semProj The project identifier for ftok to provide to `sem_get`. This needs to be a one + * character string. + * **Defaults to** 'B'. * @type int $memsize The memory size in bytes for shm_attach. **Defaults to** 10000. * @type int $perm The permission for shm_attach. **Defaults to** 0600. * } */ public function __construct($options = []) { - if (! extension_loaded('sysvshm')) { + if (!extension_loaded('sysvshm')) { throw new \RuntimeException( 'sysvshm extension is required to use this ItemPool' ); @@ -84,12 +103,20 @@ public function __construct($options = []) $this->options = $options + [ 'variableKey' => self::VAR_KEY, 'proj' => self::DEFAULT_PROJ, + 'semProj' => self::DEFAULT_SEM_PROJ, 'memsize' => self::DEFAULT_MEMSIZE, 'perm' => self::DEFAULT_PERM ]; $this->items = []; $this->deferredItems = []; $this->sysvKey = ftok(__FILE__, $this->options['proj']); + + // gracefully handle when `sysvsem` isn't loaded + // @TODO(v2): throw an exception when the extension isn't loaded + if (extension_loaded('sysvsem')) { + $semKey = ftok(__FILE__, $this->options['semProj']); + $this->semId = sem_get($semKey, 1, $this->options['perm'], true); + } } /** @@ -132,9 +159,17 @@ public function hasItem($key): bool */ public function clear(): bool { + if (!$this->acquireLock()) { + return false; + } + $this->items = []; $this->deferredItems = []; - return $this->saveCurrentItems(); + $ret = $this->saveCurrentItems(); + + $this->resetShm(); + $this->releaseLock(); + return $ret; } /** @@ -150,6 +185,10 @@ public function deleteItem($key): bool */ public function deleteItems(array $keys): bool { + if (!$this->acquireLock()) { + return false; + } + if (!$this->hasLoadedItems) { $this->loadItems(); } @@ -157,7 +196,11 @@ public function deleteItems(array $keys): bool foreach ($keys as $key) { unset($this->items[$key]); } - return $this->saveCurrentItems(); + $ret = $this->saveCurrentItems(); + + $this->resetShm(); + $this->releaseLock(); + return $ret; } /** @@ -165,12 +208,18 @@ public function deleteItems(array $keys): bool */ public function save(CacheItemInterface $item): bool { + if (!$this->acquireLock()) { + return false; + } + if (!$this->hasLoadedItems) { $this->loadItems(); } $this->items[$item->getKey()] = $item; - return $this->saveCurrentItems(); + $ret = $this->saveCurrentItems(); + $this->releaseLock(); + return $ret; } /** @@ -187,12 +236,18 @@ public function saveDeferred(CacheItemInterface $item): bool */ public function commit(): bool { + if (!$this->acquireLock()) { + return false; + } + foreach ($this->deferredItems as $item) { if ($this->save($item) === false) { + $this->releaseLock(); return false; } } $this->deferredItems = []; + $this->releaseLock(); return true; } @@ -203,20 +258,21 @@ public function commit(): bool */ private function saveCurrentItems() { - $shmid = shm_attach( - $this->sysvKey, - $this->options['memsize'], - $this->options['perm'] - ); - if ($shmid !== false) { - $ret = shm_put_var( + if (!$this->acquireLock()) { + return false; + } + + if (false !== $shmid = $this->attachShm()) { + $success = shm_put_var( $shmid, $this->options['variableKey'], $this->items ); shm_detach($shmid); - return $ret; + $this->releaseLock(); + return $success; } + $this->releaseLock(); return false; } @@ -227,22 +283,70 @@ private function saveCurrentItems() */ private function loadItems() { - $shmid = shm_attach( - $this->sysvKey, - $this->options['memsize'], - $this->options['perm'] - ); - if ($shmid !== false) { + if (!$this->acquireLock()) { + return false; + } + + if (false !== $shmid = $this->attachShm()) { $data = @shm_get_var($shmid, $this->options['variableKey']); - if (!empty($data)) { - $this->items = $data; - } else { - $this->items = []; - } + $this->items = $data ?: []; shm_detach($shmid); $this->hasLoadedItems = true; + $this->releaseLock(); + return true; + } + $this->releaseLock(); + return false; + } + + private function acquireLock(): bool + { + if ($this->semId === false) { + // if `sysvsem` isn't loaded, or if `sem_get` fails, return true + // this ensures BC with previous versions of the auth library. + // @TODO consider better handling when `sem_get` fails. + return true; + } + + $currentPid = getmypid(); + if ($this->lockOwnerPid === $currentPid) { + // We already have the lock + return true; + } + + if (sem_acquire($this->semId)) { + $this->lockOwnerPid = (int) $currentPid; return true; } return false; } + + private function releaseLock(): bool + { + if ($this->semId === false || $this->lockOwnerPid !== getmypid()) { + return true; + } + + $this->lockOwnerPid = null; + return sem_release($this->semId); + } + + private function resetShm(): void + { + // Remove the shared memory segment and semaphore when clearing the cache + $shmid = @shm_attach($this->sysvKey); + if ($shmid !== false) { + @shm_remove($shmid); + @shm_detach($shmid); + } + } + + private function attachShm(): SysvSharedMemory|false + { + return shm_attach( + $this->sysvKey, + $this->options['memsize'], + $this->options['perm'] + ); + } } diff --git a/src/Credentials/ExternalAccountCredentials.php b/src/Credentials/ExternalAccountCredentials.php index c0306ee807..afaf1ee3f8 100644 --- a/src/Credentials/ExternalAccountCredentials.php +++ b/src/Credentials/ExternalAccountCredentials.php @@ -35,6 +35,15 @@ use GuzzleHttp\Psr7\Request; use InvalidArgumentException; +/** + * **IMPORTANT**: + * This class does not validate the credential configuration. A security + * risk occurs when a credential configuration configured with malicious urls + * is used. + * When the credential configuration is accepted from an + * untrusted source, you should validate it before creating this class. + * @see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials + */ class ExternalAccountCredentials implements FetchAuthTokenInterface, UpdateMetadataInterface, diff --git a/src/Credentials/ImpersonatedServiceAccountCredentials.php b/src/Credentials/ImpersonatedServiceAccountCredentials.php index 8842cd17c9..f4a339b2bf 100644 --- a/src/Credentials/ImpersonatedServiceAccountCredentials.php +++ b/src/Credentials/ImpersonatedServiceAccountCredentials.php @@ -30,6 +30,15 @@ use InvalidArgumentException; use LogicException; +/** + * **IMPORTANT**: + * This class does not validate the credential configuration. A security + * risk occurs when a credential configuration configured with malicious urls + * is used. + * When the credential configuration is accepted from an + * untrusted source, you should validate it before creating this class. + * @see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials + */ class ImpersonatedServiceAccountCredentials extends CredentialsLoader implements SignBlobInterface, GetUniverseDomainInterface @@ -78,11 +87,14 @@ class ImpersonatedServiceAccountCredentials extends CredentialsLoader implements * @type string[] $delegates The delegates to impersonate * } * @param string|null $targetAudience The audience to request an ID token. + * @param string|string[]|null $defaultScope The scopes to be used if no "scopes" field exists + * in the `$jsonKey`. */ public function __construct( string|array|null $scope, string|array $jsonKey, - private ?string $targetAudience = null + private ?string $targetAudience = null, + string|array|null $defaultScope = null, ) { if (is_string($jsonKey)) { if (!file_exists($jsonKey)) { @@ -101,6 +113,9 @@ public function __construct( if (!array_key_exists('source_credentials', $jsonKey)) { throw new LogicException('json key is missing the source_credentials field'); } + + $jsonKeyScope = $jsonKey['scopes'] ?? null; + $scope = $scope ?: $jsonKeyScope ?: $defaultScope; if ($scope && $targetAudience) { throw new InvalidArgumentException( 'Scope and targetAudience cannot both be supplied' @@ -118,7 +133,13 @@ public function __construct( // an ID token, the narrowest scope we can request is `iam`. $scope = self::IAM_SCOPE; } - $jsonKey['source_credentials'] = CredentialsLoader::makeCredentials($scope, $jsonKey['source_credentials']); + $jsonKey['source_credentials'] = match ($jsonKey['source_credentials']['type'] ?? null) { + // Do not pass $defaultScope to ServiceAccountCredentials + 'service_account' => new ServiceAccountCredentials($scope, $jsonKey['source_credentials']), + 'authorized_user' => new UserRefreshCredentials($scope, $jsonKey['source_credentials']), + 'external_account' => new ExternalAccountCredentials($scope, $jsonKey['source_credentials']), + default => throw new \InvalidArgumentException('invalid value in the type field'), + }; } $this->targetScope = $scope ?? []; diff --git a/src/CredentialsLoader.php b/src/CredentialsLoader.php index 9fc07416da..118f3a902d 100644 --- a/src/CredentialsLoader.php +++ b/src/CredentialsLoader.php @@ -120,19 +120,38 @@ public static function fromWellKnownFile() /** * Create a new Credentials instance. * - * **Important**: If you accept a credential configuration (credential JSON/File/Stream) from an - * external source for authentication to Google Cloud Platform, you must validate it before - * providing it to any Google API or library. Providing an unvalidated credential configuration to - * Google APIs can compromise the security of your systems and data. For more information - * {@see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials} + * @deprecated This method is being deprecated because of a potential security risk. * - * @param string|string[] $scope the scope of the access request, expressed - * either as an Array or as a space-delimited String. - * @param array $jsonKey the JSON credentials. - * @param string|string[] $defaultScope The default scope to use if no - * user-defined scopes exist, expressed either as an Array or as a - * space-delimited string. + * This method does not validate the credential configuration. The security + * risk occurs when a credential configuration is accepted from a source + * that is not under your control and used without validation on your side. * + * If you know that you will be loading credential configurations of a + * specific type, it is recommended to use a credential-type-specific + * method. + * This will ensure that an unexpected credential type with potential for + * malicious intent is not loaded unintentionally. You might still have to do + * validation for certain credential types. Please follow the recommendation + * for that method. For example, if you want to load only service accounts, + * you can create the {@see ServiceAccountCredentials} explicitly: + * + * ``` + * use Google\Auth\Credentials\ServiceAccountCredentials; + * $creds = new ServiceAccountCredentials($scopes, $json); + * ``` + * + * If you are loading your credential configuration from an untrusted source and have + * not mitigated the risks (e.g. by validating the configuration yourself), make + * these changes as soon as possible to prevent security risks to your environment. + * + * Regardless of the method used, it is always your responsibility to validate + * configurations received from external sources. + * + * @see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials + * + * @param string|string[] $scope + * @param array $jsonKey + * @param string|string[] $defaultScope * @return ServiceAccountCredentials|UserRefreshCredentials|ImpersonatedServiceAccountCredentials|ExternalAccountCredentials */ public static function makeCredentials( @@ -155,8 +174,7 @@ public static function makeCredentials( } if ($jsonKey['type'] == 'impersonated_service_account') { - $anyScope = $scope ?: $defaultScope; - return new ImpersonatedServiceAccountCredentials($anyScope, $jsonKey); + return new ImpersonatedServiceAccountCredentials($scope, $jsonKey, null, $defaultScope); } if ($jsonKey['type'] == 'external_account') { diff --git a/src/Logging/LoggingTrait.php b/src/Logging/LoggingTrait.php index 2441a9bd7a..0b8330d78a 100644 --- a/src/Logging/LoggingTrait.php +++ b/src/Logging/LoggingTrait.php @@ -36,6 +36,7 @@ private function logRequest(RpcLogEvent $event): void 'severity' => strtoupper(LogLevel::DEBUG), 'processId' => $event->processId ?? null, 'requestId' => $event->requestId ?? null, + 'rpcName' => $event->rpcName ?? null, ]; $debugEvent = array_filter($debugEvent, fn ($value) => !is_null($value)); diff --git a/tests/ApplicationDefaultCredentialsTest.php b/tests/ApplicationDefaultCredentialsTest.php index 1db9b9e43e..873b289a74 100644 --- a/tests/ApplicationDefaultCredentialsTest.php +++ b/tests/ApplicationDefaultCredentialsTest.php @@ -70,7 +70,7 @@ public function testLoadsOKIfEnvSpecifiedIsValid() public function testLoadsDefaultFileIfPresentAndEnvVarIsNotSet() { - putenv('HOME=' . __DIR__ . '/fixtures'); + setHomeEnv(__DIR__ . '/fixtures'); $this->assertNotNull( ApplicationDefaultCredentials::getCredentials('a scope') ); @@ -80,7 +80,7 @@ public function testFailsIfNotOnGceAndNoDefaultFileFound() { $this->expectException(DomainException::class); - putenv('HOME=' . __DIR__ . '/not_exist_fixtures'); + setHomeEnv(__DIR__ . '/not_exist_fixtures'); // simulate not being GCE and retry attempts by returning multiple 500s $httpHandler = getHandler([ new Response(500), @@ -93,7 +93,7 @@ public function testFailsIfNotOnGceAndNoDefaultFileFound() public function testSuccedsIfNoDefaultFilesButIsOnGCE() { - putenv('HOME'); + setHomeEnv(null); $wantedTokens = [ 'access_token' => '1/abdef1234567890', @@ -116,7 +116,7 @@ public function testSuccedsIfNoDefaultFilesButIsOnGCE() public function testGceCredentials() { - putenv('HOME'); + setHomeEnv(null); $jsonTokens = json_encode(['access_token' => 'abc']); @@ -160,7 +160,7 @@ public function testGceCredentials() public function testImpersonatedServiceAccountCredentials() { - putenv('HOME=' . __DIR__ . '/fixtures5'); + setHomeEnv(__DIR__ . '/fixtures5'); $creds = ApplicationDefaultCredentials::getCredentials( null, null, @@ -183,7 +183,7 @@ public function testImpersonatedServiceAccountCredentials() public function testUserRefreshCredentials() { - putenv('HOME=' . __DIR__ . '/fixtures2'); + setHomeEnv(__DIR__ . '/fixtures2'); $creds = ApplicationDefaultCredentials::getCredentials( null, // $scope @@ -219,7 +219,7 @@ public function testUserRefreshCredentials() public function testServiceAccountCredentials() { - putenv('HOME=' . __DIR__ . '/fixtures'); + setHomeEnv(__DIR__ . '/fixtures'); $creds = ApplicationDefaultCredentials::getCredentials( null, // $scope @@ -255,7 +255,7 @@ public function testServiceAccountCredentials() public function testDefaultScopeArray() { - putenv('HOME=' . __DIR__ . '/fixtures2'); + setHomeEnv(__DIR__ . '/fixtures2'); $creds = ApplicationDefaultCredentials::getCredentials( null, // $scope @@ -292,7 +292,7 @@ public function testGetMiddlewareLoadsOKIfEnvSpecifiedIsValid() public function testLGetMiddlewareoadsDefaultFileIfPresentAndEnvVarIsNotSet() { - putenv('HOME=' . __DIR__ . '/fixtures'); + setHomeEnv(__DIR__ . '/fixtures'); $this->assertNotNull(ApplicationDefaultCredentials::getMiddleware('a scope')); } @@ -300,7 +300,7 @@ public function testGetMiddlewareFailsIfNotOnGceAndNoDefaultFileFound() { $this->expectException(DomainException::class); - putenv('HOME=' . __DIR__ . '/not_exist_fixtures'); + setHomeEnv(__DIR__ . '/not_exist_fixtures'); // simulate not being GCE and retry attempts by returning multiple 500s $httpHandler = getHandler([ @@ -356,7 +356,7 @@ public function testOnGceCacheWithHit() { $this->expectException(DomainException::class); - putenv('HOME=' . __DIR__ . '/not_exist_fixtures'); + setHomeEnv(__DIR__ . '/not_exist_fixtures'); $mockCacheItem = $this->prophesize('Psr\Cache\CacheItemInterface'); $mockCacheItem->isHit() @@ -380,7 +380,7 @@ public function testOnGceCacheWithHit() public function testOnGceCacheWithoutHit() { - putenv('HOME=' . __DIR__ . '/not_exist_fixtures'); + setHomeEnv(__DIR__ . '/not_exist_fixtures'); $gceIsCalled = false; $dummyHandler = function ($request) use (&$gceIsCalled) { @@ -416,7 +416,7 @@ public function testOnGceCacheWithoutHit() public function testOnGceCacheWithOptions() { - putenv('HOME=' . __DIR__ . '/not_exist_fixtures'); + setHomeEnv(__DIR__ . '/not_exist_fixtures'); $prefix = 'test_prefix_'; $lifetime = '70707'; @@ -473,7 +473,7 @@ public function testGetIdTokenCredentialsLoadsOKIfEnvSpecifiedIsValid() public function testGetIdTokenCredentialsLoadsDefaultFileIfPresentAndEnvVarIsNotSet() { - putenv('HOME=' . __DIR__ . '/fixtures'); + setHomeEnv(__DIR__ . '/fixtures'); $creds = ApplicationDefaultCredentials::getIdTokenCredentials($this->targetAudience); $this->assertInstanceOf(ServiceAccountCredentials::class, $creds); } @@ -483,7 +483,7 @@ public function testGetIdTokenCredentialsFailsIfNotOnGceAndNoDefaultFileFound() $this->expectException(DomainException::class); $this->expectExceptionMessage('Your default credentials were not found'); - putenv('HOME=' . __DIR__ . '/not_exist_fixtures'); + setHomeEnv(__DIR__ . '/not_exist_fixtures'); // simulate not being GCE and retry attempts by returning multiple 500s $httpHandler = getHandler([ @@ -500,7 +500,7 @@ public function testGetIdTokenCredentialsFailsIfNotOnGceAndNoDefaultFileFound() public function testGetIdTokenCredentialsWithImpersonatedServiceAccountCredentials() { - putenv('HOME=' . __DIR__ . '/fixtures5'); + setHomeEnv(__DIR__ . '/fixtures5'); $creds = ApplicationDefaultCredentials::getIdTokenCredentials('123@456.com'); $this->assertInstanceOf(ImpersonatedServiceAccountCredentials::class, $creds); } @@ -529,7 +529,7 @@ public function testGetIdTokenCredentialsWithCacheOptions() public function testGetIdTokenCredentialsSuccedsIfNoDefaultFilesButIsOnGCE() { - putenv('HOME=' . __DIR__ . '/not_exist_fixtures'); + setHomeEnv(__DIR__ . '/not_exist_fixtures'); $wantedTokens = [ 'access_token' => '1/abdef1234567890', 'expires_in' => '57', @@ -553,7 +553,7 @@ public function testGetIdTokenCredentialsSuccedsIfNoDefaultFilesButIsOnGCE() public function testGetIdTokenCredentialsWithUserRefreshCredentials() { - putenv('HOME=' . __DIR__ . '/fixtures2'); + setHomeEnv(__DIR__ . '/fixtures2'); $creds = ApplicationDefaultCredentials::getIdTokenCredentials( $this->targetAudience, @@ -610,7 +610,7 @@ public function testGetCredentialsUtilizesQuotaProjectEnvVar() { $quotaProject = 'quota-project-from-env-var'; putenv(CredentialsLoader::QUOTA_PROJECT_ENV_VAR . '=' . $quotaProject); - putenv('HOME=' . __DIR__ . '/fixtures'); + setHomeEnv(__DIR__ . '/fixtures'); $credentials = ApplicationDefaultCredentials::getCredentials(); @@ -625,7 +625,7 @@ public function testGetCredentialsUtilizesQuotaProjectParameterOverEnvVar() { $quotaProject = 'quota-project-from-parameter'; putenv(CredentialsLoader::QUOTA_PROJECT_ENV_VAR . '=quota-project-from-env-var'); - putenv('HOME=' . __DIR__ . '/fixtures'); + setHomeEnv(__DIR__ . '/fixtures'); $credentials = ApplicationDefaultCredentials::getCredentials( null, // $scope @@ -688,7 +688,7 @@ public function testWithFetchAuthTokenCacheAndExplicitQuotaProject() public function testWithGCECredentials() { - putenv('HOME=' . __DIR__ . '/not_exist_fixtures'); + setHomeEnv(__DIR__ . '/not_exist_fixtures'); $wantedTokens = [ 'access_token' => '1/abdef1234567890', 'expires_in' => '57', @@ -721,7 +721,7 @@ public function testWithGCECredentials() public function testAppEngineStandard() { $_SERVER['SERVER_SOFTWARE'] = 'Google App Engine'; - putenv('HOME=' . __DIR__ . '/not_exist_fixtures'); + setHomeEnv(__DIR__ . '/not_exist_fixtures'); $this->assertInstanceOf( 'Google\Auth\Credentials\AppIdentityCredentials', ApplicationDefaultCredentials::getCredentials() @@ -732,7 +732,7 @@ public function testAppEngineFlexible() { $_SERVER['SERVER_SOFTWARE'] = 'Google App Engine'; putenv('GAE_INSTANCE=aef-default-20180313t154438'); - putenv('HOME=' . __DIR__ . '/not_exist_fixtures'); + setHomeEnv(__DIR__ . '/not_exist_fixtures'); $httpHandler = getHandler([ new Response(200, [GCECredentials::FLAVOR_HEADER => 'Google']), ]); @@ -746,7 +746,7 @@ public function testAppEngineFlexibleIdToken() { $_SERVER['SERVER_SOFTWARE'] = 'Google App Engine'; putenv('GAE_INSTANCE=aef-default-20180313t154438'); - putenv('HOME=' . __DIR__ . '/not_exist_fixtures'); + setHomeEnv(__DIR__ . '/not_exist_fixtures'); $httpHandler = getHandler([ new Response(200, [GCECredentials::FLAVOR_HEADER => 'Google']), ]); @@ -868,7 +868,7 @@ public function testUniverseDomainInKeyFile() /** @runInSeparateProcess */ public function testUniverseDomainInGceCredentials() { - putenv('HOME'); + setHomeEnv(null); $expectedUniverseDomain = 'example-universe.com'; $creds = ApplicationDefaultCredentials::getCredentials( diff --git a/tests/Cache/FileSystemCacheItemPoolTest.php b/tests/Cache/FileSystemCacheItemPoolTest.php index 86b3e4eb55..8e262eb667 100644 --- a/tests/Cache/FileSystemCacheItemPoolTest.php +++ b/tests/Cache/FileSystemCacheItemPoolTest.php @@ -21,10 +21,12 @@ use Google\Auth\Cache\TypedItem; use PHPUnit\Framework\TestCase; use Psr\Cache\InvalidArgumentException; +use Symfony\Component\Filesystem\Filesystem; class FileSystemCacheItemPoolTest extends TestCase { - private string $defaultCacheDirectory = '.cache'; + private string $cachePath; + private Filesystem $filesystem; private FileSystemCacheItemPool $pool; private array $invalidChars = [ '`', '~', '!', '@', '#', '$', @@ -36,28 +38,21 @@ class FileSystemCacheItemPoolTest extends TestCase public function setUp(): void { - $this->pool = new FileSystemCacheItemPool($this->defaultCacheDirectory); + $this->cachePath = sys_get_temp_dir() . '/google_auth_php_test/'; + $this->filesystem = new Filesystem(); + $this->filesystem->remove($this->cachePath); + $this->pool = new FileSystemCacheItemPool($this->cachePath); } public function tearDown(): void { - $files = scandir($this->defaultCacheDirectory); - - foreach ($files as $fileName) { - if ($fileName === '.' || $fileName === '..') { - continue; - } - - unlink($this->defaultCacheDirectory . '/' . $fileName); - } - - rmdir($this->defaultCacheDirectory); + $this->filesystem->remove($this->cachePath); } public function testInstanceCreatesCacheFolder() { - $this->assertTrue(file_exists($this->defaultCacheDirectory)); - $this->assertTrue(is_dir($this->defaultCacheDirectory)); + $this->assertTrue(file_exists($this->cachePath)); + $this->assertTrue(is_dir($this->cachePath)); } public function testSaveAndGetItem() @@ -134,10 +129,10 @@ public function testClear() { $item = $this->getNewItem(); $this->pool->save($item); - $this->assertLessThan(scandir($this->defaultCacheDirectory), 2); + $this->assertLessThan(scandir($this->cachePath), 2); $this->pool->clear(); // Clear removes all the files, but scandir returns `.` and `..` as files - $this->assertEquals(count(scandir($this->defaultCacheDirectory)), 2); + $this->assertEquals(count(scandir($this->cachePath)), 2); } public function testSaveDeferredAndCommit() diff --git a/tests/Cache/RaceConditionTest.php b/tests/Cache/RaceConditionTest.php new file mode 100644 index 0000000000..f6b9e3216b --- /dev/null +++ b/tests/Cache/RaceConditionTest.php @@ -0,0 +1,114 @@ +remove(self::$cachePath); + } + + /** + * @runInSeparateProcess + * @dataProvider provideRaceCondition + */ + public function testRaceCondition(string $cacheClass) + { + if (!function_exists('pcntl_fork')) { + $this->markTestSkipped('pcntl_fork is not available'); + } + for ($i = 0; $i < 50; $i++) { + $pids = []; + for ($j = 0; $j < 4; $j++) { + $pid = pcntl_fork(); + if ($pid == -1) { + $this->fail('Could not fork'); + } + $pool = $this->createCacheItemPool($cacheClass); + $item = $pool->getItem('foo'); + $item->set('bar'); + $this->assertTrue($pool->save($item)); + + if ($pid) { + // parent + $pids[] = $pid; + } else { + // child + exit(0); + } + } + + // parent + $this->assertTrue($pool->save($item)); + + foreach ($pids as $pid) { + pcntl_waitpid($pid, $status); + $this->assertEquals(0, $status); + } + + $this->assertTrue($pool->hasItem('foo')); + $cachedItem = $pool->getItem('foo'); + $this->assertEquals('bar', $cachedItem->get()); + + $pool->clear(); + } + } + + public function createCacheItemPool(string $cacheClass): CacheItemPoolInterface + { + switch ($cacheClass) { + case FileSystemCacheItemPool::class: + $cachePath = self::$cachePath . '/google_auth_php_test-' . rand(); + return new FileSystemCacheItemPool($cachePath); + case MemoryCacheItemPool::class: + return new MemoryCacheItemPool(); + case SysVCacheItemPool::class: + return new SysVCacheItemPool(); + } + + throw new \Exception('Unrecognized cache class: ' . $cacheClass); + } + + public function provideRaceCondition() + { + return [ + [FileSystemCacheItemPool::class], + [MemoryCacheItemPool::class], + [SysVCacheItemPool::class], + ]; + } + + public static function tearDownAfterClass(): void + { + // remove all files generated from the filecaches + self::$filesystem->remove(self::$cachePath); + } +} diff --git a/tests/Cache/SysVCacheItemPoolTest.php b/tests/Cache/SysVCacheItemPoolTest.php index 2c2d5be2e3..d85e60152f 100644 --- a/tests/Cache/SysVCacheItemPoolTest.php +++ b/tests/Cache/SysVCacheItemPoolTest.php @@ -23,6 +23,8 @@ class SysVCacheItemPoolTest extends BaseTest { + const VARIABLE_KEY = 99; + private $pool; public function setUp(): void @@ -32,10 +34,17 @@ public function setUp(): void 'sysvshm extension is required for running the test' ); } - $this->pool = new SysVCacheItemPool(['variableKey' => 99]); + $this->pool = new SysVCacheItemPool(['variableKey' => self::VARIABLE_KEY]); $this->pool->clear(); } + public function tearDown(): void + { + if (extension_loaded('sysvshm')) { + $this->pool->clear(); + } + } + public function saveItem($key, $value) { $item = $this->pool->getItem($key); @@ -158,4 +167,39 @@ public function testCommitsDeferredItems() $this->pool->getItem($keys[1])->get() ); } + + public function testRaceCondition() + { + if (!extension_loaded('sysvsem')) { + $this->markTestSkipped( + 'sysvsem extension is required for running the race condition test' + ); + } + + $key = 'race-item'; + $initialValue = 0; + $this->saveItem($key, $initialValue); + + $numProcesses = 100; + $processes = []; + for ($i = 0; $i < $numProcesses; $i++) { + $command = sprintf( + 'php %s/sysv_cache_race_condition_writer.php %s %s', + __DIR__, + $key, + self::VARIABLE_KEY + ); + $processes[] = proc_open($command, [], $pipes); + } + + foreach ($processes as $process) { + // proc_close waits for the process to terminate and returns its exit code. + // This ensures that all child processes have completed their writes + // before the parent process proceeds to read the final value. + proc_close($process); + } + + $finalValue = $this->pool->getItem($key)->get(); + $this->assertEquals($numProcesses, $finalValue); + } } diff --git a/tests/Cache/sysv_cache_race_condition_writer.php b/tests/Cache/sysv_cache_race_condition_writer.php new file mode 100644 index 0000000000..384db8fcd2 --- /dev/null +++ b/tests/Cache/sysv_cache_race_condition_writer.php @@ -0,0 +1,26 @@ + $argv[2]]); + +$key = $argv[1]; + +$semKey = ftok(__FILE__, 'B'); +$semId = sem_get($semKey); +if (sem_acquire($semId)) { + $item = $pool->getItem($key); + $value = (int) $item->get(); + $value++; + usleep(10000); // Simulate some work + $item->set($value); + $pool->save($item); + + sem_release($semId); +} diff --git a/tests/Credentials/ExternalAccountCredentialsTest.php b/tests/Credentials/ExternalAccountCredentialsTest.php index 3cade03037..60149b7970 100644 --- a/tests/Credentials/ExternalAccountCredentialsTest.php +++ b/tests/Credentials/ExternalAccountCredentialsTest.php @@ -586,7 +586,12 @@ public function testExecutableSourceCacheKey() */ public function testExecutableCredentialSourceEnvironmentVars() { + if (PHP_OS_FAMILY === 'Windows') { + $this->markTestSkipped('This test does not work on Windows'); + } + putenv('GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES=1'); + $tmpFile = tempnam(sys_get_temp_dir(), 'test'); $outputFile = tempnam(sys_get_temp_dir(), 'output'); $fileContents = 'foo-' . rand(); @@ -597,20 +602,23 @@ public function testExecutableCredentialSourceEnvironmentVars() 'id_token' => 'abc', 'expiration_time' => time() + 100, ]); + + $command = sprintf( + 'echo $GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE,$GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE,%s > %s' . + ' && echo \'%s\' > $GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE' . + ' && echo \'%s\'', + $fileContents, + $tmpFile, + $successJson, + $successJson + ); + $json = [ 'audience' => 'test-audience', 'subject_token_type' => 'test-token-type', 'credential_source' => [ 'executable' => [ - 'command' => sprintf( - 'echo $GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE,$GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE,%s > %s' . - ' && echo \'%s\' > $GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE ' . - ' && echo \'%s\'', - $fileContents, - $tmpFile, - $successJson, - $successJson, - ), + 'command' => $command, 'timeout_millis' => 5000, 'output_file' => $outputFile, ], diff --git a/tests/Credentials/ImpersonatedServiceAccountCredentialsTest.php b/tests/Credentials/ImpersonatedServiceAccountCredentialsTest.php index d923c226be..9ab446fcdb 100644 --- a/tests/Credentials/ImpersonatedServiceAccountCredentialsTest.php +++ b/tests/Credentials/ImpersonatedServiceAccountCredentialsTest.php @@ -485,4 +485,70 @@ public function testIdTokenWithAuthTokenMiddleware() $this->assertEquals(1, $requestCount); } + + /** + * @dataProvider provideScopePrecedence + */ + public function testScopePrecedence( + string|array|null $userScope, + string|array|null $jsonKeyScope, + string|null $defaultScope, + string|array $expectedScope + ) { + $jsonKey = self::SERVICE_ACCOUNT_TO_SERVICE_ACCOUNT_JSON; + $jsonKey['scopes'] = $jsonKeyScope; + $credentials = new ImpersonatedServiceAccountCredentials( + scope: $userScope, + jsonKey: $jsonKey, + defaultScope: $defaultScope, + ); + + $scopeProp = (new ReflectionClass($credentials))->getProperty('targetScope'); + $this->assertEquals($expectedScope, $scopeProp->getValue($credentials)); + } + + public function testScopePrecedenceWithNoJsonKey() + { + $defaultScope = 'a-default-scope'; + $jsonKey = self::SERVICE_ACCOUNT_TO_SERVICE_ACCOUNT_JSON; + $credentials = new ImpersonatedServiceAccountCredentials( + scope: null, + jsonKey: $jsonKey, + defaultScope: $defaultScope, + ); + + $scopeProp = (new ReflectionClass($credentials))->getProperty('targetScope'); + $this->assertEquals($defaultScope, $scopeProp->getValue($credentials)); + } + + public function provideScopePrecedence() + { + $userScope = 'a-user-scope'; + $jsonKeyScope = 'a-json-key-scope'; + $defaultScope = 'a-default-scope'; + return [ + // User scope always takes precendence + [$userScope, $jsonKeyScope, $defaultScope, 'expectedScope' => $userScope], + [$userScope, null, $defaultScope, 'expectedScope' => $userScope], + [$userScope, $jsonKeyScope, null, 'expectedScope' => $userScope], + [$userScope, null, null, 'expectedScope' => $userScope], + + // JSON Key Scope is next + [null, $jsonKeyScope, $defaultScope, 'expectedScope' => $jsonKeyScope], + [null, $jsonKeyScope, null, 'expectedScope' => $jsonKeyScope], + + // Default Scope is last + [null, null, $defaultScope, 'expectedScope' => $defaultScope], + // JSON Key scope is exists but is an empty array, still return default + [null, [], $defaultScope, 'expectedScope' => $defaultScope], + + // No scope is empty array + [null, null, null, 'expectedScope' => []], + + // Test empty strings and arrays + ['', $jsonKeyScope, null, 'expectedScope' => $jsonKeyScope], + [[], $jsonKeyScope, null, 'expectedScope' => $jsonKeyScope], + [[], '', $defaultScope, 'expectedScope' => $defaultScope], + ]; + } } diff --git a/tests/Credentials/ServiceAccountCredentialsTest.php b/tests/Credentials/ServiceAccountCredentialsTest.php index 63110448e8..3af95e939a 100644 --- a/tests/Credentials/ServiceAccountCredentialsTest.php +++ b/tests/Credentials/ServiceAccountCredentialsTest.php @@ -192,7 +192,7 @@ public function testSucceedIfFileExists() /** @runInSeparateProcess */ public function testIsNullIfFileDoesNotExist() { - putenv('HOME=' . __DIR__ . '/../not_exists_fixtures'); + setHomeEnv(__DIR__ . '/../not_exists_fixtures'); $this->assertNull( ServiceAccountCredentials::fromWellKnownFile() ); @@ -201,7 +201,7 @@ public function testIsNullIfFileDoesNotExist() /** @runInSeparateProcess */ public function testSucceedIfFileIsPresent() { - putenv('HOME=' . __DIR__ . '/../fixtures'); + setHomeEnv(__DIR__ . '/../fixtures'); $this->assertNotNull( ApplicationDefaultCredentials::getCredentials('a scope') ); diff --git a/tests/Credentials/UserRefreshCredentialsTest.php b/tests/Credentials/UserRefreshCredentialsTest.php index 7a8f393b90..0432182492 100644 --- a/tests/Credentials/UserRefreshCredentialsTest.php +++ b/tests/Credentials/UserRefreshCredentialsTest.php @@ -40,7 +40,7 @@ protected function tearDown(): void { putenv(UserRefreshCredentials::ENV_VAR); // removes it from if ($this->originalHome != getenv('HOME')) { - putenv('HOME=' . $this->originalHome); + setHomeEnv($this->originalHome); } } @@ -179,7 +179,7 @@ public function testSucceedIfFileExists() public function testIsNullIfFileDoesNotExist() { - putenv('HOME=' . __DIR__ . '/../not_exist_fixtures'); + setHomeEnv(__DIR__ . '/../not_exist_fixtures'); $this->assertNull( UserRefreshCredentials::fromWellKnownFile('a scope') ); @@ -187,7 +187,7 @@ public function testIsNullIfFileDoesNotExist() public function testSucceedIfFileIsPresent() { - putenv('HOME=' . __DIR__ . '/../fixtures2'); + setHomeEnv(__DIR__ . '/../fixtures2'); $this->assertNotNull( ApplicationDefaultCredentials::getCredentials('a scope') ); diff --git a/tests/CredentialsLoaderTest.php b/tests/CredentialsLoaderTest.php index dcf257b765..d0336766c5 100644 --- a/tests/CredentialsLoaderTest.php +++ b/tests/CredentialsLoaderTest.php @@ -35,7 +35,7 @@ public function testUpdateMetadataSkipsWhenAuthenticationisSet() /** @runInSeparateProcess */ public function testGetDefaultClientCertSource() { - putenv('HOME=' . __DIR__ . '/fixtures4/valid'); + setHomeEnv(__DIR__ . '/fixtures4/valid'); $callback = CredentialsLoader::getDefaultClientCertSource(); $this->assertNotNull($callback); @@ -47,7 +47,7 @@ public function testGetDefaultClientCertSource() /** @runInSeparateProcess */ public function testNonExistantDefaultClientCertSource() { - putenv('HOME='); + setHomeEnv(null); $callback = CredentialsLoader::getDefaultClientCertSource(); $this->assertNull($callback); @@ -61,7 +61,7 @@ public function testDefaultClientCertSourceInvalidJsonThrowsException() $this->expectException(UnexpectedValueException::class); $this->expectExceptionMessage('Invalid client cert source JSON'); - putenv('HOME=' . __DIR__ . '/fixtures4/invalidjson'); + setHomeEnv(__DIR__ . '/fixtures4/invalidjson'); CredentialsLoader::getDefaultClientCertSource(); } @@ -74,7 +74,7 @@ public function testDefaultClientCertSourceInvalidKeyThrowsException() $this->expectException(UnexpectedValueException::class); $this->expectExceptionMessage('cert source requires "cert_provider_command"'); - putenv('HOME=' . __DIR__ . '/fixtures4/invalidkey'); + setHomeEnv(__DIR__ . '/fixtures4/invalidkey'); CredentialsLoader::getDefaultClientCertSource(); } @@ -87,7 +87,7 @@ public function testDefaultClientCertSourceInvalidValueThrowsException() $this->expectException(UnexpectedValueException::class); $this->expectExceptionMessage('cert source expects "cert_provider_command" to be an array'); - putenv('HOME=' . __DIR__ . '/fixtures4/invalidvalue'); + setHomeEnv(__DIR__ . '/fixtures4/invalidvalue'); CredentialsLoader::getDefaultClientCertSource(); } @@ -115,7 +115,7 @@ public function testDefaultClientCertSourceInvalidCmdThrowsException() $this->expectException(RuntimeException::class); $this->expectExceptionMessage('"cert_provider_command" failed with a nonzero exit code'); - putenv('HOME=' . __DIR__ . '/fixtures4/invalidcmd'); + setHomeEnv(__DIR__ . '/fixtures4/invalidcmd'); $callback = CredentialsLoader::getDefaultClientCertSource(); diff --git a/tests/ExecutableHandler/ExecutableHandlerTest.php b/tests/ExecutableHandler/ExecutableHandlerTest.php index 16a3537db2..7561a4b546 100644 --- a/tests/ExecutableHandler/ExecutableHandlerTest.php +++ b/tests/ExecutableHandler/ExecutableHandlerTest.php @@ -26,17 +26,17 @@ class ExecutableHandlerTest extends TestCase public function testEnvironmentVariables() { $handler = new ExecutableHandler(['ENV_VAR_1' => 'foo', 'ENV_VAR_2' => 'bar']); - $this->assertEquals(0, $handler('echo $ENV_VAR_1')); + $this->assertEquals(0, $handler('bash -c "echo $ENV_VAR_1"')); $this->assertEquals("foo\n", $handler->getOutput()); - $this->assertEquals(0, $handler('echo $ENV_VAR_2')); + $this->assertEquals(0, $handler('bash -c "echo $ENV_VAR_2"')); $this->assertEquals("bar\n", $handler->getOutput()); } public function testTimeoutMs() { - $handler = new ExecutableHandler([], 300); - $this->assertEquals(0, $handler('sleep "0.2"')); + $handler = new ExecutableHandler([], 3000); + $this->assertEquals(0, $handler('bash -c \'sleep "0.1"\'')); } public function testTimeoutMsExceeded() @@ -51,7 +51,7 @@ public function testTimeoutMsExceeded() public function testErrorOutputIsReturnedAsOutput() { $handler = new ExecutableHandler(); - $this->assertEquals(0, $handler('echo "Bad Response." >&2')); + $this->assertEquals(0, $handler('bash -c \'echo "Bad Response." >&2\'')); $this->assertEquals("Bad Response.\n", $handler->getOutput()); } } diff --git a/tests/Logging/LoggingTraitTest.php b/tests/Logging/LoggingTraitTest.php index c6058fee72..443f281d3b 100644 --- a/tests/Logging/LoggingTraitTest.php +++ b/tests/Logging/LoggingTraitTest.php @@ -86,6 +86,18 @@ public function testLogResponse() $this->assertEquals($event->headers, $parsedDebugEvent['jsonPayload']['response.headers']); } + public function testRpcNameShouldBeIncluded() + { + $event = $this->getNewLogEvent(); + $event->headers = ['Thisis' => 'a header']; + $this->loggerContainer->logRequest($event); + + $buffer = $this->getActualOutput(); + + $parsedDebugEvent = json_decode($buffer, true); + $this->assertEquals($event->rpcName, $parsedDebugEvent['rpcName']); + } + private function getNewLogEvent(): RpcLogEvent { $event = new RpcLogEvent(); diff --git a/tests/bootstrap.php b/tests/bootstrap.php index e15bbb6290..e62b42ee22 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -28,3 +28,15 @@ function getHandler(array $mockResponses = []) return new \Google\Auth\HttpHandler\Guzzle6HttpHandler($client); } + +function setHomeEnv(string|null $value): void +{ + $assigment = sprintf( + '%s%s%s', + PHP_OS_FAMILY === 'Windows' ? 'APPDATA' : 'HOME', + $value === null ? '' : '=', + (string) $value + ); + + putenv($assigment); +} diff --git a/tests/fixtures/.config/gcloud b/tests/fixtures/.config/gcloud new file mode 120000 index 0000000000..38d21f0a3c --- /dev/null +++ b/tests/fixtures/.config/gcloud @@ -0,0 +1 @@ +../gcloud/ \ No newline at end of file diff --git a/tests/fixtures/.config/gcloud/application_default_credentials.json b/tests/fixtures/gcloud/application_default_credentials.json similarity index 100% rename from tests/fixtures/.config/gcloud/application_default_credentials.json rename to tests/fixtures/gcloud/application_default_credentials.json diff --git a/tests/fixtures2/.config/gcloud b/tests/fixtures2/.config/gcloud new file mode 120000 index 0000000000..38d21f0a3c --- /dev/null +++ b/tests/fixtures2/.config/gcloud @@ -0,0 +1 @@ +../gcloud/ \ No newline at end of file diff --git a/tests/fixtures2/.config/gcloud/application_default_credentials.json b/tests/fixtures2/gcloud/application_default_credentials.json similarity index 100% rename from tests/fixtures2/.config/gcloud/application_default_credentials.json rename to tests/fixtures2/gcloud/application_default_credentials.json diff --git a/tests/fixtures5/.config/gcloud b/tests/fixtures5/.config/gcloud new file mode 120000 index 0000000000..38d21f0a3c --- /dev/null +++ b/tests/fixtures5/.config/gcloud @@ -0,0 +1 @@ +../gcloud/ \ No newline at end of file diff --git a/tests/fixtures5/.config/gcloud/application_default_credentials.json b/tests/fixtures5/gcloud/application_default_credentials.json similarity index 100% rename from tests/fixtures5/.config/gcloud/application_default_credentials.json rename to tests/fixtures5/gcloud/application_default_credentials.json diff --git a/tests/mocks/TestFileCacheItemPool.php b/tests/mocks/TestFileCacheItemPool.php index 65fbc8a774..de9f510c69 100644 --- a/tests/mocks/TestFileCacheItemPool.php +++ b/tests/mocks/TestFileCacheItemPool.php @@ -17,7 +17,6 @@ namespace Google\Auth\Tests; -use Google\Auth\Cache\Item; use Google\Auth\Cache\TypedItem; use Psr\Cache\CacheItemInterface; use Psr\Cache\CacheItemPoolInterface; @@ -68,8 +67,7 @@ public function getItems(array $keys = []): iterable if ($this->hasItem($key)) { $items[$key] = unserialize(file_get_contents($this->cacheDir . '/' . $key)); } else { - $itemClass = \PHP_VERSION_ID >= 80000 ? TypedItem::class : Item::class; - $items[$key] = new $itemClass($key); + $items[$key] = new TypedItem($key); } }