Skip to content

Commit

Permalink
feat: add caching for universe domain (#533)
Browse files Browse the repository at this point in the history
  • Loading branch information
bshaffer authored Feb 21, 2024
1 parent 155c61a commit 69249ab
Show file tree
Hide file tree
Showing 4 changed files with 358 additions and 0 deletions.
16 changes: 16 additions & 0 deletions src/FetchAuthTokenCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public function __construct(
$this->cacheConfig = array_merge([
'lifetime' => 1500,
'prefix' => '',
'cacheUniverseDomain' => $fetcher instanceof Credentials\GCECredentials,
], (array) $cacheConfig);
}

Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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;
}
}
97 changes: 97 additions & 0 deletions tests/FetchAuthTokenCacheTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -37,6 +38,7 @@ class FetchAuthTokenCacheTest extends BaseTest
private $mockCacheItem;
private $mockCache;
private $mockSigner;
private static string $cacheKey;

protected function setUp(): void
{
Expand Down Expand Up @@ -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));
}
}
198 changes: 198 additions & 0 deletions tests/mocks/TestFileCacheItemPool.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
<?php
/*
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

namespace Google\Auth\Tests;

use Google\Auth\Cache\Item;
use Google\Auth\Cache\TypedItem;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;

/**
* Simple in-memory cache implementation.
*/
final class TestFileCacheItemPool implements CacheItemPoolInterface
{
/**
* @var string
*/
private string $cacheDir;

/**
* @var CacheItemInterface[]
*/
private $deferredItems;


public function __construct(string $cacheDir)
{
$this->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<CacheItemInterface>
* 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;
}
}
47 changes: 47 additions & 0 deletions tests/mocks/test_file_cache_separate_process.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

require_once __DIR__ . '/../../vendor/autoload.php';
require_once __DIR__ . '/TestFileCacheItemPool.php';

use Google\Auth\FetchAuthTokenCache;
use Google\Auth\FetchAuthTokenInterface;
use Google\Auth\GetUniverseDomainInterface;
use Google\Auth\Tests\TestFileCacheItemPool;

$cache = new TestFileCacheItemPool(sys_get_temp_dir() . '/google-auth-test');

$fetcher = new class($argv[1]) implements FetchAuthTokenInterface, GetUniverseDomainInterface {
private $cacheKey;

public function __construct(string $cacheKey)
{
$this->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();

0 comments on commit 69249ab

Please sign in to comment.