From 4eee6d705f694b08077f75299ba0ba138973b562 Mon Sep 17 00:00:00 2001 From: Kalindi Adhiya Date: Mon, 5 Jan 2026 17:29:09 +0530 Subject: [PATCH 1/4] Fix Content-Type header requirement for API POST/PUT/PATCH requests - Add Content-Type: application/json header for POST/PUT/PATCH requests - Send empty JSON body for requests without data to satisfy API requirements - Resolves Content-Type header must exist errors in API commands --- src/Command/Api/ApiBaseCommand.php | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Command/Api/ApiBaseCommand.php b/src/Command/Api/ApiBaseCommand.php index 2dc5d1256..8b8976962 100644 --- a/src/Command/Api/ApiBaseCommand.php +++ b/src/Command/Api/ApiBaseCommand.php @@ -111,9 +111,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int // Acquia PHP SDK cannot set the Accept header itself because it would break // API calls returning octet streams (e.g., db backups). It's safe to use // here because the API command should always return JSON. - $acquiaCloudClient->addOption('headers', [ + $headers = [ 'Accept' => 'application/hal+json, version=2', - ]); + ]; + + // Add Content-Type header for POST/PUT/PATCH requests. + if (in_array(strtoupper($this->method), ['POST', 'PUT', 'PATCH'])) { + $headers['Content-Type'] = 'application/json'; + } + + $acquiaCloudClient->addOption('headers', $headers); try { if ($this->output->isVeryVerbose()) { @@ -398,14 +405,21 @@ protected function addQueryParamsToClient(InputInterface $input, Client $acquiaC private function addPostParamsToClient(InputInterface $input, Client $acquiaCloudClient): void { + $hasData = false; if ($this->postParams) { foreach ($this->postParams as $paramName => $paramSpec) { $paramValue = $this->getParamFromInput($input, $paramName); if (!is_null($paramValue)) { $this->addPostParamToClient($paramName, $paramSpec, $paramValue, $acquiaCloudClient); + $hasData = true; } } } + + // For POST/PUT/PATCH requests without data, send an empty JSON body to satisfy Content-Type requirement. + if (!$hasData && in_array(strtoupper($this->method ?? ''), ['POST', 'PUT', 'PATCH'])) { + $acquiaCloudClient->addOption('json', []); + } } /** From 6f1333a4804f7e9d5f1d24a10fb95742434567be Mon Sep 17 00:00:00 2001 From: Kalindi Adhiya Date: Mon, 5 Jan 2026 17:44:07 +0530 Subject: [PATCH 2/4] fix header issue --- src/Command/Api/ApiBaseCommand.php | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/Command/Api/ApiBaseCommand.php b/src/Command/Api/ApiBaseCommand.php index 8b8976962..59a12fa8c 100644 --- a/src/Command/Api/ApiBaseCommand.php +++ b/src/Command/Api/ApiBaseCommand.php @@ -111,16 +111,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int // Acquia PHP SDK cannot set the Accept header itself because it would break // API calls returning octet streams (e.g., db backups). It's safe to use // here because the API command should always return JSON. - $headers = [ + $acquiaCloudClient->addOption('headers', [ 'Accept' => 'application/hal+json, version=2', - ]; - - // Add Content-Type header for POST/PUT/PATCH requests. - if (in_array(strtoupper($this->method), ['POST', 'PUT', 'PATCH'])) { - $headers['Content-Type'] = 'application/json'; - } - - $acquiaCloudClient->addOption('headers', $headers); + ]); try { if ($this->output->isVeryVerbose()) { @@ -416,9 +409,14 @@ private function addPostParamsToClient(InputInterface $input, Client $acquiaClou } } - // For POST/PUT/PATCH requests without data, send an empty JSON body to satisfy Content-Type requirement. + // For POST/PUT/PATCH requests without data, send an empty JSON body with Content-Type header + // to satisfy API requirements. if (!$hasData && in_array(strtoupper($this->method ?? ''), ['POST', 'PUT', 'PATCH'])) { $acquiaCloudClient->addOption('json', []); + $acquiaCloudClient->addOption('headers', [ + 'Accept' => 'application/hal+json, version=2', + 'Content-Type' => 'application/json', + ]); } } From f64e734536bad762378f169b7f6e394fd34130c1 Mon Sep 17 00:00:00 2001 From: Kalindi Adhiya Date: Mon, 5 Jan 2026 18:05:36 +0530 Subject: [PATCH 3/4] mutation fix --- src/Command/Api/ApiBaseCommand.php | 4 +- .../src/Commands/Api/ApiCommandTest.php | 141 ++++++++++++++++++ 2 files changed, 143 insertions(+), 2 deletions(-) diff --git a/src/Command/Api/ApiBaseCommand.php b/src/Command/Api/ApiBaseCommand.php index 59a12fa8c..e1ee644af 100644 --- a/src/Command/Api/ApiBaseCommand.php +++ b/src/Command/Api/ApiBaseCommand.php @@ -411,10 +411,10 @@ private function addPostParamsToClient(InputInterface $input, Client $acquiaClou // For POST/PUT/PATCH requests without data, send an empty JSON body with Content-Type header // to satisfy API requirements. - if (!$hasData && in_array(strtoupper($this->method ?? ''), ['POST', 'PUT', 'PATCH'])) { + if (!$hasData && in_array(strtoupper($this->method), ['POST', 'PUT', 'PATCH'])) { $acquiaCloudClient->addOption('json', []); $acquiaCloudClient->addOption('headers', [ - 'Accept' => 'application/hal+json, version=2', + 'Content-Type' => 'application/json', ]); } diff --git a/tests/phpunit/src/Commands/Api/ApiCommandTest.php b/tests/phpunit/src/Commands/Api/ApiCommandTest.php index b4b6b92a3..791667674 100644 --- a/tests/phpunit/src/Commands/Api/ApiCommandTest.php +++ b/tests/phpunit/src/Commands/Api/ApiCommandTest.php @@ -396,6 +396,91 @@ public function testConvertInvalidEnvironmentAliasToUuidArgument(): void $this->executeCommand(['environmentId' => $alias], []); } + public function testApiCommandExecutionForHttpPostWithoutData(): void + { + // Test POST request without body data (only path params) - should add empty JSON and Content-Type. + $this->clientProphecy->addOption('headers', ['Accept' => 'application/hal+json, version=2']) + ->shouldBeCalled(); + + // Expect empty JSON body and Content-Type header for POST without data. + $this->clientProphecy->addOption('json', []) + ->shouldBeCalled(); + $this->clientProphecy->addOption('headers', ['Content-Type' => 'application/json']) + ->shouldBeCalled(); + + $mockResponseBody = self::getMockResponseFromSpec('/account/applications/{applicationUuid}/actions/mark-recent', 'post', '200'); + $this->clientProphecy->request('post', '/account/applications/a47ac10b-58cc-4372-a567-0e02b2c3d470/actions/mark-recent') + ->willReturn($mockResponseBody) + ->shouldBeCalled(); + + $this->command = $this->getApiCommandByName('api:accounts:application-mark-recent'); + $this->executeCommand(['applicationUuid' => 'a47ac10b-58cc-4372-a567-0e02b2c3d470']); + + // Assert. + $output = $this->getDisplay(); + $this->assertNotNull($output); + $this->assertJson($output); + $this->assertStringContainsString('The application has been marked as recently viewed.', $output); + } + + public function testApiCommandExecutionForHttpPostLowercase(): void + { + // Test that method comparison is case-insensitive - using 'post' instead of 'POST'. + $this->clientProphecy->addOption('headers', ['Accept' => 'application/hal+json, version=2']) + ->shouldBeCalled(); + + // Should still add empty JSON body and Content-Type header even with lowercase method. + $this->clientProphecy->addOption('json', []) + ->shouldBeCalled(); + $this->clientProphecy->addOption('headers', ['Content-Type' => 'application/json']) + ->shouldBeCalled(); + + $mockResponseBody = self::getMockResponseFromSpec('/account/applications/{applicationUuid}/actions/star', 'post', '200'); + $this->clientProphecy->request('post', '/account/applications/a47ac10b-58cc-4372-a567-0e02b2c3d470/actions/star') + ->willReturn($mockResponseBody) + ->shouldBeCalled(); + + $this->command = $this->getApiCommandByName('api:accounts:application-star'); + $this->executeCommand(['applicationUuid' => 'a47ac10b-58cc-4372-a567-0e02b2c3d470']); + + // Assert. + $output = $this->getDisplay(); + $this->assertNotNull($output); + $this->assertJson($output); + } + + public function testApiCommandExecutionForHttpPostWithDataDoesNotAddEmptyJson(): void + { + // Test POST request WITH body data - should NOT add empty JSON body. + $this->clientProphecy->addOption('headers', ['Accept' => 'application/hal+json, version=2']) + ->shouldBeCalled(); + + $mockRequestArgs = self::getMockRequestBodyFromSpec('/account/ssh-keys'); + $mockResponseBody = self::getMockResponseFromSpec('/account/ssh-keys', 'post', '202'); + + // Should add actual data, not empty JSON. + foreach ($mockRequestArgs as $name => $value) { + $this->clientProphecy->addOption('json', [$name => $value]) + ->shouldBeCalled(); + } + + // Should NOT call addOption with empty JSON array when data is present. + $this->clientProphecy->addOption('json', []) + ->shouldNotBeCalled(); + + $this->clientProphecy->request('post', '/account/ssh-keys') + ->willReturn($mockResponseBody) + ->shouldBeCalled(); + + $this->command = $this->getApiCommandByName('api:accounts:ssh-key-create'); + $this->executeCommand($mockRequestArgs); + + // Assert. + $output = $this->getDisplay(); + $this->assertNotNull($output); + $this->assertJson($output); + } + public function testApiCommandExecutionForHttpPost(): void { $this->clientProphecy->addOption('headers', ['Accept' => 'application/hal+json, version=2']) @@ -761,4 +846,60 @@ public function testPrereleaseCommandsAreHidden(): void } } } + + /** + * Tests that pre-release commands have appropriate help text. + */ + public function testPrereleaseCommandsHaveCorrectHelpText(): void + { + // Load the API spec to find pre-release commands. + $apiSpec = self::getCloudApiSpec(); + + foreach ($apiSpec['paths'] as $path => $endpoint) { + foreach ($endpoint as $method => $schema) { + if (!array_key_exists('x-cli-name', $schema)) { + continue; + } + + // Test pre-release commands have correct help text. + if (array_key_exists('x-prerelease', $schema) && $schema['x-prerelease'] === true) { + $commandName = 'api:' . $schema['x-cli-name']; + $command = $this->getApiCommandByName($commandName); + if ($command) { + $helpText = $command->getHelp(); + $this->assertStringContainsString('This endpoint is pre-release and therefore unsupported', $helpText); + $this->assertStringContainsString('cloudapi-docs.acquia.com', $helpText); + } + } + } + } + } + + /** + * Tests that deprecated commands have appropriate help text. + */ + public function testDeprecatedCommandsHaveCorrectHelpText(): void + { + // Load the API spec to find deprecated commands. + $apiSpec = self::getCloudApiSpec(); + + foreach ($apiSpec['paths'] as $path => $endpoint) { + foreach ($endpoint as $method => $schema) { + if (!array_key_exists('x-cli-name', $schema)) { + continue; + } + + // Test deprecated commands have correct help text. + if (array_key_exists('deprecated', $schema) && $schema['deprecated'] === true) { + $commandName = 'api:' . $schema['x-cli-name']; + $command = $this->getApiCommandByName($commandName); + if ($command) { + $helpText = $command->getHelp(); + $this->assertStringContainsString('This endpoint is deprecated', $helpText); + $this->assertStringContainsString('cloudapi-docs.acquia.com', $helpText); + } + } + } + } + } } From b3da9d9a8498d78fce937c41b6c3be9c4c708ea6 Mon Sep 17 00:00:00 2001 From: jigar-shah-acquia Date: Mon, 5 Jan 2026 18:13:05 +0530 Subject: [PATCH 4/4] Update src/Command/Api/ApiBaseCommand.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Command/Api/ApiBaseCommand.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Command/Api/ApiBaseCommand.php b/src/Command/Api/ApiBaseCommand.php index e1ee644af..b3f13b2c7 100644 --- a/src/Command/Api/ApiBaseCommand.php +++ b/src/Command/Api/ApiBaseCommand.php @@ -414,7 +414,6 @@ private function addPostParamsToClient(InputInterface $input, Client $acquiaClou if (!$hasData && in_array(strtoupper($this->method), ['POST', 'PUT', 'PATCH'])) { $acquiaCloudClient->addOption('json', []); $acquiaCloudClient->addOption('headers', [ - 'Content-Type' => 'application/json', ]); }