diff --git a/src/Command/App/TaskWaitCommand.php b/src/Command/App/TaskWaitCommand.php index 73fabd872..1616685ad 100644 --- a/src/Command/App/TaskWaitCommand.php +++ b/src/Command/App/TaskWaitCommand.php @@ -5,7 +5,6 @@ namespace Acquia\Cli\Command\App; use Acquia\Cli\Command\CommandBase; -use Acquia\Cli\Exception\AcquiaCliException; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -23,31 +22,13 @@ protected function configure(): void { $this->setDescription('Wait for a task to complete') ->addArgument('notification-uuid', InputArgument::REQUIRED, 'The task notification UUID or Cloud Platform API response containing a linked notification') ->setHelp('Accepts either a notification UUID or Cloud Platform API response as JSON string. The JSON string must contain the _links->notification->href property.') - ->addUsage('"$(api:environments:domain-clear-caches [environmentId] [domain])"'); + ->addUsage('"$(acli api:environments:domain-clear-caches [environmentId] [domain])"'); } protected function execute(InputInterface $input, OutputInterface $output): int { - $notificationUuid = $this->getNotificationUuid($input); - $success = $this->waitForNotificationToComplete($this->cloudApiClientService->getClient(), $notificationUuid, "Waiting for task $notificationUuid to complete"); - if ($success) { - return Command::SUCCESS; - } - else { - return Command::FAILURE; - } - } - - private function getNotificationUuid(InputInterface $input): string { $notificationUuid = $input->getArgument('notification-uuid'); - $json = json_decode($notificationUuid, FALSE); - if (json_last_error() === JSON_ERROR_NONE) { - if (is_object($json) && property_exists($json, '_links') && property_exists($json->_links, 'notification') && property_exists($json->_links->notification, 'href')) { - return $this->getNotificationUuidFromResponse($json); - } - throw new AcquiaCliException("Input JSON must contain the _links.notification.href property."); - } - - return self::validateUuid($input->getArgument('notification-uuid')); + $success = $this->waitForNotificationToComplete($this->cloudApiClientService->getClient(), $notificationUuid, "Waiting for task $notificationUuid to complete"); + return $success ? Command::SUCCESS : Command::FAILURE; } } diff --git a/src/Command/CommandBase.php b/src/Command/CommandBase.php index b34f4ef4c..3be9d57e0 100644 --- a/src/Command/CommandBase.php +++ b/src/Command/CommandBase.php @@ -40,6 +40,7 @@ use Composer\Semver\VersionParser; use Exception; use GuzzleHttp\HandlerStack; +use JsonException; use Kevinrob\GuzzleCache\CacheMiddleware; use Kevinrob\GuzzleCache\Storage\Psr6CacheStorage; use Kevinrob\GuzzleCache\Strategy\PrivateCacheStrategy; @@ -206,6 +207,8 @@ protected function initialize(InputInterface $input, OutputInterface $output): v $this->convertEnvironmentAliasToUuid($input, 'source-environment'); $this->convertEnvironmentAliasToUuid($input, 'destination-environment'); $this->convertEnvironmentAliasToUuid($input, 'source'); + $this->convertNotificationToUuid($input, 'notificationUuid'); + $this->convertNotificationToUuid($input, 'notification-uuid'); if ($latest = $this->checkForNewVersion()) { $this->output->writeln("Acquia CLI $latest is available. Run acli self-update to update."); @@ -1045,6 +1048,34 @@ private function validateUserUuid(string $userUuidArgument, string $orgUuidArgum return $userUuidArgument; } + private static function getNotificationUuid(string $notification): string { + // Greedily hope this is already a UUID. + try { + self::validateUuid($notification); + return $notification; + } + catch (ValidatorException) { + } + + // Not a UUID, maybe a JSON object? + try { + $json = json_decode($notification, NULL, 4, JSON_THROW_ON_ERROR); + return CommandBase::getNotificationUuidFromResponse($json); + } + catch (JsonException | AcquiaCliException) { + } + + // Last chance, maybe a URL? + try { + return self::getNotificationUuidFromUrl($notification); + } + catch (ValidatorException | AcquiaCliException) { + } + + // Womp womp. + throw new AcquiaCliException('Notification format is not one of UUID, JSON response, or URL'); + } + /** * @param String $userAlias User alias like uuid or email. * @param String $orgUuidArgument Organization uuid. @@ -1078,7 +1109,7 @@ protected function convertApplicationAliasToUuid(InputInterface $input): void { } } - protected function convertEnvironmentAliasToUuid(InputInterface $input, mixed $argumentName): void { + protected function convertEnvironmentAliasToUuid(InputInterface $input, string $argumentName): void { if ($input->hasArgument($argumentName) && $input->getArgument($argumentName)) { $envUuidArgument = $input->getArgument($argumentName); $environmentUuid = $this->validateEnvironmentUuid($envUuidArgument, $argumentName); @@ -1086,6 +1117,14 @@ protected function convertEnvironmentAliasToUuid(InputInterface $input, mixed $a } } + protected function convertNotificationToUuid(InputInterface $input, string $argumentName): void { + if ($input->hasArgument($argumentName) && $input->getArgument($argumentName)) { + $notificationArgument = $input->getArgument($argumentName); + $notificationUuid = CommandBase::getNotificationUuid($notificationArgument); + $input->setArgument($argumentName, $notificationUuid); + } + } + /** * @param string $sshUrl The SSH URL to the server. * @return string The sitegroup. E.g., eemgrasmick. @@ -1235,7 +1274,7 @@ protected function getDrushDatabaseConnectionStatus(Closure $outputCallback = NU '--no-interaction', ], $outputCallback, $this->dir, FALSE); if ($process->isSuccessful()) { - $drushStatusReturnOutput = json_decode($process->getOutput(), TRUE, 512); + $drushStatusReturnOutput = json_decode($process->getOutput(), TRUE); if (is_array($drushStatusReturnOutput) && array_key_exists('db-status', $drushStatusReturnOutput) && $drushStatusReturnOutput['db-status'] === 'Connected') { $this->drushHasActiveDatabaseConnection = TRUE; return $this->drushHasActiveDatabaseConnection; @@ -1472,16 +1511,29 @@ private function writeCompletedMessage(NotificationResponse $notification): void $this->io->writeln("Duration: $duration seconds"); } - protected function getNotificationUuidFromResponse(object $response): string { + protected static function getNotificationUuidFromResponse(object $response): string { if (property_exists($response, 'links')) { $links = $response->links; } - else { + elseif (property_exists($response, '_links')) { $links = $response->_links; } - $notificationUrl = $links->notification->href; - $urlParts = explode('/', $notificationUrl); - return $urlParts[5]; + else { + throw new AcquiaCliException('JSON object must contain the _links.notification.href property'); + } + if (property_exists($links, 'notification') && property_exists($links->notification, 'href')) { + return self::getNotificationUuidFromUrl($links->notification->href); + } + throw new AcquiaCliException('JSON object must contain the _links.notification.href property'); + } + + private static function getNotificationUuidFromUrl(string $notificationUrl): string { + $notificationUrlPattern = '/^https:\/\/cloud.acquia.com\/api\/notifications\/([\w-]*)$/'; + if (preg_match($notificationUrlPattern, $notificationUrl, $matches)) { + self::validateUuid($matches[1]); + return $matches[1]; + } + throw new AcquiaCliException('Notification UUID not found in URL'); } protected function validateRequiredCloudPermissions(Client $acquiaCloudClient, ?string $cloudApplicationUuid, AccountResponse $account, array $requiredPermissions): void { diff --git a/src/Command/Env/EnvCertCreateCommand.php b/src/Command/Env/EnvCertCreateCommand.php index 2b12e7424..7133248f4 100644 --- a/src/Command/Env/EnvCertCreateCommand.php +++ b/src/Command/Env/EnvCertCreateCommand.php @@ -52,7 +52,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $csrId, $legacy ); - $notificationUuid = $this->getNotificationUuidFromResponse($response); + $notificationUuid = CommandBase::getNotificationUuidFromResponse($response); $this->waitForNotificationToComplete($acquiaCloudClient, $notificationUuid, 'Installing certificate'); return Command::SUCCESS; } diff --git a/src/Command/Env/EnvCreateCommand.php b/src/Command/Env/EnvCreateCommand.php index aacae3a66..1f24f7fef 100644 --- a/src/Command/Env/EnvCreateCommand.php +++ b/src/Command/Env/EnvCreateCommand.php @@ -46,7 +46,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->checklist->addItem("Initiating environment creation"); $response = $environmentsResource->create($cloudAppUuid, $label, $branch, $databaseNames); - $notificationUuid = $this->getNotificationUuidFromResponse($response); + $notificationUuid = CommandBase::getNotificationUuidFromResponse($response); $this->checklist->completePreviousItem(); $success = function () use ($environmentsResource, $cloudAppUuid, $label): void { diff --git a/src/Command/Env/EnvMirrorCommand.php b/src/Command/Env/EnvMirrorCommand.php index d67881bfe..84894ead0 100644 --- a/src/Command/Env/EnvMirrorCommand.php +++ b/src/Command/Env/EnvMirrorCommand.php @@ -80,16 +80,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int } if (isset($codeCopyResponse)) { - $this->waitForNotificationToComplete($acquiaCloudClient, $this->getNotificationUuidFromResponse($codeCopyResponse), 'Waiting for code copy to complete'); + $this->waitForNotificationToComplete($acquiaCloudClient, CommandBase::getNotificationUuidFromResponse($codeCopyResponse), 'Waiting for code copy to complete'); } if (isset($dbCopyResponse)) { - $this->waitForNotificationToComplete($acquiaCloudClient, $this->getNotificationUuidFromResponse($dbCopyResponse), 'Waiting for database copy to complete'); + $this->waitForNotificationToComplete($acquiaCloudClient, CommandBase::getNotificationUuidFromResponse($dbCopyResponse), 'Waiting for database copy to complete'); } if (isset($filesCopyResponse)) { - $this->waitForNotificationToComplete($acquiaCloudClient, $this->getNotificationUuidFromResponse($filesCopyResponse), 'Waiting for files copy to complete'); + $this->waitForNotificationToComplete($acquiaCloudClient, CommandBase::getNotificationUuidFromResponse($filesCopyResponse), 'Waiting for files copy to complete'); } if (isset($configCopyResponse)) { - $this->waitForNotificationToComplete($acquiaCloudClient, $this->getNotificationUuidFromResponse($configCopyResponse), 'Waiting for config copy to complete'); + $this->waitForNotificationToComplete($acquiaCloudClient, CommandBase::getNotificationUuidFromResponse($configCopyResponse), 'Waiting for config copy to complete'); } $this->io->success([ diff --git a/tests/phpunit/src/CommandTestBase.php b/tests/phpunit/src/CommandTestBase.php index 5f371fde4..3df30847e 100644 --- a/tests/phpunit/src/CommandTestBase.php +++ b/tests/phpunit/src/CommandTestBase.php @@ -344,20 +344,9 @@ protected function mockDatabaseBackupCreateResponse( return $backupCreateResponse; } - protected function mockNotificationResponseFromObject(object $responseWithNotificationLink): mixed { - return $this->mockNotificationResponse(substr($responseWithNotificationLink->_links->notification->href, -36)); - } - - protected function mockNotificationResponse(string $notificationUuid, string $status = NULL): mixed { - $notificationResponse = $this->getMockResponseFromSpec('/notifications/{notificationUuid}', 'get', 200); - if ($status) { - $notificationResponse->status = $status; - } - $this->clientProphecy->request('get', "/notifications/$notificationUuid") - ->willReturn($notificationResponse) - ->shouldBeCalled(); - - return $notificationResponse; + protected function mockNotificationResponseFromObject(object $responseWithNotificationLink): array|object { + $uuid = substr($responseWithNotificationLink->_links->notification->href, -36); + return $this->mockRequest('getNotificationByUuid', $uuid); } protected function mockCreateMySqlDumpOnLocal(ObjectProphecy $localMachineHelper): void { diff --git a/tests/phpunit/src/Commands/Acsf/AcsfAuthLoginCommandTest.php b/tests/phpunit/src/Commands/Acsf/AcsfAuthLoginCommandTest.php index f8cfc0548..b1d1f0fc7 100644 --- a/tests/phpunit/src/Commands/Acsf/AcsfAuthLoginCommandTest.php +++ b/tests/phpunit/src/Commands/Acsf/AcsfAuthLoginCommandTest.php @@ -79,7 +79,7 @@ public function providerTestAuthLoginCommand(): array { // Arguments. [], // Output to assert. - "Acquia CLI is now logged in to {$this->acsfCurrentFactoryUrl} as {$this->acsfUsername}", + "Acquia CLI is now logged in to $this->acsfCurrentFactoryUrl as $this->acsfUsername", // $config. $this->getAcsfCredentialsFileContents(), ], @@ -88,14 +88,9 @@ public function providerTestAuthLoginCommand(): array { /** * @dataProvider providerTestAuthLoginCommand - * @param $machineIsAuthenticated - * @param $inputs - * @param $args - * @param $outputToAssert - * @param array $config * @requires OS linux|darwin */ - public function testAcsfAuthLoginCommand(mixed $machineIsAuthenticated, mixed $inputs, mixed $args, mixed $outputToAssert, array $config = []): void { + public function testAcsfAuthLoginCommand(bool $machineIsAuthenticated, array $inputs, array $args, string $outputToAssert, array $config = []): void { if (!$machineIsAuthenticated) { $this->clientServiceProphecy->isMachineAuthenticated()->willReturn(FALSE); $this->removeMockCloudConfigFile(); diff --git a/tests/phpunit/src/Commands/App/TaskWaitCommandTest.php b/tests/phpunit/src/Commands/App/TaskWaitCommandTest.php index a47a3d40f..5b732eda7 100644 --- a/tests/phpunit/src/Commands/App/TaskWaitCommandTest.php +++ b/tests/phpunit/src/Commands/App/TaskWaitCommandTest.php @@ -21,58 +21,135 @@ protected function createCommand(): Command { /** * @dataProvider providerTestTaskWaitCommand */ - public function testTaskWaitCommand(string $status, string $message, int $statusCode): void { - $notificationUuid = '94835c3e-b112-4660-a14d-d541906c205b'; - $this->mockNotificationResponse($notificationUuid, $status); + public function testTaskWaitCommand(string $notification): void { + $notificationUuid = '1bd3487e-71d1-4fca-a2d9-5f969b3d35c1'; + $this->mockRequest('getNotificationByUuid', $notificationUuid); $this->executeCommand([ - 'notification-uuid' => $notificationUuid, + 'notification-uuid' => $notification, ]); - - // Assert. $this->prophet->checkPredictions(); $output = $this->getDisplay(); - self::assertStringContainsString($message, $output); + self::assertStringContainsString(' [OK] The task with notification uuid 1bd3487e-71d1-4fca-a2d9-5f969b3d35c1 completed', $output); $this->assertStringContainsString('Progress: 100', $output); $this->assertStringContainsString('Completed: Mon Jul 29 20:47:13 UTC 2019', $output); $this->assertStringContainsString('Task type: Application added to recents list', $output); $this->assertStringContainsString('Duration: 0 seconds', $output); - $this->assertEquals($statusCode, $this->getStatusCode()); + $this->assertEquals(Command::SUCCESS, $this->getStatusCode()); + } + + public function testTaskWaitCommandWithFailedTask(): void { + $notificationUuid = '1bd3487e-71d1-4fca-a2d9-5f969b3d35c1'; + $this->mockRequest( + 'getNotificationByUuid', + $notificationUuid, + NULL, + NULL, + function ($response): void { + $response->status = 'failed';} + ); + $this->executeCommand([ + 'notification-uuid' => $notificationUuid, + ]); + $this->prophet->checkPredictions(); + self::assertStringContainsString(' [ERROR] The task with notification uuid 1bd3487e-71d1-4fca-a2d9-5f969b3d35c1 failed', $this->getDisplay()); + $this->assertEquals(Command::FAILURE, $this->getStatusCode()); } /** - * @return array + * Valid notifications. + * + * @return (string|int)[][] */ public function providerTestTaskWaitCommand(): array { return [ [ - 'completed', - ' [OK] The task with notification uuid 1bd3487e-71d1-4fca-a2d9-5f969b3d35c1 completed', - Command::SUCCESS, + '1bd3487e-71d1-4fca-a2d9-5f969b3d35c1', ], [ - 'failed', - ' [ERROR] The task with notification uuid 1bd3487e-71d1-4fca-a2d9-5f969b3d35c1 failed', - Command::FAILURE, + 'https://cloud.acquia.com/api/notifications/1bd3487e-71d1-4fca-a2d9-5f969b3d35c1', + ], + [ + <<<'EOT' +{ + "message": "Caches are being cleared.", + "_links": { + "self": { + "href": "https://cloud.acquia.com/api/environments/12-d314739e-296f-11e9-b210-d663bd873d93/domains/example.com/actions/clear-caches" + }, + "notification": { + "href": "https://cloud.acquia.com/api/notifications/1bd3487e-71d1-4fca-a2d9-5f969b3d35c1" + } + } +} +EOT, ], ]; } - public function testTaskWaitCommandWithStandardInput(): void { - $taskResponse = $this->getMockResponseFromSpec('/environments/{environmentId}/domains/{domain}/actions/clear-caches', 'post', 202); - $this->mockNotificationResponseFromObject($taskResponse->{'Clearing cache'}->value); - $json = json_encode($taskResponse->{'Clearing cache'}->value); - $this->executeCommand(['notification-uuid' => $json]); + public function testTaskWaitCommandWithEmptyJson(): void { + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage('Notification format is not one of UUID, JSON response, or URL'); + $this->executeCommand(['notification-uuid' => '{}']); // Assert. $this->prophet->checkPredictions(); } - public function testTaskWaitCommandWithInvalidInput(): void { + public function testTaskWaitCommandWithInvalidUrl(): void { $this->expectException(AcquiaCliException::class); - $this->executeCommand(['notification-uuid' => '{}']); + $this->expectExceptionMessage('Notification format is not one of UUID, JSON response, or URL'); + $this->executeCommand(['notification-uuid' => 'https://cloud.acquia.com/api/notifications/foo']); // Assert. $this->prophet->checkPredictions(); } + /** + * @dataProvider providerTestTaskWaitCommandWithInvalidJson + */ + public function testTaskWaitCommandWithInvalidJson(string $notification): void { + $this->expectException(AcquiaCliException::class); + $this->executeCommand([ + 'notification-uuid' => $notification, + ]); + } + + /** + * @return string[] + */ + public function providerTestTaskWaitCommandWithInvalidJson(): array { + return [ + [ + <<<'EOT' +{ + "message": "Caches are being cleared.", + "_links": { + "self": { + "href": "https://cloud.acquia.com/api/environments/12-d314739e-296f-11e9-b210-d663bd873d93/domains/example.com/actions/clear-caches", + "invalid": { + "too-deep": "5" + } + }, + "notification": { + "href": "https://cloud.acquia.com/api/notifications/1bd3487e-71d1-4fca-a2d9-5f969b3d35c1" + } + } +} +EOT, + ], + [ + <<<'EOT' +{ + "message": "Caches are being cleared.", + "_links": { + "self": { + "href": "https://cloud.acquia.com/api/environments/12-d314739e-296f-11e9-b210-d663bd873d93/domains/example.com/actions/clear-caches" + } + } +} +EOT, + ], + ]; + } + }