diff --git a/src/Command/Env/EnvCertCreateCommand.php b/src/Command/Env/EnvCertCreateCommand.php index 3b5f763d6..72643f6b2 100644 --- a/src/Command/Env/EnvCertCreateCommand.php +++ b/src/Command/Env/EnvCertCreateCommand.php @@ -6,6 +6,7 @@ use Acquia\Cli\Attribute\RequireAuth; use Acquia\Cli\Command\CommandBase; +use Acquia\Cli\Exception\AcquiaCliException; use AcquiaCloudApi\Endpoints\SslCertificates; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; @@ -53,7 +54,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int $legacy ); $notificationUuid = CommandBase::getNotificationUuidFromResponse($response); - $this->waitForNotificationToComplete($acquiaCloudClient, $notificationUuid, 'Installing certificate'); + $success = $this->waitForNotificationToComplete($acquiaCloudClient, $notificationUuid, 'Installing certificate'); + if (!$success) { + throw new AcquiaCliException('Cloud API failed to install certificate'); + } return Command::SUCCESS; } } diff --git a/src/Command/Env/EnvCreateCommand.php b/src/Command/Env/EnvCreateCommand.php index 19cf133a6..29585fd8c 100644 --- a/src/Command/Env/EnvCreateCommand.php +++ b/src/Command/Env/EnvCreateCommand.php @@ -62,7 +62,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int ]); } }; - $this->waitForNotificationToComplete($acquiaCloudClient, $notificationUuid, "Waiting for the environment to be ready. This usually takes 2 - 15 minutes.", $success); + $success = $this->waitForNotificationToComplete($acquiaCloudClient, $notificationUuid, "Waiting for the environment to be ready. This usually takes 2 - 15 minutes.", $success); + if (!$success) { + throw new AcquiaCliException('Cloud API failed to create environment'); + } return Command::SUCCESS; } diff --git a/src/Command/Env/EnvMirrorCommand.php b/src/Command/Env/EnvMirrorCommand.php index 0b2b871e6..b0e79ac9f 100644 --- a/src/Command/Env/EnvMirrorCommand.php +++ b/src/Command/Env/EnvMirrorCommand.php @@ -6,6 +6,7 @@ use Acquia\Cli\Attribute\RequireAuth; use Acquia\Cli\Command\CommandBase; +use Acquia\Cli\Exception\AcquiaCliException; use Acquia\Cli\Output\Checklist; use AcquiaCloudApi\Connector\Client; use AcquiaCloudApi\Endpoints\Databases; @@ -78,17 +79,17 @@ protected function execute(InputInterface $input, OutputInterface $output): int $configCopyResponse = $this->mirrorConfig($sourceEnvironment, $destinationEnvironment, $environmentsResource, $destinationEnvironmentUuid, $outputCallback); } - if (isset($codeCopyResponse)) { - $this->waitForNotificationToComplete($acquiaCloudClient, CommandBase::getNotificationUuidFromResponse($codeCopyResponse), 'Waiting for code copy to complete'); + if (isset($codeCopyResponse) && !$this->waitForNotificationToComplete($acquiaCloudClient, CommandBase::getNotificationUuidFromResponse($codeCopyResponse), 'Waiting for code copy to complete')) { + throw new AcquiaCliException('Cloud API failed to copy code'); } - if (isset($dbCopyResponse)) { - $this->waitForNotificationToComplete($acquiaCloudClient, CommandBase::getNotificationUuidFromResponse($dbCopyResponse), 'Waiting for database copy to complete'); + if (isset($dbCopyResponse) && !$this->waitForNotificationToComplete($acquiaCloudClient, CommandBase::getNotificationUuidFromResponse($dbCopyResponse), 'Waiting for database copy to complete')) { + throw new AcquiaCliException('Cloud API failed to copy database'); } - if (isset($filesCopyResponse)) { - $this->waitForNotificationToComplete($acquiaCloudClient, CommandBase::getNotificationUuidFromResponse($filesCopyResponse), 'Waiting for files copy to complete'); + if (isset($filesCopyResponse) && !$this->waitForNotificationToComplete($acquiaCloudClient, CommandBase::getNotificationUuidFromResponse($filesCopyResponse), 'Waiting for files copy to complete')) { + throw new AcquiaCliException('Cloud API failed to copy files'); } - if (isset($configCopyResponse)) { - $this->waitForNotificationToComplete($acquiaCloudClient, CommandBase::getNotificationUuidFromResponse($configCopyResponse), 'Waiting for config copy to complete'); + if (isset($configCopyResponse) && !$this->waitForNotificationToComplete($acquiaCloudClient, CommandBase::getNotificationUuidFromResponse($configCopyResponse), 'Waiting for config copy to complete')) { + throw new AcquiaCliException('Cloud API failed to copy config'); } $this->io->success([ diff --git a/src/Command/Pull/PullCommandBase.php b/src/Command/Pull/PullCommandBase.php index 5a12dec73..985166d98 100644 --- a/src/Command/Pull/PullCommandBase.php +++ b/src/Command/Pull/PullCommandBase.php @@ -347,8 +347,11 @@ protected function waitForBackup(string $notificationUuid, Client $acquiaCloudCl $this->output->writeln(''); $this->output->writeln('Database backup is ready!'); }; - $this->waitForNotificationToComplete($acquiaCloudClient, $notificationUuid, $spinnerMessage, $successCallback); + $success = $this->waitForNotificationToComplete($acquiaCloudClient, $notificationUuid, $spinnerMessage, $successCallback); Loop::run(); + if (!$success) { + throw new AcquiaCliException('Cloud API failed to create a backup'); + } } private function connectToLocalDatabase(string $dbHost, string $dbUser, string $dbName, string $dbPassword, callable $outputCallback = null): void diff --git a/tests/phpunit/src/CommandTestBase.php b/tests/phpunit/src/CommandTestBase.php index 46f89217a..75f37d0dd 100644 --- a/tests/phpunit/src/CommandTestBase.php +++ b/tests/phpunit/src/CommandTestBase.php @@ -408,10 +408,16 @@ protected function mockDatabaseBackupCreateResponse( return $backupCreateResponse; } - protected function mockNotificationResponseFromObject(object $responseWithNotificationLink): array|object + protected function mockNotificationResponseFromObject(object $responseWithNotificationLink, ?bool $success = true): array|object { $uuid = substr($responseWithNotificationLink->_links->notification->href, -36); - return $this->mockRequest('getNotificationByUuid', $uuid); + if ($success) { + return $this->mockRequest('getNotificationByUuid', $uuid); + } + + return $this->mockRequest('getNotificationByUuid', $uuid, null, null, function ($response): void { + $response->status = 'failed'; + }); } protected function mockCreateMySqlDumpOnLocal(ObjectProphecy $localMachineHelper, bool $printOutput = true, bool $pv = true): void diff --git a/tests/phpunit/src/Commands/Env/EnvCertCreateCommandTest.php b/tests/phpunit/src/Commands/Env/EnvCertCreateCommandTest.php index 5916748b1..e5c8462af 100644 --- a/tests/phpunit/src/Commands/Env/EnvCertCreateCommandTest.php +++ b/tests/phpunit/src/Commands/Env/EnvCertCreateCommandTest.php @@ -6,6 +6,7 @@ use Acquia\Cli\Command\CommandBase; use Acquia\Cli\Command\Env\EnvCertCreateCommand; +use Acquia\Cli\Exception\AcquiaCliException; use Acquia\Cli\Tests\CommandTestBase; class EnvCertCreateCommandTest extends CommandTestBase @@ -74,6 +75,67 @@ public function testCreateCert(): void ); } + public function testCreateCertFailed(): void + { + $applications = $this->mockRequest('getApplications'); + $application = $this->mockRequest('getApplicationByUuid', $applications[self::$INPUT_DEFAULT_CHOICE]->uuid); + $environments = $this->mockRequest('getApplicationEnvironments', $application->uuid); + $localMachineHelper = $this->mockLocalMachineHelper(); + $certContents = 'cert-contents'; + $keyContents = 'key-contents'; + $certName = 'cert.pem'; + $keyName = 'key.pem'; + $label = 'My certificate'; + $csrId = 123; + $localMachineHelper->readFile($certName) + ->willReturn($certContents) + ->shouldBeCalled(); + $localMachineHelper->readFile($keyName) + ->willReturn($keyContents) + ->shouldBeCalled(); + + $sslResponse = $this->getMockResponseFromSpec( + '/environments/{environmentId}/ssl/certificates', + 'post', + '202' + ); + $options = [ + 'json' => [ + 'ca_certificates' => null, + 'certificate' => $certContents, + 'csr_id' => $csrId, + 'label' => $label, + 'legacy' => false, + 'private_key' => $keyContents, + ], + ]; + $this->clientProphecy->request('post', "/environments/{$environments[1]->id}/ssl/certificates", $options) + ->willReturn($sslResponse->{'Site is being imported'}->value) + ->shouldBeCalled(); + $this->mockNotificationResponseFromObject($sslResponse->{'Site is being imported'}->value, false); + + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage('Cloud API failed to install certificate'); + $this->executeCommand( + [ + '--csr-id' => $csrId, + '--label' => $label, + '--legacy' => false, + 'certificate' => $certName, + 'private-key' => $keyName, + ], + [ + // Would you like Acquia CLI to search for a Cloud application that matches your local git config?'. + 'n', + // Select a Cloud Platform application: [Sample application 1]: + 0, + 'n', + 1, + '', + ] + ); + } + public function testCreateCertNode(): void { $applications = $this->mockRequest('getApplications'); diff --git a/tests/phpunit/src/Commands/Env/EnvCreateCommandTest.php b/tests/phpunit/src/Commands/Env/EnvCreateCommandTest.php index 3b8b301b9..d7f0b4397 100644 --- a/tests/phpunit/src/Commands/Env/EnvCreateCommandTest.php +++ b/tests/phpunit/src/Commands/Env/EnvCreateCommandTest.php @@ -17,7 +17,7 @@ class EnvCreateCommandTest extends CommandTestBase { private static string $validLabel = 'New CDE'; - private function setupCdeTest(string $label): string + private function setupCdeTest(string $label, ?bool $apiSuccess = true): string { $applicationsResponse = $this->mockApplicationsRequest(); $applicationResponse = $this->mockApplicationRequest(); @@ -60,7 +60,7 @@ private function setupCdeTest(string $label): string ->willReturn($environmentsResponse->{'Adding environment'}->value) ->shouldBeCalled(); - $this->mockNotificationResponseFromObject($environmentsResponse->{'Adding environment'}->value); + $this->mockNotificationResponseFromObject($environmentsResponse->{'Adding environment'}->value, $apiSuccess); return $response2->_embedded->items[3]->domains[0]; } @@ -160,4 +160,23 @@ public function testCreateCdeInvalidTag(): void ] ); } + + /** + * @group brokenProphecy + */ + public function testCreateCdeApiFailure(): void + { + $this->setupCdeTest(self::$validLabel, false); + + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage('Cloud API failed to create environment'); + + $this->executeCommand( + [ + 'applicationUuid' => $this->getApplication(), + 'branch' => $this->getBranch(), + 'label' => self::$validLabel, + ] + ); + } } diff --git a/tests/phpunit/src/Commands/Env/EnvMirrorCommandTest.php b/tests/phpunit/src/Commands/Env/EnvMirrorCommandTest.php index 8882e7d91..92add7484 100644 --- a/tests/phpunit/src/Commands/Env/EnvMirrorCommandTest.php +++ b/tests/phpunit/src/Commands/Env/EnvMirrorCommandTest.php @@ -6,6 +6,7 @@ use Acquia\Cli\Command\CommandBase; use Acquia\Cli\Command\Env\EnvMirrorCommand; +use Acquia\Cli\Exception\AcquiaCliException; use Acquia\Cli\Tests\CommandTestBase; use Prophecy\Argument; @@ -96,4 +97,217 @@ public function testEnvironmentMirror(): void $this->assertStringContainsString("Copying PHP version, acpu memory limit, etc.", $output); $this->assertStringContainsString("[OK] Done! $environmentResponse->label now matches $environmentResponse->label", $output); } + + public function testEnvironmentMirrorDbCopyFail(): void + { + $environmentResponse = $this->mockGetEnvironments(); + $codeSwitchResponse = $this->getMockResponseFromSpec("/environments/{environmentId}/code/actions/switch", 'post', '202'); + $response = $codeSwitchResponse->{'Switching code'}->value; + $this->mockNotificationResponseFromObject($response); + $response->links = $response->{'_links'}; + $this->clientProphecy->request( + 'post', + "/environments/$environmentResponse->id/code/actions/switch", + [ + 'form_params' => [ + 'branch' => $environmentResponse->vcs->path, + ], + ] + ) + ->willReturn($response) + ->shouldBeCalled(); + + $databasesResponse = $this->getMockResponseFromSpec("/environments/{environmentId}/databases", 'get', '200'); + $this->clientProphecy->request( + 'get', + "/environments/$environmentResponse->id/databases" + ) + ->willReturn($databasesResponse->_embedded->items) + ->shouldBeCalled(); + + $dbCopyResponse = $this->getMockResponseFromSpec("/environments/{environmentId}/databases", 'post', '202'); + $response = $dbCopyResponse->{'Database being copied'}->value; + $this->mockNotificationResponseFromObject($response, false); + $response->links = $response->{'_links'}; + $this->clientProphecy->request('post', "/environments/$environmentResponse->id/databases", [ + 'json' => [ + 'name' => $databasesResponse->_embedded->items[0]->name, + 'source' => $environmentResponse->id, + ], + ]) + ->willReturn($response) + ->shouldBeCalled(); + + $filesCopyResponse = $this->getMockResponseFromSpec("/environments/{environmentId}/files", 'post', '202'); + $response = $filesCopyResponse->{'Files queued for copying'}->value; + $response->links = $response->{'_links'}; + $this->clientProphecy->request('post', "/environments/$environmentResponse->id/files", [ + 'json' => [ + 'source' => $environmentResponse->id, + ], + ]) + ->willReturn($response) + ->shouldBeCalled(); + + $environmentUpdateResponse = $this->getMockResponseFromSpec("/environments/{environmentId}", 'put', '202'); + $this->clientProphecy->request('put', "/environments/$environmentResponse->id", Argument::type('array')) + ->willReturn($environmentUpdateResponse) + ->shouldBeCalled(); + + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage('Cloud API failed to copy database'); + $this->executeCommand( + [ + 'destination-environment' => $environmentResponse->id, + 'source-environment' => $environmentResponse->id, + ], + [ + // Are you sure that you want to overwrite everything ... + 'y', + ] + ); + } + + public function testEnvironmentMirrorFileCopyFail(): void + { + $environmentResponse = $this->mockGetEnvironments(); + $codeSwitchResponse = $this->getMockResponseFromSpec("/environments/{environmentId}/code/actions/switch", 'post', '202'); + $response = $codeSwitchResponse->{'Switching code'}->value; + $this->mockNotificationResponseFromObject($response); + $response->links = $response->{'_links'}; + $this->clientProphecy->request( + 'post', + "/environments/$environmentResponse->id/code/actions/switch", + [ + 'form_params' => [ + 'branch' => $environmentResponse->vcs->path, + ], + ] + ) + ->willReturn($response) + ->shouldBeCalled(); + + $databasesResponse = $this->getMockResponseFromSpec("/environments/{environmentId}/databases", 'get', '200'); + $this->clientProphecy->request( + 'get', + "/environments/$environmentResponse->id/databases" + ) + ->willReturn($databasesResponse->_embedded->items) + ->shouldBeCalled(); + + $dbCopyResponse = $this->getMockResponseFromSpec("/environments/{environmentId}/databases", 'post', '202'); + $response = $dbCopyResponse->{'Database being copied'}->value; + $this->mockNotificationResponseFromObject($response); + $response->links = $response->{'_links'}; + $this->clientProphecy->request('post', "/environments/$environmentResponse->id/databases", [ + 'json' => [ + 'name' => $databasesResponse->_embedded->items[0]->name, + 'source' => $environmentResponse->id, + ], + ]) + ->willReturn($response) + ->shouldBeCalled(); + + $filesCopyResponse = $this->getMockResponseFromSpec("/environments/{environmentId}/files", 'post', '202'); + $response = $filesCopyResponse->{'Files queued for copying'}->value; + $this->mockNotificationResponseFromObject($response, false); + $response->links = $response->{'_links'}; + $this->clientProphecy->request('post', "/environments/$environmentResponse->id/files", [ + 'json' => [ + 'source' => $environmentResponse->id, + ], + ]) + ->willReturn($response) + ->shouldBeCalled(); + + $environmentUpdateResponse = $this->getMockResponseFromSpec("/environments/{environmentId}", 'put', '202'); + $this->clientProphecy->request('put', "/environments/$environmentResponse->id", Argument::type('array')) + ->willReturn($environmentUpdateResponse) + ->shouldBeCalled(); + + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage('Cloud API failed to copy files'); + $this->executeCommand( + [ + 'destination-environment' => $environmentResponse->id, + 'source-environment' => $environmentResponse->id, + ], + [ + // Are you sure that you want to overwrite everything ... + 'y', + ] + ); + } + + public function testEnvironmentMirrorFiCopyFail(): void + { + $environmentResponse = $this->mockGetEnvironments(); + $codeSwitchResponse = $this->getMockResponseFromSpec("/environments/{environmentId}/code/actions/switch", 'post', '202'); + $response = $codeSwitchResponse->{'Switching code'}->value; + $this->mockNotificationResponseFromObject($response); + $response->links = $response->{'_links'}; + $this->clientProphecy->request( + 'post', + "/environments/$environmentResponse->id/code/actions/switch", + [ + 'form_params' => [ + 'branch' => $environmentResponse->vcs->path, + ], + ] + ) + ->willReturn($response) + ->shouldBeCalled(); + + $databasesResponse = $this->getMockResponseFromSpec("/environments/{environmentId}/databases", 'get', '200'); + $this->clientProphecy->request( + 'get', + "/environments/$environmentResponse->id/databases" + ) + ->willReturn($databasesResponse->_embedded->items) + ->shouldBeCalled(); + + $dbCopyResponse = $this->getMockResponseFromSpec("/environments/{environmentId}/databases", 'post', '202'); + $response = $dbCopyResponse->{'Database being copied'}->value; + $this->mockNotificationResponseFromObject($response); + $response->links = $response->{'_links'}; + $this->clientProphecy->request('post', "/environments/$environmentResponse->id/databases", [ + 'json' => [ + 'name' => $databasesResponse->_embedded->items[0]->name, + 'source' => $environmentResponse->id, + ], + ]) + ->willReturn($response) + ->shouldBeCalled(); + + $filesCopyResponse = $this->getMockResponseFromSpec("/environments/{environmentId}/files", 'post', '202'); + $response = $filesCopyResponse->{'Files queued for copying'}->value; + $this->mockNotificationResponseFromObject($response); + $response->links = $response->{'_links'}; + $this->clientProphecy->request('post', "/environments/$environmentResponse->id/files", [ + 'json' => [ + 'source' => $environmentResponse->id, + ], + ]) + ->willReturn($response) + ->shouldBeCalled(); + + $environmentUpdateResponse = $this->getMockResponseFromSpec("/environments/{environmentId}", 'put', '202'); + $this->clientProphecy->request('put', "/environments/$environmentResponse->id", Argument::type('array')) + ->willReturn($environmentUpdateResponse) + ->shouldBeCalled(); + $this->mockNotificationResponseFromObject($environmentUpdateResponse, false); + + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage('Cloud API failed to copy config'); + $this->executeCommand( + [ + 'destination-environment' => $environmentResponse->id, + 'source-environment' => $environmentResponse->id, + ], + [ + // Are you sure that you want to overwrite everything ... + 'y', + ] + ); + } } diff --git a/tests/phpunit/src/Commands/Pull/PullDatabaseCommandTest.php b/tests/phpunit/src/Commands/Pull/PullDatabaseCommandTest.php index da95bb0dd..1dc19d658 100644 --- a/tests/phpunit/src/Commands/Pull/PullDatabaseCommandTest.php +++ b/tests/phpunit/src/Commands/Pull/PullDatabaseCommandTest.php @@ -208,6 +208,19 @@ public function testPullDatabasesOnDemand(): void $this->assertStringContainsString('jxr5000596dev (oracletest1.dev-profserv2.acsitefactory.com)', $output); } + public function testPullDatabasesOnDemandFail(): void + { + $this->setupPullDatabase(true, true, true, true, false, 0, true, false); + $inputs = self::inputChooseEnvironment(); + + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage('Cloud API failed to create a backup'); + $this->executeCommand([ + '--no-scripts' => true, + '--on-demand' => true, + ], $inputs); + } + public function testPullDatabasesNoExistingBackup(): void { $this->setupPullDatabase(true, true, true, true, false, 0, false); @@ -302,7 +315,7 @@ public function testPullDatabaseWithInvalidSslCertificate(int $errorCode): void $this->assertStringContainsString('Trying alternative host other.example.com', $output); } - protected function setupPullDatabase(bool $mysqlConnectSuccessful, bool $mockIdeFs = false, bool $onDemand = false, bool $mockGetAcsfSites = true, bool $multiDb = false, int $curlCode = 0, bool $existingBackups = true): void + protected function setupPullDatabase(bool $mysqlConnectSuccessful, bool $mockIdeFs = false, bool $onDemand = false, bool $mockGetAcsfSites = true, bool $multiDb = false, int $curlCode = 0, bool $existingBackups = true, bool $onDemandSuccess = true): void { $applicationsResponse = $this->mockApplicationsRequest(); $this->mockApplicationRequest(); @@ -312,8 +325,7 @@ protected function setupPullDatabase(bool $mysqlConnectSuccessful, bool $mockIde $databasesResponse = $this->mockAcsfDatabasesResponse($selectedEnvironment); $databaseResponse = $databasesResponse[array_search('jxr5000596dev', array_column($databasesResponse, 'name'), true)]; - $databaseBackupsResponse = $this->mockDatabaseBackupsResponse($selectedEnvironment, $databaseResponse->name, 1, $existingBackups); - $selectedDatabase = $this->mockDownloadBackup($databaseResponse, $selectedEnvironment, $databaseBackupsResponse->_embedded->items[0], $curlCode); + $selectedDatabase = $databaseResponse; if ($multiDb) { $databaseResponse2 = $databasesResponse[array_search('profserv2', array_column($databasesResponse, 'name'), true)]; @@ -329,12 +341,20 @@ protected function setupPullDatabase(bool $mysqlConnectSuccessful, bool $mockIde if ($onDemand) { $backupResponse = $this->mockDatabaseBackupCreateResponse($selectedEnvironment, $selectedDatabase->name); // Cloud API does not provide the notification UUID as part of the backup response, so we must hardcode it. - $this->mockNotificationResponseFromObject($backupResponse); + $this->mockNotificationResponseFromObject($backupResponse, $onDemandSuccess); } - $fs = $this->prophet->prophesize(Filesystem::class); $localMachineHelper = $this->mockLocalMachineHelper(); $this->mockExecuteMySqlConnect($localMachineHelper, $mysqlConnectSuccessful); + + if (!$onDemandSuccess) { + return; + } + + $databaseBackupsResponse = $this->mockDatabaseBackupsResponse($selectedEnvironment, $databaseResponse->name, 1, $existingBackups); + $this->mockDownloadBackup($databaseResponse, $selectedEnvironment, $databaseBackupsResponse->_embedded->items[0], $curlCode); + + $fs = $this->prophet->prophesize(Filesystem::class); // Set up file system. $localMachineHelper->getFilesystem()->willReturn($fs)->shouldBeCalled();