From a04efb2bf2044483d51a424f239d4de57cc36937 Mon Sep 17 00:00:00 2001 From: Pepa Martinec Date: Wed, 22 Feb 2023 23:08:54 +0100 Subject: [PATCH 1/2] [azure-api-client] Integrate authentication directly into ApiClient --- libs/azure-api-client/README.md | 50 ++-- libs/azure-api-client/src/ApiClient.php | 30 ++- .../AuthenticatedAzureApiClientFactory.php | 43 ---- .../AuthorizationHeaderResolver.php | 57 ----- .../PlainAzureApiClientFactory.php | 39 --- .../Authentication/AuthenticationToken.php | 16 ++ .../Authenticator/AuthenticatorInterface.php | 12 + .../ClientCredentialsAuthenticator.php} | 77 +++--- .../ManagedCredentialsAuthenticator.php | 59 +++++ .../StaticTokenAuthenticator.php | 20 ++ .../SystemAuthenticatorResolver.php | 54 ++++ .../Authentication/AuthenticatorFactory.php | 34 --- .../Authentication/AuthenticatorInterface.php | 12 - .../AuthorizationHeaderResolver.php | 44 ++++ .../ManagedCredentialsAuthenticator.php | 71 ------ .../{ => Model}/MetadataResponse.php | 2 +- .../{ => Model}/TokenResponse.php | 2 +- .../src/Marketplace/MarketplaceApiClient.php | 26 +- .../Marketplace/MeteringServiceApiClient.php | 16 +- ...AuthenticatedAzureApiClientFactoryTest.php | 88 ------- libs/azure-api-client/tests/ApiClientTest.php | 16 +- .../AuthenticationFactoryTest.php | 123 ---------- .../ClientCredentialsAuthenticatorTest.php} | 199 ++++++++------- .../ManagedCredentialsAuthenticatorTest.php | 79 +----- .../StaticTokenAuthenticatorTest.php | 20 ++ .../SystemAuthenticatorResolverTest.php | 119 +++++++++ .../Authenticator/arm-metadata.json | 33 +++ .../AuthorizationHeaderResolverTest.php | 31 +-- .../{ => Model}/MetadataResponseTest.php | 4 +- .../{ => Model}/TokenResponseTest.php | 4 +- libs/azure-api-client/tests/BaseTest.php | 59 ----- .../Marketplace/MarketplaceApiClientTest.php | 231 ++++++++++-------- .../MeteringServiceApiClientTest.php | 135 +++++----- 33 files changed, 814 insertions(+), 991 deletions(-) delete mode 100644 libs/azure-api-client/src/ApiClientFactory/AuthenticatedAzureApiClientFactory.php delete mode 100644 libs/azure-api-client/src/ApiClientFactory/AuthorizationHeaderResolver.php delete mode 100644 libs/azure-api-client/src/ApiClientFactory/PlainAzureApiClientFactory.php create mode 100644 libs/azure-api-client/src/Authentication/AuthenticationToken.php create mode 100644 libs/azure-api-client/src/Authentication/Authenticator/AuthenticatorInterface.php rename libs/azure-api-client/src/Authentication/{ClientCredentialsEnvironmentAuthenticator.php => Authenticator/ClientCredentialsAuthenticator.php} (52%) create mode 100644 libs/azure-api-client/src/Authentication/Authenticator/ManagedCredentialsAuthenticator.php create mode 100644 libs/azure-api-client/src/Authentication/Authenticator/StaticTokenAuthenticator.php create mode 100644 libs/azure-api-client/src/Authentication/Authenticator/SystemAuthenticatorResolver.php delete mode 100644 libs/azure-api-client/src/Authentication/AuthenticatorFactory.php delete mode 100644 libs/azure-api-client/src/Authentication/AuthenticatorInterface.php create mode 100644 libs/azure-api-client/src/Authentication/AuthorizationHeaderResolver.php delete mode 100644 libs/azure-api-client/src/Authentication/ManagedCredentialsAuthenticator.php rename libs/azure-api-client/src/Authentication/{ => Model}/MetadataResponse.php (91%) rename libs/azure-api-client/src/Authentication/{ => Model}/TokenResponse.php (95%) delete mode 100644 libs/azure-api-client/tests/ApiClientFactory/AuthenticatedAzureApiClientFactoryTest.php delete mode 100644 libs/azure-api-client/tests/Authentication/AuthenticationFactoryTest.php rename libs/azure-api-client/tests/Authentication/{ClientCredentialsEnvironmentAuthenticatorTest.php => Authenticator/ClientCredentialsAuthenticatorTest.php} (59%) rename libs/azure-api-client/tests/Authentication/{ => Authenticator}/ManagedCredentialsAuthenticatorTest.php (52%) create mode 100644 libs/azure-api-client/tests/Authentication/Authenticator/StaticTokenAuthenticatorTest.php create mode 100644 libs/azure-api-client/tests/Authentication/Authenticator/SystemAuthenticatorResolverTest.php create mode 100644 libs/azure-api-client/tests/Authentication/Authenticator/arm-metadata.json rename libs/azure-api-client/tests/{ApiClientFactory => Authentication}/AuthorizationHeaderResolverTest.php (72%) rename libs/azure-api-client/tests/Authentication/{ => Model}/MetadataResponseTest.php (94%) rename libs/azure-api-client/tests/Authentication/{ => Model}/TokenResponseTest.php (91%) delete mode 100644 libs/azure-api-client/tests/BaseTest.php diff --git a/libs/azure-api-client/README.md b/libs/azure-api-client/README.md index ee0019bf3..ef2f7f28b 100644 --- a/libs/azure-api-client/README.md +++ b/libs/azure-api-client/README.md @@ -1,39 +1,51 @@ # Azure API Client ## Installation - ```bash composer require keboola/azure-api-client ``` ## Usage - -To create API client using PHP: +The simplest way to use API client is just by creating its instance. ```php -# example.php -use Keboola\AzureApiClient\Authentication\AuthenticatorFactory; -use Keboola\AzureApiClient\ApiClientFactory\PlainAzureApiClientFactory; -use Keboola\AzureApiClient\GuzzleClientFactory; use Keboola\AzureApiClient\Marketplace\MarketplaceApiClient; use Monolog\Logger; -$authenticatorFactory = new AuthenticatorFactory(); -$clientFactory = new PlainAzureApiClientFactory($guzzleClientFactory, $authenticatorFactory, $logger); - -$marketingApiClient = MarketplaceApiClient::create($clientFactory); +$logger = new Logger('azure-api-client'); +$marketplaces = new MarketplaceApiClient([ + 'logger' => $logger, +]); ``` -To create API client using Symfony container, just register class services and everything should auto-wire just fine: -```yaml -# services.yaml -services: - Keboola\AzureApiClient\Authentication\AuthenticatorFactory: +### Authentication +By default, API client will try to authenticate using ENV variables `AZURE_TENANT_ID`, `AZURE_CLIENT_ID` and +`AZURE_CLIENT_SECRET`. If some of them is not available, it'll fall back to Azure metadata API. + +If you want to supply your own credentials, you can pass custom authenticator instance in the client options: +```php +use Keboola\AzureApiClient\Authentication\Authenticator\ClientCredentialsAuthenticator; +use Keboola\AzureApiClient\Marketplace\MarketplaceApiClient; + +$logger = new Logger('azure-api-client'); +$marketplaces = new MarketplaceApiClient([ + 'authenticator' => new ClientCredentialsAuthenticator( + 'tenant-id', + 'client-id', + 'client-secret', + ), +]); +``` - Keboola\AzureApiClient\AzureApiClientFactory: +Or can provide custom authentication token directly: +```php +use Keboola\AzureApiClient\Authentication\Authenticator\StaticTokenAuthenticator; +use Keboola\AzureApiClient\Marketplace\MarketplaceApiClient; - Keboola\AzureApiClient\Marketplace\MarketplaceApiClient: - factory: ['App\Marketplace\Azure\MarketplaceApiClient\Marketplace\MarketplaceApiClient', 'create'] +$logger = new Logger('azure-api-client'); +$marketplaces = new MarketplaceApiClient([ + 'authenticator' => new StaticTokenAuthenticator('my-token'), +]); ``` ## License diff --git a/libs/azure-api-client/src/ApiClient.php b/libs/azure-api-client/src/ApiClient.php index 11dcee873..9f6506df3 100644 --- a/libs/azure-api-client/src/ApiClient.php +++ b/libs/azure-api-client/src/ApiClient.php @@ -11,6 +11,9 @@ use GuzzleHttp\MessageFormatter; use GuzzleHttp\Middleware; use JsonException; +use Keboola\AzureApiClient\Authentication\Authenticator\AuthenticatorInterface; +use Keboola\AzureApiClient\Authentication\Authenticator\SystemAuthenticatorResolver; +use Keboola\AzureApiClient\Authentication\AuthorizationHeaderResolver; use Keboola\AzureApiClient\Exception\ClientException; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; @@ -28,13 +31,14 @@ class ApiClient private readonly HandlerStack $requestHandlerStack; private readonly GuzzleClient $httpClient; + private readonly AuthenticatorInterface $authenticator; private readonly LoggerInterface $logger; /** * @param array{ * baseUrl?: null|non-empty-string, * backoffMaxTries?: null|int<0, max>, - * middleware?: null|list, + * authenticator?: null|AuthenticatorInterface, * requestHandler?: null|callable, * logger?: null|LoggerInterface, * } $options @@ -56,9 +60,7 @@ public function __construct( new Assert\GreaterThanOrEqual(0), ]), - 'middleware' => new Assert\All([ - new Assert\Type('callable'), - ]), + 'authenticator' => new Assert\Type(AuthenticatorInterface::class), 'requestHandler' => new Assert\Type('callable'), @@ -81,10 +83,13 @@ public function __construct( $this->logger = $options['logger'] ?? new NullLogger(); + $this->authenticator = $options['authenticator'] ?? new SystemAuthenticatorResolver([ + 'backoffMaxTries' => $options['backoffMaxTries'] ?? null, + 'requestHandler' => $options['requestHandler'] ?? null, + 'logger' => $this->logger, + ]); + $this->requestHandlerStack = HandlerStack::create($options['requestHandler'] ?? null); - foreach ($options['middleware'] ?? [] as $middleware) { - $this->requestHandlerStack->push($middleware); - } $backoffMaxTries = $options['backoffMaxTries'] ?? self::DEFAULT_BACKOFF_RETRIES; if ($backoffMaxTries > 0) { @@ -107,6 +112,17 @@ public function __construct( ]); } + public function authenticate(string $resource): void + { + $middleware = Middleware::mapRequest(new AuthorizationHeaderResolver( + $this->authenticator, + $resource + )); + + $this->requestHandlerStack->remove('auth'); + $this->requestHandlerStack->push($middleware, 'auth'); + } + /** * @template TResponseClass of ResponseModelInterface * @param class-string $responseClass diff --git a/libs/azure-api-client/src/ApiClientFactory/AuthenticatedAzureApiClientFactory.php b/libs/azure-api-client/src/ApiClientFactory/AuthenticatedAzureApiClientFactory.php deleted file mode 100644 index c7e88fbc8..000000000 --- a/libs/azure-api-client/src/ApiClientFactory/AuthenticatedAzureApiClientFactory.php +++ /dev/null @@ -1,43 +0,0 @@ -options, $options); - $options['baseUrl'] = $baseUrl; - - $options['middleware'] ??= []; - $options['middleware'][] = Middleware::mapRequest(new AuthorizationHeaderResolver( - $this->authenticatorFactory, - $resource - )); - - return new ApiClient($options); - } -} diff --git a/libs/azure-api-client/src/ApiClientFactory/AuthorizationHeaderResolver.php b/libs/azure-api-client/src/ApiClientFactory/AuthorizationHeaderResolver.php deleted file mode 100644 index ff78f2d4d..000000000 --- a/libs/azure-api-client/src/ApiClientFactory/AuthorizationHeaderResolver.php +++ /dev/null @@ -1,57 +0,0 @@ -isTokenValid()) { - $this->refreshToken(); - } - assert($this->token !== null); - - return $request->withHeader('Authorization', 'Bearer ' . $this->token->accessToken); - } - - private function isTokenValid(): bool - { - if ($this->token === null) { - return false; - } - - $expirationTimestamp = $this->token->accessTokenExpiration->getTimestamp(); - if ($expirationTimestamp - self::EXPIRATION_MARGIN < time()) { - return false; - } - - return true; - } - - private function refreshToken(): void - { - if ($this->authenticator === null) { - $this->authenticator = $this->authenticatorFactory->createAuthenticator(); - } - - $this->token = $this->authenticator->getAuthenticationToken($this->resource); - } -} diff --git a/libs/azure-api-client/src/ApiClientFactory/PlainAzureApiClientFactory.php b/libs/azure-api-client/src/ApiClientFactory/PlainAzureApiClientFactory.php deleted file mode 100644 index 6cd99c9bb..000000000 --- a/libs/azure-api-client/src/ApiClientFactory/PlainAzureApiClientFactory.php +++ /dev/null @@ -1,39 +0,0 @@ -, - * middleware?: null|list, - * requestHandler?: null|callable, - * logger?: null|LoggerInterface, - * } - */ -class PlainAzureApiClientFactory -{ - /** - * @param Options $options - */ - public function __construct( - private readonly array $options = [], - ) { - } - - /** - * @param non-empty-string $baseUrl - * @param Options $options - */ - public function createClient(string $baseUrl, array $options = []): ApiClient - { - $options = array_merge($this->options, $options); - $options['baseUrl'] = $baseUrl; - - return new ApiClient($options); - } -} diff --git a/libs/azure-api-client/src/Authentication/AuthenticationToken.php b/libs/azure-api-client/src/Authentication/AuthenticationToken.php new file mode 100644 index 000000000..2769baf49 --- /dev/null +++ b/libs/azure-api-client/src/Authentication/AuthenticationToken.php @@ -0,0 +1,16 @@ +, + * requestHandler?: null|callable, + * logger?: null|LoggerInterface, + * } $options + */ public function __construct( - PlainAzureApiClientFactory $clientFactory, - private readonly LoggerInterface $logger, + private readonly string $tenantId, + private readonly string $clientId, + private readonly string $clientSecret, + array $options = [], ) { - $this->armUrl = (string) getenv(self::ENV_AZURE_AD_RESOURCE); - if (!$this->armUrl) { - $this->armUrl = self::DEFAULT_ARM_URL; - $this->logger->debug( + $logger = $options['logger'] ?? new NullLogger(); + + $armUrl = (string) getenv(self::ENV_AZURE_AD_RESOURCE); + if (!$armUrl) { + $armUrl = self::DEFAULT_ARM_URL; + $logger->debug( self::ENV_AZURE_AD_RESOURCE . ' environment variable is not specified, falling back to default.' ); } @@ -47,22 +53,18 @@ public function __construct( $this->cloudName = (string) getenv(self::ENV_AZURE_ENVIRONMENT); if (!$this->cloudName) { $this->cloudName = self::DEFAULT_PUBLIC_CLOUD_NAME; - $this->logger->debug( + $logger->debug( self::ENV_AZURE_ENVIRONMENT . ' environment variable is not specified, falling back to default.' ); } - $this->tenantId = (string) getenv(self::ENV_AZURE_TENANT_ID); - $this->clientId = (string) getenv(self::ENV_AZURE_CLIENT_ID); - $this->clientSecret = (string) getenv(self::ENV_AZURE_CLIENT_SECRET); - $this->apiClient = $clientFactory->createClient($this->armUrl); + $options['baseUrl'] = $armUrl; + $this->apiClient = new ApiClient($options); } - public function getAuthenticationToken(string $resource): TokenResponse + public function getAuthenticationToken(string $resource): AuthenticationToken { - if ($this->authEndpoint === null) { - $this->authEndpoint = $this->getMetadata($this->armUrl, $this->cloudName)->authenticationLoginEndpoint; - } + $this->authEndpoint ??= $this->getMetadata($this->cloudName)->authenticationLoginEndpoint; $request = new Request('POST', sprintf('%s%s/oauth2/token', $this->authEndpoint, $this->tenantId)); $formData = [ @@ -78,28 +80,17 @@ public function getAuthenticationToken(string $resource): TokenResponse ['form_params' => $formData] ); - $this->logger->info('Successfully authenticated using client credentials.'); - return $token; - } - - public function checkUsability(): void - { - $errors = []; - foreach ([self::ENV_AZURE_TENANT_ID, self::ENV_AZURE_CLIENT_ID, self::ENV_AZURE_CLIENT_SECRET] as $envVar) { - if (!getenv($envVar)) { - $errors[] = sprintf('Environment variable "%s" is not set.', $envVar); - } - } - if ($errors) { - throw new ClientException(implode(' ', $errors)); - } + return new AuthenticationToken( + $token->accessToken, + $token->accessTokenExpiration, + ); } - private function getMetadata(string $armUrl, string $cloudName): MetadataResponse + private function getMetadata(string $cloudName): MetadataResponse { try { $metadataList = $this->apiClient->sendRequestAndMapResponse( - new Request('GET', $armUrl), + new Request('GET', ''), MetadataResponse::class, [], true diff --git a/libs/azure-api-client/src/Authentication/Authenticator/ManagedCredentialsAuthenticator.php b/libs/azure-api-client/src/Authentication/Authenticator/ManagedCredentialsAuthenticator.php new file mode 100644 index 000000000..bb8ed7b6c --- /dev/null +++ b/libs/azure-api-client/src/Authentication/Authenticator/ManagedCredentialsAuthenticator.php @@ -0,0 +1,59 @@ +, + * requestHandler?: null|callable, + * logger?: null|LoggerInterface, + * } $options + */ + public function __construct( + array $options = [], + ) { + $options['baseUrl'] = self::INSTANCE_METADATA_SERVICE_ENDPOINT; + $this->apiClient = new ApiClient($options); + } + + public function getAuthenticationToken(string $resource): AuthenticationToken + { + $token = $this->apiClient->sendRequestAndMapResponse( + new Request( + 'GET', + sprintf( + '/metadata/identity/oauth2/token?%s', + http_build_query([ + 'api-version' => self::API_VERSION, + 'format' => 'text', + 'resource' => $resource, + ]) + ), + [ + 'Metadata' => 'true', + ], + ), + TokenResponse::class + ); + + return new AuthenticationToken( + $token->accessToken, + $token->accessTokenExpiration, + ); + } +} diff --git a/libs/azure-api-client/src/Authentication/Authenticator/StaticTokenAuthenticator.php b/libs/azure-api-client/src/Authentication/Authenticator/StaticTokenAuthenticator.php new file mode 100644 index 000000000..aadaaaf00 --- /dev/null +++ b/libs/azure-api-client/src/Authentication/Authenticator/StaticTokenAuthenticator.php @@ -0,0 +1,20 @@ +value, null); + } +} diff --git a/libs/azure-api-client/src/Authentication/Authenticator/SystemAuthenticatorResolver.php b/libs/azure-api-client/src/Authentication/Authenticator/SystemAuthenticatorResolver.php new file mode 100644 index 000000000..a3c39bc23 --- /dev/null +++ b/libs/azure-api-client/src/Authentication/Authenticator/SystemAuthenticatorResolver.php @@ -0,0 +1,54 @@ +, + * requestHandler?: null|callable, + * logger?: null|LoggerInterface, + * } $options + */ + public function __construct( + private readonly array $options = [], + ) { + } + + public function getAuthenticationToken(string $resource): AuthenticationToken + { + $this->resolvedAuthenticator ??= $this->resolveAuthenticator(); + return $this->resolvedAuthenticator->getAuthenticationToken($resource); + } + + private function resolveAuthenticator(): AuthenticatorInterface + { + $logger = $this->options['logger'] ?? new NullLogger(); + + $tenantId = (string) getenv('AZURE_TENANT_ID'); + $clientId = (string) getenv('AZURE_CLIENT_ID'); + $clientSecret = (string) getenv('AZURE_CLIENT_SECRET'); + if ($tenantId !== '' && $clientId !== '' && $clientSecret !== '') { + $logger->debug('Found Azure client credentials in ENV, using ClientCredentialsAuthenticator'); + + return new ClientCredentialsAuthenticator( + $tenantId, + $clientId, + $clientSecret, + $this->options, + ); + } + + $logger->debug('Azure client credentials not found in ENV, using ManagedCredentialsAuthenticator'); + return new ManagedCredentialsAuthenticator($this->options); + } +} diff --git a/libs/azure-api-client/src/Authentication/AuthenticatorFactory.php b/libs/azure-api-client/src/Authentication/AuthenticatorFactory.php deleted file mode 100644 index b9595aa9f..000000000 --- a/libs/azure-api-client/src/Authentication/AuthenticatorFactory.php +++ /dev/null @@ -1,34 +0,0 @@ -clientFactory, $this->logger); - try { - $authenticator->checkUsability(); - return $authenticator; - } catch (ClientException $e) { - $this->logger->debug( - 'ClientCredentialsEnvironmentAuthenticator is not usable: ' . $e->getMessage() - ); - } - /* ManagedCredentialsAuthenticator checkUsability method has poor performance due to slow responses - from GET /metadata requests */ - return new ManagedCredentialsAuthenticator($this->clientFactory, $this->logger); - } -} diff --git a/libs/azure-api-client/src/Authentication/AuthenticatorInterface.php b/libs/azure-api-client/src/Authentication/AuthenticatorInterface.php deleted file mode 100644 index 5d6429000..000000000 --- a/libs/azure-api-client/src/Authentication/AuthenticatorInterface.php +++ /dev/null @@ -1,12 +0,0 @@ -token === null || !$this->isTokenValid($this->token)) { + $this->token = $this->authenticator->getAuthenticationToken($this->resource); + } + + return $request->withHeader('Authorization', 'Bearer ' . $this->token->value); + } + + private function isTokenValid(AuthenticationToken $token): bool + { + if ($token->expiresAt === null) { + return true; + } + + $expirationTimestamp = $token->expiresAt->getTimestamp(); + if ($expirationTimestamp - self::EXPIRATION_MARGIN < time()) { + return false; + } + + return true; + } +} diff --git a/libs/azure-api-client/src/Authentication/ManagedCredentialsAuthenticator.php b/libs/azure-api-client/src/Authentication/ManagedCredentialsAuthenticator.php deleted file mode 100644 index 14d78751e..000000000 --- a/libs/azure-api-client/src/Authentication/ManagedCredentialsAuthenticator.php +++ /dev/null @@ -1,71 +0,0 @@ -clientFactory->createClient(self::INSTANCE_METADATA_SERVICE_ENDPOINT); - $token = $client->sendRequestAndMapResponse( - new Request( - 'GET', - sprintf( - '/metadata/identity/oauth2/token?%s', - http_build_query([ - 'api-version' => self::API_VERSION, - 'format' => 'text', - 'resource' => $resource, - ]) - ), - [ - 'Metadata' => 'true', - ], - ), - TokenResponse::class - ); - - $this->logger->info('Successfully authenticated using instance metadata.'); - return $token; - } - - public function checkUsability(): void - { - try { - $client = $this->clientFactory->createClient( - self::INSTANCE_METADATA_SERVICE_ENDPOINT, - ['backoffMaxTries' => 1] - ); - $client->sendRequest( - new Request( - 'GET', - sprintf('/metadata?%s', http_build_query([ - 'api-version' => self::API_VERSION, - 'format' => 'text', - ])), - [ - 'Metadata' => 'true', - ], - ), - ); - } catch (ClientException $e) { - throw new ClientException('Instance metadata service not available: ' . $e->getMessage(), 0, $e); - } - } -} diff --git a/libs/azure-api-client/src/Authentication/MetadataResponse.php b/libs/azure-api-client/src/Authentication/Model/MetadataResponse.php similarity index 91% rename from libs/azure-api-client/src/Authentication/MetadataResponse.php rename to libs/azure-api-client/src/Authentication/Model/MetadataResponse.php index a9375e502..92c057325 100644 --- a/libs/azure-api-client/src/Authentication/MetadataResponse.php +++ b/libs/azure-api-client/src/Authentication/Model/MetadataResponse.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Keboola\AzureApiClient\Authentication; +namespace Keboola\AzureApiClient\Authentication\Model; use Keboola\AzureApiClient\ResponseModelInterface; diff --git a/libs/azure-api-client/src/Authentication/TokenResponse.php b/libs/azure-api-client/src/Authentication/Model/TokenResponse.php similarity index 95% rename from libs/azure-api-client/src/Authentication/TokenResponse.php rename to libs/azure-api-client/src/Authentication/Model/TokenResponse.php index e0fcfc180..86f9c7eca 100644 --- a/libs/azure-api-client/src/Authentication/TokenResponse.php +++ b/libs/azure-api-client/src/Authentication/Model/TokenResponse.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Keboola\AzureApiClient\Authentication; +namespace Keboola\AzureApiClient\Authentication\Model; use DateTimeImmutable; use Keboola\AzureApiClient\Exception\InvalidResponseException; diff --git a/libs/azure-api-client/src/Marketplace/MarketplaceApiClient.php b/libs/azure-api-client/src/Marketplace/MarketplaceApiClient.php index e2e3d0b45..b7144a7f8 100644 --- a/libs/azure-api-client/src/Marketplace/MarketplaceApiClient.php +++ b/libs/azure-api-client/src/Marketplace/MarketplaceApiClient.php @@ -6,26 +6,30 @@ use GuzzleHttp\Psr7\Request; use Keboola\AzureApiClient\ApiClient; -use Keboola\AzureApiClient\ApiClientFactory\AuthenticatedAzureApiClientFactory; +use Keboola\AzureApiClient\Authentication\Authenticator\AuthenticatorInterface; use Keboola\AzureApiClient\Json; use Keboola\AzureApiClient\Marketplace\Model\ActivateSubscriptionRequest; use Keboola\AzureApiClient\Marketplace\Model\ResolveSubscriptionResult; use Keboola\AzureApiClient\Marketplace\Model\Subscription; +use Psr\Log\LoggerInterface; class MarketplaceApiClient { - public function __construct( - private readonly ApiClient $apiClient, - ) { - } + private ApiClient $apiClient; - public static function create(AuthenticatedAzureApiClientFactory $clientFactory): self + /** + * @param array{ + * backoffMaxTries?: null|int<0, max>, + * authenticator?: null|AuthenticatorInterface, + * requestHandler?: null|callable, + * logger?: null|LoggerInterface, + * } $options + */ + public function __construct(array $options = []) { - $apiClient = $clientFactory->createClient( - 'https://marketplaceapi.microsoft.com', - Resources::AZURE_MARKETPLACE - ); - return new self($apiClient); + $options['baseUrl'] = 'https://marketplaceapi.microsoft.com'; + $this->apiClient = new ApiClient($options); + $this->apiClient->authenticate(Resources::AZURE_MARKETPLACE); } public function resolveSubscription(string $marketplaceToken): ResolveSubscriptionResult diff --git a/libs/azure-api-client/src/Marketplace/MeteringServiceApiClient.php b/libs/azure-api-client/src/Marketplace/MeteringServiceApiClient.php index 45398ff60..4d3d60e48 100644 --- a/libs/azure-api-client/src/Marketplace/MeteringServiceApiClient.php +++ b/libs/azure-api-client/src/Marketplace/MeteringServiceApiClient.php @@ -6,7 +6,6 @@ use GuzzleHttp\Psr7\Request; use Keboola\AzureApiClient\ApiClient; -use Keboola\AzureApiClient\ApiClientFactory\AuthenticatedAzureApiClientFactory; use Keboola\AzureApiClient\Json; use Keboola\AzureApiClient\Marketplace\Model\ReportUsageEventsBatchResult; use Keboola\AzureApiClient\Marketplace\Model\UsageEvent; @@ -14,18 +13,13 @@ class MeteringServiceApiClient { - public function __construct( - private readonly ApiClient $apiClient, - ) { - } + private ApiClient $apiClient; - public static function create(AuthenticatedAzureApiClientFactory $clientFactory): self + public function __construct(array $options = []) { - $apiClient = $clientFactory->createClient( - 'https://marketplaceapi.microsoft.com/api/', - Resources::AZURE_MARKETPLACE - ); - return new self($apiClient); + $options['baseUrl'] = 'https://marketplaceapi.microsoft.com/api/'; + $this->apiClient = new ApiClient($options); + $this->apiClient->authenticate(Resources::AZURE_MARKETPLACE); } /** diff --git a/libs/azure-api-client/tests/ApiClientFactory/AuthenticatedAzureApiClientFactoryTest.php b/libs/azure-api-client/tests/ApiClientFactory/AuthenticatedAzureApiClientFactoryTest.php deleted file mode 100644 index 855621231..000000000 --- a/libs/azure-api-client/tests/ApiClientFactory/AuthenticatedAzureApiClientFactoryTest.php +++ /dev/null @@ -1,88 +0,0 @@ -createRequestHandler($requestsHistory, [ - new Response( - 200, - ['Content-Type' => 'application/json'], - Json::encodeArray(['foo' => 'bar']), - ), - new Response( - 200, - ['Content-Type' => 'application/json'], - Json::encodeArray(['foo' => 'bar']), - ), - ]); - - $factory = new AuthenticatedAzureApiClientFactory( - $this->createFakeAuthenticatorFactory('auth-token'), - [ - 'requestHandler' => $requestHandler, - ], - ); - $client = $factory->createClient('http://example.com', 'foo'); - - $client->sendRequest(new Request('GET', '/foo')); - self::assertCount(1, $requestsHistory); - - $request = $requestsHistory[0]['request']; - self::assertSame('Bearer auth-token', $request->getHeaderLine('Authorization')); - } - - /** - * @param non-empty-string $authToken - */ - private function createFakeAuthenticatorFactory(string $authToken): AuthenticatorFactory - { - $authenticator = $this->createMock(AuthenticatorInterface::class); - $authenticator->expects(self::once()) - ->method('getAuthenticationToken') - ->willReturn(new TokenResponse( - $authToken, - new DateTimeImmutable('+1 hour'), - )) - ; - - $authenticatorFactory = $this->createMock(AuthenticatorFactory::class); - $authenticatorFactory->expects(self::once()) - ->method('createAuthenticator') - ->willReturn($authenticator) - ; - - return $authenticatorFactory; - } - - /** - * @param list $requestsHistory - * @param list $responses - */ - private function createRequestHandler(?array &$requestsHistory, array $responses): HandlerStack - { - $requestsHistory = []; - - $stack = HandlerStack::create(new MockHandler($responses)); - $stack->push(Middleware::history($requestsHistory)); - - return $stack; - } -} diff --git a/libs/azure-api-client/tests/ApiClientTest.php b/libs/azure-api-client/tests/ApiClientTest.php index f22aade18..acaf9815e 100644 --- a/libs/azure-api-client/tests/ApiClientTest.php +++ b/libs/azure-api-client/tests/ApiClientTest.php @@ -5,9 +5,6 @@ namespace Keboola\AzureApiClient\Tests; use GuzzleHttp\Client as GuzzleClient; -use GuzzleHttp\Handler\MockHandler; -use GuzzleHttp\HandlerStack; -use GuzzleHttp\Middleware; use GuzzleHttp\Promise\Create; use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Response; @@ -88,18 +85,11 @@ public function provideInvalidOptions(): iterable 'error' => 'Invalid options when creating client: [backoffMaxTries]: This value should be greater than or equal to 0.', ]; - yield 'invalid middleware' => [ + yield 'invalid authenticator' => [ 'options' => [ - 'middleware' => 'foo', + 'authenticator' => ['foo'], ], - 'error' => 'Invalid options when creating client: [middleware]: This value should be of type iterable.', - ]; - - yield 'invalid middleware item' => [ - 'options' => [ - 'middleware' => ['foo'], - ], - 'error' => 'Invalid options when creating client: [middleware][0]: This value should be of type callable.', + 'error' => 'Invalid options when creating client: [authenticator]: This value should be of type Keboola\AzureApiClient\Authentication\Authenticator\AuthenticatorInterface.', ]; yield 'invalid requestHandler' => [ diff --git a/libs/azure-api-client/tests/Authentication/AuthenticationFactoryTest.php b/libs/azure-api-client/tests/Authentication/AuthenticationFactoryTest.php deleted file mode 100644 index 0f1e74ad4..000000000 --- a/libs/azure-api-client/tests/Authentication/AuthenticationFactoryTest.php +++ /dev/null @@ -1,123 +0,0 @@ -logsHandler = new TestHandler(); - $this->logger = new Logger('test', [$this->logsHandler]); - } - - public function testCreateEnvironmentAuthenticator(): void - { - $requestHandler = $this->createRequestHandler($requestsHistory, []); - $authenticationFactory = new AuthenticatorFactory( - new PlainAzureApiClientFactory([ - 'requestHandler' => $requestHandler, - ]), - $this->logger - ); - - putenv('AZURE_TENANT_ID=foo'); - putenv('AZURE_CLIENT_ID=foo'); - putenv('AZURE_CLIENT_SECRET=foo'); - - $authenticator = $authenticationFactory->createAuthenticator(); - self::assertInstanceOf(ClientCredentialsEnvironmentAuthenticator::class, $authenticator); - } - - public function testCreateManagedCredentialsAuthenticator(): void - { - $requestHandler = $this->createRequestHandler($requestsHistory, []); - $authenticationFactory = new AuthenticatorFactory( - new PlainAzureApiClientFactory([ - 'requestHandler' => $requestHandler, - ]), - $this->logger - ); - - putenv('AZURE_TENANT_ID='); - putenv('AZURE_CLIENT_ID=foo'); - putenv('AZURE_CLIENT_SECRET=foo'); - - $authenticator = $authenticationFactory->createAuthenticator(); - self::assertInstanceOf(ManagedCredentialsAuthenticator::class, $authenticator); - - self::assertTrue($this->logsHandler->hasDebug( - 'ClientCredentialsEnvironmentAuthenticator is not usable: ' . - 'Environment variable "AZURE_TENANT_ID" is not set.' - )); - } - - public function testManagedCredentialsAuthenticatorIsUsedEventIfNotUsable(): void - { - /* Even if the instance metadata is not available, the managed credentials authenticator is - returned because it's verification is optimized out */ - $requestHandler = $this->createRequestHandler($requestsHistory, [ - new Response(400), - ]); - $authenticationFactory = new AuthenticatorFactory( - new PlainAzureApiClientFactory([ - 'requestHandler' => $requestHandler, - ]), - $this->logger - ); - - putenv('AZURE_TENANT_ID='); - putenv('AZURE_CLIENT_ID=foo'); - putenv('AZURE_CLIENT_SECRET=foo'); - - $authenticator = $authenticationFactory->createAuthenticator(); - self::assertInstanceOf(ManagedCredentialsAuthenticator::class, $authenticator); - self::assertInstanceOf(ManagedCredentialsAuthenticator::class, $authenticator); - self::assertTrue($this->logsHandler->hasDebugThatContains( - 'ClientCredentialsEnvironmentAuthenticator is not usable: ' . - 'Environment variable "AZURE_TENANT_ID" is not set.' - )); - - $this->expectException(ClientException::class); - $this->expectExceptionMessage( - 'Client error: `GET http://169.254.169.254/metadata/identity/oauth2/token?api-version=2019-11-01&' . - 'format=text&resource=foo` resulted in a `400 Bad Request` response' - ); - $authenticator->getAuthenticationToken('foo'); - } - - /** - * @param list $requestsHistory - * @param list $responses - */ - private function createRequestHandler(?array &$requestsHistory, array $responses): HandlerStack - { - $requestsHistory = []; - - $stack = HandlerStack::create(new MockHandler($responses)); - $stack->push(Middleware::history($requestsHistory)); - - return $stack; - } -} diff --git a/libs/azure-api-client/tests/Authentication/ClientCredentialsEnvironmentAuthenticatorTest.php b/libs/azure-api-client/tests/Authentication/Authenticator/ClientCredentialsAuthenticatorTest.php similarity index 59% rename from libs/azure-api-client/tests/Authentication/ClientCredentialsEnvironmentAuthenticatorTest.php rename to libs/azure-api-client/tests/Authentication/Authenticator/ClientCredentialsAuthenticatorTest.php index 496e196b1..901daf2ae 100644 --- a/libs/azure-api-client/tests/Authentication/ClientCredentialsEnvironmentAuthenticatorTest.php +++ b/libs/azure-api-client/tests/Authentication/Authenticator/ClientCredentialsAuthenticatorTest.php @@ -2,25 +2,23 @@ declare(strict_types=1); -namespace Keboola\AzureApiClient\Tests\Authentication; +namespace Keboola\AzureApiClient\Tests\Authentication\Authenticator; use GuzzleHttp\Handler\MockHandler; use GuzzleHttp\HandlerStack; use GuzzleHttp\Middleware; use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Response; -use Keboola\AzureApiClient\ApiClientFactory\PlainAzureApiClientFactory; -use Keboola\AzureApiClient\Authentication\ClientCredentialsEnvironmentAuthenticator; +use Keboola\AzureApiClient\Authentication\Authenticator\ClientCredentialsAuthenticator; use Keboola\AzureApiClient\Exception\ClientException; use Keboola\AzureApiClient\Json; -use Keboola\AzureApiClient\Tests\BaseTest; use Monolog\Handler\TestHandler; use Monolog\Logger; +use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; -class ClientCredentialsEnvironmentAuthenticatorTest extends BaseTest +class ClientCredentialsAuthenticatorTest extends TestCase { - private readonly PlainAzureApiClientFactory $clientFactory; private readonly LoggerInterface $logger; private readonly TestHandler $logsHandler; @@ -31,19 +29,37 @@ public function setUp(): void $this->logsHandler = new TestHandler(); $this->logger = new Logger('tests', [$this->logsHandler]); - $this->clientFactory = new PlainAzureApiClientFactory(); + putenv('AZURE_TENANT_ID'); + putenv('AZURE_CLIENT_ID'); + putenv('AZURE_CLIENT_SECRET'); + putenv('AZURE_AD_RESOURCE'); + putenv('AZURE_ENVIRONMENT'); } public function testOptionalEnvsFallback(): void { - putenv('AZURE_AD_RESOURCE=http://foo'); + putenv('AZURE_AD_RESOURCE=https://foo'); putenv('AZURE_ENVIRONMENT=foo'); - new ClientCredentialsEnvironmentAuthenticator($this->clientFactory, $this->logger); + new ClientCredentialsAuthenticator( + 'tenant-id', + 'client-id', + 'client-secret', + [ + 'logger' => $this->logger, + ] + ); self::assertCount(0, $this->logsHandler->getRecords()); putenv('AZURE_AD_RESOURCE'); putenv('AZURE_ENVIRONMENT'); - new ClientCredentialsEnvironmentAuthenticator($this->clientFactory, $this->logger); + new ClientCredentialsAuthenticator( + 'tenant-id', + 'client-id', + 'client-secret', + [ + 'logger' => $this->logger, + ] + ); self::assertTrue($this->logsHandler->hasDebug( 'AZURE_AD_RESOURCE environment variable is not specified, falling back to default.' )); @@ -52,43 +68,11 @@ public function testOptionalEnvsFallback(): void )); } - public function testCheckUsabilitySuccess(): void - { - putenv('AZURE_TENANT_ID=foo'); - putenv('AZURE_CLIENT_ID=foo'); - putenv('AZURE_CLIENT_SECRET=foo'); - - $authenticator = new ClientCredentialsEnvironmentAuthenticator($this->clientFactory, $this->logger); - $authenticator->checkUsability(); - - self::expectNotToPerformAssertions(); - } - - /** @dataProvider provideCheckUsabilityFailureTestData */ - public function testCheckUsabilityFailure(string $requiredEnv): void - { - putenv($requiredEnv); - - $authenticator = new ClientCredentialsEnvironmentAuthenticator($this->clientFactory, $this->logger); - - $this->expectException(ClientException::class); - $this->expectExceptionMessage(sprintf('Environment variable "%s" is not set.', $requiredEnv)); - - $authenticator->checkUsability(); - } - - public function provideCheckUsabilityFailureTestData(): iterable - { - yield 'AZURE_TENANT_ID' => ['AZURE_TENANT_ID']; - yield 'AZURE_CLIENT_ID' => ['AZURE_CLIENT_ID']; - yield 'AZURE_CLIENT_SECRET' => ['AZURE_CLIENT_SECRET']; - } - public function testGetAuthenticationToken(): void { $metadata = $this->getSampleArmMetadata(); - $requestHandler = self::prepareGuzzleMockHandler($requestsHistory, [ + $requestHandler = self::createRequestHandler($requestsHistory, [ new Response( 200, ['Content-Type' => 'application/json'], @@ -106,19 +90,20 @@ public function testGetAuthenticationToken(): void ), ]); - $apiClientFactory = new PlainAzureApiClientFactory([ - 'requestHandler' => $requestHandler, - ]); - - $auth = new ClientCredentialsEnvironmentAuthenticator( - $apiClientFactory, - $this->logger + $auth = new ClientCredentialsAuthenticator( + 'tenant-id', + 'client-id', + 'client-secret', + [ + 'requestHandler' => $requestHandler, + 'logger' => $this->logger, + ] ); $token = $auth->getAuthenticationToken('resource-id'); self::assertCount(2, $requestsHistory); - self::assertSame('ey....ey', $token->accessToken); - self::assertEqualsWithDelta(time() + 3599, $token->accessTokenExpiration->getTimestamp(), 1); + self::assertSame('ey....ey', $token->value); + self::assertEqualsWithDelta(time() + 3599, $token->expiresAt?->getTimestamp(), 1); $request = $requestsHistory[0]['request']; self::assertSame( @@ -128,11 +113,11 @@ public function testGetAuthenticationToken(): void self::assertSame('GET', $request->getMethod()); $request = $requestsHistory[1]['request']; - self::assertSame('https://login.windows.net/tenant123/oauth2/token', $request->getUri()->__toString()); + self::assertSame('https://login.windows.net/tenant-id/oauth2/token', $request->getUri()->__toString()); self::assertSame('POST', $request->getMethod()); self::assertSame('application/x-www-form-urlencoded', $request->getHeader('Content-type')[0]); self::assertSame( - 'grant_type=client_credentials&client_id=client123&client_secret=secret123&resource=resource-id', + 'grant_type=client_credentials&client_id=client-id&client_secret=client-secret&resource=resource-id', $request->getBody()->getContents() ); } @@ -146,7 +131,7 @@ public function testGetAuthenticationTokenWithCustomMetadata(): void putenv('AZURE_ENVIRONMENT=my-azure'); putenv('AZURE_AD_RESOURCE=https://example.com'); - $requestHandler = self::prepareGuzzleMockHandler($requestsHistory, [ + $requestHandler = self::createRequestHandler($requestsHistory, [ new Response( 200, ['Content-Type' => 'application/json'], @@ -164,19 +149,20 @@ public function testGetAuthenticationTokenWithCustomMetadata(): void ), ]); - $apiClientFactory = new PlainAzureApiClientFactory([ - 'requestHandler' => $requestHandler, - ]); - - $auth = new ClientCredentialsEnvironmentAuthenticator( - $apiClientFactory, - $this->logger + $auth = new ClientCredentialsAuthenticator( + 'tenant-id', + 'client-id', + 'client-secret', + [ + 'requestHandler' => $requestHandler, + 'logger' => $this->logger, + ] ); $token = $auth->getAuthenticationToken('resource-id'); self::assertCount(2, $requestsHistory); - self::assertSame('ey....ey', $token->accessToken); - self::assertEqualsWithDelta(time() + 3599, $token->accessTokenExpiration->getTimestamp(), 1); + self::assertSame('ey....ey', $token->value); + self::assertEqualsWithDelta(time() + 3599, $token->expiresAt?->getTimestamp(), 1); $request = $requestsHistory[0]['request']; self::assertSame( @@ -186,11 +172,11 @@ public function testGetAuthenticationTokenWithCustomMetadata(): void self::assertSame('GET', $request->getMethod()); $request = $requestsHistory[1]['request']; - self::assertSame('https://my-custom-login/tenant123/oauth2/token', $request->getUri()->__toString()); + self::assertSame('https://my-custom-login/tenant-id/oauth2/token', $request->getUri()->__toString()); self::assertSame('POST', $request->getMethod()); self::assertSame('application/x-www-form-urlencoded', $request->getHeader('Content-type')[0]); self::assertSame( - 'grant_type=client_credentials&client_id=client123&client_secret=secret123&resource=resource-id', + 'grant_type=client_credentials&client_id=client-id&client_secret=client-secret&resource=resource-id', $request->getBody()->getContents() ); } @@ -199,7 +185,7 @@ public function testGetAuthenticationTokenWithInvalidCustomMetadata(): void { putenv('AZURE_ENVIRONMENT=non-existent'); - $requestHandler = self::prepareGuzzleMockHandler($requestsHistory, [ + $requestHandler = self::createRequestHandler($requestsHistory, [ new Response( 200, ['Content-Type' => 'application/json'], @@ -207,13 +193,14 @@ public function testGetAuthenticationTokenWithInvalidCustomMetadata(): void ), ]); - $apiClientFactory = new PlainAzureApiClientFactory([ - 'requestHandler' => $requestHandler, - ]); - - $auth = new ClientCredentialsEnvironmentAuthenticator( - $apiClientFactory, - $this->logger + $auth = new ClientCredentialsAuthenticator( + 'tenant-id', + 'client-id', + 'client-secret', + [ + 'requestHandler' => $requestHandler, + 'logger' => $this->logger, + ] ); $this->expectException(ClientException::class); @@ -224,7 +211,7 @@ public function testGetAuthenticationTokenWithInvalidCustomMetadata(): void public function testGetAuthenticationTokenMetadataRetry(): void { - $requestHandler = self::prepareGuzzleMockHandler($requestsHistory, [ + $requestHandler = self::createRequestHandler($requestsHistory, [ new Response( 500, ['Content-Type' => 'application/json'], @@ -247,23 +234,24 @@ public function testGetAuthenticationTokenMetadataRetry(): void ), ]); - $apiClientFactory = new PlainAzureApiClientFactory([ - 'requestHandler' => $requestHandler, - ]); - - $auth = new ClientCredentialsEnvironmentAuthenticator( - $apiClientFactory, - $this->logger + $auth = new ClientCredentialsAuthenticator( + 'tenant-id', + 'client-id', + 'client-secret', + [ + 'requestHandler' => $requestHandler, + 'logger' => $this->logger, + ] ); $token = $auth->getAuthenticationToken('resource-id'); - self::assertEquals('ey....ey', $token->accessToken); - self::assertEqualsWithDelta(time() + 3599, $token->accessTokenExpiration->getTimestamp(), 1); + self::assertEquals('ey....ey', $token->value); + self::assertEqualsWithDelta(time() + 3599, $token->expiresAt?->getTimestamp(), 1); } public function testGetAuthenticationTokenMetadataFailure(): void { - $requestHandler = self::prepareGuzzleMockHandler($requestsHistory, [ + $requestHandler = self::createRequestHandler($requestsHistory, [ new Response( 200, ['Content-Type' => 'application/json'], @@ -271,13 +259,14 @@ public function testGetAuthenticationTokenMetadataFailure(): void ), ]); - $apiClientFactory = new PlainAzureApiClientFactory([ - 'requestHandler' => $requestHandler, - ]); - - $auth = new ClientCredentialsEnvironmentAuthenticator( - $apiClientFactory, - $this->logger + $auth = new ClientCredentialsAuthenticator( + 'tenant-id', + 'client-id', + 'client-secret', + [ + 'requestHandler' => $requestHandler, + 'logger' => $this->logger, + ] ); $this->expectException(ClientException::class); @@ -287,7 +276,7 @@ public function testGetAuthenticationTokenMetadataFailure(): void public function testGetAuthenticationTokenTokenError(): void { - $requestHandler = self::prepareGuzzleMockHandler($requestsHistory, [ + $requestHandler = self::createRequestHandler($requestsHistory, [ new Response( 200, ['Content-Type' => 'application/json'], @@ -300,13 +289,14 @@ public function testGetAuthenticationTokenTokenError(): void ), ]); - $apiClientFactory = new PlainAzureApiClientFactory([ - 'requestHandler' => $requestHandler, - ]); - - $auth = new ClientCredentialsEnvironmentAuthenticator( - $apiClientFactory, - $this->logger + $auth = new ClientCredentialsAuthenticator( + 'tenant-id', + 'client-id', + 'client-secret', + [ + 'requestHandler' => $requestHandler, + 'logger' => $this->logger, + ] ); $this->expectException(ClientException::class); @@ -318,10 +308,10 @@ public function testGetAuthenticationTokenTokenError(): void /** * @param list $requestsHistory - * @param list $responses + * @param array $responses * @return HandlerStack */ - private static function prepareGuzzleMockHandler(?array &$requestsHistory, array $responses): HandlerStack + private static function createRequestHandler(?array &$requestsHistory, array $responses): HandlerStack { $requestsHistory = []; @@ -330,4 +320,9 @@ private static function prepareGuzzleMockHandler(?array &$requestsHistory, array return $stack; } + + private function getSampleArmMetadata(): array + { + return Json::decodeArray((string) file_get_contents(__DIR__.'/arm-metadata.json')); + } } diff --git a/libs/azure-api-client/tests/Authentication/ManagedCredentialsAuthenticatorTest.php b/libs/azure-api-client/tests/Authentication/Authenticator/ManagedCredentialsAuthenticatorTest.php similarity index 52% rename from libs/azure-api-client/tests/Authentication/ManagedCredentialsAuthenticatorTest.php rename to libs/azure-api-client/tests/Authentication/Authenticator/ManagedCredentialsAuthenticatorTest.php index 4dbd39e6a..e5a97a489 100644 --- a/libs/azure-api-client/tests/Authentication/ManagedCredentialsAuthenticatorTest.php +++ b/libs/azure-api-client/tests/Authentication/Authenticator/ManagedCredentialsAuthenticatorTest.php @@ -2,15 +2,14 @@ declare(strict_types=1); -namespace Keboola\AzureApiClient\Tests\Authentication; +namespace Keboola\AzureApiClient\Tests\Authentication\Authenticator; use GuzzleHttp\Handler\MockHandler; use GuzzleHttp\HandlerStack; use GuzzleHttp\Middleware; use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Response; -use Keboola\AzureApiClient\ApiClientFactory\PlainAzureApiClientFactory; -use Keboola\AzureApiClient\Authentication\ManagedCredentialsAuthenticator; +use Keboola\AzureApiClient\Authentication\Authenticator\ManagedCredentialsAuthenticator; use Keboola\AzureApiClient\Exception\ClientException; use Monolog\Handler\TestHandler; use Monolog\Logger; @@ -45,15 +44,14 @@ public function testGetAuthenticationToken(): void ), ]); - $apiClientFactory = new PlainAzureApiClientFactory([ + $auth = new ManagedCredentialsAuthenticator([ 'requestHandler' => $requestHandler, + 'logger' => $this->logger, ]); - $auth = new ManagedCredentialsAuthenticator($apiClientFactory, $this->logger); - $token = $auth->getAuthenticationToken('resource-id'); - self::assertSame('ey....ey', $token->accessToken); - self::assertEqualsWithDelta(time() + 3599, $token->accessTokenExpiration->getTimestamp(), 1); + self::assertSame('ey....ey', $token->value); + self::assertEqualsWithDelta(time() + 3599, $token->expiresAt?->getTimestamp(), 1); self::assertCount(1, $requestsHistory); $request = $requestsHistory[0]['request']; @@ -64,8 +62,6 @@ public function testGetAuthenticationToken(): void ); self::assertSame('GET', $request->getMethod()); self::assertSame('true', $request->getHeader('Metadata')[0]); - - self::assertTrue($this->logsHandler->hasInfo('Successfully authenticated using instance metadata.')); } public function testGetAuthenticationTokenWithInvalidResponse(): void @@ -80,12 +76,11 @@ public function testGetAuthenticationTokenWithInvalidResponse(): void ), ]); - $apiClientFactory = new PlainAzureApiClientFactory([ + $auth = new ManagedCredentialsAuthenticator([ 'requestHandler' => $requestHandler, + 'logger' => $this->logger, ]); - $auth = new ManagedCredentialsAuthenticator($apiClientFactory, $this->logger); - $this->expectException(ClientException::class); $this->expectExceptionMessage( 'Failed to map response data: Missing or invalid "access_token" in response: {"foo":"bar"}' @@ -93,64 +88,6 @@ public function testGetAuthenticationTokenWithInvalidResponse(): void $auth->getAuthenticationToken('resource-id'); } - public function testCheckUsabilitySuccess(): void - { - $requestHandler = self::createRequestHandler($requestsHistory, [ - new Response( - 200, - ['Content-Type' => 'application/json'], - '' - ), - ]); - - $apiClientFactory = new PlainAzureApiClientFactory([ - 'requestHandler' => $requestHandler, - ]); - - $auth = new ManagedCredentialsAuthenticator($apiClientFactory, $this->logger); - - $auth->checkUsability(); - self::assertCount(1, $requestsHistory); - - $request = $requestsHistory[0]['request']; - self::assertSame( - 'http://169.254.169.254/metadata?api-version=2019-11-01&format=text', - $request->getUri()->__toString() - ); - self::assertSame('GET', $request->getMethod()); - self::assertSame('true', $request->getHeader('Metadata')[0]); - } - - public function testCheckUsabilityFailure(): void - { - $requestHandler = self::createRequestHandler($requestsHistory, [ - new Response( - 500, - ['Content-Type' => 'application/json'], - '' - ), - new Response( - 500, - ['Content-Type' => 'application/json'], - '' - ), - ]); - - $apiClientFactory = new PlainAzureApiClientFactory([ - 'requestHandler' => $requestHandler, - ]); - - $auth = new ManagedCredentialsAuthenticator($apiClientFactory, $this->logger); - - $this->expectExceptionMessage( - 'Instance metadata service not available: Server error: ' . - '`GET http://169.254.169.254/metadata?api-version=2019-11-01&format=text` resulted in a ' . - '`500 Internal Server Error` response' - ); - $this->expectException(ClientException::class); - $auth->checkUsability(); - } - /** * @param list $requestsHistory * @param list $responses diff --git a/libs/azure-api-client/tests/Authentication/Authenticator/StaticTokenAuthenticatorTest.php b/libs/azure-api-client/tests/Authentication/Authenticator/StaticTokenAuthenticatorTest.php new file mode 100644 index 000000000..51afd94e7 --- /dev/null +++ b/libs/azure-api-client/tests/Authentication/Authenticator/StaticTokenAuthenticatorTest.php @@ -0,0 +1,20 @@ +getAuthenticationToken('foo-resource'); + + self::assertSame('my-token', $token->value); + self::assertNull($token->expiresAt); + } +} diff --git a/libs/azure-api-client/tests/Authentication/Authenticator/SystemAuthenticatorResolverTest.php b/libs/azure-api-client/tests/Authentication/Authenticator/SystemAuthenticatorResolverTest.php new file mode 100644 index 000000000..2ed9d604b --- /dev/null +++ b/libs/azure-api-client/tests/Authentication/Authenticator/SystemAuthenticatorResolverTest.php @@ -0,0 +1,119 @@ +logsHandler = new TestHandler(); + $this->logger = new Logger('tests', [$this->logsHandler]); + + putenv('AZURE_TENANT_ID'); + putenv('AZURE_CLIENT_ID'); + putenv('AZURE_CLIENT_SECRET'); + } + + public function testClientCredentialsAuthenticatorIsUsedWhenEnvIsSet(): void + { + $requestHandler = self::createRequestHandler($requestsHistory, [ + new Response( + 200, + ['Content-Type' => 'application/json'], + (string) file_get_contents(__DIR__ . '/arm-metadata.json'), + ), + new Response( + 200, + ['Content-Type' => 'application/json'], + '{ + "token_type": "Bearer", + "expires_in": 3599, + "resource": "https://vault.azure.net", + "access_token": "ey....ey" + }' + ), + ]); + + $auth = new SystemAuthenticatorResolver([ + 'requestHandler' => $requestHandler, + 'logger' => $this->logger, + ]); + + putenv('AZURE_TENANT_ID=tenant-id'); + putenv('AZURE_CLIENT_ID=client-id'); + putenv('AZURE_CLIENT_SECRET=client-secret-id'); + + $token = $auth->getAuthenticationToken('resource-id'); + + self::assertSame('ey....ey', $token->value); + self::assertEqualsWithDelta(time() + 3599, $token->expiresAt?->getTimestamp(), 1); + self::assertTrue($this->logsHandler->hasDebug( + 'Found Azure client credentials in ENV, using ClientCredentialsAuthenticator' + )); + } + + public function testManagedCredentialsAuthenticatorIsUsedAsFallback(): void + { + $requestHandler = self::createRequestHandler($requestsHistory, [ + new Response( + 200, + ['Content-Type' => 'application/json'], + '{ + "token_type": "Bearer", + "expires_in": 3599, + "resource": "https://vault.azure.net", + "access_token": "ey....ey" + }' + ), + ]); + + $auth = new SystemAuthenticatorResolver([ + 'requestHandler' => $requestHandler, + 'logger' => $this->logger, + ]); + + $token = $auth->getAuthenticationToken('resource-id'); + + self::assertSame('ey....ey', $token->value); + self::assertEqualsWithDelta(time() + 3599, $token->expiresAt?->getTimestamp(), 1); + self::assertTrue($this->logsHandler->hasDebug( + 'Azure client credentials not found in ENV, using ManagedCredentialsAuthenticator' + )); + } + + /** + * @param list $requestsHistory + * @param list $responses + * @return HandlerStack + */ + private static function createRequestHandler(?array &$requestsHistory, array $responses): HandlerStack + { + $requestsHistory = []; + + $stack = HandlerStack::create(new MockHandler($responses)); + $stack->push(Middleware::history($requestsHistory)); + + return $stack; + } +} diff --git a/libs/azure-api-client/tests/Authentication/Authenticator/arm-metadata.json b/libs/azure-api-client/tests/Authentication/Authenticator/arm-metadata.json new file mode 100644 index 000000000..a4db309af --- /dev/null +++ b/libs/azure-api-client/tests/Authentication/Authenticator/arm-metadata.json @@ -0,0 +1,33 @@ +[ + { + "portal": "https://portal.azure.com", + "authentication": { + "loginEndpoint": "https://login.windows.net/", + "audiences": [ + "https://management.core.windows.net/", + "https://management.azure.com/" + ], + "tenant": "common", + "identityProvider": "AAD" + }, + "media": "https://rest.media.azure.net", + "graphAudience": "https://graph.windows.net/", + "graph": "https://graph.windows.net/", + "name": "AzureCloud", + "suffixes": { + "azureDataLakeStoreFileSystem": "azuredatalakestore.net", + "acrLoginServer": "azurecr.io", + "sqlServerHostname": "database.windows.net", + "azureDataLakeAnalyticsCatalogAndJob": "azuredatalakeanalytics.net", + "keyVaultDns": "vault.azure.net", + "storage": "core.windows.net", + "azureFrontDoorEndpointSuffix": "azurefd.net" + }, + "batch": "https://batch.core.windows.net/", + "resourceManager": "https://management.azure.com/", + "vmImageAliasDoc": "https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/arm-compute/quickstart-templates/aliases.json", + "activeDirectoryDataLake": "https://datalake.azure.net/", + "sqlManagement": "https://management.core.windows.net:8443/", + "gallery": "https://gallery.azure.com/" + } +] diff --git a/libs/azure-api-client/tests/ApiClientFactory/AuthorizationHeaderResolverTest.php b/libs/azure-api-client/tests/Authentication/AuthorizationHeaderResolverTest.php similarity index 72% rename from libs/azure-api-client/tests/ApiClientFactory/AuthorizationHeaderResolverTest.php rename to libs/azure-api-client/tests/Authentication/AuthorizationHeaderResolverTest.php index 73b326d29..579edfa19 100644 --- a/libs/azure-api-client/tests/ApiClientFactory/AuthorizationHeaderResolverTest.php +++ b/libs/azure-api-client/tests/Authentication/AuthorizationHeaderResolverTest.php @@ -2,14 +2,13 @@ declare(strict_types=1); -namespace Keboola\AzureApiClient\Tests\ApiClientFactory; +namespace Keboola\AzureApiClient\Tests\Authentication; use DateTimeImmutable; use GuzzleHttp\Psr7\Request; -use Keboola\AzureApiClient\ApiClientFactory\AuthorizationHeaderResolver; -use Keboola\AzureApiClient\Authentication\AuthenticatorFactory; -use Keboola\AzureApiClient\Authentication\AuthenticatorInterface; -use Keboola\AzureApiClient\Authentication\TokenResponse; +use Keboola\AzureApiClient\Authentication\AuthenticationToken; +use Keboola\AzureApiClient\Authentication\Authenticator\AuthenticatorInterface; +use Keboola\AzureApiClient\Authentication\AuthorizationHeaderResolver; use PHPUnit\Framework\TestCase; class AuthorizationHeaderResolverTest extends TestCase @@ -22,20 +21,14 @@ public function testTokenResolve(): void $authenticator ->method('getAuthenticationToken') ->with('foo-resource') - ->willReturn(new TokenResponse( + ->willReturn(new AuthenticationToken( 'auth-token', new DateTimeImmutable('+1 hour'), )) ; - $authenticatorFactory = $this->createMock(AuthenticatorFactory::class); - $authenticatorFactory->expects(self::once()) - ->method('createAuthenticator') - ->willReturn($authenticator) - ; - $resolver = new AuthorizationHeaderResolver( - $authenticatorFactory, + $authenticator, 'foo-resource', ); @@ -52,25 +45,19 @@ public function testTokenIsRefreshesBeforeExpiration(): void ->method('getAuthenticationToken') ->with('foo-resource') ->willReturnOnConsecutiveCalls( - new TokenResponse( + new AuthenticationToken( 'auth-token1', new DateTimeImmutable('+63 seconds'), // 60 seconds is EXPIRATION_MARGIN ), - new TokenResponse( + new AuthenticationToken( 'auth-token2', new DateTimeImmutable('+1 hour'), ), ) ; - $authenticatorFactory = $this->createMock(AuthenticatorFactory::class); - $authenticatorFactory->expects(self::once()) - ->method('createAuthenticator') - ->willReturn($authenticator) - ; - $resolver = new AuthorizationHeaderResolver( - $authenticatorFactory, + $authenticator, 'foo-resource', ); diff --git a/libs/azure-api-client/tests/Authentication/MetadataResponseTest.php b/libs/azure-api-client/tests/Authentication/Model/MetadataResponseTest.php similarity index 94% rename from libs/azure-api-client/tests/Authentication/MetadataResponseTest.php rename to libs/azure-api-client/tests/Authentication/Model/MetadataResponseTest.php index 31392412b..9be518cd0 100644 --- a/libs/azure-api-client/tests/Authentication/MetadataResponseTest.php +++ b/libs/azure-api-client/tests/Authentication/Model/MetadataResponseTest.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace Keboola\AzureApiClient\Tests\Authentication; +namespace Keboola\AzureApiClient\Tests\Authentication\Model; -use Keboola\AzureApiClient\Authentication\MetadataResponse; +use Keboola\AzureApiClient\Authentication\Model\MetadataResponse; use PHPUnit\Framework\TestCase; class MetadataResponseTest extends TestCase diff --git a/libs/azure-api-client/tests/Authentication/TokenResponseTest.php b/libs/azure-api-client/tests/Authentication/Model/TokenResponseTest.php similarity index 91% rename from libs/azure-api-client/tests/Authentication/TokenResponseTest.php rename to libs/azure-api-client/tests/Authentication/Model/TokenResponseTest.php index 4e8429b68..62a6427f5 100644 --- a/libs/azure-api-client/tests/Authentication/TokenResponseTest.php +++ b/libs/azure-api-client/tests/Authentication/Model/TokenResponseTest.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace Keboola\AzureApiClient\Tests\Authentication; +namespace Keboola\AzureApiClient\Tests\Authentication\Model; -use Keboola\AzureApiClient\Authentication\TokenResponse; +use Keboola\AzureApiClient\Authentication\Model\TokenResponse; use Keboola\AzureApiClient\Exception\ClientException; use PHPUnit\Framework\TestCase; diff --git a/libs/azure-api-client/tests/BaseTest.php b/libs/azure-api-client/tests/BaseTest.php deleted file mode 100644 index e81004393..000000000 --- a/libs/azure-api-client/tests/BaseTest.php +++ /dev/null @@ -1,59 +0,0 @@ - 'https://portal.azure.com', - 'authentication' => [ - 'loginEndpoint' => 'https://login.windows.net/', - 'audiences' => [ - 'https://management.core.windows.net/', - 'https://management.azure.com/', - ], - 'tenant' => 'common', - 'identityProvider' => 'AAD', - ], - 'media' => 'https://rest.media.azure.net', - 'graphAudience' => 'https://graph.windows.net/', - 'graph' => 'https://graph.windows.net/', - 'name' => 'AzureCloud', - 'suffixes' => [ - 'azureDataLakeStoreFileSystem' => 'azuredatalakestore.net', - 'acrLoginServer' => 'azurecr.io', - 'sqlServerHostname' => 'database.windows.net', - 'azureDataLakeAnalyticsCatalogAndJob' => 'azuredatalakeanalytics.net', - 'keyVaultDns' => 'vault.azure.net', - 'storage' => 'core.windows.net', - 'azureFrontDoorEndpointSuffix' => 'azurefd.net', - ], - 'batch' => 'https://batch.core.windows.net/', - 'resourceManager' => 'https://management.azure.com/', - // phpcs:ignore Generic.Files.LineLength - 'vmImageAliasDoc' => 'https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/arm-compute/quickstart-templates/aliases.json', - 'activeDirectoryDataLake' => 'https://datalake.azure.net/', - 'sqlManagement' => 'https://management.core.windows.net:8443/', - 'gallery' => 'https://gallery.azure.com/', - ], - ]; - } -} diff --git a/libs/azure-api-client/tests/Marketplace/MarketplaceApiClientTest.php b/libs/azure-api-client/tests/Marketplace/MarketplaceApiClientTest.php index 562b69597..9a91c0b3f 100644 --- a/libs/azure-api-client/tests/Marketplace/MarketplaceApiClientTest.php +++ b/libs/azure-api-client/tests/Marketplace/MarketplaceApiClientTest.php @@ -4,42 +4,25 @@ namespace Keboola\AzureApiClient\Tests\Marketplace; +use GuzzleHttp\Handler\MockHandler; +use GuzzleHttp\HandlerStack; +use GuzzleHttp\Middleware; use GuzzleHttp\Psr7\Request; -use Keboola\AzureApiClient\ApiClient; -use Keboola\AzureApiClient\ApiClientFactory\AuthenticatedAzureApiClientFactory; +use GuzzleHttp\Psr7\Response; +use Keboola\AzureApiClient\Authentication\Authenticator\StaticTokenAuthenticator; use Keboola\AzureApiClient\Json; use Keboola\AzureApiClient\Marketplace\MarketplaceApiClient; use Keboola\AzureApiClient\Marketplace\Model\ActivateSubscriptionRequest; use Keboola\AzureApiClient\Marketplace\Model\ResolveSubscriptionResult; use Keboola\AzureApiClient\Marketplace\Model\Subscription; use Keboola\AzureApiClient\Marketplace\OperationStatus; -use Keboola\AzureApiClient\Tests\ReflectionPropertyAccessTestCase; -use PHPUnit\Framework\Constraint\Callback; use PHPUnit\Framework\TestCase; class MarketplaceApiClientTest extends TestCase { - use ReflectionPropertyAccessTestCase; - - public function testCreateClient(): void - { - $azureApiClient = $this->createMock(ApiClient::class); - - $clientFactory = $this->createMock(AuthenticatedAzureApiClientFactory::class); - $clientFactory->expects(self::once()) - ->method('createClient') - ->with('https://marketplaceapi.microsoft.com', '20e940b3-4c77-4b0b-9a53-9e16a1b010a7') - ->willReturn($azureApiClient) - ; - - $client = MarketplaceApiClient::create($clientFactory); - - self::assertSame($azureApiClient, self::getPrivatePropertyValue($client, 'apiClient')); - } - public function testResolveSubscription(): void { - $apiResponse = ResolveSubscriptionResult::fromResponseData([ + $subscriptionData = [ 'id' => 'subscription-id', 'subscriptionName' => 'subscription-name', 'offerId' => 'offer-id', @@ -53,30 +36,43 @@ public function testResolveSubscription(): void 'quantity' => 1, 'saasSubscriptionStatus' => 'status', ], + ]; + + $requestHandler = self::createRequestHandler($requestsHistory, [ + new Response( + 200, + ['Content-Type' => 'application/json'], + Json::encodeArray($subscriptionData) + ), ]); - $azureApiClient = $this->createMock(ApiClient::class); - $azureApiClient->expects(self::once()) - ->method('sendRequestAndMapResponse') - ->with(self::checkRequestEquals( - 'POST', - '/api/saas/subscriptions/resolve?api-version=2018-08-31', - [ - 'x-ms-marketplace-token' => ['marketplace-token'], - ] - )) - ->willReturn($apiResponse) - ; - - $client = new MarketplaceApiClient($azureApiClient); + $client = new MarketplaceApiClient([ + 'authenticator' => new StaticTokenAuthenticator('my-token'), + 'requestHandler' => $requestHandler, + ]); $result = $client->resolveSubscription('marketplace-token'); - self::assertSame($apiResponse, $result); + self::assertEquals( + ResolveSubscriptionResult::fromResponseData($subscriptionData), + $result + ); + + self::assertCount(1, $requestsHistory); + self::assertRequestEquals( + 'POST', + 'https://marketplaceapi.microsoft.com/api/saas/subscriptions/resolve?api-version=2018-08-31', + [ + 'authorization' => 'Bearer my-token', + 'x-ms-marketplace-token' => 'marketplace-token', + ], + null, + $requestsHistory[0]['request'], + ); } public function testGetSubscription(): void { - $apiResponse = Subscription::fromResponseData([ + $subscriptionData = [ 'id' => 'subscription id', 'publisherId' => 'publisher-id', 'offerId' => 'offer-id', @@ -84,90 +80,133 @@ public function testGetSubscription(): void 'name' => 'subscription-name', 'quantity' => 1, 'saasSubscriptionStatus' => 'status', + ]; + + $requestHandler = self::createRequestHandler($requestsHistory, [ + new Response( + 200, + ['Content-Type' => 'application/json'], + Json::encodeArray($subscriptionData) + ), ]); - $azureApiClient = $this->createMock(ApiClient::class); - $azureApiClient->expects(self::once()) - ->method('sendRequestAndMapResponse') - ->with(self::checkRequestEquals( - 'GET', - '/api/saas/subscriptions/subscription+id?api-version=2018-08-31', - )) - ->willReturn($apiResponse) - ; - - $client = new MarketplaceApiClient($azureApiClient); + $client = new MarketplaceApiClient([ + 'authenticator' => new StaticTokenAuthenticator('my-token'), + 'requestHandler' => $requestHandler, + ]); $result = $client->getSubscription('subscription id'); - self::assertSame($apiResponse, $result); + self::assertEquals( + Subscription::fromResponseData($subscriptionData), + $result + ); + + self::assertCount(1, $requestsHistory); + self::assertRequestEquals( + 'GET', + 'https://marketplaceapi.microsoft.com/api/saas/subscriptions/subscription+id?api-version=2018-08-31', + [ + 'Authorization' => 'Bearer my-token', + ], + null, + $requestsHistory[0]['request'], + ); } public function testActivateSubscription(): void { - $azureApiClient = $this->createMock(ApiClient::class); - $azureApiClient->expects(self::once()) - ->method('sendRequest') - ->with(self::checkRequestEquals( - 'POST', - '/api/saas/subscriptions/subscription+id/activate?api-version=2018-08-31', - [ - 'Content-Type' => ['application/json'], - ], - Json::encodeArray([ - 'planId' => 'plan-id', - 'quantity' => 1, - ]), - )) - ; - - $client = new MarketplaceApiClient($azureApiClient); + $requestHandler = self::createRequestHandler($requestsHistory, [ + new Response(200), + ]); + + $client = new MarketplaceApiClient([ + 'authenticator' => new StaticTokenAuthenticator('my-token'), + 'requestHandler' => $requestHandler, + ]); $client->activateSubscription(new ActivateSubscriptionRequest( 'subscription id', 'plan-id', 1, )); + + self::assertCount(1, $requestsHistory); + self::assertRequestEquals( + 'POST', + // phpcs:ignore Generic.Files.LineLength + 'https://marketplaceapi.microsoft.com/api/saas/subscriptions/subscription+id/activate?api-version=2018-08-31', + [ + 'Authorization' => 'Bearer my-token', + 'Content-Type' => 'application/json', + ], + Json::encodeArray([ + 'planId' => 'plan-id', + 'quantity' => 1, + ]), + $requestsHistory[0]['request'], + ); } public function testUpdateOperationStatus(): void { - $azureApiClient = $this->createMock(ApiClient::class); - $azureApiClient->expects(self::once()) - ->method('sendRequest') - ->with(self::checkRequestEquals( - 'PATCH', - '/api/saas/subscriptions/subscription+id/operations/operation+id?api-version=2018-08-31', - [ - 'Content-Type' => ['application/json'], - ], - Json::encodeArray([ - 'status' => 'Success', - ]), - )) - ; - - $client = new MarketplaceApiClient($azureApiClient); + $requestHandler = self::createRequestHandler($requestsHistory, [ + new Response(200), + ]); + + $client = new MarketplaceApiClient([ + 'authenticator' => new StaticTokenAuthenticator('my-token'), + 'requestHandler' => $requestHandler, + ]); $client->updateOperationStatus( 'subscription id', 'operation id', OperationStatus::SUCCESS, ); + + self::assertCount(1, $requestsHistory); + self::assertRequestEquals( + 'PATCH', + // phpcs:ignore Generic.Files.LineLength + 'https://marketplaceapi.microsoft.com/api/saas/subscriptions/subscription+id/operations/operation+id?api-version=2018-08-31', + [ + 'Authorization' => 'Bearer my-token', + 'Content-Type' => 'application/json', + ], + Json::encodeArray([ + 'status' => 'Success', + ]), + $requestsHistory[0]['request'], + ); } /** - * @return Callback + * @param list $requestsHistory + * @param list $responses + * @return HandlerStack */ - private static function checkRequestEquals( + private static function createRequestHandler(?array &$requestsHistory, array $responses): HandlerStack + { + $requestsHistory = []; + + $stack = HandlerStack::create(new MockHandler($responses)); + $stack->push(Middleware::history($requestsHistory)); + + return $stack; + } + + private static function assertRequestEquals( string $method, string $uri, - array $headers = [], - ?string $body = null - ): Callback { - return self::callback(function (Request $request) use ($method, $uri, $headers, $body) { - self::assertSame($method, $request->getMethod()); - self::assertSame($uri, $request->getUri()->__toString()); - self::assertSame($headers, $request->getHeaders()); - self::assertSame($body ?? '', $request->getBody()->getContents()); - return true; - }); + array $headers, + ?string $body, + Request $request, + ): void { + self::assertSame($method, $request->getMethod()); + self::assertSame($uri, $request->getUri()->__toString()); + + foreach ($headers as $headerName => $headerValue) { + self::assertSame($headerValue, $request->getHeaderLine($headerName)); + } + + self::assertSame($body ?? '', $request->getBody()->getContents()); } } diff --git a/libs/azure-api-client/tests/Marketplace/MeteringServiceApiClientTest.php b/libs/azure-api-client/tests/Marketplace/MeteringServiceApiClientTest.php index 29a52cf8c..d326f96e3 100644 --- a/libs/azure-api-client/tests/Marketplace/MeteringServiceApiClientTest.php +++ b/libs/azure-api-client/tests/Marketplace/MeteringServiceApiClientTest.php @@ -5,37 +5,21 @@ namespace Keboola\AzureApiClient\Tests\Marketplace; use DateTimeImmutable; +use GuzzleHttp\Handler\MockHandler; +use GuzzleHttp\HandlerStack; +use GuzzleHttp\Middleware; use GuzzleHttp\Psr7\Request; -use Keboola\AzureApiClient\ApiClient; -use Keboola\AzureApiClient\ApiClientFactory\AuthenticatedAzureApiClientFactory; +use GuzzleHttp\Psr7\Response; +use Keboola\AzureApiClient\Authentication\Authenticator\StaticTokenAuthenticator; use Keboola\AzureApiClient\Json; use Keboola\AzureApiClient\Marketplace\MeteringServiceApiClient; -use Keboola\AzureApiClient\Marketplace\Model\ReportUsageEventsBatchResult; use Keboola\AzureApiClient\Marketplace\Model\UsageEvent; use Keboola\AzureApiClient\Marketplace\Model\UsageEventResult; -use Keboola\AzureApiClient\Tests\ReflectionPropertyAccessTestCase; -use PHPUnit\Framework\Constraint\Callback; use PHPUnit\Framework\TestCase; class MeteringServiceApiClientTest extends TestCase { - use ReflectionPropertyAccessTestCase; - public function testCreateClient(): void - { - $azureApiClient = $this->createMock(ApiClient::class); - - $clientFactory = $this->createMock(AuthenticatedAzureApiClientFactory::class); - $clientFactory->expects(self::once()) - ->method('createClient') - ->with('https://marketplaceapi.microsoft.com/api/', '20e940b3-4c77-4b0b-9a53-9e16a1b010a7') - ->willReturn($azureApiClient) - ; - - $client = MeteringServiceApiClient::create($clientFactory); - - self::assertSame($azureApiClient, self::getPrivatePropertyValue($client, 'apiClient')); - } public function testReportUsageEventsBatch(): void { @@ -68,38 +52,18 @@ public function testReportUsageEventsBatch(): void ], ]; - $azureApiClient = $this->createMock(ApiClient::class); - $azureApiClient->expects(self::once()) - ->method('sendRequestAndMapResponse') - ->with(self::checkRequestEquals( - 'POST', - 'batchUsageEvent?api-version=2018-08-31', - [ - 'Content-Type' => ['application/json'], - ], - Json::encodeArray([ - 'request' => [ - [ - 'resourceId' => 'resource-1', - 'planId' => 'plan-1', - 'dimension' => 'dim-1', - 'quantity' => 1.0, - 'effectiveStartTime' => '2023-01-01T12:00:00+00:00', - ], - [ - 'resourceId' => 'resource-2', - 'planId' => 'plan-2', - 'dimension' => 'dim-2', - 'quantity' => 2.5, - 'effectiveStartTime' => '2023-01-02T12:00:00+00:00', - ], - ], - ]), - )) - ->willReturn(ReportUsageEventsBatchResult::fromResponseData($apiResponse)) - ; + $requestHandler = self::createRequestHandler($requestsHistory, [ + new Response( + 200, + ['Content-Type' => 'application/json'], + Json::encodeArray($apiResponse) + ), + ]); - $client = new MeteringServiceApiClient($azureApiClient); + $client = new MeteringServiceApiClient([ + 'authenticator' => new StaticTokenAuthenticator('my-token'), + 'requestHandler' => $requestHandler, + ]); $result = $client->reportUsageEventsBatch([ new UsageEvent( 'resource-1', @@ -121,23 +85,66 @@ public function testReportUsageEventsBatch(): void UsageEventResult::fromResponseData($apiResponse['result'][0]), UsageEventResult::fromResponseData($apiResponse['result'][1]), ], $result); + + self::assertCount(1, $requestsHistory); + self::assertRequestEquals( + 'POST', + 'https://marketplaceapi.microsoft.com/api/batchUsageEvent?api-version=2018-08-31', + [ + 'Authorization' => 'Bearer my-token', + 'Content-Type' => 'application/json', + ], + Json::encodeArray([ + 'request' => [ + [ + 'resourceId' => 'resource-1', + 'planId' => 'plan-1', + 'dimension' => 'dim-1', + 'quantity' => 1.0, + 'effectiveStartTime' => '2023-01-01T12:00:00+00:00', + ], + [ + 'resourceId' => 'resource-2', + 'planId' => 'plan-2', + 'dimension' => 'dim-2', + 'quantity' => 2.5, + 'effectiveStartTime' => '2023-01-02T12:00:00+00:00', + ], + ], + ]), + $requestsHistory[0]['request'], + ); } /** - * @return Callback + * @param list $requestsHistory + * @param list $responses + * @return HandlerStack */ - private static function checkRequestEquals( + private static function createRequestHandler(?array &$requestsHistory, array $responses): HandlerStack + { + $requestsHistory = []; + + $stack = HandlerStack::create(new MockHandler($responses)); + $stack->push(Middleware::history($requestsHistory)); + + return $stack; + } + + private static function assertRequestEquals( string $method, string $uri, - array $headers = [], - ?string $body = null - ): Callback { - return self::callback(function (Request $request) use ($method, $uri, $headers, $body) { - self::assertSame($method, $request->getMethod()); - self::assertSame($uri, $request->getUri()->__toString()); - self::assertSame($headers, $request->getHeaders()); - self::assertSame($body ?? '', $request->getBody()->getContents()); - return true; - }); + array $headers, + ?string $body, + Request $request, + ): void { + self::assertSame($method, $request->getMethod()); + self::assertSame($uri, $request->getUri()->__toString()); + + foreach ($headers as $headerName => $headerValue) { + self::assertSame($headerValue, $request->getHeaderLine($headerName)); + } + + self::assertSame($body ?? '', $request->getBody()->getContents()); } } From deead210240a42e8687b040bf46af6189ad42e6f Mon Sep 17 00:00:00 2001 From: Pepa Martinec Date: Fri, 3 Mar 2023 09:45:25 +0100 Subject: [PATCH 2/2] [azure-api-client] Use named arguments instead of optons array --- libs/azure-api-client/composer.json | 3 +- libs/azure-api-client/phpstan.neon | 1 + libs/azure-api-client/src/ApiClient.php | 78 +++++-------------- .../ClientCredentialsAuthenticator.php | 20 ++--- .../ManagedCredentialsAuthenticator.php | 19 +++-- .../SystemAuthenticatorResolver.php | 27 ++++--- .../src/Marketplace/MarketplaceApiClient.php | 24 +++--- .../Marketplace/MeteringServiceApiClient.php | 22 +++++- .../tests/ApiClientFunctionalTest.php | 58 +++++++------- libs/azure-api-client/tests/ApiClientTest.php | 75 +++++------------- .../ClientCredentialsAuthenticatorTest.php | 44 ++++------- .../ManagedCredentialsAuthenticatorTest.php | 17 ++-- .../SystemAuthenticatorResolverTest.php | 19 ++--- .../Marketplace/MarketplaceApiClientTest.php | 32 ++++---- .../MeteringServiceApiClientTest.php | 8 +- 15 files changed, 193 insertions(+), 254 deletions(-) diff --git a/libs/azure-api-client/composer.json b/libs/azure-api-client/composer.json index 76bff7f3d..a8a5dd3c7 100644 --- a/libs/azure-api-client/composer.json +++ b/libs/azure-api-client/composer.json @@ -23,7 +23,7 @@ "php": "^8.1", "guzzlehttp/guzzle": "^7.5", "monolog/monolog": "^2.0|^3.0", - "symfony/validator": "^5.0|^6.0" + "webmozart/assert": "^1.11" }, "config": { "lock": false, @@ -38,6 +38,7 @@ "keboola/coding-standard": "^14.0", "phpstan/phpstan": "^1.9", "phpstan/phpstan-phpunit": "^1.3", + "phpstan/phpstan-webmozart-assert": "^1.2", "sempro/phpunit-pretty-print": "^1.4", "symfony/http-client": "^6.2" }, diff --git a/libs/azure-api-client/phpstan.neon b/libs/azure-api-client/phpstan.neon index 17c041465..87d678836 100644 --- a/libs/azure-api-client/phpstan.neon +++ b/libs/azure-api-client/phpstan.neon @@ -7,3 +7,4 @@ parameters: includes: - vendor/phpstan/phpstan-phpunit/extension.neon + - vendor/phpstan/phpstan-webmozart-assert/extension.neon diff --git a/libs/azure-api-client/src/ApiClient.php b/libs/azure-api-client/src/ApiClient.php index 9f6506df3..787a2e92c 100644 --- a/libs/azure-api-client/src/ApiClient.php +++ b/libs/azure-api-client/src/ApiClient.php @@ -19,10 +19,8 @@ use Psr\Http\Message\ResponseInterface; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; -use Symfony\Component\Validator\Constraints as Assert; -use Symfony\Component\Validator\ConstraintViolationInterface; -use Symfony\Component\Validator\Validation; use Throwable; +use Webmozart\Assert\Assert; class ApiClient { @@ -32,77 +30,43 @@ class ApiClient private readonly HandlerStack $requestHandlerStack; private readonly GuzzleClient $httpClient; private readonly AuthenticatorInterface $authenticator; - private readonly LoggerInterface $logger; /** - * @param array{ - * baseUrl?: null|non-empty-string, - * backoffMaxTries?: null|int<0, max>, - * authenticator?: null|AuthenticatorInterface, - * requestHandler?: null|callable, - * logger?: null|LoggerInterface, - * } $options + * @param non-empty-string|null $baseUrl + * @param int<0, max>|null $backoffMaxTries */ public function __construct( - array $options = [], + ?string $baseUrl = null, + ?int $backoffMaxTries = null, + ?AuthenticatorInterface $authenticator = null, + ?callable $requestHandler = null, + ?LoggerInterface $logger = null, ) { - $errors = Validation::createValidator()->validate($options, new Assert\Collection([ - 'allowMissingFields' => true, - 'allowExtraFields' => false, - 'fields' => [ - 'baseUrl' => new Assert\Sequentially([ - new Assert\Url(), - new Assert\Length(['min' => 1]), - ]), - - 'backoffMaxTries' => new Assert\Sequentially([ - new Assert\Type('int'), - new Assert\GreaterThanOrEqual(0), - ]), - - 'authenticator' => new Assert\Type(AuthenticatorInterface::class), + $backoffMaxTries ??= self::DEFAULT_BACKOFF_RETRIES; + $logger ??= new NullLogger(); - 'requestHandler' => new Assert\Type('callable'), + Assert::nullOrMinLength($baseUrl, 1); + Assert::greaterThanEq($backoffMaxTries, 0); - 'logger' => new Assert\Type(LoggerInterface::class), - ], - ])); - - if ($errors->count() > 0) { - $messages = array_map( - fn(ConstraintViolationInterface $error) => sprintf( - '%s: %s', - $error->getPropertyPath(), - $error->getMessage() - ), - iterator_to_array($errors), - ); - - throw new ClientException(sprintf('Invalid options when creating client: %s', implode("\n", $messages))); - } - - $this->logger = $options['logger'] ?? new NullLogger(); - - $this->authenticator = $options['authenticator'] ?? new SystemAuthenticatorResolver([ - 'backoffMaxTries' => $options['backoffMaxTries'] ?? null, - 'requestHandler' => $options['requestHandler'] ?? null, - 'logger' => $this->logger, - ]); + $this->authenticator = $authenticator ?? new SystemAuthenticatorResolver( + backoffMaxTries: $backoffMaxTries, + requestHandler: $requestHandler ? $requestHandler(...) : null, + logger: $logger, + ); - $this->requestHandlerStack = HandlerStack::create($options['requestHandler'] ?? null); + $this->requestHandlerStack = HandlerStack::create($requestHandler); - $backoffMaxTries = $options['backoffMaxTries'] ?? self::DEFAULT_BACKOFF_RETRIES; if ($backoffMaxTries > 0) { - $this->requestHandlerStack->push(Middleware::retry(new RetryDecider($backoffMaxTries, $this->logger))); + $this->requestHandlerStack->push(Middleware::retry(new RetryDecider($backoffMaxTries, $logger))); } - $this->requestHandlerStack->push(Middleware::log($this->logger, new MessageFormatter( + $this->requestHandlerStack->push(Middleware::log($logger, new MessageFormatter( '{hostname} {req_header_User-Agent} - [{ts}] "{method} {resource} {protocol}/{version}"' . ' {code} {res_header_Content-Length}' ))); $this->httpClient = new GuzzleClient([ - 'base_uri' => $options['baseUrl'] ?? null, + 'base_uri' => $baseUrl, 'handler' => $this->requestHandlerStack, 'headers' => [ 'User-Agent' => self::USER_AGENT, diff --git a/libs/azure-api-client/src/Authentication/Authenticator/ClientCredentialsAuthenticator.php b/libs/azure-api-client/src/Authentication/Authenticator/ClientCredentialsAuthenticator.php index 048fec9e3..bacaedb0c 100644 --- a/libs/azure-api-client/src/Authentication/Authenticator/ClientCredentialsAuthenticator.php +++ b/libs/azure-api-client/src/Authentication/Authenticator/ClientCredentialsAuthenticator.php @@ -28,20 +28,16 @@ class ClientCredentialsAuthenticator implements AuthenticatorInterface private ?string $authEndpoint = null; /** - * @param array{ - * backoffMaxTries?: null|int<0, max>, - * requestHandler?: null|callable, - * logger?: null|LoggerInterface, - * } $options + * @param int<0, max>|null $backoffMaxTries */ public function __construct( private readonly string $tenantId, private readonly string $clientId, private readonly string $clientSecret, - array $options = [], + ?int $backoffMaxTries = null, + ?callable $requestHandler = null, + LoggerInterface $logger = new NullLogger(), ) { - $logger = $options['logger'] ?? new NullLogger(); - $armUrl = (string) getenv(self::ENV_AZURE_AD_RESOURCE); if (!$armUrl) { $armUrl = self::DEFAULT_ARM_URL; @@ -58,8 +54,12 @@ public function __construct( ); } - $options['baseUrl'] = $armUrl; - $this->apiClient = new ApiClient($options); + $this->apiClient = new ApiClient( + baseUrl: $armUrl, + backoffMaxTries: $backoffMaxTries, + requestHandler: $requestHandler, + logger: $logger, + ); } public function getAuthenticationToken(string $resource): AuthenticationToken diff --git a/libs/azure-api-client/src/Authentication/Authenticator/ManagedCredentialsAuthenticator.php b/libs/azure-api-client/src/Authentication/Authenticator/ManagedCredentialsAuthenticator.php index bb8ed7b6c..fa83bbba9 100644 --- a/libs/azure-api-client/src/Authentication/Authenticator/ManagedCredentialsAuthenticator.php +++ b/libs/azure-api-client/src/Authentication/Authenticator/ManagedCredentialsAuthenticator.php @@ -4,6 +4,7 @@ namespace Keboola\AzureApiClient\Authentication\Authenticator; +use Closure; use GuzzleHttp\Psr7\Request; use Keboola\AzureApiClient\ApiClient; use Keboola\AzureApiClient\Authentication\AuthenticationToken; @@ -18,17 +19,19 @@ class ManagedCredentialsAuthenticator implements AuthenticatorInterface private ApiClient $apiClient; /** - * @param array{ - * backoffMaxTries?: null|int<0, max>, - * requestHandler?: null|callable, - * logger?: null|LoggerInterface, - * } $options + * @param int<0, max>|null $backoffMaxTries */ public function __construct( - array $options = [], + ?int $backoffMaxTries = null, + ?Closure $requestHandler = null, + ?LoggerInterface $logger = null, ) { - $options['baseUrl'] = self::INSTANCE_METADATA_SERVICE_ENDPOINT; - $this->apiClient = new ApiClient($options); + $this->apiClient = new ApiClient( + baseUrl: self::INSTANCE_METADATA_SERVICE_ENDPOINT, + backoffMaxTries: $backoffMaxTries, + requestHandler: $requestHandler, + logger: $logger, + ); } public function getAuthenticationToken(string $resource): AuthenticationToken diff --git a/libs/azure-api-client/src/Authentication/Authenticator/SystemAuthenticatorResolver.php b/libs/azure-api-client/src/Authentication/Authenticator/SystemAuthenticatorResolver.php index a3c39bc23..a68c2079d 100644 --- a/libs/azure-api-client/src/Authentication/Authenticator/SystemAuthenticatorResolver.php +++ b/libs/azure-api-client/src/Authentication/Authenticator/SystemAuthenticatorResolver.php @@ -4,6 +4,7 @@ namespace Keboola\AzureApiClient\Authentication\Authenticator; +use Closure; use Keboola\AzureApiClient\Authentication\AuthenticationToken; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; @@ -13,14 +14,12 @@ class SystemAuthenticatorResolver implements AuthenticatorInterface private ?AuthenticatorInterface $resolvedAuthenticator = null; /** - * @param array{ - * backoffMaxTries?: null|int<0, max>, - * requestHandler?: null|callable, - * logger?: null|LoggerInterface, - * } $options + * @param int<0, max>|null $backoffMaxTries */ public function __construct( - private readonly array $options = [], + private readonly ?int $backoffMaxTries = null, + private readonly ?Closure $requestHandler = null, + private readonly LoggerInterface $logger = new NullLogger(), ) { } @@ -32,23 +31,27 @@ public function getAuthenticationToken(string $resource): AuthenticationToken private function resolveAuthenticator(): AuthenticatorInterface { - $logger = $this->options['logger'] ?? new NullLogger(); - $tenantId = (string) getenv('AZURE_TENANT_ID'); $clientId = (string) getenv('AZURE_CLIENT_ID'); $clientSecret = (string) getenv('AZURE_CLIENT_SECRET'); if ($tenantId !== '' && $clientId !== '' && $clientSecret !== '') { - $logger->debug('Found Azure client credentials in ENV, using ClientCredentialsAuthenticator'); + $this->logger->debug('Found Azure client credentials in ENV, using ClientCredentialsAuthenticator'); return new ClientCredentialsAuthenticator( $tenantId, $clientId, $clientSecret, - $this->options, + backoffMaxTries: $this->backoffMaxTries, + requestHandler: $this->requestHandler, + logger: $this->logger, ); } - $logger->debug('Azure client credentials not found in ENV, using ManagedCredentialsAuthenticator'); - return new ManagedCredentialsAuthenticator($this->options); + $this->logger->debug('Azure client credentials not found in ENV, using ManagedCredentialsAuthenticator'); + return new ManagedCredentialsAuthenticator( + backoffMaxTries: $this->backoffMaxTries, + requestHandler: $this->requestHandler, + logger: $this->logger, + ); } } diff --git a/libs/azure-api-client/src/Marketplace/MarketplaceApiClient.php b/libs/azure-api-client/src/Marketplace/MarketplaceApiClient.php index b7144a7f8..4b639e0b6 100644 --- a/libs/azure-api-client/src/Marketplace/MarketplaceApiClient.php +++ b/libs/azure-api-client/src/Marketplace/MarketplaceApiClient.php @@ -18,17 +18,21 @@ class MarketplaceApiClient private ApiClient $apiClient; /** - * @param array{ - * backoffMaxTries?: null|int<0, max>, - * authenticator?: null|AuthenticatorInterface, - * requestHandler?: null|callable, - * logger?: null|LoggerInterface, - * } $options + * @param int<0, max>|null $backoffMaxTries */ - public function __construct(array $options = []) - { - $options['baseUrl'] = 'https://marketplaceapi.microsoft.com'; - $this->apiClient = new ApiClient($options); + public function __construct( + ?int $backoffMaxTries = null, + ?AuthenticatorInterface $authenticator = null, + ?callable $requestHandler = null, + ?LoggerInterface $logger = null, + ) { + $this->apiClient = new ApiClient( + baseUrl: 'https://marketplaceapi.microsoft.com', + backoffMaxTries: $backoffMaxTries, + authenticator: $authenticator, + requestHandler: $requestHandler, + logger: $logger, + ); $this->apiClient->authenticate(Resources::AZURE_MARKETPLACE); } diff --git a/libs/azure-api-client/src/Marketplace/MeteringServiceApiClient.php b/libs/azure-api-client/src/Marketplace/MeteringServiceApiClient.php index 4d3d60e48..55cb779e9 100644 --- a/libs/azure-api-client/src/Marketplace/MeteringServiceApiClient.php +++ b/libs/azure-api-client/src/Marketplace/MeteringServiceApiClient.php @@ -6,19 +6,33 @@ use GuzzleHttp\Psr7\Request; use Keboola\AzureApiClient\ApiClient; +use Keboola\AzureApiClient\Authentication\Authenticator\AuthenticatorInterface; use Keboola\AzureApiClient\Json; use Keboola\AzureApiClient\Marketplace\Model\ReportUsageEventsBatchResult; use Keboola\AzureApiClient\Marketplace\Model\UsageEvent; use Keboola\AzureApiClient\Marketplace\Model\UsageEventResult; +use Psr\Log\LoggerInterface; class MeteringServiceApiClient { private ApiClient $apiClient; - public function __construct(array $options = []) - { - $options['baseUrl'] = 'https://marketplaceapi.microsoft.com/api/'; - $this->apiClient = new ApiClient($options); + /** + * @param int<0, max>|null $backoffMaxTries + */ + public function __construct( + ?int $backoffMaxTries = null, + ?AuthenticatorInterface $authenticator = null, + ?callable $requestHandler = null, + ?LoggerInterface $logger = null, + ) { + $this->apiClient = new ApiClient( + baseUrl: 'https://marketplaceapi.microsoft.com/api/', + backoffMaxTries: $backoffMaxTries, + authenticator: $authenticator, + requestHandler: $requestHandler, + logger: $logger, + ); $this->apiClient->authenticate(Resources::AZURE_MARKETPLACE); } diff --git a/libs/azure-api-client/tests/ApiClientFunctionalTest.php b/libs/azure-api-client/tests/ApiClientFunctionalTest.php index a863c552d..dafa016a5 100644 --- a/libs/azure-api-client/tests/ApiClientFunctionalTest.php +++ b/libs/azure-api-client/tests/ApiClientFunctionalTest.php @@ -26,9 +26,9 @@ public function testSendRequest(): void ], ]); - $apiClient = new ApiClient([ - 'baseUrl' => $mockserver->getServerUrl(), - ]); + $apiClient = new ApiClient( + baseUrl: $mockserver->getServerUrl(), + ); $apiClient->sendRequest(new Request('GET', 'foo/bar')); $recordedRequests = $mockserver->fetchRecordedRequests([ @@ -64,9 +64,9 @@ public function testSendRequestWithMappedResponse(): void ], ]); - $apiClient = new ApiClient([ - 'baseUrl' => $mockserver->getServerUrl(), - ]); + $apiClient = new ApiClient( + baseUrl: $mockserver->getServerUrl(), + ); $response = $apiClient->sendRequestAndMapResponse( new Request( @@ -120,9 +120,9 @@ public function testSendRequestWithMappedArrayResponse(): void ], ]); - $apiClient = new ApiClient([ - 'baseUrl' => $mockserver->getServerUrl(), - ]); + $apiClient = new ApiClient( + baseUrl: $mockserver->getServerUrl(), + ); $response = $apiClient->sendRequestAndMapResponse( new Request( @@ -162,9 +162,9 @@ public function testSendRequestFailingWithRegularError(): void ], ]); - $apiClient = new ApiClient([ - 'baseUrl' => $mockserver->getServerUrl(), - ]); + $apiClient = new ApiClient( + baseUrl: $mockserver->getServerUrl(), + ); $this->expectException(ClientException::class); $this->expectExceptionMessage('BadRequest: This is not good'); @@ -187,9 +187,9 @@ public function testSendRequestFailingWithUnexpectedError(): void ], ]); - $apiClient = new ApiClient([ - 'baseUrl' => $mockserver->getServerUrl(), - ]); + $apiClient = new ApiClient( + baseUrl: $mockserver->getServerUrl(), + ); $this->expectException(ClientException::class); $this->expectExceptionMessage( @@ -214,9 +214,9 @@ public function testSendRequestFailingOnResponseMapping(): void ], ]); - $apiClient = new ApiClient([ - 'baseUrl' => $mockserver->getServerUrl(), - ]); + $apiClient = new ApiClient( + baseUrl: $mockserver->getServerUrl(), + ); $this->expectException(ClientException::class); $this->expectExceptionMessage( @@ -254,9 +254,9 @@ public function testRetrySuccess(): void ], ]); - $apiClient = new ApiClient([ - 'baseUrl' => $mockserver->getServerUrl(), - ]); + $apiClient = new ApiClient( + baseUrl: $mockserver->getServerUrl(), + ); $apiClient->sendRequest(new Request('GET', 'foo/bar')); $recordedRequests = $mockserver->fetchRecordedRequests([ @@ -281,10 +281,10 @@ public function testRetryFailure(): void ], ]); - $apiClient = new ApiClient([ - 'baseUrl' => $mockserver->getServerUrl(), - 'backoffMaxTries' => 2, - ]); + $apiClient = new ApiClient( + baseUrl: $mockserver->getServerUrl(), + backoffMaxTries: 2, + ); $this->expectException(ClientException::class); $this->expectExceptionMessage( @@ -323,10 +323,10 @@ public function testRetryOnThrottling(): void ], ]); - $apiClient = new ApiClient([ - 'baseUrl' => $mockserver->getServerUrl(), - 'backoffMaxTries' => 2, - ]); + $apiClient = new ApiClient( + baseUrl: $mockserver->getServerUrl(), + backoffMaxTries: 2, + ); $response = $apiClient->sendRequestAndMapResponse( new Request('GET', 'foo/bar'), diff --git a/libs/azure-api-client/tests/ApiClientTest.php b/libs/azure-api-client/tests/ApiClientTest.php index acaf9815e..8ff2d0091 100644 --- a/libs/azure-api-client/tests/ApiClientTest.php +++ b/libs/azure-api-client/tests/ApiClientTest.php @@ -8,8 +8,8 @@ use GuzzleHttp\Promise\Create; use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Response; +use InvalidArgumentException; use Keboola\AzureApiClient\ApiClient; -use Keboola\AzureApiClient\Exception\ClientException; use Monolog\Handler\TestHandler; use Monolog\Logger; use PHPUnit\Framework\TestCase; @@ -46,75 +46,42 @@ public function testCreateClientWithDefaults(): void } /** @dataProvider provideInvalidOptions */ - public function testInvalidOptions(array $options, string $expectedError): void - { - $this->expectException(ClientException::class); + public function testInvalidOptions( + ?string $baseUrl, + ?int $backoffMaxTries, + string $expectedError + ): void { + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage($expectedError); - new ApiClient($options); + new ApiClient( + baseUrl: $baseUrl, // @phpstan-ignore-line invalid arguments provided intentionally + backoffMaxTries: $backoffMaxTries, // @phpstan-ignore-line + ); } public function provideInvalidOptions(): iterable { - // phpcs:disable Generic.Files.LineLength yield 'empty baseUrl' => [ - 'options' => [ - 'baseUrl' => '', - ], - 'error' => 'Invalid options when creating client: [baseUrl]: This value is too short. It should have 1 character or more.', - ]; - - yield 'invalid baseUrl' => [ - 'options' => [ - 'baseUrl' => 'foo', - ], - 'error' => 'Invalid options when creating client: [baseUrl]: This value is not a valid URL.', - ]; - - yield 'invalid backoffMaxTries' => [ - 'options' => [ - 'backoffMaxTries' => 'foo', - ], - 'error' => 'Invalid options when creating client: [backoffMaxTries]: This value should be of type int.', + 'baseUrl' => '', + 'backoffMaxTries' => null, + 'error' => 'Expected a value to contain at least 1 characters. Got: ""', ]; yield 'negative backoffMaxTries' => [ - 'options' => [ - 'backoffMaxTries' => -1, - ], - 'error' => 'Invalid options when creating client: [backoffMaxTries]: This value should be greater than or equal to 0.', - ]; - - yield 'invalid authenticator' => [ - 'options' => [ - 'authenticator' => ['foo'], - ], - 'error' => 'Invalid options when creating client: [authenticator]: This value should be of type Keboola\AzureApiClient\Authentication\Authenticator\AuthenticatorInterface.', - ]; - - yield 'invalid requestHandler' => [ - 'options' => [ - 'requestHandler' => ['foo'], - ], - 'error' => 'Invalid options when creating client: [requestHandler]: This value should be of type callable.', - ]; - - yield 'invalid logger' => [ - 'options' => [ - 'logger' => 'foo', - ], - 'error' => 'Invalid options when creating client: [logger]: This value should be of type Psr\Log\LoggerInterface.', + 'baseUrl' => null, + 'backoffMaxTries' => -1, + 'error' => 'Expected a value greater than or equal to 0. Got: -1', ]; - // phpcs:enable Generic.Files.LineLength } public function testLogger(): void { - $client = new ApiClient([ - 'logger' => $this->logger, - 'requestHandler' => fn($request) => Create::promiseFor(new Response(201, [], 'boo')), - ]); + $client = new ApiClient( + logger: $this->logger, + requestHandler: fn($request) => Create::promiseFor(new Response(201, [], 'boo')), + ); $client->sendRequest(new Request('GET', '/')); self::assertTrue($this->logsHandler->hasInfoThatMatches( '#^[\w\d]+ Keboola Azure PHP Client - \[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\+00:00\] "GET /1.1" 201 $#', diff --git a/libs/azure-api-client/tests/Authentication/Authenticator/ClientCredentialsAuthenticatorTest.php b/libs/azure-api-client/tests/Authentication/Authenticator/ClientCredentialsAuthenticatorTest.php index 901daf2ae..6fd7041d1 100644 --- a/libs/azure-api-client/tests/Authentication/Authenticator/ClientCredentialsAuthenticatorTest.php +++ b/libs/azure-api-client/tests/Authentication/Authenticator/ClientCredentialsAuthenticatorTest.php @@ -44,9 +44,7 @@ public function testOptionalEnvsFallback(): void 'tenant-id', 'client-id', 'client-secret', - [ - 'logger' => $this->logger, - ] + logger: $this->logger, ); self::assertCount(0, $this->logsHandler->getRecords()); @@ -56,9 +54,7 @@ public function testOptionalEnvsFallback(): void 'tenant-id', 'client-id', 'client-secret', - [ - 'logger' => $this->logger, - ] + logger: $this->logger, ); self::assertTrue($this->logsHandler->hasDebug( 'AZURE_AD_RESOURCE environment variable is not specified, falling back to default.' @@ -94,10 +90,8 @@ public function testGetAuthenticationToken(): void 'tenant-id', 'client-id', 'client-secret', - [ - 'requestHandler' => $requestHandler, - 'logger' => $this->logger, - ] + requestHandler: $requestHandler, + logger: $this->logger, ); $token = $auth->getAuthenticationToken('resource-id'); @@ -153,10 +147,8 @@ public function testGetAuthenticationTokenWithCustomMetadata(): void 'tenant-id', 'client-id', 'client-secret', - [ - 'requestHandler' => $requestHandler, - 'logger' => $this->logger, - ] + requestHandler: $requestHandler, + logger: $this->logger, ); $token = $auth->getAuthenticationToken('resource-id'); @@ -197,10 +189,8 @@ public function testGetAuthenticationTokenWithInvalidCustomMetadata(): void 'tenant-id', 'client-id', 'client-secret', - [ - 'requestHandler' => $requestHandler, - 'logger' => $this->logger, - ] + requestHandler: $requestHandler, + logger: $this->logger, ); $this->expectException(ClientException::class); @@ -238,10 +228,8 @@ public function testGetAuthenticationTokenMetadataRetry(): void 'tenant-id', 'client-id', 'client-secret', - [ - 'requestHandler' => $requestHandler, - 'logger' => $this->logger, - ] + requestHandler: $requestHandler, + logger: $this->logger, ); $token = $auth->getAuthenticationToken('resource-id'); @@ -263,10 +251,8 @@ public function testGetAuthenticationTokenMetadataFailure(): void 'tenant-id', 'client-id', 'client-secret', - [ - 'requestHandler' => $requestHandler, - 'logger' => $this->logger, - ] + requestHandler: $requestHandler, + logger: $this->logger, ); $this->expectException(ClientException::class); @@ -293,10 +279,8 @@ public function testGetAuthenticationTokenTokenError(): void 'tenant-id', 'client-id', 'client-secret', - [ - 'requestHandler' => $requestHandler, - 'logger' => $this->logger, - ] + requestHandler: $requestHandler, + logger: $this->logger, ); $this->expectException(ClientException::class); diff --git a/libs/azure-api-client/tests/Authentication/Authenticator/ManagedCredentialsAuthenticatorTest.php b/libs/azure-api-client/tests/Authentication/Authenticator/ManagedCredentialsAuthenticatorTest.php index e5a97a489..d2a176213 100644 --- a/libs/azure-api-client/tests/Authentication/Authenticator/ManagedCredentialsAuthenticatorTest.php +++ b/libs/azure-api-client/tests/Authentication/Authenticator/ManagedCredentialsAuthenticatorTest.php @@ -4,6 +4,7 @@ namespace Keboola\AzureApiClient\Tests\Authentication\Authenticator; +use Closure; use GuzzleHttp\Handler\MockHandler; use GuzzleHttp\HandlerStack; use GuzzleHttp\Middleware; @@ -44,10 +45,10 @@ public function testGetAuthenticationToken(): void ), ]); - $auth = new ManagedCredentialsAuthenticator([ - 'requestHandler' => $requestHandler, - 'logger' => $this->logger, - ]); + $auth = new ManagedCredentialsAuthenticator( + requestHandler: $requestHandler(...), + logger: $this->logger, + ); $token = $auth->getAuthenticationToken('resource-id'); self::assertSame('ey....ey', $token->value); @@ -76,10 +77,10 @@ public function testGetAuthenticationTokenWithInvalidResponse(): void ), ]); - $auth = new ManagedCredentialsAuthenticator([ - 'requestHandler' => $requestHandler, - 'logger' => $this->logger, - ]); + $auth = new ManagedCredentialsAuthenticator( + requestHandler: $requestHandler(...), + logger: $this->logger, + ); $this->expectException(ClientException::class); $this->expectExceptionMessage( diff --git a/libs/azure-api-client/tests/Authentication/Authenticator/SystemAuthenticatorResolverTest.php b/libs/azure-api-client/tests/Authentication/Authenticator/SystemAuthenticatorResolverTest.php index 2ed9d604b..15423b6ee 100644 --- a/libs/azure-api-client/tests/Authentication/Authenticator/SystemAuthenticatorResolverTest.php +++ b/libs/azure-api-client/tests/Authentication/Authenticator/SystemAuthenticatorResolverTest.php @@ -10,13 +10,10 @@ use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Response; use Keboola\AzureApiClient\Authentication\Authenticator\SystemAuthenticatorResolver; -use Keboola\AzureApiClient\Json; use Monolog\Handler\TestHandler; use Monolog\Logger; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; -use function _PHPStan_7d6f0f6a4\RingCentral\Psr7\str; -use function Later\now; class SystemAuthenticatorResolverTest extends TestCase { @@ -55,10 +52,10 @@ public function testClientCredentialsAuthenticatorIsUsedWhenEnvIsSet(): void ), ]); - $auth = new SystemAuthenticatorResolver([ - 'requestHandler' => $requestHandler, - 'logger' => $this->logger, - ]); + $auth = new SystemAuthenticatorResolver( + requestHandler: $requestHandler(...), + logger: $this->logger, + ); putenv('AZURE_TENANT_ID=tenant-id'); putenv('AZURE_CLIENT_ID=client-id'); @@ -88,10 +85,10 @@ public function testManagedCredentialsAuthenticatorIsUsedAsFallback(): void ), ]); - $auth = new SystemAuthenticatorResolver([ - 'requestHandler' => $requestHandler, - 'logger' => $this->logger, - ]); + $auth = new SystemAuthenticatorResolver( + requestHandler: $requestHandler(...), + logger: $this->logger, + ); $token = $auth->getAuthenticationToken('resource-id'); diff --git a/libs/azure-api-client/tests/Marketplace/MarketplaceApiClientTest.php b/libs/azure-api-client/tests/Marketplace/MarketplaceApiClientTest.php index 9a91c0b3f..6fd2f7431 100644 --- a/libs/azure-api-client/tests/Marketplace/MarketplaceApiClientTest.php +++ b/libs/azure-api-client/tests/Marketplace/MarketplaceApiClientTest.php @@ -46,10 +46,10 @@ public function testResolveSubscription(): void ), ]); - $client = new MarketplaceApiClient([ - 'authenticator' => new StaticTokenAuthenticator('my-token'), - 'requestHandler' => $requestHandler, - ]); + $client = new MarketplaceApiClient( + authenticator: new StaticTokenAuthenticator('my-token'), + requestHandler: $requestHandler, + ); $result = $client->resolveSubscription('marketplace-token'); self::assertEquals( @@ -90,10 +90,10 @@ public function testGetSubscription(): void ), ]); - $client = new MarketplaceApiClient([ - 'authenticator' => new StaticTokenAuthenticator('my-token'), - 'requestHandler' => $requestHandler, - ]); + $client = new MarketplaceApiClient( + authenticator: new StaticTokenAuthenticator('my-token'), + requestHandler: $requestHandler, + ); $result = $client->getSubscription('subscription id'); self::assertEquals( @@ -119,10 +119,10 @@ public function testActivateSubscription(): void new Response(200), ]); - $client = new MarketplaceApiClient([ - 'authenticator' => new StaticTokenAuthenticator('my-token'), - 'requestHandler' => $requestHandler, - ]); + $client = new MarketplaceApiClient( + authenticator: new StaticTokenAuthenticator('my-token'), + requestHandler: $requestHandler, + ); $client->activateSubscription(new ActivateSubscriptionRequest( 'subscription id', 'plan-id', @@ -152,10 +152,10 @@ public function testUpdateOperationStatus(): void new Response(200), ]); - $client = new MarketplaceApiClient([ - 'authenticator' => new StaticTokenAuthenticator('my-token'), - 'requestHandler' => $requestHandler, - ]); + $client = new MarketplaceApiClient( + authenticator: new StaticTokenAuthenticator('my-token'), + requestHandler: $requestHandler, + ); $client->updateOperationStatus( 'subscription id', 'operation id', diff --git a/libs/azure-api-client/tests/Marketplace/MeteringServiceApiClientTest.php b/libs/azure-api-client/tests/Marketplace/MeteringServiceApiClientTest.php index d326f96e3..7873b2826 100644 --- a/libs/azure-api-client/tests/Marketplace/MeteringServiceApiClientTest.php +++ b/libs/azure-api-client/tests/Marketplace/MeteringServiceApiClientTest.php @@ -60,10 +60,10 @@ public function testReportUsageEventsBatch(): void ), ]); - $client = new MeteringServiceApiClient([ - 'authenticator' => new StaticTokenAuthenticator('my-token'), - 'requestHandler' => $requestHandler, - ]); + $client = new MeteringServiceApiClient( + authenticator: new StaticTokenAuthenticator('my-token'), + requestHandler: $requestHandler, + ); $result = $client->reportUsageEventsBatch([ new UsageEvent( 'resource-1',