mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-08-03 15:11:40 +00:00
Store temporary TME access keys in cache
This commit is contained in:
parent
d3020698a3
commit
18e250abce
2 changed files with 113 additions and 3 deletions
|
|
@ -24,6 +24,7 @@ declare(strict_types=1);
|
||||||
namespace App\Services\InfoProviderSystem\Providers;
|
namespace App\Services\InfoProviderSystem\Providers;
|
||||||
|
|
||||||
use App\Settings\InfoProviderSystem\TMESettings;
|
use App\Settings\InfoProviderSystem\TMESettings;
|
||||||
|
use Psr\Cache\CacheItemPoolInterface;
|
||||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||||
|
|
||||||
|
|
@ -31,16 +32,69 @@ class TMEClient
|
||||||
{
|
{
|
||||||
public const BASE_URI = 'https://api.tme.eu';
|
public const BASE_URI = 'https://api.tme.eu';
|
||||||
|
|
||||||
|
private const CACHE_KEY_PREFIX = 'tme_oauth_token_';
|
||||||
|
|
||||||
private ?string $accessToken = null;
|
private ?string $accessToken = null;
|
||||||
private ?string $refreshToken = null;
|
private ?string $refreshToken = null;
|
||||||
private int $tokenExpiry = 0;
|
private int $tokenExpiry = 0;
|
||||||
|
private bool $loadedFromCache = false;
|
||||||
|
|
||||||
public function __construct(private readonly HttpClientInterface $tmeClient, private readonly TMESettings $settings)
|
public function __construct(
|
||||||
|
private readonly HttpClientInterface $tmeClient,
|
||||||
|
private readonly TMESettings $settings,
|
||||||
|
private readonly CacheItemPoolInterface $infoProviderCache,
|
||||||
|
)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The cache key is derived from the configured credentials, so changing the
|
||||||
|
* token/secret in the settings automatically invalidates any previously cached token.
|
||||||
|
*/
|
||||||
|
private function getCacheKey(): string
|
||||||
|
{
|
||||||
|
return self::CACHE_KEY_PREFIX . hash('xxh3', $this->settings->apiToken . ':' . $this->settings->apiSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a previously persisted token (if any) into the in-memory properties, so it can be
|
||||||
|
* reused across requests instead of always starting a fresh client_credentials flow.
|
||||||
|
*/
|
||||||
|
private function loadFromCache(): void
|
||||||
|
{
|
||||||
|
if ($this->loadedFromCache) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$this->loadedFromCache = true;
|
||||||
|
|
||||||
|
$item = $this->infoProviderCache->getItem($this->getCacheKey());
|
||||||
|
if ($item->isHit()) {
|
||||||
|
$data = $item->get();
|
||||||
|
$this->accessToken = $data['access_token'];
|
||||||
|
$this->refreshToken = $data['refresh_token'];
|
||||||
|
$this->tokenExpiry = $data['expiry'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function saveToCache(): void
|
||||||
|
{
|
||||||
|
$item = $this->infoProviderCache->getItem($this->getCacheKey());
|
||||||
|
$item->set([
|
||||||
|
'access_token' => $this->accessToken,
|
||||||
|
'refresh_token' => $this->refreshToken,
|
||||||
|
'expiry' => $this->tokenExpiry,
|
||||||
|
]);
|
||||||
|
// Keep the refresh token available in the cache for a while after the access token itself expired
|
||||||
|
$item->expiresAfter(max($this->tokenExpiry - time(), 0) + 60 * 60 * 24 * 7);
|
||||||
|
$this->infoProviderCache->save($item);
|
||||||
|
}
|
||||||
|
|
||||||
private function getAccessToken(): string
|
private function getAccessToken(): string
|
||||||
{
|
{
|
||||||
|
if ($this->accessToken === null) {
|
||||||
|
$this->loadFromCache();
|
||||||
|
}
|
||||||
|
|
||||||
// Return cached token if still valid (30-second safety margin before expiry)
|
// Return cached token if still valid (30-second safety margin before expiry)
|
||||||
if ($this->accessToken !== null && time() < $this->tokenExpiry - 30) {
|
if ($this->accessToken !== null && time() < $this->tokenExpiry - 30) {
|
||||||
return $this->accessToken;
|
return $this->accessToken;
|
||||||
|
|
@ -75,6 +129,8 @@ class TMEClient
|
||||||
$this->accessToken = $data['access_token'];
|
$this->accessToken = $data['access_token'];
|
||||||
$this->tokenExpiry = time() + (int) $data['expires_in'];
|
$this->tokenExpiry = time() + (int) $data['expires_in'];
|
||||||
$this->refreshToken = $data['refresh_token'] ?? null;
|
$this->refreshToken = $data['refresh_token'] ?? null;
|
||||||
|
|
||||||
|
$this->saveToCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ use App\Services\InfoProviderSystem\Providers\TMEProvider;
|
||||||
use App\Settings\InfoProviderSystem\TMESettings;
|
use App\Settings\InfoProviderSystem\TMESettings;
|
||||||
use App\Tests\SettingsTestHelper;
|
use App\Tests\SettingsTestHelper;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\Cache\Adapter\ArrayAdapter;
|
||||||
use Symfony\Component\HttpClient\MockHttpClient;
|
use Symfony\Component\HttpClient\MockHttpClient;
|
||||||
use Symfony\Component\HttpClient\Response\MockResponse;
|
use Symfony\Component\HttpClient\Response\MockResponse;
|
||||||
|
|
||||||
|
|
@ -51,7 +52,7 @@ final class TMEProviderTest extends TestCase
|
||||||
$this->settings->currency = 'EUR';
|
$this->settings->currency = 'EUR';
|
||||||
$this->settings->language = 'en';
|
$this->settings->language = 'en';
|
||||||
$this->settings->country = 'DE';
|
$this->settings->country = 'DE';
|
||||||
$this->provider = new TMEProvider(new TMEClient($this->httpClient, $this->settings), $this->settings);
|
$this->provider = new TMEProvider(new TMEClient($this->httpClient, $this->settings, new ArrayAdapter()), $this->settings, $this->httpClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Mock response helpers ---
|
// --- Mock response helpers ---
|
||||||
|
|
@ -258,7 +259,7 @@ final class TMEProviderTest extends TestCase
|
||||||
public function testIsActiveWithoutCredentials(): void
|
public function testIsActiveWithoutCredentials(): void
|
||||||
{
|
{
|
||||||
$this->settings->apiToken = null;
|
$this->settings->apiToken = null;
|
||||||
$provider = new TMEProvider(new TMEClient($this->httpClient, $this->settings), $this->settings);
|
$provider = new TMEProvider(new TMEClient($this->httpClient, $this->settings, new ArrayAdapter()), $this->settings, $this->httpClient);
|
||||||
$this->assertFalse($provider->isActive());
|
$this->assertFalse($provider->isActive());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -286,6 +287,59 @@ final class TMEProviderTest extends TestCase
|
||||||
$this->assertNull($this->provider->getIDFromURL('https://www.tme.eu/en/'));
|
$this->assertNull($this->provider->getIDFromURL('https://www.tme.eu/en/'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testAccessTokenIsCachedAcrossClientInstances(): void
|
||||||
|
{
|
||||||
|
// A single ArrayAdapter shared between two independent TMEClient instances simulates
|
||||||
|
// the token cache surviving across separate requests (each request builds a fresh client).
|
||||||
|
$cache = new ArrayAdapter();
|
||||||
|
|
||||||
|
// First client has to fetch a token before making its API call
|
||||||
|
$client1 = new TMEClient($this->httpClient, $this->settings, $cache);
|
||||||
|
$this->httpClient->setResponseFactory([
|
||||||
|
$this->mockTokenResponse(),
|
||||||
|
$this->smd0603SearchResults(),
|
||||||
|
]);
|
||||||
|
$client1->makeRequest('products/search', ['phrase' => 'SMD0603-5K1-1%']);
|
||||||
|
|
||||||
|
// Second client is a fresh instance, but shares the cache pool, so no token request should be made
|
||||||
|
$client2 = new TMEClient($this->httpClient, $this->settings, $cache);
|
||||||
|
$this->httpClient->setResponseFactory([
|
||||||
|
$this->smd0603SearchResults(),
|
||||||
|
]);
|
||||||
|
$response = $client2->makeRequest('products/search', ['phrase' => 'SMD0603-5K1-1%']);
|
||||||
|
|
||||||
|
$this->assertSame('OK', $response->toArray()['status']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAccessTokenIsRefetchedAfterExpiryEvenWithSharedCache(): void
|
||||||
|
{
|
||||||
|
$cache = new ArrayAdapter();
|
||||||
|
|
||||||
|
$client1 = new TMEClient($this->httpClient, $this->settings, $cache);
|
||||||
|
$this->httpClient->setResponseFactory([
|
||||||
|
new MockResponse(json_encode([
|
||||||
|
'access_token' => 'mock_access_token',
|
||||||
|
'token_type' => 'Bearer',
|
||||||
|
'expires_in' => -1, // already expired
|
||||||
|
'refresh_token' => 'mock_refresh_token',
|
||||||
|
])),
|
||||||
|
$this->smd0603SearchResults(),
|
||||||
|
]);
|
||||||
|
$client1->makeRequest('products/search', ['phrase' => 'SMD0603-5K1-1%']);
|
||||||
|
|
||||||
|
// The cached token is expired, and the refresh_token grant fails (no mock queued for it beyond
|
||||||
|
// the token response below), so a fresh client_credentials token must be fetched.
|
||||||
|
$client2 = new TMEClient($this->httpClient, $this->settings, $cache);
|
||||||
|
$this->httpClient->setResponseFactory([
|
||||||
|
new MockResponse('', ['http_code' => 400]), // refresh_token grant fails
|
||||||
|
$this->mockTokenResponse(), // fallback client_credentials grant
|
||||||
|
$this->smd0603SearchResults(),
|
||||||
|
]);
|
||||||
|
$response = $client2->makeRequest('products/search', ['phrase' => 'SMD0603-5K1-1%']);
|
||||||
|
|
||||||
|
$this->assertSame('OK', $response->toArray()['status']);
|
||||||
|
}
|
||||||
|
|
||||||
public function testSearchByKeyword(): void
|
public function testSearchByKeyword(): void
|
||||||
{
|
{
|
||||||
// Request order: POST /auth/token, GET /products/search
|
// Request order: POST /auth/token, GET /products/search
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue