diff --git a/libs/git-service-api-client/composer.json b/libs/git-service-api-client/composer.json index 16fbff0c9..4d990bac4 100644 --- a/libs/git-service-api-client/composer.json +++ b/libs/git-service-api-client/composer.json @@ -19,9 +19,16 @@ "Keboola\\GitServiceApiClient\\Tests\\": "tests/" } }, + "repositories": { + "libs": { + "type": "path", + "url": "../../libs/*" + } + }, "require": { "php": "^8.2", "guzzlehttp/guzzle": "^7.5", + "keboola/php-api-client-base": "*@dev", "psr/http-message": "^1.0|^2.0", "psr/log": "^1.0|^2.0|^3.0", "webmozart/assert": "^1.11" diff --git a/libs/git-service-api-client/src/ApiClient.php b/libs/git-service-api-client/src/ApiClient.php deleted file mode 100644 index 969dc1e68..000000000 --- a/libs/git-service-api-client/src/ApiClient.php +++ /dev/null @@ -1,160 +0,0 @@ -auth; - - $stack = $configuration->requestHandler instanceof HandlerStack - ? $configuration->requestHandler - : HandlerStack::create($configuration->requestHandler); - // Resolve headers per request so file-backed auth (e.g. the projected - // Kubernetes SA token) can pick up rotated tokens on every call. - $stack->push(Middleware::mapRequest( - function (RequestInterface $request) use ($auth): RequestInterface { - foreach ($auth->getAuthenticationHeaders() as $name => $value) { - $request = $request->withHeader($name, $value); - } - return $request; - }, - )); - - if ($configuration->backoffMaxTries > 0) { - $stack->push(Middleware::retry(new RetryDecider( - $configuration->backoffMaxTries, - $configuration->logger, - ))); - } - - $stack->push(Middleware::log( - $configuration->logger, - new MessageFormatter('[git-service-api] {method} {uri} : {code} {res_header_Content-Length}'), - )); - - $userAgent = self::USER_AGENT; - if ($configuration->userAgent !== null) { - $userAgent .= ' - ' . $configuration->userAgent; - } - - $this->httpClient = new GuzzleClient([ - 'base_uri' => rtrim($baseUrl, '/') . '/', - 'handler' => $stack, - 'headers' => [ - 'User-Agent' => $userAgent, - 'Content-Type' => 'application/json', - ], - 'connect_timeout' => 10, - 'timeout' => 120, - ]); - } - - public function sendRequest(RequestInterface $request): void - { - $this->doSendRequest($request); - } - - /** - * @template T of ResponseModelInterface - * @param class-string $responseClass - * @param array $options - * @return ($isList is true ? list : T) - */ - public function sendRequestAndMapResponse( - RequestInterface $request, - string $responseClass, - array $options = [], - bool $isList = false, - ) { - $response = $this->doSendRequest($request, $options); - - try { - $data = Json::decodeArray($response->getBody()->getContents()); - } catch (JsonException $e) { - throw new ClientException('Response is not valid JSON: ' . $e->getMessage(), 0, $e); - } - - try { - if ($isList) { - return array_values(array_map($responseClass::fromResponseData(...), $data)); - } - return $responseClass::fromResponseData($data); - } catch (Throwable $e) { - throw new ClientException('Failed to map response data: ' . $e->getMessage(), 0, $e); - } - } - - /** - * @param array $options - */ - private function doSendRequest(RequestInterface $request, array $options = []): ResponseInterface - { - try { - return $this->httpClient->send($request, $options); - } catch (RequestException $e) { - throw $this->processRequestException($e) ?? new ClientException( - $e->getMessage(), - $e->getResponse()?->getStatusCode() ?? 0, - $e, - ); - } catch (GuzzleException $e) { - throw new ClientException($e->getMessage(), 0, $e); - } - } - - private function processRequestException(RequestException $e): ?ClientException - { - $response = $e->getResponse(); - if ($response === null) { - return null; - } - - try { - $data = Json::decodeArray((string) $response->getBody()); - } catch (JsonException) { - return new ClientException(trim($e->getMessage()), $response->getStatusCode(), $e); - } - - $code = $data['code'] ?? null; - $error = $data['error'] ?? null; - if (!is_string($code) || !is_string($error) || $code === '' || $error === '') { - return new ClientException(trim($e->getMessage()), $response->getStatusCode(), $e); - } - - return new ClientException( - trim($code . ': ' . $error), - $response->getStatusCode(), - $e, - ); - } -} diff --git a/libs/git-service-api-client/src/ApiClientConfiguration.php b/libs/git-service-api-client/src/ApiClientConfiguration.php deleted file mode 100644 index a83b42f3a..000000000 --- a/libs/git-service-api-client/src/ApiClientConfiguration.php +++ /dev/null @@ -1,37 +0,0 @@ - $backoffMaxTries - * @param AuthInterface $auth - * How the client authenticates against git-service. Defaults to a - * {@see KeboolaServiceAccountAuth} pointing at the standard - * in-cluster SA token path, which reads (and re-reads) the file on - * every request and throws if the file is missing or empty. Pass - * {@see Auth\ManageApiTokenAuth} for a Manage API token, or - * {@see KeboolaServiceAccountAuth} with a custom path for a - * non-default projected-token mount. - */ - public function __construct( - public readonly AuthInterface $auth = new KeboolaServiceAccountAuth(), - public readonly ?string $userAgent = null, - public readonly int $backoffMaxTries = self::DEFAULT_BACKOFF_RETRIES, - public readonly null|Closure|HandlerStack $requestHandler = null, - public readonly LoggerInterface $logger = new NullLogger(), - ) { - } -} diff --git a/libs/git-service-api-client/src/Auth/AuthInterface.php b/libs/git-service-api-client/src/Auth/AuthInterface.php deleted file mode 100644 index e16316c49..000000000 --- a/libs/git-service-api-client/src/Auth/AuthInterface.php +++ /dev/null @@ -1,13 +0,0 @@ - - */ - public function getAuthenticationHeaders(): array; -} diff --git a/libs/git-service-api-client/src/Auth/KeboolaServiceAccountAuth.php b/libs/git-service-api-client/src/Auth/KeboolaServiceAccountAuth.php deleted file mode 100644 index 6f3de0983..000000000 --- a/libs/git-service-api-client/src/Auth/KeboolaServiceAccountAuth.php +++ /dev/null @@ -1,57 +0,0 @@ -tokenPath)) { - throw new RuntimeException(sprintf( - 'Service account token file "%s" is not readable', - $this->tokenPath, - )); - } - - $token = file_get_contents($this->tokenPath); - if ($token === false) { - throw new RuntimeException(sprintf( - 'Failed to read service account token file "%s"', - $this->tokenPath, - )); - } - - $token = trim($token); - if ($token === '') { - throw new RuntimeException(sprintf( - 'Service account token file is empty: "%s"', - $this->tokenPath, - )); - } - - return [ - 'X-Kubernetes-Authorization' => 'Bearer ' . $token, - ]; - } -} diff --git a/libs/git-service-api-client/src/Auth/ManageApiTokenAuth.php b/libs/git-service-api-client/src/Auth/ManageApiTokenAuth.php deleted file mode 100644 index 6f60a7ed6..000000000 --- a/libs/git-service-api-client/src/Auth/ManageApiTokenAuth.php +++ /dev/null @@ -1,28 +0,0 @@ - $this->token, - ]; - } -} diff --git a/libs/git-service-api-client/src/Exception/ClientException.php b/libs/git-service-api-client/src/Exception/ClientException.php deleted file mode 100644 index c1a115f84..000000000 --- a/libs/git-service-api-client/src/Exception/ClientException.php +++ /dev/null @@ -1,11 +0,0 @@ - 'application/json']; private ApiClient $apiClient; /** * @param non-empty-string $baseUrl + * @param non-empty-string|null $manageToken + * @param int<0, max> $backoffMaxTries * - * Authentication and all other client options come from - * {@see ApiClientConfiguration}. See {@see ApiClient::__construct()}. + * When $manageToken is provided, authenticates with X-KBC-ManageApiToken. + * When null (default), authenticates via the projected Kubernetes ServiceAccount + * token at the default path — see {@see KeboolaServiceAccountAuthenticator}. */ public function __construct( string $baseUrl, - ?ApiClientConfiguration $configuration = null, + ?string $manageToken = null, + ?LoggerInterface $logger = null, + int $backoffMaxTries = ApiClientOptions::DEFAULT_BACKOFF_MAX_TRIES, + int $connectTimeout = ApiClientOptions::DEFAULT_CONNECT_TIMEOUT, + int $requestTimeout = ApiClientOptions::DEFAULT_REQUEST_TIMEOUT, + string $userAgent = self::FALLBACK_USER_AGENT, + null|Closure|HandlerStack $requestHandler = null, ) { - $this->apiClient = new ApiClient($baseUrl, $configuration); + $authenticator = $manageToken !== null + ? new ManageApiTokenAuthenticator($manageToken) + : new KeboolaServiceAccountAuthenticator(); + + $this->apiClient = new ApiClient( + $baseUrl, + $authenticator, + new ApiClientOptions( + userAgent: $userAgent, + backoffMaxTries: $backoffMaxTries, + connectTimeout: $connectTimeout, + requestTimeout: $requestTimeout, + requestHandler: $requestHandler, + logger: $logger, + ), + errorMessageResolver: new GitServiceErrorMessageResolver(), + exceptionClass: GitServiceClientException::class, + ); } public function createRepository(string $name): Repository diff --git a/libs/git-service-api-client/src/GitServiceErrorMessageResolver.php b/libs/git-service-api-client/src/GitServiceErrorMessageResolver.php new file mode 100644 index 000000000..232043adf --- /dev/null +++ b/libs/git-service-api-client/src/GitServiceErrorMessageResolver.php @@ -0,0 +1,29 @@ + $data - */ - public static function encodeArray(array $data): string - { - return json_encode($data, JSON_THROW_ON_ERROR); - } - - /** - * @return array - */ - public static function decodeArray(string $data): array - { - $result = json_decode($data, true, flags: JSON_THROW_ON_ERROR); - if (!is_array($result)) { - throw new JsonException(sprintf('Decoded data is %s, array expected', get_debug_type($result))); - } - - return $result; - } -} diff --git a/libs/git-service-api-client/src/Model/Credential.php b/libs/git-service-api-client/src/Model/Credential.php index fe07704a2..00279d8e5 100644 --- a/libs/git-service-api-client/src/Model/Credential.php +++ b/libs/git-service-api-client/src/Model/Credential.php @@ -4,9 +4,9 @@ namespace Keboola\GitServiceApiClient\Model; +use Keboola\ApiClientBase\ResponseModelInterface; use Keboola\GitServiceApiClient\CredentialType; use Keboola\GitServiceApiClient\KeyPermission; -use Keboola\GitServiceApiClient\ResponseModelInterface; use Webmozart\Assert\Assert; readonly class Credential implements ResponseModelInterface diff --git a/libs/git-service-api-client/src/Model/CredentialListWrapper.php b/libs/git-service-api-client/src/Model/CredentialListWrapper.php index 88eb3569a..0f7727bde 100644 --- a/libs/git-service-api-client/src/Model/CredentialListWrapper.php +++ b/libs/git-service-api-client/src/Model/CredentialListWrapper.php @@ -4,7 +4,7 @@ namespace Keboola\GitServiceApiClient\Model; -use Keboola\GitServiceApiClient\ResponseModelInterface; +use Keboola\ApiClientBase\ResponseModelInterface; use Webmozart\Assert\Assert; /** diff --git a/libs/git-service-api-client/src/Model/Repository.php b/libs/git-service-api-client/src/Model/Repository.php index 082fc8c20..25008f08d 100644 --- a/libs/git-service-api-client/src/Model/Repository.php +++ b/libs/git-service-api-client/src/Model/Repository.php @@ -4,7 +4,7 @@ namespace Keboola\GitServiceApiClient\Model; -use Keboola\GitServiceApiClient\ResponseModelInterface; +use Keboola\ApiClientBase\ResponseModelInterface; use Webmozart\Assert\Assert; final readonly class Repository implements ResponseModelInterface diff --git a/libs/git-service-api-client/src/ResponseModelInterface.php b/libs/git-service-api-client/src/ResponseModelInterface.php deleted file mode 100644 index 7b0539a82..000000000 --- a/libs/git-service-api-client/src/ResponseModelInterface.php +++ /dev/null @@ -1,13 +0,0 @@ - $data - */ - public static function fromResponseData(array $data): static; -} diff --git a/libs/git-service-api-client/src/RetryDecider.php b/libs/git-service-api-client/src/RetryDecider.php deleted file mode 100644 index d7f1a44f9..000000000 --- a/libs/git-service-api-client/src/RetryDecider.php +++ /dev/null @@ -1,57 +0,0 @@ -= $this->maxRetries) { - return false; - } - - $code = null; - if ($response !== null) { - $code = $response->getStatusCode(); - } elseif ($error instanceof Throwable) { - $code = $error->getCode(); - } - - if ($code !== null && $code >= 400 && $code < 500) { - return false; - } - - if ($error !== null || ($code !== null && $code >= 500)) { - $this->logger->warning(sprintf( - 'Request failed (%s), retrying (%s of %s)', - match (true) { - $error instanceof Throwable => $error->getMessage(), - $response !== null => 'HTTP ' . $code, - default => 'unknown', - }, - $retries, - $this->maxRetries, - )); - return true; - } - - return false; - } -} diff --git a/libs/git-service-api-client/tests/ApiClientConfigurationTest.php b/libs/git-service-api-client/tests/ApiClientConfigurationTest.php deleted file mode 100644 index 3120ff274..000000000 --- a/libs/git-service-api-client/tests/ApiClientConfigurationTest.php +++ /dev/null @@ -1,19 +0,0 @@ -auth); - } -} diff --git a/libs/git-service-api-client/tests/ApiClientTest.php b/libs/git-service-api-client/tests/ApiClientTest.php index 92f9e98fd..67890a072 100644 --- a/libs/git-service-api-client/tests/ApiClientTest.php +++ b/libs/git-service-api-client/tests/ApiClientTest.php @@ -8,22 +8,28 @@ use GuzzleHttp\HandlerStack; use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Response; -use Keboola\GitServiceApiClient\ApiClient; -use Keboola\GitServiceApiClient\ApiClientConfiguration; -use Keboola\GitServiceApiClient\Auth\KeboolaServiceAccountAuth; -use Keboola\GitServiceApiClient\Auth\ManageApiTokenAuth; -use Keboola\GitServiceApiClient\Exception\ClientException; +use Keboola\ApiClientBase\ApiClient; +use Keboola\ApiClientBase\ApiClientOptions; +use Keboola\ApiClientBase\Auth\KeboolaServiceAccountAuthenticator; +use Keboola\GitServiceApiClient\Exception\GitServiceClientException; +use Keboola\GitServiceApiClient\GitServiceApiClient; use Keboola\GitServiceApiClient\Model\Repository; use PHPUnit\Framework\TestCase; use RuntimeException; +/** + * Tests that exercise transport-level behaviour of GitServiceApiClient + * (auth headers, retry, error mapping). The underlying transport is now + * provided by keboola/php-api-client-base; these tests confirm the facade + * wires it up correctly for git-service's specific auth and error-format. + */ class ApiClientTest extends TestCase { public function testDefaultAuthThrowsOnFirstRequestWhenNoSaTokenFileExists(): void { // Assumes the test container is NOT running with a projected // connection-token at the Keboola SA path (true in CI). - $defaultPath = KeboolaServiceAccountAuth::DEFAULT_TOKEN_PATH; + $defaultPath = KeboolaServiceAccountAuthenticator::DEFAULT_TOKEN_PATH; if (is_readable($defaultPath)) { self::markTestSkipped(sprintf( 'Keboola SA token at "%s" is mounted in this environment; ' @@ -34,26 +40,31 @@ public function testDefaultAuthThrowsOnFirstRequestWhenNoSaTokenFileExists(): vo // Construction succeeds (the default auth is lazy); the first outbound // request triggers the file read and the resulting failure. - $client = new ApiClient('https://example.test'); + $client = new GitServiceApiClient('https://example.test'); $this->expectException(RuntimeException::class); $this->expectExceptionMessage($defaultPath); - $client->sendRequest(new Request('GET', 'foo')); + // Any domain method that issues a request works; deleteRepository is void and simple. + $client->deleteRepository('some-repo'); } - public function testAddsAuthHeader(): void + public function testAddsManageApiTokenAuthHeader(): void { - $mock = new MockHandler([new Response(200, [], '{}')]); + $mock = new MockHandler([new Response(200, [], (string) json_encode([ + 'name' => 'app-1', + 'createdAt' => 't', + 'defaultBranch' => 'main', + 'sshUrl' => 's', + 'httpsUrl' => 'h', + ]))]); $stack = HandlerStack::create($mock); - $client = new ApiClient( + $client = new GitServiceApiClient( 'https://example.test', - new ApiClientConfiguration( - auth: new ManageApiTokenAuth('secret-token'), - requestHandler: $stack, - ), + manageToken: 'secret-token', + requestHandler: $stack, ); - $client->sendRequest(new Request('GET', 'foo')); + $client->getRepository('app-1'); $lastRequest = $mock->getLastRequest(); self::assertNotNull($lastRequest); @@ -69,20 +80,22 @@ public function testKeboolaSaAuthRereadsTokenEachRequest(): void file_put_contents($tokenPath, "first-token\n"); $mock = new MockHandler([ - new Response(200, [], '{}'), - new Response(200, [], '{}'), + new Response(204), + new Response(204), ]); $stack = HandlerStack::create($mock); - $client = new ApiClient( + // Use the base authenticator directly, which accepts a custom path. + // The facade always uses the default path for SA auth, so we test + // the re-read behaviour at the authenticator level here. + $auth = new KeboolaServiceAccountAuthenticator($tokenPath); + $apiClient = new ApiClient( 'https://example.test', - new ApiClientConfiguration( - requestHandler: $stack, - auth: new KeboolaServiceAccountAuth($tokenPath), - ), + $auth, + new ApiClientOptions(requestHandler: $stack), ); - $client->sendRequest(new Request('GET', 'foo')); + $apiClient->sendRequest(new Request('DELETE', 'repos/app-1')); $lastRequest = $mock->getLastRequest(); self::assertNotNull($lastRequest); self::assertSame( @@ -93,7 +106,7 @@ public function testKeboolaSaAuthRereadsTokenEachRequest(): void // Simulate kubelet rotating the projected token file. file_put_contents($tokenPath, "second-token\n"); - $client->sendRequest(new Request('GET', 'foo')); + $apiClient->sendRequest(new Request('DELETE', 'repos/app-2')); $lastRequest = $mock->getLastRequest(); self::assertNotNull($lastRequest); self::assertSame( @@ -110,19 +123,17 @@ public function testRetriesOn5xx(): void $mock = new MockHandler([ new Response(500), new Response(500), - new Response(200, [], '{}'), + new Response(204), ]); $stack = HandlerStack::create($mock); - $client = new ApiClient( + $client = new GitServiceApiClient( 'https://example.test', - new ApiClientConfiguration( - auth: new ManageApiTokenAuth('token'), - backoffMaxTries: 3, - requestHandler: $stack, - ), + manageToken: 'token', + backoffMaxTries: 3, + requestHandler: $stack, ); - $client->sendRequest(new Request('GET', 'foo')); + $client->deleteRepository('app-1'); // If retries didn't fire, MockHandler would still hold remaining responses. self::assertSame(0, $mock->count()); @@ -135,18 +146,16 @@ public function testThrowsClientExceptionOn4xxWithErrorCodeBody(): void ]); $stack = HandlerStack::create($mock); - $client = new ApiClient( + $client = new GitServiceApiClient( 'https://example.test', - new ApiClientConfiguration( - auth: new ManageApiTokenAuth('token'), - requestHandler: $stack, - ), + manageToken: 'token', + requestHandler: $stack, ); - $this->expectException(ClientException::class); + $this->expectException(GitServiceClientException::class); $this->expectExceptionMessage('repository.notFound: repo missing'); $this->expectExceptionCode(404); - $client->sendRequest(new Request('GET', 'foo')); + $client->deleteRepository('app-1'); } public function testThrowsClientExceptionOn4xxWithoutJson(): void @@ -154,17 +163,15 @@ public function testThrowsClientExceptionOn4xxWithoutJson(): void $mock = new MockHandler([new Response(400, [], 'plain text error')]); $stack = HandlerStack::create($mock); - $client = new ApiClient( + $client = new GitServiceApiClient( 'https://example.test', - new ApiClientConfiguration( - auth: new ManageApiTokenAuth('token'), - requestHandler: $stack, - ), + manageToken: 'token', + requestHandler: $stack, ); - $this->expectException(ClientException::class); + $this->expectException(GitServiceClientException::class); $this->expectExceptionCode(400); - $client->sendRequest(new Request('GET', 'foo')); + $client->deleteRepository('app-1'); } public function testMapsResponseIntoSingleModel(): void @@ -178,65 +185,29 @@ public function testMapsResponseIntoSingleModel(): void ]))]); $stack = HandlerStack::create($mock); - $client = new ApiClient( + $client = new GitServiceApiClient( 'https://example.test', - new ApiClientConfiguration( - auth: new ManageApiTokenAuth('token'), - requestHandler: $stack, - ), + manageToken: 'token', + requestHandler: $stack, ); - $repo = $client->sendRequestAndMapResponse( - new Request('GET', 'repos/app-1'), - Repository::class, - ); + $repo = $client->getRepository('app-1'); self::assertSame('app-1', $repo->name); } - public function testMapsResponseIntoListOfModels(): void - { - $mock = new MockHandler([new Response(200, [], (string) json_encode([ - ['name' => 'a1', 'createdAt' => 't', 'defaultBranch' => 'main', 'sshUrl' => 's1', 'httpsUrl' => 'h1'], - ['name' => 'a2', 'createdAt' => 't', 'defaultBranch' => 'main', 'sshUrl' => 's2', 'httpsUrl' => 'h2'], - ]))]); - $stack = HandlerStack::create($mock); - - $client = new ApiClient( - 'https://example.test', - new ApiClientConfiguration( - auth: new ManageApiTokenAuth('token'), - requestHandler: $stack, - ), - ); - - $repos = $client->sendRequestAndMapResponse( - new Request('GET', 'repos'), - Repository::class, - isList: true, - ); - - self::assertCount(2, $repos); - self::assertSame('a1', $repos[0]->name); - } - public function testThrowsClientExceptionOnInvalidJson(): void { $mock = new MockHandler([new Response(200, [], 'not json')]); $stack = HandlerStack::create($mock); - $client = new ApiClient( + $client = new GitServiceApiClient( 'https://example.test', - new ApiClientConfiguration( - auth: new ManageApiTokenAuth('token'), - requestHandler: $stack, - ), + manageToken: 'token', + requestHandler: $stack, ); - $this->expectException(ClientException::class); - $client->sendRequestAndMapResponse( - new Request('GET', 'foo'), - Repository::class, - ); + $this->expectException(GitServiceClientException::class); + $client->getRepository('app-1'); } } diff --git a/libs/git-service-api-client/tests/Auth/KeboolaServiceAccountAuthTest.php b/libs/git-service-api-client/tests/Auth/KeboolaServiceAccountAuthTest.php deleted file mode 100644 index fcbd2c694..000000000 --- a/libs/git-service-api-client/tests/Auth/KeboolaServiceAccountAuthTest.php +++ /dev/null @@ -1,135 +0,0 @@ -expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('token path must not be empty'); - /** @phpstan-ignore-next-line argument.type — exercising the runtime guard */ - new KeboolaServiceAccountAuth(''); - } - - public function testDefaultTokenPathPointsAtKeboolaProjectedMount(): void - { - self::assertSame( - '/var/run/secrets/connection.keboola.com/serviceaccount/token', - KeboolaServiceAccountAuth::DEFAULT_TOKEN_PATH, - ); - } - - public function testReturnsBearerHeaderFromFile(): void - { - $tokenPath = $this->makeTempFile('jwt-from-file'); - try { - $auth = new KeboolaServiceAccountAuth($tokenPath); - - self::assertSame( - ['X-Kubernetes-Authorization' => 'Bearer jwt-from-file'], - $auth->getAuthenticationHeaders(), - ); - } finally { - @unlink($tokenPath); - } - } - - public function testTrimsTrailingWhitespaceFromFileContents(): void - { - // Projected SA tokens are usually written without a trailing newline, - // but be defensive — `kubectl create token` and similar tools add one. - $tokenPath = $this->makeTempFile("jwt-from-file\n"); - try { - $auth = new KeboolaServiceAccountAuth($tokenPath); - - self::assertSame( - ['X-Kubernetes-Authorization' => 'Bearer jwt-from-file'], - $auth->getAuthenticationHeaders(), - ); - } finally { - @unlink($tokenPath); - } - } - - public function testRereadsFileOnEveryCall(): void - { - $tokenPath = $this->makeTempFile('first-token'); - try { - $auth = new KeboolaServiceAccountAuth($tokenPath); - - self::assertSame( - ['X-Kubernetes-Authorization' => 'Bearer first-token'], - $auth->getAuthenticationHeaders(), - ); - - // Simulate kubelet rotating the projected token file. - file_put_contents($tokenPath, 'second-token'); - - self::assertSame( - ['X-Kubernetes-Authorization' => 'Bearer second-token'], - $auth->getAuthenticationHeaders(), - ); - } finally { - @unlink($tokenPath); - } - } - - public function testThrowsWhenFileIsNotReadable(): void - { - $missingPath = sys_get_temp_dir() . '/does-not-exist-' . bin2hex(random_bytes(8)); - $auth = new KeboolaServiceAccountAuth($missingPath); - - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('is not readable'); - $this->expectExceptionMessage($missingPath); - $auth->getAuthenticationHeaders(); - } - - public function testThrowsWhenFileIsEmpty(): void - { - $tokenPath = $this->makeTempFile(''); - try { - $auth = new KeboolaServiceAccountAuth($tokenPath); - - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('is empty'); - $this->expectExceptionMessage($tokenPath); - $auth->getAuthenticationHeaders(); - } finally { - @unlink($tokenPath); - } - } - - public function testThrowsWhenFileContainsOnlyWhitespace(): void - { - $tokenPath = $this->makeTempFile("\n\t "); - try { - $auth = new KeboolaServiceAccountAuth($tokenPath); - - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('is empty'); - $auth->getAuthenticationHeaders(); - } finally { - @unlink($tokenPath); - } - } - - /** - * @return non-empty-string - */ - private function makeTempFile(string $contents): string - { - $path = (string) tempnam(sys_get_temp_dir(), 'kbla-sa-auth-'); - self::assertNotSame('', $path); - file_put_contents($path, $contents); - return $path; - } -} diff --git a/libs/git-service-api-client/tests/Auth/ManageApiTokenAuthTest.php b/libs/git-service-api-client/tests/Auth/ManageApiTokenAuthTest.php deleted file mode 100644 index e84813df8..000000000 --- a/libs/git-service-api-client/tests/Auth/ManageApiTokenAuthTest.php +++ /dev/null @@ -1,30 +0,0 @@ - 'secret-token'], - $auth->getAuthenticationHeaders(), - ); - } - - public function testRejectsEmptyToken(): void - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Manage API token must not be empty'); - /** @phpstan-ignore-next-line argument.type — exercising the runtime guard */ - new ManageApiTokenAuth(''); - } -} diff --git a/libs/git-service-api-client/tests/GitServiceApiClientTest.php b/libs/git-service-api-client/tests/GitServiceApiClientTest.php index 61c0cf74c..08d483235 100644 --- a/libs/git-service-api-client/tests/GitServiceApiClientTest.php +++ b/libs/git-service-api-client/tests/GitServiceApiClientTest.php @@ -7,8 +7,6 @@ use GuzzleHttp\Handler\MockHandler; use GuzzleHttp\HandlerStack; use GuzzleHttp\Psr7\Response; -use Keboola\GitServiceApiClient\ApiClientConfiguration; -use Keboola\GitServiceApiClient\Auth\ManageApiTokenAuth; use Keboola\GitServiceApiClient\CredentialType; use Keboola\GitServiceApiClient\GitServiceApiClient; use Keboola\GitServiceApiClient\KeyPermission; @@ -22,10 +20,8 @@ private function buildClient(MockHandler $mock): GitServiceApiClient $stack = HandlerStack::create($mock); return new GitServiceApiClient( 'https://example.test', - new ApiClientConfiguration( - auth: new ManageApiTokenAuth('token'), - requestHandler: $stack, - ), + manageToken: 'token', + requestHandler: $stack, ); } diff --git a/libs/git-service-api-client/tests/GitServiceErrorMessageResolverTest.php b/libs/git-service-api-client/tests/GitServiceErrorMessageResolverTest.php new file mode 100644 index 000000000..472da84be --- /dev/null +++ b/libs/git-service-api-client/tests/GitServiceErrorMessageResolverTest.php @@ -0,0 +1,89 @@ +resolver = new GitServiceErrorMessageResolver(); + } + + public function testReturnsFormattedMessageWhenCodeAndErrorPresent(): void + { + $body = (string) json_encode(['code' => 'repository.notFound', 'error' => 'repo missing']); + $result = ($this->resolver)($body, 404); + + self::assertSame('repository.notFound: repo missing', $result); + } + + public function testReturnsNullWhenBodyIsNotJson(): void + { + $result = ($this->resolver)('plain text error', 400); + + self::assertNull($result); + } + + public function testReturnsNullWhenCodeMissing(): void + { + $body = (string) json_encode(['error' => 'something went wrong']); + $result = ($this->resolver)($body, 500); + + self::assertNull($result); + } + + public function testReturnsNullWhenErrorMissing(): void + { + $body = (string) json_encode(['code' => 'some.code']); + $result = ($this->resolver)($body, 500); + + self::assertNull($result); + } + + public function testReturnsNullWhenCodeIsEmpty(): void + { + $body = (string) json_encode(['code' => '', 'error' => 'repo missing']); + $result = ($this->resolver)($body, 404); + + self::assertNull($result); + } + + public function testReturnsNullWhenErrorIsEmpty(): void + { + $body = (string) json_encode(['code' => 'repository.notFound', 'error' => '']); + $result = ($this->resolver)($body, 404); + + self::assertNull($result); + } + + public function testReturnsNullWhenCodeIsNotString(): void + { + $body = (string) json_encode(['code' => 42, 'error' => 'repo missing']); + $result = ($this->resolver)($body, 404); + + self::assertNull($result); + } + + public function testReturnsNullWhenBodyIsEmptyJson(): void + { + $result = ($this->resolver)('{}', 500); + + self::assertNull($result); + } + + public function testTrimsWhitespaceFromMessage(): void + { + $body = (string) json_encode(['code' => ' repo.error ', 'error' => ' bad request ']); + $result = ($this->resolver)($body, 400); + + // trim is applied to the concatenated string + self::assertSame('repo.error : bad request', $result); + } +} diff --git a/libs/git-service-api-client/tests/RetryDeciderTest.php b/libs/git-service-api-client/tests/RetryDeciderTest.php deleted file mode 100644 index d0a56630b..000000000 --- a/libs/git-service-api-client/tests/RetryDeciderTest.php +++ /dev/null @@ -1,51 +0,0 @@ -