diff --git a/src/FetchAuthTokenCache.php b/src/FetchAuthTokenCache.php index 63f0c827b..bd9de6809 100644 --- a/src/FetchAuthTokenCache.php +++ b/src/FetchAuthTokenCache.php @@ -58,6 +58,7 @@ public function __construct( $this->cacheConfig = array_merge([ 'lifetime' => 1500, 'prefix' => '', + 'cacheUniverseDomain' => $fetcher instanceof Credentials\GCECredentials, ], (array) $cacheConfig); } @@ -212,6 +213,9 @@ public function getProjectId(callable $httpHandler = null) public function getUniverseDomain(): string { if ($this->fetcher instanceof GetUniverseDomainInterface) { + if ($this->cacheConfig['cacheUniverseDomain']) { + return $this->getCachedUniverseDomain($this->fetcher); + } return $this->fetcher->getUniverseDomain(); } @@ -320,4 +324,16 @@ private function saveAuthTokenInCache($authToken, $authUri = null) $this->setCachedValue($cacheKey, $authToken); } } + + private function getCachedUniverseDomain(GetUniverseDomainInterface $fetcher): string + { + $cacheKey = $this->getFullCacheKey($fetcher->getCacheKey() . 'universe_domain'); // @phpstan-ignore-line + if ($universeDomain = $this->getCachedValue($cacheKey)) { + return $universeDomain; + } + + $universeDomain = $fetcher->getUniverseDomain(); + $this->setCachedValue($cacheKey, $universeDomain); + return $universeDomain; + } } diff --git a/tests/FetchAuthTokenCacheTest.php b/tests/FetchAuthTokenCacheTest.php index 09845bb00..d430dc207 100644 --- a/tests/FetchAuthTokenCacheTest.php +++ b/tests/FetchAuthTokenCacheTest.php @@ -22,6 +22,7 @@ use Google\Auth\Credentials\ServiceAccountCredentials; use Google\Auth\CredentialsLoader; use Google\Auth\FetchAuthTokenCache; +use Google\Auth\FetchAuthTokenInterface; use Google\Auth\GetUniverseDomainInterface; use GuzzleHttp\Psr7\Response; use GuzzleHttp\Psr7\Utils; @@ -37,6 +38,7 @@ class FetchAuthTokenCacheTest extends BaseTest private $mockCacheItem; private $mockCache; private $mockSigner; + private static string $cacheKey; protected function setUp(): void { @@ -700,4 +702,99 @@ public function testGetFetcher() $this->assertSame($mockFetcher, $fetcher->getFetcher()); } + + public function testCacheUniverseDomain() + { + $mockFetcher = $this->prophesize(FetchAuthTokenInterface::class); + $mockFetcher->willImplement(GetUniverseDomainInterface::class); + $mockFetcher->getUniverseDomain() + ->shouldBeCalledTimes(2) + ->willReturn('example-universe.domain'); + $mockFetcher->getCacheKey() + ->shouldNotBeCalled(); + + $fetcher = new FetchAuthTokenCache( + $mockFetcher->reveal(), + ['cacheUniverseDomain' => false], + new MemoryCacheItemPool() + ); + + // Call it twice + $this->assertEquals('example-universe.domain', $fetcher->getUniverseDomain()); + $this->assertEquals('example-universe.domain', $fetcher->getUniverseDomain()); + + // Now set the cache option and ensure it's only called once + $mockFetcher = $this->prophesize(FetchAuthTokenInterface::class); + $mockFetcher->willImplement(GetUniverseDomainInterface::class); + $mockFetcher->getUniverseDomain() + ->shouldBeCalledOnce() + ->willReturn('example-universe.domain'); + $mockFetcher->getCacheKey() + ->shouldBeCalledTimes(2) + ->willReturn('my-cache-key'); + + $fetcher = new FetchAuthTokenCache( + $mockFetcher->reveal(), + ['cacheUniverseDomain' => true], + new MemoryCacheItemPool() + ); + $this->assertEquals('example-universe.domain', $fetcher->getUniverseDomain()); + $this->assertEquals('example-universe.domain', $fetcher->getUniverseDomain()); + } + + public function testCacheUniverseDomainByDefaultForGCECredentials() + { + $mockFetcher = $this->prophesize(GCECredentials::class); + $mockFetcher->getUniverseDomain() + ->shouldBeCalledOnce() + ->willReturn('example-universe.domain'); + $mockFetcher->getCacheKey() + ->shouldBeCalledTimes(2) + ->willReturn('my-cache-key'); + + $fetcher = new FetchAuthTokenCache( + $mockFetcher->reveal(), + [], // don't set cacheUniverseDomain, it will be true by default + new MemoryCacheItemPool() + ); + + $this->assertEquals('example-universe.domain', $fetcher->getUniverseDomain()); + $this->assertEquals('example-universe.domain', $fetcher->getUniverseDomain()); + } + + public function testUniverseDomainWithFileCache() + { + require_once __DIR__ . '/mocks/TestFileCacheItemPool.php'; + self::$cacheKey = 'universe-domain-check-' . time() . rand(); + + $cache = new TestFileCacheItemPool(sys_get_temp_dir() . '/google-auth-test'); + + $mockFetcher = $this->prophesize(FetchAuthTokenInterface::class); + $mockFetcher->willImplement(GetUniverseDomainInterface::class); + $mockFetcher->getUniverseDomain() + ->shouldBeCalledOnce() + ->willReturn('example-universe.domain'); + $mockFetcher->getCacheKey() + ->shouldBeCalledOnce() + ->willReturn(self::$cacheKey); + + $fetcher = new FetchAuthTokenCache( + $mockFetcher->reveal(), + ['cacheUniverseDomain' => true], + $cache + ); + $this->assertEquals('example-universe.domain', $fetcher->getUniverseDomain()); + } + + /** + * @depends testUniverseDomainWithFileCache + */ + public function testUniverseDomainWithFileCacheProcess2() + { + $cmd = sprintf('php %s/mocks/test_file_cache_separate_process.php %s', __DIR__, self::$cacheKey); + exec($cmd, $output, $retVar); + + $this->assertEquals(0, $retVar); + $this->assertEquals('example-universe.domain', implode('', $output)); + } } diff --git a/tests/mocks/TestFileCacheItemPool.php b/tests/mocks/TestFileCacheItemPool.php new file mode 100644 index 000000000..42c8d5a5c --- /dev/null +++ b/tests/mocks/TestFileCacheItemPool.php @@ -0,0 +1,198 @@ +cacheDir = $cacheDir; + } + + /** + * {@inheritdoc} + * + * @return CacheItemInterface The corresponding Cache Item. + */ + public function getItem($key): CacheItemInterface + { + return current($this->getItems([$key])); // @phpstan-ignore-line + } + + /** + * {@inheritdoc} + * + * @return iterable + * A traversable collection of Cache Items keyed by the cache keys of + * each item. A Cache item will be returned for each key, even if that + * key is not found. However, if no keys are specified then an empty + * traversable MUST be returned instead. + */ + public function getItems(array $keys = []): iterable + { + $items = []; + foreach ($keys as $key) { + 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); + } + } + + return $items; + } + + /** + * {@inheritdoc} + * + * @return bool + * True if item exists in the cache, false otherwise. + */ + public function hasItem($key): bool + { + $this->isValidKey($key); + + return file_exists($this->cacheDir . '/' . $key) + && unserialize(file_get_contents($this->cacheDir . '/' . $key))->isHit(); + } + + /** + * {@inheritdoc} + * + * @return bool + * True if the pool was successfully cleared. False if there was an error. + */ + public function clear(): bool + { + $this->deferredItems = []; + + return true; + } + + /** + * {@inheritdoc} + * + * @return bool + * True if the item was successfully removed. False if there was an error. + */ + public function deleteItem($key): bool + { + return $this->deleteItems([$key]); + } + + /** + * {@inheritdoc} + * + * @return bool + * True if the items were successfully removed. False if there was an error. + */ + public function deleteItems(array $keys): bool + { + array_walk($keys, [$this, 'isValidKey']); + + foreach ($keys as $key) { + unlink($this->cacheDir . '/' . $key); + } + + return true; + } + + /** + * {@inheritdoc} + * + * @return bool + * True if the item was successfully persisted. False if there was an error. + */ + public function save(CacheItemInterface $item): bool + { + if (!is_dir($this->cacheDir)) { + mkdir($this->cacheDir, 0777, true); + } + file_put_contents($this->cacheDir . '/' . $item->getKey(), serialize($item)); + + return true; + } + + /** + * {@inheritdoc} + * + * @return bool + * False if the item could not be queued or if a commit was attempted and failed. True otherwise. + */ + public function saveDeferred(CacheItemInterface $item): bool + { + $this->deferredItems[$item->getKey()] = $item; + + return true; + } + + /** + * {@inheritdoc} + * + * @return bool + * True if all not-yet-saved items were successfully saved or there were none. False otherwise. + */ + public function commit(): bool + { + foreach ($this->deferredItems as $item) { + $this->save($item); + } + + $this->deferredItems = []; + + return true; + } + + /** + * Determines if the provided key is valid. + * + * @param string $key + * @return bool + * @throws InvalidArgumentException + */ + private function isValidKey($key) + { + $invalidCharacters = '{}()/\\\\@:'; + + if (!is_string($key) || preg_match("#[$invalidCharacters]#", $key)) { + throw new InvalidArgumentException('The provided key is not valid: ' . var_export($key, true)); + } + + return true; + } +} diff --git a/tests/mocks/test_file_cache_separate_process.php b/tests/mocks/test_file_cache_separate_process.php new file mode 100644 index 000000000..caeff26bb --- /dev/null +++ b/tests/mocks/test_file_cache_separate_process.php @@ -0,0 +1,47 @@ +cacheKey = $cacheKey; + } + + public function getUniverseDomain(): string + { + throw new \Exception('Should not be called!'); + } + + public function getCacheKey() + { + return $this->cacheKey; + } + + // no op + public function fetchAuthToken(?callable $httpHandle = null) + { + } + // no op + public function getLastReceivedToken() + { + } +}; + +$cacheFetcher = new FetchAuthTokenCache( + $fetcher, + ['cacheUniverseDomain' => true], + $cache +); + +echo $cacheFetcher->getUniverseDomain();