diff --git a/azure-pipelines.tags.yml b/azure-pipelines.tags.yml index 192cbd8ee..b5cbc838e 100644 --- a/azure-pipelines.tags.yml +++ b/azure-pipelines.tags.yml @@ -22,6 +22,10 @@ resources: type: github endpoint: keboola name: keboola/configuration-variables-resolver + - repository: encryption-api-php-client + type: github + endpoint: keboola + name: keboola/encryption-api-php-client - repository: input-mapping type: github endpoint: keboola @@ -58,6 +62,10 @@ resources: type: github endpoint: keboola name: keboola/settle + - repository: slicer + type: github + endpoint: keboola + name: keboola/slicer - repository: staging-provider type: github endpoint: keboola @@ -66,10 +74,6 @@ resources: type: github endpoint: keboola name: keboola/vault-api-php-client - - repository: slicer - type: github - endpoint: keboola - name: keboola/slicer variables: DOCKER_BUILDKIT: 1 @@ -100,6 +104,14 @@ jobs: libraryPath: libs/configuration-variables-resolver tagPrefix: configuration-variables-resolver/ + - template: azure-pipelines/jobs/split-library.yml + parameters: + condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/encryption-api-client/') + sourceRepo: platform-libraries + targetRepo: encryption-api-php-client + libraryPath: libs/encryption-api-client + tagPrefix: encryption-api-client/ + - template: azure-pipelines/jobs/split-library.yml parameters: condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/input-mapping/') @@ -172,6 +184,14 @@ jobs: libraryPath: libs/settle tagPrefix: settle/ + - template: azure-pipelines/jobs/split-library.yml + parameters: + condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/slicer/') + sourceRepo: platform-libraries + targetRepo: slicer + libraryPath: libs/slicer + tagPrefix: slicer/ + - template: azure-pipelines/jobs/split-library.yml parameters: condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/staging-provider/') @@ -180,7 +200,6 @@ jobs: libraryPath: libs/staging-provider tagPrefix: staging-provider/ - - template: azure-pipelines/jobs/split-library.yml parameters: condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/vault-api-client/') @@ -188,11 +207,3 @@ jobs: targetRepo: vault-api-php-client libraryPath: libs/vault-api-client tagPrefix: vault-api-client/ - - - template: azure-pipelines/jobs/split-library.yml - parameters: - condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/slicer/') - sourceRepo: platform-libraries - targetRepo: slicer - libraryPath: libs/slicer - tagPrefix: slicer/ diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 804997871..44801bb84 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -22,6 +22,10 @@ resources: type: github endpoint: keboola name: keboola/configuration-variables-resolver + - repository: encryption-api-php-client + type: github + endpoint: keboola + name: keboola/encryption-api-php-client - repository: input-mapping type: github endpoint: keboola @@ -58,6 +62,10 @@ resources: type: github endpoint: keboola name: keboola/settle + - repository: slicer + type: github + endpoint: keboola + name: keboola/slicer - repository: staging-provider type: github endpoint: keboola @@ -66,10 +74,6 @@ resources: type: github endpoint: keboola name: keboola/vault-api-php-client - - repository: slicer - type: github - endpoint: keboola - name: keboola/slicer variables: DOCKER_BUILDKIT: 1 @@ -88,6 +92,7 @@ stages: apiBundle:libs/api-bundle \ azureApiClient:libs/azure-api-client \ configurationVariablesResolver:libs/configuration-variables-resolver \ + encryptionApiClient:libs/encryption-api-client \ inputMapping:libs/input-mapping \ k8sClient:libs/k8s-client \ loggingBundle:libs/logging-bundle \ @@ -97,9 +102,9 @@ stages: sandboxesServiceApiClient:libs/sandboxes-service-api-client \ serviceClient:libs/service-client \ settle:libs/settle \ + slicer:libs/slicer \ stagingProvider:libs/staging-provider \ - vaultApiClient:libs/vault-api-client \ - slicer:libs/slicer + vaultApiClient:libs/vault-api-client displayName: 'Find changes' name: findChanges @@ -140,6 +145,14 @@ stages: jobs: - template: libs/configuration-variables-resolver/azure-pipelines.tests.yml + - stage: tests_encryptionApiClient + displayName: Tests - Encryption API Client + lockBehavior: sequential + dependsOn: build + condition: and(succeeded(), dependencies.build.outputs['checkChanges.findChanges.changedProjects_encryptionApiClient']) + jobs: + - template: libs/encryption-api-client/azure-pipelines.tests.yml + - stage: tests_inputMapping displayName: Tests - Input Mapping lockBehavior: sequential @@ -212,6 +225,14 @@ stages: jobs: - template: libs/settle/azure-pipelines.tests.yml + - stage: tests_slicer + displayName: Tests - Slicer + lockBehavior: sequential + dependsOn: build + condition: and(succeeded(), dependencies.build.outputs['checkChanges.findChanges.changedProjects_slicer']) + jobs: + - template: libs/slicer/azure-pipelines.tests.yml + - stage: tests_stagingProvider displayName: Tests - Staging Provider lockBehavior: sequential @@ -228,14 +249,6 @@ stages: jobs: - template: libs/vault-api-client/azure-pipelines.tests.yml - - stage: tests_slicer - displayName: Tests - Slicer - lockBehavior: sequential - dependsOn: build - condition: and(succeeded(), dependencies.build.outputs['checkChanges.findChanges.changedProjects_slicer']) - jobs: - - template: libs/slicer/azure-pipelines.tests.yml - - stage: testsResults displayName: Wait for tests results dependsOn: @@ -243,6 +256,7 @@ stages: - tests_apiBundle - tests_azureApiClient - tests_configurationVariablesResolver + - tests_encryptionApiClient - tests_inputMapping - tests_k8sClient - tests_loggingBundle @@ -252,14 +266,15 @@ stages: - tests_sandboxesServiceApiClient - tests_serviceClient - tests_settle + - tests_slicer - tests_stagingProvider - tests_vaultApiClient - - tests_slicer condition: | and( in(dependencies.tests_apiBundle.result, 'Succeeded', 'Skipped'), in(dependencies.tests_azureApiClient.result, 'Succeeded', 'Skipped'), in(dependencies.tests_configurationVariablesResolver.result, 'Succeeded', 'Skipped'), + in(dependencies.tests_encryptionApiClient.result, 'Succeeded', 'Skipped'), in(dependencies.tests_inputMapping.result, 'Succeeded', 'Skipped'), in(dependencies.tests_k8sClient.result, 'Succeeded', 'Skipped'), in(dependencies.tests_loggingBundle.result, 'Succeeded', 'Skipped'), @@ -269,9 +284,9 @@ stages: in(dependencies.tests_sandboxesServiceApiClient.result, 'Succeeded', 'Skipped'), in(dependencies.tests_serviceClient.result, 'Succeeded', 'Skipped'), in(dependencies.tests_settle.result, 'Succeeded', 'Skipped'), + in(dependencies.tests_slicer.result, 'Succeeded', 'Skipped'), in(dependencies.tests_stagingProvider.result, 'Succeeded', 'Skipped'), - in(dependencies.tests_vaultApiClient.result, 'Succeeded', 'Skipped'), - in(dependencies.tests_slicer.result, 'Succeeded', 'Skipped') + in(dependencies.tests_vaultApiClient.result, 'Succeeded', 'Skipped') ) jobs: - job: @@ -307,6 +322,14 @@ stages: libraryPath: libs/configuration-variables-resolver tagPrefix: configuration-variables-resolver/ + - template: azure-pipelines/jobs/split-library.yml + parameters: + condition: and(succeeded(), stageDependencies.build.checkChanges.outputs['findChanges.changedProjects_encryptionApiClient']) + sourceRepo: platform-libraries + targetRepo: encryption-api-php-client + libraryPath: libs/encryption-api-client + tagPrefix: encryption-api-client/ + - template: azure-pipelines/jobs/split-library.yml parameters: condition: and(succeeded(), stageDependencies.build.checkChanges.outputs['findChanges.changedProjects_inputMapping']) @@ -379,6 +402,14 @@ stages: libraryPath: libs/settle tagPrefix: settle/ + - template: azure-pipelines/jobs/split-library.yml + parameters: + condition: and(succeeded(), stageDependencies.build.checkChanges.outputs['findChanges.changedProjects_slicer']) + sourceRepo: platform-libraries + targetRepo: slicer + libraryPath: libs/slicer + tagPrefix: slicer/ + - template: azure-pipelines/jobs/split-library.yml parameters: condition: and(succeeded(), stageDependencies.build.checkChanges.outputs['findChanges.changedProjects_stagingProvider']) @@ -394,11 +425,3 @@ stages: targetRepo: vault-api-php-client libraryPath: libs/vault-api-client tagPrefix: vault-api-client/ - - - template: azure-pipelines/jobs/split-library.yml - parameters: - condition: and(succeeded(), stageDependencies.build.checkChanges.outputs['findChanges.changedProjects_slicer']) - sourceRepo: platform-libraries - targetRepo: slicer - libraryPath: libs/slicer - tagPrefix: slicer/ diff --git a/docker-compose.yml b/docker-compose.yml index 73fb66e69..33f788d86 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -60,6 +60,11 @@ services: - STORAGE_API_TOKEN_MASTER - STORAGE_API_URL + dev-encryption-api-client: + <<: *dev + image: keboola/encryption-api-client + working_dir: /code/libs/encryption-api-client + dev-input-mapping: <<: *dev image: keboola/input-mapping @@ -77,6 +82,10 @@ services: image: keboola/k8s-client working_dir: /code/libs/k8s-client + dev-logging-bundle: + <<: *dev + working_dir: /code/libs/logging-bundle + dev-messenger-bundle: <<: *dev82 working_dir: /code/libs/messenger-bundle @@ -117,6 +126,11 @@ services: image: keboola/settle working_dir: /code/libs/settle + dev-slicer: + <<: *dev + image: keboola/slicer + working_dir: /code/libs/slicer + dev-staging-provider: <<: *dev image: keboola/staging-provider @@ -132,12 +146,3 @@ services: <<: *dev82 image: keboola/vault-api-client working_dir: /code/libs/vault-api-client - - dev-slicer: - <<: *dev - image: keboola/slicer - working_dir: /code/libs/slicer - - dev-logging-bundle: - <<: *dev - working_dir: /code/libs/logging-bundle diff --git a/libs/encryption-api-client/.gitignore b/libs/encryption-api-client/.gitignore new file mode 100644 index 000000000..48b8bf907 --- /dev/null +++ b/libs/encryption-api-client/.gitignore @@ -0,0 +1 @@ +vendor/ diff --git a/libs/encryption-api-client/LICENSE b/libs/encryption-api-client/LICENSE new file mode 100644 index 000000000..d16cdbea9 --- /dev/null +++ b/libs/encryption-api-client/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Keboola :(){:|:&};: s.r.o. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/libs/encryption-api-client/README.md b/libs/encryption-api-client/README.md new file mode 100644 index 000000000..771b3f3ef --- /dev/null +++ b/libs/encryption-api-client/README.md @@ -0,0 +1,28 @@ +# Encryption API Client + +## Installation +```bash +composer require keboola/encryption-api-php-client +``` + +## Usage + +```php +use Keboola\EncryptionApiClient\Migrations; + +$migrations = new Migrations(getenv('STORAGE_API_TOKEN')); + +$resultMessage = $migrations->migrateConfiguration( + sourceStorageApiToken: '...', + destinationStack: 'connection.europe-west3.gcp.keboola.com', + destinationStorageApiToken: '...', + componentId: 'keboola.data-apps', + configId: '123456', + branchId: '102', + dryRun: true, +); +``` + +## License + +MIT licensed, see [LICENSE](./LICENSE) file. diff --git a/libs/encryption-api-client/azure-pipelines.tests.yml b/libs/encryption-api-client/azure-pipelines.tests.yml new file mode 100644 index 000000000..18cd72ca9 --- /dev/null +++ b/libs/encryption-api-client/azure-pipelines.tests.yml @@ -0,0 +1,21 @@ +jobs: + - template: ../../azure-pipelines/jobs/run-tests.yml + parameters: + displayName: Tests (PHP 7.4) + serviceName: dev74 + testCommand: | + bash -c '\ + echo "xdebug.mode=coverage" >> /usr/local/etc/php/conf.d/xdebug.ini + cd libs/encryption-api-client && \ + composer update --prefer-lowest && \ + composer ci' + + - template: ../../azure-pipelines/jobs/run-tests.yml + parameters: + displayName: Tests (PHP 8.2) + serviceName: dev82 + testCommand: | + bash -c '\ + cd libs/encryption-api-client && \ + composer update && \ + composer ci' diff --git a/libs/encryption-api-client/composer.json b/libs/encryption-api-client/composer.json new file mode 100644 index 000000000..03584d082 --- /dev/null +++ b/libs/encryption-api-client/composer.json @@ -0,0 +1,61 @@ +{ + "name": "keboola/encryption-api-php-client", + "type": "library", + "license": "MIT", + "description": "Keboola Encryption API Client", + "authors": [ + { + "name": "Keboola", + "email": "devel@keboola.com" + } + ], + "autoload": { + "psr-4": { + "Keboola\\EncryptionApiClient\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Keboola\\EncryptionApiClient\\Tests\\": "tests/" + } + }, + "require": { + "php": "^7.4 || ^8.2", + "ext-json": "*", + "ext-curl": "*", + "guzzlehttp/guzzle": "^6.5 || ^7.8" + }, + "config": { + "lock": false, + "sort-packages": true, + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true, + "infection/extension-installer": true + } + }, + "require-dev": { + "infection/infection": "0.26.6 || ^0.27.9", + "keboola/coding-standard": "^15.0", + "phpstan/phpstan": "^1.10", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5", + "sempro/phpunit-pretty-print": "^1.4" + }, + "scripts": { + "ci": [ + "@composer validate --no-check-publish --no-check-all", + "@phpcs", + "@phpstan", + "@tests", + "@infection" + ], + "phpcs": "phpcs -n --ignore=vendor,cache,Kernel.php --extensions=php .", + "phpcbf": "phpcbf --extensions=php src tests", + "phpstan": "phpstan analyse --no-progress -c phpstan.neon", + "tests": [ + "@putenv XDEBUG_MODE=coverage", + "phpunit --coverage-clover /tmp/build-logs/clover.xml --coverage-xml=/tmp/build-logs/coverage-xml --log-junit=/tmp/build-logs/phpunit.junit.xml" + ], + "infection": "infection --log-verbosity=all --threads=20 --min-covered-msi=75 --coverage=/tmp/build-logs --only-covered" + } +} diff --git a/libs/encryption-api-client/infection.json.dist b/libs/encryption-api-client/infection.json.dist new file mode 100644 index 000000000..022744e3f --- /dev/null +++ b/libs/encryption-api-client/infection.json.dist @@ -0,0 +1,14 @@ +{ + "timeout": 60, + "source": { + "directories": [ + "src\/" + ] + }, + "logs": { + "text": "/tmp/infection.log" + }, + "mutators": { + "@default": true + } +} diff --git a/libs/encryption-api-client/phpcs.xml b/libs/encryption-api-client/phpcs.xml new file mode 100644 index 000000000..47081669c --- /dev/null +++ b/libs/encryption-api-client/phpcs.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + */src/Kernel.php + diff --git a/libs/encryption-api-client/phpstan.neon b/libs/encryption-api-client/phpstan.neon new file mode 100644 index 000000000..c8d6d1662 --- /dev/null +++ b/libs/encryption-api-client/phpstan.neon @@ -0,0 +1,14 @@ +parameters: + checkMissingIterableValueType: false + level: max + paths: + - src + - tests + ignoreErrors: + - + message: '#Static method GuzzleHttp\\Middleware::httpErrors\(\) invoked with 1 parameter, 0 required\.#' + path: %currentWorkingDirectory%/src/Common.php + reportUnmatched: false # error occurs only at PHP 7.4/Guzzle 6.5 + +includes: + - vendor/phpstan/phpstan-phpunit/extension.neon diff --git a/libs/encryption-api-client/phpunit.xml.dist b/libs/encryption-api-client/phpunit.xml.dist new file mode 100644 index 000000000..bb3064b30 --- /dev/null +++ b/libs/encryption-api-client/phpunit.xml.dist @@ -0,0 +1,26 @@ + + + + + tests/ + + + + + src/ + tests/ + + + diff --git a/libs/encryption-api-client/src/Common.php b/libs/encryption-api-client/src/Common.php new file mode 100644 index 000000000..e6c03e352 --- /dev/null +++ b/libs/encryption-api-client/src/Common.php @@ -0,0 +1,163 @@ + 'application/json']; + private const MAX_HTTP_ERROR_MESSAGE_LENGTH = 1024^2; + + /** + * @throws ClientException + */ + public function __construct(array $headers, array $config) + { + // Initialize handlers (start with those supplied in constructor) + $handlerStack = $config['handler'] ?? HandlerStack::create(); + + $handlerStack->push( + Middleware::retry( + self::createDefaultDecider($config['backoffMaxTries'] ?? self::DEFAULT_BACKOFF_RETRIES), + ), + ); + + if (class_exists('GuzzleHttp\BodySummarizer')) { + $handlerStack->remove('http_errors'); + $handlerStack->unshift( + Middleware::httpErrors(new BodySummarizer(self::MAX_HTTP_ERROR_MESSAGE_LENGTH)), + 'http_errors', + ); + } + + if (!isset($config['url'])) { + throw new InvalidArgumentException('url must be set'); + } + + $this->client = new Client( + [ + 'base_uri' => $config['url'], + 'headers' => array_merge( + $headers, + self::DEFAULT_HEADERS, + ), + 'handler' => $handlerStack, + ], + ); + } + + protected function apiGet(string $url): array + { + $request = new Request('GET', $url); + return $this->sendRequest($request); + } + + protected function apiDelete(string $url): void + { + $request = new Request('DELETE', $url); + $this->sendRequest($request); + } + + protected function apiPost(string $url, array $body): array + { + $request = new Request( + 'POST', + $url, + [ + 'Content-Type' => 'application/json', + ], + json_encode($body, JSON_THROW_ON_ERROR), + ); + return $this->sendRequest($request); + } + + protected function apiPatch(string $url, array $body): array + { + $request = new Request( + 'PATCH', + $url, + [ + 'Content-Type' => 'application/json', + ], + json_encode($body, JSON_THROW_ON_ERROR), + ); + return $this->sendRequest($request); + } + + protected function sendRequest(Request $request): array + { + try { + $response = $this->client->send($request); + $body = $response->getBody()->getContents(); + if ($body === '') { + return []; + } + return (array) json_decode($body, true, 512, JSON_THROW_ON_ERROR); + } catch (RequestException $e) { + $message = $e->getMessage(); + if ($e->getResponse()) { + try { + $response = (array) json_decode( + $e->getResponse()->getBody()->getContents(), + true, + 512, + JSON_THROW_ON_ERROR, + ); + if (!empty($response['message'])) { + $message = $response['message']; + } + } catch (Throwable $e) { + } + } + throw new ClientException('Encryption API error: ' . $message, $e->getCode(), $e); + } catch (GuzzleException $e) { + throw new ClientException('Encryption API error: ' . $e->getMessage(), $e->getCode(), $e); + } catch (JsonException $e) { + throw new ClientException('Unable to parse response body into JSON: ' . $e->getMessage()); + } + } + + private static function createDefaultDecider(int $maxRetries): Closure + { + return function ( + int $retries, + RequestInterface $request, + ?ResponseInterface $response = null, + ?Throwable $error = null + ) use ($maxRetries) { + if ($retries >= $maxRetries) { + return false; + } elseif ($response && $response->getStatusCode() >= 500) { + return true; + } elseif ($error && $error->getCode() >= 500) { + return true; + } elseif ($error && + (is_a($error, RequestException::class) || is_a($error, ConnectException::class)) && + in_array($error->getHandlerContext()['errno'] ?? 0, [CURLE_RECV_ERROR, CURLE_SEND_ERROR], true) + ) { + return true; + } else { + return false; + } + }; + } +} diff --git a/libs/encryption-api-client/src/Encryption.php b/libs/encryption-api-client/src/Encryption.php new file mode 100644 index 000000000..b7573c887 --- /dev/null +++ b/libs/encryption-api-client/src/Encryption.php @@ -0,0 +1,32 @@ + $sapiToken], $config); + } + + public function encryptPlainTextForConfiguration( + string $value, + string $projectId, + string $componentId, + string $configId + ): string { + $url = 'encrypt?'; + $url .= http_build_query([ + 'projectId' => $projectId, + 'componentId' => $componentId, + 'configId' => $configId, + ]); + + // because at the moment Common client is made for json requests/responses, + // we need to wrap the value in an array + $result = $this->apiPost($url, ['#value' => $value]); + return $result['#value']; + } +} diff --git a/libs/encryption-api-client/src/Exception/ClientException.php b/libs/encryption-api-client/src/Exception/ClientException.php new file mode 100644 index 000000000..b48bea289 --- /dev/null +++ b/libs/encryption-api-client/src/Exception/ClientException.php @@ -0,0 +1,11 @@ + $manageApiToken], $config); + } + + public function migrateConfiguration( + string $sourceStorageApiToken, + string $destinationStack, + string $destinationStorageApiToken, + string $componentId, + string $configId, + string $branchId, + bool $dryRun = false + ): array { + $queryParams = []; + if ($dryRun) { + $queryParams['dry-run'] = 'true'; + } + + $url = 'migrate-configuration'; + if (count($queryParams) > 0) { + $url .= '?' . http_build_query($queryParams); + } + + return $this->apiPost($url, [ + 'sourceStorageApiToken' => $sourceStorageApiToken, + 'destinationStack' => $destinationStack, + 'destinationStorageApiToken' => $destinationStorageApiToken, + 'componentId' => $componentId, + 'configId' => $configId, + 'branchId' => $branchId, + ]); + } +} diff --git a/libs/encryption-api-client/tests/EncryptionTest.php b/libs/encryption-api-client/tests/EncryptionTest.php new file mode 100644 index 000000000..fc6dbe30d --- /dev/null +++ b/libs/encryption-api-client/tests/EncryptionTest.php @@ -0,0 +1,58 @@ + 'application/json'], + '{"#value":"KBC::ConfigSecure::abcdefghijkl"}', + ), + ], + ); + + $stack = HandlerStack::create($mock); + + $requestsMade = []; + $history = Middleware::history($requestsMade); + $stack->push($history); + + $encryptionClient = new Encryption( + 'some-token', + ['handler' => $stack, 'url' => 'https://encryption.keboola.com'], + ); + $result = $encryptionClient->encryptPlainTextForConfiguration( + 'plainValue', + 'project-id', + 'keboola.component-id', + 'config-id', + ); + self::assertSame('KBC::ConfigSecure::abcdefghijkl', $result); + + self::assertCount(1, $requestsMade); + $request = $requestsMade[0]['request']; + self::assertInstanceOf(RequestInterface::class, $request); + self::assertSame('POST', $request->getMethod()); + self::assertSame( + 'https://encryption.keboola.com/encrypt?projectId=project-id' + . '&componentId=keboola.component-id&configId=config-id', + (string) $request->getUri(), + ); + self::assertSame('{"#value":"plainValue"}', $request->getBody()->getContents()); + } +} diff --git a/libs/encryption-api-client/tests/MigrationsTest.php b/libs/encryption-api-client/tests/MigrationsTest.php new file mode 100644 index 000000000..5f3255ccb --- /dev/null +++ b/libs/encryption-api-client/tests/MigrationsTest.php @@ -0,0 +1,395 @@ + 'application/json'], + '{"message":"Configuration with ID \'1234\' successfully migrated to stack \'some-stack\'."' + .',"data":{"destinationStack":"some-stack","componentId":"keboola.some-component",' + .'"configId":"1234","branchId":"102"}}', + ), + ], + ); + + // Add the history middleware to the handler stack. + $container = []; + $history = Middleware::history($container); + $stack = HandlerStack::create($mock); + $stack->push($history); + + $migrations = new Migrations( + 'some-token', + ['handler' => $stack, 'url' => 'https://encryption.keboola.com'], + ); + $result = $migrations->migrateConfiguration( + 'some-token', + 'some-stack', + 'some-token', + 'keboola.some-component', + '1234', + '102', + ); + self::assertIsArray($result); + self::assertSame( + [ + 'message' => 'Configuration with ID \'1234\' successfully migrated to stack \'some-stack\'.', + 'data' => [ + 'destinationStack' => 'some-stack', + 'componentId' => 'keboola.some-component', + 'configId' => '1234', + 'branchId' => '102', + ], + ], + $result, + ); + + /** @var Request $request */ + $request = $container[0]['request']; + self::assertSame('POST', $request->getMethod()); + self::assertSame('application/json', $request->getHeader('Accept')[0]); + self::assertSame('application/json', $request->getHeader('Content-Type')[0]); + self::assertSame('some-token', $request->getHeader('X-KBC-ManageApiToken')[0]); + self::assertSame( + 'https://encryption.keboola.com/migrate-configuration', + (string) $request->getUri(), + ); + } + + public function testDryRun(): void + { + $mock = new MockHandler([ + new Response(), + new Response(), + ]); + + // Add the history middleware to the handler stack. + $container = []; + $history = Middleware::history($container); + $stack = HandlerStack::create($mock); + $stack->push($history); + + $migrations = new Migrations( + 'some-token', + ['handler' => $stack, 'url' => 'https://encryption.keboola.com'], + ); + + $migrations->migrateConfiguration( + 'some-token', + 'some-stack', + 'some-token', + 'keboola.some-component', + '1234', + '102', + false + ); + + $migrations->migrateConfiguration( + 'some-token', + 'some-stack', + 'some-token', + 'keboola.some-component', + '1234', + '102', + true + ); + + /** @var Request $request */ + + $request = array_shift($container)['request']; + self::assertSame( + 'https://encryption.keboola.com/migrate-configuration', + (string) $request->getUri(), + ); + + $request = array_shift($container)['request']; + self::assertSame( + 'https://encryption.keboola.com/migrate-configuration?dry-run=true', + (string) $request->getUri(), + ); + } + + public function testRetryCurlExceptionFail(): void + { + $mock = new MockHandler( + [ + new Response(500, ['Content-Type' => 'application/json'], 'not used'), + new Response(500, ['Content-Type' => 'application/json'], 'not used'), + new Response(500, ['Content-Type' => 'application/json'], 'not used'), + ], + function (ResponseInterface $a) { + // abusing the mockhandler here: override the mock response and throw a Request exception + throw new RequestException( + 'Encryption API error: cURL error 56: OpenSSL SSL_read: Connection reset by peer, errno 104', + new Request('GET', 'https://example.com'), + null, + null, + [ + 'errno' => 56, + 'error' => 'OpenSSL SSL_read: Connection reset by peer, errno 104', + ], + ); + }, + ); + + // Add the history middleware to the handler stack. + $container = []; + $history = Middleware::history($container); + $stack = HandlerStack::create($mock); + $stack->push($history); + + $migrations = new Migrations( + 'some-token', + ['handler' => $stack, 'url' => 'https://encryption.keboola.com', 'backoffMaxTries' => 2], + ); + $this->expectException(ClientException::class); + $this->expectExceptionMessage('Encryption API error: Encryption API error: cURL error 56:'); + $migrations->migrateConfiguration( + 'some-token', + 'some-stack', + 'some-token', + 'keboola.some-component', + '1234', + '102', + ); + } + + public function testRetryCurlException(): void + { + $mock = new MockHandler( + [ + new Response(500, ['Content-Type' => 'application/json'], 'not used'), + new Response(500, ['Content-Type' => 'application/json'], 'not used'), + new Response( + 200, + ['Content-Type' => 'application/json'], + '{"message":"Configuration with ID \'1234\' successfully migrated to stack \'some-stack\'."' + .',"data":{"destinationStack":"some-stack","componentId":"sandboxes.data-apps","configId":"1234"' + . ',"branchId":"102"}}', + ), + ], + function (ResponseInterface $a) { + if ($a->getStatusCode() === 500) { + // abusing the mockhandler here: override the mock response and throw a Request exception + throw new RequestException( + 'Encryption API error: cURL error 56: OpenSSL SSL_read: Connection reset by peer, errno 104', + new Request('GET', 'https://example.com'), + null, + null, + [ + 'errno' => 56, + 'error' => 'OpenSSL SSL_read: Connection reset by peer, errno 104', + ], + ); + } + }, + ); + + // Add the history middleware to the handler stack. + $container = []; + $history = Middleware::history($container); + $stack = HandlerStack::create($mock); + $stack->push($history); + + $migrations = new Migrations( + 'some-token', + ['handler' => $stack, 'url' => 'https://encryption.keboola.com'], + ); + $result = $migrations->migrateConfiguration( + 'some-token', + 'some-stack', + 'some-token', + 'keboola.some-component', + '1234', + '102', + ); + self::assertIsArray($result); + self::assertSame( + [ + 'message' => 'Configuration with ID \'1234\' successfully migrated to stack \'some-stack\'.', + 'data' => [ + 'destinationStack' => 'some-stack', + 'componentId' => 'sandboxes.data-apps', + 'configId' => '1234', + 'branchId' => '102', + ], + ], + $result, + ); + } + + public function testRetryCurlExceptionWithoutContext(): void + { + $mock = new MockHandler( + [ + new Response(500, ['Content-Type' => 'application/json'], 'not used'), + ], + function (ResponseInterface $a) { + // abusing the mockhandler here: override the mock response and throw a Request exception + throw new RequestException( + 'Encryption API error: cURL error 56: OpenSSL SSL_read: Connection reset by peer, errno 104', + new Request('GET', 'https://example.com'), + null, + null, + [], + ); + }, + ); + + // Add the history middleware to the handler stack. + $container = []; + $history = Middleware::history($container); + $stack = HandlerStack::create($mock); + $stack->push($history); + + $migrations = new Migrations( + 'some-token', + ['handler' => $stack, 'url' => 'https://encryption.keboola.com'], + ); + $this->expectException(ClientException::class); + $this->expectExceptionMessage('Encryption API error: Encryption API error: cURL error 56:'); + $migrations->migrateConfiguration( + 'some-token', + 'some-stack', + 'some-token', + 'keboola.some-component', + '1234', + '102', + ); + } + + public function testInvalidJsonResponse(): void + { + $mock = new MockHandler( + [ + new Response( + 200, + ['Content-Type' => 'application/json'], + '{"message":"Configuration with ID \'1234\' successf#$@&*', + ), + ], + ); + + // Add the history middleware to the handler stack. + $container = []; + $history = Middleware::history($container); + $stack = HandlerStack::create($mock); + $stack->push($history); + + $migrations = new Migrations( + 'some-token', + ['handler' => $stack, 'url' => 'https://encryption.keboola.com'], + ); + $this->expectException(ClientException::class); + $this->expectExceptionMessage( + 'Unable to parse response body into JSON: Control character error, possibly incorrectly encoded' + ); + $migrations->migrateConfiguration( + 'some-token', + 'some-stack', + 'some-token', + 'keboola.some-component', + '1234', + '102', + ); + } + + public function testConnectionException(): void + { + $mock = new MockHandler( + [ + new ConnectException('connection error', new Request('GET', 'test')), + ], + ); + + $stack = HandlerStack::create($mock); + + $migrations = new Migrations( + 'some-token', + ['handler' => $stack, 'url' => 'https://encryption.keboola.com'], + ); + + $this->expectException(ClientException::class); + $this->expectExceptionMessage( + 'Encryption API error: connection error' + ); + $migrations->migrateConfiguration( + 'some-token', + 'some-stack', + 'some-token', + 'keboola.some-component', + '1234', + '102', + ); + } + + public function testRequestException(): void + { + $mock = new MockHandler( + [ + new RequestException( + 'simple error message', + new Request('GET', 'test') + ), + new RequestException( + 'error message to be overridden with response body message', + new Request('GET', 'test'), + new Response(400, [], '{"message":"bad bad request"}') + ), + ], + ); + + $stack = HandlerStack::create($mock); + + $migrations = new Migrations( + 'some-token', + ['handler' => $stack, 'url' => 'https://encryption.keboola.com'], + ); + + $this->expectException(ClientException::class); + $this->expectExceptionMessage( + 'Encryption API error: simple error message' + ); + $migrations->migrateConfiguration( + 'some-token', + 'some-stack', + 'some-token', + 'keboola.some-component', + '1234', + '102', + ); + + $this->expectException(ClientException::class); + $this->expectExceptionMessage( + 'Encryption API error: bad bad request' + ); + $migrations->migrateConfiguration( + 'some-token', + 'some-stack', + 'some-token', + 'keboola.some-component', + '1234', + '102', + ); + } +} diff --git a/libs/encryption-api-client/tests/bootstrap.php b/libs/encryption-api-client/tests/bootstrap.php new file mode 100644 index 000000000..06fdcb453 --- /dev/null +++ b/libs/encryption-api-client/tests/bootstrap.php @@ -0,0 +1,5 @@ +