diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index fdec017..7f8ee76 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -9,7 +9,7 @@ jobs: code_quality: strategy: matrix: - php_version: [8.1, 8.2, 8.3, 8.4] + php_version: [8.1, 8.2, 8.3, 8.4, 8.5] runs-on: ubuntu-latest steps: - name: Checkout hypernode-deploy diff --git a/README.md b/README.md index a08477d..b31a326 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,14 @@ Here's a list of Hypernode API features implemented in the client. - Updating one or multiple Hypernode settings at once. - Querying/polling the logbook for the status of a job. - Listing, creating, updating and cancelling Brancher Hypernode instances. +- Ordering a new Hypernode. +- Listing and creating backups. +- Listing, creating and deleting whitelist entries. +- Listing products and retrieving the current product of a Hypernode. +- Listing SLA addons. +- Listing available preinstall configurations. +- Listing available settings (EAV descriptions) and block attack descriptions. +- Listing and creating Insights annotations. ## Related projects diff --git a/src/HypernodeClient.php b/src/HypernodeClient.php index 0774f76..5f85b86 100644 --- a/src/HypernodeClient.php +++ b/src/HypernodeClient.php @@ -7,27 +7,45 @@ use Http\Client\Common\HttpMethodsClientInterface; use Hypernode\Api\Exception\HypernodeApiClientException; use Hypernode\Api\Exception\HypernodeApiServerException; +use Hypernode\Api\Service\Addon; use Hypernode\Api\Service\App; +use Hypernode\Api\Service\Backup; use Hypernode\Api\Service\BrancherApp; +use Hypernode\Api\Service\Configuration; +use Hypernode\Api\Service\InsightsAnnotation; use Hypernode\Api\Service\Logbook; +use Hypernode\Api\Service\Product; use Hypernode\Api\Service\Settings; +use Hypernode\Api\Service\Whitelist; use Psr\Http\Message\ResponseInterface; class HypernodeClient { public HttpMethodsClientInterface $api; + public Addon $addon; public App $app; + public Backup $backup; public BrancherApp $brancherApp; + public Configuration $configuration; + public InsightsAnnotation $insightsAnnotation; + public Product $product; public Settings $settings; public Logbook $logbook; + public Whitelist $whitelist; public function __construct(HttpMethodsClientInterface $apiClient) { $this->api = $apiClient; + $this->addon = new Addon($this); $this->app = new App($this); + $this->backup = new Backup($this); $this->brancherApp = new BrancherApp($this); + $this->configuration = new Configuration($this); + $this->insightsAnnotation = new InsightsAnnotation($this); + $this->product = new Product($this); $this->settings = new Settings($this); $this->logbook = new Logbook($this); + $this->whitelist = new Whitelist($this); } public function getJsonFromResponse(ResponseInterface $response) diff --git a/src/Service/Addon.php b/src/Service/Addon.php new file mode 100644 index 0000000..11cdd28 --- /dev/null +++ b/src/Service/Addon.php @@ -0,0 +1,29 @@ +client->api->get(self::V2_ADDON_SLA_LIST_URL); + + $this->client->maybeThrowApiExceptions($response); + + return $this->client->getJsonFromResponse($response); + } +} diff --git a/src/Service/App.php b/src/Service/App.php index c2eac05..ae91fa3 100644 --- a/src/Service/App.php +++ b/src/Service/App.php @@ -12,6 +12,9 @@ class App extends AbstractService public const V2_BRANCHER_APP_URL = "/v2/brancher/app/%s/"; public const V2_BRANCHER_DETAIL_URL = "/v2/brancher/%s/"; public const V1_APP_FLOWS_URL = "/logbook/v1/logbooks/%s/flows/"; + public const V2_APP_BLOCK_ATTACK_DESCRIPTIONS_URL = "/v2/app/block_attack_descriptions/"; + public const V2_APP_EAV_DESCRIPTIONS_URL = "/v2/app/eav_descriptions/"; + public const V2_APP_ORDER_URL = "/v2/app/order/"; /** * @param array $params @@ -44,4 +47,56 @@ public function getList(array $params = []): array return $apps; } + + /** + * Show the available blocks that can be triggered using the block attack endpoint. + * + * @return array Map of attack names to their descriptions + * @throws \Http\Client\Exception + * @throws \Hypernode\Api\Exception\HypernodeApiClientException + * @throws \Hypernode\Api\Exception\HypernodeApiServerException + */ + public function getBlockAttackDescriptions(): array + { + $response = $this->client->api->get(self::V2_APP_BLOCK_ATTACK_DESCRIPTIONS_URL); + + $this->client->maybeThrowApiExceptions($response); + + return $this->client->getJsonFromResponse($response); + } + + /** + * Show the available EAVs (entity-attribute values) that can be changed + * using the settings service. + * + * @return array Map of setting names to their descriptions + * @throws \Http\Client\Exception + * @throws \Hypernode\Api\Exception\HypernodeApiClientException + * @throws \Hypernode\Api\Exception\HypernodeApiServerException + */ + public function getEavDescriptions(): array + { + $response = $this->client->api->get(self::V2_APP_EAV_DESCRIPTIONS_URL); + + $this->client->maybeThrowApiExceptions($response); + + return $this->client->getJsonFromResponse($response); + } + + /** + * Order a new Hypernode under your account. + * + * @param array $data Order data, requires at least the keys + * `product_code`, `app_name` and `initial_app_configuration`. + * @return void + * @throws \Http\Client\Exception + * @throws \Hypernode\Api\Exception\HypernodeApiClientException + * @throws \Hypernode\Api\Exception\HypernodeApiServerException + */ + public function order(array $data): void + { + $response = $this->client->api->post(self::V2_APP_ORDER_URL, [], json_encode($data)); + + $this->client->maybeThrowApiExceptions($response); + } } diff --git a/src/Service/Backup.php b/src/Service/Backup.php new file mode 100644 index 0000000..3a80c2b --- /dev/null +++ b/src/Service/Backup.php @@ -0,0 +1,67 @@ +client->api->get($requestUrl); + + $this->client->maybeThrowApiExceptions($response); + $data = $this->client->getJsonFromResponse($response); + + foreach ($data['results'] as $item) { + $backups[] = $item; + } + + $requestUrl = $data['next']; + } + + return $backups; + } + + /** + * Create a new snapshot backup. This requires sla-standard to be enabled + * on the Hypernode. + * + * @param string $app Name of the Hypernode + * @return string Response message + * @throws HypernodeApiClientException + * @throws HypernodeApiServerException + */ + public function create(string $app): string + { + $url = sprintf(self::V2_APP_BACKUP_URL, $app); + + $response = $this->client->api->post($url); + + $this->client->maybeThrowApiExceptions($response); + + return $this->client->getJsonFromResponse($response); + } +} diff --git a/src/Service/Configuration.php b/src/Service/Configuration.php new file mode 100644 index 0000000..157f0c0 --- /dev/null +++ b/src/Service/Configuration.php @@ -0,0 +1,46 @@ +client->api->get($requestUrl); + + $this->client->maybeThrowApiExceptions($response); + $data = $this->client->getJsonFromResponse($response); + + foreach ($data['results'] as $item) { + $configurations[] = $item; + } + + $requestUrl = $data['next']; + } + + return $configurations; + } +} diff --git a/src/Service/InsightsAnnotation.php b/src/Service/InsightsAnnotation.php new file mode 100644 index 0000000..939c489 --- /dev/null +++ b/src/Service/InsightsAnnotation.php @@ -0,0 +1,75 @@ +client->api->get($requestUrl); + + $this->client->maybeThrowApiExceptions($response); + $data = $this->client->getJsonFromResponse($response); + + foreach ($data['results'] as $item) { + $annotations[] = $item; + } + + $requestUrl = $data['next']; + } + + return $annotations; + } + + /** + * Create a custom Insights annotation for a Hypernode. The annotation is + * shown in the Insights graphs at the point in time given by `x_axis` (a + * unix timestamp in seconds). Optionally restrict the annotation to + * specific graphs with `metrics`, a comma-separated list of metric names; + * when omitted, the annotation applies to all metrics. + * + * @param array $data Annotation data, requires at least the keys + * `name`, `x_axis` and `app`. + * @return array The created annotation + * @throws HypernodeApiClientException + * @throws HypernodeApiServerException + */ + public function create(array $data): array + { + $response = $this->client->api->post( + self::V2_INSIGHTS_ANNOTATION_CREATE_URL, + [], + json_encode($data) + ); + + $this->client->maybeThrowApiExceptions($response); + + return $this->client->getJsonFromResponse($response); + } +} diff --git a/src/Service/Product.php b/src/Service/Product.php new file mode 100644 index 0000000..0f67089 --- /dev/null +++ b/src/Service/Product.php @@ -0,0 +1,49 @@ +client->api->get(self::V2_PRODUCT_LIST_URL); + + $this->client->maybeThrowApiExceptions($response); + + return $this->client->getJsonFromResponse($response); + } + + /** + * Show the current product for the Hypernode. + * + * @param string $app Name of the Hypernode + * @return array The current product + * @throws HypernodeApiClientException + * @throws HypernodeApiServerException + */ + public function getCurrent(string $app): array + { + $url = sprintf(self::V2_PRODUCT_APP_CURRENT_URL, $app); + + $response = $this->client->api->get($url); + + $this->client->maybeThrowApiExceptions($response); + + return $this->client->getJsonFromResponse($response); + } +} diff --git a/src/Service/Whitelist.php b/src/Service/Whitelist.php new file mode 100644 index 0000000..37ebbb7 --- /dev/null +++ b/src/Service/Whitelist.php @@ -0,0 +1,77 @@ + 'waf'] + * @return array List of whitelist entries + * @throws HypernodeApiClientException + * @throws HypernodeApiServerException + */ + public function getList(string $app, array $params = []): array + { + $url = sprintf(self::V2_WHITELIST_URL, $app); + if ($params) { + $url .= '?' . http_build_query($params); + } + + $response = $this->client->api->get($url); + + $this->client->maybeThrowApiExceptions($response); + + return $this->client->getJsonFromResponse($response); + } + + /** + * Add a whitelist entry for the Hypernode. + * + * @param string $app Name of the Hypernode + * @param array $data Whitelist entry data, requires at least the key `ip`. + * Optional keys are `type` (database, ftp, phpmyadmin, ssh or waf, + * defaults to database) and `description`. + * @return array The created whitelist entry + * @throws HypernodeApiClientException + * @throws HypernodeApiServerException + */ + public function create(string $app, array $data): array + { + $url = sprintf(self::V2_WHITELIST_URL, $app); + + $response = $this->client->api->post($url, [], json_encode($data)); + + $this->client->maybeThrowApiExceptions($response); + + return $this->client->getJsonFromResponse($response); + } + + /** + * Remove a whitelist entry for the Hypernode. + * + * @param string $app Name of the Hypernode + * @param array $data Data identifying the entry to remove, e.g. + * ['ip' => '1.2.3.4', 'type' => 'waf'] + * @return void + * @throws HypernodeApiClientException + * @throws HypernodeApiServerException + */ + public function delete(string $app, array $data): void + { + $url = sprintf(self::V2_WHITELIST_URL, $app); + + $response = $this->client->api->delete($url, [], json_encode($data)); + + $this->client->maybeThrowApiExceptions($response); + } +} diff --git a/tests/unit/Service/AddonTest.php b/tests/unit/Service/AddonTest.php new file mode 100644 index 0000000..ec2c2a7 --- /dev/null +++ b/tests/unit/Service/AddonTest.php @@ -0,0 +1,62 @@ + 123, + 'code' => 'sla-standard', + 'name' => 'SLA Standard', + 'price' => 2500, + 'currency' => 'EUR', + 'billing_period' => 1, + 'billing_period_unit' => 'month', + ], + ]; + $this->responses->append( + new Response(200, [], json_encode($slas)), + ); + + $result = $this->client->addon->getSlas(); + + $request = $this->responses->getLastRequest(); + $this->assertEquals('GET', $request->getMethod()); + $this->assertEquals('/v2/addon/slas/', $request->getUri()); + $this->assertEquals($slas, $result); + } + + public function testGetSlasRaisesClientExceptions() + { + $badRequestResponse = new Response(400, [], json_encode([ + 'non_field_errors' => ['Your request was invalid.'] + ])); + $this->responses->append($badRequestResponse); + + $this->expectExceptionObject(new HypernodeApiClientException($badRequestResponse)); + + $this->client->addon->getSlas(); + } + + public function testGetSlasRaisesServerExceptions() + { + $badRequestResponse = new Response(500, [], json_encode([ + 'non_field_errors' => ['Something went wrong processing your request.'] + ])); + $this->responses->append($badRequestResponse); + + $this->expectExceptionObject(new HypernodeApiServerException($badRequestResponse)); + + $this->client->addon->getSlas(); + } +} diff --git a/tests/unit/Service/AppTest.php b/tests/unit/Service/AppTest.php index 15edf05..f75edf6 100644 --- a/tests/unit/Service/AppTest.php +++ b/tests/unit/Service/AppTest.php @@ -5,6 +5,8 @@ namespace Hypernode\Api\Service; use GuzzleHttp\Psr7\Response; +use Hypernode\Api\Exception\HypernodeApiClientException; +use Hypernode\Api\Exception\HypernodeApiServerException; use Hypernode\Api\HypernodeClientTestCase; class AppTest extends HypernodeClientTestCase @@ -112,4 +114,132 @@ public function testGetListPaginates() $this->assertEquals('GET', $request->getMethod()); $this->assertEquals('https://api.hypernode.com/v2/app/?limit=1&offset=1', $request->getUri()); } + + public function testGetBlockAttackDescriptions() + { + $descriptions = [ + 'BlockSqliBruteForce' => 'Attempts to deploy NGINX rules to block suspected (blind) SQL injection attack', + 'BlockChinaBruteForce' => 'Attempts to deploy NGINX rules to block brute force attacks from China', + ]; + $this->responses->append( + new Response(200, [], json_encode($descriptions)), + ); + + $result = $this->client->app->getBlockAttackDescriptions(); + + $request = $this->responses->getLastRequest(); + $this->assertEquals('GET', $request->getMethod()); + $this->assertEquals('/v2/app/block_attack_descriptions/', $request->getUri()); + $this->assertEquals($descriptions, $result); + } + + public function testGetBlockAttackDescriptionsRaisesClientExceptions() + { + $badRequestResponse = new Response(400, [], json_encode([ + 'non_field_errors' => ['Your request was invalid.'] + ])); + $this->responses->append($badRequestResponse); + + $this->expectExceptionObject(new HypernodeApiClientException($badRequestResponse)); + + $this->client->app->getBlockAttackDescriptions(); + } + + public function testGetBlockAttackDescriptionsRaisesServerExceptions() + { + $badRequestResponse = new Response(500, [], json_encode([ + 'non_field_errors' => ['Something went wrong processing your request.'] + ])); + $this->responses->append($badRequestResponse); + + $this->expectExceptionObject(new HypernodeApiServerException($badRequestResponse)); + + $this->client->app->getBlockAttackDescriptions(); + } + + public function testGetEavDescriptions() + { + $descriptions = [ + 'php_version' => 'The PHP version of the Hypernode', + 'nodejs_version' => 'The Node.js version of the Hypernode', + ]; + $this->responses->append( + new Response(200, [], json_encode($descriptions)), + ); + + $result = $this->client->app->getEavDescriptions(); + + $request = $this->responses->getLastRequest(); + $this->assertEquals('GET', $request->getMethod()); + $this->assertEquals('/v2/app/eav_descriptions/', $request->getUri()); + $this->assertEquals($descriptions, $result); + } + + public function testGetEavDescriptionsRaisesClientExceptions() + { + $badRequestResponse = new Response(400, [], json_encode([ + 'non_field_errors' => ['Your request was invalid.'] + ])); + $this->responses->append($badRequestResponse); + + $this->expectExceptionObject(new HypernodeApiClientException($badRequestResponse)); + + $this->client->app->getEavDescriptions(); + } + + public function testGetEavDescriptionsRaisesServerExceptions() + { + $badRequestResponse = new Response(500, [], json_encode([ + 'non_field_errors' => ['Something went wrong processing your request.'] + ])); + $this->responses->append($badRequestResponse); + + $this->expectExceptionObject(new HypernodeApiServerException($badRequestResponse)); + + $this->client->app->getEavDescriptions(); + } + + public function testOrder() + { + $this->responses->append( + new Response(201, [], null), + ); + + $orderData = [ + 'product_code' => 'FALCON_M_202203', + 'app_name' => 'johndoe', + 'initial_app_configuration' => 'magento_2', + ]; + $this->client->app->order($orderData); + + $request = $this->responses->getLastRequest(); + $this->assertEquals('POST', $request->getMethod()); + $this->assertEquals('/v2/app/order/', $request->getUri()); + $this->assertJson((string)$request->getBody()); + $this->assertEquals($orderData, json_decode((string)$request->getBody(), true)); + } + + public function testOrderRaisesClientExceptions() + { + $badRequestResponse = new Response(400, [], json_encode([ + 'non_field_errors' => ['Your request was invalid.'] + ])); + $this->responses->append($badRequestResponse); + + $this->expectExceptionObject(new HypernodeApiClientException($badRequestResponse)); + + $this->client->app->order(['product_code' => 'FALCON_M_202203']); + } + + public function testOrderRaisesServerExceptions() + { + $badRequestResponse = new Response(500, [], json_encode([ + 'non_field_errors' => ['Something went wrong processing your request.'] + ])); + $this->responses->append($badRequestResponse); + + $this->expectExceptionObject(new HypernodeApiServerException($badRequestResponse)); + + $this->client->app->order(['product_code' => 'FALCON_M_202203']); + } } diff --git a/tests/unit/Service/BackupTest.php b/tests/unit/Service/BackupTest.php new file mode 100644 index 0000000..f9ca735 --- /dev/null +++ b/tests/unit/Service/BackupTest.php @@ -0,0 +1,172 @@ + 'backup-123', + 'type' => 'periodic', + 'backup_created_at' => '2026-07-14T01:00:00Z', + 'expired_at' => '2026-07-28T01:00:00Z', + ], + [ + 'backup_id' => 'backup-456', + 'type' => 'instant', + 'backup_created_at' => '2026-07-15T09:30:00Z', + 'expired_at' => '2026-07-29T09:30:00Z', + ], + ]; + $this->responses->append( + new Response(200, [], json_encode([ + 'count' => 2, + 'next' => null, + 'previous' => null, + 'results' => $backups, + ])), + ); + + $result = $this->client->backup->getList('johndoe'); + + $request = $this->responses->getLastRequest(); + $this->assertEquals('GET', $request->getMethod()); + $this->assertEquals('/v2/app/johndoe/backup/', $request->getUri()); + $this->assertEquals($backups, $result); + } + + public function testGetListAcceptsParams() + { + $this->responses->append( + new Response(200, [], json_encode([ + 'count' => 0, + 'next' => null, + 'previous' => null, + 'results' => [], + ])), + ); + + $result = $this->client->backup->getList('johndoe', ['limit' => 10, 'offset' => 20]); + + $request = $this->responses->getLastRequest(); + $this->assertEquals('GET', $request->getMethod()); + $this->assertEquals('/v2/app/johndoe/backup/?limit=10&offset=20', $request->getUri()); + $this->assertEquals([], $result); + } + + public function testGetListPaginates() + { + $this->responses->append( + new Response(200, [], json_encode([ + 'count' => 2, + 'next' => 'https://api.hypernode.com/v2/app/johndoe/backup/?limit=1&offset=1', + 'previous' => null, + 'results' => [ + [ + 'backup_id' => 'backup-123', + 'type' => 'periodic', + 'backup_created_at' => '2026-07-14T01:00:00Z', + 'expired_at' => '2026-07-28T01:00:00Z', + ], + ], + ])), + new Response(200, [], json_encode([ + 'count' => 2, + 'next' => null, + 'previous' => null, + 'results' => [ + [ + 'backup_id' => 'backup-456', + 'type' => 'instant', + 'backup_created_at' => '2026-07-15T09:30:00Z', + 'expired_at' => '2026-07-29T09:30:00Z', + ], + ], + ])), + ); + + $result = $this->client->backup->getList('johndoe'); + + $this->assertCount(2, $result); + $this->assertEquals('backup-123', $result[0]['backup_id']); + $this->assertEquals('backup-456', $result[1]['backup_id']); + + $request = $this->responses->getLastRequest(); + $this->assertEquals('GET', $request->getMethod()); + $this->assertEquals( + 'https://api.hypernode.com/v2/app/johndoe/backup/?limit=1&offset=1', + $request->getUri() + ); + } + + public function testGetListRaisesClientExceptions() + { + $badRequestResponse = new Response(400, [], json_encode([ + 'non_field_errors' => ['Your request was invalid.'] + ])); + $this->responses->append($badRequestResponse); + + $this->expectExceptionObject(new HypernodeApiClientException($badRequestResponse)); + + $this->client->backup->getList('johndoe'); + } + + public function testGetListRaisesServerExceptions() + { + $badRequestResponse = new Response(500, [], json_encode([ + 'non_field_errors' => ['Something went wrong processing your request.'] + ])); + $this->responses->append($badRequestResponse); + + $this->expectExceptionObject(new HypernodeApiServerException($badRequestResponse)); + + $this->client->backup->getList('johndoe'); + } + + public function testCreate() + { + $this->responses->append( + new Response(200, [], json_encode('Backup creation initiated')), + ); + + $result = $this->client->backup->create('johndoe'); + + $request = $this->responses->getLastRequest(); + $this->assertEquals('POST', $request->getMethod()); + $this->assertEquals('/v2/app/johndoe/backup/', $request->getUri()); + $this->assertEquals('Backup creation initiated', $result); + } + + public function testCreateRaisesClientExceptions() + { + $badRequestResponse = new Response(400, [], json_encode( + 'Creating backups requires sla-standard to be enabled.' + )); + $this->responses->append($badRequestResponse); + + $this->expectExceptionObject(new HypernodeApiClientException($badRequestResponse)); + + $this->client->backup->create('johndoe'); + } + + public function testCreateRaisesServerExceptions() + { + $badRequestResponse = new Response(500, [], json_encode([ + 'non_field_errors' => ['Something went wrong processing your request.'] + ])); + $this->responses->append($badRequestResponse); + + $this->expectExceptionObject(new HypernodeApiServerException($badRequestResponse)); + + $this->client->backup->create('johndoe'); + } +} diff --git a/tests/unit/Service/ConfigurationTest.php b/tests/unit/Service/ConfigurationTest.php new file mode 100644 index 0000000..e8f5b5d --- /dev/null +++ b/tests/unit/Service/ConfigurationTest.php @@ -0,0 +1,132 @@ + 'Magento 2', + 'configuration_id' => 'magento_2', + 'php_version' => '8.1', + 'mysql_version' => '8.0', + 'varnish_enabled' => false, + ], + [ + 'name' => 'Shopware 6', + 'configuration_id' => 'shopware_6', + 'php_version' => '8.2', + 'mysql_version' => '8.0', + 'varnish_enabled' => true, + ], + ]; + $this->responses->append( + new Response(200, [], json_encode([ + 'count' => 2, + 'next' => null, + 'previous' => null, + 'results' => $configurations, + ])), + ); + + $result = $this->client->configuration->getList(); + + $request = $this->responses->getLastRequest(); + $this->assertEquals('GET', $request->getMethod()); + $this->assertEquals('/v2/configuration/', $request->getUri()); + $this->assertEquals($configurations, $result); + } + + public function testGetListAcceptsParams() + { + $this->responses->append( + new Response(200, [], json_encode([ + 'count' => 0, + 'next' => null, + 'previous' => null, + 'results' => [], + ])), + ); + + $result = $this->client->configuration->getList(['allow_preinstall' => 'true']); + + $request = $this->responses->getLastRequest(); + $this->assertEquals('GET', $request->getMethod()); + $this->assertEquals('/v2/configuration/?allow_preinstall=true', $request->getUri()); + $this->assertEquals([], $result); + } + + public function testGetListPaginates() + { + $this->responses->append( + new Response(200, [], json_encode([ + 'count' => 2, + 'next' => 'https://api.hypernode.com/v2/configuration/?limit=1&offset=1', + 'previous' => null, + 'results' => [ + [ + 'name' => 'Magento 2', + 'configuration_id' => 'magento_2', + ], + ], + ])), + new Response(200, [], json_encode([ + 'count' => 2, + 'next' => null, + 'previous' => null, + 'results' => [ + [ + 'name' => 'Shopware 6', + 'configuration_id' => 'shopware_6', + ], + ], + ])), + ); + + $result = $this->client->configuration->getList(); + + $this->assertCount(2, $result); + $this->assertEquals('magento_2', $result[0]['configuration_id']); + $this->assertEquals('shopware_6', $result[1]['configuration_id']); + + $request = $this->responses->getLastRequest(); + $this->assertEquals('GET', $request->getMethod()); + $this->assertEquals( + 'https://api.hypernode.com/v2/configuration/?limit=1&offset=1', + $request->getUri() + ); + } + + public function testGetListRaisesClientExceptions() + { + $badRequestResponse = new Response(400, [], json_encode([ + 'non_field_errors' => ['Your request was invalid.'] + ])); + $this->responses->append($badRequestResponse); + + $this->expectExceptionObject(new HypernodeApiClientException($badRequestResponse)); + + $this->client->configuration->getList(); + } + + public function testGetListRaisesServerExceptions() + { + $badRequestResponse = new Response(500, [], json_encode([ + 'non_field_errors' => ['Something went wrong processing your request.'] + ])); + $this->responses->append($badRequestResponse); + + $this->expectExceptionObject(new HypernodeApiServerException($badRequestResponse)); + + $this->client->configuration->getList(); + } +} diff --git a/tests/unit/Service/InsightsAnnotationTest.php b/tests/unit/Service/InsightsAnnotationTest.php new file mode 100644 index 0000000..13471d0 --- /dev/null +++ b/tests/unit/Service/InsightsAnnotationTest.php @@ -0,0 +1,178 @@ + 'Deployed release 1.2.3', + 'x_axis' => 1756384957, + 'app_name' => 'johndoe', + 'metrics_list' => ['memory_usage'], + 'metadata' => [], + ], + ]; + $this->responses->append( + new Response(200, [], json_encode([ + 'count' => 1, + 'next' => null, + 'previous' => null, + 'results' => $annotations, + ])), + ); + + $result = $this->client->insightsAnnotation->getList(); + + $request = $this->responses->getLastRequest(); + $this->assertEquals('GET', $request->getMethod()); + $this->assertEquals('/v2/insights-annotation/', $request->getUri()); + $this->assertEquals($annotations, $result); + } + + public function testGetListAcceptsParams() + { + $this->responses->append( + new Response(200, [], json_encode([ + 'count' => 0, + 'next' => null, + 'previous' => null, + 'results' => [], + ])), + ); + + $result = $this->client->insightsAnnotation->getList(['limit' => 10, 'offset' => 20]); + + $request = $this->responses->getLastRequest(); + $this->assertEquals('GET', $request->getMethod()); + $this->assertEquals('/v2/insights-annotation/?limit=10&offset=20', $request->getUri()); + $this->assertEquals([], $result); + } + + public function testGetListPaginates() + { + $this->responses->append( + new Response(200, [], json_encode([ + 'count' => 2, + 'next' => 'https://api.hypernode.com/v2/insights-annotation/?limit=1&offset=1', + 'previous' => null, + 'results' => [ + [ + 'name' => 'Deployed release 1.2.3', + 'x_axis' => 1756384957, + 'app_name' => 'johndoe', + ], + ], + ])), + new Response(200, [], json_encode([ + 'count' => 2, + 'next' => null, + 'previous' => null, + 'results' => [ + [ + 'name' => 'Deployed release 1.2.4', + 'x_axis' => 1756395957, + 'app_name' => 'johndoe', + ], + ], + ])), + ); + + $result = $this->client->insightsAnnotation->getList(); + + $this->assertCount(2, $result); + $this->assertEquals('Deployed release 1.2.3', $result[0]['name']); + $this->assertEquals('Deployed release 1.2.4', $result[1]['name']); + + $request = $this->responses->getLastRequest(); + $this->assertEquals('GET', $request->getMethod()); + $this->assertEquals( + 'https://api.hypernode.com/v2/insights-annotation/?limit=1&offset=1', + $request->getUri() + ); + } + + public function testGetListRaisesClientExceptions() + { + $badRequestResponse = new Response(400, [], json_encode([ + 'non_field_errors' => ['Your request was invalid.'] + ])); + $this->responses->append($badRequestResponse); + + $this->expectExceptionObject(new HypernodeApiClientException($badRequestResponse)); + + $this->client->insightsAnnotation->getList(); + } + + public function testGetListRaisesServerExceptions() + { + $badRequestResponse = new Response(500, [], json_encode([ + 'non_field_errors' => ['Something went wrong processing your request.'] + ])); + $this->responses->append($badRequestResponse); + + $this->expectExceptionObject(new HypernodeApiServerException($badRequestResponse)); + + $this->client->insightsAnnotation->getList(); + } + + public function testCreate() + { + $annotationData = [ + 'name' => 'Deployed release 1.2.3', + 'x_axis' => 1756384957, + 'app' => 'johndoe', + 'metrics' => 'memory_usage', + ]; + $createdAnnotation = array_merge($annotationData, [ + 'id' => 123, + 'created' => '2026-07-15T12:00:00Z', + 'modified' => '2026-07-15T12:00:00Z', + ]); + $this->responses->append( + new Response(201, [], json_encode($createdAnnotation)), + ); + + $result = $this->client->insightsAnnotation->create($annotationData); + + $request = $this->responses->getLastRequest(); + $this->assertEquals('POST', $request->getMethod()); + $this->assertEquals('/v2/insights-annotation/create/', $request->getUri()); + $this->assertJson((string)$request->getBody()); + $this->assertEquals($annotationData, json_decode((string)$request->getBody(), true)); + $this->assertEquals($createdAnnotation, $result); + } + + public function testCreateRaisesClientExceptions() + { + $badRequestResponse = new Response(400, [], json_encode([ + 'non_field_errors' => ['Your request was invalid.'] + ])); + $this->responses->append($badRequestResponse); + + $this->expectExceptionObject(new HypernodeApiClientException($badRequestResponse)); + + $this->client->insightsAnnotation->create(['name' => 'Deployed release 1.2.3']); + } + + public function testCreateRaisesServerExceptions() + { + $badRequestResponse = new Response(500, [], json_encode([ + 'non_field_errors' => ['Something went wrong processing your request.'] + ])); + $this->responses->append($badRequestResponse); + + $this->expectExceptionObject(new HypernodeApiServerException($badRequestResponse)); + + $this->client->insightsAnnotation->create(['name' => 'Deployed release 1.2.3']); + } +} diff --git a/tests/unit/Service/ProductTest.php b/tests/unit/Service/ProductTest.php new file mode 100644 index 0000000..3129dd4 --- /dev/null +++ b/tests/unit/Service/ProductTest.php @@ -0,0 +1,125 @@ + 'FALCON_XS_202203', + 'name' => 'Falcon XS', + 'backups_enabled' => true, + 'is_development' => false, + 'varnish_supported' => false, + 'supports_sla' => true, + 'price' => 1234, + 'currency' => 'EUR', + ], + [ + 'code' => 'FALCON_M_202203', + 'name' => 'Falcon M', + 'backups_enabled' => true, + 'is_development' => false, + 'varnish_supported' => true, + 'supports_sla' => true, + 'price' => 2345, + 'currency' => 'EUR', + ], + ]; + $this->responses->append( + new Response(200, [], json_encode($products)), + ); + + $result = $this->client->product->getList(); + + $request = $this->responses->getLastRequest(); + $this->assertEquals('GET', $request->getMethod()); + $this->assertEquals('/v2/product/', $request->getUri()); + $this->assertEquals($products, $result); + } + + public function testGetListRaisesClientExceptions() + { + $badRequestResponse = new Response(400, [], json_encode([ + 'non_field_errors' => ['Your request was invalid.'] + ])); + $this->responses->append($badRequestResponse); + + $this->expectExceptionObject(new HypernodeApiClientException($badRequestResponse)); + + $this->client->product->getList(); + } + + public function testGetListRaisesServerExceptions() + { + $badRequestResponse = new Response(500, [], json_encode([ + 'non_field_errors' => ['Something went wrong processing your request.'] + ])); + $this->responses->append($badRequestResponse); + + $this->expectExceptionObject(new HypernodeApiServerException($badRequestResponse)); + + $this->client->product->getList(); + } + + public function testGetCurrent() + { + $product = [ + 'code' => 'FALCON_S_202203', + 'name' => 'Falcon S', + 'backups_enabled' => true, + 'is_development' => false, + 'varnish_supported' => true, + 'supports_sla' => true, + 'price' => 1234, + 'currency' => 'EUR', + 'flavor' => [ + 'name' => '2CPU/8GB/60GB (Falcon S 202202)', + 'redis_size' => '1024', + ], + ]; + $this->responses->append( + new Response(200, [], json_encode($product)), + ); + + $result = $this->client->product->getCurrent('johndoe'); + + $request = $this->responses->getLastRequest(); + $this->assertEquals('GET', $request->getMethod()); + $this->assertEquals('/v2/product/app/johndoe/current/', $request->getUri()); + $this->assertEquals($product, $result); + } + + public function testGetCurrentRaisesClientExceptions() + { + $badRequestResponse = new Response(400, [], json_encode([ + 'non_field_errors' => ['Your request was invalid.'] + ])); + $this->responses->append($badRequestResponse); + + $this->expectExceptionObject(new HypernodeApiClientException($badRequestResponse)); + + $this->client->product->getCurrent('johndoe'); + } + + public function testGetCurrentRaisesServerExceptions() + { + $badRequestResponse = new Response(500, [], json_encode([ + 'non_field_errors' => ['Something went wrong processing your request.'] + ])); + $this->responses->append($badRequestResponse); + + $this->expectExceptionObject(new HypernodeApiServerException($badRequestResponse)); + + $this->client->product->getCurrent('johndoe'); + } +} diff --git a/tests/unit/Service/WhitelistTest.php b/tests/unit/Service/WhitelistTest.php new file mode 100644 index 0000000..9f5efe9 --- /dev/null +++ b/tests/unit/Service/WhitelistTest.php @@ -0,0 +1,165 @@ + 1234, + 'created' => '2026-07-14T14:49:06Z', + 'type' => 'waf', + 'description' => 'Office IP', + 'ip' => '1.2.3.4', + 'app' => 'johndoe', + ], + ]; + $this->responses->append( + new Response(200, [], json_encode($entries)), + ); + + $result = $this->client->whitelist->getList('johndoe'); + + $request = $this->responses->getLastRequest(); + $this->assertEquals('GET', $request->getMethod()); + $this->assertEquals('/v2/whitelist/johndoe/', $request->getUri()); + $this->assertEquals($entries, $result); + } + + public function testGetListAcceptsParams() + { + $this->responses->append( + new Response(200, [], json_encode([])), + ); + + $result = $this->client->whitelist->getList('johndoe', ['type' => 'waf']); + + $request = $this->responses->getLastRequest(); + $this->assertEquals('GET', $request->getMethod()); + $this->assertEquals('/v2/whitelist/johndoe/?type=waf', $request->getUri()); + $this->assertEquals([], $result); + } + + public function testGetListRaisesClientExceptions() + { + $badRequestResponse = new Response(400, [], json_encode([ + 'non_field_errors' => ['Your request was invalid.'] + ])); + $this->responses->append($badRequestResponse); + + $this->expectExceptionObject(new HypernodeApiClientException($badRequestResponse)); + + $this->client->whitelist->getList('johndoe'); + } + + public function testGetListRaisesServerExceptions() + { + $badRequestResponse = new Response(500, [], json_encode([ + 'non_field_errors' => ['Something went wrong processing your request.'] + ])); + $this->responses->append($badRequestResponse); + + $this->expectExceptionObject(new HypernodeApiServerException($badRequestResponse)); + + $this->client->whitelist->getList('johndoe'); + } + + public function testCreate() + { + $entryData = [ + 'ip' => '1.2.3.4', + 'type' => 'waf', + 'description' => 'Office IP', + ]; + $createdEntry = array_merge($entryData, [ + 'id' => 1234, + 'created' => '2026-07-15T12:00:00Z', + 'app' => 'johndoe', + ]); + $this->responses->append( + new Response(200, [], json_encode($createdEntry)), + ); + + $result = $this->client->whitelist->create('johndoe', $entryData); + + $request = $this->responses->getLastRequest(); + $this->assertEquals('POST', $request->getMethod()); + $this->assertEquals('/v2/whitelist/johndoe/', $request->getUri()); + $this->assertJson((string)$request->getBody()); + $this->assertEquals($entryData, json_decode((string)$request->getBody(), true)); + $this->assertEquals($createdEntry, $result); + } + + public function testCreateRaisesClientExceptions() + { + $badRequestResponse = new Response(400, [], json_encode([ + 'non_field_errors' => ['Your request was invalid.'] + ])); + $this->responses->append($badRequestResponse); + + $this->expectExceptionObject(new HypernodeApiClientException($badRequestResponse)); + + $this->client->whitelist->create('johndoe', ['ip' => '1.2.3.4']); + } + + public function testCreateRaisesServerExceptions() + { + $badRequestResponse = new Response(500, [], json_encode([ + 'non_field_errors' => ['Something went wrong processing your request.'] + ])); + $this->responses->append($badRequestResponse); + + $this->expectExceptionObject(new HypernodeApiServerException($badRequestResponse)); + + $this->client->whitelist->create('johndoe', ['ip' => '1.2.3.4']); + } + + public function testDelete() + { + $this->responses->append( + new Response(204, [], null), + ); + + $entryData = ['ip' => '1.2.3.4', 'type' => 'waf']; + $this->client->whitelist->delete('johndoe', $entryData); + + $request = $this->responses->getLastRequest(); + $this->assertEquals('DELETE', $request->getMethod()); + $this->assertEquals('/v2/whitelist/johndoe/', $request->getUri()); + $this->assertJson((string)$request->getBody()); + $this->assertEquals($entryData, json_decode((string)$request->getBody(), true)); + } + + public function testDeleteRaisesClientExceptions() + { + $badRequestResponse = new Response(400, [], json_encode([ + 'non_field_errors' => ['Your request was invalid.'] + ])); + $this->responses->append($badRequestResponse); + + $this->expectExceptionObject(new HypernodeApiClientException($badRequestResponse)); + + $this->client->whitelist->delete('johndoe', ['ip' => '1.2.3.4']); + } + + public function testDeleteRaisesServerExceptions() + { + $badRequestResponse = new Response(500, [], json_encode([ + 'non_field_errors' => ['Something went wrong processing your request.'] + ])); + $this->responses->append($badRequestResponse); + + $this->expectExceptionObject(new HypernodeApiServerException($badRequestResponse)); + + $this->client->whitelist->delete('johndoe', ['ip' => '1.2.3.4']); + } +}