diff --git a/composer.json b/composer.json index b49eff4c4..9479abfbb 100644 --- a/composer.json +++ b/composer.json @@ -10,6 +10,11 @@ "role": "Maintainer" } ], + "repositories": [{ + "type": "path", + "url": "../coding-standards-php" + } + ], "minimum-stability": "dev", "require": { "php": "^8.1", @@ -53,7 +58,7 @@ "zumba/amplitude-php": "^1.0.4" }, "require-dev": { - "acquia/coding-standards": "^2", + "acquia/coding-standards": "*", "brianium/paratest": "^6.6", "dealerdirect/phpcodesniffer-composer-installer": "^1.0.0", "dominikb/composer-license-checker": "^2.4", diff --git a/composer.lock b/composer.lock index 39c1d97dd..36c7ab369 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ab4ff60bae8f6336db700b8e95faf26f", + "content-hash": "af855bbe270547764bcca9fce1344172", "packages": [ { "name": "acquia/drupal-environment-detector", @@ -6501,17 +6501,11 @@ "packages-dev": [ { "name": "acquia/coding-standards", - "version": "v2.0.0", - "source": { - "type": "git", - "url": "https://github.com/acquia/coding-standards-php.git", - "reference": "bb52b780a00ca7586e8b2e502e09dc53c5e90a60" - }, + "version": "3.x-dev", "dist": { - "type": "zip", - "url": "https://api.github.com/repos/acquia/coding-standards-php/zipball/bb52b780a00ca7586e8b2e502e09dc53c5e90a60", - "reference": "bb52b780a00ca7586e8b2e502e09dc53c5e90a60", - "shasum": "" + "type": "path", + "url": "../coding-standards-php", + "reference": "b4bef3bc0af6fcbfcd087a5a194e55116709932e" }, "require": { "drupal/coder": "^8.3", @@ -6534,9 +6528,8 @@ "Acquia\\CodingStandards\\": "src/" } }, - "notification-url": "https://packagist.org/downloads/", "license": [ - "GPL-2.0-only" + "GPL-2.0-or-later" ], "authors": [ { @@ -6547,7 +6540,7 @@ ], "description": "PHP_CodeSniffer rules (sniffs) for Acquia coding standards", "keywords": [ - "drupal", + "Drupal", "phpcs", "standards", "static analysis" @@ -6556,7 +6549,9 @@ "issues": "https://github.com/acquia/coding-standards/issues", "source": "https://github.com/acquia/coding-standards" }, - "time": "2023-03-06T17:49:20+00:00" + "transport-options": { + "relative": true + } }, { "name": "amphp/amp", diff --git a/phpcs.xml.dist b/phpcs.xml.dist index d80a25759..9244f7f39 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -22,37 +22,7 @@ tests/fixtures/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -62,4 +32,5 @@ + diff --git a/src/AcsfApi/AcsfClient.php b/src/AcsfApi/AcsfClient.php index afeccd501..63a94eeb0 100644 --- a/src/AcsfApi/AcsfClient.php +++ b/src/AcsfApi/AcsfClient.php @@ -1,6 +1,6 @@ getBody(); - $body = json_decode((string) $bodyJson, FALSE, 512, JSON_THROW_ON_ERROR); - - // ACSF sometimes returns an array rather than an object. - if (is_array($body)) { - return $body; +class AcsfClient extends Client +{ + public function processResponse(ResponseInterface $response): mixed + { + $bodyJson = $response->getBody(); + $body = json_decode((string) $bodyJson, false, 512, JSON_THROW_ON_ERROR); + + // ACSF sometimes returns an array rather than an object. + if (is_array($body)) { + return $body; + } + + if (property_exists($body, '_embedded') && property_exists($body->_embedded, 'items')) { + return $body->_embedded->items; + } + + if (property_exists($body, 'error') && property_exists($body, 'message')) { + throw new ApiErrorException($body); + } + // Throw error for 4xx and 5xx responses. + if (property_exists($body, 'message') && in_array(substr((string) $response->getStatusCode(), 0, 1), ['4', '5'], true)) { + $body->error = $response->getStatusCode(); + throw new ApiErrorException($body); + } + + return $body; } - - if (property_exists($body, '_embedded') && property_exists($body->_embedded, 'items')) { - return $body->_embedded->items; - } - - if (property_exists($body, 'error') && property_exists($body, 'message')) { - throw new ApiErrorException($body); - } - // Throw error for 4xx and 5xx responses. - if (property_exists($body, 'message') && in_array(substr((string) $response->getStatusCode(), 0, 1), ['4', '5'], TRUE)) { - $body->error = $response->getStatusCode(); - throw new ApiErrorException($body); - } - - return $body; - } - } diff --git a/src/AcsfApi/AcsfClientService.php b/src/AcsfApi/AcsfClientService.php index 78ecfc587..ae78602cd 100644 --- a/src/AcsfApi/AcsfClientService.php +++ b/src/AcsfApi/AcsfClientService.php @@ -1,27 +1,29 @@ connector); - $this->configureClient($client); - - return $client; - } - - protected function checkAuthentication(): bool { - return ($this->credentials->getCloudKey() && $this->credentials->getCloudSecret()); - } - +class AcsfClientService extends ClientService +{ + public function __construct(AcsfConnectorFactory $connectorFactory, Application $application, AcsfCredentials $cloudCredentials) + { + parent::__construct($connectorFactory, $application, $cloudCredentials); + } + + public function getClient(): AcsfClient + { + $client = AcsfClient::factory($this->connector); + $this->configureClient($client); + + return $client; + } + + protected function checkAuthentication(): bool + { + return ($this->credentials->getCloudKey() && $this->credentials->getCloudSecret()); + } } diff --git a/src/AcsfApi/AcsfConnector.php b/src/AcsfApi/AcsfConnector.php index 5c6d0c80d..24dc8778e 100644 --- a/src/AcsfApi/AcsfConnector.php +++ b/src/AcsfApi/AcsfConnector.php @@ -1,6 +1,6 @@ $config - * @param string|null $baseUri - * @param string|null $urlAccessToken - */ - public function __construct(array $config, string $baseUri = NULL, string $urlAccessToken = NULL) { - parent::__construct($config, $baseUri, $urlAccessToken); - - $this->client = new GuzzleClient([ - 'auth' => [ +class AcsfConnector extends Connector +{ + /** + * @param array $config + * @param string|null $baseUri + * @param string|null $urlAccessToken + */ + public function __construct(array $config, string $baseUri = null, string $urlAccessToken = null) + { + parent::__construct($config, $baseUri, $urlAccessToken); + + $this->client = new GuzzleClient([ + 'auth' => [ $config['key'], $config['secret'], - ], - 'base_uri' => $this->getBaseUri(), - ]); - } - - /** - * @param array $options - */ - public function sendRequest(string $verb, string $path, array $options): ResponseInterface { - return $this->client->request($verb, $path, $options); - } - + ], + 'base_uri' => $this->getBaseUri(), + ]); + } + + /** + * @param array $options + */ + public function sendRequest(string $verb, string $path, array $options): ResponseInterface + { + return $this->client->request($verb, $path, $options); + } } diff --git a/src/AcsfApi/AcsfConnectorFactory.php b/src/AcsfApi/AcsfConnectorFactory.php index 4a1228fa9..186ec5f20 100644 --- a/src/AcsfApi/AcsfConnectorFactory.php +++ b/src/AcsfApi/AcsfConnectorFactory.php @@ -1,21 +1,22 @@ $config - */ - public function __construct(protected array $config, protected ?string $baseUri = NULL) { - } - - public function createConnector(): AcsfConnector { - return new AcsfConnector($this->config, $this->baseUri); - } +class AcsfConnectorFactory implements ConnectorFactoryInterface +{ + /** + * @param array $config + */ + public function __construct(protected array $config, protected ?string $baseUri = null) + { + } + public function createConnector(): AcsfConnector + { + return new AcsfConnector($this->config, $this->baseUri); + } } diff --git a/src/AcsfApi/AcsfCredentials.php b/src/AcsfApi/AcsfCredentials.php index 6fa966ddc..971e98bdf 100644 --- a/src/AcsfApi/AcsfCredentials.php +++ b/src/AcsfApi/AcsfCredentials.php @@ -1,74 +1,79 @@ getCurrentFactory()) && $activeUser = $this->getFactoryActiveUser($currentFactory)) { - return $activeUser['username']; - } + public function getCloudKey(): ?string + { + if (getenv('ACSF_USERNAME')) { + return getenv('ACSF_USERNAME'); + } - return NULL; - } + if (($currentFactory = $this->getCurrentFactory()) && $activeUser = $this->getFactoryActiveUser($currentFactory)) { + return $activeUser['username']; + } - /** - * @param array $factory - */ - public function getFactoryActiveUser(array $factory): mixed { - if (array_key_exists('active_user', $factory)) { - $activeUser = $factory['active_user']; - if (array_key_exists($activeUser, $factory['users'])) { - return $factory['users'][$activeUser]; - } + return null; } - return NULL; - } - - private function getCurrentFactory(): mixed { - if (($factory = $this->datastoreCloud->get('acsf_active_factory')) && ($acsfFactories = $this->datastoreCloud->get('acsf_factories')) && array_key_exists($factory, $acsfFactories)) { - return $acsfFactories[$factory]; + /** + * @param array $factory + */ + public function getFactoryActiveUser(array $factory): mixed + { + if (array_key_exists('active_user', $factory)) { + $activeUser = $factory['active_user']; + if (array_key_exists($activeUser, $factory['users'])) { + return $factory['users'][$activeUser]; + } + } + + return null; } - return NULL; - } - public function getCloudSecret(): ?string { - if (getenv('ACSF_KEY')) { - return getenv('ACSF_KEY'); + private function getCurrentFactory(): mixed + { + if (($factory = $this->datastoreCloud->get('acsf_active_factory')) && ($acsfFactories = $this->datastoreCloud->get('acsf_factories')) && array_key_exists($factory, $acsfFactories)) { + return $acsfFactories[$factory]; + } + return null; } - if (($currentFactory = $this->getCurrentFactory()) && $activeUser = $this->getFactoryActiveUser($currentFactory)) { - return $activeUser['key']; - } + public function getCloudSecret(): ?string + { + if (getenv('ACSF_KEY')) { + return getenv('ACSF_KEY'); + } - return NULL; - } + if (($currentFactory = $this->getCurrentFactory()) && $activeUser = $this->getFactoryActiveUser($currentFactory)) { + return $activeUser['key']; + } - public function getBaseUri(): ?string { - if (getenv('ACSF_FACTORY_URI')) { - return getenv('ACSF_FACTORY_URI'); - } - if ($factory = $this->datastoreCloud->get('acsf_active_factory')) { - return $factory; + return null; } - return NULL; - } + public function getBaseUri(): ?string + { + if (getenv('ACSF_FACTORY_URI')) { + return getenv('ACSF_FACTORY_URI'); + } + if ($factory = $this->datastoreCloud->get('acsf_active_factory')) { + return $factory; + } + return null; + } } diff --git a/src/ApiCredentialsInterface.php b/src/ApiCredentialsInterface.php index bddeb5caf..1224e1db0 100644 --- a/src/ApiCredentialsInterface.php +++ b/src/ApiCredentialsInterface.php @@ -1,15 +1,14 @@ helpMessages; - } - - public function setHelpMessages(array $helpMessages): void { - $this->helpMessages = $helpMessages; - } - - public function renderThrowable( - Throwable $e, - OutputInterface $output - ): void { - parent::renderThrowable($e, $output); - - if ($this->getHelpMessages()) { - $io = new SymfonyStyle(new ArrayInput([]), $output); - $outputStyle = new OutputFormatterStyle('white', 'blue'); - $output->getFormatter()->setStyle('help', $outputStyle); - $io->block($this->getHelpMessages(), 'help', 'help', ' ', TRUE, FALSE); +class Application extends \Symfony\Component\Console\Application +{ + /** + * @var string[] + */ + protected array $helpMessages = []; + + /** + * @return string[] + */ + private function getHelpMessages(): array + { + return $this->helpMessages; } - } + public function setHelpMessages(array $helpMessages): void + { + $this->helpMessages = $helpMessages; + } + + public function renderThrowable( + Throwable $e, + OutputInterface $output + ): void { + parent::renderThrowable($e, $output); + + if ($this->getHelpMessages()) { + $io = new SymfonyStyle(new ArrayInput([]), $output); + $outputStyle = new OutputFormatterStyle('white', 'blue'); + $output->getFormatter()->setStyle('help', $outputStyle); + $io->block($this->getHelpMessages(), 'help', 'help', ' ', true, false); + } + } } diff --git a/src/Attribute/RequireAuth.php b/src/Attribute/RequireAuth.php index af6ca0c80..e28cc8d47 100644 --- a/src/Attribute/RequireAuth.php +++ b/src/Attribute/RequireAuth.php @@ -1,6 +1,6 @@ $config - */ - public function __construct(array $config, string $baseUri = NULL, string $urlAccessToken = NULL) { - $this->accessToken = new AccessToken(['access_token' => $config['access_token']]); - parent::__construct($config, $baseUri, $urlAccessToken); - } - - public function createRequest(string $verb, string $path): RequestInterface { - if ($file = getenv('ACLI_ACCESS_TOKEN_FILE')) { - if (!file_exists($file)) { - throw new AcquiaCliException('Access token file not found at {file}', ['file' => $file]); - } - $this->accessToken = new AccessToken(['access_token' => trim(file_get_contents($file), "\"\n")]); +class AccessTokenConnector extends Connector +{ + /** + * @var \League\OAuth2\Client\Provider\GenericProvider + */ + protected AbstractProvider $provider; + + /** + * @param array $config + */ + public function __construct(array $config, string $baseUri = null, string $urlAccessToken = null) + { + $this->accessToken = new AccessToken(['access_token' => $config['access_token']]); + parent::__construct($config, $baseUri, $urlAccessToken); } - return $this->provider->getAuthenticatedRequest( - $verb, - $this->getBaseUri() . $path, - $this->accessToken - ); - } - - public function setProvider( - GenericProvider $provider - ): void { - $this->provider = $provider; - } - - public function getAccessToken(): AccessToken { - return $this->accessToken; - } + public function createRequest(string $verb, string $path): RequestInterface + { + if ($file = getenv('ACLI_ACCESS_TOKEN_FILE')) { + if (!file_exists($file)) { + throw new AcquiaCliException('Access token file not found at {file}', ['file' => $file]); + } + $this->accessToken = new AccessToken(['access_token' => trim(file_get_contents($file), "\"\n")]); + } + return $this->provider->getAuthenticatedRequest( + $verb, + $this->getBaseUri() . $path, + $this->accessToken + ); + } + + public function setProvider( + GenericProvider $provider + ): void { + $this->provider = $provider; + } + + public function getAccessToken(): AccessToken + { + return $this->accessToken; + } } diff --git a/src/CloudApi/ClientService.php b/src/CloudApi/ClientService.php index 96afa2ad8..95acecea6 100644 --- a/src/CloudApi/ClientService.php +++ b/src/CloudApi/ClientService.php @@ -1,6 +1,6 @@ connectorFactory = $connectorFactory; - $this->setConnector($connectorFactory->createConnector()); - $this->setApplication($application); - } - - public function setConnector(ConnectorInterface $connector): void { - $this->connector = $connector; - } + public function __construct(ConnectorFactoryInterface $connectorFactory, Application $application, protected ApiCredentialsInterface $credentials) + { + $this->connectorFactory = $connectorFactory; + $this->setConnector($connectorFactory->createConnector()); + $this->setApplication($application); + } - private function setApplication(Application $application): void { - $this->application = $application; - } + public function setConnector(ConnectorInterface $connector): void + { + $this->connector = $connector; + } - public function getClient(): Client { - $client = Client::factory($this->connector); - $this->configureClient($client); + private function setApplication(Application $application): void + { + $this->application = $application; + } - return $client; - } + public function getClient(): Client + { + $client = Client::factory($this->connector); + $this->configureClient($client); - protected function configureClient(Client $client): void { - $userAgent = sprintf("acli/%s", $this->application->getVersion()); - $customHeaders = [ - 'User-Agent' => [$userAgent], - ]; - if ($uuid = getenv("REMOTEIDE_UUID")) { - $customHeaders['X-Cloud-IDE-UUID'] = $uuid; + return $client; } - $client->addOption('headers', $customHeaders); - } - public function isMachineAuthenticated(): bool { - if ($this->machineIsAuthenticated !== NULL) { - return $this->machineIsAuthenticated; + protected function configureClient(Client $client): void + { + $userAgent = sprintf("acli/%s", $this->application->getVersion()); + $customHeaders = [ + 'User-Agent' => [$userAgent], + ]; + if ($uuid = getenv("REMOTEIDE_UUID")) { + $customHeaders['X-Cloud-IDE-UUID'] = $uuid; + } + $client->addOption('headers', $customHeaders); } - $this->machineIsAuthenticated = $this->checkAuthentication(); - return $this->machineIsAuthenticated; - } - protected function checkAuthentication(): bool { - return ( - $this->credentials->getCloudAccessToken() || - ($this->credentials->getCloudKey() && $this->credentials->getCloudSecret()) - ); - } + public function isMachineAuthenticated(): bool + { + if ($this->machineIsAuthenticated !== null) { + return $this->machineIsAuthenticated; + } + $this->machineIsAuthenticated = $this->checkAuthentication(); + return $this->machineIsAuthenticated; + } + protected function checkAuthentication(): bool + { + return ( + $this->credentials->getCloudAccessToken() || + ($this->credentials->getCloudKey() && $this->credentials->getCloudSecret()) + ); + } } diff --git a/src/CloudApi/CloudCredentials.php b/src/CloudApi/CloudCredentials.php index 81b89593d..98ca77847 100644 --- a/src/CloudApi/CloudCredentials.php +++ b/src/CloudApi/CloudCredentials.php @@ -1,6 +1,6 @@ $file]); - } - return trim(file_get_contents($file), "\"\n"); - } + public function getCloudAccessToken(): ?string + { + if ($token = getenv('ACLI_ACCESS_TOKEN')) { + return $token; + } - return NULL; - } + if ($file = getenv('ACLI_ACCESS_TOKEN_FILE')) { + if (!file_exists($file)) { + throw new AcquiaCliException('Access token file not found at {file}', ['file' => $file]); + } + return trim(file_get_contents($file), "\"\n"); + } - public function getCloudAccessTokenExpiry(): ?string { - if ($token = getenv('ACLI_ACCESS_TOKEN_EXPIRY')) { - return $token; + return null; } - if ($file = getenv('ACLI_ACCESS_TOKEN_EXPIRY_FILE')) { - if (!file_exists($file)) { - throw new AcquiaCliException('Access token expiry file not found at {file}', ['file' => $file]); - } - return trim(file_get_contents($file), "\"\n"); - } + public function getCloudAccessTokenExpiry(): ?string + { + if ($token = getenv('ACLI_ACCESS_TOKEN_EXPIRY')) { + return $token; + } - return NULL; - } + if ($file = getenv('ACLI_ACCESS_TOKEN_EXPIRY_FILE')) { + if (!file_exists($file)) { + throw new AcquiaCliException('Access token expiry file not found at {file}', ['file' => $file]); + } + return trim(file_get_contents($file), "\"\n"); + } - public function getCloudKey(): ?string { - if ($key = getenv('ACLI_KEY')) { - return $key; + return null; } - if ($this->datastoreCloud->get('acli_key')) { - return $this->datastoreCloud->get('acli_key'); - } + public function getCloudKey(): ?string + { + if ($key = getenv('ACLI_KEY')) { + return $key; + } - return NULL; - } + if ($this->datastoreCloud->get('acli_key')) { + return $this->datastoreCloud->get('acli_key'); + } - public function getCloudSecret(): ?string { - if ($secret = getenv('ACLI_SECRET')) { - return $secret; + return null; } - $acliKey = $this->getCloudKey(); - if ($this->datastoreCloud->get('keys')) { - $keys = $this->datastoreCloud->get('keys'); - if (is_array($keys) && array_key_exists($acliKey, $keys)) { - return $this->datastoreCloud->get('keys')[$acliKey]['secret']; - } + public function getCloudSecret(): ?string + { + if ($secret = getenv('ACLI_SECRET')) { + return $secret; + } + + $acliKey = $this->getCloudKey(); + if ($this->datastoreCloud->get('keys')) { + $keys = $this->datastoreCloud->get('keys'); + if (is_array($keys) && array_key_exists($acliKey, $keys)) { + return $this->datastoreCloud->get('keys')[$acliKey]['secret']; + } + } + + return null; } - return NULL; - } - - public function getBaseUri(): ?string { - if ($uri = getenv('ACLI_CLOUD_API_BASE_URI')) { - return $uri; + public function getBaseUri(): ?string + { + if ($uri = getenv('ACLI_CLOUD_API_BASE_URI')) { + return $uri; + } + return null; } - return NULL; - } - public function getAccountsUri(): ?string { - if ($uri = getenv('ACLI_CLOUD_API_ACCOUNTS_URI')) { - return $uri; + public function getAccountsUri(): ?string + { + if ($uri = getenv('ACLI_CLOUD_API_ACCOUNTS_URI')) { + return $uri; + } + return null; } - return NULL; - } - } diff --git a/src/CloudApi/ConnectorFactory.php b/src/CloudApi/ConnectorFactory.php index d42ebce69..f8fd82488 100644 --- a/src/CloudApi/ConnectorFactory.php +++ b/src/CloudApi/ConnectorFactory.php @@ -1,6 +1,6 @@ $config - */ - public function __construct(protected array $config, protected ?string $baseUri = NULL, protected ?string $accountsUri = NULL) { - } - - /** - * @return \Acquia\Cli\CloudApi\AccessTokenConnector|\AcquiaCloudApi\Connector\Connector - */ - public function createConnector(): Connector|AccessTokenConnector { - // A defined key & secret takes priority. - if ($this->config['key'] && $this->config['secret']) { - return new Connector($this->config, $this->baseUri, $this->accountsUri); +class ConnectorFactory implements ConnectorFactoryInterface +{ + /** + * @param array $config + */ + public function __construct(protected array $config, protected ?string $baseUri = null, protected ?string $accountsUri = null) + { } - // Fall back to a valid access token. - if ($this->config['accessToken']) { - $accessToken = $this->createAccessToken(); - if (!$accessToken->hasExpired()) { - // @todo Add debug log entry indicating that access token is being used. - return new AccessTokenConnector([ - 'access_token' => $accessToken, - 'key' => NULL, - 'secret' => NULL, - ], $this->baseUri, $this->accountsUri); - } + /** + * @return \Acquia\Cli\CloudApi\AccessTokenConnector|\AcquiaCloudApi\Connector\Connector + */ + public function createConnector(): Connector|AccessTokenConnector + { + // A defined key & secret takes priority. + if ($this->config['key'] && $this->config['secret']) { + return new Connector($this->config, $this->baseUri, $this->accountsUri); + } + + // Fall back to a valid access token. + if ($this->config['accessToken']) { + $accessToken = $this->createAccessToken(); + if (!$accessToken->hasExpired()) { + // @todo Add debug log entry indicating that access token is being used. + return new AccessTokenConnector([ + 'access_token' => $accessToken, + 'key' => null, + 'secret' => null, + ], $this->baseUri, $this->accountsUri); + } + } + + // Fall back to an unauthenticated request. + return new Connector($this->config, $this->baseUri, $this->accountsUri); } - // Fall back to an unauthenticated request. - return new Connector($this->config, $this->baseUri, $this->accountsUri); - } - - private function createAccessToken(): AccessToken { - return new AccessToken([ - 'access_token' => $this->config['accessToken'], - 'expires' => $this->config['accessTokenExpiry'], - ]); - } - + private function createAccessToken(): AccessToken + { + return new AccessToken([ + 'access_token' => $this->config['accessToken'], + 'expires' => $this->config['accessTokenExpiry'], + ]); + } } diff --git a/src/Command/Acsf/AcsfCommandFactory.php b/src/Command/Acsf/AcsfCommandFactory.php index 510f40d56..9031d4590 100644 --- a/src/Command/Acsf/AcsfCommandFactory.php +++ b/src/Command/Acsf/AcsfCommandFactory.php @@ -1,6 +1,6 @@ localMachineHelper, - $this->datastoreCloud, - $this->datastoreAcli, - $this->cloudCredentials, - $this->telemetryHelper, - $this->projectDir, - $this->cloudApiClientService, - $this->sshHelper, - $this->sshDir, - $this->logger, - ); - } - - public function createListCommand(): AcsfListCommand { - return new AcsfListCommand( - $this->localMachineHelper, - $this->datastoreCloud, - $this->datastoreAcli, - $this->cloudCredentials, - $this->telemetryHelper, - $this->projectDir, - $this->cloudApiClientService, - $this->sshHelper, - $this->sshDir, - $this->logger, - ); - } + public function createCommand(): ApiBaseCommand + { + return new ApiBaseCommand( + $this->localMachineHelper, + $this->datastoreCloud, + $this->datastoreAcli, + $this->cloudCredentials, + $this->telemetryHelper, + $this->projectDir, + $this->cloudApiClientService, + $this->sshHelper, + $this->sshDir, + $this->logger, + ); + } + public function createListCommand(): AcsfListCommand + { + return new AcsfListCommand( + $this->localMachineHelper, + $this->datastoreCloud, + $this->datastoreAcli, + $this->cloudCredentials, + $this->telemetryHelper, + $this->projectDir, + $this->cloudApiClientService, + $this->sshHelper, + $this->sshDir, + $this->logger, + ); + } } diff --git a/src/Command/Acsf/AcsfListCommand.php b/src/Command/Acsf/AcsfListCommand.php index 7262d17f8..69cc91379 100644 --- a/src/Command/Acsf/AcsfListCommand.php +++ b/src/Command/Acsf/AcsfListCommand.php @@ -1,6 +1,6 @@ namespace = $namespace; - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $commands = $this->getApplication()->all(); - foreach ($commands as $command) { - if ($command->getName() !== $this->namespace - // E.g., if the namespace is acsf:api, show all acsf:api:* commands. - && str_contains($command->getName(), $this->namespace . ':') - // This is a lazy way to exclude api:base and acsf:base. - && $command->getDescription() - ) { - $command->setHidden(FALSE); - } - else { - $command->setHidden(); - } - } - - $command = $this->getApplication()->find('list'); - $arguments = [ - 'command' => 'list', - 'namespace' => 'acsf', - ]; - $listInput = new ArrayInput($arguments); +class AcsfListCommandBase extends CommandBase +{ + protected string $namespace; - return $command->run($listInput, $output); - } + public function setNamespace(string $namespace): void + { + $this->namespace = $namespace; + } + protected function execute(InputInterface $input, OutputInterface $output): int + { + $commands = $this->getApplication()->all(); + foreach ($commands as $command) { + if ( + $command->getName() !== $this->namespace + // E.g., if the namespace is acsf:api, show all acsf:api:* commands. + && str_contains($command->getName(), $this->namespace . ':') + // This is a lazy way to exclude api:base and acsf:base. + && $command->getDescription() + ) { + $command->setHidden(false); + } else { + $command->setHidden(); + } + } + + $command = $this->getApplication()->find('list'); + $arguments = [ + 'command' => 'list', + 'namespace' => 'acsf', + ]; + $listInput = new ArrayInput($arguments); + + return $command->run($listInput, $output); + } } diff --git a/src/Command/Api/ApiBaseCommand.php b/src/Command/Api/ApiBaseCommand.php index 1a9a743e6..6f6bc3545 100644 --- a/src/Command/Api/ApiBaseCommand.php +++ b/src/Command/Api/ApiBaseCommand.php @@ -1,6 +1,6 @@ - */ - protected array $responses; - - /** - * @var array - */ - protected array $servers; - - protected string $path; - - /** - * @var array - */ - private array $queryParams = []; - - /** - * @var array - */ - private array $postParams = []; - - /** - * @var array - */ - private array $pathParams = []; - - protected function interact(InputInterface $input, OutputInterface $output): void { - $params = array_merge($this->queryParams, $this->postParams, $this->pathParams); - foreach ($this->getDefinition()->getArguments() as $argument) { - if ($argument->isRequired() && !$input->getArgument($argument->getName())) { - $this->io->note([ - "{$argument->getName()} is a required argument.", - $argument->getDescription(), - ]); - // Choice question. - if (array_key_exists($argument->getName(), $params) - && array_key_exists('schema', $params[$argument->getName()]) - && array_key_exists('enum', $params[$argument->getName()]['schema'])) { - $choices = $params[$argument->getName()]['schema']['enum']; - $answer = $this->io->choice("Select a value for {$argument->getName()}", $choices, $argument->getDefault()); +#[AsCommand(name: 'api:base', hidden: true)] +class ApiBaseCommand extends CommandBase +{ + protected string $method; + + /** + * @var array + */ + protected array $responses; + + /** + * @var array + */ + protected array $servers; + + protected string $path; + + /** + * @var array + */ + private array $queryParams = []; + + /** + * @var array + */ + private array $postParams = []; + + /** + * @var array + */ + private array $pathParams = []; + + protected function interact(InputInterface $input, OutputInterface $output): void + { + $params = array_merge($this->queryParams, $this->postParams, $this->pathParams); + foreach ($this->getDefinition()->getArguments() as $argument) { + if ($argument->isRequired() && !$input->getArgument($argument->getName())) { + $this->io->note([ + "{$argument->getName()} is a required argument.", + $argument->getDescription(), + ]); + // Choice question. + if ( + array_key_exists($argument->getName(), $params) + && array_key_exists('schema', $params[$argument->getName()]) + && array_key_exists('enum', $params[$argument->getName()]['schema']) + ) { + $choices = $params[$argument->getName()]['schema']['enum']; + $answer = $this->io->choice("Select a value for {$argument->getName()}", $choices, $argument->getDefault()); + } elseif ( + array_key_exists($argument->getName(), $params) + && array_key_exists('type', $params[$argument->getName()]) + && $params[$argument->getName()]['type'] === 'boolean' + ) { + $answer = $this->io->choice("Select a value for {$argument->getName()}", ['false', 'true'], $argument->getDefault()); + $answer = $answer === 'true'; + } + // Free form. + else { + $answer = $this->askFreeFormQuestion($argument, $params); + } + $input->setArgument($argument->getName(), $answer); + } } - elseif (array_key_exists($argument->getName(), $params) - && array_key_exists('type', $params[$argument->getName()]) - && $params[$argument->getName()]['type'] === 'boolean') { - $answer = $this->io->choice("Select a value for {$argument->getName()}", ['false', 'true'], $argument->getDefault()); - $answer = $answer === 'true'; + parent::interact($input, $output); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + if ($this->getName() === 'api:base') { + throw new AcquiaCliException('api:base is not a valid command'); } - // Free form. - else { - $answer = $this->askFreeFormQuestion($argument, $params); + // Build query from non-null options. + $acquiaCloudClient = $this->cloudApiClientService->getClient(); + $this->addQueryParamsToClient($input, $acquiaCloudClient); + $this->addPostParamsToClient($input, $acquiaCloudClient); + // 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', [ + 'Accept' => 'application/hal+json, version=2', + ]); + + try { + if ($this->output->isVeryVerbose()) { + $acquiaCloudClient->addOption('debug', $this->output); + } + $path = $this->getRequestPath($input); + $response = $acquiaCloudClient->request($this->method, $path); + $exitCode = 0; } - $input->setArgument($argument->getName(), $answer); - } - } - parent::interact($input, $output); - } + // Ignore PhpStorm warning here. + // @see https://youtrack.jetbrains.com/issue/WI-77190/Exception-is-never-thrown-when-thrown-from-submethod + catch (ApiErrorException $exception) { + $response = $exception->getResponseBody(); + $exitCode = 1; + } + + $contents = json_encode($response, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT); + $this->output->writeln($contents); - protected function execute(InputInterface $input, OutputInterface $output): int { - if ($this->getName() === 'api:base') { - throw new AcquiaCliException('api:base is not a valid command'); + return $exitCode; } - // Build query from non-null options. - $acquiaCloudClient = $this->cloudApiClientService->getClient(); - $this->addQueryParamsToClient($input, $acquiaCloudClient); - $this->addPostParamsToClient($input, $acquiaCloudClient); - // 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', [ - 'Accept' => 'application/hal+json, version=2', - ]); - - try { - if ($this->output->isVeryVerbose()) { - $acquiaCloudClient->addOption('debug', $this->output); - } - $path = $this->getRequestPath($input); - $response = $acquiaCloudClient->request($this->method, $path); - $exitCode = 0; + + public function setMethod(string $method): void + { + $this->method = $method; } - // Ignore PhpStorm warning here. - // @see https://youtrack.jetbrains.com/issue/WI-77190/Exception-is-never-thrown-when-thrown-from-submethod - catch (ApiErrorException $exception) { - $response = $exception->getResponseBody(); - $exitCode = 1; + + public function setResponses(array $responses): void + { + $this->responses = $responses; } - $contents = json_encode($response, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT); - $this->output->writeln($contents); + public function setServers(array $servers): void + { + $this->servers = $servers; + } - return $exitCode; - } + public function setPath(string $path): void + { + $this->path = $path; + } - public function setMethod(string $method): void { - $this->method = $method; - } + protected function getRequestPath(InputInterface $input): string + { + $path = $this->path; + + $arguments = $input->getArguments(); + // The command itself is the first argument. Remove it. + array_shift($arguments); + foreach ($arguments as $key => $value) { + $token = '{' . $key . '}'; + if (str_contains($path, $token)) { + $path = str_replace($token, $value, $path); + } + } - public function setResponses(array $responses): void { - $this->responses = $responses; - } + return $path; + } - public function setServers(array $servers): void { - $this->servers = $servers; - } + public function getMethod(): string + { + return $this->method; + } - public function setPath(string $path): void { - $this->path = $path; - } + public function addPostParameter(string $paramName, mixed $value): void + { + $this->postParams[$paramName] = $value; + } - protected function getRequestPath(InputInterface $input): string { - $path = $this->path; + public function addQueryParameter(string $paramName, mixed $value): void + { + $this->queryParams[$paramName] = $value; + } - $arguments = $input->getArguments(); - // The command itself is the first argument. Remove it. - array_shift($arguments); - foreach ($arguments as $key => $value) { - $token = '{' . $key . '}'; - if (str_contains($path, $token)) { - $path = str_replace($token, $value, $path); - } + public function getPath(): string + { + return $this->path; } - return $path; - } + public function addPathParameter(string $paramName, mixed $value): void + { + $this->pathParams[$paramName] = $value; + } - public function getMethod(): string { - return $this->method; - } + private function getParamFromInput(InputInterface $input, string $paramName): array|bool|string|int|null + { + if ($input->hasArgument($paramName)) { + return $input->getArgument($paramName); + } - public function addPostParameter(string $paramName, mixed $value): void { - $this->postParams[$paramName] = $value; - } + if ($input->hasParameterOption('--' . $paramName)) { + return $input->getOption($paramName); + } + return null; + } - public function addQueryParameter(string $paramName, mixed $value): void { - $this->queryParams[$paramName] = $value; - } + private function castParamType(array $paramSpec, array|string|bool|int $value): array|bool|int|string|object + { + $oneOf = $this->getParamTypeOneOf($paramSpec); + if (isset($oneOf)) { + $types = []; + foreach ($oneOf as $type) { + if ($type['type'] === 'array' && str_contains($value, ',')) { + return $this->castParamToArray($type, $value); + } + $types[] = $type['type']; + } + if (in_array('integer', $types, true) && ctype_digit($value)) { + return $this->doCastParamType('integer', $value); + } + } elseif ($paramSpec['type'] === 'array') { + if (is_array($value) && count($value) === 1) { + return $this->castParamToArray($paramSpec, $value[0]); + } + + return $this->castParamToArray($paramSpec, $value); + } - public function getPath(): string { - return $this->path; - } + $type = $this->getParamType($paramSpec); + if (!$type) { + return $value; + } - public function addPathParameter(string $paramName, mixed $value): void { - $this->pathParams[$paramName] = $value; - } + return $this->doCastParamType($type, $value); + } - private function getParamFromInput(InputInterface $input, string $paramName): array|bool|string|int|null { - if ($input->hasArgument($paramName)) { - return $input->getArgument($paramName); + private function doCastParamType(string $type, mixed $value): array|bool|int|string|object + { + return match ($type) { + 'integer' => (int) $value, + 'boolean' => $this->castBool($value), + 'array' => is_string($value) ? explode(',', $value) : (array) $value, + 'string' => (string) $value, + 'object' => json_decode($value, false, 512, JSON_THROW_ON_ERROR), + }; } - if ($input->hasParameterOption('--' . $paramName)) { - return $input->getOption($paramName); + public function castBool(mixed $val): bool + { + return (bool) (is_string($val) ? filter_var($val, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) : $val); } - return NULL; - } - - private function castParamType(array $paramSpec, array|string|bool|int $value): array|bool|int|string|object { - $oneOf = $this->getParamTypeOneOf($paramSpec); - if (isset($oneOf)) { - $types = []; - foreach ($oneOf as $type) { - if ($type['type'] === 'array' && str_contains($value, ',')) { - return $this->castParamToArray($type, $value); + + private function getParamType(array $paramSpec): ?string + { + // @todo File a CXAPI ticket regarding the inconsistent nesting of the 'type' property. + if (array_key_exists('type', $paramSpec)) { + return $paramSpec['type']; } - $types[] = $type['type']; - } - if (in_array('integer', $types, TRUE) && ctype_digit($value)) { - return $this->doCastParamType('integer', $value); - } - } - elseif ($paramSpec['type'] === 'array') { - if (is_array($value) && count($value) === 1) { - return $this->castParamToArray($paramSpec, $value[0]); - } - return $this->castParamToArray($paramSpec, $value); + if (array_key_exists('schema', $paramSpec) && array_key_exists('type', $paramSpec['schema'])) { + return $paramSpec['schema']['type']; + } + return null; } - $type = $this->getParamType($paramSpec); - if (!$type) { - return $value; + private function createCallableValidator(InputArgument $argument, array $params): ?callable + { + $validator = null; + if (array_key_exists($argument->getName(), $params)) { + $paramSpec = $params[$argument->getName()]; + $constraints = [ + new NotBlank(), + ]; + if ($type = $this->getParamType($paramSpec)) { + if (in_array($type, ['int', 'integer'])) { + // Need to evaluate whether a string contains only digits. + $constraints[] = new Type('digit'); + } elseif ($type === 'array') { + $constraints[] = new Type('string'); + } else { + $constraints[] = new Type($type); + } + } + if (array_key_exists('schema', $paramSpec)) { + $schema = $paramSpec['schema']; + $constraints = $this->createLengthConstraint($schema, $constraints); + $constraints = $this->createRegexConstraint($schema, $constraints); + } + $validator = $this->createValidatorFromConstraints($constraints); + } + return $validator; } - return $this->doCastParamType($type, $value); - } - - private function doCastParamType(string $type, mixed $value): array|bool|int|string|object { - return match ($type) { - 'integer' => (int) $value, - 'boolean' => $this->castBool($value), - 'array' => is_string($value) ? explode(',', $value) : (array) $value, - 'string' => (string) $value, - 'object' => json_decode($value, FALSE, 512, JSON_THROW_ON_ERROR), - }; - } - - public function castBool(mixed $val): bool { - return (bool) (is_string($val) ? filter_var($val, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) : $val); - } - - private function getParamType(array $paramSpec): ?string { - // @todo File a CXAPI ticket regarding the inconsistent nesting of the 'type' property. - if (array_key_exists('type', $paramSpec)) { - return $paramSpec['type']; + /** + * @return array + */ + private function createLengthConstraint(array $schema, array $constraints): array + { + if (array_key_exists('minLength', $schema) || array_key_exists('maxLength', $schema)) { + $lengthOptions = []; + if (array_key_exists('minLength', $schema)) { + $lengthOptions['min'] = $schema['minLength']; + } + if (array_key_exists('maxLength', $schema)) { + $lengthOptions['max'] = $schema['maxLength']; + } + $constraints[] = new Length($lengthOptions); + } + return $constraints; } - if (array_key_exists('schema', $paramSpec) && array_key_exists('type', $paramSpec['schema'])) { - return $paramSpec['schema']['type']; - } - return NULL; - } - - private function createCallableValidator(InputArgument $argument, array $params): ?callable { - $validator = NULL; - if (array_key_exists($argument->getName(), $params)) { - $paramSpec = $params[$argument->getName()]; - $constraints = [ - new NotBlank(), - ]; - if ($type = $this->getParamType($paramSpec)) { - if (in_array($type, ['int', 'integer'])) { - // Need to evaluate whether a string contains only digits. - $constraints[] = new Type('digit'); + /** + * @return array + */ + protected function createRegexConstraint(array $schema, array $constraints): array + { + if (array_key_exists('format', $schema)) { + if ($schema['format'] === 'uuid') { + $constraints[] = CommandBase::getUuidRegexConstraint(); + } + } elseif (array_key_exists('pattern', $schema)) { + $constraints[] = new Regex([ + 'message' => 'It must match the pattern ' . $schema['pattern'], + 'pattern' => '/' . $schema['pattern'] . '/', + ]); } - elseif ($type === 'array') { - $constraints[] = new Type('string'); - } - else { - $constraints[] = new Type($type); - } - } - if (array_key_exists('schema', $paramSpec)) { - $schema = $paramSpec['schema']; - $constraints = $this->createLengthConstraint($schema, $constraints); - $constraints = $this->createRegexConstraint($schema, $constraints); - } - $validator = $this->createValidatorFromConstraints($constraints); + return $constraints; } - return $validator; - } - - /** - * @return array - */ - private function createLengthConstraint(array $schema, array $constraints): array { - if (array_key_exists('minLength', $schema) || array_key_exists('maxLength', $schema)) { - $lengthOptions = []; - if (array_key_exists('minLength', $schema)) { - $lengthOptions['min'] = $schema['minLength']; - } - if (array_key_exists('maxLength', $schema)) { - $lengthOptions['max'] = $schema['maxLength']; - } - $constraints[] = new Length($lengthOptions); + + private function createValidatorFromConstraints(array $constraints): Closure + { + return static function (mixed $value) use ($constraints) { + $violations = Validation::createValidator() + ->validate($value, $constraints); + if (count($violations)) { + throw new ValidatorException($violations->get(0)->getMessage()); + } + return $value; + }; } - return $constraints; - } - - /** - * @return array - */ - protected function createRegexConstraint(array $schema, array $constraints): array { - if (array_key_exists('format', $schema)) { - if ($schema['format'] === 'uuid') { - $constraints[] = CommandBase::getUuidRegexConstraint(); - } + + protected function addQueryParamsToClient(InputInterface $input, Client $acquiaCloudClient): void + { + if ($this->queryParams) { + foreach ($this->queryParams as $key => $paramSpec) { + // We may have a queryParam that is used in the path rather than the query string. + if ($input->hasOption($key) && $input->getOption($key) !== null) { + $acquiaCloudClient->addQuery($key, $input->getOption($key)); + } elseif ($input->hasArgument($key) && $input->getArgument($key) !== null) { + $acquiaCloudClient->addQuery($key, $input->getArgument($key)); + } + } + } } - elseif (array_key_exists('pattern', $schema)) { - $constraints[] = new Regex([ - 'message' => 'It must match the pattern ' . $schema['pattern'], - 'pattern' => '/' . $schema['pattern'] . '/', - ]); + + private function addPostParamsToClient(InputInterface $input, Client $acquiaCloudClient): void + { + if ($this->postParams) { + foreach ($this->postParams as $paramName => $paramSpec) { + $paramValue = $this->getParamFromInput($input, $paramName); + if (!is_null($paramValue)) { + $this->addPostParamToClient($paramName, $paramSpec, $paramValue, $acquiaCloudClient); + } + } + } } - return $constraints; - } - - private function createValidatorFromConstraints(array $constraints): Closure { - return static function (mixed $value) use ($constraints) { - $violations = Validation::createValidator() - ->validate($value, $constraints); - if (count($violations)) { - throw new ValidatorException($violations->get(0)->getMessage()); - } - return $value; - }; - } - - protected function addQueryParamsToClient(InputInterface $input, Client $acquiaCloudClient): void { - if ($this->queryParams) { - foreach ($this->queryParams as $key => $paramSpec) { - // We may have a queryParam that is used in the path rather than the query string. - if ($input->hasOption($key) && $input->getOption($key) !== NULL) { - $acquiaCloudClient->addQuery($key, $input->getOption($key)); + + /** + * @param array|null $paramSpec + */ + private function addPostParamToClient(string $paramName, ?array $paramSpec, mixed $paramValue, Client $acquiaCloudClient): void + { + $paramName = ApiCommandHelper::restoreRenamedParameter($paramName); + if ($paramSpec) { + $paramValue = $this->castParamType($paramSpec, $paramValue); } - elseif ($input->hasArgument($key) && $input->getArgument($key) !== NULL) { - $acquiaCloudClient->addQuery($key, $input->getArgument($key)); + if ($paramSpec && array_key_exists('format', $paramSpec) && $paramSpec["format"] === 'binary') { + $acquiaCloudClient->addOption('multipart', [ + [ + 'contents' => Utils::tryFopen($paramValue, 'r'), + 'name' => $paramName, + ], + ]); + } else { + $acquiaCloudClient->addOption('json', [$paramName => $paramValue]); } - } } - } - - private function addPostParamsToClient(InputInterface $input, Client $acquiaCloudClient): void { - if ($this->postParams) { - foreach ($this->postParams as $paramName => $paramSpec) { - $paramValue = $this->getParamFromInput($input, $paramName); - if (!is_null($paramValue)) { - $this->addPostParamToClient($paramName, $paramSpec, $paramValue, $acquiaCloudClient); + + private function askFreeFormQuestion(InputArgument $argument, array $params): mixed + { + // Default value may be an empty array, which causes Question to choke. + $default = $argument->getDefault() ?: null; + $question = new Question("Enter a value for {$argument->getName()}", $default); + switch ($argument->getName()) { + case 'applicationUuid': + // @todo Provide a list of application UUIDs. + $question->setValidator(function (mixed $value) { + return $this->validateApplicationUuid($value); + }); + break; + case 'environmentId': + // @todo Provide a list of environment IDs. + case 'source': + $question->setValidator(function (mixed $value) use ($argument): string { + return $this->validateEnvironmentUuid($value, $argument->getName()); + }); + break; + + default: + $validator = $this->createCallableValidator($argument, $params); + $question->setValidator($validator); + break; } - } - } - } - - /** - * @param array|null $paramSpec - */ - private function addPostParamToClient(string $paramName, ?array $paramSpec, mixed $paramValue, Client $acquiaCloudClient): void { - $paramName = ApiCommandHelper::restoreRenamedParameter($paramName); - if ($paramSpec) { - $paramValue = $this->castParamType($paramSpec, $paramValue); - } - if ($paramSpec && array_key_exists('format', $paramSpec) && $paramSpec["format"] === 'binary') { - $acquiaCloudClient->addOption('multipart', [ - [ - 'contents' => Utils::tryFopen($paramValue, 'r'), - 'name' => $paramName, - ], - ]); - } - else { - $acquiaCloudClient->addOption('json', [$paramName => $paramValue]); - } - } - - private function askFreeFormQuestion(InputArgument $argument, array $params): mixed { - // Default value may be an empty array, which causes Question to choke. - $default = $argument->getDefault() ?: NULL; - $question = new Question("Enter a value for {$argument->getName()}", $default); - switch ($argument->getName()) { - case 'applicationUuid': - // @todo Provide a list of application UUIDs. - $question->setValidator(function (mixed $value) { - return $this->validateApplicationUuid($value); - }); - break; - case 'environmentId': - // @todo Provide a list of environment IDs. - case 'source': - $question->setValidator(function (mixed $value) use ($argument): string { - return $this->validateEnvironmentUuid($value, $argument->getName()); - }); - break; - - default: - $validator = $this->createCallableValidator($argument, $params); - $question->setValidator($validator); - break; - } - // Allow unlimited attempts. - $question->setMaxAttempts(NULL); - return $this->io->askQuestion($question); - } - - /** - * @return null|array - */ - private function getParamTypeOneOf(array $paramSpec): ?array { - $oneOf = $paramSpec['oneOf'] ?? NULL; - if (array_key_exists('schema', $paramSpec) && array_key_exists('oneOf', $paramSpec['schema'])) { - $oneOf = $paramSpec['schema']['oneOf']; + // Allow unlimited attempts. + $question->setMaxAttempts(null); + return $this->io->askQuestion($question); } - return $oneOf; - } - - private function castParamToArray(array $paramSpec, array|string $originalValue): string|array|bool|int { - if (array_key_exists('items', $paramSpec) && array_key_exists('type', $paramSpec['items'])) { - if (!is_array($originalValue)) { - $originalValue = $this->doCastParamType('array', $originalValue); - } - $itemType = $paramSpec['items']['type']; - $array = []; - foreach ($originalValue as $key => $v) { - $array[$key] = $this->doCastParamType($itemType, $v); - } - return $array; + + /** + * @return null|array + */ + private function getParamTypeOneOf(array $paramSpec): ?array + { + $oneOf = $paramSpec['oneOf'] ?? null; + if (array_key_exists('schema', $paramSpec) && array_key_exists('oneOf', $paramSpec['schema'])) { + $oneOf = $paramSpec['schema']['oneOf']; + } + return $oneOf; } - return $this->doCastParamType('array', $originalValue); - } + private function castParamToArray(array $paramSpec, array|string $originalValue): string|array|bool|int + { + if (array_key_exists('items', $paramSpec) && array_key_exists('type', $paramSpec['items'])) { + if (!is_array($originalValue)) { + $originalValue = $this->doCastParamType('array', $originalValue); + } + $itemType = $paramSpec['items']['type']; + $array = []; + foreach ($originalValue as $key => $v) { + $array[$key] = $this->doCastParamType($itemType, $v); + } + return $array; + } + return $this->doCastParamType('array', $originalValue); + } } diff --git a/src/Command/Api/ApiCommandFactory.php b/src/Command/Api/ApiCommandFactory.php index b9821c55a..ed226121f 100644 --- a/src/Command/Api/ApiCommandFactory.php +++ b/src/Command/Api/ApiCommandFactory.php @@ -1,6 +1,6 @@ localMachineHelper, - $this->datastoreCloud, - $this->datastoreAcli, - $this->cloudCredentials, - $this->telemetryHelper, - $this->projectDir, - $this->cloudApiClientService, - $this->sshHelper, - $this->sshDir, - $this->logger, - ); - } - - public function createListCommand(): ApiListCommand { - return new ApiListCommand( - $this->localMachineHelper, - $this->datastoreCloud, - $this->datastoreAcli, - $this->cloudCredentials, - $this->telemetryHelper, - $this->projectDir, - $this->cloudApiClientService, - $this->sshHelper, - $this->sshDir, - $this->logger, - ); - } + public function createCommand(): ApiBaseCommand + { + return new ApiBaseCommand( + $this->localMachineHelper, + $this->datastoreCloud, + $this->datastoreAcli, + $this->cloudCredentials, + $this->telemetryHelper, + $this->projectDir, + $this->cloudApiClientService, + $this->sshHelper, + $this->sshDir, + $this->logger, + ); + } + public function createListCommand(): ApiListCommand + { + return new ApiListCommand( + $this->localMachineHelper, + $this->datastoreCloud, + $this->datastoreAcli, + $this->cloudCredentials, + $this->telemetryHelper, + $this->projectDir, + $this->cloudApiClientService, + $this->sshHelper, + $this->sshDir, + $this->logger, + ); + } } diff --git a/src/Command/Api/ApiCommandHelper.php b/src/Command/Api/ApiCommandHelper.php index bb807ddbe..867a3d84a 100644 --- a/src/Command/Api/ApiCommandHelper.php +++ b/src/Command/Api/ApiCommandHelper.php @@ -1,6 +1,6 @@ - */ - public function getApiCommands(string $acquiaCloudSpecFilePath, string $commandPrefix, CommandFactoryInterface $commandFactory): array { - $acquiaCloudSpec = $this->getCloudApiSpec($acquiaCloudSpecFilePath); - $apiCommands = $this->generateApiCommandsFromSpec($acquiaCloudSpec, $commandPrefix, $commandFactory); - $apiListCommands = $this->generateApiListCommands($apiCommands, $commandPrefix, $commandFactory); - return array_merge($apiCommands, $apiListCommands); - } - - private function useCloudApiSpecCache(): bool { - return !(getenv('ACQUIA_CLI_USE_CLOUD_API_SPEC_CACHE') === '0'); - } - - protected function addArgumentExampleToUsageForGetEndpoint(array $paramDefinition, string $usage): mixed { - if (array_key_exists('example', $paramDefinition)) { - if (is_array($paramDefinition['example'])) { - $usage = reset($paramDefinition['example']); - } - elseif (is_string($paramDefinition['example']) && str_contains($paramDefinition['example'], ' ')) { - $usage .= '"' . $paramDefinition['example'] . '" '; - } - else { - $usage .= $paramDefinition['example'] . ' '; - } +class ApiCommandHelper +{ + public function __construct( + private ConsoleLogger $logger + ) { } - return $usage; - } + /** + * @return array + */ + public function getApiCommands(string $acquiaCloudSpecFilePath, string $commandPrefix, CommandFactoryInterface $commandFactory): array + { + $acquiaCloudSpec = $this->getCloudApiSpec($acquiaCloudSpecFilePath); + $apiCommands = $this->generateApiCommandsFromSpec($acquiaCloudSpec, $commandPrefix, $commandFactory); + $apiListCommands = $this->generateApiListCommands($apiCommands, $commandPrefix, $commandFactory); + return array_merge($apiCommands, $apiListCommands); + } - private function addOptionExampleToUsageForGetEndpoint(array $paramDefinition, string $usage): string { - if (array_key_exists('example', $paramDefinition)) { - $usage .= '--' . $paramDefinition['name'] . '="' . $paramDefinition['example'] . '" '; + private function useCloudApiSpecCache(): bool + { + return !(getenv('ACQUIA_CLI_USE_CLOUD_API_SPEC_CACHE') === '0'); } - return $usage; - } - - private function addApiCommandParameters(array $schema, array $acquiaCloudSpec, ApiBaseCommand $command): void { - $inputDefinition = []; - $usage = ''; - - // Parameters to be used in the request query and path. - if (array_key_exists('parameters', $schema)) { - [$queryInputDefinition, $queryParamUsageSuffix] = $this->addApiCommandParametersForPathAndQuery($schema, $acquiaCloudSpec); - /** @var \Symfony\Component\Console\Input\InputOption|InputArgument $parameterDefinition */ - foreach ($queryInputDefinition as $parameterDefinition) { - $parameterSpecification = $this->getParameterDefinitionFromSpec($parameterDefinition->getName(), $acquiaCloudSpec, $schema); - if ($parameterSpecification['in'] === 'query') { - $command->addQueryParameter($parameterDefinition->getName(), $parameterSpecification); - } - elseif ($parameterSpecification['in'] === 'path') { - $command->addPathParameter($parameterDefinition->getName(), $parameterSpecification); + protected function addArgumentExampleToUsageForGetEndpoint(array $paramDefinition, string $usage): mixed + { + if (array_key_exists('example', $paramDefinition)) { + if (is_array($paramDefinition['example'])) { + $usage = reset($paramDefinition['example']); + } elseif (is_string($paramDefinition['example']) && str_contains($paramDefinition['example'], ' ')) { + $usage .= '"' . $paramDefinition['example'] . '" '; + } else { + $usage .= $paramDefinition['example'] . ' '; + } } - } - $usage .= $queryParamUsageSuffix; - $inputDefinition = array_merge($inputDefinition, $queryInputDefinition); - } - // Parameters to be used in the request body. - if (array_key_exists('requestBody', $schema)) { - [ - $bodyInputDefinition, - $requestBodyParamUsageSuffix, - ] = $this->addApiCommandParametersForRequestBody($schema, $acquiaCloudSpec); - $requestBodySchema = $this->getRequestBodyFromParameterSchema($schema, $acquiaCloudSpec); - /** @var \Symfony\Component\Console\Input\InputOption|InputArgument $parameterDefinition */ - foreach ($bodyInputDefinition as $parameterDefinition) { - $parameterSpecification = $this->getPropertySpecFromRequestBodyParam($requestBodySchema, $parameterDefinition); - $command->addPostParameter($parameterDefinition->getName(), $parameterSpecification); - } - $usage .= $requestBodyParamUsageSuffix; - $inputDefinition = array_merge($inputDefinition, $bodyInputDefinition); + return $usage; } - $command->setDefinition(new InputDefinition($inputDefinition)); - if ($usage) { - $command->addUsage(rtrim($usage)); - } - $this->addAliasUsageExamples($command, $inputDefinition, rtrim($usage)); - } - - /** - * @return array - */ - private function addApiCommandParametersForRequestBody(array $schema, array $acquiaCloudSpec): array { - $usage = ''; - $inputDefinition = []; - $requestBodySchema = $this->getRequestBodyFromParameterSchema($schema, $acquiaCloudSpec); - - if (!array_key_exists('properties', $requestBodySchema)) { - return []; + private function addOptionExampleToUsageForGetEndpoint(array $paramDefinition, string $usage): string + { + if (array_key_exists('example', $paramDefinition)) { + $usage .= '--' . $paramDefinition['name'] . '="' . $paramDefinition['example'] . '" '; + } + + return $usage; } - foreach ($requestBodySchema['properties'] as $propKey => $paramDefinition) { - $isRequired = array_key_exists('required', $requestBodySchema) && in_array($propKey, $requestBodySchema['required'], TRUE); - $propKey = self::renameParameter($propKey); - if ($isRequired) { - if (!array_key_exists('description', $paramDefinition)) { - $description = $paramDefinition["additionalProperties"]["description"]; + private function addApiCommandParameters(array $schema, array $acquiaCloudSpec, ApiBaseCommand $command): void + { + $inputDefinition = []; + $usage = ''; + + // Parameters to be used in the request query and path. + if (array_key_exists('parameters', $schema)) { + [$queryInputDefinition, $queryParamUsageSuffix] = $this->addApiCommandParametersForPathAndQuery($schema, $acquiaCloudSpec); + /** @var \Symfony\Component\Console\Input\InputOption|InputArgument $parameterDefinition */ + foreach ($queryInputDefinition as $parameterDefinition) { + $parameterSpecification = $this->getParameterDefinitionFromSpec($parameterDefinition->getName(), $acquiaCloudSpec, $schema); + if ($parameterSpecification['in'] === 'query') { + $command->addQueryParameter($parameterDefinition->getName(), $parameterSpecification); + } elseif ($parameterSpecification['in'] === 'path') { + $command->addPathParameter($parameterDefinition->getName(), $parameterSpecification); + } + } + $usage .= $queryParamUsageSuffix; + $inputDefinition = array_merge($inputDefinition, $queryInputDefinition); + } + + // Parameters to be used in the request body. + if (array_key_exists('requestBody', $schema)) { + [ + $bodyInputDefinition, + $requestBodyParamUsageSuffix, + ] = $this->addApiCommandParametersForRequestBody($schema, $acquiaCloudSpec); + $requestBodySchema = $this->getRequestBodyFromParameterSchema($schema, $acquiaCloudSpec); + /** @var \Symfony\Component\Console\Input\InputOption|InputArgument $parameterDefinition */ + foreach ($bodyInputDefinition as $parameterDefinition) { + $parameterSpecification = $this->getPropertySpecFromRequestBodyParam($requestBodySchema, $parameterDefinition); + $command->addPostParameter($parameterDefinition->getName(), $parameterSpecification); + } + $usage .= $requestBodyParamUsageSuffix; + $inputDefinition = array_merge($inputDefinition, $bodyInputDefinition); } - else { - $description = $paramDefinition['description']; + + $command->setDefinition(new InputDefinition($inputDefinition)); + if ($usage) { + $command->addUsage(rtrim($usage)); } - $inputDefinition[] = new InputArgument( - $propKey, - array_key_exists('type', $paramDefinition) && $paramDefinition['type'] === 'array' ? InputArgument::IS_ARRAY | InputArgument::REQUIRED : InputArgument::REQUIRED, - $description - ); - $usage = $this->addPostArgumentUsageToExample($schema['requestBody'], $propKey, $paramDefinition, 'argument', $usage); - } - else { - $inputDefinition[] = new InputOption( - $propKey, - NULL, - array_key_exists('type', $paramDefinition) && $paramDefinition['type'] === 'array' ? InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED : InputOption::VALUE_REQUIRED, - array_key_exists('description', $paramDefinition) ? $paramDefinition['description'] : $propKey - ); - $usage = $this->addPostArgumentUsageToExample($schema["requestBody"], $propKey, $paramDefinition, 'option', $usage); - // @todo Add validator for $param['enum'] values? - } - } - /** @var \Symfony\Component\Console\Input\InputArgument|InputOption $parameterDefinition */ - foreach ($inputDefinition as $index => $parameterDefinition) { - if ($parameterDefinition->isArray()) { - // Move to the end of the array. - unset($inputDefinition[$index]); - $inputDefinition[] = $parameterDefinition; - } + $this->addAliasUsageExamples($command, $inputDefinition, rtrim($usage)); } - return [$inputDefinition, $usage]; - } - - private function addPostArgumentUsageToExample(mixed $requestBody, mixed $propKey, mixed $paramDefinition, string $type, string $usage): string { - $requestBodyContent = $this->getRequestBodyContent($requestBody); - - if (array_key_exists('example', $requestBodyContent)) { - $example = $requestBodyContent['example']; - $prefix = $type === 'argument' ? '' : "--{$propKey}="; - if (array_key_exists($propKey, $example)) { - switch ($paramDefinition['type']) { - case 'object': - $usage .= $prefix . '"' . json_encode($example[$propKey], JSON_THROW_ON_ERROR) . '"" '; - break; - - case 'array': - $isMultidimensional = count($example[$propKey]) !== count($example[$propKey], COUNT_RECURSIVE); - if (!$isMultidimensional) { - foreach ($example[$propKey] as $value) { - $usage .= $prefix . "\"$value\" "; - } + /** + * @return array + */ + private function addApiCommandParametersForRequestBody(array $schema, array $acquiaCloudSpec): array + { + $usage = ''; + $inputDefinition = []; + $requestBodySchema = $this->getRequestBodyFromParameterSchema($schema, $acquiaCloudSpec); + + if (!array_key_exists('properties', $requestBodySchema)) { + return []; + } + foreach ($requestBodySchema['properties'] as $propKey => $paramDefinition) { + $isRequired = array_key_exists('required', $requestBodySchema) && in_array($propKey, $requestBodySchema['required'], true); + $propKey = self::renameParameter($propKey); + + if ($isRequired) { + if (!array_key_exists('description', $paramDefinition)) { + $description = $paramDefinition["additionalProperties"]["description"]; + } else { + $description = $paramDefinition['description']; + } + $inputDefinition[] = new InputArgument( + $propKey, + array_key_exists('type', $paramDefinition) && $paramDefinition['type'] === 'array' ? InputArgument::IS_ARRAY | InputArgument::REQUIRED : InputArgument::REQUIRED, + $description + ); + $usage = $this->addPostArgumentUsageToExample($schema['requestBody'], $propKey, $paramDefinition, 'argument', $usage); + } else { + $inputDefinition[] = new InputOption( + $propKey, + null, + array_key_exists('type', $paramDefinition) && $paramDefinition['type'] === 'array' ? InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED : InputOption::VALUE_REQUIRED, + array_key_exists('description', $paramDefinition) ? $paramDefinition['description'] : $propKey + ); + $usage = $this->addPostArgumentUsageToExample($schema["requestBody"], $propKey, $paramDefinition, 'option', $usage); + // @todo Add validator for $param['enum'] values? } - else { - // @todo Pretty sure prevents the user from using the arguments. - // Probably a bug. How can we allow users to specify a multidimensional array as an - // argument? - $value = json_encode($example[$propKey], JSON_THROW_ON_ERROR); - $usage .= $prefix . "\"$value\" "; + } + /** @var \Symfony\Component\Console\Input\InputArgument|InputOption $parameterDefinition */ + foreach ($inputDefinition as $index => $parameterDefinition) { + if ($parameterDefinition->isArray()) { + // Move to the end of the array. + unset($inputDefinition[$index]); + $inputDefinition[] = $parameterDefinition; } - break; + } - case 'string': - case 'boolean': - case 'integer': - if (is_array($example[$propKey])) { - $value = reset($example[$propKey]); - } - else { - $value = $example[$propKey]; + return [$inputDefinition, $usage]; + } + + private function addPostArgumentUsageToExample(mixed $requestBody, mixed $propKey, mixed $paramDefinition, string $type, string $usage): string + { + $requestBodyContent = $this->getRequestBodyContent($requestBody); + + if (array_key_exists('example', $requestBodyContent)) { + $example = $requestBodyContent['example']; + $prefix = $type === 'argument' ? '' : "--{$propKey}="; + if (array_key_exists($propKey, $example)) { + switch ($paramDefinition['type']) { + case 'object': + $usage .= $prefix . '"' . json_encode($example[$propKey], JSON_THROW_ON_ERROR) . '"" '; + break; + + case 'array': + $isMultidimensional = count($example[$propKey]) !== count($example[$propKey], COUNT_RECURSIVE); + if (!$isMultidimensional) { + foreach ($example[$propKey] as $value) { + $usage .= $prefix . "\"$value\" "; + } + } else { + // @todo Pretty sure prevents the user from using the arguments. + // Probably a bug. How can we allow users to specify a multidimensional array as an + // argument? + $value = json_encode($example[$propKey], JSON_THROW_ON_ERROR); + $usage .= $prefix . "\"$value\" "; + } + break; + + case 'string': + case 'boolean': + case 'integer': + if (is_array($example[$propKey])) { + $value = reset($example[$propKey]); + } else { + $value = $example[$propKey]; + } + $usage .= $prefix . "\"{$value}\" "; + break; + } } - $usage .= $prefix . "\"{$value}\" "; - break; } - } - } - return $usage; - } - - /** - * @return array - */ - private function addApiCommandParametersForPathAndQuery(array $schema, array $acquiaCloudSpec): array { - $usage = ''; - $inputDefinition = []; - if (!array_key_exists('parameters', $schema)) { - return []; - } - foreach ($schema['parameters'] as $parameter) { - if (array_key_exists('$ref', $parameter)) { - $parts = explode('/', $parameter['$ref']); - $paramKey = end($parts); - $paramDefinition = $this->getParameterDefinitionFromSpec($paramKey, $acquiaCloudSpec, $schema); - } - else { - $paramDefinition = $parameter; - } - - $required = array_key_exists('required', $paramDefinition) && $paramDefinition['required']; - $this->addAliasParameterDescriptions($paramDefinition); - if ($required) { - $inputDefinition[] = new InputArgument( - $paramDefinition['name'], - InputArgument::REQUIRED, - $paramDefinition['description'] - ); - $usage = $this->addArgumentExampleToUsageForGetEndpoint($paramDefinition, $usage); - } - else { - $inputDefinition[] = new InputOption( - $paramDefinition['name'], - NULL, - InputOption::VALUE_REQUIRED, - $paramDefinition['description'] - ); - $usage = $this->addOptionExampleToUsageForGetEndpoint($paramDefinition, $usage); - } + return $usage; } - return [$inputDefinition, $usage]; - } + /** + * @return array + */ + private function addApiCommandParametersForPathAndQuery(array $schema, array $acquiaCloudSpec): array + { + $usage = ''; + $inputDefinition = []; + if (!array_key_exists('parameters', $schema)) { + return []; + } + foreach ($schema['parameters'] as $parameter) { + if (array_key_exists('$ref', $parameter)) { + $parts = explode('/', $parameter['$ref']); + $paramKey = end($parts); + $paramDefinition = $this->getParameterDefinitionFromSpec($paramKey, $acquiaCloudSpec, $schema); + } else { + $paramDefinition = $parameter; + } + + $required = array_key_exists('required', $paramDefinition) && $paramDefinition['required']; + $this->addAliasParameterDescriptions($paramDefinition); + if ($required) { + $inputDefinition[] = new InputArgument( + $paramDefinition['name'], + InputArgument::REQUIRED, + $paramDefinition['description'] + ); + $usage = $this->addArgumentExampleToUsageForGetEndpoint($paramDefinition, $usage); + } else { + $inputDefinition[] = new InputOption( + $paramDefinition['name'], + null, + InputOption::VALUE_REQUIRED, + $paramDefinition['description'] + ); + $usage = $this->addOptionExampleToUsageForGetEndpoint($paramDefinition, $usage); + } + } - private function getParameterDefinitionFromSpec(string $paramKey, array $acquiaCloudSpec, mixed $schema): mixed { - $uppercaseKey = ucfirst($paramKey); - if (array_key_exists('parameters', $acquiaCloudSpec['components']) - && array_key_exists($uppercaseKey, $acquiaCloudSpec['components']['parameters'])) { - return $acquiaCloudSpec['components']['parameters'][$uppercaseKey]; - } - foreach ($schema['parameters'] as $parameter) { - if ($parameter['name'] === $paramKey) { - return $parameter; - } - } - return NULL; - } - - private function getParameterSchemaFromSpec(string $paramKey, array $acquiaCloudSpec): mixed { - return $acquiaCloudSpec['components']['schemas'][$paramKey]; - } - - // @infection-ignore-all - private function isApiSpecChecksumCacheValid(\Symfony\Component\Cache\CacheItem $cacheItem, string $acquiaCloudSpecFileChecksum): bool { - // If the spec file doesn't exist, assume cache is valid. - if (!$acquiaCloudSpecFileChecksum && $cacheItem->isHit()) { - return TRUE; - } - // If there's an invalid entry OR there's no entry, return false. - if (!$cacheItem->isHit() || $cacheItem->get() !== $acquiaCloudSpecFileChecksum) { - return FALSE; + return [$inputDefinition, $usage]; } - return TRUE; - } - - /** - * Breaking caching during tests will cause performance issues and timeouts. - * - * @return array - * @infection-ignore-all - */ - private function getCloudApiSpec(string $specFilePath): array { - $cacheKey = basename($specFilePath); - $cache = new PhpArrayAdapter(__DIR__ . '/../../../var/cache/' . $cacheKey . '.cache', new NullAdapter()); - $cacheItemChecksum = $cache->getItem($cacheKey . '.checksum'); - $cacheItemSpec = $cache->getItem($cacheKey); - - // When running the phar, the original file may not exist. In that case, always use the cache. - if (!file_exists($specFilePath) && $cacheItemSpec->isHit()) { - return $cacheItemSpec->get(); + + private function getParameterDefinitionFromSpec(string $paramKey, array $acquiaCloudSpec, mixed $schema): mixed + { + $uppercaseKey = ucfirst($paramKey); + if ( + array_key_exists('parameters', $acquiaCloudSpec['components']) + && array_key_exists($uppercaseKey, $acquiaCloudSpec['components']['parameters']) + ) { + return $acquiaCloudSpec['components']['parameters'][$uppercaseKey]; + } + foreach ($schema['parameters'] as $parameter) { + if ($parameter['name'] === $paramKey) { + return $parameter; + } + } + return null; } - // Otherwise, only use cache when it is valid. - $checksum = md5_file($specFilePath); - // @infection-ignore-all - if ($this->useCloudApiSpecCache() - && $this->isApiSpecChecksumCacheValid($cacheItemChecksum, $checksum) && $cacheItemSpec->isHit() - ) { - return $cacheItemSpec->get(); + private function getParameterSchemaFromSpec(string $paramKey, array $acquiaCloudSpec): mixed + { + return $acquiaCloudSpec['components']['schemas'][$paramKey]; } - // Parse file. This can take a long while! - $this->logger->debug("Rebuilding caches..."); - $spec = json_decode(file_get_contents($specFilePath), TRUE); - - $cache->warmUp([ - $cacheKey => $spec, - $cacheKey . '.checksum' => $checksum, - ]); - - return $spec; - } - - /** - * @return ApiBaseCommand[] - */ - private function generateApiCommandsFromSpec(array $acquiaCloudSpec, string $commandPrefix, CommandFactoryInterface $commandFactory): array { - $apiCommands = []; - foreach ($acquiaCloudSpec['paths'] as $path => $endpoint) { - foreach ($endpoint as $method => $schema) { - if (!array_key_exists('x-cli-name', $schema)) { - continue; + // @infection-ignore-all + private function isApiSpecChecksumCacheValid(\Symfony\Component\Cache\CacheItem $cacheItem, string $acquiaCloudSpecFileChecksum): bool + { + // If the spec file doesn't exist, assume cache is valid. + if (!$acquiaCloudSpecFileChecksum && $cacheItem->isHit()) { + return true; } + // If there's an invalid entry OR there's no entry, return false. + if (!$cacheItem->isHit() || $cacheItem->get() !== $acquiaCloudSpecFileChecksum) { + return false; + } + return true; + } - if (in_array($schema['x-cli-name'], $this->getSkippedApiCommands(), TRUE)) { - continue; + /** + * Breaking caching during tests will cause performance issues and timeouts. + * + * @return array + * @infection-ignore-all + */ + private function getCloudApiSpec(string $specFilePath): array + { + $cacheKey = basename($specFilePath); + $cache = new PhpArrayAdapter(__DIR__ . '/../../../var/cache/' . $cacheKey . '.cache', new NullAdapter()); + $cacheItemChecksum = $cache->getItem($cacheKey . '.checksum'); + $cacheItemSpec = $cache->getItem($cacheKey); + + // When running the phar, the original file may not exist. In that case, always use the cache. + if (!file_exists($specFilePath) && $cacheItemSpec->isHit()) { + return $cacheItemSpec->get(); } - // Skip deprecated endpoints. + // Otherwise, only use cache when it is valid. + $checksum = md5_file($specFilePath); // @infection-ignore-all - if (array_key_exists('deprecated', $schema) && $schema['deprecated']) { - continue; + if ( + $this->useCloudApiSpecCache() + && $this->isApiSpecChecksumCacheValid($cacheItemChecksum, $checksum) && $cacheItemSpec->isHit() + ) { + return $cacheItemSpec->get(); } - $commandName = $commandPrefix . ':' . $schema['x-cli-name']; - $command = $commandFactory->createCommand(); - $command->setName($commandName); - $command->setDescription($schema['summary']); - $command->setMethod($method); - $command->setResponses($schema['responses']); - $command->setHidden(FALSE); - if (array_key_exists('servers', $acquiaCloudSpec)) { - $command->setServers($acquiaCloudSpec['servers']); + // Parse file. This can take a long while! + $this->logger->debug("Rebuilding caches..."); + $spec = json_decode(file_get_contents($specFilePath), true); + + $cache->warmUp([ + $cacheKey => $spec, + $cacheKey . '.checksum' => $checksum, + ]); + + return $spec; + } + + /** + * @return ApiBaseCommand[] + */ + private function generateApiCommandsFromSpec(array $acquiaCloudSpec, string $commandPrefix, CommandFactoryInterface $commandFactory): array + { + $apiCommands = []; + foreach ($acquiaCloudSpec['paths'] as $path => $endpoint) { + foreach ($endpoint as $method => $schema) { + if (!array_key_exists('x-cli-name', $schema)) { + continue; + } + + if (in_array($schema['x-cli-name'], $this->getSkippedApiCommands(), true)) { + continue; + } + + // Skip deprecated endpoints. + // @infection-ignore-all + if (array_key_exists('deprecated', $schema) && $schema['deprecated']) { + continue; + } + + $commandName = $commandPrefix . ':' . $schema['x-cli-name']; + $command = $commandFactory->createCommand(); + $command->setName($commandName); + $command->setDescription($schema['summary']); + $command->setMethod($method); + $command->setResponses($schema['responses']); + $command->setHidden(false); + if (array_key_exists('servers', $acquiaCloudSpec)) { + $command->setServers($acquiaCloudSpec['servers']); + } + $command->setPath($path); + $command->setHelp("For more help, see https://cloudapi-docs.acquia.com/ or https://dev.acquia.com/api-documentation/acquia-cloud-site-factory-api for acsf commands."); + $this->addApiCommandParameters($schema, $acquiaCloudSpec, $command); + $apiCommands[] = $command; + } } - $command->setPath($path); - $command->setHelp("For more help, see https://cloudapi-docs.acquia.com/ or https://dev.acquia.com/api-documentation/acquia-cloud-site-factory-api for acsf commands."); - $this->addApiCommandParameters($schema, $acquiaCloudSpec, $command); - $apiCommands[] = $command; - } + + return $apiCommands; + } + + /** + * @return array + */ + protected function getSkippedApiCommands(): array + { + return [ + // Skip accounts:drush-aliases since we have remote:aliases:download instead and it actually returns + // application/gzip content. + 'accounts:drush-aliases', + // Skip any command that has a duplicative corresponding ACLI command. + 'ide:create', + 'log:tail', + 'ssh-key:create', + 'ssh-key:create-upload', + 'ssh-key:delete', + 'ssh-key:list', + 'ssh-key:upload', + // Skip buggy or unsupported endpoints. + 'environments:stack-metrics-data-metric', + ]; } - return $apiCommands; - } - - /** - * @return array - */ - protected function getSkippedApiCommands(): array { - return [ - // Skip accounts:drush-aliases since we have remote:aliases:download instead and it actually returns - // application/gzip content. - 'accounts:drush-aliases', - // Skip any command that has a duplicative corresponding ACLI command. - 'ide:create', - 'log:tail', - 'ssh-key:create', - 'ssh-key:create-upload', - 'ssh-key:delete', - 'ssh-key:list', - 'ssh-key:upload', - // Skip buggy or unsupported endpoints. - 'environments:stack-metrics-data-metric', - ]; - } - - private function addAliasUsageExamples(ApiBaseCommand $command, array $inputDefinition, string $usage): void { - foreach ($inputDefinition as $key => $parameter) { - if ($parameter->getName() === 'applicationUuid') { - $usageParts = explode(' ', $usage); - $usageParts[$key] = "myapp"; - $usage = implode(' ', $usageParts); - $command->addUsage($usage); - } - if ($parameter->getName() === 'environmentId') { - $usageParts = explode(' ', $usage); - $usageParts[$key] = "myapp.dev"; - $usage = implode(' ', $usageParts); - $command->addUsage($usage); - } + private function addAliasUsageExamples(ApiBaseCommand $command, array $inputDefinition, string $usage): void + { + foreach ($inputDefinition as $key => $parameter) { + if ($parameter->getName() === 'applicationUuid') { + $usageParts = explode(' ', $usage); + $usageParts[$key] = "myapp"; + $usage = implode(' ', $usageParts); + $command->addUsage($usage); + } + if ($parameter->getName() === 'environmentId') { + $usageParts = explode(' ', $usage); + $usageParts[$key] = "myapp.dev"; + $usage = implode(' ', $usageParts); + $command->addUsage($usage); + } + } } - } - private function addAliasParameterDescriptions(mixed &$paramDefinition): void { - if ($paramDefinition['name'] === 'applicationUuid') { - $paramDefinition['description'] .= ' You may also use an application alias or omit the argument if you run the command in a linked directory.'; + private function addAliasParameterDescriptions(mixed &$paramDefinition): void + { + if ($paramDefinition['name'] === 'applicationUuid') { + $paramDefinition['description'] .= ' You may also use an application alias or omit the argument if you run the command in a linked directory.'; + } + if ($paramDefinition['name'] === 'environmentId') { + $paramDefinition['description'] .= " You may also use an environment alias or UUID."; + } } - if ($paramDefinition['name'] === 'environmentId') { - $paramDefinition['description'] .= " You may also use an environment alias or UUID."; + + /** + * @return array + */ + private function getRequestBodyFromParameterSchema(array $schema, array $acquiaCloudSpec): array + { + $requestBodyContent = $this->getRequestBodyContent($schema['requestBody']); + $requestBodySchema = $requestBodyContent['schema']; + + // If this is a reference to the top level schema, go grab the referenced component. + if (array_key_exists('$ref', $requestBodySchema)) { + $parts = explode('/', $requestBodySchema['$ref']); + $paramKey = end($parts); + $requestBodySchema = $this->getParameterSchemaFromSpec($paramKey, $acquiaCloudSpec); + } + + return $requestBodySchema; } - } - - /** - * @return array - */ - private function getRequestBodyFromParameterSchema(array $schema, array $acquiaCloudSpec): array { - $requestBodyContent = $this->getRequestBodyContent($schema['requestBody']); - $requestBodySchema = $requestBodyContent['schema']; - - // If this is a reference to the top level schema, go grab the referenced component. - if (array_key_exists('$ref', $requestBodySchema)) { - $parts = explode('/', $requestBodySchema['$ref']); - $paramKey = end($parts); - $requestBodySchema = $this->getParameterSchemaFromSpec($paramKey, $acquiaCloudSpec); + + private function getPropertySpecFromRequestBodyParam(array $requestBodySchema, mixed $parameterDefinition): mixed + { + return $requestBodySchema['properties'][$parameterDefinition->getName()] ?? null; } - return $requestBodySchema; - } - - private function getPropertySpecFromRequestBodyParam(array $requestBodySchema, mixed $parameterDefinition): mixed { - return $requestBodySchema['properties'][$parameterDefinition->getName()] ?? NULL; - } - - /** - * @return array - */ - protected static function getParameterRenameMap(): array { - // Format should be ['original => new']. - return [ - // @see api:environments:cron-create - 'command' => 'cron_command', - // @see api:environments:update. - 'version' => 'lang_version', - ]; - } - - public static function renameParameter(mixed $propKey): mixed { - $parameterRenameMap = self::getParameterRenameMap(); - if (array_key_exists($propKey, $parameterRenameMap)) { - $propKey = $parameterRenameMap[$propKey]; + /** + * @return array + */ + protected static function getParameterRenameMap(): array + { + // Format should be ['original => new']. + return [ + // @see api:environments:cron-create + 'command' => 'cron_command', + // @see api:environments:update. + 'version' => 'lang_version', + ]; } - return $propKey; - } - public static function restoreRenamedParameter(string $propKey): int|string { - $parameterRenameMap = array_flip(self::getParameterRenameMap()); - if (array_key_exists($propKey, $parameterRenameMap)) { - $propKey = $parameterRenameMap[$propKey]; + public static function renameParameter(mixed $propKey): mixed + { + $parameterRenameMap = self::getParameterRenameMap(); + if (array_key_exists($propKey, $parameterRenameMap)) { + $propKey = $parameterRenameMap[$propKey]; + } + return $propKey; } - return $propKey; - } - - /** - * @return ApiListCommandBase[] - */ - private function generateApiListCommands(array $apiCommands, string $commandPrefix, CommandFactoryInterface $commandFactory): array { - $apiListCommands = []; - foreach ($apiCommands as $apiCommand) { - $commandNameParts = explode(':', $apiCommand->getName()); - if (count($commandNameParts) < 3) { - continue; - } - $namespace = $commandNameParts[1]; - if (!array_key_exists($namespace, $apiListCommands)) { - /** @var \Acquia\Cli\Command\Acsf\AcsfListCommand|\Acquia\Cli\Command\Api\ApiListCommand $command */ - $command = $commandFactory->createListCommand(); - $name = $commandPrefix . ':' . $namespace; - $command->setName($name); - $command->setNamespace($name); - $command->setAliases([]); - $command->setDescription("List all API commands for the {$namespace} resource"); - $apiListCommands[$name] = $command; - } + + public static function restoreRenamedParameter(string $propKey): int|string + { + $parameterRenameMap = array_flip(self::getParameterRenameMap()); + if (array_key_exists($propKey, $parameterRenameMap)) { + $propKey = $parameterRenameMap[$propKey]; + } + return $propKey; } - return $apiListCommands; - } - - /** - * @param array $requestBody - * @return array - */ - private function getRequestBodyContent(array $requestBody): array { - $content = $requestBody['content']; - $knownContentTypes = [ - 'application/hal+json', - 'application/json', - 'application/x-www-form-urlencoded', - 'multipart/form-data', - ]; - foreach ($knownContentTypes as $contentType) { - if (array_key_exists($contentType, $content)) { - return $content[$contentType]; - } + + /** + * @return ApiListCommandBase[] + */ + private function generateApiListCommands(array $apiCommands, string $commandPrefix, CommandFactoryInterface $commandFactory): array + { + $apiListCommands = []; + foreach ($apiCommands as $apiCommand) { + $commandNameParts = explode(':', $apiCommand->getName()); + if (count($commandNameParts) < 3) { + continue; + } + $namespace = $commandNameParts[1]; + if (!array_key_exists($namespace, $apiListCommands)) { + /** @var \Acquia\Cli\Command\Acsf\AcsfListCommand|\Acquia\Cli\Command\Api\ApiListCommand $command */ + $command = $commandFactory->createListCommand(); + $name = $commandPrefix . ':' . $namespace; + $command->setName($name); + $command->setNamespace($name); + $command->setAliases([]); + $command->setDescription("List all API commands for the {$namespace} resource"); + $apiListCommands[$name] = $command; + } + } + return $apiListCommands; } - throw new AcquiaCliException("requestBody content doesn't match any known schema"); - } + /** + * @param array $requestBody + * @return array + */ + private function getRequestBodyContent(array $requestBody): array + { + $content = $requestBody['content']; + $knownContentTypes = [ + 'application/hal+json', + 'application/json', + 'application/x-www-form-urlencoded', + 'multipart/form-data', + ]; + foreach ($knownContentTypes as $contentType) { + if (array_key_exists($contentType, $content)) { + return $content[$contentType]; + } + } + throw new AcquiaCliException("requestBody content doesn't match any known schema"); + } } diff --git a/src/Command/Api/ApiListCommand.php b/src/Command/Api/ApiListCommand.php index 3f71a8c19..6b0d4c007 100644 --- a/src/Command/Api/ApiListCommand.php +++ b/src/Command/Api/ApiListCommand.php @@ -1,6 +1,6 @@ namespace = $namespace; - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $commands = $this->getApplication()->all(); - foreach ($commands as $command) { - if ($command->getName() !== $this->namespace - && str_contains($command->getName(), $this->namespace . ':') - // This is a lazy way to exclude api:base and acsf:base. - && $command->getDescription() - ) { - $command->setHidden(FALSE); - } - else { - $command->setHidden(); - } - } - - $command = $this->getApplication()->find('list'); - $arguments = [ - 'command' => 'list', - 'namespace' => 'api', - ]; - $listInput = new ArrayInput($arguments); +class ApiListCommandBase extends CommandBase +{ + protected string $namespace; - return $command->run($listInput, $output); - } + public function setNamespace(string $namespace): void + { + $this->namespace = $namespace; + } + protected function execute(InputInterface $input, OutputInterface $output): int + { + $commands = $this->getApplication()->all(); + foreach ($commands as $command) { + if ( + $command->getName() !== $this->namespace + && str_contains($command->getName(), $this->namespace . ':') + // This is a lazy way to exclude api:base and acsf:base. + && $command->getDescription() + ) { + $command->setHidden(false); + } else { + $command->setHidden(); + } + } + + $command = $this->getApplication()->find('list'); + $arguments = [ + 'command' => 'list', + 'namespace' => 'api', + ]; + $listInput = new ArrayInput($arguments); + + return $command->run($listInput, $output); + } } diff --git a/src/Command/App/AppOpenCommand.php b/src/Command/App/AppOpenCommand.php index 7d78ec612..386154d3b 100644 --- a/src/Command/App/AppOpenCommand.php +++ b/src/Command/App/AppOpenCommand.php @@ -1,6 +1,6 @@ acceptApplicationUuid(); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - if (!$this->localMachineHelper->isBrowserAvailable()) { - throw new AcquiaCliException('No browser is available on this machine'); +final class AppOpenCommand extends CommandBase +{ + protected function configure(): void + { + $this + ->acceptApplicationUuid(); } - $applicationUuid = $this->determineCloudApplication(); - $this->localMachineHelper->startBrowser('https://cloud.acquia.com/a/applications/' . $applicationUuid); - return Command::SUCCESS; - } + protected function execute(InputInterface $input, OutputInterface $output): int + { + if (!$this->localMachineHelper->isBrowserAvailable()) { + throw new AcquiaCliException('No browser is available on this machine'); + } + $applicationUuid = $this->determineCloudApplication(); + $this->localMachineHelper->startBrowser('https://cloud.acquia.com/a/applications/' . $applicationUuid); + return Command::SUCCESS; + } } diff --git a/src/Command/App/AppVcsInfo.php b/src/Command/App/AppVcsInfo.php index 768f72522..85250d224 100644 --- a/src/Command/App/AppVcsInfo.php +++ b/src/Command/App/AppVcsInfo.php @@ -1,6 +1,6 @@ addOption('deployed', NULL, InputOption::VALUE_OPTIONAL, 'Show only deployed branches and tags') - ->addUsage('[] --deployed'); - $this->acceptApplicationUuid(); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $applicationUuid = $this->determineCloudApplication(); +class AppVcsInfo extends CommandBase +{ + protected function configure(): void + { + $this + ->addOption('deployed', null, InputOption::VALUE_OPTIONAL, 'Show only deployed branches and tags') + ->addUsage('[] --deployed'); + $this->acceptApplicationUuid(); + } - $cloudApiClient = $this->cloudApiClientService->getClient(); + protected function execute(InputInterface $input, OutputInterface $output): int + { + $applicationUuid = $this->determineCloudApplication(); - $envResource = new Environments($cloudApiClient); - $environments = $envResource->getAll($applicationUuid); + $cloudApiClient = $this->cloudApiClientService->getClient(); - if (!$environments->count()) { - throw new AcquiaCliException('There are no environments available with this application.'); - } + $envResource = new Environments($cloudApiClient); + $environments = $envResource->getAll($applicationUuid); - // To show branches and tags which are deployed only. - $showDeployedVcsOnly = $input->hasParameterOption('--deployed'); + if (!$environments->count()) { + throw new AcquiaCliException('There are no environments available with this application.'); + } - // Prepare list of all deployed VCS paths. - $deployedVcs = []; - foreach ($environments as $environment) { - if (isset($environment->vcs->path)) { - $deployedVcs[$environment->vcs->path] = $environment->label; - } - } + // To show branches and tags which are deployed only. + $showDeployedVcsOnly = $input->hasParameterOption('--deployed'); - // If only to show the deployed VCS but no VCS is deployed. - if ($showDeployedVcsOnly && empty($deployedVcs)) { - throw new AcquiaCliException('No branch or tag is deployed on any of the environment of this application.'); - } + // Prepare list of all deployed VCS paths. + $deployedVcs = []; + foreach ($environments as $environment) { + if (isset($environment->vcs->path)) { + $deployedVcs[$environment->vcs->path] = $environment->label; + } + } - $applicationCodeResource = new Code($cloudApiClient); - $allBranchesAndTags = $applicationCodeResource->getAll($applicationUuid); + // If only to show the deployed VCS but no VCS is deployed. + if ($showDeployedVcsOnly && empty($deployedVcs)) { + throw new AcquiaCliException('No branch or tag is deployed on any of the environment of this application.'); + } - if (!$allBranchesAndTags->count()) { - throw new AcquiaCliException('No branch or tag is available with this application.'); - } + $applicationCodeResource = new Code($cloudApiClient); + $allBranchesAndTags = $applicationCodeResource->getAll($applicationUuid); - $nonDeployedVcs = []; - // Show both deployed and non-deployed VCS. - if (!$showDeployedVcsOnly) { - // Prepare list of all non-deployed VCS paths. - foreach ($allBranchesAndTags as $branchTag) { - if (!isset($deployedVcs[$branchTag->name])) { - $nonDeployedVcs[$branchTag->name] = $branchTag->name; + if (!$allBranchesAndTags->count()) { + throw new AcquiaCliException('No branch or tag is available with this application.'); } - } - } - // To show the deployed VCS paths on top. - $allVcs = array_merge($deployedVcs, $nonDeployedVcs); - $headers = ['Branch / Tag Name', 'Deployed', 'Deployed Environment']; - $table = new Table($output); - $table->setHeaders($headers); - $table->setHeaderTitle('Status of Branches and Tags of the Application'); - foreach ($allVcs as $vscPath => $env) { - $table->addRow([ - $vscPath, - // If VCS and env name is not same, it means it is deployed. - $vscPath !== $env ? 'Yes' : 'No', - // If VCS and env name is same, it means it is deployed. - $vscPath !== $env ? $env : 'None', - ]); - } + $nonDeployedVcs = []; + // Show both deployed and non-deployed VCS. + if (!$showDeployedVcsOnly) { + // Prepare list of all non-deployed VCS paths. + foreach ($allBranchesAndTags as $branchTag) { + if (!isset($deployedVcs[$branchTag->name])) { + $nonDeployedVcs[$branchTag->name] = $branchTag->name; + } + } + } - $table->render(); - $this->io->newLine(); + // To show the deployed VCS paths on top. + $allVcs = array_merge($deployedVcs, $nonDeployedVcs); + $headers = ['Branch / Tag Name', 'Deployed', 'Deployed Environment']; + $table = new Table($output); + $table->setHeaders($headers); + $table->setHeaderTitle('Status of Branches and Tags of the Application'); + foreach ($allVcs as $vscPath => $env) { + $table->addRow([ + $vscPath, + // If VCS and env name is not same, it means it is deployed. + $vscPath !== $env ? 'Yes' : 'No', + // If VCS and env name is same, it means it is deployed. + $vscPath !== $env ? $env : 'None', + ]); + } - return self::SUCCESS; - } + $table->render(); + $this->io->newLine(); + return self::SUCCESS; + } } diff --git a/src/Command/App/From/Composer/ProjectBuilder.php b/src/Command/App/From/Composer/ProjectBuilder.php index b7f56558e..71a17b6a9 100644 --- a/src/Command/App/From/Composer/ProjectBuilder.php +++ b/src/Command/App/From/Composer/ProjectBuilder.php @@ -1,6 +1,6 @@ configuration = $configuration; - $this->resolver = $recommendation_resolver; - $this->siteInspector = $site_inspector; - } - - /** - * Gets an array representing a D9+ composer.json file for the current site. - * - * @return array - * An array that can be encoded as JSON and written to a file. Calling - * `composer install` in the same directory as that file should yield a new - * Drupal project with Drupal 9+ installed, in addition to the Acquia - * Migrate module, and some of all of the D9 replacements for the current - * site's Drupal 7 modules. - */ - public function buildProject(): array { - $modules_to_install = []; - $recommendations = []; - $composer_json = $this->configuration->getRootPackageDefinition(); - - // Add recommended dependencies and patches, if any. - foreach ($this->resolver->getRecommendations() as $recommendation) { - assert($recommendation instanceof RecommendationInterface); - if ($recommendation instanceof NormalizableInterface) { - $recommendations[] = $recommendation->normalize(); - } - if ($recommendation instanceof AbandonmentRecommendation) { - continue; - } - $recommended_package_name = $recommendation->getPackageName(); - // Special case: to guarantee that a valid and installable `composer.json` - // file is generated, `$composer_json` is first populated with valid - // `require`s based on `config.json`: - // - `drupal/core-composer-scaffold` - // - `drupal/core-project-message` - // - `drupal/core-recommended` - // When `recommendations.json` is unreachable or invalid, the versions - // specified in `config.json` are what end up in the `composer.json` file. - // Since without a `recommendations.json` file no patches can be applied, - // this guarantees a viable Drupal 9-based `composer.json`. - // However, when a valid `recommendations.json` is available, then that - // default version that `$composer_json` was initialized with should be - // overwritten by the recommended `drupal/core` version in - // `recommendations.json`, because the patches associated with Drupal core - // may only apply to a particular version. - // Because the `drupal/core-*` package versions have sensible defaults - // from `config.json` and are overwritten if and only if a valid - // `recommendations.json` is available, we can guarantee a viable - // `composer.json` file. - if ($recommended_package_name === 'drupal/core') { - $core_version_constraint = $recommendation->getVersionConstraint(); - if ($core_version_constraint !== '*') { - $composer_json['require']['drupal/core-composer-scaffold'] = $core_version_constraint; - $composer_json['require']['drupal/core-project-message'] = $core_version_constraint; - $composer_json['require']['drupal/core-recommended'] = $core_version_constraint; - } - } - else { - $composer_json['require'][$recommended_package_name] = $recommendation->getVersionConstraint(); - } - if ($recommendation->hasPatches()) { - $composer_json['extra']['patches'][$recommended_package_name] = $recommendation->getPatches(); - } - if ($recommendation->isVetted() && $recommendation->hasModulesToInstall()) { - array_push($modules_to_install, ...$recommendation->getModulesToInstall()); - } +final class ProjectBuilder +{ + /** + * The current configuration. + */ + protected Configuration $configuration; + + /** + * The recommendation resolver. + */ + protected Resolver $resolver; + + /** + * The site inspector. + */ + protected SiteInspectorInterface $siteInspector; + + /** + * ProjectBuilder constructor. + * + * @param \Acquia\Cli\Command\App\From\Configuration $configuration + * A configuration object. + * @param \Acquia\Cli\Command\App\From\Recommendation\Resolver $recommendation_resolver + * A recommendation resolver. + * @param \Acquia\Cli\Command\App\From\SourceSite\SiteInspectorInterface $site_inspector + * A site inspector. + */ + public function __construct(Configuration $configuration, Resolver $recommendation_resolver, SiteInspectorInterface $site_inspector) + { + $this->configuration = $configuration; + $this->resolver = $recommendation_resolver; + $this->siteInspector = $site_inspector; } - // Multiple recommendations may ask the same module to get installed. - $modules_to_install = array_unique($modules_to_install); - - // Sort the dependencies and patches by package name. - sort($modules_to_install); - if (isset($composer_json['require'])) { - ksort($composer_json['require']); - } - if (isset($composer_json['extra']['patches'])) { - ksort($composer_json['extra']['patches']); - } + /** + * Gets an array representing a D9+ composer.json file for the current site. + * + * @return array + * An array that can be encoded as JSON and written to a file. Calling + * `composer install` in the same directory as that file should yield a new + * Drupal project with Drupal 9+ installed, in addition to the Acquia + * Migrate module, and some of all of the D9 replacements for the current + * site's Drupal 7 modules. + */ + public function buildProject(): array + { + $modules_to_install = []; + $recommendations = []; + $composer_json = $this->configuration->getRootPackageDefinition(); + + // Add recommended dependencies and patches, if any. + foreach ($this->resolver->getRecommendations() as $recommendation) { + assert($recommendation instanceof RecommendationInterface); + if ($recommendation instanceof NormalizableInterface) { + $recommendations[] = $recommendation->normalize(); + } + if ($recommendation instanceof AbandonmentRecommendation) { + continue; + } + $recommended_package_name = $recommendation->getPackageName(); + // Special case: to guarantee that a valid and installable `composer.json` + // file is generated, `$composer_json` is first populated with valid + // `require`s based on `config.json`: + // - `drupal/core-composer-scaffold` + // - `drupal/core-project-message` + // - `drupal/core-recommended` + // When `recommendations.json` is unreachable or invalid, the versions + // specified in `config.json` are what end up in the `composer.json` file. + // Since without a `recommendations.json` file no patches can be applied, + // this guarantees a viable Drupal 9-based `composer.json`. + // However, when a valid `recommendations.json` is available, then that + // default version that `$composer_json` was initialized with should be + // overwritten by the recommended `drupal/core` version in + // `recommendations.json`, because the patches associated with Drupal core + // may only apply to a particular version. + // Because the `drupal/core-*` package versions have sensible defaults + // from `config.json` and are overwritten if and only if a valid + // `recommendations.json` is available, we can guarantee a viable + // `composer.json` file. + if ($recommended_package_name === 'drupal/core') { + $core_version_constraint = $recommendation->getVersionConstraint(); + if ($core_version_constraint !== '*') { + $composer_json['require']['drupal/core-composer-scaffold'] = $core_version_constraint; + $composer_json['require']['drupal/core-project-message'] = $core_version_constraint; + $composer_json['require']['drupal/core-recommended'] = $core_version_constraint; + } + } else { + $composer_json['require'][$recommended_package_name] = $recommendation->getVersionConstraint(); + } + if ($recommendation->hasPatches()) { + $composer_json['extra']['patches'][$recommended_package_name] = $recommendation->getPatches(); + } + if ($recommendation->isVetted() && $recommendation->hasModulesToInstall()) { + array_push($modules_to_install, ...$recommendation->getModulesToInstall()); + } + } - $source_modules = array_values(array_map(function (ExtensionInterface $module) { - // phpcs:disable SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys - return [ - 'name' => $module->getName(), - 'humanName' => $module->getHumanName(), - 'version' => $module->getVersion(), - ]; - // phpcs:enable - }, $this->siteInspector->getExtensions(SiteInspectorInterface::FLAG_EXTENSION_MODULE | SiteInspectorInterface::FLAG_EXTENSION_ENABLED))); - $module_names = array_column($source_modules, 'name'); - array_multisort($module_names, SORT_STRING, $source_modules); + // Multiple recommendations may ask the same module to get installed. + $modules_to_install = array_unique($modules_to_install); - $recommendation_ids = array_column($recommendations, 'id'); - array_multisort($recommendation_ids, SORT_STRING, $recommendations); + // Sort the dependencies and patches by package name. + sort($modules_to_install); + if (isset($composer_json['require'])) { + ksort($composer_json['require']); + } + if (isset($composer_json['extra']['patches'])) { + ksort($composer_json['extra']['patches']); + } - // phpcs:disable SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys - return [ - 'installModules' => $modules_to_install, - 'filePaths' => [ + $source_modules = array_values(array_map(function (ExtensionInterface $module) { + // phpcs:disable SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys + return [ + 'name' => $module->getName(), + 'humanName' => $module->getHumanName(), + 'version' => $module->getVersion(), + ]; + // phpcs:enable + }, $this->siteInspector->getExtensions(SiteInspectorInterface::FLAG_EXTENSION_MODULE | SiteInspectorInterface::FLAG_EXTENSION_ENABLED))); + $module_names = array_column($source_modules, 'name'); + array_multisort($module_names, SORT_STRING, $source_modules); + + $recommendation_ids = array_column($recommendations, 'id'); + array_multisort($recommendation_ids, SORT_STRING, $recommendations); + + // phpcs:disable SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys + return [ + 'installModules' => $modules_to_install, + 'filePaths' => [ 'public' => $this->siteInspector->getPublicFilePath(), 'private' => $this->siteInspector->getPrivateFilePath(), - ], - 'sourceModules' => $source_modules, - 'recommendations' => $recommendations, - 'rootPackageDefinition' => $composer_json, - ]; - // phpcs:enable - } - + ], + 'sourceModules' => $source_modules, + 'recommendations' => $recommendations, + 'rootPackageDefinition' => $composer_json, + ]; + // phpcs:enable + } } diff --git a/src/Command/App/From/Configuration.php b/src/Command/App/From/Configuration.php index eb645875f..bacf660d0 100644 --- a/src/Command/App/From/Configuration.php +++ b/src/Command/App/From/Configuration.php @@ -1,6 +1,6 @@ - */ - protected array $array; - - /** - * Configuration constructor. - * - * @param array $config - * An array of configuration, usually parsed from a configuration file. - */ - protected function __construct(array $config) { - $this->array = static::schema([ - 'rootPackageDefinition' => 'is_array', - ])($config); - } - - /** - * Creates a configuration object from configuration given as a PHP resource. - * - * The given PHP resource is usually obtained by calling fopen($location). - * - * @param resource $configuration_resource - * Configuration to be parse; given as a PHP resource. - * @return \Acquia\Cli\Command\App\From\Configuration - * A new configuration object. - */ - public static function createFromResource($configuration_resource): Configuration { - return new static(static::parseJsonResource($configuration_resource)); - } - - /** - * Gets an basic root composer package definition for a Drupal 9+ project. - * - * @return array - * An array representing a root composer package definition. From this - * starting point, additional dependencies and metadata can be added until - * an acceptable project is defined for migrating a source site to Drupal - * 9+. - */ - public function getRootPackageDefinition(): array { - return $this->array['rootPackageDefinition']; - } +final class Configuration +{ + use ArrayValidationTrait; + use JsonResourceParserTrait; + /** + * The current configuration, usually parsed from a file. + * + * @var array + */ + protected array $array; + + /** + * Configuration constructor. + * + * @param array $config + * An array of configuration, usually parsed from a configuration file. + */ + protected function __construct(array $config) + { + $this->array = static::schema([ + 'rootPackageDefinition' => 'is_array', + ])($config); + } + + /** + * Creates a configuration object from configuration given as a PHP resource. + * + * The given PHP resource is usually obtained by calling fopen($location). + * + * @param resource $configuration_resource + * Configuration to be parse; given as a PHP resource. + * @return \Acquia\Cli\Command\App\From\Configuration + * A new configuration object. + */ + public static function createFromResource($configuration_resource): Configuration + { + return new static(static::parseJsonResource($configuration_resource)); + } + + /** + * Gets an basic root composer package definition for a Drupal 9+ project. + * + * @return array + * An array representing a root composer package definition. From this + * starting point, additional dependencies and metadata can be added until + * an acceptable project is defined for migrating a source site to Drupal + * 9+. + */ + public function getRootPackageDefinition(): array + { + return $this->array['rootPackageDefinition']; + } } diff --git a/src/Command/App/From/JsonResourceParserTrait.php b/src/Command/App/From/JsonResourceParserTrait.php index f01f47e81..4311fd20b 100644 --- a/src/Command/App/From/JsonResourceParserTrait.php +++ b/src/Command/App/From/JsonResourceParserTrait.php @@ -1,26 +1,26 @@ - */ - protected array $definition; - - /** - * An array of extensions to which this recommendation applied. - * - * @var \Acquia\Cli\Command\App\From\SourceSite\ExtensionInterface[] - */ - protected array $appliedTo = []; - - /** - * AbandonmentRecommendation constructor. - * - * @param \Closure $extension_evaluator - * An anonymous function that determines if this recommendation applies to - * an extension. - * @param array $definition - * The original decoded definition. - */ - protected function __construct(Closure $extension_evaluator, array $definition) { - $this->evaluateExtension = $extension_evaluator; - $this->definition = $definition; - } - - /** - * Creates a new recommendation. - * - * @param mixed $definition - * A static recommendation definition. This must be an array. However, other - * value types are accepted because this method performs validation on the - * given value. - * @return \Acquia\Cli\Command\App\From\Recommendation\RecommendationInterface - * A new AbandonmentRecommendation object if the given definition is valid or - * a new NoRecommendation object otherwise. - */ - public static function createFromDefinition(mixed $definition): RecommendationInterface { - // phpcs:disable SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys - $validator = static::schema([ - 'package' => 'is_null', - 'note' => 'is_string', - 'replaces' => static::schema([ +class AbandonmentRecommendation implements RecommendationInterface, NormalizableInterface +{ + use ArrayValidationTrait; + + /** + * An anonymous function that determines if this recommendation is applicable. + */ + protected \Closure $evaluateExtension; + + /** + * The original decoded definition. + * + * @var array + */ + protected array $definition; + + /** + * An array of extensions to which this recommendation applied. + * + * @var \Acquia\Cli\Command\App\From\SourceSite\ExtensionInterface[] + */ + protected array $appliedTo = []; + + /** + * AbandonmentRecommendation constructor. + * + * @param \Closure $extension_evaluator + * An anonymous function that determines if this recommendation applies to + * an extension. + * @param array $definition + * The original decoded definition. + */ + protected function __construct(Closure $extension_evaluator, array $definition) + { + $this->evaluateExtension = $extension_evaluator; + $this->definition = $definition; + } + + /** + * Creates a new recommendation. + * + * @param mixed $definition + * A static recommendation definition. This must be an array. However, other + * value types are accepted because this method performs validation on the + * given value. + * @return \Acquia\Cli\Command\App\From\Recommendation\RecommendationInterface + * A new AbandonmentRecommendation object if the given definition is valid or + * a new NoRecommendation object otherwise. + */ + public static function createFromDefinition(mixed $definition): RecommendationInterface + { + // phpcs:disable SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys + $validator = static::schema([ + 'package' => 'is_null', + 'note' => 'is_string', + 'replaces' => static::schema([ 'name' => 'is_string', - ]), - 'vetted' => 'is_bool', - ]); - // phpcs:enable - try { - $validated = $validator($definition); + ]), + 'vetted' => 'is_bool', + ]); + // phpcs:enable + try { + $validated = $validator($definition); + } catch (Exception $e) { + // Under any circumstance where the given recommendation configuration is + // invalid, we still want the rest of the script to proceed. I.e. it's + // better to produce a valid composer.json with a missing recommendation + // than to fail to create one at all. + return new NoRecommendation(); + } + return new AbandonmentRecommendation(Closure::fromCallable(function (ExtensionInterface $extension) use ($validated): bool { + return $extension->getName() === $validated['replaces']['name']; + }), $validated); + } + + public function applies(ExtensionInterface $extension): bool + { + if (($this->evaluateExtension)($extension)) { + array_push($this->appliedTo, $extension); + return true; + } + return false; + } + + public function getPackageName(): string + { + throw new LogicException(sprintf('It is nonsensical to call the %s() method on a % class instance.', __FUNCTION__, __CLASS__)); + } + + public function getVersionConstraint(): string + { + throw new LogicException(sprintf('It is nonsensical to call the %s() method on a %s class instance.', __FUNCTION__, __CLASS__)); + } + + public function hasModulesToInstall(): bool + { + throw new LogicException(sprintf('It is nonsensical to call the %s() method on a %s class instance.', __FUNCTION__, __CLASS__)); } - catch (Exception $e) { - // Under any circumstance where the given recommendation configuration is - // invalid, we still want the rest of the script to proceed. I.e. it's - // better to produce a valid composer.json with a missing recommendation - // than to fail to create one at all. - return new NoRecommendation(); + + /** + * {@inheritdoc} + */ + public function getModulesToInstall(): array + { + throw new LogicException(sprintf('It is nonsensical to call the %s() method on a %s class instance.', __FUNCTION__, __CLASS__)); + } + + public function hasPatches(): bool + { + throw new LogicException(sprintf('It is nonsensical to call the %s() method on a %s class instance.', __FUNCTION__, __CLASS__)); + } + + public function isVetted(): bool + { + throw new LogicException(sprintf('It is nonsensical to call the %s() method on a %s class instance.', __FUNCTION__, __CLASS__)); } - return new AbandonmentRecommendation(Closure::fromCallable(function (ExtensionInterface $extension) use ($validated): bool { - return $extension->getName() === $validated['replaces']['name']; - }), $validated); - } - - public function applies(ExtensionInterface $extension): bool { - if (($this->evaluateExtension)($extension)) { - array_push($this->appliedTo, $extension); - return TRUE; + + /** + * {@inheritdoc} + */ + public function getPatches(): array + { + throw new LogicException(sprintf('It is nonsensical to call the %s() method on a %s class instance.', __FUNCTION__, __CLASS__)); } - return FALSE; - } - - public function getPackageName(): string { - throw new LogicException(sprintf('It is nonsensical to call the %s() method on a % class instance.', __FUNCTION__, __CLASS__)); - } - - public function getVersionConstraint(): string { - throw new LogicException(sprintf('It is nonsensical to call the %s() method on a %s class instance.', __FUNCTION__, __CLASS__)); - } - - public function hasModulesToInstall(): bool { - throw new LogicException(sprintf('It is nonsensical to call the %s() method on a %s class instance.', __FUNCTION__, __CLASS__)); - } - - /** - * {@inheritdoc} - */ - public function getModulesToInstall(): array { - throw new LogicException(sprintf('It is nonsensical to call the %s() method on a %s class instance.', __FUNCTION__, __CLASS__)); - } - - public function hasPatches(): bool { - throw new LogicException(sprintf('It is nonsensical to call the %s() method on a %s class instance.', __FUNCTION__, __CLASS__)); - } - - public function isVetted(): bool { - throw new LogicException(sprintf('It is nonsensical to call the %s() method on a %s class instance.', __FUNCTION__, __CLASS__)); - } - - /** - * {@inheritdoc} - */ - public function getPatches(): array { - throw new LogicException(sprintf('It is nonsensical to call the %s() method on a %s class instance.', __FUNCTION__, __CLASS__)); - } - - /** - * {@inheritDoc} - */ - public function normalize(): array { - // phpcs:disable SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys - $normalized = [ - 'type' => 'abandonmentRecommendation', - 'id' => "abandon:{$this->definition['replaces']['name']}", - 'attributes' => [ + + /** + * {@inheritDoc} + */ + public function normalize(): array + { + // phpcs:disable SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys + $normalized = [ + 'type' => 'abandonmentRecommendation', + 'id' => "abandon:{$this->definition['replaces']['name']}", + 'attributes' => [ 'note' => $this->definition['note'], - ], - ]; - - $recommended_for = [ - 'data' => array_map(function (ExtensionInterface $extension) { - return [ - 'type' => $extension->isModule() ? 'module' : 'theme', - 'id' => $extension->getName(), + ], ]; - }, $this->appliedTo), - ]; - // phpcs:enable - if (!empty($recommended_for['data'])) { - $normalized['relationships']['recommendedFor'] = $recommended_for; - } - return $normalized; - } + $recommended_for = [ + 'data' => array_map(function (ExtensionInterface $extension) { + return [ + 'type' => $extension->isModule() ? 'module' : 'theme', + 'id' => $extension->getName(), + ]; + }, $this->appliedTo), + ]; + // phpcs:enable + if (!empty($recommended_for['data'])) { + $normalized['relationships']['recommendedFor'] = $recommended_for; + } + return $normalized; + } } diff --git a/src/Command/App/From/Recommendation/DefinedRecommendation.php b/src/Command/App/From/Recommendation/DefinedRecommendation.php index 0abcd74aa..c858c9c69 100644 --- a/src/Command/App/From/Recommendation/DefinedRecommendation.php +++ b/src/Command/App/From/Recommendation/DefinedRecommendation.php @@ -1,6 +1,6 @@ - */ - protected array $patches; - - /** - * An array of extensions to which this recommendation applied. - * - * @var \Acquia\Cli\Command\App\From\SourceSite\ExtensionInterface[] - */ - protected array $appliedTo = []; - - /** - * DefinedRecommendation constructor. - * - * @param \Closure $extension_evaluator - * An anonymous function that determines if this recommendation applies to - * an extension. - * @param string $package_name - * The name of the package recommended by this object. - * @param string $version_constraint - * The version constraint recommended by this object. - * @param string[] $install - * A list of recommended modules to install. - * @param bool $vetted - * Whether this is a vetted recommendation. - * @param string $note - * A note about the recommendation. - * @param array $patches - * An array of patch recommendations. - */ - protected function __construct(Closure $extension_evaluator, string $package_name, string $version_constraint, array $install, bool $vetted, string $note, array $patches = []) { - $this->evaluateExtension = $extension_evaluator; - $this->packageName = $package_name; - $this->versionConstraint = $version_constraint; - $this->install = $install; - $this->vetted = $vetted; - $this->note = $note; - $this->patches = $patches; - } - - /** - * Creates a new recommendation. - * - * @param mixed $definition - * A static recommendation definition. This must be an array. However, other - * value types are accepted because this method performs validation on the - * given value. - * @return \Acquia\Cli\Command\App\From\Recommendation\RecommendationInterface - * A new DefinedRecommendation object if the given definition is valid or - * a new NoRecommendation object otherwise. - */ - public static function createFromDefinition(mixed $definition): RecommendationInterface { - // phpcs:disable SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys - $defaults = [ - 'universal' => FALSE, - 'patches' => [], - 'install' => [], - 'vetted' => FALSE, - 'note' => static::NOTE_PLACEHOLDER_STRING, - ]; - $validate_if_universal_is_false = Closure::fromCallable(function ($context) { - return $context['universal'] === FALSE; - }); - - if (is_array($definition) && array_key_exists('package', $definition) && is_null($definition['package'])) { - return AbandonmentRecommendation::createFromDefinition($definition); +class DefinedRecommendation implements RecommendationInterface, NormalizableInterface +{ + use ArrayValidationTrait; + + /** + * Default note value. + * + * @const string + */ + const NOTE_PLACEHOLDER_STRING = '%note%'; + + /** + * An anonymous function that determines if this recommendation is applicable. + */ + protected \Closure $evaluateExtension; + + /** + * The name of a recommended package. + */ + protected string $packageName; + + /** + * A recommended composer version constraint. + */ + protected string $versionConstraint; + + /** + * A list of recommended modules to install. + * + * @var string[] + */ + protected array $install; + + /** + * Whether this is a vetted recommendation. + */ + protected bool $vetted; + + /** + * A note about the recommendation. + */ + protected string $note; + + /** + * A list of recommended patches. + * + * The keys of the array should be descriptions of the patch contents and the + * values should be URLs where the recommended patch can be downloaded. + * + * @var array + */ + protected array $patches; + + /** + * An array of extensions to which this recommendation applied. + * + * @var \Acquia\Cli\Command\App\From\SourceSite\ExtensionInterface[] + */ + protected array $appliedTo = []; + + /** + * DefinedRecommendation constructor. + * + * @param \Closure $extension_evaluator + * An anonymous function that determines if this recommendation applies to + * an extension. + * @param string $package_name + * The name of the package recommended by this object. + * @param string $version_constraint + * The version constraint recommended by this object. + * @param string[] $install + * A list of recommended modules to install. + * @param bool $vetted + * Whether this is a vetted recommendation. + * @param string $note + * A note about the recommendation. + * @param array $patches + * An array of patch recommendations. + */ + protected function __construct(Closure $extension_evaluator, string $package_name, string $version_constraint, array $install, bool $vetted, string $note, array $patches = []) + { + $this->evaluateExtension = $extension_evaluator; + $this->packageName = $package_name; + $this->versionConstraint = $version_constraint; + $this->install = $install; + $this->vetted = $vetted; + $this->note = $note; + $this->patches = $patches; } - $validator = static::schema([ - 'universal' => 'is_bool', - 'install' => static::listOf('is_string'), - 'package' => 'is_string', - 'constraint' => 'is_string', - 'note' => 'is_string', - 'replaces' => static::conditionalSchema([ + /** + * Creates a new recommendation. + * + * @param mixed $definition + * A static recommendation definition. This must be an array. However, other + * value types are accepted because this method performs validation on the + * given value. + * @return \Acquia\Cli\Command\App\From\Recommendation\RecommendationInterface + * A new DefinedRecommendation object if the given definition is valid or + * a new NoRecommendation object otherwise. + */ + public static function createFromDefinition(mixed $definition): RecommendationInterface + { + // phpcs:disable SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys + $defaults = [ + 'universal' => false, + 'patches' => [], + 'install' => [], + 'vetted' => false, + 'note' => static::NOTE_PLACEHOLDER_STRING, + ]; + $validate_if_universal_is_false = Closure::fromCallable(function ($context) { + return $context['universal'] === false; + }); + + if (is_array($definition) && array_key_exists('package', $definition) && is_null($definition['package'])) { + return AbandonmentRecommendation::createFromDefinition($definition); + } + + $validator = static::schema([ + 'universal' => 'is_bool', + 'install' => static::listOf('is_string'), + 'package' => 'is_string', + 'constraint' => 'is_string', + 'note' => 'is_string', + 'replaces' => static::conditionalSchema([ 'name' => 'is_string', - ], $validate_if_universal_is_false), - 'patches' => static::dictionaryOf('is_string'), - 'vetted' => 'is_bool', - ], $defaults); - // phpcs:enable - try { - $validated = $validator($definition); - } - catch (Exception $e) { - // Under any circumstance where the given recommendation configuration is - // invalid, we still want the rest of the script to proceed. I.e. it's - // better to produce a valid composer.json with a missing recommendation - // than to fail to create one at all. - return new NoRecommendation(); - } - $package_name = $validated['package']; - $version_constraint = $validated['constraint']; - $install = $validated['install']; - $patches = $validated['patches']; - $vetted = $validated['vetted']; - $note = $validated['note']; - if ($validated['universal']) { - return new UniversalRecommendation($package_name, $version_constraint, $install, $vetted, $note, $patches); + ], $validate_if_universal_is_false), + 'patches' => static::dictionaryOf('is_string'), + 'vetted' => 'is_bool', + ], $defaults); + // phpcs:enable + try { + $validated = $validator($definition); + } catch (Exception $e) { + // Under any circumstance where the given recommendation configuration is + // invalid, we still want the rest of the script to proceed. I.e. it's + // better to produce a valid composer.json with a missing recommendation + // than to fail to create one at all. + return new NoRecommendation(); + } + $package_name = $validated['package']; + $version_constraint = $validated['constraint']; + $install = $validated['install']; + $patches = $validated['patches']; + $vetted = $validated['vetted']; + $note = $validated['note']; + if ($validated['universal']) { + return new UniversalRecommendation($package_name, $version_constraint, $install, $vetted, $note, $patches); + } + return new DefinedRecommendation(Closure::fromCallable(function (ExtensionInterface $extension) use ($validated): bool { + return $extension->getName() === $validated['replaces']['name']; + }), $package_name, $version_constraint, $install, $vetted, $note, $patches); } - return new DefinedRecommendation(Closure::fromCallable(function (ExtensionInterface $extension) use ($validated): bool { - return $extension->getName() === $validated['replaces']['name']; - }), $package_name, $version_constraint, $install, $vetted, $note, $patches); - } - public function applies(ExtensionInterface $extension): bool { - if (($this->evaluateExtension)($extension)) { - array_push($this->appliedTo, $extension); - return TRUE; + public function applies(ExtensionInterface $extension): bool + { + if (($this->evaluateExtension)($extension)) { + array_push($this->appliedTo, $extension); + return true; + } + return false; } - return FALSE; - } - public function getPackageName(): string { - return $this->packageName; - } + public function getPackageName(): string + { + return $this->packageName; + } - public function getVersionConstraint(): string { - return $this->versionConstraint; - } + public function getVersionConstraint(): string + { + return $this->versionConstraint; + } - public function hasModulesToInstall(): bool { - return !empty($this->install); - } + public function hasModulesToInstall(): bool + { + return !empty($this->install); + } - /** - * {@inheritDoc} - */ - public function getModulesToInstall(): array { - return $this->install; - } + /** + * {@inheritDoc} + */ + public function getModulesToInstall(): array + { + return $this->install; + } - public function isVetted(): bool { - return $this->vetted; - } + public function isVetted(): bool + { + return $this->vetted; + } - public function hasPatches(): bool { - return !empty($this->patches); - } + public function hasPatches(): bool + { + return !empty($this->patches); + } - /** - * {@inheritDoc} - */ - public function getPatches(): array { - return $this->patches; - } + /** + * {@inheritDoc} + */ + public function getPatches(): array + { + return $this->patches; + } - /** - * {@inheritdoc} - */ - public function normalize(): array { - // phpcs:disable SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys - $normalized = [ - 'type' => 'packageRecommendation', - 'id' => "{$this->packageName}:{$this->versionConstraint}", - 'attributes' => [ + /** + * {@inheritdoc} + */ + public function normalize(): array + { + // phpcs:disable SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys + $normalized = [ + 'type' => 'packageRecommendation', + 'id' => "{$this->packageName}:{$this->versionConstraint}", + 'attributes' => [ 'requirePackage' => [ - 'name' => $this->packageName, - 'versionConstraint' => $this->versionConstraint, + 'name' => $this->packageName, + 'versionConstraint' => $this->versionConstraint, ], 'installModules' => $this->install, 'vetted' => $this->vetted, - ], - ]; - - if (!empty($this->note) && $this->note !== static::NOTE_PLACEHOLDER_STRING) { - $normalized['attributes']['note'] = $this->note; - } - - $recommended_for = [ - 'data' => array_map(function (ExtensionInterface $extension) { - return [ - 'type' => $extension->isModule() ? 'module' : 'theme', - 'id' => $extension->getName(), + ], ]; - }, $this->appliedTo), - ]; - // phpcs:enable - if (!empty($recommended_for['data'])) { - $normalized['relationships']['recommendedFor'] = $recommended_for; - } - $links = array_reduce(array_keys($this->patches), function (array $links, string $patch_description) { - $links['patch-file--' . md5($patch_description)] = [ - 'href' => $this->patches[$patch_description], - 'rel' => 'https://github.com/acquia/acquia_migrate#link-rel-patch-file', - 'title' => $patch_description, - ]; - return $links; - }, []); - if (!empty($links)) { - $normalized['links'] = $links; + if (!empty($this->note) && $this->note !== static::NOTE_PLACEHOLDER_STRING) { + $normalized['attributes']['note'] = $this->note; + } + + $recommended_for = [ + 'data' => array_map(function (ExtensionInterface $extension) { + return [ + 'type' => $extension->isModule() ? 'module' : 'theme', + 'id' => $extension->getName(), + ]; + }, $this->appliedTo), + ]; + // phpcs:enable + if (!empty($recommended_for['data'])) { + $normalized['relationships']['recommendedFor'] = $recommended_for; + } + + $links = array_reduce(array_keys($this->patches), function (array $links, string $patch_description) { + $links['patch-file--' . md5($patch_description)] = [ + 'href' => $this->patches[$patch_description], + 'rel' => 'https://github.com/acquia/acquia_migrate#link-rel-patch-file', + 'title' => $patch_description, + ]; + return $links; + }, []); + if (!empty($links)) { + $normalized['links'] = $links; + } + + return $normalized; } - - return $normalized; - } - } diff --git a/src/Command/App/From/Recommendation/NoRecommendation.php b/src/Command/App/From/Recommendation/NoRecommendation.php index 3f1884a3d..a2f4625dd 100644 --- a/src/Command/App/From/Recommendation/NoRecommendation.php +++ b/src/Command/App/From/Recommendation/NoRecommendation.php @@ -1,6 +1,6 @@ - */ - public function normalize(): array; - +interface NormalizableInterface +{ + /** + * Normalizes an object into a single- or multi-dimensional array of scalars. + * + * @return array + */ + public function normalize(): array; } diff --git a/src/Command/App/From/Recommendation/RecommendationInterface.php b/src/Command/App/From/Recommendation/RecommendationInterface.php index 959c90734..e7343df35 100644 --- a/src/Command/App/From/Recommendation/RecommendationInterface.php +++ b/src/Command/App/From/Recommendation/RecommendationInterface.php @@ -1,6 +1,6 @@ - * An associative array whose keys are a description of the patch's contents - * and whose values are URLs or relative paths to a patch file. - */ - public function getPatches(): array; + /** + * Whether the recommendation contains patches or not. + * + * @return bool + * TRUE if the recommendation contains patches; FALSE otherwise. + */ + public function hasPatches(): bool; + /** + * Gets an array of recommended patches for the recommended package. + * + * @return array + * An associative array whose keys are a description of the patch's contents + * and whose values are URLs or relative paths to a patch file. + */ + public function getPatches(): array; } diff --git a/src/Command/App/From/Recommendation/Recommendations.php b/src/Command/App/From/Recommendation/Recommendations.php index db964b051..90667a283 100644 --- a/src/Command/App/From/Recommendation/Recommendations.php +++ b/src/Command/App/From/Recommendation/Recommendations.php @@ -1,6 +1,6 @@ inspector = $inspector; + $this->universalRecommendations = new Recommendations([]); + $this->conditionalRecommendations = new Recommendations([]); + foreach ($recommendations as $recommendation) { + if ($recommendation instanceof UniversalRecommendation) { + $this->universalRecommendations->append($recommendation); + } else { + $this->conditionalRecommendations->append($recommendation); + } + } + } - /** - * Resolver constructor. - * - * @param \Acquia\Cli\Command\App\From\SourceSite\SiteInspectorInterface $inspector - * A site inspector. - * @param \Acquia\Cli\Command\App\From\Recommendation\Recommendations $recommendations - * A set of defined recommendations. These are *all possible* - * recommendations. It is the resolves job to narrow these down by using the - * site inspector to retrieve information about the source site. - */ - public function __construct(SiteInspectorInterface $inspector, Recommendations $recommendations) { - $this->inspector = $inspector; - $this->universalRecommendations = new Recommendations([]); - $this->conditionalRecommendations = new Recommendations([]); - foreach ($recommendations as $recommendation) { - if ($recommendation instanceof UniversalRecommendation) { - $this->universalRecommendations->append($recommendation); - } - else { - $this->conditionalRecommendations->append($recommendation); - } + /** + * Gets a recommendation for the given extension. + * + * @return \Acquia\Cli\Command\App\From\Recommendation\Recommendations + * A resolved suite of recommendations. + */ + public function getRecommendations(): Recommendations + { + $enabled_modules = $this->inspector->getExtensions(SiteInspectorInterface::FLAG_EXTENSION_ENABLED | SiteInspectorInterface::FLAG_EXTENSION_MODULE); + return array_reduce($enabled_modules, function (Recommendations $recommendations, ExtensionInterface $extension) { + $resolutions = $this->getRecommendationsForExtension($extension); + foreach ($resolutions as $resolution) { + if (!$resolution instanceof NoRecommendation) { + $recommendations->append($resolution); + } + } + return $recommendations; + }, $this->universalRecommendations); } - } - /** - * Gets a recommendation for the given extension. - * - * @return \Acquia\Cli\Command\App\From\Recommendation\Recommendations - * A resolved suite of recommendations. - */ - public function getRecommendations(): Recommendations { - $enabled_modules = $this->inspector->getExtensions(SiteInspectorInterface::FLAG_EXTENSION_ENABLED | SiteInspectorInterface::FLAG_EXTENSION_MODULE); - return array_reduce($enabled_modules, function (Recommendations $recommendations, ExtensionInterface $extension) { - $resolutions = $this->getRecommendationsForExtension($extension); - foreach ($resolutions as $resolution) { - if (!$resolution instanceof NoRecommendation) { - $recommendations->append($resolution); + /** + * Gets a recommendation for the given extension. + * + * @param \Acquia\Cli\Command\App\From\SourceSite\ExtensionInterface $extension + * A Drupal 7 extension for which a package recommendation should be + * resolved. + * @return \Acquia\Cli\Command\App\From\Recommendation\Recommendations + * A resolved recommendation. + */ + protected function getRecommendationsForExtension(ExtensionInterface $extension): Recommendations + { + $recommendations = new Recommendations(); + foreach ($this->conditionalRecommendations as $recommendation) { + if ($recommendation->applies($extension)) { + $recommendations->append($recommendation); + } } - } - return $recommendations; - }, $this->universalRecommendations); - } - - /** - * Gets a recommendation for the given extension. - * - * @param \Acquia\Cli\Command\App\From\SourceSite\ExtensionInterface $extension - * A Drupal 7 extension for which a package recommendation should be - * resolved. - * @return \Acquia\Cli\Command\App\From\Recommendation\Recommendations - * A resolved recommendation. - */ - protected function getRecommendationsForExtension(ExtensionInterface $extension): Recommendations { - $recommendations = new Recommendations(); - foreach ($this->conditionalRecommendations as $recommendation) { - if ($recommendation->applies($extension)) { - $recommendations->append($recommendation); - } + return $recommendations; } - return $recommendations; - } - } diff --git a/src/Command/App/From/Recommendation/UniversalRecommendation.php b/src/Command/App/From/Recommendation/UniversalRecommendation.php index cb1760cc4..f102f679d 100644 --- a/src/Command/App/From/Recommendation/UniversalRecommendation.php +++ b/src/Command/App/From/Recommendation/UniversalRecommendation.php @@ -1,6 +1,6 @@ $value) { - if (!$key_validator($index)) { - throw new DomainException("The array key `$index` must be an integer or a string."); - } - elseif ($value_validator instanceof StructuredArrayValidator || $value_validator instanceof Closure) { - $values[$index] = $value_validator($value); - } - elseif (!call_user_func_array($value_validator, [$value])) { - throw new DomainException('Failed to validate value.'); - } - } - return $values; - }); - } + /** + * Creates a validator for an associative array with arbitrary string as keys. + * + * @param callable $entry_validator + * A validator to apply to each entry in a validated array. + * @return \Closure + * A validation function. + */ + protected static function dictionaryOf(callable $entry_validator): Closure + { + return static::arrayOf('is_string', $entry_validator); + } + /** + * Creates an arbitrarily keyed array validator. + * + * @param callable $key_validator + * A callable, either 'is_int' or 'is_string' to check against each key of + * the given array. + * @param callable $value_validator + * A callable to evaluate against each value in the given array. + * @return \Closure + * A validation function. + */ + private static function arrayOf(callable $key_validator, callable $value_validator): \Closure + { + assert(in_array($key_validator, ['is_int', 'is_string'], true)); + return Closure::fromCallable(function ($values) use ($key_validator, $value_validator) { + if (!is_array($values)) { + throw new DomainException('Validated value is not an array.'); + } + foreach ($values as $index => $value) { + if (!$key_validator($index)) { + throw new DomainException("The array key `$index` must be an integer or a string."); + } elseif ($value_validator instanceof StructuredArrayValidator || $value_validator instanceof Closure) { + $values[$index] = $value_validator($value); + } elseif (!call_user_func_array($value_validator, [$value])) { + throw new DomainException('Failed to validate value.'); + } + } + return $values; + }); + } } diff --git a/src/Command/App/From/Safety/StructuredArrayValidator.php b/src/Command/App/From/Safety/StructuredArrayValidator.php index 3e3d63a47..25bc4a7da 100644 --- a/src/Command/App/From/Safety/StructuredArrayValidator.php +++ b/src/Command/App/From/Safety/StructuredArrayValidator.php @@ -1,6 +1,6 @@ + */ + protected array $schema; - /** - * A schema definition for the array to be validated. - * - * @var array - */ - protected array $schema; + /** + * A set of defaults for the array to be validated. + * + * @var array + */ + protected array $defaults; - /** - * A set of defaults for the array to be validated. - * - * @var array - */ - protected array $defaults; + /** + * Whether the schema is conditional or not. + * + * @var bool|callable + */ + protected $conditional; - /** - * Whether the schema is conditional or not. - * - * @var bool|callable - */ - protected $conditional; - - /** - * ArrayValidator constructor. - * - * @param array $schema - * A schema definition for the array to be validated. - * @param array $defaults - * A set of defaults for the array to be validated. - * @param bool|\Closure $conditional - * A callable or FALSE. See self::createChildValidator(). - */ - protected function __construct(array $schema, array $defaults, bool|\Closure $conditional) { - assert(!isset($schema[static::KEYS_ARE_STRINGS]) || empty(array_diff_key($schema, array_flip([static::KEYS_ARE_STRINGS]))), 'A schema must contain either the KEYS_ARE_STRINGS constant or validations for specific array keys, but not both.'); - assert($conditional === FALSE || $conditional instanceof Closure); - $this->schema = $schema; - $this->defaults = $defaults; - $this->conditional = $conditional; - } - - /** - * Creates a new ArrayValidator. - * - * @param array $schema - * A schema definition for the array to be validated. - * @param array $defaults - * A set of defaults for the array to be validated. - * @return static - * A new Array validator. - */ - public static function create(array $schema, array $defaults = []): static { - return new static($schema, $defaults, FALSE); - } + /** + * ArrayValidator constructor. + * + * @param array $schema + * A schema definition for the array to be validated. + * @param array $defaults + * A set of defaults for the array to be validated. + * @param bool|\Closure $conditional + * A callable or FALSE. See self::createChildValidator(). + */ + protected function __construct(array $schema, array $defaults, bool|\Closure $conditional) + { + assert(!isset($schema[static::KEYS_ARE_STRINGS]) || empty(array_diff_key($schema, array_flip([static::KEYS_ARE_STRINGS]))), 'A schema must contain either the KEYS_ARE_STRINGS constant or validations for specific array keys, but not both.'); + assert($conditional === false || $conditional instanceof Closure); + $this->schema = $schema; + $this->defaults = $defaults; + $this->conditional = $conditional; + } - /** - * Creates a new ArrayValidator. - * - * Note: only child validators, that is, validators that are children of - * another validator should be conditional. - * - * @param array $schema - * A schema definition for the array to be validated. - * @param \Closure $conditional - * The function should be a function that receives the context array and - * returns a bool. If TRUE, the validation will be applied, otherwise it - * will be skipped and the value will be omitted from the final validated - * array. - * @param array $defaults - * A set of defaults for the array to be validated. - * @return static - * A new Array validator. - */ - public static function createConditionalValidator(array $schema, Closure $conditional, array $defaults = []): static { - return new static($schema, $defaults, $conditional); - } + /** + * Creates a new ArrayValidator. + * + * @param array $schema + * A schema definition for the array to be validated. + * @param array $defaults + * A set of defaults for the array to be validated. + * @return static + * A new Array validator. + */ + public static function create(array $schema, array $defaults = []): static + { + return new static($schema, $defaults, false); + } - /** - * Validates and returns a validated array. - * - * @param mixed $arr - * An array to validate. - * @return array - * If the given $arr is valid, the given $arr value. Array keys not defined - * in the schema definition will be stripped from return value. - */ - public function __invoke(mixed $arr): array { - if (!is_array($arr)) { - throw new DomainException('Validated value is not an array.'); + /** + * Creates a new ArrayValidator. + * + * Note: only child validators, that is, validators that are children of + * another validator should be conditional. + * + * @param array $schema + * A schema definition for the array to be validated. + * @param \Closure $conditional + * The function should be a function that receives the context array and + * returns a bool. If TRUE, the validation will be applied, otherwise it + * will be skipped and the value will be omitted from the final validated + * array. + * @param array $defaults + * A set of defaults for the array to be validated. + * @return static + * A new Array validator. + */ + public static function createConditionalValidator(array $schema, Closure $conditional, array $defaults = []): static + { + return new static($schema, $defaults, $conditional); } - $arr += $this->defaults; - // The schema must define expected keys, so validate each key accordingly. - foreach ($this->schema as $schema_key => $validation) { - // Validate in every case except where the validation is conditional and - // the condition does *not* evaluate to TRUE. - $should_validate = !$validation instanceof self || !$validation->isConditional() || ($validation->conditional)($arr); - if ($should_validate && !array_key_exists($schema_key, $arr)) { - throw new DomainException("Missing required key: $schema_key"); - } - else { - // If the validation does not apply, omit the value from the validated - // return value. - if ($validation instanceof self && !$should_validate) { - unset($arr[$schema_key]); + + /** + * Validates and returns a validated array. + * + * @param mixed $arr + * An array to validate. + * @return array + * If the given $arr is valid, the given $arr value. Array keys not defined + * in the schema definition will be stripped from return value. + */ + public function __invoke(mixed $arr): array + { + if (!is_array($arr)) { + throw new DomainException('Validated value is not an array.'); } - else { - if ($validation instanceof self || $validation instanceof Closure) { - $arr[$schema_key] = $validation($arr[$schema_key]); - } - elseif (!call_user_func_array($validation, [$arr[$schema_key]])) { - throw new DomainException('Failed to validate value.'); - } + $arr += $this->defaults; + // The schema must define expected keys, so validate each key accordingly. + foreach ($this->schema as $schema_key => $validation) { + // Validate in every case except where the validation is conditional and + // the condition does *not* evaluate to TRUE. + $should_validate = !$validation instanceof self || !$validation->isConditional() || ($validation->conditional)($arr); + if ($should_validate && !array_key_exists($schema_key, $arr)) { + throw new DomainException("Missing required key: $schema_key"); + } else { + // If the validation does not apply, omit the value from the validated + // return value. + if ($validation instanceof self && !$should_validate) { + unset($arr[$schema_key]); + } else { + if ($validation instanceof self || $validation instanceof Closure) { + $arr[$schema_key] = $validation($arr[$schema_key]); + } elseif (!call_user_func_array($validation, [$arr[$schema_key]])) { + throw new DomainException('Failed to validate value.'); + } + } + } } - } + return array_intersect_key($arr, $this->schema); } - return array_intersect_key($arr, $this->schema); - } - /** - * Whether the given argument is valid. - * - * @param mixed $arr - * An array to be validated. - * @return bool - * TRUE if the argument is valid; FALSE otherwise. - */ - public function isValid(mixed $arr): bool { - try { - $this($arr); - } - catch (Exception $e) { - return FALSE; + /** + * Whether the given argument is valid. + * + * @param mixed $arr + * An array to be validated. + * @return bool + * TRUE if the argument is valid; FALSE otherwise. + */ + public function isValid(mixed $arr): bool + { + try { + $this($arr); + } catch (Exception $e) { + return false; + } + return true; } - return TRUE; - } - - /** - * Whether the validator is conditional. - * - * @return bool - * TRUE if the validator may or not be applied, depending on context. FALSE - * if the validator will be applied unconditionally. - */ - public function isConditional(): bool { - return (bool) $this->conditional; - } + /** + * Whether the validator is conditional. + * + * @return bool + * TRUE if the validator may or not be applied, depending on context. FALSE + * if the validator will be applied unconditionally. + */ + public function isConditional(): bool + { + return (bool) $this->conditional; + } } diff --git a/src/Command/App/From/SourceSite/Drupal7Extension.php b/src/Command/App/From/SourceSite/Drupal7Extension.php index 4c323f648..fe9cae37a 100644 --- a/src/Command/App/From/SourceSite/Drupal7Extension.php +++ b/src/Command/App/From/SourceSite/Drupal7Extension.php @@ -1,105 +1,112 @@ type = $type; - $this->name = $name; - $this->enabled = $enabled; - $this->humanName = !empty($human_name) ? $human_name : $name; - $this->version = $version; - } - - /** - * Creates an extension object given a Drush extension object. - * - * @param object $extension - * An extension object as returned by drush_get_extensions(). - * @return \Acquia\Cli\Command\App\From\SourceSite\Drupal7Extension - * A new extension. - */ - public static function createFromStdClass(object $extension): Drupal7Extension { - return new static( - $extension->type, - $extension->name, - $extension->status, - $extension->humanName ?? $extension->name, - $extension->version ?? 'Unknown', - ); - } - - public function getName(): string { - return $this->name; - } - - public function getHumanName(): string { - return $this->humanName; - } - - public function getVersion(): string { - return $this->version; - } - - public function isModule(): bool { - return $this->type === 'module'; - } - - public function isTheme(): bool { - return $this->type === 'theme'; - } - - public function isEnabled(): bool { - return $this->enabled; - } - +final class Drupal7Extension implements ExtensionInterface +{ + /** + * The type of the extension. + * + * @var string + * Either 'module' or 'theme'. + */ + protected string $type; + + /** + * The name of the extension. + */ + protected string $name; + + /** + * Whether the extension is enabled or not. + */ + protected bool $enabled; + + /** + * The human-readable name of the extension. + */ + protected string $humanName; + + /** + * The extension's version. + */ + protected string $version; + + /** + * Extension constructor. + * + * @param string $type + * The extension type. Either 'module' or 'theme'. + * @param string $name + * The extension name. + * @param bool $enabled + * Whether the extension is enabled or not. + * @param string $human_name + * The human-readable name of the extension. + * @param string $version + * The extension version. + */ + protected function __construct(string $type, string $name, bool $enabled, string $human_name, string $version) + { + assert(in_array($type, ['module', 'theme'])); + $this->type = $type; + $this->name = $name; + $this->enabled = $enabled; + $this->humanName = !empty($human_name) ? $human_name : $name; + $this->version = $version; + } + + /** + * Creates an extension object given a Drush extension object. + * + * @param object $extension + * An extension object as returned by drush_get_extensions(). + * @return \Acquia\Cli\Command\App\From\SourceSite\Drupal7Extension + * A new extension. + */ + public static function createFromStdClass(object $extension): Drupal7Extension + { + return new static( + $extension->type, + $extension->name, + $extension->status, + $extension->humanName ?? $extension->name, + $extension->version ?? 'Unknown', + ); + } + + public function getName(): string + { + return $this->name; + } + + public function getHumanName(): string + { + return $this->humanName; + } + + public function getVersion(): string + { + return $this->version; + } + + public function isModule(): bool + { + return $this->type === 'module'; + } + + public function isTheme(): bool + { + return $this->type === 'theme'; + } + + public function isEnabled(): bool + { + return $this->enabled; + } } diff --git a/src/Command/App/From/SourceSite/Drupal7SiteInspector.php b/src/Command/App/From/SourceSite/Drupal7SiteInspector.php index 8e03cfe19..78c51b85e 100644 --- a/src/Command/App/From/SourceSite/Drupal7SiteInspector.php +++ b/src/Command/App/From/SourceSite/Drupal7SiteInspector.php @@ -1,6 +1,6 @@ root = $drupal_root; - $this->uri = $uri; - } - - /** - * {@inheritDoc} - * - * Uses drush to get all known extensions on the context Drupal 7 site. - */ - protected function readExtensions(): array { - $this->bootstrap(); - // @phpstan-ignore-next-line - $enabled = system_list('module_enabled'); - // Special case to remove 'standard' from the module's list. - unset($enabled['standard']); - $modules = array_values(array_map(function (string $name) use ($enabled) { - // phpcs:disable SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys - return (object) [ - 'name' => $name, - 'status' => TRUE, - 'type' => 'module', - 'humanName' => $enabled[$name]->info['name'], - 'version' => $enabled[$name]->info['version'], - ]; - // phpcs:enable - }, array_keys($enabled))); - return array_map([Drupal7Extension::class, 'createFromStdClass'], $modules); - } - - public function getPublicFilePath(): string { - $this->bootstrap(); - // @see https://git.drupalcode.org/project/drupal/-/blob/7.x/includes/stream_wrappers.inc#L919 - // @phpstan-ignore-next-line - return variable_get('file_public_path', conf_path() . '/files'); - } - - public function getPrivateFilePath(): ?string { - $this->bootstrap(); - // @phpstan-ignore-next-line - return variable_get('file_private_path', NULL); - } - - /** - * Bootstraps the inspected Drupal site. - */ - protected function bootstrap(): void { - static $bootstrapped; - if ($bootstrapped) { - return; +final class Drupal7SiteInspector extends SiteInspectorBase +{ + /** + * The path to Drupal site root. + */ + protected string $root; + + /** + * The host name to use in order to resolve the appropriate settings.php. + */ + public string $uri; + + /** + * Drupal7SiteInspector constructor. + * + * @param string $drupal_root + * The path to Drupal site root. + * @param string $uri + * (optional) The host name to use in order to resolve the appropriate + * settings.php directory. Defaults to 'default'. + */ + public function __construct(string $drupal_root, string $uri = 'default') + { + $this->root = $drupal_root; + $this->uri = $uri; } - $previous_directory = getcwd(); - chdir($this->root); - if (!defined('DRUPAL_ROOT')) { - define('DRUPAL_ROOT', $this->root); + + /** + * {@inheritDoc} + * + * Uses drush to get all known extensions on the context Drupal 7 site. + */ + protected function readExtensions(): array + { + $this->bootstrap(); + // @phpstan-ignore-next-line + $enabled = system_list('module_enabled'); + // Special case to remove 'standard' from the module's list. + unset($enabled['standard']); + $modules = array_values(array_map(function (string $name) use ($enabled) { + // phpcs:disable SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys + return (object) [ + 'name' => $name, + 'status' => true, + 'type' => 'module', + 'humanName' => $enabled[$name]->info['name'], + 'version' => $enabled[$name]->info['version'], + ]; + // phpcs:enable + }, array_keys($enabled))); + return array_map([Drupal7Extension::class, 'createFromStdClass'], $modules); } - // phpcs:disable SlevomatCodingStandard.Variables.DisallowSuperGlobalVariable - $_SERVER['HTTP_HOST'] = $this->uri; - $_SERVER['REQUEST_URI'] = $this->uri . '/'; - $_SERVER['PHP_SELF'] = $_SERVER['REQUEST_URI'] . 'index.php'; - $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; - $_SERVER['REQUEST_METHOD'] = 'GET'; - $_SERVER['SERVER_SOFTWARE'] = NULL; - $_SERVER['HTTP_USER_AGENT'] = 'console'; - $_SERVER['SCRIPT_FILENAME'] = DRUPAL_ROOT . '/index.php'; - // phpcs:enable SlevomatCodingStandard.Variables.DisallowSuperGlobalVariable - require_once DRUPAL_ROOT . '/includes/bootstrap.inc'; - // @phpstan-ignore-next-line - drupal_bootstrap(DRUPAL_BOOTSTRAP_VARIABLES); - chdir($previous_directory); - $bootstrapped = TRUE; - } - /** - * Validates the given Drupal 7 application root. - * - * @param string $path - * The path to validate. - * @return string - * The received Drupal 7 path, if it is valid, without trailing slashes. - */ - public static function validateDrupal7Root(string $path): string { - $path = rtrim($path, '/'); - if (!file_exists($path)) { - throw new ValidatorException(sprintf("The path '%s' does not exist. Please enter the absolute path to a Drupal 7 application root.", $path)); + public function getPublicFilePath(): string + { + $this->bootstrap(); + // @see https://git.drupalcode.org/project/drupal/-/blob/7.x/includes/stream_wrappers.inc#L919 + // @phpstan-ignore-next-line + return variable_get('file_public_path', conf_path() . '/files'); } - if (!file_exists("$path/index.php")) { - throw new ValidatorException(sprintf("The '%s' directory does not seem to be the root of a Drupal 7 application. It does not contain a index.php file.", $path)); + + public function getPrivateFilePath(): ?string + { + $this->bootstrap(); + // @phpstan-ignore-next-line + return variable_get('file_private_path', null); } - if (!file_exists("$path/sites/default/default.settings.php")) { - throw new ValidatorException(sprintf("The '%s' directory does not seem to be the root of a Drupal 7 application. It does not contain a sites/default/default.settings.php.", $path)); + + /** + * Bootstraps the inspected Drupal site. + */ + protected function bootstrap(): void + { + static $bootstrapped; + if ($bootstrapped) { + return; + } + $previous_directory = getcwd(); + chdir($this->root); + if (!defined('DRUPAL_ROOT')) { + define('DRUPAL_ROOT', $this->root); + } + // phpcs:disable SlevomatCodingStandard.Variables.DisallowSuperGlobalVariable + $_SERVER['HTTP_HOST'] = $this->uri; + $_SERVER['REQUEST_URI'] = $this->uri . '/'; + $_SERVER['PHP_SELF'] = $_SERVER['REQUEST_URI'] . 'index.php'; + $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; + $_SERVER['REQUEST_METHOD'] = 'GET'; + $_SERVER['SERVER_SOFTWARE'] = null; + $_SERVER['HTTP_USER_AGENT'] = 'console'; + $_SERVER['SCRIPT_FILENAME'] = DRUPAL_ROOT . '/index.php'; + // phpcs:enable SlevomatCodingStandard.Variables.DisallowSuperGlobalVariable + require_once DRUPAL_ROOT . '/includes/bootstrap.inc'; + // @phpstan-ignore-next-line + drupal_bootstrap(DRUPAL_BOOTSTRAP_VARIABLES); + chdir($previous_directory); + $bootstrapped = true; } - return $path; - } - /** - * Determines the best URI to use for bootstrapping the source site. - * - * @param \Symfony\Component\Console\Input\InputInterface $input - * The input passed into this Symfony command. - * @param string $drupal_root - * The root of the source site. - * @return string - * A URI string corresponding to an installed site. - */ - public static function getSiteUri(InputInterface $input, string $drupal_root): string { - // Construct a list of site directories which contain a settings.php file. - $site_dirs = array_map(function ($path) use ($drupal_root) { - return substr($path, strlen("$drupal_root/sites/"), -1 * strlen('/settings.php')); - }, glob("$drupal_root/sites/*/settings.php")); - // If the --drupal7-uri flag is defined, defer to it and attempt to ensure that it's - // valid. - if ($input->getOption('drupal7-uri') !== NULL) { - $uri = $input->getOption('drupal7-uri'); - $sites_location = "$drupal_root/sites/sites.php"; - // If there isn't a sites.php file and the URI does not correspond to a - // site directory, the site will be unable to bootstrap. - if (!file_exists($sites_location) && !in_array($uri, $site_dirs, TRUE)) { - throw new \InvalidArgumentException( - sprintf('The given --drupal7-uri value does not correspond to an installed sites directory and a sites.php file could not be located.'), - NewFromDrupal7Command::ERR_UNRECOGNIZED_HOST - ); - } - // Parse the contents of sites.php. - $sites = []; - // This will override $sites. - // @see https://git.drupalcode.org/project/drupal/-/blob/7.x/includes/bootstrap.inc#L563 - include $sites_location; + /** + * Validates the given Drupal 7 application root. + * + * @param string $path + * The path to validate. + * @return string + * The received Drupal 7 path, if it is valid, without trailing slashes. + */ + public static function validateDrupal7Root(string $path): string + { + $path = rtrim($path, '/'); + if (!file_exists($path)) { + throw new ValidatorException(sprintf("The path '%s' does not exist. Please enter the absolute path to a Drupal 7 application root.", $path)); + } + if (!file_exists("$path/index.php")) { + throw new ValidatorException(sprintf("The '%s' directory does not seem to be the root of a Drupal 7 application. It does not contain a index.php file.", $path)); + } + if (!file_exists("$path/sites/default/default.settings.php")) { + throw new ValidatorException(sprintf("The '%s' directory does not seem to be the root of a Drupal 7 application. It does not contain a sites/default/default.settings.php.", $path)); + } + return $path; + } - // @phpstan-ignore-next-line - if (!empty($sites)) { - // If the URI corresponds to a configuration in sites.php, then ensure - // that the identified directory also has a settings.php file. If it - // does not, then the site is probably not installed. - if (isset($sites[$uri])) { - if (!in_array($sites[$uri], $site_dirs, TRUE)) { - throw new \InvalidArgumentException( - sprintf('The given --drupal7-uri value corresponds to a site directory in sites.php, but that directory does not have a settings.php file. This typically means that the site has not been installed.'), - NewFromDrupal7Command::ERR_UNRECOGNIZED_HOST - ); - } - // The URI is assumed to be valid. - return $uri; + /** + * Determines the best URI to use for bootstrapping the source site. + * + * @param \Symfony\Component\Console\Input\InputInterface $input + * The input passed into this Symfony command. + * @param string $drupal_root + * The root of the source site. + * @return string + * A URI string corresponding to an installed site. + */ + public static function getSiteUri(InputInterface $input, string $drupal_root): string + { + // Construct a list of site directories which contain a settings.php file. + $site_dirs = array_map(function ($path) use ($drupal_root) { + return substr($path, strlen("$drupal_root/sites/"), -1 * strlen('/settings.php')); + }, glob("$drupal_root/sites/*/settings.php")); + // If the --drupal7-uri flag is defined, defer to it and attempt to ensure that it's + // valid. + if ($input->getOption('drupal7-uri') !== null) { + $uri = $input->getOption('drupal7-uri'); + $sites_location = "$drupal_root/sites/sites.php"; + // If there isn't a sites.php file and the URI does not correspond to a + // site directory, the site will be unable to bootstrap. + if (!file_exists($sites_location) && !in_array($uri, $site_dirs, true)) { + throw new \InvalidArgumentException( + sprintf('The given --drupal7-uri value does not correspond to an installed sites directory and a sites.php file could not be located.'), + NewFromDrupal7Command::ERR_UNRECOGNIZED_HOST + ); + } + // Parse the contents of sites.php. + $sites = []; + // This will override $sites. + // @see https://git.drupalcode.org/project/drupal/-/blob/7.x/includes/bootstrap.inc#L563 + include $sites_location; + + // @phpstan-ignore-next-line + if (!empty($sites)) { + // If the URI corresponds to a configuration in sites.php, then ensure + // that the identified directory also has a settings.php file. If it + // does not, then the site is probably not installed. + if (isset($sites[$uri])) { + if (!in_array($sites[$uri], $site_dirs, true)) { + throw new \InvalidArgumentException( + sprintf('The given --drupal7-uri value corresponds to a site directory in sites.php, but that directory does not have a settings.php file. This typically means that the site has not been installed.'), + NewFromDrupal7Command::ERR_UNRECOGNIZED_HOST + ); + } + // The URI is assumed to be valid. + return $uri; + } + // The given URI doesn't match anything in sites.php. + throw new \InvalidArgumentException( + sprintf('The given --drupal7-uri value does not correspond to any configuration in sites.php.'), + NewFromDrupal7Command::ERR_UNRECOGNIZED_HOST + ); + } + + if (in_array($uri, $site_dirs, true)) { + return $uri; + } } - // The given URI doesn't match anything in sites.php. + // There was no --drupal7-uri flag specified, so attempt to determine a sane + // default. If there is only one possible site, use it. If there is more + // than one, but there is a default directory with a settings.php, use that. + if (count($site_dirs) === 1) { + return current($site_dirs); + } elseif (in_array('default', $site_dirs, true)) { + return 'default'; + } + // A URI corresponding to a site directory could not be determined, rather + // than make a faulty assumption (e.g. use the first found), exit. throw new \InvalidArgumentException( - sprintf('The given --drupal7-uri value does not correspond to any configuration in sites.php.'), - NewFromDrupal7Command::ERR_UNRECOGNIZED_HOST + sprintf('A Drupal 7 installation could not be located.'), + NewFromDrupal7Command::ERR_INDETERMINATE_SITE ); - } - - if (in_array($uri, $site_dirs, TRUE)) { - return $uri; - } - } - // There was no --drupal7-uri flag specified, so attempt to determine a sane - // default. If there is only one possible site, use it. If there is more - // than one, but there is a default directory with a settings.php, use that. - if (count($site_dirs) === 1) { - return current($site_dirs); } - elseif (in_array('default', $site_dirs, TRUE)) { - return 'default'; - } - // A URI corresponding to a site directory could not be determined, rather - // than make a faulty assumption (e.g. use the first found), exit. - throw new \InvalidArgumentException( - sprintf('A Drupal 7 installation could not be located.'), - NewFromDrupal7Command::ERR_INDETERMINATE_SITE - ); - } - } diff --git a/src/Command/App/From/SourceSite/ExportedDrupal7ExtensionsInspector.php b/src/Command/App/From/SourceSite/ExportedDrupal7ExtensionsInspector.php index 4fcff9ea7..a382738ae 100644 --- a/src/Command/App/From/SourceSite/ExportedDrupal7ExtensionsInspector.php +++ b/src/Command/App/From/SourceSite/ExportedDrupal7ExtensionsInspector.php @@ -1,66 +1,71 @@ extensions; + } - /** - * {@inheritDoc} - */ - protected function readExtensions(): array { - return $this->extensions; - } + public function getPublicFilePath(): string + { + return 'sites/default/files'; + } - public function getPublicFilePath(): string { - return 'sites/default/files'; - } - - public function getPrivateFilePath(): ?string { - return NULL; - } - - /** - * Reads an extensions resource into extensions objects. - * - * @param resource $extensions_resource - * A serialized extensions resource from which to parse extensions. - * @return \Acquia\Cli\Command\App\From\SourceSite\Drupal7Extension[] - * An array of extensions. - */ - protected static function parseExtensionsFromResource($extensions_resource): array { - return array_map(function (array $extension) { - $extension['status'] = $extension['enabled']; - return Drupal7Extension::createFromStdClass((object) $extension); - }, static::parseJsonResource($extensions_resource)); - } + public function getPrivateFilePath(): ?string + { + return null; + } + /** + * Reads an extensions resource into extensions objects. + * + * @param resource $extensions_resource + * A serialized extensions resource from which to parse extensions. + * @return \Acquia\Cli\Command\App\From\SourceSite\Drupal7Extension[] + * An array of extensions. + */ + protected static function parseExtensionsFromResource($extensions_resource): array + { + return array_map(function (array $extension) { + $extension['status'] = $extension['enabled']; + return Drupal7Extension::createFromStdClass((object) $extension); + }, static::parseJsonResource($extensions_resource)); + } } diff --git a/src/Command/App/From/SourceSite/ExtensionInterface.php b/src/Command/App/From/SourceSite/ExtensionInterface.php index c7607fbb7..371ee573a 100644 --- a/src/Command/App/From/SourceSite/ExtensionInterface.php +++ b/src/Command/App/From/SourceSite/ExtensionInterface.php @@ -1,60 +1,59 @@ readExtensions(), function (ExtensionInterface $extension) use ($state_flags, $type_flags) { - // Generate a flag for the extension's enabled/disabled state. - $has = $extension->isEnabled() ? SiteInspectorInterface::FLAG_EXTENSION_ENABLED : SiteInspectorInterface::FLAG_EXTENSION_DISABLED; - // Incorporate the extension's type. - $has = $has | ($extension->isModule() ? SiteInspectorInterface::FLAG_EXTENSION_MODULE : 0); - $has = $has | ($extension->isTheme() ? SiteInspectorInterface::FLAG_EXTENSION_THEME : 0); - // TRUE if the extension has a flag in $type_flags AND a flag in - // $state_flags, FALSE otherwise. - return ($has & $type_flags) && ($has & $state_flags); - }); - } - - /** - * Returns a list of extensions discovered on the inspected site. - * - * @return \Acquia\Cli\Command\App\From\SourceSite\ExtensionInterface[] - * An array of extensions discovered on the inspected source site. - */ - abstract protected function readExtensions(): array; +abstract class SiteInspectorBase implements SiteInspectorInterface +{ + /** + * {@inheritDoc} + */ + public function getExtensions(int $flags): array + { + $state_flags = $flags & (SiteInspectorInterface::FLAG_EXTENSION_ENABLED | SiteInspectorInterface::FLAG_EXTENSION_DISABLED); + $type_flags = $flags & (SiteInspectorInterface::FLAG_EXTENSION_MODULE | SiteInspectorInterface::FLAG_EXTENSION_THEME); + return array_filter($this->readExtensions(), function (ExtensionInterface $extension) use ($state_flags, $type_flags) { + // Generate a flag for the extension's enabled/disabled state. + $has = $extension->isEnabled() ? SiteInspectorInterface::FLAG_EXTENSION_ENABLED : SiteInspectorInterface::FLAG_EXTENSION_DISABLED; + // Incorporate the extension's type. + $has = $has | ($extension->isModule() ? SiteInspectorInterface::FLAG_EXTENSION_MODULE : 0); + $has = $has | ($extension->isTheme() ? SiteInspectorInterface::FLAG_EXTENSION_THEME : 0); + // TRUE if the extension has a flag in $type_flags AND a flag in + // $state_flags, FALSE otherwise. + return ($has & $type_flags) && ($has & $state_flags); + }); + } + /** + * Returns a list of extensions discovered on the inspected site. + * + * @return \Acquia\Cli\Command\App\From\SourceSite\ExtensionInterface[] + * An array of extensions discovered on the inspected source site. + */ + abstract protected function readExtensions(): array; } diff --git a/src/Command/App/From/SourceSite/SiteInspectorInterface.php b/src/Command/App/From/SourceSite/SiteInspectorInterface.php index 82101bac8..98181799c 100644 --- a/src/Command/App/From/SourceSite/SiteInspectorInterface.php +++ b/src/Command/App/From/SourceSite/SiteInspectorInterface.php @@ -1,69 +1,69 @@ getExtensions(Drupal7SiteInspector::FLAG_EXTENSION_ENABLED|Drupal7SiteInspector::FLAG_EXTENSION_MODULE); - * @endcode - * @return \Acquia\Cli\Command\App\From\SourceSite\ExtensionInterface[] - * An array of identified extensions, filtered to contain only those - * included by the given flags. - */ - public function getExtensions(int $flags): array; + /** + * Gets extensions on an inspected site. + * + * @param int $flags + * Bitwise flags indicting various subsets of extensions to be returned. + * Omitting flags omits those extensions from the return value. I.e. if the + * FLAG_EXTENSION_ENABLED flag is given, but not the FLAG_EXTENSION_DISABLED + * flag, then only enabled extensions will be returned. In the example + * below, all enabled modules will be returned. Themes and disabled modules + * will be excluded. + * @code + * $inspector->getExtensions(Drupal7SiteInspector::FLAG_EXTENSION_ENABLED|Drupal7SiteInspector::FLAG_EXTENSION_MODULE); + * @endcode + * @return \Acquia\Cli\Command\App\From\SourceSite\ExtensionInterface[] + * An array of identified extensions, filtered to contain only those + * included by the given flags. + */ + public function getExtensions(int $flags): array; - /** - * Gets the public file path relative to the Drupal root. - */ - public function getPublicFilePath(): string; - - /** - * Gets the private file path relative to the Drupal root, if it exists. - * - * @return string|null - * NULL if the the inspected site does not use private files, a string - * otherwise. - */ - public function getPrivateFilePath(): ?string; + /** + * Gets the public file path relative to the Drupal root. + */ + public function getPublicFilePath(): string; + /** + * Gets the private file path relative to the Drupal root, if it exists. + * + * @return string|null + * NULL if the the inspected site does not use private files, a string + * otherwise. + */ + public function getPrivateFilePath(): ?string; } diff --git a/src/Command/App/LinkCommand.php b/src/Command/App/LinkCommand.php index d0c626a36..4247a0dc9 100644 --- a/src/Command/App/LinkCommand.php +++ b/src/Command/App/LinkCommand.php @@ -1,6 +1,6 @@ acceptApplicationUuid(); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $this->validateCwdIsValidDrupalProject(); - if ($cloudApplicationUuid = $this->getCloudUuidFromDatastore()) { - $cloudApplication = $this->getCloudApplication($cloudApplicationUuid); - $output->writeln('This repository is already linked to Cloud application ' . $cloudApplication->name . '. Run acli unlink to unlink it.'); - return 1; +final class LinkCommand extends CommandBase +{ + protected function configure(): void + { + $this->acceptApplicationUuid(); } - $this->determineCloudApplication(TRUE); - - return Command::SUCCESS; - } + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->validateCwdIsValidDrupalProject(); + if ($cloudApplicationUuid = $this->getCloudUuidFromDatastore()) { + $cloudApplication = $this->getCloudApplication($cloudApplicationUuid); + $output->writeln('This repository is already linked to Cloud application ' . $cloudApplication->name . '. Run acli unlink to unlink it.'); + return 1; + } + $this->determineCloudApplication(true); + + return Command::SUCCESS; + } } diff --git a/src/Command/App/LogTailCommand.php b/src/Command/App/LogTailCommand.php index 543d70040..5fd9cf648 100644 --- a/src/Command/App/LogTailCommand.php +++ b/src/Command/App/LogTailCommand.php @@ -1,6 +1,6 @@ localMachineHelper, $this->datastoreCloud, $this->datastoreAcli, $this->cloudCredentials, $this->telemetryHelper, $this->projectDir, $this->cloudApiClientService, $this->sshHelper, $this->sshDir, $logger); + } - public function __construct( - public LocalMachineHelper $localMachineHelper, - protected CloudDataStore $datastoreCloud, - protected AcquiaCliDatastore $datastoreAcli, - protected ApiCredentialsInterface $cloudCredentials, - protected TelemetryHelper $telemetryHelper, - protected string $projectDir, - protected ClientService $cloudApiClientService, - public SshHelper $sshHelper, - protected string $sshDir, - LoggerInterface $logger, - protected LogstreamManager $logstreamManager, - ) { - parent::__construct($this->localMachineHelper, $this->datastoreCloud, $this->datastoreAcli, $this->cloudCredentials, $this->telemetryHelper, $this->projectDir, $this->cloudApiClientService, $this->sshHelper, $this->sshDir, $logger); - } - - protected function configure(): void { - $this - ->acceptEnvironmentId(); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $environment = $this->determineEnvironment($input, $output); - $acquiaCloudClient = $this->cloudApiClientService->getClient(); - $logs = $this->promptChooseLogs(); - $logTypes = array_map(static function (mixed $log) { - return $log['type']; - }, $logs); - $logsResource = new Logs($acquiaCloudClient); - $stream = $logsResource->stream($environment->uuid); - $this->logstreamManager->setParams($stream->logstream->params); - $this->logstreamManager->setColourise(TRUE); - $this->logstreamManager->setLogTypeFilter($logTypes); - $output->writeln('Streaming has started and new logs will appear below. Use Ctrl+C to exit.'); - $this->logstreamManager->stream(); - return Command::SUCCESS; - } + protected function configure(): void + { + $this + ->acceptEnvironmentId(); + } + protected function execute(InputInterface $input, OutputInterface $output): int + { + $environment = $this->determineEnvironment($input, $output); + $acquiaCloudClient = $this->cloudApiClientService->getClient(); + $logs = $this->promptChooseLogs(); + $logTypes = array_map(static function (mixed $log) { + return $log['type']; + }, $logs); + $logsResource = new Logs($acquiaCloudClient); + $stream = $logsResource->stream($environment->uuid); + $this->logstreamManager->setParams($stream->logstream->params); + $this->logstreamManager->setColourise(true); + $this->logstreamManager->setLogTypeFilter($logTypes); + $output->writeln('Streaming has started and new logs will appear below. Use Ctrl+C to exit.'); + $this->logstreamManager->stream(); + return Command::SUCCESS; + } } diff --git a/src/Command/App/NewCommand.php b/src/Command/App/NewCommand.php index 99a576c15..df2efb75b 100644 --- a/src/Command/App/NewCommand.php +++ b/src/Command/App/NewCommand.php @@ -1,6 +1,6 @@ addArgument('directory', InputArgument::OPTIONAL, 'The destination directory'); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $this->output->writeln('Acquia recommends most customers use acquia/drupal-recommended-project to setup a Drupal project, which includes useful utilities such as Acquia Connector.'); - $this->output->writeln('acquia/next-acms is a starter template for building a headless site powered by Acquia CMS and Next.js.'); - $distros = [ - 'acquia_drupal_recommended' => 'acquia/drupal-recommended-project', - 'acquia_next_acms' => 'acquia/next-acms', - ]; - $project = $this->io->choice('Choose a starting project', array_values($distros), $distros['acquia_drupal_recommended']); - $project = array_search($project, $distros, TRUE); - - if ($input->hasArgument('directory') && $input->getArgument('directory')) { - $dir = Path::canonicalize($input->getArgument('directory')); - $dir = Path::makeAbsolute($dir, getcwd()); +final class NewCommand extends CommandBase +{ + protected function configure(): void + { + $this + ->addArgument('directory', InputArgument::OPTIONAL, 'The destination directory'); } - else if (AcquiaDrupalEnvironmentDetector::isAhIdeEnv()) { - $dir = '/home/ide/project'; - } - else { - $dir = Path::makeAbsolute($project, getcwd()); - } - - $output->writeln('Creating project. This may take a few minutes.'); - if ($project === 'acquia_next_acms') { - $successMessage = "New Next JS project created in $dir. 🎉"; - $this->localMachineHelper->checkRequiredBinariesExist(['node']); - $this->createNextJsProject($dir); + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->output->writeln('Acquia recommends most customers use acquia/drupal-recommended-project to setup a Drupal project, which includes useful utilities such as Acquia Connector.'); + $this->output->writeln('acquia/next-acms is a starter template for building a headless site powered by Acquia CMS and Next.js.'); + $distros = [ + 'acquia_drupal_recommended' => 'acquia/drupal-recommended-project', + 'acquia_next_acms' => 'acquia/next-acms', + ]; + $project = $this->io->choice('Choose a starting project', array_values($distros), $distros['acquia_drupal_recommended']); + $project = array_search($project, $distros, true); + + if ($input->hasArgument('directory') && $input->getArgument('directory')) { + $dir = Path::canonicalize($input->getArgument('directory')); + $dir = Path::makeAbsolute($dir, getcwd()); + } elseif (AcquiaDrupalEnvironmentDetector::isAhIdeEnv()) { + $dir = '/home/ide/project'; + } else { + $dir = Path::makeAbsolute($project, getcwd()); + } + + $output->writeln('Creating project. This may take a few minutes.'); + + if ($project === 'acquia_next_acms') { + $successMessage = "New Next JS project created in $dir. 🎉"; + $this->localMachineHelper->checkRequiredBinariesExist(['node']); + $this->createNextJsProject($dir); + } else { + $successMessage = "New 💧 Drupal project created in $dir. 🎉"; + $this->localMachineHelper->checkRequiredBinariesExist(['composer']); + $this->createDrupalProject($distros[$project], $dir); + } + + $this->initializeGitRepository($dir); + + $output->writeln(''); + $output->writeln($successMessage); + + return Command::SUCCESS; } - else { - $successMessage = "New 💧 Drupal project created in $dir. 🎉"; - $this->localMachineHelper->checkRequiredBinariesExist(['composer']); - $this->createDrupalProject($distros[$project], $dir); - } - - $this->initializeGitRepository($dir); - $output->writeln(''); - $output->writeln($successMessage); - - return Command::SUCCESS; - } - - private function createNextJsProject(string $dir): void { - $process = $this->localMachineHelper->execute([ - 'npx', - 'create-next-app', - '-e', - 'https://github.com/acquia/next-acms/tree/main/starters/basic-starter', - $dir, - ]); - if (!$process->isSuccessful()) { - throw new AcquiaCliException("Unable to create new next-acms project."); + private function createNextJsProject(string $dir): void + { + $process = $this->localMachineHelper->execute([ + 'npx', + 'create-next-app', + '-e', + 'https://github.com/acquia/next-acms/tree/main/starters/basic-starter', + $dir, + ]); + if (!$process->isSuccessful()) { + throw new AcquiaCliException("Unable to create new next-acms project."); + } } - } - - private function createDrupalProject(string $project, string $dir): void { - $process = $this->localMachineHelper->execute([ - 'composer', - 'create-project', - $project, - $dir, - '--no-interaction', - ]); - if (!$process->isSuccessful()) { - throw new AcquiaCliException("Unable to create new project."); - } - } - private function initializeGitRepository(string $dir): void { - if ($this->localMachineHelper->getFilesystem()->exists(Path::join($dir, '.git'))) { - $this->logger->debug('.git directory detected, skipping Git repo initialization'); - return; + private function createDrupalProject(string $project, string $dir): void + { + $process = $this->localMachineHelper->execute([ + 'composer', + 'create-project', + $project, + $dir, + '--no-interaction', + ]); + if (!$process->isSuccessful()) { + throw new AcquiaCliException("Unable to create new project."); + } } - $this->localMachineHelper->checkRequiredBinariesExist(['git']); - $this->localMachineHelper->execute([ - 'git', - 'init', - '--initial-branch=main', - ], NULL, $dir); - - $this->localMachineHelper->execute([ - 'git', - 'add', - '-A', - ], NULL, $dir); - - $this->localMachineHelper->execute([ - 'git', - 'commit', - '--message', - 'Initial commit.', - '--quiet', - ], NULL, $dir); - // @todo Check that this was successful! - } + private function initializeGitRepository(string $dir): void + { + if ($this->localMachineHelper->getFilesystem()->exists(Path::join($dir, '.git'))) { + $this->logger->debug('.git directory detected, skipping Git repo initialization'); + return; + } + $this->localMachineHelper->checkRequiredBinariesExist(['git']); + $this->localMachineHelper->execute([ + 'git', + 'init', + '--initial-branch=main', + ], null, $dir); + + $this->localMachineHelper->execute([ + 'git', + 'add', + '-A', + ], null, $dir); + + $this->localMachineHelper->execute([ + 'git', + 'commit', + '--message', + 'Initial commit.', + '--quiet', + ], null, $dir); + // @todo Check that this was successful! + } } diff --git a/src/Command/App/NewFromDrupal7Command.php b/src/Command/App/NewFromDrupal7Command.php index 75c67ded4..cf1dba8e3 100644 --- a/src/Command/App/NewFromDrupal7Command.php +++ b/src/Command/App/NewFromDrupal7Command.php @@ -1,6 +1,6 @@ addOption('drupal7-directory', 'source', InputOption::VALUE_OPTIONAL, 'The root of the Drupal 7 application') - ->addOption('drupal7-uri', 'uri', InputOption::VALUE_OPTIONAL, 'Only necessary in case of a multisite. If a single site, this will be computed automatically.') - ->addOption('stored-analysis', 'analysis', InputOption::VALUE_OPTIONAL, 'As an alternative to drupal7-directory, it is possible to pass a stored analysis.') - ->addOption('recommendations', 'recommendations', InputOption::VALUE_OPTIONAL, 'Overrides the default recommendations.') - ->addOption('directory', 'destination', InputOption::VALUE_OPTIONAL, 'The directory where to generate the new application.'); - } - - private function getInspector(InputInterface $input): SiteInspectorInterface { - if ($input->getOption('stored-analysis') !== NULL) { - $analysis_json = $input->getOption('stored-analysis'); - $extensions_resource = fopen($analysis_json, 'r'); - $inspector = ExportedDrupal7ExtensionsInspector::createFromResource($extensions_resource); - fclose($extensions_resource); - return $inspector; +final class NewFromDrupal7Command extends CommandBase +{ + /** + * Exit code raised when the URI flag does not correspond to configuration. + * + * This typically indicates the value of the --drupal7-uri flag does not + * correspond to any configuration in a Drupal site's sites/sites.php file. + */ + public const ERR_UNRECOGNIZED_HOST = 3; + + /** + * Exit code raised when a Drupal 7 installation cannot be determined. + * + * This indicates the --drupal7-uri was not given and a sane default site could not be + * determined. + */ + public const ERR_INDETERMINATE_SITE = 4; + + protected function configure(): void + { + $this + ->addOption('drupal7-directory', 'source', InputOption::VALUE_OPTIONAL, 'The root of the Drupal 7 application') + ->addOption('drupal7-uri', 'uri', InputOption::VALUE_OPTIONAL, 'Only necessary in case of a multisite. If a single site, this will be computed automatically.') + ->addOption('stored-analysis', 'analysis', InputOption::VALUE_OPTIONAL, 'As an alternative to drupal7-directory, it is possible to pass a stored analysis.') + ->addOption('recommendations', 'recommendations', InputOption::VALUE_OPTIONAL, 'Overrides the default recommendations.') + ->addOption('directory', 'destination', InputOption::VALUE_OPTIONAL, 'The directory where to generate the new application.'); } - // First: Determine the Drupal 7 root. - $d7_root = $this->determineOption('drupal7-directory', FALSE, Drupal7SiteInspector::validateDrupal7Root(...), NULL, '.'); - - // Second, determine which "sites" subdirectory is being assessed. - $uri = Drupal7SiteInspector::getSiteUri($input, $d7_root); - - return new Drupal7SiteInspector($d7_root, $uri); - } - - private function getLocation(string $location, bool $should_exist = TRUE): string { - if (strpos($location, '://') === FALSE) { - $file_exists = file_exists($location); - if ($file_exists && !$should_exist) { - throw new ValidatorException(sprintf('The %s directory already exists.', $location)); - } - elseif (!$file_exists && $should_exist) { - throw new ValidatorException(sprintf('%s could not be located. Check that the path is correct and try again.', $location)); - } - if (strpos($location, '.') === 0 || !static::isAbsolutePath($location)) { - $absolute = getcwd() . '/' . $location; - $location = $should_exist ? realpath($absolute) : $absolute; - } - } - return $location; - } + private function getInspector(InputInterface $input): SiteInspectorInterface + { + if ($input->getOption('stored-analysis') !== null) { + $analysis_json = $input->getOption('stored-analysis'); + $extensions_resource = fopen($analysis_json, 'r'); + $inspector = ExportedDrupal7ExtensionsInspector::createFromResource($extensions_resource); + fclose($extensions_resource); + return $inspector; + } - private static function isAbsolutePath(string $path): bool { - // @see https://stackoverflow.com/a/23570509 - return $path[0] === DIRECTORY_SEPARATOR || preg_match('~\A[A-Z]:(?![^/\\\\])~i', $path) > 0; - } + // First: Determine the Drupal 7 root. + $d7_root = $this->determineOption('drupal7-directory', false, Drupal7SiteInspector::validateDrupal7Root(...), null, '.'); - protected function execute(InputInterface $input, OutputInterface $output): int { - try { - $inspector = $this->getInspector($input); - } - catch (\Exception $e) { - $this->io->error($e->getMessage()); - // Important: ensure that the unique error code that ::getSiteUri() - // computed is passed on, to enable scripting this command. - return $e->getCode(); - } + // Second, determine which "sites" subdirectory is being assessed. + $uri = Drupal7SiteInspector::getSiteUri($input, $d7_root); - // Now the Drupal 7 site can be inspected. Inform the user. - $output->writeln('🤖 Scanning Drupal 7 site.'); - $extensions = $inspector->getExtensions(SiteInspectorInterface::FLAG_EXTENSION_MODULE | SiteInspectorInterface::FLAG_EXTENSION_ENABLED); - $module_count = count($extensions); - $system_module_version = array_reduce( - array_filter($extensions, fn (ExtensionInterface $extension) => $extension->isModule() && $extension->getName() === 'system'), - fn (mixed $carry, ExtensionInterface $extension) => $extension->getVersion() - ); - $site_location = property_exists($inspector, 'uri') ? 'sites/' . $inspector->uri : ''; - $output->writeln(sprintf("👍 Found Drupal 7 site (%s to be precise) at %s, with %d modules enabled!", $system_module_version, $site_location, $module_count)); - - // Parse config for project builder. - $configuration_location = __DIR__ . '/../../../config/from_d7_config.json'; - $config_resource = fopen($configuration_location, 'r'); - $configuration = Configuration::createFromResource($config_resource); - fclose($config_resource); - - // Parse recommendations for project builder. - $recommendations_location = "https://git.drupalcode.org/project/acquia_migrate/-/raw/recommendations/recommendations.json"; - if ($input->getOption('recommendations') !== NULL) { - $raw_recommendations_location = $input->getOption('recommendations'); - try { - $recommendations_location = $this->getLocation($raw_recommendations_location); - } - catch (\InvalidArgumentException $e) { - $this->io->error($e->getMessage()); - return Command::FAILURE; - } - } - // PHP defaults to no user agent. (Drupal.org's) GitLab requires it. - // @see https://www.php.net/manual/en/filesystem.configuration.php#ini.user-agent - ini_set('user_agent', 'ACLI'); - $recommendations_resource = fopen($recommendations_location, 'r'); - $recommendations = Recommendations::createFromResource($recommendations_resource); - fclose($recommendations_resource); - - // Build project (in memory) using the configuration and the given - // recommendations from the inspected Drupal 7 site and inform the user. - $output->writeln('🤖 Computing recommendations for this Drupal 7 site…'); - $project_builder = new ProjectBuilder($configuration, new Resolver($inspector, $recommendations), $inspector); - $results = $project_builder->buildProject(); - $unique_patch_count = array_reduce( - $results['rootPackageDefinition']['extra']['patches'], - fn (array $unique_patches, array $patches) => array_unique(array_merge($unique_patches, array_values($patches))), - [] - ); - $output->writeln(sprintf( - "🥳 Great news: found %d recommendations that apply to this Drupal 7 site, resulting in a composer.json with:\n\t- %d packages\n\t- %d patches\n\t- %d modules to be installed!", - count($results['recommendations']), - count($results['rootPackageDefinition']['require']), - $unique_patch_count, - count($results['installModules']), - )); - - // Ask where to store the generated project (in other words: where to write - // a composer.json file). If a directory path is passed, assume the user - // knows what they're doing. - if ($input->getOption('directory') === NULL) { - $answer = $this->io->ask( - 'Where should the generated composer.json be written?', - NULL, - function (mixed $path): string { - if (!is_string($path) || !file_exists($path) || file_exists("$path/composer.json")) { - throw new ValidatorException(sprintf("The '%s' directory either does not exist or it already contains a composer.json file.", $path)); - } - return $path; - }, - ); - $input->setOption('directory', $answer); - } - $dir = $input->getOption('directory'); - - // Create the info metadata array, including a complete root. Write this to - // a metadata JSON file in the given directory. Also generate a - // composer.json from this. Initialize a new Git repo and commit both. - $data = array_merge( - ['generated' => date(DATE_ATOM)], - $project_builder->buildProject() - ); - $json_encode_flags = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR; - file_put_contents("$dir/acli-generated-project-metadata.json", json_encode($data, $json_encode_flags)); - file_put_contents("$dir/composer.json", json_encode($data['rootPackageDefinition'], $json_encode_flags)); - $this->initializeGitRepository($dir); - $output->writeln('🚀 Generated composer.json and committed to a new git repo.'); - $output->writeln(''); - - // Helpfully automatically run `composer install`, but equally helpfully do - // not commit it yet, to allow the user to choose whether to commit build - // artifacts. - $output->writeln('⏳ Installing. This may take a few minutes.'); - $this->localMachineHelper->checkRequiredBinariesExist(['composer']); - $process = $this->localMachineHelper->execute([ - 'composer', - 'install', - '--working-dir', - $dir, - '--no-interaction', - ]); - if (!$process->isSuccessful()) { - throw new AcquiaCliException("Unable to create new project."); + return new Drupal7SiteInspector($d7_root, $uri); } - $output->writeln(''); - $output->writeln("New 💧 Drupal project created in $dir. 🎉"); + private function getLocation(string $location, bool $should_exist = true): string + { + if (strpos($location, '://') === false) { + $file_exists = file_exists($location); + if ($file_exists && !$should_exist) { + throw new ValidatorException(sprintf('The %s directory already exists.', $location)); + } elseif (!$file_exists && $should_exist) { + throw new ValidatorException(sprintf('%s could not be located. Check that the path is correct and try again.', $location)); + } + if (strpos($location, '.') === 0 || !static::isAbsolutePath($location)) { + $absolute = getcwd() . '/' . $location; + $location = $should_exist ? realpath($absolute) : $absolute; + } + } + return $location; + } - return Command::SUCCESS; - } + private static function isAbsolutePath(string $path): bool + { + // @see https://stackoverflow.com/a/23570509 + return $path[0] === DIRECTORY_SEPARATOR || preg_match('~\A[A-Z]:(?![^/\\\\])~i', $path) > 0; + } - private function initializeGitRepository(string $dir): void { - if ($this->localMachineHelper->getFilesystem()->exists(Path::join($dir, '.git'))) { - $this->logger->debug('.git directory detected, skipping Git repo initialization'); - return; + protected function execute(InputInterface $input, OutputInterface $output): int + { + try { + $inspector = $this->getInspector($input); + } catch (\Exception $e) { + $this->io->error($e->getMessage()); + // Important: ensure that the unique error code that ::getSiteUri() + // computed is passed on, to enable scripting this command. + return $e->getCode(); + } + + // Now the Drupal 7 site can be inspected. Inform the user. + $output->writeln('🤖 Scanning Drupal 7 site.'); + $extensions = $inspector->getExtensions(SiteInspectorInterface::FLAG_EXTENSION_MODULE | SiteInspectorInterface::FLAG_EXTENSION_ENABLED); + $module_count = count($extensions); + $system_module_version = array_reduce( + array_filter($extensions, fn (ExtensionInterface $extension) => $extension->isModule() && $extension->getName() === 'system'), + fn (mixed $carry, ExtensionInterface $extension) => $extension->getVersion() + ); + $site_location = property_exists($inspector, 'uri') ? 'sites/' . $inspector->uri : ''; + $output->writeln(sprintf("👍 Found Drupal 7 site (%s to be precise) at %s, with %d modules enabled!", $system_module_version, $site_location, $module_count)); + + // Parse config for project builder. + $configuration_location = __DIR__ . '/../../../config/from_d7_config.json'; + $config_resource = fopen($configuration_location, 'r'); + $configuration = Configuration::createFromResource($config_resource); + fclose($config_resource); + + // Parse recommendations for project builder. + $recommendations_location = "https://git.drupalcode.org/project/acquia_migrate/-/raw/recommendations/recommendations.json"; + if ($input->getOption('recommendations') !== null) { + $raw_recommendations_location = $input->getOption('recommendations'); + try { + $recommendations_location = $this->getLocation($raw_recommendations_location); + } catch (\InvalidArgumentException $e) { + $this->io->error($e->getMessage()); + return Command::FAILURE; + } + } + // PHP defaults to no user agent. (Drupal.org's) GitLab requires it. + // @see https://www.php.net/manual/en/filesystem.configuration.php#ini.user-agent + ini_set('user_agent', 'ACLI'); + $recommendations_resource = fopen($recommendations_location, 'r'); + $recommendations = Recommendations::createFromResource($recommendations_resource); + fclose($recommendations_resource); + + // Build project (in memory) using the configuration and the given + // recommendations from the inspected Drupal 7 site and inform the user. + $output->writeln('🤖 Computing recommendations for this Drupal 7 site…'); + $project_builder = new ProjectBuilder($configuration, new Resolver($inspector, $recommendations), $inspector); + $results = $project_builder->buildProject(); + $unique_patch_count = array_reduce( + $results['rootPackageDefinition']['extra']['patches'], + fn (array $unique_patches, array $patches) => array_unique(array_merge($unique_patches, array_values($patches))), + [] + ); + $output->writeln(sprintf( + "🥳 Great news: found %d recommendations that apply to this Drupal 7 site, resulting in a composer.json with:\n\t- %d packages\n\t- %d patches\n\t- %d modules to be installed!", + count($results['recommendations']), + count($results['rootPackageDefinition']['require']), + $unique_patch_count, + count($results['installModules']), + )); + + // Ask where to store the generated project (in other words: where to write + // a composer.json file). If a directory path is passed, assume the user + // knows what they're doing. + if ($input->getOption('directory') === null) { + $answer = $this->io->ask( + 'Where should the generated composer.json be written?', + null, + function (mixed $path): string { + if (!is_string($path) || !file_exists($path) || file_exists("$path/composer.json")) { + throw new ValidatorException(sprintf("The '%s' directory either does not exist or it already contains a composer.json file.", $path)); + } + return $path; + }, + ); + $input->setOption('directory', $answer); + } + $dir = $input->getOption('directory'); + + // Create the info metadata array, including a complete root. Write this to + // a metadata JSON file in the given directory. Also generate a + // composer.json from this. Initialize a new Git repo and commit both. + $data = array_merge( + ['generated' => date(DATE_ATOM)], + $project_builder->buildProject() + ); + $json_encode_flags = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR; + file_put_contents("$dir/acli-generated-project-metadata.json", json_encode($data, $json_encode_flags)); + file_put_contents("$dir/composer.json", json_encode($data['rootPackageDefinition'], $json_encode_flags)); + $this->initializeGitRepository($dir); + $output->writeln('🚀 Generated composer.json and committed to a new git repo.'); + $output->writeln(''); + + // Helpfully automatically run `composer install`, but equally helpfully do + // not commit it yet, to allow the user to choose whether to commit build + // artifacts. + $output->writeln('⏳ Installing. This may take a few minutes.'); + $this->localMachineHelper->checkRequiredBinariesExist(['composer']); + $process = $this->localMachineHelper->execute([ + 'composer', + 'install', + '--working-dir', + $dir, + '--no-interaction', + ]); + if (!$process->isSuccessful()) { + throw new AcquiaCliException("Unable to create new project."); + } + + $output->writeln(''); + $output->writeln("New 💧 Drupal project created in $dir. 🎉"); + + return Command::SUCCESS; } - $this->localMachineHelper->checkRequiredBinariesExist(['git']); - $this->localMachineHelper->execute([ - 'git', - 'init', - '--initial-branch=main', - '--quiet', - ], NULL, $dir); - - $this->localMachineHelper->execute([ - 'git', - 'add', - '-A', - ], NULL, $dir); - - $this->localMachineHelper->execute([ - 'git', - 'commit', - '--message', - "Generated by Acquia CLI's app:new:from:drupal7.", - '--quiet', - ], NULL, $dir); - // @todo Check that this was successful! - } + private function initializeGitRepository(string $dir): void + { + if ($this->localMachineHelper->getFilesystem()->exists(Path::join($dir, '.git'))) { + $this->logger->debug('.git directory detected, skipping Git repo initialization'); + return; + } + $this->localMachineHelper->checkRequiredBinariesExist(['git']); + $this->localMachineHelper->execute([ + 'git', + 'init', + '--initial-branch=main', + '--quiet', + ], null, $dir); + + $this->localMachineHelper->execute([ + 'git', + 'add', + '-A', + ], null, $dir); + + $this->localMachineHelper->execute([ + 'git', + 'commit', + '--message', + "Generated by Acquia CLI's app:new:from:drupal7.", + '--quiet', + ], null, $dir); + // @todo Check that this was successful! + } } diff --git a/src/Command/App/TaskWaitCommand.php b/src/Command/App/TaskWaitCommand.php index 501afb8c5..4c0585720 100644 --- a/src/Command/App/TaskWaitCommand.php +++ b/src/Command/App/TaskWaitCommand.php @@ -1,6 +1,6 @@ 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('"$(acli api:environments:domain-clear-caches [environmentId] [domain])"'); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $notificationUuid = $input->getArgument('notification-uuid'); - $success = $this->waitForNotificationToComplete($this->cloudApiClientService->getClient(), $notificationUuid, "Waiting for task $notificationUuid to complete"); - return $success ? Command::SUCCESS : Command::FAILURE; - } +final class TaskWaitCommand extends CommandBase +{ + protected function configure(): void + { + $this + ->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('"$(acli api:environments:domain-clear-caches [environmentId] [domain])"'); + } + protected function execute(InputInterface $input, OutputInterface $output): int + { + $notificationUuid = $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/App/UnlinkCommand.php b/src/Command/App/UnlinkCommand.php index d7f922bb7..f2d07ae20 100644 --- a/src/Command/App/UnlinkCommand.php +++ b/src/Command/App/UnlinkCommand.php @@ -1,6 +1,6 @@ validateCwdIsValidDrupalProject(); - - $projectDir = $this->projectDir; - if (!$this->getCloudUuidFromDatastore()) { - throw new AcquiaCliException('There is no Cloud Platform application linked to {projectDir}', ['projectDir' => $projectDir]); +final class UnlinkCommand extends CommandBase +{ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->validateCwdIsValidDrupalProject(); + + $projectDir = $this->projectDir; + if (!$this->getCloudUuidFromDatastore()) { + throw new AcquiaCliException('There is no Cloud Platform application linked to {projectDir}', ['projectDir' => $projectDir]); + } + + $application = $this->getCloudApplication($this->datastoreAcli->get('cloud_app_uuid')); + $this->datastoreAcli->set('cloud_app_uuid', null); + $output->writeln("Unlinked $projectDir from Cloud application {$application->name}"); + + return Command::SUCCESS; } - - $application = $this->getCloudApplication($this->datastoreAcli->get('cloud_app_uuid')); - $this->datastoreAcli->set('cloud_app_uuid', NULL); - $output->writeln("Unlinked $projectDir from Cloud application {$application->name}"); - - return Command::SUCCESS; - } - } diff --git a/src/Command/Archive/ArchiveExportCommand.php b/src/Command/Archive/ArchiveExportCommand.php index e26f52301..46a786d7e 100644 --- a/src/Command/Archive/ArchiveExportCommand.php +++ b/src/Command/Archive/ArchiveExportCommand.php @@ -1,6 +1,6 @@ addArgument('destination-dir', InputArgument::REQUIRED, 'The destination directory for the archive file') - ->addOption('source-dir', 'dir', InputOption::VALUE_REQUIRED, 'The directory containing the Drupal project to be pushed') - ->addOption('no-files', NULL, InputOption::VALUE_NONE, 'Exclude public files directory from archive') - ->addOption('no-database', 'no-db', InputOption::VALUE_NONE, 'Exclude database dump from archive'); - } - - protected function initialize(InputInterface $input, OutputInterface $output): void { - parent::initialize($input, $output); - $this->fs = $this->localMachineHelper->getFilesystem(); - $this->checklist = new Checklist($output); - $this->setDirAndRequireProjectCwd($input); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $this->determineDestinationDir($input); - $outputCallback = $this->getOutputCallback($output, $this->checklist); - - $randomString = (string) random_int(10000, 100000); - $tempDirName = 'acli-archive-' . basename($this->dir) . '-' . time() . '-' . $randomString; - $archiveTempDir = Path::join(sys_get_temp_dir(), $tempDirName); - $this->io->confirm("This will generate a new archive in {$this->destinationDir} containing the contents of your Drupal application at {$this->dir}.\n Do you want to continue?"); - - $this->checklist->addItem('Removing temporary artifact directory'); - $this->checklist->updateProgressBar("Removing $archiveTempDir"); - $this->fs->remove($archiveTempDir); - $this->fs->mkdir([$archiveTempDir, $archiveTempDir . '/repository']); - $this->checklist->completePreviousItem(); - - $this->checklist->addItem('Generating temporary archive directory'); - $this->createArchiveDirectory($archiveTempDir . '/repository'); - $this->checklist->completePreviousItem(); - - if (!$input->getOption('no-database')) { - $this->checklist->addItem('Dumping MySQL database'); - $this->exportDatabaseToArchiveDir($outputCallback, $archiveTempDir); - $this->checklist->completePreviousItem(); +final class ArchiveExportCommand extends CommandBase +{ + protected Checklist $checklist; + + private Filesystem $fs; + + /** + * @var bool|string|string[]|null + */ + private string|array|bool|null $destinationDir; + + private const PUBLIC_FILES_DIR = '/docroot/sites/default/files'; + + protected function configure(): void + { + $this + ->addArgument('destination-dir', InputArgument::REQUIRED, 'The destination directory for the archive file') + ->addOption('source-dir', 'dir', InputOption::VALUE_REQUIRED, 'The directory containing the Drupal project to be pushed') + ->addOption('no-files', null, InputOption::VALUE_NONE, 'Exclude public files directory from archive') + ->addOption('no-database', 'no-db', InputOption::VALUE_NONE, 'Exclude database dump from archive'); } - $this->checklist->addItem('Compressing archive into a tarball'); - $destinationFilepath = $this->compressArchiveDirectory($archiveTempDir, $this->destinationDir, $outputCallback); - $outputCallback('out', "Removing $archiveTempDir"); - $this->fs->remove($archiveTempDir); - $this->checklist->completePreviousItem(); - - $this->io->newLine(); - $this->io->success("An archive of your Drupal application was created at $destinationFilepath"); - if (AcquiaDrupalEnvironmentDetector::isAhIdeEnv()) { - $this->io->note('You can download the archive through the Cloud IDE user interface by right-clicking the file in your IDE workspace file browser and selecting "Download."'); + protected function initialize(InputInterface $input, OutputInterface $output): void + { + parent::initialize($input, $output); + $this->fs = $this->localMachineHelper->getFilesystem(); + $this->checklist = new Checklist($output); + $this->setDirAndRequireProjectCwd($input); } - return Command::SUCCESS; - } - - private function determineDestinationDir(InputInterface $input): void { - $this->destinationDir = $input->getArgument('destination-dir'); - if (!$this->fs->exists($this->destinationDir)) { - throw new AcquiaCliException("The destination directory {$this->destinationDir} does not exist!"); + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->determineDestinationDir($input); + $outputCallback = $this->getOutputCallback($output, $this->checklist); + + $randomString = (string) random_int(10000, 100000); + $tempDirName = 'acli-archive-' . basename($this->dir) . '-' . time() . '-' . $randomString; + $archiveTempDir = Path::join(sys_get_temp_dir(), $tempDirName); + $this->io->confirm("This will generate a new archive in {$this->destinationDir} containing the contents of your Drupal application at {$this->dir}.\n Do you want to continue?"); + + $this->checklist->addItem('Removing temporary artifact directory'); + $this->checklist->updateProgressBar("Removing $archiveTempDir"); + $this->fs->remove($archiveTempDir); + $this->fs->mkdir([$archiveTempDir, $archiveTempDir . '/repository']); + $this->checklist->completePreviousItem(); + + $this->checklist->addItem('Generating temporary archive directory'); + $this->createArchiveDirectory($archiveTempDir . '/repository'); + $this->checklist->completePreviousItem(); + + if (!$input->getOption('no-database')) { + $this->checklist->addItem('Dumping MySQL database'); + $this->exportDatabaseToArchiveDir($outputCallback, $archiveTempDir); + $this->checklist->completePreviousItem(); + } + + $this->checklist->addItem('Compressing archive into a tarball'); + $destinationFilepath = $this->compressArchiveDirectory($archiveTempDir, $this->destinationDir, $outputCallback); + $outputCallback('out', "Removing $archiveTempDir"); + $this->fs->remove($archiveTempDir); + $this->checklist->completePreviousItem(); + + $this->io->newLine(); + $this->io->success("An archive of your Drupal application was created at $destinationFilepath"); + if (AcquiaDrupalEnvironmentDetector::isAhIdeEnv()) { + $this->io->note('You can download the archive through the Cloud IDE user interface by right-clicking the file in your IDE workspace file browser and selecting "Download."'); + } + + return Command::SUCCESS; } - } - - /** - * Build the artifact. - */ - private function createArchiveDirectory(string $artifactDir): void { - $this->checklist->updateProgressBar("Mirroring source files from {$this->dir} to {$artifactDir}"); - $originFinder = $this->localMachineHelper->getFinder(); - $originFinder->files()->in($this->dir) - // Include dot files like .htaccess. - ->ignoreDotFiles(FALSE) - // If .gitignore exists, ignore VCS files like vendor. - ->ignoreVCSIgnored(file_exists(Path::join($this->dir, '.gitignore'))); - if ($this->input->getOption('no-files')) { - $this->checklist->updateProgressBar( 'Skipping ' . self::PUBLIC_FILES_DIR); - $originFinder->exclude([self::PUBLIC_FILES_DIR]); + + private function determineDestinationDir(InputInterface $input): void + { + $this->destinationDir = $input->getArgument('destination-dir'); + if (!$this->fs->exists($this->destinationDir)) { + throw new AcquiaCliException("The destination directory {$this->destinationDir} does not exist!"); + } } - $targetFinder = $this->localMachineHelper->getFinder(); - $targetFinder->files()->in($artifactDir)->ignoreDotFiles(FALSE); - $this->localMachineHelper->getFilesystem()->mirror($this->dir, $artifactDir, $originFinder, ['override' => TRUE, 'delete' => TRUE], $targetFinder); - } - - private function exportDatabaseToArchiveDir( - Closure $outputCallback, - string $archiveTempDir - ): void { - if (!$this->getDrushDatabaseConnectionStatus($outputCallback)) { - throw new AcquiaCliException("Could not connect to local database."); + + /** + * Build the artifact. + */ + private function createArchiveDirectory(string $artifactDir): void + { + $this->checklist->updateProgressBar("Mirroring source files from {$this->dir} to {$artifactDir}"); + $originFinder = $this->localMachineHelper->getFinder(); + $originFinder->files()->in($this->dir) + // Include dot files like .htaccess. + ->ignoreDotFiles(false) + // If .gitignore exists, ignore VCS files like vendor. + ->ignoreVCSIgnored(file_exists(Path::join($this->dir, '.gitignore'))); + if ($this->input->getOption('no-files')) { + $this->checklist->updateProgressBar('Skipping ' . self::PUBLIC_FILES_DIR); + $originFinder->exclude([self::PUBLIC_FILES_DIR]); + } + $targetFinder = $this->localMachineHelper->getFinder(); + $targetFinder->files()->in($artifactDir)->ignoreDotFiles(false); + $this->localMachineHelper->getFilesystem()->mirror($this->dir, $artifactDir, $originFinder, ['override' => true, 'delete' => true], $targetFinder); } - $dumpTempFilepath = $this->createMySqlDumpOnLocal( - $this->getLocalDbHost(), - $this->getLocalDbUser(), - $this->getLocalDbName(), - $this->getLocalDbPassword(), - $outputCallback - ); - $dumpFilepath = Path::join($archiveTempDir, basename($dumpTempFilepath)); - $this->checklist->updateProgressBar("Moving MySQL dump to $dumpFilepath"); - $this->fs->rename($dumpTempFilepath, $dumpFilepath); - } - - private function compressArchiveDirectory(string $archiveDir, string|bool|array|null $destinationDir, Closure $outputCallback = NULL): string { - $destinationFilename = basename($archiveDir) . '.tar.gz'; - $destinationFilepath = Path::join($destinationDir, $destinationFilename); - $this->localMachineHelper->checkRequiredBinariesExist(['tar']); - $process = $this->localMachineHelper->execute(['tar', '-zcvf', $destinationFilepath, '--directory', $archiveDir, '.'], $outputCallback, NULL, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL)); - if (!$process->isSuccessful()) { - throw new AcquiaCliException('Unable to create tarball: {message}', ['message' => $process->getErrorOutput()]); + + private function exportDatabaseToArchiveDir( + Closure $outputCallback, + string $archiveTempDir + ): void { + if (!$this->getDrushDatabaseConnectionStatus($outputCallback)) { + throw new AcquiaCliException("Could not connect to local database."); + } + $dumpTempFilepath = $this->createMySqlDumpOnLocal( + $this->getLocalDbHost(), + $this->getLocalDbUser(), + $this->getLocalDbName(), + $this->getLocalDbPassword(), + $outputCallback + ); + $dumpFilepath = Path::join($archiveTempDir, basename($dumpTempFilepath)); + $this->checklist->updateProgressBar("Moving MySQL dump to $dumpFilepath"); + $this->fs->rename($dumpTempFilepath, $dumpFilepath); } - return $destinationFilepath; - } + private function compressArchiveDirectory(string $archiveDir, string|bool|array|null $destinationDir, Closure $outputCallback = null): string + { + $destinationFilename = basename($archiveDir) . '.tar.gz'; + $destinationFilepath = Path::join($destinationDir, $destinationFilename); + $this->localMachineHelper->checkRequiredBinariesExist(['tar']); + $process = $this->localMachineHelper->execute(['tar', '-zcvf', $destinationFilepath, '--directory', $archiveDir, '.'], $outputCallback, null, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL)); + if (!$process->isSuccessful()) { + throw new AcquiaCliException('Unable to create tarball: {message}', ['message' => $process->getErrorOutput()]); + } + return $destinationFilepath; + } } diff --git a/src/Command/Auth/AuthAcsfLoginCommand.php b/src/Command/Auth/AuthAcsfLoginCommand.php index 25d865812..8e424efe2 100644 --- a/src/Command/Auth/AuthAcsfLoginCommand.php +++ b/src/Command/Auth/AuthAcsfLoginCommand.php @@ -1,6 +1,6 @@ addOption('username', 'u', InputOption::VALUE_REQUIRED, "Your Site Factory username") - ->addOption('key', 'k', InputOption::VALUE_REQUIRED, "Your Site Factory key") - ->addOption('factory-url', 'f', InputOption::VALUE_REQUIRED, "Your Site Factory URL (including https://)"); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - if ($input->getOption('factory-url')) { - $factoryUrl = $input->getOption('factory-url'); +final class AuthAcsfLoginCommand extends CommandBase +{ + protected function configure(): void + { + $this + ->addOption('username', 'u', InputOption::VALUE_REQUIRED, "Your Site Factory username") + ->addOption('key', 'k', InputOption::VALUE_REQUIRED, "Your Site Factory key") + ->addOption('factory-url', 'f', InputOption::VALUE_REQUIRED, "Your Site Factory URL (including https://)"); } - elseif ($input->isInteractive() && $this->datastoreCloud->get('acsf_factories')) { - $factories = $this->datastoreCloud->get('acsf_factories'); - $factoryChoices = $factories; - foreach ($factoryChoices as $url => $factoryChoice) { - $factoryChoices[$url]['url'] = $url; - } - $factoryChoices['add_new'] = [ - 'url' => 'Enter a new factory URL', - ]; - $factory = $this->promptChooseFromObjectsOrArrays($factoryChoices, 'url', 'url', 'Choose a Factory to login to'); - if ($factory['url'] === 'Enter a new factory URL') { - $factoryUrl = $this->io->ask('Enter the full URL of the factory'); - $factory = [ - 'url' => $factoryUrl, - 'users' => [], - ]; - } - else { - $factoryUrl = $factory['url']; - } - $users = $factory['users']; - $users['add_new'] = [ - 'username' => 'Enter a new user', - ]; - $selectedUser = $this->promptChooseFromObjectsOrArrays($users, 'username', 'username', 'Choose which user to login as'); - if ($selectedUser['username'] !== 'Enter a new user') { - $this->datastoreCloud->set('acsf_active_factory', $factoryUrl); - $factories[$factoryUrl]['active_user'] = $selectedUser['username']; - $this->datastoreCloud->set('acsf_factories', $factories); - $output->writeln([ - "Acquia CLI is now logged in to {$factory['url']} as {$selectedUser['username']}", - ]); - return Command::SUCCESS; - } - } - else { - $factoryUrl = $this->determineOption('factory-url'); - } + protected function execute(InputInterface $input, OutputInterface $output): int + { + if ($input->getOption('factory-url')) { + $factoryUrl = $input->getOption('factory-url'); + } elseif ($input->isInteractive() && $this->datastoreCloud->get('acsf_factories')) { + $factories = $this->datastoreCloud->get('acsf_factories'); + $factoryChoices = $factories; + foreach ($factoryChoices as $url => $factoryChoice) { + $factoryChoices[$url]['url'] = $url; + } + $factoryChoices['add_new'] = [ + 'url' => 'Enter a new factory URL', + ]; + $factory = $this->promptChooseFromObjectsOrArrays($factoryChoices, 'url', 'url', 'Choose a Factory to login to'); + if ($factory['url'] === 'Enter a new factory URL') { + $factoryUrl = $this->io->ask('Enter the full URL of the factory'); + $factory = [ + 'url' => $factoryUrl, + 'users' => [], + ]; + } else { + $factoryUrl = $factory['url']; + } - $username = $this->determineOption('username'); - $key = $this->determineOption('key', TRUE); + $users = $factory['users']; + $users['add_new'] = [ + 'username' => 'Enter a new user', + ]; + $selectedUser = $this->promptChooseFromObjectsOrArrays($users, 'username', 'username', 'Choose which user to login as'); + if ($selectedUser['username'] !== 'Enter a new user') { + $this->datastoreCloud->set('acsf_active_factory', $factoryUrl); + $factories[$factoryUrl]['active_user'] = $selectedUser['username']; + $this->datastoreCloud->set('acsf_factories', $factories); + $output->writeln([ + "Acquia CLI is now logged in to {$factory['url']} as {$selectedUser['username']}", + ]); + return Command::SUCCESS; + } + } else { + $factoryUrl = $this->determineOption('factory-url'); + } - $this->writeAcsfCredentialsToDisk($factoryUrl, $username, $key); - $output->writeln("Saved credentials"); + $username = $this->determineOption('username'); + $key = $this->determineOption('key', true); - return Command::SUCCESS; - } + $this->writeAcsfCredentialsToDisk($factoryUrl, $username, $key); + $output->writeln("Saved credentials"); - private function writeAcsfCredentialsToDisk(?string $factoryUrl, string $username, string $key): void { - $keys = $this->datastoreCloud->get('acsf_factories'); - $keys[$factoryUrl]['users'][$username] = [ - 'key' => $key, - 'username' => $username, - ]; - $keys[$factoryUrl]['url'] = $factoryUrl; - $keys[$factoryUrl]['active_user'] = $username; - $this->datastoreCloud->set('acsf_factories', $keys); - $this->datastoreCloud->set('acsf_active_factory', $factoryUrl); - } + return Command::SUCCESS; + } + private function writeAcsfCredentialsToDisk(?string $factoryUrl, string $username, string $key): void + { + $keys = $this->datastoreCloud->get('acsf_factories'); + $keys[$factoryUrl]['users'][$username] = [ + 'key' => $key, + 'username' => $username, + ]; + $keys[$factoryUrl]['url'] = $factoryUrl; + $keys[$factoryUrl]['active_user'] = $username; + $this->datastoreCloud->set('acsf_factories', $keys); + $this->datastoreCloud->set('acsf_active_factory', $factoryUrl); + } } diff --git a/src/Command/Auth/AuthAcsfLogoutCommand.php b/src/Command/Auth/AuthAcsfLogoutCommand.php index 910018958..8d84c9fe6 100644 --- a/src/Command/Auth/AuthAcsfLogoutCommand.php +++ b/src/Command/Auth/AuthAcsfLogoutCommand.php @@ -1,6 +1,6 @@ datastoreCloud->get('acsf_factories'); - if (empty($factories)) { - $this->io->error(['You are not logged into any factories.']); - return Command::FAILURE; - } - foreach ($factories as $url => $factory) { - $factories[$url]['url'] = $url; - } - $factory = $this->promptChooseFromObjectsOrArrays($factories, 'url', 'url', 'Choose a Factory to logout of'); - $factoryUrl = $factory['url']; - - /** @var \Acquia\Cli\AcsfApi\AcsfCredentials $cloudCredentials */ - $cloudCredentials = $this->cloudCredentials; - $activeUser = $cloudCredentials->getFactoryActiveUser($factory); - // @todo Only show factories the user is logged into. - if (!$activeUser) { - $this->io->error("You're already logged out of $factoryUrl"); - return 1; +final class AuthAcsfLogoutCommand extends CommandBase +{ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $factories = $this->datastoreCloud->get('acsf_factories'); + if (empty($factories)) { + $this->io->error(['You are not logged into any factories.']); + return Command::FAILURE; + } + foreach ($factories as $url => $factory) { + $factories[$url]['url'] = $url; + } + $factory = $this->promptChooseFromObjectsOrArrays($factories, 'url', 'url', 'Choose a Factory to logout of'); + $factoryUrl = $factory['url']; + + /** @var \Acquia\Cli\AcsfApi\AcsfCredentials $cloudCredentials */ + $cloudCredentials = $this->cloudCredentials; + $activeUser = $cloudCredentials->getFactoryActiveUser($factory); + // @todo Only show factories the user is logged into. + if (!$activeUser) { + $this->io->error("You're already logged out of $factoryUrl"); + return 1; + } + $answer = $this->io->confirm("Are you sure you'd like to logout the user {$activeUser['username']} from $factoryUrl?"); + if (!$answer) { + return Command::SUCCESS; + } + $factories[$factoryUrl]['active_user'] = null; + $this->datastoreCloud->set('acsf_factories', $factories); + $this->datastoreCloud->remove('acsf_active_factory'); + + $output->writeln("Logged {$activeUser['username']} out of $factoryUrl"); + + return Command::SUCCESS; } - $answer = $this->io->confirm("Are you sure you'd like to logout the user {$activeUser['username']} from $factoryUrl?"); - if (!$answer) { - return Command::SUCCESS; - } - $factories[$factoryUrl]['active_user'] = NULL; - $this->datastoreCloud->set('acsf_factories', $factories); - $this->datastoreCloud->remove('acsf_active_factory'); - - $output->writeln("Logged {$activeUser['username']} out of $factoryUrl"); - - return Command::SUCCESS; - } - } diff --git a/src/Command/Auth/AuthLoginCommand.php b/src/Command/Auth/AuthLoginCommand.php index 580262308..ba8274741 100644 --- a/src/Command/Auth/AuthLoginCommand.php +++ b/src/Command/Auth/AuthLoginCommand.php @@ -1,6 +1,6 @@ addOption('key', 'k', InputOption::VALUE_REQUIRED, 'Your Cloud Platform API key') - ->addOption('secret', 's', InputOption::VALUE_REQUIRED, 'Your Cloud Platform API secret') - ->setHelp('Acquia CLI can store multiple sets of credentials in case you have multiple Cloud Platform accounts. However, only a single account can be active at a time. This command allows you to activate a new or existing set of credentials.'); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $keys = $this->datastoreCloud->get('keys'); - $activeKey = $this->datastoreCloud->get('acli_key'); - if ($activeKey) { - $activeKeyLabel = $keys[$activeKey]['label']; - $output->writeln("The following Cloud Platform API key is active: $activeKeyLabel"); - } - else { - $output->writeln('No Cloud Platform API key is active'); +final class AuthLoginCommand extends CommandBase +{ + protected function configure(): void + { + $this + ->addOption('key', 'k', InputOption::VALUE_REQUIRED, 'Your Cloud Platform API key') + ->addOption('secret', 's', InputOption::VALUE_REQUIRED, 'Your Cloud Platform API secret') + ->setHelp('Acquia CLI can store multiple sets of credentials in case you have multiple Cloud Platform accounts. However, only a single account can be active at a time. This command allows you to activate a new or existing set of credentials.'); } - // If keys already are saved locally, prompt to select. - if ($keys && $input->isInteractive()) { - foreach ($keys as $uuid => $key) { - $keys[$uuid]['uuid'] = $uuid; - } - $keys['create_new'] = [ - 'label' => 'Enter a new API key', - 'uuid' => 'create_new', - ]; - $selectedKey = $this->promptChooseFromObjectsOrArrays($keys, 'uuid', 'label', 'Activate a Cloud Platform API key'); - if ($selectedKey['uuid'] !== 'create_new') { - $this->datastoreCloud->set('acli_key', $selectedKey['uuid']); - $output->writeln("Acquia CLI will use the API key {$selectedKey['label']}"); - $this->reAuthenticate($this->cloudCredentials->getCloudKey(), $this->cloudCredentials->getCloudSecret(), $this->cloudCredentials->getBaseUri(), $this->cloudCredentials->getAccountsUri()); - return Command::SUCCESS; - } - } + protected function execute(InputInterface $input, OutputInterface $output): int + { + $keys = $this->datastoreCloud->get('keys'); + $activeKey = $this->datastoreCloud->get('acli_key'); + if ($activeKey) { + $activeKeyLabel = $keys[$activeKey]['label']; + $output->writeln("The following Cloud Platform API key is active: $activeKeyLabel"); + } else { + $output->writeln('No Cloud Platform API key is active'); + } - $this->promptOpenBrowserToCreateToken($input); - $apiKey = $this->determineApiKey(); - $apiSecret = $this->determineApiSecret(); - $this->reAuthenticate($apiKey, $apiSecret, $this->cloudCredentials->getBaseUri(), $this->cloudCredentials->getAccountsUri()); - $this->writeApiCredentialsToDisk($apiKey, $apiSecret); - $output->writeln("Saved credentials"); + // If keys already are saved locally, prompt to select. + if ($keys && $input->isInteractive()) { + foreach ($keys as $uuid => $key) { + $keys[$uuid]['uuid'] = $uuid; + } + $keys['create_new'] = [ + 'label' => 'Enter a new API key', + 'uuid' => 'create_new', + ]; + $selectedKey = $this->promptChooseFromObjectsOrArrays($keys, 'uuid', 'label', 'Activate a Cloud Platform API key'); + if ($selectedKey['uuid'] !== 'create_new') { + $this->datastoreCloud->set('acli_key', $selectedKey['uuid']); + $output->writeln("Acquia CLI will use the API key {$selectedKey['label']}"); + $this->reAuthenticate($this->cloudCredentials->getCloudKey(), $this->cloudCredentials->getCloudSecret(), $this->cloudCredentials->getBaseUri(), $this->cloudCredentials->getAccountsUri()); + return Command::SUCCESS; + } + } - return Command::SUCCESS; - } + $this->promptOpenBrowserToCreateToken($input); + $apiKey = $this->determineApiKey(); + $apiSecret = $this->determineApiSecret(); + $this->reAuthenticate($apiKey, $apiSecret, $this->cloudCredentials->getBaseUri(), $this->cloudCredentials->getAccountsUri()); + $this->writeApiCredentialsToDisk($apiKey, $apiSecret); + $output->writeln("Saved credentials"); - private function writeApiCredentialsToDisk(string $apiKey, string $apiSecret): void { - $account = new Account($this->cloudApiClientService->getClient()); - $accountInfo = $account->get(); - $keys = $this->datastoreCloud->get('keys'); - $keys[$apiKey] = [ - 'label' => $accountInfo->mail, - 'secret' => $apiSecret, - 'uuid' => $apiKey, - ]; - $this->datastoreCloud->set('keys', $keys); - $this->datastoreCloud->set('acli_key', $apiKey); - } + return Command::SUCCESS; + } + private function writeApiCredentialsToDisk(string $apiKey, string $apiSecret): void + { + $account = new Account($this->cloudApiClientService->getClient()); + $accountInfo = $account->get(); + $keys = $this->datastoreCloud->get('keys'); + $keys[$apiKey] = [ + 'label' => $accountInfo->mail, + 'secret' => $apiSecret, + 'uuid' => $apiKey, + ]; + $this->datastoreCloud->set('keys', $keys); + $this->datastoreCloud->set('acli_key', $apiKey); + } } diff --git a/src/Command/Auth/AuthLogoutCommand.php b/src/Command/Auth/AuthLogoutCommand.php index e498709fc..35e48f431 100644 --- a/src/Command/Auth/AuthLogoutCommand.php +++ b/src/Command/Auth/AuthLogoutCommand.php @@ -1,6 +1,6 @@ addOption('delete', NULL, InputOption::VALUE_NEGATABLE, 'Delete the active Cloud Platform API credentials'); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $keys = $this->datastoreCloud->get('keys'); - $activeKey = $this->datastoreCloud->get('acli_key'); - if (!$activeKey) { - throw new AcquiaCliException('There is no active Cloud Platform API key'); - } - $activeKeyLabel = $keys[$activeKey]['label']; - $output->writeln("The key $activeKeyLabel will be deactivated on this machine. However, the credentials will remain on disk and can be reactivated by running acli auth:login unless you also choose to delete them."); - $delete = $this->determineOption('delete', FALSE, NULL, NULL, FALSE); - $this->datastoreCloud->remove('acli_key'); - $action = 'deactivated'; - if ($delete) { - $this->datastoreCloud->remove("keys.$activeKey"); - $action = 'deleted'; +final class AuthLogoutCommand extends CommandBase +{ + protected function configure(): void + { + $this->addOption('delete', null, InputOption::VALUE_NEGATABLE, 'Delete the active Cloud Platform API credentials'); } - $output->writeln("The active Cloud Platform API credentials were $action"); - $output->writeln('No Cloud Platform API key is active. Run acli auth:login to continue using the Cloud Platform API.'); - - return Command::SUCCESS; - } + protected function execute(InputInterface $input, OutputInterface $output): int + { + $keys = $this->datastoreCloud->get('keys'); + $activeKey = $this->datastoreCloud->get('acli_key'); + if (!$activeKey) { + throw new AcquiaCliException('There is no active Cloud Platform API key'); + } + $activeKeyLabel = $keys[$activeKey]['label']; + $output->writeln("The key $activeKeyLabel will be deactivated on this machine. However, the credentials will remain on disk and can be reactivated by running acli auth:login unless you also choose to delete them."); + $delete = $this->determineOption('delete', false, null, null, false); + $this->datastoreCloud->remove('acli_key'); + $action = 'deactivated'; + if ($delete) { + $this->datastoreCloud->remove("keys.$activeKey"); + $action = 'deleted'; + } + $output->writeln("The active Cloud Platform API credentials were $action"); + $output->writeln('No Cloud Platform API key is active. Run acli auth:login to continue using the Cloud Platform API.'); + + return Command::SUCCESS; + } } diff --git a/src/Command/CodeStudio/CodeStudioCiCdVariables.php b/src/Command/CodeStudio/CodeStudioCiCdVariables.php index de7691aac..d6f01d174 100644 --- a/src/Command/CodeStudio/CodeStudioCiCdVariables.php +++ b/src/Command/CodeStudio/CodeStudioCiCdVariables.php @@ -1,117 +1,119 @@ + */ + public static function getList(): array + { + // Getlist is being utilised in pipeline-migrate command. By default command is supporting drupal project but going forward need to support both drupal and nodejs project. + return array_column(self::getDefaultsForPhp(), 'key'); + } - /** - * @return array - */ - public static function getList(): array { - // Getlist is being utilised in pipeline-migrate command. By default command is supporting drupal project but going forward need to support both drupal and nodejs project. - return array_column(self::getDefaultsForPhp(), 'key'); - } - - /** - * @return array - */ - public static function getDefaultsForNode(?string $cloudApplicationUuid = NULL, ?string $cloudKey = NULL, ?string $cloudSecret = NULL, ?string $projectAccessTokenName = NULL, ?string $projectAccessToken = NULL, ?string $nodeVersion = NULL): array { - return [ - [ + /** + * @return array + */ + public static function getDefaultsForNode(?string $cloudApplicationUuid = null, ?string $cloudKey = null, ?string $cloudSecret = null, ?string $projectAccessTokenName = null, ?string $projectAccessToken = null, ?string $nodeVersion = null): array + { + return [ + [ 'key' => 'ACQUIA_APPLICATION_UUID', - 'masked' => TRUE, - 'protected' => FALSE, + 'masked' => true, + 'protected' => false, 'value' => $cloudApplicationUuid, 'variable_type' => 'env_var', - ], - [ + ], + [ 'key' => 'ACQUIA_CLOUD_API_TOKEN_KEY', - 'masked' => TRUE, - 'protected' => FALSE, + 'masked' => true, + 'protected' => false, 'value' => $cloudKey, 'variable_type' => 'env_var', - ], - [ + ], + [ 'key' => 'ACQUIA_CLOUD_API_TOKEN_SECRET', - 'masked' => TRUE, - 'protected' => FALSE, + 'masked' => true, + 'protected' => false, 'value' => $cloudSecret, 'variable_type' => 'env_var', - ], - [ + ], + [ 'key' => 'ACQUIA_GLAB_TOKEN_NAME', - 'masked' => TRUE, - 'protected' => FALSE, + 'masked' => true, + 'protected' => false, 'value' => $projectAccessTokenName, 'variable_type' => 'env_var', - ], - [ + ], + [ 'key' => 'ACQUIA_GLAB_TOKEN_SECRET', - 'masked' => TRUE, - 'protected' => FALSE, + 'masked' => true, + 'protected' => false, 'value' => $projectAccessToken, 'variable_type' => 'env_var', - ], - [ + ], + [ 'key' => 'NODE_VERSION', - 'masked' => FALSE, - 'protected' => FALSE, + 'masked' => false, + 'protected' => false, 'value' => $nodeVersion, 'variable_type' => 'env_var', - ], - ]; - } + ], + ]; + } - /** - * @return array - */ - public static function getDefaultsForPhp(?string $cloudApplicationUuid = NULL, ?string $cloudKey = NULL, ?string $cloudSecret = NULL, ?string $projectAccessTokenName = NULL, ?string $projectAccessToken = NULL, ?string $phpVersion = NULL): array { - return [ - [ + /** + * @return array + */ + public static function getDefaultsForPhp(?string $cloudApplicationUuid = null, ?string $cloudKey = null, ?string $cloudSecret = null, ?string $projectAccessTokenName = null, ?string $projectAccessToken = null, ?string $phpVersion = null): array + { + return [ + [ 'key' => 'ACQUIA_APPLICATION_UUID', - 'masked' => TRUE, - 'protected' => FALSE, + 'masked' => true, + 'protected' => false, 'value' => $cloudApplicationUuid, 'variable_type' => 'env_var', - ], - [ + ], + [ 'key' => 'ACQUIA_CLOUD_API_TOKEN_KEY', - 'masked' => TRUE, - 'protected' => FALSE, + 'masked' => true, + 'protected' => false, 'value' => $cloudKey, 'variable_type' => 'env_var', - ], - [ + ], + [ 'key' => 'ACQUIA_CLOUD_API_TOKEN_SECRET', - 'masked' => TRUE, - 'protected' => FALSE, + 'masked' => true, + 'protected' => false, 'value' => $cloudSecret, 'variable_type' => 'env_var', - ], - [ + ], + [ 'key' => 'ACQUIA_GLAB_TOKEN_NAME', - 'masked' => TRUE, - 'protected' => FALSE, + 'masked' => true, + 'protected' => false, 'value' => $projectAccessTokenName, 'variable_type' => 'env_var', - ], - [ + ], + [ 'key' => 'ACQUIA_GLAB_TOKEN_SECRET', - 'masked' => TRUE, - 'protected' => FALSE, + 'masked' => true, + 'protected' => false, 'value' => $projectAccessToken, 'variable_type' => 'env_var', - ], - [ + ], + [ 'key' => 'PHP_VERSION', - 'masked' => FALSE, - 'protected' => FALSE, + 'masked' => false, + 'protected' => false, 'value' => $phpVersion, 'variable_type' => 'env_var', - ], - ]; - } - + ], + ]; + } } diff --git a/src/Command/CodeStudio/CodeStudioCommandTrait.php b/src/Command/CodeStudio/CodeStudioCommandTrait.php index b137a147e..fb7dfe7d9 100644 --- a/src/Command/CodeStudio/CodeStudioCommandTrait.php +++ b/src/Command/CodeStudio/CodeStudioCommandTrait.php @@ -1,6 +1,6 @@ + */ + protected array $gitLabAccount; - /** - * @var array - */ - protected array $gitLabAccount; + private string $gitLabProjectDescription; - private string $gitLabProjectDescription; + /** + * Getting the gitlab token from user. + */ + private function getGitLabToken(string $gitlabHost): string + { + if ($this->input->getOption('gitlab-token')) { + return $this->input->getOption('gitlab-token'); + } + if (!$this->localMachineHelper->commandExists('glab')) { + throw new AcquiaCliException("Install glab to continue: https://gitlab.com/gitlab-org/cli#installation"); + } + $process = $this->localMachineHelper->execute([ + 'glab', + 'config', + 'get', + 'token', + '--host=' . $gitlabHost, + ], null, null, false); + if ($process->isSuccessful() && trim($process->getOutput())) { + return trim($process->getOutput()); + } - /** - * Getting the gitlab token from user. - */ - private function getGitLabToken(string $gitlabHost): string { - if ($this->input->getOption('gitlab-token')) { - return $this->input->getOption('gitlab-token'); - } - if (!$this->localMachineHelper->commandExists('glab')) { - throw new AcquiaCliException("Install glab to continue: https://gitlab.com/gitlab-org/cli#installation"); - } - $process = $this->localMachineHelper->execute([ - 'glab', - 'config', - 'get', - 'token', - '--host=' . $gitlabHost, - ], NULL, NULL, FALSE); - if ($process->isSuccessful() && trim($process->getOutput())) { - return trim($process->getOutput()); - } + $this->io->writeln([ + "", + "You must first authenticate with Code Studio by creating a personal access token:", + "* Visit https://$gitlabHost/-/profile/personal_access_tokens", + "* Create a token and grant it both api and write repository scopes", + "* Copy the token to your clipboard", + "* Run glab auth login --hostname=$gitlabHost and paste the token when prompted", + "* Try this command again.", + ]); - $this->io->writeln([ - "", - "You must first authenticate with Code Studio by creating a personal access token:", - "* Visit https://$gitlabHost/-/profile/personal_access_tokens", - "* Create a token and grant it both api and write repository scopes", - "* Copy the token to your clipboard", - "* Run glab auth login --hostname=$gitlabHost and paste the token when prompted", - "* Try this command again.", - ]); + throw new AcquiaCliException("Could not determine GitLab token"); + } - throw new AcquiaCliException("Could not determine GitLab token"); - } + /** + * Getting gitlab host from user. + */ + private function getGitLabHost(): string + { + // If hostname is available as argument, use that. + if ( + $this->input->hasOption('gitlab-host-name') + && $this->input->getOption('gitlab-host-name') + ) { + return $this->input->getOption('gitlab-host-name'); + } + if (!$this->localMachineHelper->commandExists('glab')) { + throw new AcquiaCliException("Install glab to continue: https://gitlab.com/gitlab-org/cli#installation"); + } + $process = $this->localMachineHelper->execute([ + 'glab', + 'config', + 'get', + 'host', + ], null, null, false); + if (!$process->isSuccessful()) { + throw new AcquiaCliException("Could not determine GitLab host: {error_message}", ['error_message' => $process->getErrorOutput()]); + } + $output = trim($process->getOutput()); + $urlParts = parse_url($output); + if (!array_key_exists('scheme', $urlParts) && !array_key_exists('host', $urlParts)) { + // $output looks like code.cloudservices.acquia.io. + return $output; + } + // $output looks like http://code.cloudservices.acquia.io/. + return $urlParts['host']; + } - /** - * Getting gitlab host from user. - */ - private function getGitLabHost(): string { - // If hostname is available as argument, use that. - if ($this->input->hasOption('gitlab-host-name') - && $this->input->getOption('gitlab-host-name')) { - return $this->input->getOption('gitlab-host-name'); + private function getGitLabClient(): Client + { + if (!isset($this->gitLabClient)) { + $gitlabClient = new Client(new Builder(new \GuzzleHttp\Client())); + $gitlabClient->setUrl('https://' . $this->gitLabHost); + $gitlabClient->authenticate($this->gitLabToken, Client::AUTH_OAUTH_TOKEN); + $this->setGitLabClient($gitlabClient); + } + return $this->gitLabClient; } - if (!$this->localMachineHelper->commandExists('glab')) { - throw new AcquiaCliException("Install glab to continue: https://gitlab.com/gitlab-org/cli#installation"); + + public function setGitLabClient(Client $client): void + { + $this->gitLabClient = $client; } - $process = $this->localMachineHelper->execute([ - 'glab', - 'config', - 'get', - 'host', - ], NULL, NULL, FALSE); - if (!$process->isSuccessful()) { - throw new AcquiaCliException("Could not determine GitLab host: {error_message}", ['error_message' => $process->getErrorOutput()]); + + private function writeApiTokenMessage(InputInterface $input): void + { + // Get Cloud access tokens. + if (!$input->getOption('key') || !$input->getOption('secret')) { + $tokenUrl = 'https://cloud.acquia.com/a/profile/tokens'; + $this->io->writeln([ + "", + "This will configure AutoDevOps for a Code Studio project using credentials", + "(an API Token and SSH Key) belonging to your current Acquia Cloud Platform user account.", + "Before continuing, make sure that you're logged into the right Acquia Cloud Platform user account.", + "", + "Typically this command should only be run once per application", + "but if your Cloud Platform account is deleted in the future, the Code Studio project will", + "need to be re-configured using a different user account.", + "", + "To begin, visit this URL and create a new API Token for Code Studio to use:", + "$tokenUrl", + ]); + } } - $output = trim($process->getOutput()); - $urlParts = parse_url($output); - if (!array_key_exists('scheme', $urlParts) && !array_key_exists('host', $urlParts)) { - // $output looks like code.cloudservices.acquia.io. - return $output; + + protected function validateEnvironment(): void + { + if (!empty(self::isAcquiaCloudIde()) && !getenv('GITLAB_HOST')) { + throw new AcquiaCliException('The GITLAB_HOST environment variable must be set or the `--gitlab-host-name` option must be passed.'); + } } - // $output looks like http://code.cloudservices.acquia.io/. - return $urlParts['host']; - } - private function getGitLabClient(): Client { - if (!isset($this->gitLabClient)) { - $gitlabClient = new Client(new Builder(new \GuzzleHttp\Client())); - $gitlabClient->setUrl('https://' . $this->gitLabHost); - $gitlabClient->authenticate($this->gitLabToken, Client::AUTH_OAUTH_TOKEN); - $this->setGitLabClient($gitlabClient); + private function authenticateWithGitLab(): void + { + $this->validateEnvironment(); + $this->gitLabHost = $this->getGitLabHost(); + $this->gitLabToken = $this->getGitLabToken($this->gitLabHost); + $this->getGitLabClient(); + try { + $this->gitLabAccount = $this->gitLabClient->users()->me(); + } catch (RuntimeException $exception) { + $this->io->error([ + "Unable to authenticate with Code Studio", + "Did you set a valid token with the api and write_repository scopes?", + "Try running `glab auth login` to re-authenticate.", + "Alternatively, pass the --gitlab-token option.", + "Then try again.", + ]); + throw new AcquiaCliException("Unable to authenticate with Code Studio"); + } } - return $this->gitLabClient; - } - public function setGitLabClient(Client $client): void { - $this->gitLabClient = $client; - } + /** + * @return array + */ + private function determineGitLabProject(ApplicationResponse $cloudApplication): array + { + // Use command option. + if ($this->input->getOption('gitlab-project-id')) { + $id = $this->input->getOption('gitlab-project-id'); + return $this->gitLabClient->projects()->show($id); + } + // Search for existing project that matches expected description pattern. + $projects = $this->gitLabClient->projects()->all(['search' => $cloudApplication->uuid]); + if ($projects) { + if (count($projects) == 1) { + return reset($projects); + } - private function writeApiTokenMessage(InputInterface $input): void { - // Get Cloud access tokens. - if (!$input->getOption('key') || !$input->getOption('secret')) { - $tokenUrl = 'https://cloud.acquia.com/a/profile/tokens'; - $this->io->writeln([ - "", - "This will configure AutoDevOps for a Code Studio project using credentials", - "(an API Token and SSH Key) belonging to your current Acquia Cloud Platform user account.", - "Before continuing, make sure that you're logged into the right Acquia Cloud Platform user account.", - "", - "Typically this command should only be run once per application", - "but if your Cloud Platform account is deleted in the future, the Code Studio project will", - "need to be re-configured using a different user account.", + return $this->promptChooseFromObjectsOrArrays( + $projects, + 'id', + 'path_with_namespace', + "Found multiple projects that could match the {$cloudApplication->name} application. Choose which one to configure." + ); + } + // Prompt to create project. + $this->io->writeln([ "", - "To begin, visit this URL and create a new API Token for Code Studio to use:", - "$tokenUrl", - ]); + "Could not find any existing Code Studio project for Acquia Cloud Platform application {$cloudApplication->name}.", + "Searched for UUID {$cloudApplication->uuid} in project descriptions.", + ]); + $createProject = $this->io->confirm('Would you like to create a new Code Studio project? If you select "no" you may choose from a full list of existing projects.'); + if ($createProject) { + return $this->createGitLabProject($cloudApplication); + } + // Prompt to choose from full list, regardless of description. + return $this->promptChooseFromObjectsOrArrays( + $this->gitLabClient->projects()->all(), + 'id', + 'path_with_namespace', + "Choose a Code Studio project to configure for the {$cloudApplication->name} application" + ); } - } - protected function validateEnvironment(): void { - if (!empty(self::isAcquiaCloudIde()) && !getenv('GITLAB_HOST')) { - throw new AcquiaCliException('The GITLAB_HOST environment variable must be set or the `--gitlab-host-name` option must be passed.'); - } - } + /** + * @return array + */ + private function createGitLabProject(ApplicationResponse $cloudApplication): array + { + $userGroups = $this->gitLabClient->groups()->all([ + 'all_available' => true, + 'min_access_level' => 40, + ]); + $parameters = $this->getGitLabProjectDefaults(); + if ($userGroups) { + $userGroups[] = $this->gitLabClient->namespaces()->show($this->gitLabAccount['username']); + $projectGroup = $this->promptChooseFromObjectsOrArrays($userGroups, 'id', 'path', 'Choose which group this new project should belong to:'); + $parameters['namespace_id'] = $projectGroup['id']; + } - private function authenticateWithGitLab(): void { - $this->validateEnvironment(); - $this->gitLabHost = $this->getGitLabHost(); - $this->gitLabToken = $this->getGitLabToken($this->gitLabHost); - $this->getGitLabClient(); - try { - $this->gitLabAccount = $this->gitLabClient->users()->me(); - } - catch (RuntimeException $exception) { - $this->io->error([ - "Unable to authenticate with Code Studio", - "Did you set a valid token with the api and write_repository scopes?", - "Try running `glab auth login` to re-authenticate.", - "Alternatively, pass the --gitlab-token option.", - "Then try again.", - ]); - throw new AcquiaCliException("Unable to authenticate with Code Studio"); - } - } + $slugger = new AsciiSlugger(); + $projectName = (string) $slugger->slug($cloudApplication->name); + $project = $this->gitLabClient->projects()->create($projectName, $parameters); + try { + $this->gitLabClient->projects() + ->uploadAvatar($project['id'], __DIR__ . '/drupal_icon.png'); + } catch (ValidationFailedException) { + $this->io->warning("Failed to upload project avatar"); + } + $this->io->success("Created {$project['path_with_namespace']} project in Code Studio."); - /** - * @return array - */ - private function determineGitLabProject(ApplicationResponse $cloudApplication): array { - // Use command option. - if ($this->input->getOption('gitlab-project-id')) { - $id = $this->input->getOption('gitlab-project-id'); - return $this->gitLabClient->projects()->show($id); + return $project; } - // Search for existing project that matches expected description pattern. - $projects = $this->gitLabClient->projects()->all(['search' => $cloudApplication->uuid]); - if ($projects) { - if (count($projects) == 1) { - return reset($projects); - } - return $this->promptChooseFromObjectsOrArrays( - $projects, - 'id', - 'path_with_namespace', - "Found multiple projects that could match the {$cloudApplication->name} application. Choose which one to configure." - ); + private function setGitLabProjectDescription(mixed $cloudApplicationUuid): void + { + $this->gitLabProjectDescription = "Source repository for Acquia Cloud Platform application $cloudApplicationUuid"; } - // Prompt to create project. - $this->io->writeln([ - "", - "Could not find any existing Code Studio project for Acquia Cloud Platform application {$cloudApplication->name}.", - "Searched for UUID {$cloudApplication->uuid} in project descriptions.", - ]); - $createProject = $this->io->confirm('Would you like to create a new Code Studio project? If you select "no" you may choose from a full list of existing projects.'); - if ($createProject) { - return $this->createGitLabProject($cloudApplication); - } - // Prompt to choose from full list, regardless of description. - return $this->promptChooseFromObjectsOrArrays( - $this->gitLabClient->projects()->all(), - 'id', - 'path_with_namespace', - "Choose a Code Studio project to configure for the {$cloudApplication->name} application" - ); - } - /** - * @return array - */ - private function createGitLabProject(ApplicationResponse $cloudApplication): array { - $userGroups = $this->gitLabClient->groups()->all([ - 'all_available' => TRUE, - 'min_access_level' => 40, - ]); - $parameters = $this->getGitLabProjectDefaults(); - if ($userGroups) { - $userGroups[] = $this->gitLabClient->namespaces()->show($this->gitLabAccount['username']); - $projectGroup = $this->promptChooseFromObjectsOrArrays($userGroups, 'id', 'path', 'Choose which group this new project should belong to:'); - $parameters['namespace_id'] = $projectGroup['id']; + /** + * @return array + */ + private function getGitLabProjectDefaults(): array + { + return [ + 'container_registry_access_level' => 'disabled', + 'default_branch' => 'main', + 'description' => $this->gitLabProjectDescription, + 'initialize_with_readme' => true, + 'topics' => 'Acquia Cloud Application', + ]; } - $slugger = new AsciiSlugger(); - $projectName = (string) $slugger->slug($cloudApplication->name); - $project = $this->gitLabClient->projects()->create($projectName, $parameters); - try { - $this->gitLabClient->projects() - ->uploadAvatar($project['id'], __DIR__ . '/drupal_icon.png'); - } - catch (ValidationFailedException) { - $this->io->warning("Failed to upload project avatar"); + /** + * Add gitlab options to the command. + * + * @return $this + */ + private function acceptGitlabOptions(): static + { + $this->addOption('gitlab-token', null, InputOption::VALUE_REQUIRED, 'The GitLab personal access token that will be used to communicate with the GitLab instance') + ->addOption('gitlab-project-id', null, InputOption::VALUE_REQUIRED, 'The project ID (an integer) of the GitLab project to configure.') + ->addOption('gitlab-host-name', null, InputOption::VALUE_REQUIRED, 'The GitLab hostname.'); + return $this; } - $this->io->success("Created {$project['path_with_namespace']} project in Code Studio."); - - return $project; - } - - private function setGitLabProjectDescription(mixed $cloudApplicationUuid): void { - $this->gitLabProjectDescription = "Source repository for Acquia Cloud Platform application $cloudApplicationUuid"; - } - - /** - * @return array - */ - private function getGitLabProjectDefaults(): array { - return [ - 'container_registry_access_level' => 'disabled', - 'default_branch' => 'main', - 'description' => $this->gitLabProjectDescription, - 'initialize_with_readme' => TRUE, - 'topics' => 'Acquia Cloud Application', - ]; - } - - /** - * Add gitlab options to the command. - * - * @return $this - */ - private function acceptGitlabOptions(): static { - $this->addOption('gitlab-token', NULL, InputOption::VALUE_REQUIRED, 'The GitLab personal access token that will be used to communicate with the GitLab instance') - ->addOption('gitlab-project-id', NULL, InputOption::VALUE_REQUIRED, 'The project ID (an integer) of the GitLab project to configure.') - ->addOption('gitlab-host-name', NULL, InputOption::VALUE_REQUIRED, 'The GitLab hostname.'); - return $this; - } - } diff --git a/src/Command/CodeStudio/CodeStudioPhpVersionCommand.php b/src/Command/CodeStudio/CodeStudioPhpVersionCommand.php index 64a9cf69c..537ffcc71 100644 --- a/src/Command/CodeStudio/CodeStudioPhpVersionCommand.php +++ b/src/Command/CodeStudio/CodeStudioPhpVersionCommand.php @@ -1,6 +1,6 @@ addArgument('php-version', InputArgument::REQUIRED, 'The PHP version that needs to configured or updated') + ->addUsage('8.1 myapp') + ->addUsage('8.1 abcd1234-1111-2222-3333-0e02b2c3d470'); + $this->acceptApplicationUuid(); + $this->acceptGitlabOptions(); + } - protected function configure(): void { - $this - ->addArgument('php-version', InputArgument::REQUIRED, 'The PHP version that needs to configured or updated') - ->addUsage('8.1 myapp') - ->addUsage('8.1 abcd1234-1111-2222-3333-0e02b2c3d470'); - $this->acceptApplicationUuid(); - $this->acceptGitlabOptions(); - } + protected function execute(InputInterface $input, OutputInterface $output): int + { + $phpVersion = $input->getArgument('php-version'); + $this->validatePhpVersion($phpVersion); + $this->authenticateWithGitLab(); + $acquiaCloudAppId = $this->determineCloudApplication(); - protected function execute(InputInterface $input, OutputInterface $output): int { - $phpVersion = $input->getArgument('php-version'); - $this->validatePhpVersion($phpVersion); - $this->authenticateWithGitLab(); - $acquiaCloudAppId = $this->determineCloudApplication(); + // Get the GitLab project details attached with the given Cloud application. + $cloudApplication = $this->getCloudApplication($acquiaCloudAppId); + $project = $this->determineGitLabProject($cloudApplication); - // Get the GitLab project details attached with the given Cloud application. - $cloudApplication = $this->getCloudApplication($acquiaCloudAppId); - $project = $this->determineGitLabProject($cloudApplication); + // If CI/CD is not enabled for the project in Code Studio. + if (empty($project['jobs_enabled'])) { + $this->io->error('CI/CD is not enabled for this application in code studio. Enable it first and then try again.'); + return self::FAILURE; + } - // If CI/CD is not enabled for the project in Code Studio. - if (empty($project['jobs_enabled'])) { - $this->io->error('CI/CD is not enabled for this application in code studio. Enable it first and then try again.'); - return self::FAILURE; - } + try { + $phpVersionAlreadySet = false; + // Get all variables of the project. + $allProjectVariables = $this->gitLabClient->projects()->variables($project['id']); + if (!empty($allProjectVariables)) { + $variables = array_column($allProjectVariables, 'value', 'key'); + $phpVersionAlreadySet = $variables['PHP_VERSION'] ?? false; + } + // If PHP version is not set in variables. + if (!$phpVersionAlreadySet) { + $this->gitLabClient->projects()->addVariable($project['id'], 'PHP_VERSION', $phpVersion); + } else { + // If variable already exists, updating the variable. + $this->gitLabClient->projects()->updateVariable($project['id'], 'PHP_VERSION', $phpVersion); + } + } catch (RuntimeException) { + $this->io->error("Unable to update the PHP version to $phpVersion"); + return self::FAILURE; + } - try { - $phpVersionAlreadySet = FALSE; - // Get all variables of the project. - $allProjectVariables = $this->gitLabClient->projects()->variables($project['id']); - if (!empty($allProjectVariables)) { - $variables = array_column($allProjectVariables, 'value', 'key'); - $phpVersionAlreadySet = $variables['PHP_VERSION'] ?? FALSE; - } - // If PHP version is not set in variables. - if (!$phpVersionAlreadySet) { - $this->gitLabClient->projects()->addVariable($project['id'], 'PHP_VERSION', $phpVersion); - } - else { - // If variable already exists, updating the variable. - $this->gitLabClient->projects()->updateVariable($project['id'], 'PHP_VERSION', $phpVersion); - } - } - catch (RuntimeException) { - $this->io->error("Unable to update the PHP version to $phpVersion"); - return self::FAILURE; + $this->io->success("PHP version is updated to $phpVersion successfully!"); + return self::SUCCESS; } - - $this->io->success("PHP version is updated to $phpVersion successfully!"); - return self::SUCCESS; - } - } diff --git a/src/Command/CodeStudio/CodeStudioPipelinesMigrateCommand.php b/src/Command/CodeStudio/CodeStudioPipelinesMigrateCommand.php index ec0aa0029..46994b7c1 100644 --- a/src/Command/CodeStudio/CodeStudioPipelinesMigrateCommand.php +++ b/src/Command/CodeStudio/CodeStudioPipelinesMigrateCommand.php @@ -1,6 +1,6 @@ addOption('key', NULL, InputOption::VALUE_REQUIRED, 'The Cloud Platform API token that Code Studio will use') - ->addOption('secret', NULL, InputOption::VALUE_REQUIRED, 'The Cloud Platform API secret that Code Studio will use') - ->addOption('gitlab-token', NULL, InputOption::VALUE_REQUIRED, 'The GitLab personal access token that will be used to communicate with the GitLab instance') - ->addOption('gitlab-project-id', NULL, InputOption::VALUE_REQUIRED, 'The project ID (an integer) of the GitLab project to configure.'); - $this->acceptApplicationUuid(); - $this->setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv()); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $this->authenticateWithGitLab(); - $this->writeApiTokenMessage($input); - $cloudKey = $this->determineApiKey(); - $cloudSecret = $this->determineApiSecret(); - // We may already be authenticated with Acquia Cloud Platform via a refresh token. - // But, we specifically need an API Token key-pair of Code Studio. - // So we reauthenticate to be sure we're using the provided credentials. - $this->reAuthenticate($cloudKey, $cloudSecret, $this->cloudCredentials->getBaseUri(), $this->cloudCredentials->getAccountsUri()); - $cloudApplicationUuid = $this->determineCloudApplication(); + protected function configure(): void + { + $this + ->addOption('key', null, InputOption::VALUE_REQUIRED, 'The Cloud Platform API token that Code Studio will use') + ->addOption('secret', null, InputOption::VALUE_REQUIRED, 'The Cloud Platform API secret that Code Studio will use') + ->addOption('gitlab-token', null, InputOption::VALUE_REQUIRED, 'The GitLab personal access token that will be used to communicate with the GitLab instance') + ->addOption('gitlab-project-id', null, InputOption::VALUE_REQUIRED, 'The project ID (an integer) of the GitLab project to configure.'); + $this->acceptApplicationUuid(); + $this->setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv()); + } - // Get Cloud account. - $acquiaCloudClient = $this->cloudApiClientService->getClient(); - $accountAdapter = new Account($acquiaCloudClient); - $account = $accountAdapter->get(); - $this->setGitLabProjectDescription($cloudApplicationUuid); + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->authenticateWithGitLab(); + $this->writeApiTokenMessage($input); + $cloudKey = $this->determineApiKey(); + $cloudSecret = $this->determineApiSecret(); + // We may already be authenticated with Acquia Cloud Platform via a refresh token. + // But, we specifically need an API Token key-pair of Code Studio. + // So we reauthenticate to be sure we're using the provided credentials. + $this->reAuthenticate($cloudKey, $cloudSecret, $this->cloudCredentials->getBaseUri(), $this->cloudCredentials->getAccountsUri()); + $cloudApplicationUuid = $this->determineCloudApplication(); - // Get Cloud application. - $cloudApplication = $this->getCloudApplication($cloudApplicationUuid); - $project = $this->determineGitLabProject($cloudApplication); + // Get Cloud account. + $acquiaCloudClient = $this->cloudApiClientService->getClient(); + $accountAdapter = new Account($acquiaCloudClient); + $account = $accountAdapter->get(); + $this->setGitLabProjectDescription($cloudApplicationUuid); - // Migrate acquia-pipeline file. - $this->checkGitLabCiCdVariables($project); - $this->validateCwdIsValidDrupalProject(); - $acquiaPipelinesFileDetails = $this->getAcquiaPipelinesFileContents($project); - $acquiaPipelinesFileContents = $acquiaPipelinesFileDetails['file_contents']; - $acquiaPipelinesFileName = $acquiaPipelinesFileDetails['filename']; - $gitlabCiFileContents = $this->getGitLabCiFileTemplate(); - $this->migrateVariablesSection($acquiaPipelinesFileContents, $gitlabCiFileContents); - $this->migrateEventsSection($acquiaPipelinesFileContents, $gitlabCiFileContents); - $this->removeEmptyScript($gitlabCiFileContents); - $this->createGitLabCiFile($gitlabCiFileContents, $acquiaPipelinesFileName); - $this->io->success([ - "", - "Migration completed successfully.", - "Created .gitlab-ci.yml and removed acquia-pipeline.yml file.", - "In order to run Pipeline, push .gitlab-ci.yaml to Main branch of Code Studio project.", - "Check your pipeline is running in Code Studio for your project.", - ]); + // Get Cloud application. + $cloudApplication = $this->getCloudApplication($cloudApplicationUuid); + $project = $this->determineGitLabProject($cloudApplication); - return Command::SUCCESS; - } + // Migrate acquia-pipeline file. + $this->checkGitLabCiCdVariables($project); + $this->validateCwdIsValidDrupalProject(); + $acquiaPipelinesFileDetails = $this->getAcquiaPipelinesFileContents($project); + $acquiaPipelinesFileContents = $acquiaPipelinesFileDetails['file_contents']; + $acquiaPipelinesFileName = $acquiaPipelinesFileDetails['filename']; + $gitlabCiFileContents = $this->getGitLabCiFileTemplate(); + $this->migrateVariablesSection($acquiaPipelinesFileContents, $gitlabCiFileContents); + $this->migrateEventsSection($acquiaPipelinesFileContents, $gitlabCiFileContents); + $this->removeEmptyScript($gitlabCiFileContents); + $this->createGitLabCiFile($gitlabCiFileContents, $acquiaPipelinesFileName); + $this->io->success([ + "", + "Migration completed successfully.", + "Created .gitlab-ci.yml and removed acquia-pipeline.yml file.", + "In order to run Pipeline, push .gitlab-ci.yaml to Main branch of Code Studio project.", + "Check your pipeline is running in Code Studio for your project.", + ]); - /** - * Check whether wizard command is executed by checking the env variable of codestudio project. - */ - private function checkGitLabCiCdVariables(array $project): void { - $gitlabCicdVariables = CodeStudioCiCdVariables::getList(); - $gitlabCicdExistingVariables = $this->gitLabClient->projects()->variables($project['id']); - $existingKeys = array_column($gitlabCicdExistingVariables, 'key'); - foreach ($gitlabCicdVariables as $gitlabCicdVariable) { - if (!in_array($gitlabCicdVariable, $existingKeys, TRUE)) { - throw new AcquiaCliException("Code Studio CI/CD variable {$gitlabCicdVariable} is not configured properly"); - } + return Command::SUCCESS; } - } - /** - * Check acquia-pipeline.yml file exists in the root repo and remove ci_config_path from codestudio project. - * - * @return array - */ - private function getAcquiaPipelinesFileContents(array $project): array { - $pipelinesFilepathYml = Path::join($this->projectDir, 'acquia-pipelines.yml'); - $pipelinesFilepathYaml = Path::join($this->projectDir, 'acquia-pipelines.yaml'); - if ($this->localMachineHelper->getFilesystem()->exists($pipelinesFilepathYml) || - $this->localMachineHelper->getFilesystem()->exists($pipelinesFilepathYaml) - ) { - $this->gitLabClient->projects()->update($project['id'], ['ci_config_path' => '']); - $pipelinesFilenames = ['acquia-pipelines.yml', 'acquia-pipelines.yaml']; - foreach ($pipelinesFilenames as $pipelinesFilename) { - $pipelinesFilepath = Path::join($this->projectDir, $pipelinesFilename); - if (file_exists($pipelinesFilepath)) { - $fileContents = file_get_contents($pipelinesFilepath); - return [ - 'filename' => $pipelinesFilename, - 'file_contents' => Yaml::parse($fileContents, Yaml::PARSE_OBJECT), -]; + /** + * Check whether wizard command is executed by checking the env variable of codestudio project. + */ + private function checkGitLabCiCdVariables(array $project): void + { + $gitlabCicdVariables = CodeStudioCiCdVariables::getList(); + $gitlabCicdExistingVariables = $this->gitLabClient->projects()->variables($project['id']); + $existingKeys = array_column($gitlabCicdExistingVariables, 'key'); + foreach ($gitlabCicdVariables as $gitlabCicdVariable) { + if (!in_array($gitlabCicdVariable, $existingKeys, true)) { + throw new AcquiaCliException("Code Studio CI/CD variable {$gitlabCicdVariable} is not configured properly"); + } } - } } - throw new AcquiaCliException("Missing 'acquia-pipelines.yml' file which is required to migrate the project to Code Studio."); - } - - /** - * Migrating standard template to .gitlab-ci.yml file. - * - * @return array - */ - private function getGitLabCiFileTemplate(): array { - return [ - 'include' => ['project' => 'acquia/standard-template', 'file' => '/gitlab-ci/Auto-DevOps.acquia.gitlab-ci.yml'], - ]; - } + /** + * Check acquia-pipeline.yml file exists in the root repo and remove ci_config_path from codestudio project. + * + * @return array + */ + private function getAcquiaPipelinesFileContents(array $project): array + { + $pipelinesFilepathYml = Path::join($this->projectDir, 'acquia-pipelines.yml'); + $pipelinesFilepathYaml = Path::join($this->projectDir, 'acquia-pipelines.yaml'); + if ( + $this->localMachineHelper->getFilesystem()->exists($pipelinesFilepathYml) || + $this->localMachineHelper->getFilesystem()->exists($pipelinesFilepathYaml) + ) { + $this->gitLabClient->projects()->update($project['id'], ['ci_config_path' => '']); + $pipelinesFilenames = ['acquia-pipelines.yml', 'acquia-pipelines.yaml']; + foreach ($pipelinesFilenames as $pipelinesFilename) { + $pipelinesFilepath = Path::join($this->projectDir, $pipelinesFilename); + if (file_exists($pipelinesFilepath)) { + $fileContents = file_get_contents($pipelinesFilepath); + return [ + 'filename' => $pipelinesFilename, + 'file_contents' => Yaml::parse($fileContents, Yaml::PARSE_OBJECT), + ]; + } + } + } - /** - * Migrating `variables` section to .gitlab-ci.yml file. - */ - private function migrateVariablesSection(mixed $acquiaPipelinesFileContents, mixed &$gitlabCiFileContents): void { - if (array_key_exists('variables', $acquiaPipelinesFileContents)) { - $variablesDump = Yaml::dump(['variables' => $acquiaPipelinesFileContents['variables']]); - $removeGlobal = preg_replace('/global:/', '', $variablesDump); - $variablesParse = Yaml::parse($removeGlobal); - $gitlabCiFileContents = array_merge($gitlabCiFileContents, $variablesParse); - $this->io->success([ - "Migrated `variables` section of acquia-pipelines.yml to .gitlab-ci.yml", - ]); - } - else { - $this->io->info([ - "Checked acquia-pipeline.yml file for `variables` section", - ]); + throw new AcquiaCliException("Missing 'acquia-pipelines.yml' file which is required to migrate the project to Code Studio."); } - } - private function getPipelinesSection(array $acquiaPipelinesFileContents, string $eventName): mixed { - if (!array_key_exists('events', $acquiaPipelinesFileContents)) { - return NULL; + /** + * Migrating standard template to .gitlab-ci.yml file. + * + * @return array + */ + private function getGitLabCiFileTemplate(): array + { + return [ + 'include' => ['project' => 'acquia/standard-template', 'file' => '/gitlab-ci/Auto-DevOps.acquia.gitlab-ci.yml'], + ]; } - if (array_key_exists('build', $acquiaPipelinesFileContents['events']) && empty($acquiaPipelinesFileContents['events']['build'])) { - return NULL; + + /** + * Migrating `variables` section to .gitlab-ci.yml file. + */ + private function migrateVariablesSection(mixed $acquiaPipelinesFileContents, mixed &$gitlabCiFileContents): void + { + if (array_key_exists('variables', $acquiaPipelinesFileContents)) { + $variablesDump = Yaml::dump(['variables' => $acquiaPipelinesFileContents['variables']]); + $removeGlobal = preg_replace('/global:/', '', $variablesDump); + $variablesParse = Yaml::parse($removeGlobal); + $gitlabCiFileContents = array_merge($gitlabCiFileContents, $variablesParse); + $this->io->success([ + "Migrated `variables` section of acquia-pipelines.yml to .gitlab-ci.yml", + ]); + } else { + $this->io->info([ + "Checked acquia-pipeline.yml file for `variables` section", + ]); + } } - if (!array_key_exists($eventName, $acquiaPipelinesFileContents['events'])) { - return NULL; + + private function getPipelinesSection(array $acquiaPipelinesFileContents, string $eventName): mixed + { + if (!array_key_exists('events', $acquiaPipelinesFileContents)) { + return null; + } + if (array_key_exists('build', $acquiaPipelinesFileContents['events']) && empty($acquiaPipelinesFileContents['events']['build'])) { + return null; + } + if (!array_key_exists($eventName, $acquiaPipelinesFileContents['events'])) { + return null; + } + return $acquiaPipelinesFileContents['events'][$eventName]['steps'] ?? null; } - return $acquiaPipelinesFileContents['events'][$eventName]['steps'] ?? NULL; - } - private function migrateEventsSection(array $acquiaPipelinesFileContents, array &$gitlabCiFileContents): void { - // phpcs:disable SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys - $eventsMap = [ - 'build' => [ + private function migrateEventsSection(array $acquiaPipelinesFileContents, array &$gitlabCiFileContents): void + { + // phpcs:disable SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys + $eventsMap = [ + 'build' => [ 'skip' => [ - 'composer install' => [ - 'message' => 'Code Studio AutoDevOps will run `composer install` by default. Skipping migration of this command in your acquia-pipelines.yml file:', - 'prompt' => FALSE, - ], - '${BLT_DIR}' => [ - 'message' => 'Code Studio AutoDevOps will run BLT commands for you by default. Do you want to migrate the following command?', - 'prompt' => TRUE, - ], + 'composer install' => [ + 'message' => 'Code Studio AutoDevOps will run `composer install` by default. Skipping migration of this command in your acquia-pipelines.yml file:', + 'prompt' => false, + ], + '${BLT_DIR}' => [ + 'message' => 'Code Studio AutoDevOps will run BLT commands for you by default. Do you want to migrate the following command?', + 'prompt' => true, + ], ], 'default_stage' => 'Test Drupal', 'stage' => [ - 'setup' => 'Build Drupal', - 'npm run build' => 'Build Drupal', - 'validate' => 'Test Drupal', - 'tests' => 'Test Drupal', - 'test' => 'Test Drupal', - 'npm test' => 'Test Drupal', - 'artifact' => 'Deploy Drupal', - 'deploy' => 'Deploy Drupal', + 'setup' => 'Build Drupal', + 'npm run build' => 'Build Drupal', + 'validate' => 'Test Drupal', + 'tests' => 'Test Drupal', + 'test' => 'Test Drupal', + 'npm test' => 'Test Drupal', + 'artifact' => 'Deploy Drupal', + 'deploy' => 'Deploy Drupal', ], 'needs' => [ - 'Build Code', - 'Manage Secrets', + 'Build Code', + 'Manage Secrets', + ], ], - ], - 'post-deploy' => [ + 'post-deploy' => [ 'skip' => [ - 'launch_ode' => [ - 'message' => 'Code Studio AutoDevOps will run Launch a new Continuous Delivery Environment (CDE) automatically for new merge requests. Skipping migration of this command in your acquia-pipelines.yml file:', - 'prompt' => FALSE, - ], + 'launch_ode' => [ + 'message' => 'Code Studio AutoDevOps will run Launch a new Continuous Delivery Environment (CDE) automatically for new merge requests. Skipping migration of this command in your acquia-pipelines.yml file:', + 'prompt' => false, + ], ], 'default_stage' => 'Deploy Drupal', 'stage' => [ - 'launch_ode' => 'Deploy Drupal', + 'launch_ode' => 'Deploy Drupal', ], 'needs' => [ - 'Create artifact from branch', + 'Create artifact from branch', + ], ], - ], - ]; - // phpcs:enable + ]; + // phpcs:enable - $codeStudioJobs = []; - foreach ($eventsMap as $eventName => $eventMap) { - $eventSteps = $this->getPipelinesSection($acquiaPipelinesFileContents, $eventName); - if ($eventSteps) { - foreach ($eventSteps as $step) { - $scriptName = array_keys($step)[0]; - if (!array_key_exists('script', $step[$scriptName]) || empty($step[$scriptName]['script'])) { - continue; - } - if ($stage = $this->assignStageFromKeywords($eventMap['stage'], $scriptName)) { - $codeStudioJobs[$scriptName]['stage'] = $stage; - } - foreach ($step[$scriptName]['script'] as $command) { - foreach ($eventMap['skip'] as $needle => $messageConfig) { - if (str_contains($command, $needle)) { - if ($messageConfig['prompt']) { - $answer = $this->io->confirm($messageConfig['message'] . PHP_EOL . $command, FALSE); - if ($answer == 1) { - $codeStudioJobs[$scriptName]['script'][] = $command; - $codeStudioJobs[$scriptName]['script'] = array_values(array_unique($codeStudioJobs[$scriptName]['script'])); - } - else if (($key = array_search($command, $codeStudioJobs[$scriptName]['script'], TRUE)) !== FALSE) { - unset($codeStudioJobs[$scriptName]['script'][$key]); - } - } - else { - $this->io->note([ - $messageConfig['message'], - $command, - ]); - } - break; - } + $codeStudioJobs = []; + foreach ($eventsMap as $eventName => $eventMap) { + $eventSteps = $this->getPipelinesSection($acquiaPipelinesFileContents, $eventName); + if ($eventSteps) { + foreach ($eventSteps as $step) { + $scriptName = array_keys($step)[0]; + if (!array_key_exists('script', $step[$scriptName]) || empty($step[$scriptName]['script'])) { + continue; + } + if ($stage = $this->assignStageFromKeywords($eventMap['stage'], $scriptName)) { + $codeStudioJobs[$scriptName]['stage'] = $stage; + } + foreach ($step[$scriptName]['script'] as $command) { + foreach ($eventMap['skip'] as $needle => $messageConfig) { + if (str_contains($command, $needle)) { + if ($messageConfig['prompt']) { + $answer = $this->io->confirm($messageConfig['message'] . PHP_EOL . $command, false); + if ($answer == 1) { + $codeStudioJobs[$scriptName]['script'][] = $command; + $codeStudioJobs[$scriptName]['script'] = array_values(array_unique($codeStudioJobs[$scriptName]['script'])); + } elseif (($key = array_search($command, $codeStudioJobs[$scriptName]['script'], true)) !== false) { + unset($codeStudioJobs[$scriptName]['script'][$key]); + } + } else { + $this->io->note([ + $messageConfig['message'], + $command, + ]); + } + break; + } - if (array_key_exists($scriptName, $codeStudioJobs) && array_key_exists('script', $codeStudioJobs[$scriptName]) && in_array($command, $codeStudioJobs[$scriptName]['script'], TRUE)) { - break; - } - if (!array_key_exists($scriptName, $eventMap['skip']) ) { - $codeStudioJobs[$scriptName]['script'][] = $command; - $codeStudioJobs[$scriptName]['script'] = array_values(array_unique($codeStudioJobs[$scriptName]['script'])); - } - else if ($scriptName === 'launch_ode') { - $codeStudioJobs[$scriptName]['script'][] = $command; - } - } - if (array_key_exists($scriptName, $codeStudioJobs) && !array_key_exists('stage', $codeStudioJobs[$scriptName]) - && $stage = $this->assignStageFromKeywords($eventMap['stage'], $command)) { - $codeStudioJobs[$scriptName]['stage'] = $stage; + if (array_key_exists($scriptName, $codeStudioJobs) && array_key_exists('script', $codeStudioJobs[$scriptName]) && in_array($command, $codeStudioJobs[$scriptName]['script'], true)) { + break; + } + if (!array_key_exists($scriptName, $eventMap['skip'])) { + $codeStudioJobs[$scriptName]['script'][] = $command; + $codeStudioJobs[$scriptName]['script'] = array_values(array_unique($codeStudioJobs[$scriptName]['script'])); + } elseif ($scriptName === 'launch_ode') { + $codeStudioJobs[$scriptName]['script'][] = $command; + } + } + if ( + array_key_exists($scriptName, $codeStudioJobs) && !array_key_exists('stage', $codeStudioJobs[$scriptName]) + && $stage = $this->assignStageFromKeywords($eventMap['stage'], $command) + ) { + $codeStudioJobs[$scriptName]['stage'] = $stage; + } + } + if (!array_key_exists('stage', $codeStudioJobs[$scriptName])) { + $codeStudioJobs[$scriptName]['stage'] = $eventMap['default_stage']; + } + $codeStudioJobs[$scriptName]['needs'] = $eventMap['needs']; + } + $gitlabCiFileContents = array_merge($gitlabCiFileContents, $codeStudioJobs); + $this->io->success([ + "Completed migration of the $eventName step in your acquia-pipelines.yml file", + ]); + } else { + $this->io->writeln([ + "acquia-pipeline.yml file does not contain $eventName step to migrate", + ]); } - } - if (!array_key_exists('stage', $codeStudioJobs[$scriptName])) { - $codeStudioJobs[$scriptName]['stage'] = $eventMap['default_stage']; - } - $codeStudioJobs[$scriptName]['needs'] = $eventMap['needs']; } - $gitlabCiFileContents = array_merge($gitlabCiFileContents, $codeStudioJobs); - $this->io->success([ - "Completed migration of the $eventName step in your acquia-pipelines.yml file", - ]); - } - else { - $this->io->writeln([ - "acquia-pipeline.yml file does not contain $eventName step to migrate", - ]); - } } - } - - /** - * Removing empty script. - */ - private function removeEmptyScript(array &$gitlabCiFileContents): void { - foreach ($gitlabCiFileContents as $key => $value) { - if (array_key_exists('script', $value) && empty($value['script'])) { - unset($gitlabCiFileContents[$key]); - } + /** + * Removing empty script. + */ + private function removeEmptyScript(array &$gitlabCiFileContents): void + { + foreach ($gitlabCiFileContents as $key => $value) { + if (array_key_exists('script', $value) && empty($value['script'])) { + unset($gitlabCiFileContents[$key]); + } + } } - } - /** - * Creating .gitlab-ci.yml file. - */ - private function createGitLabCiFile(array $contents, string|iterable $acquiaPipelinesFileName): void { - $gitlabCiFilepath = Path::join($this->projectDir, '.gitlab-ci.yml'); - $this->localMachineHelper->getFilesystem()->dumpFile($gitlabCiFilepath, Yaml::dump($contents, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK)); - $this->localMachineHelper->getFilesystem()->remove($acquiaPipelinesFileName); - } - - private function assignStageFromKeywords(array $keywords, string $haystack): ?string { - foreach ($keywords as $needle => $stage) { - if (str_contains($haystack, $needle)) { - return $stage; - } + /** + * Creating .gitlab-ci.yml file. + */ + private function createGitLabCiFile(array $contents, string|iterable $acquiaPipelinesFileName): void + { + $gitlabCiFilepath = Path::join($this->projectDir, '.gitlab-ci.yml'); + $this->localMachineHelper->getFilesystem()->dumpFile($gitlabCiFilepath, Yaml::dump($contents, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK)); + $this->localMachineHelper->getFilesystem()->remove($acquiaPipelinesFileName); } - return NULL; - } + private function assignStageFromKeywords(array $keywords, string $haystack): ?string + { + foreach ($keywords as $needle => $stage) { + if (str_contains($haystack, $needle)) { + return $stage; + } + } + return null; + } } diff --git a/src/Command/CodeStudio/CodeStudioWizardCommand.php b/src/Command/CodeStudio/CodeStudioWizardCommand.php index afeea6b30..1e1c4ded8 100644 --- a/src/Command/CodeStudio/CodeStudioWizardCommand.php +++ b/src/Command/CodeStudio/CodeStudioWizardCommand.php @@ -1,6 +1,6 @@ addOption('key', null, InputOption::VALUE_REQUIRED, 'The Cloud Platform API token that Code Studio will use') + ->addOption('secret', null, InputOption::VALUE_REQUIRED, 'The Cloud Platform API secret that Code Studio will use') + ->addOption('gitlab-token', null, InputOption::VALUE_REQUIRED, 'The GitLab personal access token that will be used to communicate with the GitLab instance') + ->addOption('gitlab-project-id', null, InputOption::VALUE_REQUIRED, 'The project ID (an integer) of the GitLab project to configure.') + ->addOption('gitlab-host-name', null, InputOption::VALUE_REQUIRED, 'The GitLab hostname.'); + $this->acceptApplicationUuid(); + } - protected function configure(): void { - $this - ->addOption('key', NULL, InputOption::VALUE_REQUIRED, 'The Cloud Platform API token that Code Studio will use') - ->addOption('secret', NULL, InputOption::VALUE_REQUIRED, 'The Cloud Platform API secret that Code Studio will use') - ->addOption('gitlab-token', NULL, InputOption::VALUE_REQUIRED, 'The GitLab personal access token that will be used to communicate with the GitLab instance') - ->addOption('gitlab-project-id', NULL, InputOption::VALUE_REQUIRED, 'The project ID (an integer) of the GitLab project to configure.') - ->addOption('gitlab-host-name', NULL, InputOption::VALUE_REQUIRED, 'The GitLab hostname.'); - $this->acceptApplicationUuid(); - } + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->checklist = new Checklist($output); + $this->authenticateWithGitLab(); + $this->writeApiTokenMessage($input); + $cloudKey = $this->determineApiKey(); + $cloudSecret = $this->determineApiSecret(); + // We may already be authenticated with Acquia Cloud Platform via a refresh token. + // But, we specifically need an API Token key-pair of Code Studio. + // So we reauthenticate to be sure we're using the provided credentials. + $this->reAuthenticate($cloudKey, $cloudSecret, $this->cloudCredentials->getBaseUri(), $this->cloudCredentials->getAccountsUri()); + $phpVersion = null; + $nodeVersion = null; + $projectType = $this->getListOfProjectType(); + $projectSelected = $this->io->choice('Select a project type', $projectType, "Drupal_project"); - protected function execute(InputInterface $input, OutputInterface $output): int { - $this->checklist = new Checklist($output); - $this->authenticateWithGitLab(); - $this->writeApiTokenMessage($input); - $cloudKey = $this->determineApiKey(); - $cloudSecret = $this->determineApiSecret(); - // We may already be authenticated with Acquia Cloud Platform via a refresh token. - // But, we specifically need an API Token key-pair of Code Studio. - // So we reauthenticate to be sure we're using the provided credentials. - $this->reAuthenticate($cloudKey, $cloudSecret, $this->cloudCredentials->getBaseUri(), $this->cloudCredentials->getAccountsUri()); - $phpVersion = NULL; - $nodeVersion = NULL; - $projectType = $this->getListOfProjectType(); - $projectSelected = $this->io->choice('Select a project type', $projectType, "Drupal_project"); + switch ($projectSelected) { + case "Drupal_project": + $phpVersions = [ + 'PHP_version_8.1' => "8.1", + 'PHP_version_8.2' => "8.2", + 'PHP_version_8.3' => "8.3", + ]; + $project = $this->io->choice('Select a PHP version', array_values($phpVersions), "8.1"); + $project = array_search($project, $phpVersions, true); + $phpVersion = $phpVersions[$project]; + break; + case "Node_project": + $nodeVersions = [ + 'NODE_version_18.17.1' => "18.17.1", + 'NODE_version_20.5.1' => "20.5.1", + ]; + $project = $this->io->choice('Select a NODE version', array_values($nodeVersions), "18.17.1"); + $project = array_search($project, $nodeVersions, true); + $nodeVersion = $nodeVersions[$project]; + break; + } - switch ($projectSelected) { - case "Drupal_project": - $phpVersions = [ - 'PHP_version_8.1' => "8.1", - 'PHP_version_8.2' => "8.2", - 'PHP_version_8.3' => "8.3", - ]; - $project = $this->io->choice('Select a PHP version', array_values($phpVersions), "8.1"); - $project = array_search($project, $phpVersions, TRUE); - $phpVersion = $phpVersions[$project]; - break; - case "Node_project": - $nodeVersions = [ - 'NODE_version_18.17.1' => "18.17.1", - 'NODE_version_20.5.1' => "20.5.1", - ]; - $project = $this->io->choice('Select a NODE version', array_values($nodeVersions), "18.17.1"); - $project = array_search($project, $nodeVersions, TRUE); - $nodeVersion = $nodeVersions[$project]; - break; - } + $appUuid = $this->determineCloudApplication(); - $appUuid = $this->determineCloudApplication(); + // Get Cloud account. + $acquiaCloudClient = $this->cloudApiClientService->getClient(); + $accountAdapter = new Account($acquiaCloudClient); + $account = $accountAdapter->get(); + $this->validateRequiredCloudPermissions( + $acquiaCloudClient, + $appUuid, + $account, + [ + "deploy to non-prod", + // Add SSH key to git repository. + "add ssh key to git", + // Add SSH key to non-production environments. + "add ssh key to non-prod", + // Add a CD environment. + "add an environment", + // Delete a CD environment. + "delete an environment", + // Manage environment variables on a non-production environment. + "administer environment variables on non-prod", + ] + ); + $this->setGitLabProjectDescription($appUuid); - // Get Cloud account. - $acquiaCloudClient = $this->cloudApiClientService->getClient(); - $accountAdapter = new Account($acquiaCloudClient); - $account = $accountAdapter->get(); - $this->validateRequiredCloudPermissions( - $acquiaCloudClient, - $appUuid, - $account, - [ - "deploy to non-prod", - // Add SSH key to git repository. - "add ssh key to git", - // Add SSH key to non-production environments. - "add ssh key to non-prod", - // Add a CD environment. - "add an environment", - // Delete a CD environment. - "delete an environment", - // Manage environment variables on a non-production environment. - "administer environment variables on non-prod", - ] - ); - $this->setGitLabProjectDescription($appUuid); + // Get Cloud application. + $cloudApplication = $this->getCloudApplication($appUuid); + $project = $this->determineGitLabProject($cloudApplication); - // Get Cloud application. - $cloudApplication = $this->getCloudApplication($appUuid); - $project = $this->determineGitLabProject($cloudApplication); + $this->io->writeln([ + "", + "This command will configure the Code Studio project {$project['path_with_namespace']} for automatic deployment to the", + "Acquia Cloud Platform application {$cloudApplication->name} ($appUuid)", + "using credentials (API Token and SSH Key) belonging to {$account->mail}.", + "", + "If the {$account->mail} Cloud account is deleted in the future, this Code Studio project will need to be re-configured.", + ]); + $answer = $this->io->confirm('Do you want to continue?'); + if (!$answer) { + return Command::SUCCESS; + } - $this->io->writeln([ - "", - "This command will configure the Code Studio project {$project['path_with_namespace']} for automatic deployment to the", - "Acquia Cloud Platform application {$cloudApplication->name} ($appUuid)", - "using credentials (API Token and SSH Key) belonging to {$account->mail}.", - "", - "If the {$account->mail} Cloud account is deleted in the future, this Code Studio project will need to be re-configured.", - ]); - $answer = $this->io->confirm('Do you want to continue?'); - if (!$answer) { - return Command::SUCCESS; - } + $projectAccessTokenName = 'acquia-codestudio'; + $projectAccessToken = $this->createProjectAccessToken($project, $projectAccessTokenName); + $this->updateGitLabProject($project); + switch ($projectSelected) { + case "Drupal_project": + $this->setGitLabCiCdVariablesForPhpProject($project, $appUuid, $cloudKey, $cloudSecret, $projectAccessTokenName, $projectAccessToken, $phpVersion); + $this->createScheduledPipeline($project); + break; + case "Node_project": + $parameters = [ + 'ci_config_path' => 'gitlab-ci/Auto-DevOps.acquia.gitlab-ci.yml@acquia/node-template', + ]; + $client = $this->getGitLabClient(); + $client->projects()->update($project['id'], $parameters); + $this->setGitLabCiCdVariablesForNodeProject($project, $appUuid, $cloudKey, $cloudSecret, $projectAccessTokenName, $projectAccessToken, $nodeVersion); + break; + } - $projectAccessTokenName = 'acquia-codestudio'; - $projectAccessToken = $this->createProjectAccessToken($project, $projectAccessTokenName); - $this->updateGitLabProject($project); - switch ($projectSelected) { - case "Drupal_project": - $this->setGitLabCiCdVariablesForPhpProject($project, $appUuid, $cloudKey, $cloudSecret, $projectAccessTokenName, $projectAccessToken, $phpVersion); - $this->createScheduledPipeline($project); - break; - case "Node_project": - $parameters = [ - 'ci_config_path' => 'gitlab-ci/Auto-DevOps.acquia.gitlab-ci.yml@acquia/node-template', - ]; - $client = $this->getGitLabClient(); - $client->projects()->update($project['id'], $parameters); - $this->setGitLabCiCdVariablesForNodeProject($project, $appUuid, $cloudKey, $cloudSecret, $projectAccessTokenName, $projectAccessToken, $nodeVersion); - break; - } - - $this->io->success([ - "Successfully configured the Code Studio project!", - "This project will now use Acquia's Drupal optimized AutoDevOps to build, test, and deploy your code automatically to Acquia Cloud Platform via CI/CD pipelines.", - "You can visit it here:", - $project['web_url'], - "", - "Next, you should use git to push code to your Code Studio project. E.g.,", - " git remote add codestudio {$project['http_url_to_repo']}", - " git push codestudio", - ]); - $this->io->note(["If the {$account->mail} Cloud account is deleted in the future, this Code Studio project will need to be re-configured."]); + $this->io->success([ + "Successfully configured the Code Studio project!", + "This project will now use Acquia's Drupal optimized AutoDevOps to build, test, and deploy your code automatically to Acquia Cloud Platform via CI/CD pipelines.", + "You can visit it here:", + $project['web_url'], + "", + "Next, you should use git to push code to your Code Studio project. E.g.,", + " git remote add codestudio {$project['http_url_to_repo']}", + " git push codestudio", + ]); + $this->io->note(["If the {$account->mail} Cloud account is deleted in the future, this Code Studio project will need to be re-configured."]); - return Command::SUCCESS; - } + return Command::SUCCESS; + } - /** - * @return array|null - */ - private function getGitLabScheduleByDescription(array $project, string $scheduledPipelineDescription): ?array { - $existingSchedules = $this->gitLabClient->schedules()->showAll($project['id']); - foreach ($existingSchedules as $schedule) { - if ($schedule['description'] == $scheduledPipelineDescription) { - return $schedule; - } + /** + * @return array|null + */ + private function getGitLabScheduleByDescription(array $project, string $scheduledPipelineDescription): ?array + { + $existingSchedules = $this->gitLabClient->schedules()->showAll($project['id']); + foreach ($existingSchedules as $schedule) { + if ($schedule['description'] == $scheduledPipelineDescription) { + return $schedule; + } + } + return null; } - return NULL; - } - /** - * @return array|null ? - */ - private function getGitLabProjectAccessTokenByName(array $project, string $name): ?array { - $existingProjectAccessTokens = $this->gitLabClient->projects()->projectAccessTokens($project['id']); - foreach ($existingProjectAccessTokens as $key => $token) { - if ($token['name'] == $name) { - return $token; - } + /** + * @return array|null ? + */ + private function getGitLabProjectAccessTokenByName(array $project, string $name): ?array + { + $existingProjectAccessTokens = $this->gitLabClient->projects()->projectAccessTokens($project['id']); + foreach ($existingProjectAccessTokens as $key => $token) { + if ($token['name'] == $name) { + return $token; + } + } + return null; } - return NULL; - } - /** - * @return array|null ? - */ - private function getListOfProjectType(): ?array { - $array = [ - 'Drupal_project', - 'Node_project', - ]; - return $array; - } + /** + * @return array|null ? + */ + private function getListOfProjectType(): ?array + { + $array = [ + 'Drupal_project', + 'Node_project', + ]; + return $array; + } - private function createProjectAccessToken(array $project, string $projectAccessTokenName): string { - $this->io->writeln("Creating project access token..."); + private function createProjectAccessToken(array $project, string $projectAccessTokenName): string + { + $this->io->writeln("Creating project access token..."); - if ($existingToken = $this->getGitLabProjectAccessTokenByName($project, $projectAccessTokenName)) { - $this->checklist->addItem("Deleting access token named $projectAccessTokenName"); - $this->gitLabClient->projects() + if ($existingToken = $this->getGitLabProjectAccessTokenByName($project, $projectAccessTokenName)) { + $this->checklist->addItem("Deleting access token named $projectAccessTokenName"); + $this->gitLabClient->projects() ->deleteProjectAccessToken($project['id'], $existingToken['id']); - $this->checklist->completePreviousItem(); - } - $this->checklist->addItem("Creating access token named $projectAccessTokenName"); - $projectAccessToken = $this->gitLabClient->projects() + $this->checklist->completePreviousItem(); + } + $this->checklist->addItem("Creating access token named $projectAccessTokenName"); + $projectAccessToken = $this->gitLabClient->projects() ->createProjectAccessToken($project['id'], [ - 'expires_at' => new DateTime('+365 days'), - 'name' => $projectAccessTokenName, - 'scopes' => ['api', 'write_repository'], + 'expires_at' => new DateTime('+365 days'), + 'name' => $projectAccessTokenName, + 'scopes' => ['api', 'write_repository'], ]); - $this->checklist->completePreviousItem(); - return $projectAccessToken['token']; - } - - private function setGitLabCiCdVariablesForPhpProject(array $project, string $cloudApplicationUuid, string $cloudKey, string $cloudSecret, string $projectAccessTokenName, string $projectAccessToken, string $phpVersion): void { - $this->io->writeln("Setting GitLab CI/CD variables for {$project['path_with_namespace']}.."); - $gitlabCicdVariables = CodeStudioCiCdVariables::getDefaultsForPhp($cloudApplicationUuid, $cloudKey, $cloudSecret, $projectAccessTokenName, $projectAccessToken, $phpVersion); - $gitlabCicdExistingVariables = $this->gitLabClient->projects() - ->variables($project['id']); - $gitlabCicdExistingVariablesKeyed = []; - foreach ($gitlabCicdExistingVariables as $variable) { - $key = $variable['key']; - $gitlabCicdExistingVariablesKeyed[$key] = $variable; + $this->checklist->completePreviousItem(); + return $projectAccessToken['token']; } - foreach ($gitlabCicdVariables as $variable) { - $this->checklist->addItem("Setting GitLab CI/CD variables for {$variable['key']}"); - if (!array_key_exists($variable['key'], $gitlabCicdExistingVariablesKeyed)) { - $this->gitLabClient->projects() - ->addVariable($project['id'], $variable['key'], $variable['value'], $variable['protected'], NULL, ['masked' => $variable['masked'], 'variable_type' => $variable['variable_type']]); - } - else { - $this->gitLabClient->projects() - ->updateVariable($project['id'], $variable['key'], $variable['value'], $variable['protected'], NULL, ['masked' => $variable['masked'], 'variable_type' => $variable['variable_type']]); - } - $this->checklist->completePreviousItem(); - } - } + private function setGitLabCiCdVariablesForPhpProject(array $project, string $cloudApplicationUuid, string $cloudKey, string $cloudSecret, string $projectAccessTokenName, string $projectAccessToken, string $phpVersion): void + { + $this->io->writeln("Setting GitLab CI/CD variables for {$project['path_with_namespace']}.."); + $gitlabCicdVariables = CodeStudioCiCdVariables::getDefaultsForPhp($cloudApplicationUuid, $cloudKey, $cloudSecret, $projectAccessTokenName, $projectAccessToken, $phpVersion); + $gitlabCicdExistingVariables = $this->gitLabClient->projects() + ->variables($project['id']); + $gitlabCicdExistingVariablesKeyed = []; + foreach ($gitlabCicdExistingVariables as $variable) { + $key = $variable['key']; + $gitlabCicdExistingVariablesKeyed[$key] = $variable; + } - private function setGitLabCiCdVariablesForNodeProject(array $project, string $cloudApplicationUuid, string $cloudKey, string $cloudSecret, string $projectAccessTokenName, string $projectAccessToken, string $nodeVersion): void { - $this->io->writeln("Setting GitLab CI/CD variables for {$project['path_with_namespace']}.."); - $gitlabCicdVariables = CodeStudioCiCdVariables::getDefaultsForNode($cloudApplicationUuid, $cloudKey, $cloudSecret, $projectAccessTokenName, $projectAccessToken, $nodeVersion); - $gitlabCicdExistingVariables = $this->gitLabClient->projects() - ->variables($project['id']); - $gitlabCicdExistingVariablesKeyed = []; - foreach ($gitlabCicdExistingVariables as $variable) { - $key = $variable['key']; - $gitlabCicdExistingVariablesKeyed[$key] = $variable; + foreach ($gitlabCicdVariables as $variable) { + $this->checklist->addItem("Setting GitLab CI/CD variables for {$variable['key']}"); + if (!array_key_exists($variable['key'], $gitlabCicdExistingVariablesKeyed)) { + $this->gitLabClient->projects() + ->addVariable($project['id'], $variable['key'], $variable['value'], $variable['protected'], null, ['masked' => $variable['masked'], 'variable_type' => $variable['variable_type']]); + } else { + $this->gitLabClient->projects() + ->updateVariable($project['id'], $variable['key'], $variable['value'], $variable['protected'], null, ['masked' => $variable['masked'], 'variable_type' => $variable['variable_type']]); + } + $this->checklist->completePreviousItem(); + } } - foreach ($gitlabCicdVariables as $variable) { - $this->checklist->addItem("Setting CI/CD variable {$variable['key']}"); - if (!array_key_exists($variable['key'], $gitlabCicdExistingVariablesKeyed)) { - $this->gitLabClient->projects() - ->addVariable($project['id'], $variable['key'], $variable['value'], $variable['protected'], NULL, ['masked' => $variable['masked'], 'variable_type' => $variable['variable_type']]); - } - else { - $this->gitLabClient->projects() - ->updateVariable($project['id'], $variable['key'], $variable['value'], $variable['protected'], NULL, ['masked' => $variable['masked'], 'variable_type' => $variable['variable_type']]); - } - $this->checklist->completePreviousItem(); + private function setGitLabCiCdVariablesForNodeProject(array $project, string $cloudApplicationUuid, string $cloudKey, string $cloudSecret, string $projectAccessTokenName, string $projectAccessToken, string $nodeVersion): void + { + $this->io->writeln("Setting GitLab CI/CD variables for {$project['path_with_namespace']}.."); + $gitlabCicdVariables = CodeStudioCiCdVariables::getDefaultsForNode($cloudApplicationUuid, $cloudKey, $cloudSecret, $projectAccessTokenName, $projectAccessToken, $nodeVersion); + $gitlabCicdExistingVariables = $this->gitLabClient->projects() + ->variables($project['id']); + $gitlabCicdExistingVariablesKeyed = []; + foreach ($gitlabCicdExistingVariables as $variable) { + $key = $variable['key']; + $gitlabCicdExistingVariablesKeyed[$key] = $variable; + } + + foreach ($gitlabCicdVariables as $variable) { + $this->checklist->addItem("Setting CI/CD variable {$variable['key']}"); + if (!array_key_exists($variable['key'], $gitlabCicdExistingVariablesKeyed)) { + $this->gitLabClient->projects() + ->addVariable($project['id'], $variable['key'], $variable['value'], $variable['protected'], null, ['masked' => $variable['masked'], 'variable_type' => $variable['variable_type']]); + } else { + $this->gitLabClient->projects() + ->updateVariable($project['id'], $variable['key'], $variable['value'], $variable['protected'], null, ['masked' => $variable['masked'], 'variable_type' => $variable['variable_type']]); + } + $this->checklist->completePreviousItem(); + } } - } - private function createScheduledPipeline(array $project): void { - $this->io->writeln("Creating scheduled pipeline"); - $scheduledPipelineDescription = "Code Studio Automatic Updates"; + private function createScheduledPipeline(array $project): void + { + $this->io->writeln("Creating scheduled pipeline"); + $scheduledPipelineDescription = "Code Studio Automatic Updates"; - if (!$this->getGitLabScheduleByDescription($project, $scheduledPipelineDescription)) { - $this->checklist->addItem("Creating scheduled pipeline $scheduledPipelineDescription"); - $pipeline = $this->gitLabClient->schedules()->create($project['id'], [ - // Every Thursday at midnight. - 'cron' => '0 0 * * 4', - 'description' => $scheduledPipelineDescription, - 'ref' => $project['default_branch'], - ]); - $this->gitLabClient->schedules()->addVariable($project['id'], $pipeline['id'], [ - 'key' => 'ACQUIA_JOBS_DEPRECATED_UPDATE', - 'value' => 'true', - ]); - $this->gitLabClient->schedules()->addVariable($project['id'], $pipeline['id'], [ - 'key' => 'ACQUIA_JOBS_COMPOSER_UPDATE', - 'value' => 'true', - ]); + if (!$this->getGitLabScheduleByDescription($project, $scheduledPipelineDescription)) { + $this->checklist->addItem("Creating scheduled pipeline $scheduledPipelineDescription"); + $pipeline = $this->gitLabClient->schedules()->create($project['id'], [ + // Every Thursday at midnight. + 'cron' => '0 0 * * 4', + 'description' => $scheduledPipelineDescription, + 'ref' => $project['default_branch'], + ]); + $this->gitLabClient->schedules()->addVariable($project['id'], $pipeline['id'], [ + 'key' => 'ACQUIA_JOBS_DEPRECATED_UPDATE', + 'value' => 'true', + ]); + $this->gitLabClient->schedules()->addVariable($project['id'], $pipeline['id'], [ + 'key' => 'ACQUIA_JOBS_COMPOSER_UPDATE', + 'value' => 'true', + ]); + } else { + $this->checklist->addItem("Scheduled pipeline named $scheduledPipelineDescription already exists"); + } + $this->checklist->completePreviousItem(); } - else { - $this->checklist->addItem("Scheduled pipeline named $scheduledPipelineDescription already exists"); - } - $this->checklist->completePreviousItem(); - } - private function updateGitLabProject(array $project): void { - // Setting the description to match the known pattern will allow us to automatically find the project next time. - if ($project['description'] !== $this->gitLabProjectDescription) { - $this->gitLabClient->projects()->update($project['id'], $this->getGitLabProjectDefaults()); - try { - $this->gitLabClient->projects()->uploadAvatar($project['id'], __DIR__ . '/drupal_icon.png'); - } - catch (ValidationFailedException) { - $this->io->warning("Failed to upload project avatar"); - } + private function updateGitLabProject(array $project): void + { + // Setting the description to match the known pattern will allow us to automatically find the project next time. + if ($project['description'] !== $this->gitLabProjectDescription) { + $this->gitLabClient->projects()->update($project['id'], $this->getGitLabProjectDefaults()); + try { + $this->gitLabClient->projects()->uploadAvatar($project['id'], __DIR__ . '/drupal_icon.png'); + } catch (ValidationFailedException) { + $this->io->warning("Failed to upload project avatar"); + } + } } - } - } diff --git a/src/Command/CommandBase.php b/src/Command/CommandBase.php index 1d07953d4..51b7b219d 100644 --- a/src/Command/CommandBase.php +++ b/src/Command/CommandBase.php @@ -1,6 +1,6 @@ logger = $logger; - $this->setLocalDbPassword(); - $this->setLocalDbUser(); - $this->setLocalDbName(); - $this->setLocalDbHost(); - parent::__construct(); - if ((new \ReflectionClass(static::class))->getAttributes(RequireAuth::class)) { - $this->appendHelp('This command requires authentication via the Cloud Platform API.'); - } - if ((new \ReflectionClass(static::class))->getAttributes(RequireDb::class)) { - $this->appendHelp('This command requires an active database connection. Set the following environment variables prior to running this command: ' - . 'ACLI_DB_HOST, ACLI_DB_NAME, ACLI_DB_USER, ACLI_DB_PASSWORD'); - } - } - - public function appendHelp(string $helpText): void { - $currentHelp = $this->getHelp(); - $helpText = $currentHelp ? $currentHelp . "\n" . $helpText : $currentHelp . $helpText; - $this->setHelp($helpText); - } - - protected static function getUuidRegexConstraint(): Regex { - return new Regex([ - 'message' => 'This is not a valid UUID.', - 'pattern' => '/^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i', - ]); - } - - public function setProjectDir(string $projectDir): void { - $this->projectDir = $projectDir; - } - - public function getProjectDir(): string { - return $this->projectDir; - } - - private function setLocalDbUser(): void { - if (getenv('ACLI_DB_USER')) { - $this->localDbUser = getenv('ACLI_DB_USER'); - } - } - - public function getLocalDbUser(): string { - return $this->localDbUser; - } - - private function setLocalDbPassword(): void { - if (getenv('ACLI_DB_PASSWORD')) { - $this->localDbPassword = getenv('ACLI_DB_PASSWORD'); - } - } - - public function getLocalDbPassword(): string { - return $this->localDbPassword; - } - - private function setLocalDbName(): void { - if (getenv('ACLI_DB_NAME')) { - $this->localDbName = getenv('ACLI_DB_NAME'); - } - } - - public function getLocalDbName(): string { - return $this->localDbName; - } - - private function setLocalDbHost(): void { - if (getenv('ACLI_DB_HOST')) { - $this->localDbHost = getenv('ACLI_DB_HOST'); - } - } - - public function getLocalDbHost(): string { - return $this->localDbHost; - } - - /** - * Initializes the command just after the input has been validated. - */ - protected function initialize(InputInterface $input, OutputInterface $output): void { - $this->input = $input; - $this->output = $output; - $this->io = new SymfonyStyle($input, $output); - // Register custom progress bar format. - ProgressBar::setFormatDefinition( - 'message', - "%current%/%max% [%bar%] %percent:3s%% -- %elapsed:6s%/%estimated:-6s%\n %message%" - ); - $this->formatter = $this->getHelper('formatter'); - - $this->output->writeln('Acquia CLI version: ' . $this->getApplication()->getVersion(), OutputInterface::VERBOSITY_DEBUG); - if (getenv('ACLI_NO_TELEMETRY') !== 'true') { - $this->checkAndPromptTelemetryPreference(); - $this->telemetryHelper->initialize(); - } - $this->checkAuthentication(); - - $this->fillMissingRequiredApplicationUuid($input, $output); - $this->convertApplicationAliasToUuid($input); - $this->convertUserAliasToUuid($input, 'userUuid', 'organizationUuid'); - $this->convertEnvironmentAliasToUuid($input, 'environmentId'); - $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."); - } - } - - /** - * Check if telemetry preference is set, prompt if not. - */ - public function checkAndPromptTelemetryPreference(): void { - $sendTelemetry = $this->datastoreCloud->get(DataStoreContract::SEND_TELEMETRY); - if ($this->getName() !== 'telemetry' && (!isset($sendTelemetry)) && $this->input->isInteractive()) { - $this->output->writeln('We strive to give you the best tools for development.'); - $this->output->writeln('You can really help us improve by sharing anonymous performance and usage data.'); - $style = new SymfonyStyle($this->input, $this->output); - $pref = $style->confirm('Would you like to share anonymous performance usage and data?'); - $this->datastoreCloud->set(DataStoreContract::SEND_TELEMETRY, $pref); - if ($pref) { - $this->output->writeln('Awesome! Thank you for helping!'); - } - else { - // @todo Completely anonymously send an event to indicate some user opted out. - $this->output->writeln('Ok, no data will be collected and shared with us.'); - $this->output->writeln('We take privacy seriously.'); - $this->output->writeln('If you change your mind, run acli telemetry.'); - } - } - } - - public function run(InputInterface $input, OutputInterface $output): int { - $exitCode = parent::run($input, $output); - if ($exitCode === 0 && in_array($input->getFirstArgument(), ['self-update', 'update'])) { - // Exit immediately to avoid loading additional classes breaking updates. - // @see https://github.com/acquia/cli/issues/218 - return $exitCode; - } - $eventProperties = [ - 'app_version' => $this->getApplication()->getVersion(), - 'arguments' => $input->getArguments(), - 'exit_code' => $exitCode, - 'options' => $input->getOptions(), - 'os_name' => OsInfo::os(), - 'os_version' => OsInfo::version(), - 'platform' => OsInfo::family(), - ]; - Amplitude::getInstance()->queueEvent('Ran command', $eventProperties); - - return $exitCode; - } - - /** - * Add argument and usage examples for applicationUuid. - */ - protected function acceptApplicationUuid(): static { - $this->addArgument('applicationUuid', InputArgument::OPTIONAL, 'The Cloud Platform application UUID or alias (i.e. an application name optionally prefixed with the realm)') - ->addUsage('[]') - ->addUsage('myapp') - ->addUsage('prod:myapp') - ->addUsage('abcd1234-1111-2222-3333-0e02b2c3d470'); - - return $this; - } - - /** - * Add argument and usage examples for environmentId. - */ - protected function acceptEnvironmentId(): static { - $this->addArgument('environmentId', InputArgument::OPTIONAL, 'The Cloud Platform environment ID or alias (i.e. an application and environment name optionally prefixed with the realm)') - ->addUsage('[]') - ->addUsage('myapp.dev') - ->addUsage('prod:myapp.dev') - ->addUsage('12345-abcd1234-1111-2222-3333-0e02b2c3d470'); - - return $this; - } - - /** - * Add site argument. - * - * Only call this after acceptEnvironmentId() to keep arguments in the expected order. - * - * @return $this - */ - protected function acceptSite(): self { - // Do not set a default site in order to force a user prompt. - $this->addArgument('site', InputArgument::OPTIONAL, 'For a multisite application, the directory name of the site') - ->addUsage('myapp.dev default'); - - return $this; - } - - /** - * Prompts the user to choose from a list of available Cloud Platform - * applications. - */ - private function promptChooseSubscription( - Client $acquiaCloudClient - ): ?SubscriptionResponse { - $subscriptionsResource = new Subscriptions($acquiaCloudClient); - $customerSubscriptions = $subscriptionsResource->getAll(); - - if (!$customerSubscriptions->count()) { - throw new AcquiaCliException("You have no Cloud subscriptions."); - } - return $this->promptChooseFromObjectsOrArrays( - $customerSubscriptions, - 'uuid', - 'name', - 'Select a Cloud Platform subscription:' - ); - } - - /** - * Prompts the user to choose from a list of available Cloud Platform - * applications. - */ - private function promptChooseApplication( - Client $acquiaCloudClient - ): object|array|null { - $applicationsResource = new Applications($acquiaCloudClient); - $customerApplications = $applicationsResource->getAll(); - - if (!$customerApplications->count()) { - throw new AcquiaCliException("You have no Cloud applications."); - } - return $this->promptChooseFromObjectsOrArrays( - $customerApplications, - 'uuid', - 'name', - 'Select a Cloud Platform application:' - ); - } - - /** - * Prompts the user to choose from a list of environments for a given Cloud Platform application. - */ - private function promptChooseEnvironment( - Client $acquiaCloudClient, - string $applicationUuid - ): object|array|null { - $environmentResource = new Environments($acquiaCloudClient); - $environments = $environmentResource->getAll($applicationUuid); - if (!$environments->count()) { - throw new AcquiaCliException('There are no environments associated with this application.'); - } - return $this->promptChooseFromObjectsOrArrays( - $environments, - 'uuid', - 'name', - 'Select a Cloud Platform environment:' - ); - } - - /** - * Prompts the user to choose from a list of logs for a given Cloud Platform environment. - */ - protected function promptChooseLogs(): object|array|null { - $logs = array_map(static function (mixed $logType, mixed $logLabel): array { - return [ - 'label' => $logLabel, - 'type' => $logType, - ]; - }, array_keys(LogstreamManager::AVAILABLE_TYPES), LogstreamManager::AVAILABLE_TYPES); - return $this->promptChooseFromObjectsOrArrays( - $logs, - 'type', - 'label', - 'Select one or more logs as a comma-separated list:', - TRUE - ); - } - - /** - * Prompt a user to choose from a list. - * - * The list is generated from an array of objects. The objects much have at least one unique property and one - * property that can be used as a human-readable label. - * - * @param array[]|object[] $items An array of objects or arrays. - * @param string $uniqueProperty The property of the $item that will be used to identify the object. - */ - protected function promptChooseFromObjectsOrArrays(array|ArrayObject $items, string $uniqueProperty, string $labelProperty, string $questionText, bool $multiselect = FALSE): object|array|null { - $list = []; - foreach ($items as $item) { - if (is_array($item)) { - $list[$item[$uniqueProperty]] = trim($item[$labelProperty]); - } - else { - $list[$item->$uniqueProperty] = trim($item->$labelProperty); - } - } - $labels = array_values($list); - $default = $multiselect ? 0 : $labels[0]; - $question = new ChoiceQuestion($questionText, $labels, $default); - $question->setMultiselect($multiselect); - $choiceId = $this->io->askQuestion($question); - if (!$multiselect) { - $identifier = array_search($choiceId, $list, TRUE); - foreach ($items as $item) { - if (is_array($item)) { - if ($item[$uniqueProperty] === $identifier) { - return $item; - } - } - else if ($item->$uniqueProperty === $identifier) { - return $item; - } - } - } - else { - $chosen = []; - foreach ($choiceId as $choice) { - $identifier = array_search($choice, $list, TRUE); +abstract class CommandBase extends Command implements LoggerAwareInterface +{ + use LoggerAwareTrait; + + protected InputInterface $input; + + protected OutputInterface $output; + + protected SymfonyStyle $io; + protected FormatterHelper $formatter; + private ApplicationResponse $cloudApplication; + + protected string $dir; + protected string $localDbUser = 'drupal'; + protected string $localDbPassword = 'drupal'; + protected string $localDbName = 'drupal'; + protected string $localDbHost = 'localhost'; + protected bool $drushHasActiveDatabaseConnection; + protected \GuzzleHttp\Client $updateClient; + + public function __construct( + public LocalMachineHelper $localMachineHelper, + protected CloudDataStore $datastoreCloud, + protected AcquiaCliDatastore $datastoreAcli, + protected ApiCredentialsInterface $cloudCredentials, + protected TelemetryHelper $telemetryHelper, + protected string $projectDir, + protected ClientService $cloudApiClientService, + public SshHelper $sshHelper, + protected string $sshDir, + LoggerInterface $logger, + ) { + $this->logger = $logger; + $this->setLocalDbPassword(); + $this->setLocalDbUser(); + $this->setLocalDbName(); + $this->setLocalDbHost(); + parent::__construct(); + if ((new \ReflectionClass(static::class))->getAttributes(RequireAuth::class)) { + $this->appendHelp('This command requires authentication via the Cloud Platform API.'); + } + if ((new \ReflectionClass(static::class))->getAttributes(RequireDb::class)) { + $this->appendHelp('This command requires an active database connection. Set the following environment variables prior to running this command: ' + . 'ACLI_DB_HOST, ACLI_DB_NAME, ACLI_DB_USER, ACLI_DB_PASSWORD'); + } + } + + public function appendHelp(string $helpText): void + { + $currentHelp = $this->getHelp(); + $helpText = $currentHelp ? $currentHelp . "\n" . $helpText : $currentHelp . $helpText; + $this->setHelp($helpText); + } + + protected static function getUuidRegexConstraint(): Regex + { + return new Regex([ + 'message' => 'This is not a valid UUID.', + 'pattern' => '/^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i', + ]); + } + + public function setProjectDir(string $projectDir): void + { + $this->projectDir = $projectDir; + } + + public function getProjectDir(): string + { + return $this->projectDir; + } + + private function setLocalDbUser(): void + { + if (getenv('ACLI_DB_USER')) { + $this->localDbUser = getenv('ACLI_DB_USER'); + } + } + + public function getLocalDbUser(): string + { + return $this->localDbUser; + } + + private function setLocalDbPassword(): void + { + if (getenv('ACLI_DB_PASSWORD')) { + $this->localDbPassword = getenv('ACLI_DB_PASSWORD'); + } + } + + public function getLocalDbPassword(): string + { + return $this->localDbPassword; + } + + private function setLocalDbName(): void + { + if (getenv('ACLI_DB_NAME')) { + $this->localDbName = getenv('ACLI_DB_NAME'); + } + } + + public function getLocalDbName(): string + { + return $this->localDbName; + } + + private function setLocalDbHost(): void + { + if (getenv('ACLI_DB_HOST')) { + $this->localDbHost = getenv('ACLI_DB_HOST'); + } + } + + public function getLocalDbHost(): string + { + return $this->localDbHost; + } + + /** + * Initializes the command just after the input has been validated. + */ + protected function initialize(InputInterface $input, OutputInterface $output): void + { + $this->input = $input; + $this->output = $output; + $this->io = new SymfonyStyle($input, $output); + // Register custom progress bar format. + ProgressBar::setFormatDefinition( + 'message', + "%current%/%max% [%bar%] %percent:3s%% -- %elapsed:6s%/%estimated:-6s%\n %message%" + ); + $this->formatter = $this->getHelper('formatter'); + + $this->output->writeln('Acquia CLI version: ' . $this->getApplication()->getVersion(), OutputInterface::VERBOSITY_DEBUG); + if (getenv('ACLI_NO_TELEMETRY') !== 'true') { + $this->checkAndPromptTelemetryPreference(); + $this->telemetryHelper->initialize(); + } + $this->checkAuthentication(); + + $this->fillMissingRequiredApplicationUuid($input, $output); + $this->convertApplicationAliasToUuid($input); + $this->convertUserAliasToUuid($input, 'userUuid', 'organizationUuid'); + $this->convertEnvironmentAliasToUuid($input, 'environmentId'); + $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."); + } + } + + /** + * Check if telemetry preference is set, prompt if not. + */ + public function checkAndPromptTelemetryPreference(): void + { + $sendTelemetry = $this->datastoreCloud->get(DataStoreContract::SEND_TELEMETRY); + if ($this->getName() !== 'telemetry' && (!isset($sendTelemetry)) && $this->input->isInteractive()) { + $this->output->writeln('We strive to give you the best tools for development.'); + $this->output->writeln('You can really help us improve by sharing anonymous performance and usage data.'); + $style = new SymfonyStyle($this->input, $this->output); + $pref = $style->confirm('Would you like to share anonymous performance usage and data?'); + $this->datastoreCloud->set(DataStoreContract::SEND_TELEMETRY, $pref); + if ($pref) { + $this->output->writeln('Awesome! Thank you for helping!'); + } else { + // @todo Completely anonymously send an event to indicate some user opted out. + $this->output->writeln('Ok, no data will be collected and shared with us.'); + $this->output->writeln('We take privacy seriously.'); + $this->output->writeln('If you change your mind, run acli telemetry.'); + } + } + } + + public function run(InputInterface $input, OutputInterface $output): int + { + $exitCode = parent::run($input, $output); + if ($exitCode === 0 && in_array($input->getFirstArgument(), ['self-update', 'update'])) { + // Exit immediately to avoid loading additional classes breaking updates. + // @see https://github.com/acquia/cli/issues/218 + return $exitCode; + } + $eventProperties = [ + 'app_version' => $this->getApplication()->getVersion(), + 'arguments' => $input->getArguments(), + 'exit_code' => $exitCode, + 'options' => $input->getOptions(), + 'os_name' => OsInfo::os(), + 'os_version' => OsInfo::version(), + 'platform' => OsInfo::family(), + ]; + Amplitude::getInstance()->queueEvent('Ran command', $eventProperties); + + return $exitCode; + } + + /** + * Add argument and usage examples for applicationUuid. + */ + protected function acceptApplicationUuid(): static + { + $this->addArgument('applicationUuid', InputArgument::OPTIONAL, 'The Cloud Platform application UUID or alias (i.e. an application name optionally prefixed with the realm)') + ->addUsage('[]') + ->addUsage('myapp') + ->addUsage('prod:myapp') + ->addUsage('abcd1234-1111-2222-3333-0e02b2c3d470'); + + return $this; + } + + /** + * Add argument and usage examples for environmentId. + */ + protected function acceptEnvironmentId(): static + { + $this->addArgument('environmentId', InputArgument::OPTIONAL, 'The Cloud Platform environment ID or alias (i.e. an application and environment name optionally prefixed with the realm)') + ->addUsage('[]') + ->addUsage('myapp.dev') + ->addUsage('prod:myapp.dev') + ->addUsage('12345-abcd1234-1111-2222-3333-0e02b2c3d470'); + + return $this; + } + + /** + * Add site argument. + * + * Only call this after acceptEnvironmentId() to keep arguments in the expected order. + * + * @return $this + */ + protected function acceptSite(): self + { + // Do not set a default site in order to force a user prompt. + $this->addArgument('site', InputArgument::OPTIONAL, 'For a multisite application, the directory name of the site') + ->addUsage('myapp.dev default'); + + return $this; + } + + /** + * Prompts the user to choose from a list of available Cloud Platform + * applications. + */ + private function promptChooseSubscription( + Client $acquiaCloudClient + ): ?SubscriptionResponse { + $subscriptionsResource = new Subscriptions($acquiaCloudClient); + $customerSubscriptions = $subscriptionsResource->getAll(); + + if (!$customerSubscriptions->count()) { + throw new AcquiaCliException("You have no Cloud subscriptions."); + } + return $this->promptChooseFromObjectsOrArrays( + $customerSubscriptions, + 'uuid', + 'name', + 'Select a Cloud Platform subscription:' + ); + } + + /** + * Prompts the user to choose from a list of available Cloud Platform + * applications. + */ + private function promptChooseApplication( + Client $acquiaCloudClient + ): object|array|null { + $applicationsResource = new Applications($acquiaCloudClient); + $customerApplications = $applicationsResource->getAll(); + + if (!$customerApplications->count()) { + throw new AcquiaCliException("You have no Cloud applications."); + } + return $this->promptChooseFromObjectsOrArrays( + $customerApplications, + 'uuid', + 'name', + 'Select a Cloud Platform application:' + ); + } + + /** + * Prompts the user to choose from a list of environments for a given Cloud Platform application. + */ + private function promptChooseEnvironment( + Client $acquiaCloudClient, + string $applicationUuid + ): object|array|null { + $environmentResource = new Environments($acquiaCloudClient); + $environments = $environmentResource->getAll($applicationUuid); + if (!$environments->count()) { + throw new AcquiaCliException('There are no environments associated with this application.'); + } + return $this->promptChooseFromObjectsOrArrays( + $environments, + 'uuid', + 'name', + 'Select a Cloud Platform environment:' + ); + } + + /** + * Prompts the user to choose from a list of logs for a given Cloud Platform environment. + */ + protected function promptChooseLogs(): object|array|null + { + $logs = array_map(static function (mixed $logType, mixed $logLabel): array { + return [ + 'label' => $logLabel, + 'type' => $logType, + ]; + }, array_keys(LogstreamManager::AVAILABLE_TYPES), LogstreamManager::AVAILABLE_TYPES); + return $this->promptChooseFromObjectsOrArrays( + $logs, + 'type', + 'label', + 'Select one or more logs as a comma-separated list:', + true + ); + } + + /** + * Prompt a user to choose from a list. + * + * The list is generated from an array of objects. The objects much have at least one unique property and one + * property that can be used as a human-readable label. + * + * @param array[]|object[] $items An array of objects or arrays. + * @param string $uniqueProperty The property of the $item that will be used to identify the object. + */ + protected function promptChooseFromObjectsOrArrays(array|ArrayObject $items, string $uniqueProperty, string $labelProperty, string $questionText, bool $multiselect = false): object|array|null + { + $list = []; foreach ($items as $item) { - if (is_array($item)) { - if ($item[$uniqueProperty] === $identifier) { - $chosen[] = $item; + if (is_array($item)) { + $list[$item[$uniqueProperty]] = trim($item[$labelProperty]); + } else { + $list[$item->$uniqueProperty] = trim($item->$labelProperty); } - } - else if ($item->$uniqueProperty === $identifier) { - $chosen[] = $item; - } - } - } - return $chosen; - } - - return NULL; - } - - protected function getHostFromDatabaseResponse(mixed $environment, DatabaseResponse $database): string { - if ($this->isAcsfEnv($environment)) { - return $database->db_host . '.enterprise-g1.hosting.acquia.com'; - } - - return $database->db_host; - } - - protected function rsyncFiles(string $sourceDir, string $destinationDir, ?callable $outputCallback): void { - $this->localMachineHelper->checkRequiredBinariesExist(['rsync']); - $command = [ - 'rsync', - // -a archive mode; same as -rlptgoD. - // -z compress file data during the transfer. - // -v increase verbosity. - // -P show progress during transfer. - // -h output numbers in a human-readable format. - // -e specify the remote shell to use. - '-avPhze', - 'ssh -o StrictHostKeyChecking=no', - $sourceDir . '/', - $destinationDir, - ]; - $process = $this->localMachineHelper->execute($command, $outputCallback, NULL, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL)); - if (!$process->isSuccessful()) { - throw new AcquiaCliException('Unable to sync files. {message}', ['message' => $process->getErrorOutput()]); - } - } - - protected function getCloudFilesDir(EnvironmentResponse $chosenEnvironment, string $site): string { - $sitegroup = self::getSitegroup($chosenEnvironment); - $envAlias = self::getEnvironmentAlias($chosenEnvironment); - if ($this->isAcsfEnv($chosenEnvironment)) { - return "/mnt/files/$envAlias/sites/g/files/$site/files"; - } - return $this->getCloudSitesPath($chosenEnvironment, $sitegroup) . "/$site/files"; - } - - protected function getLocalFilesDir(string $site): string { - return $this->dir . '/docroot/sites/' . $site . '/files'; - } - - /** - * @param string|null $site - * @return DatabaseResponse[] - */ - protected function determineCloudDatabases(Client $acquiaCloudClient, EnvironmentResponse $chosenEnvironment, string $site = NULL, bool $multipleDbs = FALSE): array { - $databasesRequest = new Databases($acquiaCloudClient); - $databases = $databasesRequest->getAll($chosenEnvironment->uuid); - - if (count($databases) === 1) { - $this->logger->debug('Only a single database detected on Cloud'); - return [$databases[0]]; - } - $this->logger->debug('Multiple databases detected on Cloud'); - if ($site && !$multipleDbs) { - if ($site === 'default') { - $this->logger->debug('Site is set to default. Assuming default database'); - $site = self::getSitegroup($chosenEnvironment); - } - $databaseNames = array_column((array) $databases, 'name'); - $databaseKey = array_search($site, $databaseNames, TRUE); - if ($databaseKey !== FALSE) { - return [$databases[$databaseKey]]; - } - } - return $this->promptChooseDatabases($chosenEnvironment, $databases, $multipleDbs); - } - - /** - * @return array - */ - private function promptChooseDatabases( - EnvironmentResponse $cloudEnvironment, - DatabasesResponse $environmentDatabases, - bool $multipleDbs - ): array { - $choices = []; - if ($multipleDbs) { - $choices['all'] = 'All'; - } - $defaultDatabaseIndex = 0; - if ($this->isAcsfEnv($cloudEnvironment)) { - $acsfSites = $this->getAcsfSites($cloudEnvironment); - } - foreach ($environmentDatabases as $index => $database) { - $suffix = ''; - if (isset($acsfSites)) { - foreach ($acsfSites['sites'] as $domain => $acsfSite) { - if ($acsfSite['conf']['gardens_db_name'] === $database->name) { - $suffix .= ' (' . $domain . ')'; - break; - } - } - } - if ($database->flags->default) { - $defaultDatabaseIndex = $index; - $suffix .= ' (default)'; - } - $choices[] = $database->name . $suffix; - } - - $question = new ChoiceQuestion( - $multipleDbs ? 'Choose databases. You may choose multiple. Use commas to separate choices.' : 'Choose a database.', - $choices, - $defaultDatabaseIndex - ); - $question->setMultiselect($multipleDbs); - if ($multipleDbs) { - $chosenDatabaseKeys = $this->io->askQuestion($question); - $chosenDatabases = []; - if (count($chosenDatabaseKeys) === 1 && $chosenDatabaseKeys[0] === 'all') { - if (count($environmentDatabases) > 10) { - $this->io->warning('You have chosen to pull down more than 10 databases. This could exhaust your disk space.'); - } - return (array) $environmentDatabases; - } - foreach ($chosenDatabaseKeys as $chosenDatabaseKey) { - $chosenDatabases[] = $environmentDatabases[$chosenDatabaseKey]; - } - - return $chosenDatabases; - } - - $chosenDatabaseLabel = $this->io->choice('Choose a database', $choices, $defaultDatabaseIndex); - $chosenDatabaseIndex = array_search($chosenDatabaseLabel, $choices, TRUE); - return [$environmentDatabases[$chosenDatabaseIndex]]; - } - - protected function determineEnvironment(InputInterface $input, OutputInterface $output, bool $allowProduction = FALSE, bool $allowNode = FALSE): array|string|EnvironmentResponse { - if ($input->getArgument('environmentId')) { - $environmentId = $input->getArgument('environmentId'); - $chosenEnvironment = $this->getCloudEnvironment($environmentId); - } - else { - $cloudApplicationUuid = $this->determineCloudApplication(); - $cloudApplication = $this->getCloudApplication($cloudApplicationUuid); - $output->writeln('Using Cloud Application ' . $cloudApplication->name . ''); - $acquiaCloudClient = $this->cloudApiClientService->getClient(); - $chosenEnvironment = $this->promptChooseEnvironmentConsiderProd($acquiaCloudClient, $cloudApplicationUuid, $allowProduction, $allowNode); - } - $this->logger->debug("Using environment $chosenEnvironment->label $chosenEnvironment->uuid"); - - return $chosenEnvironment; - } - - // Todo: obviously combine this with promptChooseEnvironment. - private function promptChooseEnvironmentConsiderProd(Client $acquiaCloudClient, string $applicationUuid, bool $allowProduction, bool $allowNode): EnvironmentResponse { - $environmentResource = new Environments($acquiaCloudClient); - $applicationEnvironments = iterator_to_array($environmentResource->getAll($applicationUuid)); - $choices = []; - foreach ($applicationEnvironments as $key => $environment) { - $productionNotAllowed = !$allowProduction && $environment->flags->production; - $nodeNotAllowed = !$allowNode && $environment->type === 'node'; - if ($productionNotAllowed || $nodeNotAllowed) { - unset($applicationEnvironments[$key]); - // Re-index array so keys match those in $choices. - $applicationEnvironments = array_values($applicationEnvironments); - continue; - } - $choices[] = "$environment->label, $environment->name (vcs: {$environment->vcs->path})"; - } - if (count($choices) === 0) { - throw new AcquiaCliException('No compatible environments found'); - } - $chosenEnvironmentLabel = $this->io->choice('Choose a Cloud Platform environment', $choices, $choices[0]); - $chosenEnvironmentIndex = array_search($chosenEnvironmentLabel, $choices, TRUE); - - return $applicationEnvironments[$chosenEnvironmentIndex]; - } - - protected function isLocalGitRepoDirty(): bool { - $this->localMachineHelper->checkRequiredBinariesExist(['git']); - $process = $this->localMachineHelper->executeFromCmd( - // Problem with this is that it stages changes for the user. They may - // not want that. - 'git add . && git diff-index --cached --quiet HEAD', - NULL, $this->dir, FALSE); - - return !$process->isSuccessful(); - } - - protected function getLocalGitCommitHash(): string { - $this->localMachineHelper->checkRequiredBinariesExist(['git']); - $process = $this->localMachineHelper->execute([ - 'git', - 'rev-parse', - 'HEAD', - ], NULL, $this->dir, FALSE); - - if (!$process->isSuccessful()) { - throw new AcquiaCliException("Unable to determine Git commit hash."); - } - - return trim($process->getOutput()); - } - - /** - * Load configuration from .git/config. - * - * @return string[][] - * A multidimensional array keyed by file section. - */ - private function getGitConfig(): array { - $filePath = $this->projectDir . '/.git/config'; - return @\Safe\parse_ini_file($filePath, TRUE); - } - - /** - * Gets an array of git remotes from a .git/config array. - * - * @param string[][] $gitConfig - * @return string[] - * A flat array of git remote urls. - */ - private function getGitRemotes(array $gitConfig): array { - $localVcsRemotes = []; - foreach ($gitConfig as $sectionName => $section) { - if ((str_contains($sectionName, 'remote ')) && - array_key_exists('url', $section) && - (strpos($section['url'], 'acquia.com') || strpos($section['url'], 'acquia-sites.com')) - ) { - $localVcsRemotes[] = $section['url']; - } - } - - return $localVcsRemotes; - } - - private function findCloudApplicationByGitUrl( + } + $labels = array_values($list); + $default = $multiselect ? 0 : $labels[0]; + $question = new ChoiceQuestion($questionText, $labels, $default); + $question->setMultiselect($multiselect); + $choiceId = $this->io->askQuestion($question); + if (!$multiselect) { + $identifier = array_search($choiceId, $list, true); + foreach ($items as $item) { + if (is_array($item)) { + if ($item[$uniqueProperty] === $identifier) { + return $item; + } + } elseif ($item->$uniqueProperty === $identifier) { + return $item; + } + } + } else { + $chosen = []; + foreach ($choiceId as $choice) { + $identifier = array_search($choice, $list, true); + foreach ($items as $item) { + if (is_array($item)) { + if ($item[$uniqueProperty] === $identifier) { + $chosen[] = $item; + } + } elseif ($item->$uniqueProperty === $identifier) { + $chosen[] = $item; + } + } + } + return $chosen; + } + + return null; + } + + protected function getHostFromDatabaseResponse(mixed $environment, DatabaseResponse $database): string + { + if ($this->isAcsfEnv($environment)) { + return $database->db_host . '.enterprise-g1.hosting.acquia.com'; + } + + return $database->db_host; + } + + protected function rsyncFiles(string $sourceDir, string $destinationDir, ?callable $outputCallback): void + { + $this->localMachineHelper->checkRequiredBinariesExist(['rsync']); + $command = [ + 'rsync', + // -a archive mode; same as -rlptgoD. + // -z compress file data during the transfer. + // -v increase verbosity. + // -P show progress during transfer. + // -h output numbers in a human-readable format. + // -e specify the remote shell to use. + '-avPhze', + 'ssh -o StrictHostKeyChecking=no', + $sourceDir . '/', + $destinationDir, + ]; + $process = $this->localMachineHelper->execute($command, $outputCallback, null, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL)); + if (!$process->isSuccessful()) { + throw new AcquiaCliException('Unable to sync files. {message}', ['message' => $process->getErrorOutput()]); + } + } + + protected function getCloudFilesDir(EnvironmentResponse $chosenEnvironment, string $site): string + { + $sitegroup = self::getSitegroup($chosenEnvironment); + $envAlias = self::getEnvironmentAlias($chosenEnvironment); + if ($this->isAcsfEnv($chosenEnvironment)) { + return "/mnt/files/$envAlias/sites/g/files/$site/files"; + } + return $this->getCloudSitesPath($chosenEnvironment, $sitegroup) . "/$site/files"; + } + + protected function getLocalFilesDir(string $site): string + { + return $this->dir . '/docroot/sites/' . $site . '/files'; + } + + /** + * @param string|null $site + * @return DatabaseResponse[] + */ + protected function determineCloudDatabases(Client $acquiaCloudClient, EnvironmentResponse $chosenEnvironment, string $site = null, bool $multipleDbs = false): array + { + $databasesRequest = new Databases($acquiaCloudClient); + $databases = $databasesRequest->getAll($chosenEnvironment->uuid); + + if (count($databases) === 1) { + $this->logger->debug('Only a single database detected on Cloud'); + return [$databases[0]]; + } + $this->logger->debug('Multiple databases detected on Cloud'); + if ($site && !$multipleDbs) { + if ($site === 'default') { + $this->logger->debug('Site is set to default. Assuming default database'); + $site = self::getSitegroup($chosenEnvironment); + } + $databaseNames = array_column((array) $databases, 'name'); + $databaseKey = array_search($site, $databaseNames, true); + if ($databaseKey !== false) { + return [$databases[$databaseKey]]; + } + } + return $this->promptChooseDatabases($chosenEnvironment, $databases, $multipleDbs); + } + + /** + * @return array + */ + private function promptChooseDatabases( + EnvironmentResponse $cloudEnvironment, + DatabasesResponse $environmentDatabases, + bool $multipleDbs + ): array { + $choices = []; + if ($multipleDbs) { + $choices['all'] = 'All'; + } + $defaultDatabaseIndex = 0; + if ($this->isAcsfEnv($cloudEnvironment)) { + $acsfSites = $this->getAcsfSites($cloudEnvironment); + } + foreach ($environmentDatabases as $index => $database) { + $suffix = ''; + if (isset($acsfSites)) { + foreach ($acsfSites['sites'] as $domain => $acsfSite) { + if ($acsfSite['conf']['gardens_db_name'] === $database->name) { + $suffix .= ' (' . $domain . ')'; + break; + } + } + } + if ($database->flags->default) { + $defaultDatabaseIndex = $index; + $suffix .= ' (default)'; + } + $choices[] = $database->name . $suffix; + } + + $question = new ChoiceQuestion( + $multipleDbs ? 'Choose databases. You may choose multiple. Use commas to separate choices.' : 'Choose a database.', + $choices, + $defaultDatabaseIndex + ); + $question->setMultiselect($multipleDbs); + if ($multipleDbs) { + $chosenDatabaseKeys = $this->io->askQuestion($question); + $chosenDatabases = []; + if (count($chosenDatabaseKeys) === 1 && $chosenDatabaseKeys[0] === 'all') { + if (count($environmentDatabases) > 10) { + $this->io->warning('You have chosen to pull down more than 10 databases. This could exhaust your disk space.'); + } + return (array) $environmentDatabases; + } + foreach ($chosenDatabaseKeys as $chosenDatabaseKey) { + $chosenDatabases[] = $environmentDatabases[$chosenDatabaseKey]; + } + + return $chosenDatabases; + } + + $chosenDatabaseLabel = $this->io->choice('Choose a database', $choices, $defaultDatabaseIndex); + $chosenDatabaseIndex = array_search($chosenDatabaseLabel, $choices, true); + return [$environmentDatabases[$chosenDatabaseIndex]]; + } + + protected function determineEnvironment(InputInterface $input, OutputInterface $output, bool $allowProduction = false, bool $allowNode = false): array|string|EnvironmentResponse + { + if ($input->getArgument('environmentId')) { + $environmentId = $input->getArgument('environmentId'); + $chosenEnvironment = $this->getCloudEnvironment($environmentId); + } else { + $cloudApplicationUuid = $this->determineCloudApplication(); + $cloudApplication = $this->getCloudApplication($cloudApplicationUuid); + $output->writeln('Using Cloud Application ' . $cloudApplication->name . ''); + $acquiaCloudClient = $this->cloudApiClientService->getClient(); + $chosenEnvironment = $this->promptChooseEnvironmentConsiderProd($acquiaCloudClient, $cloudApplicationUuid, $allowProduction, $allowNode); + } + $this->logger->debug("Using environment $chosenEnvironment->label $chosenEnvironment->uuid"); + + return $chosenEnvironment; + } + + // Todo: obviously combine this with promptChooseEnvironment. + private function promptChooseEnvironmentConsiderProd(Client $acquiaCloudClient, string $applicationUuid, bool $allowProduction, bool $allowNode): EnvironmentResponse + { + $environmentResource = new Environments($acquiaCloudClient); + $applicationEnvironments = iterator_to_array($environmentResource->getAll($applicationUuid)); + $choices = []; + foreach ($applicationEnvironments as $key => $environment) { + $productionNotAllowed = !$allowProduction && $environment->flags->production; + $nodeNotAllowed = !$allowNode && $environment->type === 'node'; + if ($productionNotAllowed || $nodeNotAllowed) { + unset($applicationEnvironments[$key]); + // Re-index array so keys match those in $choices. + $applicationEnvironments = array_values($applicationEnvironments); + continue; + } + $choices[] = "$environment->label, $environment->name (vcs: {$environment->vcs->path})"; + } + if (count($choices) === 0) { + throw new AcquiaCliException('No compatible environments found'); + } + $chosenEnvironmentLabel = $this->io->choice('Choose a Cloud Platform environment', $choices, $choices[0]); + $chosenEnvironmentIndex = array_search($chosenEnvironmentLabel, $choices, true); + + return $applicationEnvironments[$chosenEnvironmentIndex]; + } + + protected function isLocalGitRepoDirty(): bool + { + $this->localMachineHelper->checkRequiredBinariesExist(['git']); + $process = $this->localMachineHelper->executeFromCmd( + // Problem with this is that it stages changes for the user. They may + // not want that. + 'git add . && git diff-index --cached --quiet HEAD', + null, + $this->dir, + false + ); + + return !$process->isSuccessful(); + } + + protected function getLocalGitCommitHash(): string + { + $this->localMachineHelper->checkRequiredBinariesExist(['git']); + $process = $this->localMachineHelper->execute([ + 'git', + 'rev-parse', + 'HEAD', + ], null, $this->dir, false); + + if (!$process->isSuccessful()) { + throw new AcquiaCliException("Unable to determine Git commit hash."); + } + + return trim($process->getOutput()); + } + + /** + * Load configuration from .git/config. + * + * @return string[][] + * A multidimensional array keyed by file section. + */ + private function getGitConfig(): array + { + $filePath = $this->projectDir . '/.git/config'; + return @\Safe\parse_ini_file($filePath, true); + } + + /** + * Gets an array of git remotes from a .git/config array. + * + * @param string[][] $gitConfig + * @return string[] + * A flat array of git remote urls. + */ + private function getGitRemotes(array $gitConfig): array + { + $localVcsRemotes = []; + foreach ($gitConfig as $sectionName => $section) { + if ( + (str_contains($sectionName, 'remote ')) && + array_key_exists('url', $section) && + (strpos($section['url'], 'acquia.com') || strpos($section['url'], 'acquia-sites.com')) + ) { + $localVcsRemotes[] = $section['url']; + } + } + + return $localVcsRemotes; + } + + private function findCloudApplicationByGitUrl( Client $acquiaCloudClient, array $localGitRemotes ): ?ApplicationResponse { - // Set up API resources. - $applicationsResource = new Applications($acquiaCloudClient); - $customerApplications = $applicationsResource->getAll(); - $environmentsResource = new Environments($acquiaCloudClient); - - // Create progress bar. - $count = count($customerApplications); - $progressBar = new ProgressBar($this->output, $count); - $progressBar->setFormat('message'); - $progressBar->setMessage("Searching $count applications on the Cloud Platform..."); - $progressBar->start(); - - // Search Cloud applications. - $terminalWidth = (new Terminal())->getWidth(); - foreach ($customerApplications as $application) { - // Ensure that the message takes up the full terminal width to prevent display artifacts. - $message = "Searching {$application->name} for matching git URLs"; - $suffixLength = $terminalWidth - strlen($message) - 17; - $suffix = $suffixLength > 0 ? str_repeat(' ', $suffixLength) : ''; - $progressBar->setMessage($message . $suffix); - $applicationEnvironments = $environmentsResource->getAll($application->uuid); - if ($application = $this->searchApplicationEnvironmentsForGitUrl( - $application, - $applicationEnvironments, - $localGitRemotes - )) { + // Set up API resources. + $applicationsResource = new Applications($acquiaCloudClient); + $customerApplications = $applicationsResource->getAll(); + $environmentsResource = new Environments($acquiaCloudClient); + + // Create progress bar. + $count = count($customerApplications); + $progressBar = new ProgressBar($this->output, $count); + $progressBar->setFormat('message'); + $progressBar->setMessage("Searching $count applications on the Cloud Platform..."); + $progressBar->start(); + + // Search Cloud applications. + $terminalWidth = (new Terminal())->getWidth(); + foreach ($customerApplications as $application) { + // Ensure that the message takes up the full terminal width to prevent display artifacts. + $message = "Searching {$application->name} for matching git URLs"; + $suffixLength = $terminalWidth - strlen($message) - 17; + $suffix = $suffixLength > 0 ? str_repeat(' ', $suffixLength) : ''; + $progressBar->setMessage($message . $suffix); + $applicationEnvironments = $environmentsResource->getAll($application->uuid); + if ( + $application = $this->searchApplicationEnvironmentsForGitUrl( + $application, + $applicationEnvironments, + $localGitRemotes + ) + ) { + $progressBar->finish(); + $progressBar->clear(); + + return $application; + } + $progressBar->advance(); + } $progressBar->finish(); $progressBar->clear(); - return $application; - } - $progressBar->advance(); - } - $progressBar->finish(); - $progressBar->clear(); - - return NULL; - } - - protected function createTable(OutputInterface $output, string $title, array $headers, mixed $widths): Table { - $terminalWidth = (new Terminal())->getWidth(); - $terminalWidth *= .90; - $table = new Table($output); - $table->setHeaders($headers); - $table->setHeaderTitle($title); - $setWidths = static function (mixed $width) use ($terminalWidth) { - return (int) ($terminalWidth * $width); - }; - $table->setColumnWidths(array_map($setWidths, $widths)); - return $table; - } - - private function searchApplicationEnvironmentsForGitUrl( - ApplicationResponse $application, - EnvironmentsResponse $applicationEnvironments, - array $localGitRemotes - ): ?ApplicationResponse { - foreach ($applicationEnvironments as $environment) { - if ($environment->flags->production && in_array($environment->vcs->url, $localGitRemotes, TRUE)) { - $this->logger->debug("Found matching Cloud application! {$application->name} with uuid {$application->uuid} matches local git URL {$environment->vcs->url}"); + return null; + } - return $application; - } + protected function createTable(OutputInterface $output, string $title, array $headers, mixed $widths): Table + { + $terminalWidth = (new Terminal())->getWidth(); + $terminalWidth *= .90; + $table = new Table($output); + $table->setHeaders($headers); + $table->setHeaderTitle($title); + $setWidths = static function (mixed $width) use ($terminalWidth) { + return (int) ($terminalWidth * $width); + }; + $table->setColumnWidths(array_map($setWidths, $widths)); + return $table; } - return NULL; - } + private function searchApplicationEnvironmentsForGitUrl( + ApplicationResponse $application, + EnvironmentsResponse $applicationEnvironments, + array $localGitRemotes + ): ?ApplicationResponse { + foreach ($applicationEnvironments as $environment) { + if ($environment->flags->production && in_array($environment->vcs->url, $localGitRemotes, true)) { + $this->logger->debug("Found matching Cloud application! {$application->name} with uuid {$application->uuid} matches local git URL {$environment->vcs->url}"); + + return $application; + } + } + + return null; + } - /** - * Infer which Cloud Platform application is associated with the current local git repository. - * - * If the local git repository has a remote with a URL that matches a Cloud Platform application's VCS URL, assume - * that we have a match. - */ - protected function inferCloudAppFromLocalGitConfig( - Client $acquiaCloudClient + /** + * Infer which Cloud Platform application is associated with the current local git repository. + * + * If the local git repository has a remote with a URL that matches a Cloud Platform application's VCS URL, assume + * that we have a match. + */ + protected function inferCloudAppFromLocalGitConfig( + Client $acquiaCloudClient ): ?ApplicationResponse { - if ($this->projectDir && $this->input->isInteractive()) { - $this->output->writeln("There is no Cloud Platform application linked to {$this->projectDir}/.git."); - $answer = $this->io->confirm('Would you like Acquia CLI to search for a Cloud application that matches your local git config?'); - if ($answer) { - $this->output->writeln('Searching for a matching Cloud application...'); - try { - $gitConfig = $this->getGitConfig(); - $localGitRemotes = $this->getGitRemotes($gitConfig); - if ($cloudApplication = $this->findCloudApplicationByGitUrl($acquiaCloudClient, - $localGitRemotes)) { - $this->output->writeln('Found a matching application!'); - return $cloudApplication; - } + if ($this->projectDir && $this->input->isInteractive()) { + $this->output->writeln("There is no Cloud Platform application linked to {$this->projectDir}/.git."); + $answer = $this->io->confirm('Would you like Acquia CLI to search for a Cloud application that matches your local git config?'); + if ($answer) { + $this->output->writeln('Searching for a matching Cloud application...'); + try { + $gitConfig = $this->getGitConfig(); + $localGitRemotes = $this->getGitRemotes($gitConfig); + if ( + $cloudApplication = $this->findCloudApplicationByGitUrl( + $acquiaCloudClient, + $localGitRemotes + ) + ) { + $this->output->writeln('Found a matching application!'); + return $cloudApplication; + } + + $this->output->writeln('Could not find a matching Cloud application.'); + return null; + } catch (FilesystemException $e) { + throw new AcquiaCliException($e->getMessage()); + } + } + } + + return null; + } - $this->output->writeln('Could not find a matching Cloud application.'); - return NULL; + /** + * @return array + */ + protected function getSubscriptionApplications(Client $client, SubscriptionResponse $subscription): array + { + $applicationsResource = new Applications($client); + $applications = $applicationsResource->getAll(); + $subscriptionApplications = []; + + foreach ($applications as $application) { + if ($application->subscription->uuid === $subscription->uuid) { + $subscriptionApplications[] = $application; + } } - catch (FilesystemException $e) { - throw new AcquiaCliException($e->getMessage()); + if (count($subscriptionApplications) === 0) { + throw new AcquiaCliException("You do not have access to any applications on the $subscription->name subscription"); } - } + return $subscriptionApplications; } - return NULL; - } + protected function determineCloudSubscription(): SubscriptionResponse + { + $acquiaCloudClient = $this->cloudApiClientService->getClient(); - /** - * @return array - */ - protected function getSubscriptionApplications(Client $client, SubscriptionResponse $subscription): array { - $applicationsResource = new Applications($client); - $applications = $applicationsResource->getAll(); - $subscriptionApplications = []; + if ($this->input->hasArgument('subscriptionUuid') && $this->input->getArgument('subscriptionUuid')) { + $cloudSubscriptionUuid = $this->input->getArgument('subscriptionUuid'); + self::validateUuid($cloudSubscriptionUuid); + return (new Subscriptions($acquiaCloudClient))->get($cloudSubscriptionUuid); + } + + // Finally, just ask. + if ($this->input->isInteractive() && $subscription = $this->promptChooseSubscription($acquiaCloudClient)) { + return $subscription; + } - foreach ($applications as $application) { - if ($application->subscription->uuid === $subscription->uuid) { - $subscriptionApplications[] = $application; - } + throw new AcquiaCliException("Could not determine Cloud subscription. Run this command interactively or use the `subscriptionUuid` argument."); } - if (count($subscriptionApplications) === 0) { - throw new AcquiaCliException("You do not have access to any applications on the $subscription->name subscription"); + + /** + * Determine the Cloud application. + */ + protected function determineCloudApplication(bool $promptLinkApp = false): ?string + { + $applicationUuid = $this->doDetermineCloudApplication(); + if (!isset($applicationUuid)) { + throw new AcquiaCliException("Could not determine Cloud Application. Run this command interactively or use `acli link` to link a Cloud Application before running non-interactively."); + } + + $application = $this->getCloudApplication($applicationUuid); + // No point in trying to link a directory that's not a repo. + if (!empty($this->projectDir) && !$this->getCloudUuidFromDatastore()) { + if ($promptLinkApp) { + $this->saveCloudUuidToDatastore($application); + } elseif (!AcquiaDrupalEnvironmentDetector::isAhIdeEnv() && !$this->getCloudApplicationUuidFromBltYaml()) { + $this->promptLinkApplication($application); + } + } + + return $applicationUuid; } - return $subscriptionApplications; - } - protected function determineCloudSubscription(): SubscriptionResponse { - $acquiaCloudClient = $this->cloudApiClientService->getClient(); + protected function doDetermineCloudApplication(): ?string + { + $acquiaCloudClient = $this->cloudApiClientService->getClient(); + + if ($this->input->hasArgument('applicationUuid') && $this->input->getArgument('applicationUuid')) { + $cloudApplicationUuid = $this->input->getArgument('applicationUuid'); + return self::validateUuid($cloudApplicationUuid); + } + + if ($this->input->hasArgument('environmentId') && $this->input->getArgument('environmentId')) { + $environmentId = $this->input->getArgument('environmentId'); + $environment = $this->getCloudEnvironment($environmentId); + return $environment->application->uuid; + } + + // Try local project info. + if ($applicationUuid = $this->getCloudUuidFromDatastore()) { + $this->logger->debug("Using Cloud application UUID: $applicationUuid from {$this->datastoreAcli->filepath}"); + return $applicationUuid; + } + + if ($applicationUuid = $this->getCloudApplicationUuidFromBltYaml()) { + $this->logger->debug("Using Cloud application UUID $applicationUuid from blt/blt.yml"); + return $applicationUuid; + } + + // Get from the Cloud Platform env var. + if ($applicationUuid = self::getThisCloudIdeCloudAppUuid()) { + return $applicationUuid; + } + + // Try to guess based on local git url config. + if ($cloudApplication = $this->inferCloudAppFromLocalGitConfig($acquiaCloudClient)) { + return $cloudApplication->uuid; + } - if ($this->input->hasArgument('subscriptionUuid') && $this->input->getArgument('subscriptionUuid')) { - $cloudSubscriptionUuid = $this->input->getArgument('subscriptionUuid'); - self::validateUuid($cloudSubscriptionUuid); - return (new Subscriptions($acquiaCloudClient))->get($cloudSubscriptionUuid); + if ($this->input->isInteractive()) { + /** @var ApplicationResponse $application */ + $application = $this->promptChooseApplication($acquiaCloudClient); + if ($application) { + return $application->uuid; + } + } + + return null; } - // Finally, just ask. - if ($this->input->isInteractive() && $subscription = $this->promptChooseSubscription($acquiaCloudClient)) { - return $subscription; + protected function getCloudApplicationUuidFromBltYaml(): ?string + { + $bltYamlFilePath = Path::join($this->projectDir, 'blt', 'blt.yml'); + if (file_exists($bltYamlFilePath)) { + $contents = Yaml::parseFile($bltYamlFilePath); + if (array_key_exists('cloud', $contents) && array_key_exists('appId', $contents['cloud'])) { + return $contents['cloud']['appId']; + } + } + + return null; } - throw new AcquiaCliException("Could not determine Cloud subscription. Run this command interactively or use the `subscriptionUuid` argument."); - } + public static function validateUuid(string $uuid): string + { + $violations = Validation::createValidator()->validate($uuid, [ + new Length([ + 'value' => 36, + ]), + self::getUuidRegexConstraint(), + ]); + if (count($violations)) { + throw new ValidatorException($violations->get(0)->getMessage()); + } - /** - * Determine the Cloud application. - */ - protected function determineCloudApplication(bool $promptLinkApp = FALSE): ?string { - $applicationUuid = $this->doDetermineCloudApplication(); - if (!isset($applicationUuid)) { - throw new AcquiaCliException("Could not determine Cloud Application. Run this command interactively or use `acli link` to link a Cloud Application before running non-interactively."); + return $uuid; } - $application = $this->getCloudApplication($applicationUuid); - // No point in trying to link a directory that's not a repo. - if (!empty($this->projectDir) && !$this->getCloudUuidFromDatastore()) { - if ($promptLinkApp) { - $this->saveCloudUuidToDatastore($application); - } - elseif (!AcquiaDrupalEnvironmentDetector::isAhIdeEnv() && !$this->getCloudApplicationUuidFromBltYaml()) { - $this->promptLinkApplication($application); - } + private function saveCloudUuidToDatastore(ApplicationResponse $application): bool + { + $this->datastoreAcli->set('cloud_app_uuid', $application->uuid); + $this->io->success("The Cloud application {$application->name} has been linked to this repository by writing to {$this->datastoreAcli->filepath}"); + + return true; } - return $applicationUuid; - } + protected function getCloudUuidFromDatastore(): ?string + { + return $this->datastoreAcli->get('cloud_app_uuid'); + } - protected function doDetermineCloudApplication(): ?string { - $acquiaCloudClient = $this->cloudApiClientService->getClient(); + private function promptLinkApplication( + ApplicationResponse $cloudApplication + ): bool { + $answer = $this->io->confirm("Would you like to link the Cloud application $cloudApplication->name to this repository?"); + if ($answer) { + return $this->saveCloudUuidToDatastore($cloudApplication); + } + return false; + } - if ($this->input->hasArgument('applicationUuid') && $this->input->getArgument('applicationUuid')) { - $cloudApplicationUuid = $this->input->getArgument('applicationUuid'); - return self::validateUuid($cloudApplicationUuid); + protected function validateCwdIsValidDrupalProject(): void + { + if (!$this->projectDir) { + throw new AcquiaCliException('Could not find a local Drupal project. Looked for `docroot/index.php` in current and parent directories. Execute this command from within a Drupal project directory.'); + } } - if ($this->input->hasArgument('environmentId') && $this->input->getArgument('environmentId')) { - $environmentId = $this->input->getArgument('environmentId'); - $environment = $this->getCloudEnvironment($environmentId); - return $environment->application->uuid; + /** + * Determines if Acquia CLI is being run from within a Cloud IDE. + * + * @return bool TRUE if Acquia CLI is being run from within a Cloud IDE. + */ + public static function isAcquiaCloudIde(): bool + { + return AcquiaDrupalEnvironmentDetector::isAhIdeEnv(); } - // Try local project info. - if ($applicationUuid = $this->getCloudUuidFromDatastore()) { - $this->logger->debug("Using Cloud application UUID: $applicationUuid from {$this->datastoreAcli->filepath}"); - return $applicationUuid; + /** + * Get the Cloud Application UUID from a Cloud IDE's environmental variable. + * + * This command assumes it is being run inside a Cloud IDE. + */ + protected static function getThisCloudIdeCloudAppUuid(): false|string + { + return getenv('ACQUIA_APPLICATION_UUID'); } - if ($applicationUuid = $this->getCloudApplicationUuidFromBltYaml()) { - $this->logger->debug("Using Cloud application UUID $applicationUuid from blt/blt.yml"); - return $applicationUuid; + /** + * Get the UUID from a Cloud IDE's environmental variable. + * + * This command assumes it is being run inside a Cloud IDE. + */ + protected static function getThisCloudIdeUuid(): false|string + { + return getenv('REMOTEIDE_UUID'); } - // Get from the Cloud Platform env var. - if ($applicationUuid = self::getThisCloudIdeCloudAppUuid()) { - return $applicationUuid; + protected static function getThisCloudIdeLabel(): false|string + { + return getenv('REMOTEIDE_LABEL'); } - // Try to guess based on local git url config. - if ($cloudApplication = $this->inferCloudAppFromLocalGitConfig($acquiaCloudClient)) { - return $cloudApplication->uuid; + protected static function getThisCloudIdeWebUrl(): false|string + { + return getenv('REMOTEIDE_WEB_HOST'); } - if ($this->input->isInteractive()) { - /** @var ApplicationResponse $application */ - $application = $this->promptChooseApplication($acquiaCloudClient); - if ($application) { - return $application->uuid; - } + protected function getCloudApplication(string $applicationUuid): ApplicationResponse + { + $applicationsResource = new Applications($this->cloudApiClientService->getClient()); + return $applicationsResource->get($applicationUuid); } - return NULL; - } + protected function getCloudEnvironment(string $environmentId): EnvironmentResponse + { + $environmentResource = new Environments($this->cloudApiClientService->getClient()); - protected function getCloudApplicationUuidFromBltYaml(): ?string { - $bltYamlFilePath = Path::join($this->projectDir, 'blt', 'blt.yml'); - if (file_exists($bltYamlFilePath)) { - $contents = Yaml::parseFile($bltYamlFilePath); - if (array_key_exists('cloud', $contents) && array_key_exists('appId', $contents['cloud'])) { - return $contents['cloud']['appId']; - } + return $environmentResource->get($environmentId); } - return NULL; - } + public static function validateEnvironmentAlias(string $alias): string + { + $violations = Validation::createValidator()->validate($alias, [ + new Length(['min' => 5]), + new NotBlank(), + new Regex(['pattern' => '/.+\..+/', 'message' => 'You must enter either an environment ID or alias. Environment aliases must match the pattern [app-name].[env]']), + ]); + if (count($violations)) { + throw new ValidatorException($violations->get(0)->getMessage()); + } + + return $alias; + } - public static function validateUuid(string $uuid): string { - $violations = Validation::createValidator()->validate($uuid, [ - new Length([ - 'value' => 36, - ]), - self::getUuidRegexConstraint(), - ]); - if (count($violations)) { - throw new ValidatorException($violations->get(0)->getMessage()); + protected function normalizeAlias(string $alias): string + { + return str_replace('@', '', $alias); + } + + protected function getEnvironmentFromAliasArg(string $alias): EnvironmentResponse + { + return $this->getEnvFromAlias($alias); } - return $uuid; - } + private function getEnvFromAlias(string $alias): EnvironmentResponse + { + return self::getAliasCache()->get($alias, function () use ($alias): \AcquiaCloudApi\Response\EnvironmentResponse { + return $this->doGetEnvFromAlias($alias); + }); + } - private function saveCloudUuidToDatastore(ApplicationResponse $application): bool { - $this->datastoreAcli->set('cloud_app_uuid', $application->uuid); - $this->io->success("The Cloud application {$application->name} has been linked to this repository by writing to {$this->datastoreAcli->filepath}"); + private function doGetEnvFromAlias(string $alias): EnvironmentResponse + { + $siteEnvParts = explode('.', $alias); + [$applicationAlias, $environmentAlias] = $siteEnvParts; + $this->logger->debug("Searching for an environment matching alias $applicationAlias.$environmentAlias."); + $customerApplication = $this->getApplicationFromAlias($applicationAlias); + $acquiaCloudClient = $this->cloudApiClientService->getClient(); + $environmentsResource = new Environments($acquiaCloudClient); + $environments = $environmentsResource->getAll($customerApplication->uuid); + foreach ($environments as $environment) { + if ($environment->name === $environmentAlias) { + $this->logger->debug("Found environment {$environment->uuid} matching $environmentAlias."); - return TRUE; - } + return $environment; + } + } - protected function getCloudUuidFromDatastore(): ?string { - return $this->datastoreAcli->get('cloud_app_uuid'); - } + throw new AcquiaCliException("Environment not found matching the alias {alias}", ['alias' => "$applicationAlias.$environmentAlias"]); + } - private function promptLinkApplication( - ApplicationResponse $cloudApplication - ): bool { - $answer = $this->io->confirm("Would you like to link the Cloud application $cloudApplication->name to this repository?"); - if ($answer) { - return $this->saveCloudUuidToDatastore($cloudApplication); - } - return FALSE; - } - - protected function validateCwdIsValidDrupalProject(): void { - if (!$this->projectDir) { - throw new AcquiaCliException('Could not find a local Drupal project. Looked for `docroot/index.php` in current and parent directories. Execute this command from within a Drupal project directory.'); - } - } - - /** - * Determines if Acquia CLI is being run from within a Cloud IDE. - * - * @return bool TRUE if Acquia CLI is being run from within a Cloud IDE. - */ - public static function isAcquiaCloudIde(): bool { - return AcquiaDrupalEnvironmentDetector::isAhIdeEnv(); - } - - /** - * Get the Cloud Application UUID from a Cloud IDE's environmental variable. - * - * This command assumes it is being run inside a Cloud IDE. - */ - protected static function getThisCloudIdeCloudAppUuid(): false|string { - return getenv('ACQUIA_APPLICATION_UUID'); - } - - /** - * Get the UUID from a Cloud IDE's environmental variable. - * - * This command assumes it is being run inside a Cloud IDE. - */ - protected static function getThisCloudIdeUuid(): false|string { - return getenv('REMOTEIDE_UUID'); - } - - protected static function getThisCloudIdeLabel(): false|string { - return getenv('REMOTEIDE_LABEL'); - } - - protected static function getThisCloudIdeWebUrl(): false|string { - return getenv('REMOTEIDE_WEB_HOST'); - } - - protected function getCloudApplication(string $applicationUuid): ApplicationResponse { - $applicationsResource = new Applications($this->cloudApiClientService->getClient()); - return $applicationsResource->get($applicationUuid); - } - - protected function getCloudEnvironment(string $environmentId): EnvironmentResponse { - $environmentResource = new Environments($this->cloudApiClientService->getClient()); - - return $environmentResource->get($environmentId); - } - - public static function validateEnvironmentAlias(string $alias): string { - $violations = Validation::createValidator()->validate($alias, [ - new Length(['min' => 5]), - new NotBlank(), - new Regex(['pattern' => '/.+\..+/', 'message' => 'You must enter either an environment ID or alias. Environment aliases must match the pattern [app-name].[env]']), - ]); - if (count($violations)) { - throw new ValidatorException($violations->get(0)->getMessage()); - } - - return $alias; - } - - protected function normalizeAlias(string $alias): string { - return str_replace('@', '', $alias); - } - - protected function getEnvironmentFromAliasArg(string $alias): EnvironmentResponse { - return $this->getEnvFromAlias($alias); - } - - private function getEnvFromAlias(string $alias): EnvironmentResponse { - return self::getAliasCache()->get($alias, function () use ($alias): \AcquiaCloudApi\Response\EnvironmentResponse { - return $this->doGetEnvFromAlias($alias); - }); - } - - private function doGetEnvFromAlias(string $alias): EnvironmentResponse { - $siteEnvParts = explode('.', $alias); - [$applicationAlias, $environmentAlias] = $siteEnvParts; - $this->logger->debug("Searching for an environment matching alias $applicationAlias.$environmentAlias."); - $customerApplication = $this->getApplicationFromAlias($applicationAlias); - $acquiaCloudClient = $this->cloudApiClientService->getClient(); - $environmentsResource = new Environments($acquiaCloudClient); - $environments = $environmentsResource->getAll($customerApplication->uuid); - foreach ($environments as $environment) { - if ($environment->name === $environmentAlias) { - $this->logger->debug("Found environment {$environment->uuid} matching $environmentAlias."); - - return $environment; - } - } - - throw new AcquiaCliException("Environment not found matching the alias {alias}", ['alias' => "$applicationAlias.$environmentAlias"]); - } - - private function getApplicationFromAlias(string $applicationAlias): mixed { - return self::getAliasCache() - ->get($applicationAlias, function () use ($applicationAlias) { - return $this->doGetApplicationFromAlias($applicationAlias); - }); - } - - /** - * Return the ACLI alias cache. - */ - public static function getAliasCache(): AliasCache { - return new AliasCache('acli_aliases'); - } - - private function doGetApplicationFromAlias(string $applicationAlias): mixed { - if (!strpos($applicationAlias, ':')) { - $applicationAlias = '*:' . $applicationAlias; - } - $acquiaCloudClient = $this->cloudApiClientService->getClient(); - // No need to clear this query later since getClient() is a factory method. - $acquiaCloudClient->addQuery('filter', 'hosting=@' . $applicationAlias); - // Allow Cloud Platform users with 'support' role to resolve aliases for applications to - // which they don't explicitly belong. - $accountResource = new Account($acquiaCloudClient); - $account = $accountResource->get(); - if ($account->flags->support) { - $acquiaCloudClient->addQuery('all', 'true'); - } - $applicationsResource = new Applications($acquiaCloudClient); - $customerApplications = $applicationsResource->getAll(); - if (count($customerApplications) === 0) { - throw new AcquiaCliException("No applications match the alias {applicationAlias}", ['applicationAlias' => $applicationAlias]); - } - if (count($customerApplications) > 1) { - $callback = static function (mixed $element) { - return $element->hosting->id; - }; - $aliases = array_map($callback, (array) $customerApplications); - $this->io->error(sprintf("Use a unique application alias: %s", implode(', ', $aliases))); - throw new AcquiaCliException("Multiple applications match the alias {applicationAlias}", ['applicationAlias' => $applicationAlias]); - } - - $customerApplication = $customerApplications[0]; - - $this->logger->debug("Found application {$customerApplication->uuid} matching alias $applicationAlias."); - - return $customerApplication; - } - - protected function requireCloudIdeEnvironment(): void { - if (!self::isAcquiaCloudIde() || !self::getThisCloudIdeUuid()) { - throw new AcquiaCliException('This command can only be run inside of an Acquia Cloud IDE'); - } - } - - /** - * @return \stdClass|null - */ - protected function findIdeSshKeyOnCloud(string $ideLabel, string $ideUuid): ?stdClass { - $acquiaCloudClient = $this->cloudApiClientService->getClient(); - $cloudKeys = $acquiaCloudClient->request('get', '/account/ssh-keys'); - $sshKeyLabel = SshKeyCommandBase::getIdeSshKeyLabel($ideLabel, $ideUuid); - foreach ($cloudKeys as $cloudKey) { - if ($cloudKey->label === $sshKeyLabel) { - return $cloudKey; - } - } - return NULL; - } - - public function checkForNewVersion(): bool|string { - // Input not set if called from an exception listener. - if (!isset($this->input)) { - return FALSE; - } - // Running on API commands would corrupt JSON output. - if (str_contains($this->input->getArgument('command'), 'api:') - || str_contains($this->input->getArgument('command'), 'acsf:')) { - return FALSE; - } - // Bail in Cloud IDEs to avoid hitting GitHub API rate limits. - if (AcquiaDrupalEnvironmentDetector::isAhIdeEnv()) { - return FALSE; - } - try { - if ($latest = $this->hasUpdate()) { - return $latest; - } - } - catch (Exception) { - $this->logger->debug("Could not determine if Acquia CLI has a new version available."); - } - return FALSE; - } - - /** - * Check if an update is available. - * - * @todo unify with consolidation/self-update and support unstable channels - */ - protected function hasUpdate(): bool|string { - $versionParser = new VersionParser(); - // Fail fast on development builds (throw UnexpectedValueException). - $currentVersion = $versionParser->normalize($this->getApplication()->getVersion()); - $client = $this->getUpdateClient(); - $response = $client->get('https://api.github.com/repos/acquia/cli/releases'); - if ($response->getStatusCode() !== 200) { - $this->logger->debug('Encountered ' . $response->getStatusCode() . ' error when attempting to check for new ACLI releases on GitHub: ' . $response->getReasonPhrase()); - return FALSE; - } - - $releases = json_decode((string) $response->getBody(), FALSE, 512, JSON_THROW_ON_ERROR); - if (!isset($releases[0])) { - $this->logger->debug('No releases found at GitHub repository acquia/cli'); - return FALSE; - } - - foreach ($releases as $release) { - if (!$release->prerelease) { - /** - * @var string $version - */ - $version = $release->tag_name; - $versionStability = VersionParser::parseStability($version); - $versionIsNewer = Comparator::greaterThan($versionParser->normalize($version), $currentVersion); - if ($versionStability === 'stable' && $versionIsNewer) { - return $version; - } - return FALSE; - } - } - return FALSE; - } - - public function setUpdateClient(\GuzzleHttp\Client $client): void { - $this->updateClient = $client; - } - - public function getUpdateClient(): \GuzzleHttp\Client { - if (!isset($this->updateClient)) { - $stack = HandlerStack::create(); - $stack->push(new CacheMiddleware( - new PrivateCacheStrategy( - new Psr6CacheStorage( - new FilesystemAdapter('acli') - ) - ) - ), - 'cache'); - $client = new \GuzzleHttp\Client(['handler' => $stack]); - $this->setUpdateClient($client); - } - return $this->updateClient; - } - - protected function fillMissingRequiredApplicationUuid(InputInterface $input, OutputInterface $output): void { - if ($input->hasArgument('applicationUuid') && !$input->getArgument('applicationUuid') && $this->getDefinition()->getArgument('applicationUuid')->isRequired()) { - $output->writeln('Inferring Cloud Application UUID for this command since none was provided...', OutputInterface::VERBOSITY_VERBOSE); - if ($applicationUuid = $this->determineCloudApplication()) { - $output->writeln("Set application uuid to $applicationUuid", OutputInterface::VERBOSITY_VERBOSE); - $input->setArgument('applicationUuid', $applicationUuid); - } - } - } - - private function convertUserAliasToUuid(InputInterface $input, string $userUuidArgument, string $orgUuidArgument): void { - if ($input->hasArgument($userUuidArgument) - && $input->getArgument($userUuidArgument) - && $input->hasArgument($orgUuidArgument) - && $input->getArgument($orgUuidArgument) - ) { - $userUuID = $input->getArgument($userUuidArgument); - $orgUuid = $input->getArgument($orgUuidArgument); - $userUuid = $this->validateUserUuid($userUuID, $orgUuid); - $input->setArgument($userUuidArgument, $userUuid); + private function getApplicationFromAlias(string $applicationAlias): mixed + { + return self::getAliasCache() + ->get($applicationAlias, function () use ($applicationAlias) { + return $this->doGetApplicationFromAlias($applicationAlias); + }); + } + + /** + * Return the ACLI alias cache. + */ + public static function getAliasCache(): AliasCache + { + return new AliasCache('acli_aliases'); + } + + private function doGetApplicationFromAlias(string $applicationAlias): mixed + { + if (!strpos($applicationAlias, ':')) { + $applicationAlias = '*:' . $applicationAlias; + } + $acquiaCloudClient = $this->cloudApiClientService->getClient(); + // No need to clear this query later since getClient() is a factory method. + $acquiaCloudClient->addQuery('filter', 'hosting=@' . $applicationAlias); + // Allow Cloud Platform users with 'support' role to resolve aliases for applications to + // which they don't explicitly belong. + $accountResource = new Account($acquiaCloudClient); + $account = $accountResource->get(); + if ($account->flags->support) { + $acquiaCloudClient->addQuery('all', 'true'); + } + $applicationsResource = new Applications($acquiaCloudClient); + $customerApplications = $applicationsResource->getAll(); + if (count($customerApplications) === 0) { + throw new AcquiaCliException("No applications match the alias {applicationAlias}", ['applicationAlias' => $applicationAlias]); + } + if (count($customerApplications) > 1) { + $callback = static function (mixed $element) { + return $element->hosting->id; + }; + $aliases = array_map($callback, (array) $customerApplications); + $this->io->error(sprintf("Use a unique application alias: %s", implode(', ', $aliases))); + throw new AcquiaCliException("Multiple applications match the alias {applicationAlias}", ['applicationAlias' => $applicationAlias]); + } + + $customerApplication = $customerApplications[0]; + + $this->logger->debug("Found application {$customerApplication->uuid} matching alias $applicationAlias."); + + return $customerApplication; + } + + protected function requireCloudIdeEnvironment(): void + { + if (!self::isAcquiaCloudIde() || !self::getThisCloudIdeUuid()) { + throw new AcquiaCliException('This command can only be run inside of an Acquia Cloud IDE'); + } } - } - /** - * @param string $userUuidArgument User alias like uuid or email. - * @param string $orgUuidArgument Organization uuid. - * @return string User uuid from alias - */ - private function validateUserUuid(string $userUuidArgument, string $orgUuidArgument): string { - try { - self::validateUuid($userUuidArgument); + /** + * @return \stdClass|null + */ + protected function findIdeSshKeyOnCloud(string $ideLabel, string $ideUuid): ?stdClass + { + $acquiaCloudClient = $this->cloudApiClientService->getClient(); + $cloudKeys = $acquiaCloudClient->request('get', '/account/ssh-keys'); + $sshKeyLabel = SshKeyCommandBase::getIdeSshKeyLabel($ideLabel, $ideUuid); + foreach ($cloudKeys as $cloudKey) { + if ($cloudKey->label === $sshKeyLabel) { + return $cloudKey; + } + } + return null; + } + + public function checkForNewVersion(): bool|string + { + // Input not set if called from an exception listener. + if (!isset($this->input)) { + return false; + } + // Running on API commands would corrupt JSON output. + if ( + str_contains($this->input->getArgument('command'), 'api:') + || str_contains($this->input->getArgument('command'), 'acsf:') + ) { + return false; + } + // Bail in Cloud IDEs to avoid hitting GitHub API rate limits. + if (AcquiaDrupalEnvironmentDetector::isAhIdeEnv()) { + return false; + } + try { + if ($latest = $this->hasUpdate()) { + return $latest; + } + } catch (Exception) { + $this->logger->debug("Could not determine if Acquia CLI has a new version available."); + } + return false; + } + + /** + * Check if an update is available. + * + * @todo unify with consolidation/self-update and support unstable channels + */ + protected function hasUpdate(): bool|string + { + $versionParser = new VersionParser(); + // Fail fast on development builds (throw UnexpectedValueException). + $currentVersion = $versionParser->normalize($this->getApplication()->getVersion()); + $client = $this->getUpdateClient(); + $response = $client->get('https://api.github.com/repos/acquia/cli/releases'); + if ($response->getStatusCode() !== 200) { + $this->logger->debug('Encountered ' . $response->getStatusCode() . ' error when attempting to check for new ACLI releases on GitHub: ' . $response->getReasonPhrase()); + return false; + } + + $releases = json_decode((string) $response->getBody(), false, 512, JSON_THROW_ON_ERROR); + if (!isset($releases[0])) { + $this->logger->debug('No releases found at GitHub repository acquia/cli'); + return false; + } + + foreach ($releases as $release) { + if (!$release->prerelease) { + /** + * @var string $version + */ + $version = $release->tag_name; + $versionStability = VersionParser::parseStability($version); + $versionIsNewer = Comparator::greaterThan($versionParser->normalize($version), $currentVersion); + if ($versionStability === 'stable' && $versionIsNewer) { + return $version; + } + return false; + } + } + return false; + } + + public function setUpdateClient(\GuzzleHttp\Client $client): void + { + $this->updateClient = $client; + } + + public function getUpdateClient(): \GuzzleHttp\Client + { + if (!isset($this->updateClient)) { + $stack = HandlerStack::create(); + $stack->push( + new CacheMiddleware( + new PrivateCacheStrategy( + new Psr6CacheStorage( + new FilesystemAdapter('acli') + ) + ) + ), + 'cache' + ); + $client = new \GuzzleHttp\Client(['handler' => $stack]); + $this->setUpdateClient($client); + } + return $this->updateClient; + } + + protected function fillMissingRequiredApplicationUuid(InputInterface $input, OutputInterface $output): void + { + if ($input->hasArgument('applicationUuid') && !$input->getArgument('applicationUuid') && $this->getDefinition()->getArgument('applicationUuid')->isRequired()) { + $output->writeln('Inferring Cloud Application UUID for this command since none was provided...', OutputInterface::VERBOSITY_VERBOSE); + if ($applicationUuid = $this->determineCloudApplication()) { + $output->writeln("Set application uuid to $applicationUuid", OutputInterface::VERBOSITY_VERBOSE); + $input->setArgument('applicationUuid', $applicationUuid); + } + } + } + + private function convertUserAliasToUuid(InputInterface $input, string $userUuidArgument, string $orgUuidArgument): void + { + if ( + $input->hasArgument($userUuidArgument) + && $input->getArgument($userUuidArgument) + && $input->hasArgument($orgUuidArgument) + && $input->getArgument($orgUuidArgument) + ) { + $userUuID = $input->getArgument($userUuidArgument); + $orgUuid = $input->getArgument($orgUuidArgument); + $userUuid = $this->validateUserUuid($userUuID, $orgUuid); + $input->setArgument($userUuidArgument, $userUuid); + } + } + + /** + * @param string $userUuidArgument User alias like uuid or email. + * @param string $orgUuidArgument Organization uuid. + * @return string User uuid from alias + */ + private function validateUserUuid(string $userUuidArgument, string $orgUuidArgument): string + { + try { + self::validateUuid($userUuidArgument); + } catch (ValidatorException) { + // Since this isn't a valid UUID, assuming this is email address. + return $this->getUserUuidFromUserAlias($userUuidArgument, $orgUuidArgument); + } + + 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); + if (is_object($json)) { + return self::getNotificationUuidFromResponse($json); + } + if (is_string($json)) { + // In rare cases, JSON can decode to a string that's a valid UUID. + self::validateUuid($json); + return $json; + } + } catch (JsonException | AcquiaCliException | ValidatorException) { + } + + // 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. + * @return string User uuid from alias + */ + private function getUserUuidFromUserAlias(string $userAlias, string $orgUuidArgument): string + { + $acquiaCloudClient = $this->cloudApiClientService->getClient(); + $organizationResource = new Organizations($acquiaCloudClient); + $orgMembers = $organizationResource->getMembers($orgUuidArgument); + + // If there are no members. + if (count($orgMembers) === 0) { + throw new AcquiaCliException('Organization has no members'); + } + + foreach ($orgMembers as $member) { + // If email matches with any member. + if ($member->mail === $userAlias) { + return $member->uuid; + } + } + + throw new AcquiaCliException('No matching user found in this organization'); } - catch (ValidatorException) { - // Since this isn't a valid UUID, assuming this is email address. - return $this->getUserUuidFromUserAlias($userUuidArgument, $orgUuidArgument); + + protected function convertApplicationAliasToUuid(InputInterface $input): void + { + if ($input->hasArgument('applicationUuid') && $input->getArgument('applicationUuid')) { + $applicationUuidArgument = $input->getArgument('applicationUuid'); + $applicationUuid = $this->validateApplicationUuid($applicationUuidArgument); + $input->setArgument('applicationUuid', $applicationUuid); + } } - return $userUuidArgument; - } + protected function convertEnvironmentAliasToUuid(InputInterface $input, string $argumentName): void + { + if ($input->hasArgument($argumentName) && $input->getArgument($argumentName)) { + $envUuidArgument = $input->getArgument($argumentName); + $environmentUuid = $this->validateEnvironmentUuid($envUuidArgument, $argumentName); + $input->setArgument($argumentName, $environmentUuid); + } + } - private static function getNotificationUuid(string $notification): string { - // Greedily hope this is already a UUID. - try { - self::validateUuid($notification); - return $notification; + 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); + } } - catch (ValidatorException) { + + public static function getSitegroup(EnvironmentResponse $environment): string + { + $sshUrlParts = explode('.', $environment->sshUrl); + return reset($sshUrlParts); } - // Not a UUID, maybe a JSON object? - try { - $json = json_decode($notification, NULL, 4, JSON_THROW_ON_ERROR); - if (is_object($json)) { - return self::getNotificationUuidFromResponse($json); - } - if (is_string($json)) { - // In rare cases, JSON can decode to a string that's a valid UUID. - self::validateUuid($json); - return $json; - } + public static function getEnvironmentAlias(EnvironmentResponse $environment): string + { + $sshUrlParts = explode('@', $environment->sshUrl); + return reset($sshUrlParts); } - catch (JsonException | AcquiaCliException | ValidatorException) { + + protected function isAcsfEnv(mixed $cloudEnvironment): bool + { + if (str_contains($cloudEnvironment->sshUrl, 'enterprise-g1')) { + return true; + } + foreach ($cloudEnvironment->domains as $domain) { + if (str_contains($domain, 'acsitefactory')) { + return true; + } + } + + return false; } - // 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. - * @return string User uuid from alias - */ - private function getUserUuidFromUserAlias(string $userAlias, string $orgUuidArgument): string { - $acquiaCloudClient = $this->cloudApiClientService->getClient(); - $organizationResource = new Organizations($acquiaCloudClient); - $orgMembers = $organizationResource->getMembers($orgUuidArgument); + /** + * @return array + */ + private function getAcsfSites(EnvironmentResponse $cloudEnvironment): array + { + $envAlias = self::getEnvironmentAlias($cloudEnvironment); + $command = ['cat', "/var/www/site-php/$envAlias/multisite-config.json"]; + $process = $this->sshHelper->executeCommand($cloudEnvironment->sshUrl, $command, false); + if ($process->isSuccessful()) { + return json_decode($process->getOutput(), true, 512, JSON_THROW_ON_ERROR); + } + throw new AcquiaCliException("Could not get ACSF sites"); + } + + /** + * @return array + */ + private function getCloudSites(EnvironmentResponse $cloudEnvironment): array + { + $sitegroup = self::getSitegroup($cloudEnvironment); + $command = ['ls', $this->getCloudSitesPath($cloudEnvironment, $sitegroup)]; + $process = $this->sshHelper->executeCommand($cloudEnvironment->sshUrl, $command, false); + $sites = array_filter(explode("\n", trim($process->getOutput()))); + if ($process->isSuccessful() && $sites) { + return $sites; + } - // If there are no members. - if (count($orgMembers) === 0) { - throw new AcquiaCliException('Organization has no members'); - } - - foreach ($orgMembers as $member) { - // If email matches with any member. - if ($member->mail === $userAlias) { - return $member->uuid; - } - } - - throw new AcquiaCliException('No matching user found in this organization'); - } - - protected function convertApplicationAliasToUuid(InputInterface $input): void { - if ($input->hasArgument('applicationUuid') && $input->getArgument('applicationUuid')) { - $applicationUuidArgument = $input->getArgument('applicationUuid'); - $applicationUuid = $this->validateApplicationUuid($applicationUuidArgument); - $input->setArgument('applicationUuid', $applicationUuid); - } - } - - protected function convertEnvironmentAliasToUuid(InputInterface $input, string $argumentName): void { - if ($input->hasArgument($argumentName) && $input->getArgument($argumentName)) { - $envUuidArgument = $input->getArgument($argumentName); - $environmentUuid = $this->validateEnvironmentUuid($envUuidArgument, $argumentName); - $input->setArgument($argumentName, $environmentUuid); - } - } - - 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); - } - } - - public static function getSitegroup(EnvironmentResponse $environment): string { - $sshUrlParts = explode('.', $environment->sshUrl); - return reset($sshUrlParts); - } - - public static function getEnvironmentAlias(EnvironmentResponse $environment): string { - $sshUrlParts = explode('@', $environment->sshUrl); - return reset($sshUrlParts); - } - - protected function isAcsfEnv(mixed $cloudEnvironment): bool { - if (str_contains($cloudEnvironment->sshUrl, 'enterprise-g1')) { - return TRUE; - } - foreach ($cloudEnvironment->domains as $domain) { - if (str_contains($domain, 'acsitefactory')) { - return TRUE; - } - } - - return FALSE; - } - - /** - * @return array - */ - private function getAcsfSites(EnvironmentResponse $cloudEnvironment): array { - $envAlias = self::getEnvironmentAlias($cloudEnvironment); - $command = ['cat', "/var/www/site-php/$envAlias/multisite-config.json"]; - $process = $this->sshHelper->executeCommand($cloudEnvironment->sshUrl, $command, FALSE); - if ($process->isSuccessful()) { - return json_decode($process->getOutput(), TRUE, 512, JSON_THROW_ON_ERROR); - } - throw new AcquiaCliException("Could not get ACSF sites"); - } - - /** - * @return array - */ - private function getCloudSites(EnvironmentResponse $cloudEnvironment): array { - $sitegroup = self::getSitegroup($cloudEnvironment); - $command = ['ls', $this->getCloudSitesPath($cloudEnvironment, $sitegroup)]; - $process = $this->sshHelper->executeCommand($cloudEnvironment->sshUrl, $command, FALSE); - $sites = array_filter(explode("\n", trim($process->getOutput()))); - if ($process->isSuccessful() && $sites) { - return $sites; - } - - throw new AcquiaCliException("Could not get Cloud sites for " . $cloudEnvironment->name); - } - - protected function getCloudSitesPath(mixed $cloudEnvironment, mixed $sitegroup): string { - if ($cloudEnvironment->platform === 'cloud-next') { - $path = "/home/clouduser/{$cloudEnvironment->name}/sites"; - } - else { - $path = "/mnt/files/$sitegroup.{$cloudEnvironment->name}/sites"; - } - return $path; - } - - protected function promptChooseAcsfSite(EnvironmentResponse $cloudEnvironment): mixed { - $choices = []; - $acsfSites = $this->getAcsfSites($cloudEnvironment); - foreach ($acsfSites['sites'] as $domain => $acsfSite) { - $choices[] = "{$acsfSite['name']} ($domain)"; - } - $choice = $this->io->choice('Choose a site', $choices, $choices[0]); - $key = array_search($choice, $choices, TRUE); - $sites = array_values($acsfSites['sites']); - $site = $sites[$key]; - - return $site['name']; - } - - protected function promptChooseCloudSite(EnvironmentResponse $cloudEnvironment): mixed { - $sites = $this->getCloudSites($cloudEnvironment); - if (count($sites) === 1) { - $site = reset($sites); - $this->logger->debug("Only a single Cloud site was detected. Assuming site is $site"); - return $site; - } - $this->logger->debug("Multisite detected"); - $this->warnMultisite(); - return $this->io->choice('Choose a site', $sites, $sites[0]); - } - - protected static function isLandoEnv(): bool { - return AcquiaDrupalEnvironmentDetector::isLandoEnv(); - } - - protected function reAuthenticate(string $apiKey, string $apiSecret, ?string $baseUri, ?string $accountsUri): void { - // Client service needs to be reinitialized with new credentials in case - // this is being run as a sub-command. - // @see https://github.com/acquia/cli/issues/403 - $this->cloudApiClientService->setConnector(new Connector([ - 'key' => $apiKey, - 'secret' => $apiSecret, - ], $baseUri, $accountsUri)); - } - - private function warnMultisite(): void { - $this->io->note("This is a multisite application. Drupal will load the default site unless you've configured sites.php for this environment: https://docs.acquia.com/cloud-platform/develop/drupal/multisite/"); - } - - protected function setDirAndRequireProjectCwd(InputInterface $input): void { - $this->determineDir($input); - if ($this->dir !== '/home/ide/project' && AcquiaDrupalEnvironmentDetector::isAhIdeEnv()) { - throw new AcquiaCliException('Run this command from the {dir} directory', ['dir' => '/home/ide/project']); - } - } - - protected function determineDir(InputInterface $input): void { - if (isset($this->dir)) { - return; - } - - if ($input->hasOption('dir') && $dir = $input->getOption('dir')) { - $this->dir = $dir; - } - elseif ($this->projectDir) { - $this->dir = $this->projectDir; - } - else { - $this->dir = getcwd(); - } - } - - protected function getOutputCallback(OutputInterface $output, Checklist $checklist): Closure { - return static function (mixed $type, mixed $buffer) use ($checklist, $output): void { - if (!$output->isVerbose() && $checklist->getItems()) { - $checklist->updateProgressBar($buffer); - } - $output->writeln($buffer, OutputInterface::VERBOSITY_VERY_VERBOSE); - }; - } - - protected function executeAllScripts(Closure $outputCallback, Checklist $checklist): void { - $this->runComposerScripts($outputCallback, $checklist); - $this->runDrushCacheClear($outputCallback, $checklist); - $this->runDrushSqlSanitize($outputCallback, $checklist); - } - - protected function runComposerScripts(callable $outputCallback, Checklist $checklist): void { - if (!file_exists(Path::join($this->dir, 'composer.json'))) { - $this->io->note('composer.json file not found. Skipping composer install.'); - return; - } - if (!$this->localMachineHelper->commandExists('composer')) { - $this->io->note('Composer not found. Skipping composer install.'); - return; - } - if (file_exists(Path::join($this->dir, 'vendor'))) { - $this->io->note('Composer dependencies already installed. Skipping composer install.'); - return; - } - $checklist->addItem("Installing Composer dependencies"); - $this->composerInstall($outputCallback); - $checklist->completePreviousItem(); - } - - protected function runDrushCacheClear(Closure $outputCallback, Checklist $checklist): void { - if ($this->getDrushDatabaseConnectionStatus()) { - $checklist->addItem('Clearing Drupal caches via Drush'); - // @todo Add support for Drush 8. - $process = $this->localMachineHelper->execute([ - 'drush', - 'cache:rebuild', - '--yes', - '--no-interaction', - '--verbose', - ], $outputCallback, $this->dir, FALSE); - if (!$process->isSuccessful()) { - throw new AcquiaCliException('Unable to rebuild Drupal caches via Drush. {message}', ['message' => $process->getErrorOutput()]); - } - $checklist->completePreviousItem(); - } - else { - $this->logger->notice('Drush does not have an active database connection. Skipping cache:rebuild'); - } - } - - protected function runDrushSqlSanitize(Closure $outputCallback, Checklist $checklist): void { - if ($this->getDrushDatabaseConnectionStatus()) { - $checklist->addItem('Sanitizing database via Drush'); - $process = $this->localMachineHelper->execute([ - 'drush', - 'sql:sanitize', - '--yes', - '--no-interaction', - '--verbose', - ], $outputCallback, $this->dir, FALSE); - if (!$process->isSuccessful()) { - throw new AcquiaCliException('Unable to sanitize Drupal database via Drush. {message}', ['message' => $process->getErrorOutput()]); - } - $checklist->completePreviousItem(); - $this->io->newLine(); - $this->io->text('Your database was sanitized via drush sql:sanitize. This has changed all user passwords to randomly generated strings. To log in to your Drupal site, use drush uli'); - } - else { - $this->logger->notice('Drush does not have an active database connection. Skipping sql:sanitize.'); - } - } - - private function composerInstall(?callable $outputCallback): void { - $process = $this->localMachineHelper->execute([ - 'composer', - 'install', - '--no-interaction', - ], $outputCallback, $this->dir, FALSE); - if (!$process->isSuccessful()) { - throw new AcquiaCliException('Unable to install Drupal dependencies via Composer. {message}', - ['message' => $process->getErrorOutput()]); - } - } - - protected function getDrushDatabaseConnectionStatus(Closure $outputCallback = NULL): bool { - if (isset($this->drushHasActiveDatabaseConnection)) { - return $this->drushHasActiveDatabaseConnection; - } - if ($this->localMachineHelper->commandExists('drush')) { - $process = $this->localMachineHelper->execute([ - 'drush', - 'status', - '--fields=db-status,drush-version', - '--format=json', + throw new AcquiaCliException("Could not get Cloud sites for " . $cloudEnvironment->name); + } + + protected function getCloudSitesPath(mixed $cloudEnvironment, mixed $sitegroup): string + { + if ($cloudEnvironment->platform === 'cloud-next') { + $path = "/home/clouduser/{$cloudEnvironment->name}/sites"; + } else { + $path = "/mnt/files/$sitegroup.{$cloudEnvironment->name}/sites"; + } + return $path; + } + + protected function promptChooseAcsfSite(EnvironmentResponse $cloudEnvironment): mixed + { + $choices = []; + $acsfSites = $this->getAcsfSites($cloudEnvironment); + foreach ($acsfSites['sites'] as $domain => $acsfSite) { + $choices[] = "{$acsfSite['name']} ($domain)"; + } + $choice = $this->io->choice('Choose a site', $choices, $choices[0]); + $key = array_search($choice, $choices, true); + $sites = array_values($acsfSites['sites']); + $site = $sites[$key]; + + return $site['name']; + } + + protected function promptChooseCloudSite(EnvironmentResponse $cloudEnvironment): mixed + { + $sites = $this->getCloudSites($cloudEnvironment); + if (count($sites) === 1) { + $site = reset($sites); + $this->logger->debug("Only a single Cloud site was detected. Assuming site is $site"); + return $site; + } + $this->logger->debug("Multisite detected"); + $this->warnMultisite(); + return $this->io->choice('Choose a site', $sites, $sites[0]); + } + + protected static function isLandoEnv(): bool + { + return AcquiaDrupalEnvironmentDetector::isLandoEnv(); + } + + protected function reAuthenticate(string $apiKey, string $apiSecret, ?string $baseUri, ?string $accountsUri): void + { + // Client service needs to be reinitialized with new credentials in case + // this is being run as a sub-command. + // @see https://github.com/acquia/cli/issues/403 + $this->cloudApiClientService->setConnector(new Connector([ + 'key' => $apiKey, + 'secret' => $apiSecret, + ], $baseUri, $accountsUri)); + } + + private function warnMultisite(): void + { + $this->io->note("This is a multisite application. Drupal will load the default site unless you've configured sites.php for this environment: https://docs.acquia.com/cloud-platform/develop/drupal/multisite/"); + } + + protected function setDirAndRequireProjectCwd(InputInterface $input): void + { + $this->determineDir($input); + if ($this->dir !== '/home/ide/project' && AcquiaDrupalEnvironmentDetector::isAhIdeEnv()) { + throw new AcquiaCliException('Run this command from the {dir} directory', ['dir' => '/home/ide/project']); + } + } + + protected function determineDir(InputInterface $input): void + { + if (isset($this->dir)) { + return; + } + + if ($input->hasOption('dir') && $dir = $input->getOption('dir')) { + $this->dir = $dir; + } elseif ($this->projectDir) { + $this->dir = $this->projectDir; + } else { + $this->dir = getcwd(); + } + } + + protected function getOutputCallback(OutputInterface $output, Checklist $checklist): Closure + { + return static function (mixed $type, mixed $buffer) use ($checklist, $output): void { + if (!$output->isVerbose() && $checklist->getItems()) { + $checklist->updateProgressBar($buffer); + } + $output->writeln($buffer, OutputInterface::VERBOSITY_VERY_VERBOSE); + }; + } + + protected function executeAllScripts(Closure $outputCallback, Checklist $checklist): void + { + $this->runComposerScripts($outputCallback, $checklist); + $this->runDrushCacheClear($outputCallback, $checklist); + $this->runDrushSqlSanitize($outputCallback, $checklist); + } + + protected function runComposerScripts(callable $outputCallback, Checklist $checklist): void + { + if (!file_exists(Path::join($this->dir, 'composer.json'))) { + $this->io->note('composer.json file not found. Skipping composer install.'); + return; + } + if (!$this->localMachineHelper->commandExists('composer')) { + $this->io->note('Composer not found. Skipping composer install.'); + return; + } + if (file_exists(Path::join($this->dir, 'vendor'))) { + $this->io->note('Composer dependencies already installed. Skipping composer install.'); + return; + } + $checklist->addItem("Installing Composer dependencies"); + $this->composerInstall($outputCallback); + $checklist->completePreviousItem(); + } + + protected function runDrushCacheClear(Closure $outputCallback, Checklist $checklist): void + { + if ($this->getDrushDatabaseConnectionStatus()) { + $checklist->addItem('Clearing Drupal caches via Drush'); + // @todo Add support for Drush 8. + $process = $this->localMachineHelper->execute([ + 'drush', + 'cache:rebuild', + '--yes', + '--no-interaction', + '--verbose', + ], $outputCallback, $this->dir, false); + if (!$process->isSuccessful()) { + throw new AcquiaCliException('Unable to rebuild Drupal caches via Drush. {message}', ['message' => $process->getErrorOutput()]); + } + $checklist->completePreviousItem(); + } else { + $this->logger->notice('Drush does not have an active database connection. Skipping cache:rebuild'); + } + } + + protected function runDrushSqlSanitize(Closure $outputCallback, Checklist $checklist): void + { + if ($this->getDrushDatabaseConnectionStatus()) { + $checklist->addItem('Sanitizing database via Drush'); + $process = $this->localMachineHelper->execute([ + 'drush', + 'sql:sanitize', + '--yes', + '--no-interaction', + '--verbose', + ], $outputCallback, $this->dir, false); + if (!$process->isSuccessful()) { + throw new AcquiaCliException('Unable to sanitize Drupal database via Drush. {message}', ['message' => $process->getErrorOutput()]); + } + $checklist->completePreviousItem(); + $this->io->newLine(); + $this->io->text('Your database was sanitized via drush sql:sanitize. This has changed all user passwords to randomly generated strings. To log in to your Drupal site, use drush uli'); + } else { + $this->logger->notice('Drush does not have an active database connection. Skipping sql:sanitize.'); + } + } + + private function composerInstall(?callable $outputCallback): void + { + $process = $this->localMachineHelper->execute([ + 'composer', + 'install', '--no-interaction', - ], $outputCallback, $this->dir, FALSE); - if ($process->isSuccessful()) { - $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; - } - } - } - - $this->drushHasActiveDatabaseConnection = FALSE; - - return $this->drushHasActiveDatabaseConnection; - } - - protected function createMySqlDumpOnLocal(string $dbHost, string $dbUser, string $dbName, string $dbPassword, Closure $outputCallback = NULL): string { - $this->localMachineHelper->checkRequiredBinariesExist(['mysqldump', 'gzip']); - $filename = "acli-mysql-dump-{$dbName}.sql.gz"; - $localTempDir = sys_get_temp_dir(); - $localFilepath = $localTempDir . '/' . $filename; - $this->logger->debug("Dumping MySQL database to $localFilepath on this machine"); - $this->localMachineHelper->checkRequiredBinariesExist(['mysqldump', 'gzip']); - if ($outputCallback) { - $outputCallback('out', "Dumping MySQL database to $localFilepath on this machine"); - } - if ($this->localMachineHelper->commandExists('pv')) { - $command = "MYSQL_PWD={$dbPassword} mysqldump --host={$dbHost} --user={$dbUser} {$dbName} | pv --rate --bytes | gzip -9 > $localFilepath"; - } - else { - $this->io->warning('Install `pv` to see progress bar'); - $command = "MYSQL_PWD={$dbPassword} mysqldump --host={$dbHost} --user={$dbUser} {$dbName} | gzip -9 > $localFilepath"; - } - - $process = $this->localMachineHelper->executeFromCmd($command, $outputCallback, NULL, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL)); - if (!$process->isSuccessful() || $process->getOutput()) { - throw new AcquiaCliException('Unable to create a dump of the local database. {message}', ['message' => $process->getErrorOutput()]); - } - - return $localFilepath; - } - - /** @infection-ignore-all */ - protected function promptOpenBrowserToCreateToken( - InputInterface $input - ): void { - if (!$input->getOption('key') || !$input->getOption('secret')) { - $tokenUrl = 'https://cloud.acquia.com/a/profile/tokens'; - $this->output->writeln("You will need a Cloud Platform API token from $tokenUrl"); - - if (!AcquiaDrupalEnvironmentDetector::isAhIdeEnv() && $this->io->confirm('Do you want to open this page to generate a token now?')) { - $this->localMachineHelper->startBrowser($tokenUrl); - } - } - } - - protected function determineApiKey(): string { - return $this->determineOption('key', FALSE, $this->validateApiKey(...)); - } - - private function validateApiKey(mixed $key): string { - $violations = Validation::createValidator()->validate($key, [ - new Length(['min' => 10]), - new NotBlank(), - new Regex(['pattern' => '/^\S*$/', 'message' => 'The value may not contain spaces']), - ]); - if (count($violations)) { - throw new ValidatorException($violations->get(0)->getMessage()); - } - return $key; - } - - protected function determineApiSecret(): string { - return $this->determineOption('secret', TRUE, $this->validateApiKey(...)); - } - - /** - * Get an option, either passed explicitly or via interactive prompt. - * - * Default can be passed explicitly, separately from the option default, - * because Symfony does not make a distinction between an option value set - * explicitly or by default. In other words, we can't prompt for the value of - * an option that already has a default value. - */ - protected function determineOption(string $optionName, bool $hidden = FALSE, ?Closure $validator = NULL, ?Closure $normalizer = NULL, string|bool|null $default = NULL): string|int|bool|null { - if ($optionValue = $this->input->getOption($optionName)) { - if (isset($normalizer)) { - $optionValue = $normalizer($optionValue); - } - if (isset($validator)) { - $validator($optionValue); - } - return $optionValue; - } - $option = $this->getDefinition()->getOption($optionName); - if ($option->isNegatable() && $this->input->getOption("no-$optionName")) { - return FALSE; - } - $optionShortcut = $option->getShortcut(); - $description = lcfirst($option->getDescription()); - if ($optionShortcut) { - $optionString = "option -$optionShortcut, --$optionName"; - } - else { - $optionString = "option --$optionName"; - } - if ($option->acceptValue()) { - $message = "Enter $description ($optionString)"; - } - else { - $message = "Do you want to $description ($optionString)?"; - } - $optional = $option->isValueOptional(); - $message .= $optional ? ' (optional)' : ''; - $message .= $hidden ? ' (input will be hidden)' : ''; - if ($option->acceptValue()) { - $question = new Question($message, $default); - } - else { - $question = new ConfirmationQuestion($message, $default); - } - $question->setHidden($this->localMachineHelper->useTty() && $hidden); - $question->setHiddenFallback($hidden); - if (isset($normalizer)) { - $question->setNormalizer($normalizer); - } - if (isset($validator)) { - $question->setValidator($validator); - } - $optionValue = $this->io->askQuestion($question); - // Question bypasses validation if session is non-interactive. - if (!$optional && is_null($optionValue)) { - throw new AcquiaCliException($message); - } - return $optionValue; - } - - /** - * Get the first environment for a given Cloud application matching a filter. - */ - private function getAnyAhEnvironment(string $cloudAppUuid, callable $filter): EnvironmentResponse|false { - $acquiaCloudClient = $this->cloudApiClientService->getClient(); - $environmentResource = new Environments($acquiaCloudClient); - /** @var EnvironmentResponse[] $applicationEnvironments */ - $applicationEnvironments = iterator_to_array($environmentResource->getAll($cloudAppUuid)); - $candidates = array_filter($applicationEnvironments, $filter); - return reset($candidates); - } - - /** - * Get the first non-prod environment for a given Cloud application. - */ - protected function getAnyNonProdAhEnvironment(string $cloudAppUuid): EnvironmentResponse|false { - return $this->getAnyAhEnvironment($cloudAppUuid, function (EnvironmentResponse $environment) { - return !$environment->flags->production && $environment->type === 'drupal'; - }); - } - - /** - * Get the first prod environment for a given Cloud application. - */ - protected function getAnyProdAhEnvironment(string $cloudAppUuid): EnvironmentResponse|false { - return $this->getAnyAhEnvironment($cloudAppUuid, function (EnvironmentResponse $environment) { - return $environment->flags->production && $environment->type === 'drupal'; - }); - } - - /** - * Get the first VCS URL for a given Cloud application. - */ - protected function getAnyVcsUrl(string $cloudAppUuid): string { - $environment = $this->getAnyAhEnvironment($cloudAppUuid, function (): bool { - return TRUE; - }); - return $environment->vcs->url; - } - - protected function validateApplicationUuid(string $applicationUuidArgument): mixed { - try { - self::validateUuid($applicationUuidArgument); - } - catch (ValidatorException) { - // Since this isn't a valid UUID, let's see if it's a valid alias. - $alias = $this->normalizeAlias($applicationUuidArgument); - return $this->getApplicationFromAlias($alias)->uuid; - } - return $applicationUuidArgument; - } - - protected function validateEnvironmentUuid(mixed $envUuidArgument, mixed $argumentName): string { - if (is_null($envUuidArgument)) { - throw new AcquiaCliException("{{$argumentName}} must not be null"); - } - try { - // Environment IDs take the form of [env-num]-[app-uuid]. - $uuidParts = explode('-', $envUuidArgument); - unset($uuidParts[0]); - $applicationUuid = implode('-', $uuidParts); - self::validateUuid($applicationUuid); - } - catch (ValidatorException) { - try { - // Since this isn't a valid environment ID, let's see if it's a valid alias. - $alias = $envUuidArgument; - $alias = $this->normalizeAlias($alias); - $alias = self::validateEnvironmentAlias($alias); - return $this->getEnvironmentFromAliasArg($alias)->uuid; - } - catch (AcquiaCliException) { - throw new AcquiaCliException("{{$argumentName}} must be a valid UUID or site alias."); - } - } - return $envUuidArgument; - } - - protected function checkAuthentication(): void { - if ((new \ReflectionClass(static::class))->getAttributes(RequireAuth::class) && !$this->cloudApiClientService->isMachineAuthenticated()) { - if ($this->cloudApiClientService instanceof AcsfClientService) { - throw new AcquiaCliException('This machine is not yet authenticated with Site Factory.'); - } - throw new AcquiaCliException('This machine is not yet authenticated with the Cloud Platform.'); - } - } - - protected function waitForNotificationToComplete(Client $acquiaCloudClient, string $uuid, string $message, callable $success = NULL): bool { - $notificationsResource = new Notifications($acquiaCloudClient); - $notification = NULL; - $checkNotificationStatus = static function () use ($notificationsResource, &$notification, $uuid): bool { - $notification = $notificationsResource->get($uuid); - // @infection-ignore-all - return $notification->status !== 'in-progress'; - }; - if ($success === NULL) { - $success = function () use (&$notification): void { - $this->writeCompletedMessage($notification); - }; - } - LoopHelper::getLoopy($this->output, $this->io, $message, $checkNotificationStatus, $success); - return $notification->status === 'completed'; - } - - private function writeCompletedMessage(NotificationResponse $notification): void { - if ($notification->status === 'completed') { - $this->io->success("The task with notification uuid {$notification->uuid} completed"); - } - else if ($notification->status === 'failed') { - $this->io->error("The task with notification uuid {$notification->uuid} failed"); - } - else { - throw new AcquiaCliException("Unknown task status: {$notification->status}"); - } - $duration = strtotime($notification->completed_at) - strtotime($notification->created_at); - $completedAt = date("D M j G:i:s T Y", strtotime($notification->completed_at)); - $this->io->writeln("Progress: {$notification->progress}"); - $this->io->writeln("Completed: $completedAt"); - $this->io->writeln("Task type: {$notification->label}"); - $this->io->writeln("Duration: $duration seconds"); - } - - protected static function getNotificationUuidFromResponse(object $response): string { - if (property_exists($response, 'links')) { - $links = $response->links; - } - elseif (property_exists($response, '_links')) { - $links = $response->_links; - } - 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 { - $permissions = $acquiaCloudClient->request('get', "/applications/{$cloudApplicationUuid}/permissions"); - $keyedPermissions = []; - foreach ($permissions as $permission) { - $keyedPermissions[$permission->name] = $permission; - } - foreach ($requiredPermissions as $name) { - if (!array_key_exists($name, $keyedPermissions)) { - throw new AcquiaCliException("The Acquia Cloud Platform account {account} does not have the required '{name}' permission. Add the permissions to this user or use an API Token belonging to a different Acquia Cloud Platform user.", [ - 'account' => $account->mail, - 'name' => $name, + ], $outputCallback, $this->dir, false); + if (!$process->isSuccessful()) { + throw new AcquiaCliException( + 'Unable to install Drupal dependencies via Composer. {message}', + ['message' => $process->getErrorOutput()] + ); + } + } + + protected function getDrushDatabaseConnectionStatus(Closure $outputCallback = null): bool + { + if (isset($this->drushHasActiveDatabaseConnection)) { + return $this->drushHasActiveDatabaseConnection; + } + if ($this->localMachineHelper->commandExists('drush')) { + $process = $this->localMachineHelper->execute([ + 'drush', + 'status', + '--fields=db-status,drush-version', + '--format=json', + '--no-interaction', + ], $outputCallback, $this->dir, false); + if ($process->isSuccessful()) { + $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; + } + } + } + + $this->drushHasActiveDatabaseConnection = false; + + return $this->drushHasActiveDatabaseConnection; + } + + protected function createMySqlDumpOnLocal(string $dbHost, string $dbUser, string $dbName, string $dbPassword, Closure $outputCallback = null): string + { + $this->localMachineHelper->checkRequiredBinariesExist(['mysqldump', 'gzip']); + $filename = "acli-mysql-dump-{$dbName}.sql.gz"; + $localTempDir = sys_get_temp_dir(); + $localFilepath = $localTempDir . '/' . $filename; + $this->logger->debug("Dumping MySQL database to $localFilepath on this machine"); + $this->localMachineHelper->checkRequiredBinariesExist(['mysqldump', 'gzip']); + if ($outputCallback) { + $outputCallback('out', "Dumping MySQL database to $localFilepath on this machine"); + } + if ($this->localMachineHelper->commandExists('pv')) { + $command = "MYSQL_PWD={$dbPassword} mysqldump --host={$dbHost} --user={$dbUser} {$dbName} | pv --rate --bytes | gzip -9 > $localFilepath"; + } else { + $this->io->warning('Install `pv` to see progress bar'); + $command = "MYSQL_PWD={$dbPassword} mysqldump --host={$dbHost} --user={$dbUser} {$dbName} | gzip -9 > $localFilepath"; + } + + $process = $this->localMachineHelper->executeFromCmd($command, $outputCallback, null, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL)); + if (!$process->isSuccessful() || $process->getOutput()) { + throw new AcquiaCliException('Unable to create a dump of the local database. {message}', ['message' => $process->getErrorOutput()]); + } + + return $localFilepath; + } + + /** @infection-ignore-all */ + protected function promptOpenBrowserToCreateToken( + InputInterface $input + ): void { + if (!$input->getOption('key') || !$input->getOption('secret')) { + $tokenUrl = 'https://cloud.acquia.com/a/profile/tokens'; + $this->output->writeln("You will need a Cloud Platform API token from $tokenUrl"); + + if (!AcquiaDrupalEnvironmentDetector::isAhIdeEnv() && $this->io->confirm('Do you want to open this page to generate a token now?')) { + $this->localMachineHelper->startBrowser($tokenUrl); + } + } + } + + protected function determineApiKey(): string + { + return $this->determineOption('key', false, $this->validateApiKey(...)); + } + + private function validateApiKey(mixed $key): string + { + $violations = Validation::createValidator()->validate($key, [ + new Length(['min' => 10]), + new NotBlank(), + new Regex(['pattern' => '/^\S*$/', 'message' => 'The value may not contain spaces']), ]); - } + if (count($violations)) { + throw new ValidatorException($violations->get(0)->getMessage()); + } + return $key; + } + + protected function determineApiSecret(): string + { + return $this->determineOption('secret', true, $this->validateApiKey(...)); + } + + /** + * Get an option, either passed explicitly or via interactive prompt. + * + * Default can be passed explicitly, separately from the option default, + * because Symfony does not make a distinction between an option value set + * explicitly or by default. In other words, we can't prompt for the value of + * an option that already has a default value. + */ + protected function determineOption(string $optionName, bool $hidden = false, ?Closure $validator = null, ?Closure $normalizer = null, string|bool|null $default = null): string|int|bool|null + { + if ($optionValue = $this->input->getOption($optionName)) { + if (isset($normalizer)) { + $optionValue = $normalizer($optionValue); + } + if (isset($validator)) { + $validator($optionValue); + } + return $optionValue; + } + $option = $this->getDefinition()->getOption($optionName); + if ($option->isNegatable() && $this->input->getOption("no-$optionName")) { + return false; + } + $optionShortcut = $option->getShortcut(); + $description = lcfirst($option->getDescription()); + if ($optionShortcut) { + $optionString = "option -$optionShortcut, --$optionName"; + } else { + $optionString = "option --$optionName"; + } + if ($option->acceptValue()) { + $message = "Enter $description ($optionString)"; + } else { + $message = "Do you want to $description ($optionString)?"; + } + $optional = $option->isValueOptional(); + $message .= $optional ? ' (optional)' : ''; + $message .= $hidden ? ' (input will be hidden)' : ''; + if ($option->acceptValue()) { + $question = new Question($message, $default); + } else { + $question = new ConfirmationQuestion($message, $default); + } + $question->setHidden($this->localMachineHelper->useTty() && $hidden); + $question->setHiddenFallback($hidden); + if (isset($normalizer)) { + $question->setNormalizer($normalizer); + } + if (isset($validator)) { + $question->setValidator($validator); + } + $optionValue = $this->io->askQuestion($question); + // Question bypasses validation if session is non-interactive. + if (!$optional && is_null($optionValue)) { + throw new AcquiaCliException($message); + } + return $optionValue; + } + + /** + * Get the first environment for a given Cloud application matching a filter. + */ + private function getAnyAhEnvironment(string $cloudAppUuid, callable $filter): EnvironmentResponse|false + { + $acquiaCloudClient = $this->cloudApiClientService->getClient(); + $environmentResource = new Environments($acquiaCloudClient); + /** @var EnvironmentResponse[] $applicationEnvironments */ + $applicationEnvironments = iterator_to_array($environmentResource->getAll($cloudAppUuid)); + $candidates = array_filter($applicationEnvironments, $filter); + return reset($candidates); + } + + /** + * Get the first non-prod environment for a given Cloud application. + */ + protected function getAnyNonProdAhEnvironment(string $cloudAppUuid): EnvironmentResponse|false + { + return $this->getAnyAhEnvironment($cloudAppUuid, function (EnvironmentResponse $environment) { + return !$environment->flags->production && $environment->type === 'drupal'; + }); + } + + /** + * Get the first prod environment for a given Cloud application. + */ + protected function getAnyProdAhEnvironment(string $cloudAppUuid): EnvironmentResponse|false + { + return $this->getAnyAhEnvironment($cloudAppUuid, function (EnvironmentResponse $environment) { + return $environment->flags->production && $environment->type === 'drupal'; + }); + } + + /** + * Get the first VCS URL for a given Cloud application. + */ + protected function getAnyVcsUrl(string $cloudAppUuid): string + { + $environment = $this->getAnyAhEnvironment($cloudAppUuid, function (): bool { + return true; + }); + return $environment->vcs->url; + } + + protected function validateApplicationUuid(string $applicationUuidArgument): mixed + { + try { + self::validateUuid($applicationUuidArgument); + } catch (ValidatorException) { + // Since this isn't a valid UUID, let's see if it's a valid alias. + $alias = $this->normalizeAlias($applicationUuidArgument); + return $this->getApplicationFromAlias($alias)->uuid; + } + return $applicationUuidArgument; + } + + protected function validateEnvironmentUuid(mixed $envUuidArgument, mixed $argumentName): string + { + if (is_null($envUuidArgument)) { + throw new AcquiaCliException("{{$argumentName}} must not be null"); + } + try { + // Environment IDs take the form of [env-num]-[app-uuid]. + $uuidParts = explode('-', $envUuidArgument); + unset($uuidParts[0]); + $applicationUuid = implode('-', $uuidParts); + self::validateUuid($applicationUuid); + } catch (ValidatorException) { + try { + // Since this isn't a valid environment ID, let's see if it's a valid alias. + $alias = $envUuidArgument; + $alias = $this->normalizeAlias($alias); + $alias = self::validateEnvironmentAlias($alias); + return $this->getEnvironmentFromAliasArg($alias)->uuid; + } catch (AcquiaCliException) { + throw new AcquiaCliException("{{$argumentName}} must be a valid UUID or site alias."); + } + } + return $envUuidArgument; } - } - protected function validatePhpVersion(string $version): string { - $violations = Validation::createValidator()->validate($version, [ - new Length(['min' => 3]), - new NotBlank(), - new Regex(['pattern' => '/^\S*$/', 'message' => 'The value may not contain spaces']), - new Regex(['pattern' => '/[0-9]{1}\.[0-9]{1}/', 'message' => 'The value must be in the format "x.y"']), - ]); - if (count($violations)) { - throw new ValidatorException($violations->get(0)->getMessage()); + protected function checkAuthentication(): void + { + if ((new \ReflectionClass(static::class))->getAttributes(RequireAuth::class) && !$this->cloudApiClientService->isMachineAuthenticated()) { + if ($this->cloudApiClientService instanceof AcsfClientService) { + throw new AcquiaCliException('This machine is not yet authenticated with Site Factory.'); + } + throw new AcquiaCliException('This machine is not yet authenticated with the Cloud Platform.'); + } } - return $version; - } + protected function waitForNotificationToComplete(Client $acquiaCloudClient, string $uuid, string $message, callable $success = null): bool + { + $notificationsResource = new Notifications($acquiaCloudClient); + $notification = null; + $checkNotificationStatus = static function () use ($notificationsResource, &$notification, $uuid): bool { + $notification = $notificationsResource->get($uuid); + // @infection-ignore-all + return $notification->status !== 'in-progress'; + }; + if ($success === null) { + $success = function () use (&$notification): void { + $this->writeCompletedMessage($notification); + }; + } + LoopHelper::getLoopy($this->output, $this->io, $message, $checkNotificationStatus, $success); + return $notification->status === 'completed'; + } + + private function writeCompletedMessage(NotificationResponse $notification): void + { + if ($notification->status === 'completed') { + $this->io->success("The task with notification uuid {$notification->uuid} completed"); + } elseif ($notification->status === 'failed') { + $this->io->error("The task with notification uuid {$notification->uuid} failed"); + } else { + throw new AcquiaCliException("Unknown task status: {$notification->status}"); + } + $duration = strtotime($notification->completed_at) - strtotime($notification->created_at); + $completedAt = date("D M j G:i:s T Y", strtotime($notification->completed_at)); + $this->io->writeln("Progress: {$notification->progress}"); + $this->io->writeln("Completed: $completedAt"); + $this->io->writeln("Task type: {$notification->label}"); + $this->io->writeln("Duration: $duration seconds"); + } + + protected static function getNotificationUuidFromResponse(object $response): string + { + if (property_exists($response, 'links')) { + $links = $response->links; + } elseif (property_exists($response, '_links')) { + $links = $response->_links; + } 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'); + } - protected function promptChooseDrupalSite(EnvironmentResponse $environment): string { - if ($this->isAcsfEnv($environment)) { - return $this->promptChooseAcsfSite($environment); + 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'); } - return $this->promptChooseCloudSite($environment); - } + protected function validateRequiredCloudPermissions(Client $acquiaCloudClient, ?string $cloudApplicationUuid, AccountResponse $account, array $requiredPermissions): void + { + $permissions = $acquiaCloudClient->request('get', "/applications/{$cloudApplicationUuid}/permissions"); + $keyedPermissions = []; + foreach ($permissions as $permission) { + $keyedPermissions[$permission->name] = $permission; + } + foreach ($requiredPermissions as $name) { + if (!array_key_exists($name, $keyedPermissions)) { + throw new AcquiaCliException("The Acquia Cloud Platform account {account} does not have the required '{name}' permission. Add the permissions to this user or use an API Token belonging to a different Acquia Cloud Platform user.", [ + 'account' => $account->mail, + 'name' => $name, + ]); + } + } + } + + protected function validatePhpVersion(string $version): string + { + $violations = Validation::createValidator()->validate($version, [ + new Length(['min' => 3]), + new NotBlank(), + new Regex(['pattern' => '/^\S*$/', 'message' => 'The value may not contain spaces']), + new Regex(['pattern' => '/[0-9]{1}\.[0-9]{1}/', 'message' => 'The value must be in the format "x.y"']), + ]); + if (count($violations)) { + throw new ValidatorException($violations->get(0)->getMessage()); + } + return $version; + } + + protected function promptChooseDrupalSite(EnvironmentResponse $environment): string + { + if ($this->isAcsfEnv($environment)) { + return $this->promptChooseAcsfSite($environment); + } + + return $this->promptChooseCloudSite($environment); + } } diff --git a/src/Command/DocsCommand.php b/src/Command/DocsCommand.php index ab9f1fb74..4512bafac 100644 --- a/src/Command/DocsCommand.php +++ b/src/Command/DocsCommand.php @@ -1,6 +1,6 @@ addArgument('product', InputArgument::OPTIONAL, 'Acquia Product Name') - ->addUsage('acli'); - } +final class DocsCommand extends CommandBase +{ + protected function configure(): void + { + $this + ->addArgument('product', InputArgument::OPTIONAL, 'Acquia Product Name') + ->addUsage('acli'); + } - protected function execute(InputInterface $input, OutputInterface $output): int { - $acquiaProducts = [ - 'Acquia CLI' => [ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $acquiaProducts = [ + 'Acquia CLI' => [ 'alias' => ['cli', 'acli'], 'url' => 'acquia-cli', - ], - 'Acquia CMS' => [ + ], + 'Acquia CMS' => [ 'alias' => ['acquia_cms', 'acms'], 'url' => 'acquia-cms', - ], - 'Acquia DAM Classic' => [ + ], + 'Acquia DAM Classic' => [ 'alias' => ['dam', 'acquia_dam', 'dam_classic', 'acquiadam', 'damclassic'], 'url' => 'dam', - ], - 'Acquia Migrate Accelerate' => [ + ], + 'Acquia Migrate Accelerate' => [ 'alias' => ['acquia-migrate-accelerate', 'ama'], 'url' => 'acquia-migrate-accelerate', - ], - 'BLT' => [ + ], + 'BLT' => [ 'alias' => ['blt'], 'url' => 'blt', - ], - 'Campaign Factory' => [ + ], + 'Campaign Factory' => [ 'alias' => ['campaign-factory', 'campaign_factory', 'campaignfactory'], 'url' => 'campaign-factory', - ], - 'Campaign Studio' => [ + ], + 'Campaign Studio' => [ 'alias' => ['campaign-studio', 'campaignstudio'], 'url' => 'campaign-studio', - ], - 'Cloud IDE' => [ + ], + 'Cloud IDE' => [ 'alias' => ['ide', 'cloud_ide', 'cloud-ide'], 'url' => 'ide', - ], - 'Cloud Platform' => [ + ], + 'Cloud Platform' => [ 'alias' => ['cloud-platform', 'acquiacloud', 'acquia_cloud', 'acquia-cloud', 'cloud'], 'url' => 'cloud-platform', - ], - 'Code Studio' => [ + ], + 'Code Studio' => [ 'alias' => ['code_studio', 'codestudio', 'cs'], 'url' => 'code-studio', - ], - 'Content Hub' => [ + ], + 'Content Hub' => [ 'alias' => ['contenthub', 'ch'], 'url' => 'contenthub', - ], - 'Customer Data Platform' => [ + ], + 'Customer Data Platform' => [ 'alias' => ['customer-data-platform', 'cdp'], 'url' => 'customer-data-platform', - ], - 'Edge' => [ + ], + 'Edge' => [ 'alias' => ['edge', 'cloudedge'], 'url' => 'edge', - ], - 'Personalization' => [ + ], + 'Personalization' => [ 'alias' => ['personalization'], 'url' => 'personalization', - ], - 'Search' => [ + ], + 'Search' => [ 'alias' => ['search', 'acquia-search'], 'url' => 'acquia-search', - ], - 'Shield' => [ + ], + 'Shield' => [ 'alias' => ['shield'], 'url' => 'shield', - ], - 'Site Factory' => [ + ], + 'Site Factory' => [ 'alias' => ['site-factory', 'acsf'], 'url' => 'site-factory', - ], - 'Site Studio' => [ + ], + 'Site Studio' => [ 'alias' => ['site-studio', 'cohesion'], 'url' => 'site-studio', - ], - ]; + ], + ]; - // If user has provided any acquia product in command. - if ($acquiaProductName = $input->getArgument('product')) { - $productUrl = NULL; - foreach ($acquiaProducts as $acquiaProduct) { - // If product provided by the user exists in the alias. - if (in_array(strtolower($acquiaProductName), $acquiaProduct['alias'], TRUE)) { - $productUrl = $acquiaProduct['url']; - break; + // If user has provided any acquia product in command. + if ($acquiaProductName = $input->getArgument('product')) { + $productUrl = null; + foreach ($acquiaProducts as $acquiaProduct) { + // If product provided by the user exists in the alias. + if (in_array(strtolower($acquiaProductName), $acquiaProduct['alias'], true)) { + $productUrl = $acquiaProduct['url']; + break; + } + } + + if ($productUrl) { + $this->localMachineHelper->startBrowser('https://docs.acquia.com/' . $productUrl . '/'); + return Command::SUCCESS; + } } - } - if ($productUrl) { - $this->localMachineHelper->startBrowser('https://docs.acquia.com/' . $productUrl . '/'); + $labels = array_keys($acquiaProducts); + $question = new ChoiceQuestion('Select the Acquia Product', $labels, $labels[0]); + $choiceId = $this->io->askQuestion($question); + $this->localMachineHelper->startBrowser('https://docs.acquia.com/' . $acquiaProducts[$choiceId]['url'] . '/'); + return Command::SUCCESS; - } } - - $labels = array_keys($acquiaProducts); - $question = new ChoiceQuestion('Select the Acquia Product', $labels, $labels[0]); - $choiceId = $this->io->askQuestion($question); - $this->localMachineHelper->startBrowser('https://docs.acquia.com/' . $acquiaProducts[$choiceId]['url'] . '/'); - - return Command::SUCCESS; - } - } diff --git a/src/Command/Email/ConfigurePlatformEmailCommand.php b/src/Command/Email/ConfigurePlatformEmailCommand.php index 3075dd46a..7f7a7e44e 100644 --- a/src/Command/Email/ConfigurePlatformEmailCommand.php +++ b/src/Command/Email/ConfigurePlatformEmailCommand.php @@ -1,6 +1,6 @@ addArgument('subscriptionUuid', InputArgument::OPTIONAL, 'The subscription UUID to register the domain with.') - ->setHelp('This command configures Platform Email for a domain in a subscription. It registers the domain with the subscription, associates the domain with an application or set of applications, and enables Platform Email for selected environments of these applications.'); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $this->io->writeln('Welcome to Platform Email setup! This script will walk you through the whole process setting up Platform Email, all through the command line and using the Cloud API!'); - $this->io->writeln('Before getting started, make sure you have the following: '); - - $checklist = new Checklist($output); - $checklist->addItem('the domain name you are registering'); - $checklist->completePreviousItem(); - $checklist->addItem('the subscription where the domain will be registered'); - $checklist->completePreviousItem(); - $checklist->addItem('the application or applications where the domain will be associated'); - $checklist->completePreviousItem(); - $checklist->addItem('the environment or environments for the above applications where Platform Email will be enabled'); - $checklist->completePreviousItem(); - $baseDomain = $this->determineDomain(); - $client = $this->cloudApiClientService->getClient(); - $subscription = $this->determineCloudSubscription(); - $client->request('post', "/subscriptions/$subscription->uuid/domains", [ - 'form_params' => [ - 'domain' => $baseDomain, - ], - ]); - - $domainUuid = $this->fetchDomainUuid($client, $subscription, $baseDomain); - - $this->io->success([ - "Great! You've registered the domain $baseDomain to subscription $subscription->name.", - "We will create a file with the DNS records for your newly registered domain", - "Provide these records to your DNS provider", - "After you've done this, continue to domain verification.", - ]); - $fileFormat = $this->io->choice('Would you like your DNS records in BIND Zone File, JSON, or YAML format?', ['BIND Zone File', 'YAML', 'JSON'], 'BIND Zone File'); - $this->createDnsText($client, $subscription, $baseDomain, $domainUuid, $fileFormat); - $continue = $this->io->confirm('Have you finished providing the DNS records to your DNS provider?'); - if (!$continue) { - $this->io->info("Make sure to give these records to your DNS provider, then rerun this script with the domain that you just registered."); - return 1; +final class ConfigurePlatformEmailCommand extends CommandBase +{ + protected function configure(): void + { + $this + ->addArgument('subscriptionUuid', InputArgument::OPTIONAL, 'The subscription UUID to register the domain with.') + ->setHelp('This command configures Platform Email for a domain in a subscription. It registers the domain with the subscription, associates the domain with an application or set of applications, and enables Platform Email for selected environments of these applications.'); } - // Allow for as many reverification tries as needed. - while (!$this->checkIfDomainVerified($subscription, $domainUuid)) { - $retryVerification = $this->io->confirm('Would you like to re-check domain verification?'); - if (!$retryVerification) { - $this->io->writeln('Check your DNS records with your DNS provider and try again by rerunning this script with the domain that you just registered.'); - return 1; - } - } + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->io->writeln('Welcome to Platform Email setup! This script will walk you through the whole process setting up Platform Email, all through the command line and using the Cloud API!'); + $this->io->writeln('Before getting started, make sure you have the following: '); + + $checklist = new Checklist($output); + $checklist->addItem('the domain name you are registering'); + $checklist->completePreviousItem(); + $checklist->addItem('the subscription where the domain will be registered'); + $checklist->completePreviousItem(); + $checklist->addItem('the application or applications where the domain will be associated'); + $checklist->completePreviousItem(); + $checklist->addItem('the environment or environments for the above applications where Platform Email will be enabled'); + $checklist->completePreviousItem(); + $baseDomain = $this->determineDomain(); + $client = $this->cloudApiClientService->getClient(); + $subscription = $this->determineCloudSubscription(); + $client->request('post', "/subscriptions/$subscription->uuid/domains", [ + 'form_params' => [ + 'domain' => $baseDomain, + ], + ]); + + $domainUuid = $this->fetchDomainUuid($client, $subscription, $baseDomain); + + $this->io->success([ + "Great! You've registered the domain $baseDomain to subscription $subscription->name.", + "We will create a file with the DNS records for your newly registered domain", + "Provide these records to your DNS provider", + "After you've done this, continue to domain verification.", + ]); + $fileFormat = $this->io->choice('Would you like your DNS records in BIND Zone File, JSON, or YAML format?', ['BIND Zone File', 'YAML', 'JSON'], 'BIND Zone File'); + $this->createDnsText($client, $subscription, $baseDomain, $domainUuid, $fileFormat); + $continue = $this->io->confirm('Have you finished providing the DNS records to your DNS provider?'); + if (!$continue) { + $this->io->info("Make sure to give these records to your DNS provider, then rerun this script with the domain that you just registered."); + return 1; + } - $this->io->success("The next step is associating your verified domain with an application (or applications) in the subscription where your domain has been registered."); + // Allow for as many reverification tries as needed. + while (!$this->checkIfDomainVerified($subscription, $domainUuid)) { + $retryVerification = $this->io->confirm('Would you like to re-check domain verification?'); + if (!$retryVerification) { + $this->io->writeln('Check your DNS records with your DNS provider and try again by rerunning this script with the domain that you just registered.'); + return 1; + } + } - if (!$this->addDomainToSubscriptionApplications($client, $subscription, $baseDomain, $domainUuid)) { - $this->io->error('Something went wrong with associating your application(s) or enabling your environment(s). Try again.'); - return 1; - } + $this->io->success("The next step is associating your verified domain with an application (or applications) in the subscription where your domain has been registered."); - $this->io->success("You're all set to start using Platform Email!"); - - return Command::SUCCESS; - } - - /** - * Generates Zone File for DNS records of the registered domain. - */ - private function generateZoneFile(string $baseDomain, array $records): void { - - $zone = new Zone($baseDomain . '.'); - - foreach ($records as $record) { - unset($record->health); - $recordToAdd = $zone->getNode($record->name . '.'); - - switch ($record->type) { - case 'MX': - $mxPriorityValueArr = explode(' ', $record->value); - $recordToAdd->getRecordAppender()->appendMxRecord((int) $mxPriorityValueArr[0], $mxPriorityValueArr[1] . '.', 3600); - break; - case 'TXT': - $recordToAdd->getRecordAppender()->appendTxtRecord($record->value, 3600); - break; - case 'CNAME': - $recordToAdd->getRecordAppender()->appendCNameRecord($record->value . '.', 3600); - break; - } - } + if (!$this->addDomainToSubscriptionApplications($client, $subscription, $baseDomain, $domainUuid)) { + $this->io->error('Something went wrong with associating your application(s) or enabling your environment(s). Try again.'); + return 1; + } - $this->localMachineHelper->getFilesystem() - ->dumpFile('dns-records.zone', (string) $zone); + $this->io->success("You're all set to start using Platform Email!"); - } + return Command::SUCCESS; + } - /** - * Determines the applications for domain association and environment - * enablement of Platform Email. - * - * @return array - */ - private function determineApplications(Client $client, SubscriptionResponse $subscription): array { - $subscriptionApplications = $this->getSubscriptionApplications($client, $subscription); + /** + * Generates Zone File for DNS records of the registered domain. + */ + private function generateZoneFile(string $baseDomain, array $records): void + { + + $zone = new Zone($baseDomain . '.'); + + foreach ($records as $record) { + unset($record->health); + $recordToAdd = $zone->getNode($record->name . '.'); + + switch ($record->type) { + case 'MX': + $mxPriorityValueArr = explode(' ', $record->value); + $recordToAdd->getRecordAppender()->appendMxRecord((int) $mxPriorityValueArr[0], $mxPriorityValueArr[1] . '.', 3600); + break; + case 'TXT': + $recordToAdd->getRecordAppender()->appendTxtRecord($record->value, 3600); + break; + case 'CNAME': + $recordToAdd->getRecordAppender()->appendCNameRecord($record->value . '.', 3600); + break; + } + } - if (count($subscriptionApplications) === 1) { - $applications = $subscriptionApplications; - $this->io->info("You have one application, {$applications[0]->name}, in this subscription."); + $this->localMachineHelper->getFilesystem() + ->dumpFile('dns-records.zone', (string) $zone); } - else { - $applications = $this->promptChooseFromObjectsOrArrays($subscriptionApplications, 'uuid', 'name', "What are the applications you'd like to associate this domain with? You may enter multiple separated by a comma.", TRUE); + + /** + * Determines the applications for domain association and environment + * enablement of Platform Email. + * + * @return array + */ + private function determineApplications(Client $client, SubscriptionResponse $subscription): array + { + $subscriptionApplications = $this->getSubscriptionApplications($client, $subscription); + + if (count($subscriptionApplications) === 1) { + $applications = $subscriptionApplications; + $this->io->info("You have one application, {$applications[0]->name}, in this subscription."); + } else { + $applications = $this->promptChooseFromObjectsOrArrays($subscriptionApplications, 'uuid', 'name', "What are the applications you'd like to associate this domain with? You may enter multiple separated by a comma.", true); + } + return $applications; } - return $applications; - } - - /** - * Checks any error from Cloud API when associating a domain with an - * application. Shows a warning and allows user to continue if the domain has - * been associated already. For any other error from the API, the setup will - * exit. - */ - private function domainAlreadyAssociated(object $application, ApiErrorException $exception): ?bool { - if (!str_contains($exception->getMessage(), 'is already associated with this application')) { - $this->io->error($exception->getMessage()); - return FALSE; + + /** + * Checks any error from Cloud API when associating a domain with an + * application. Shows a warning and allows user to continue if the domain has + * been associated already. For any other error from the API, the setup will + * exit. + */ + private function domainAlreadyAssociated(object $application, ApiErrorException $exception): ?bool + { + if (!str_contains($exception->getMessage(), 'is already associated with this application')) { + $this->io->error($exception->getMessage()); + return false; + } + + $this->io->warning($application->name . ' - ' . $exception->getMessage()); + return true; } - $this->io->warning($application->name . ' - ' . $exception->getMessage()); - return TRUE; - } - - /** - * Checks any error from Cloud API when enabling Platform Email for an - * environment. Shows a warning and allows user to continue if Platform Email - * has already been enabled for the environment. For any other error from the - * API, the setup will exit. - */ - private function environmentAlreadyEnabled(object $environment, ApiErrorException $exception): ?bool { - if (!str_contains($exception->getMessage(), 'is already enabled on this environment')) { - $this->io->error($exception->getMessage()); - return FALSE; + /** + * Checks any error from Cloud API when enabling Platform Email for an + * environment. Shows a warning and allows user to continue if Platform Email + * has already been enabled for the environment. For any other error from the + * API, the setup will exit. + */ + private function environmentAlreadyEnabled(object $environment, ApiErrorException $exception): ?bool + { + if (!str_contains($exception->getMessage(), 'is already enabled on this environment')) { + $this->io->error($exception->getMessage()); + return false; + } + + $this->io->warning($environment->label . ' - ' . $exception->getMessage()); + return true; } - $this->io->warning($environment->label . ' - ' . $exception->getMessage()); - return TRUE; - } - - /** - * Associates a domain with an application or applications, - * then enables Platform Email for an environment or environments - * of the above applications. - */ - private function addDomainToSubscriptionApplications(Client $client, SubscriptionResponse $subscription, string $baseDomain, string $domainUuid): bool { - $applications = $this->determineApplications($client, $subscription); - - $environmentsResource = new Environments($client); - foreach ($applications as $application) { - try { - $client->request('post', "/applications/$application->uuid/email/domains/$domainUuid/actions/associate"); - $this->io->success("Domain $baseDomain has been associated with Application $application->name"); - } - catch (ApiErrorException $e) { - if (!$this->domainAlreadyAssociated($application, $e)) { - return FALSE; + /** + * Associates a domain with an application or applications, + * then enables Platform Email for an environment or environments + * of the above applications. + */ + private function addDomainToSubscriptionApplications(Client $client, SubscriptionResponse $subscription, string $baseDomain, string $domainUuid): bool + { + $applications = $this->determineApplications($client, $subscription); + + $environmentsResource = new Environments($client); + foreach ($applications as $application) { + try { + $client->request('post', "/applications/$application->uuid/email/domains/$domainUuid/actions/associate"); + $this->io->success("Domain $baseDomain has been associated with Application $application->name"); + } catch (ApiErrorException $e) { + if (!$this->domainAlreadyAssociated($application, $e)) { + return false; + } + } + + $applicationEnvironments = $environmentsResource->getAll($application->uuid); + $envs = $this->promptChooseFromObjectsOrArrays( + $applicationEnvironments, + 'uuid', + 'label', + "What are the environments of $application->name that you'd like to enable email for? You may enter multiple separated by a comma.", + true + ); + foreach ($envs as $env) { + try { + $client->request('post', "/environments/$env->uuid/email/actions/enable"); + $this->io->success("Platform Email has been enabled for environment $env->label for application $application->name"); + } catch (ApiErrorException $e) { + if (!$this->environmentAlreadyEnabled($env, $e)) { + return false; + } + } + } } - } - - $applicationEnvironments = $environmentsResource->getAll($application->uuid); - $envs = $this->promptChooseFromObjectsOrArrays( - $applicationEnvironments, - 'uuid', - 'label', - "What are the environments of $application->name that you'd like to enable email for? You may enter multiple separated by a comma.", - TRUE - ); - foreach ($envs as $env) { - try { - $client->request('post', "/environments/$env->uuid/email/actions/enable"); - $this->io->success("Platform Email has been enabled for environment $env->label for application $application->name"); + return true; + } + + /** + * Validates the URL entered as the base domain name. + */ + public static function validateUrl(string $url): string + { + $constraintsList = [new NotBlank()]; + $urlParts = parse_url($url); + if (array_key_exists('host', $urlParts)) { + $constraintsList[] = new Url(); + } else { + $constraintsList[] = new Hostname(); } - catch (ApiErrorException $e) { - if (!$this->environmentAlreadyEnabled($env, $e)) { - return FALSE; - } + $violations = Validation::createValidator()->validate($url, $constraintsList); + if (count($violations)) { + throw new ValidatorException($violations->get(0)->getMessage()); } - } - } - return TRUE; - } - - /** - * Validates the URL entered as the base domain name. - */ - public static function validateUrl(string $url): string { - $constraintsList = [new NotBlank()]; - $urlParts = parse_url($url); - if (array_key_exists('host', $urlParts)) { - $constraintsList[] = new Url(); - } - else { - $constraintsList[] = new Hostname(); - } - $violations = Validation::createValidator()->validate($url, $constraintsList); - if (count($violations)) { - throw new ValidatorException($violations->get(0)->getMessage()); - } - return $url; - } - - /** - * Retrieves a domain registration UUID given the domain name. - */ - private function fetchDomainUuid(Client $client, SubscriptionResponse $subscription, string $baseDomain): mixed { - $domainsResponse = $client->request('get', "/subscriptions/$subscription->uuid/domains"); - foreach ($domainsResponse as $domain) { - if ($domain->domain_name === $baseDomain) { - return $domain->uuid; - } + return $url; } - throw new AcquiaCliException("Could not find domain $baseDomain"); - } - - /** - * Creates a file, either in Bind Zone File, JSON or YAML format, - * of the DNS records needed to complete Platform Email setup. - */ - private function createDnsText(Client $client, SubscriptionResponse $subscription, string $baseDomain, string $domainUuid, string $fileFormat): void { - $domainRegistrationResponse = $client->request('get', "/subscriptions/$subscription->uuid/domains/$domainUuid"); - if (!isset($domainRegistrationResponse->dns_records)) { - throw new AcquiaCliException('Could not retrieve DNS records for this domain. Try again by rerunning this script with the domain that you just registered.'); + + /** + * Retrieves a domain registration UUID given the domain name. + */ + private function fetchDomainUuid(Client $client, SubscriptionResponse $subscription, string $baseDomain): mixed + { + $domainsResponse = $client->request('get', "/subscriptions/$subscription->uuid/domains"); + foreach ($domainsResponse as $domain) { + if ($domain->domain_name === $baseDomain) { + return $domain->uuid; + } + } + throw new AcquiaCliException("Could not find domain $baseDomain"); } - $records = []; - $this->localMachineHelper->getFilesystem()->remove('dns-records.json'); - $this->localMachineHelper->getFilesystem()->remove('dns-records.yaml'); - $this->localMachineHelper->getFilesystem()->remove('dns-records.zone'); - if ($fileFormat === 'JSON') { - foreach ($domainRegistrationResponse->dns_records as $record) { - unset($record->health); - $records[] = $record; - } - $this->logger->debug(json_encode($records, JSON_THROW_ON_ERROR)); - $this->localMachineHelper->getFilesystem() + + /** + * Creates a file, either in Bind Zone File, JSON or YAML format, + * of the DNS records needed to complete Platform Email setup. + */ + private function createDnsText(Client $client, SubscriptionResponse $subscription, string $baseDomain, string $domainUuid, string $fileFormat): void + { + $domainRegistrationResponse = $client->request('get', "/subscriptions/$subscription->uuid/domains/$domainUuid"); + if (!isset($domainRegistrationResponse->dns_records)) { + throw new AcquiaCliException('Could not retrieve DNS records for this domain. Try again by rerunning this script with the domain that you just registered.'); + } + $records = []; + $this->localMachineHelper->getFilesystem()->remove('dns-records.json'); + $this->localMachineHelper->getFilesystem()->remove('dns-records.yaml'); + $this->localMachineHelper->getFilesystem()->remove('dns-records.zone'); + if ($fileFormat === 'JSON') { + foreach ($domainRegistrationResponse->dns_records as $record) { + unset($record->health); + $records[] = $record; + } + $this->logger->debug(json_encode($records, JSON_THROW_ON_ERROR)); + $this->localMachineHelper->getFilesystem() ->dumpFile('dns-records.json', json_encode($records, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); - } - else if ($fileFormat === 'YAML') { - foreach ($domainRegistrationResponse->dns_records as $record) { - unset($record->health); - $records[] = ['type' => $record->type, 'name' => $record->name, 'value' => $record->value]; - } - $this->logger->debug(json_encode($records, JSON_THROW_ON_ERROR)); - $this->localMachineHelper->getFilesystem() + } elseif ($fileFormat === 'YAML') { + foreach ($domainRegistrationResponse->dns_records as $record) { + unset($record->health); + $records[] = ['type' => $record->type, 'name' => $record->name, 'value' => $record->value]; + } + $this->logger->debug(json_encode($records, JSON_THROW_ON_ERROR)); + $this->localMachineHelper->getFilesystem() ->dumpFile('dns-records.yaml', Yaml::dump($records)); - } - else { - $this->generateZoneFile($baseDomain, $domainRegistrationResponse->dns_records); + } else { + $this->generateZoneFile($baseDomain, $domainRegistrationResponse->dns_records); + } } - } - - /** - * Checks the verification status of the registered domain. - */ - private function checkIfDomainVerified( - SubscriptionResponse $subscription, - string $domainUuid - ): bool { - $client = $this->cloudApiClientService->getClient(); - try { - $response = $client->request('get', "/subscriptions/$subscription->uuid/domains/$domainUuid"); - if (isset($response->health) && $response->health->code === "200") { - $this->io->success("Your domain is ready for use!"); - return TRUE; - } - - // @infection-ignore-all - if (isset($response->health) && str_starts_with($response->health->code, "4")) { - $this->io->error($response->health->details); - if ($this->io->confirm('Would you like to refresh?')) { - $client->request('post', "/subscriptions/$subscription->uuid/domains/$domainUuid/actions/verify"); - $this->io->info('Refreshing...'); + /** + * Checks the verification status of the registered domain. + */ + private function checkIfDomainVerified( + SubscriptionResponse $subscription, + string $domainUuid + ): bool { + $client = $this->cloudApiClientService->getClient(); + try { + $response = $client->request('get', "/subscriptions/$subscription->uuid/domains/$domainUuid"); + if (isset($response->health) && $response->health->code === "200") { + $this->io->success("Your domain is ready for use!"); + return true; + } + + // @infection-ignore-all + if (isset($response->health) && str_starts_with($response->health->code, "4")) { + $this->io->error($response->health->details); + if ($this->io->confirm('Would you like to refresh?')) { + $client->request('post', "/subscriptions/$subscription->uuid/domains/$domainUuid/actions/verify"); + $this->io->info('Refreshing...'); + } + } + + $this->io->info("Verification pending..."); + $this->logger->debug(json_encode($response, JSON_THROW_ON_ERROR)); + return false; + } catch (AcquiaCliException $exception) { + $this->logger->debug($exception->getMessage()); + return false; } - } - - $this->io->info("Verification pending..."); - $this->logger->debug(json_encode($response, JSON_THROW_ON_ERROR)); - return FALSE; - } - catch (AcquiaCliException $exception) { - $this->logger->debug($exception->getMessage()); - return FALSE; } - } - - /** - * Finds, validates, and trims the URL to be used as the base domain - * for setting up Platform Email. - */ - private function determineDomain(): string { - $domain = $this->io->ask("What's the domain name you'd like to register?", '', Closure::fromCallable([ - $this, - 'validateUrl', - ])); - - $domainParts = parse_url($domain); - if (array_key_exists('host', $domainParts)) { - $return = $domainParts['host']; - } - else { - $return = $domain; - } - return str_replace('www.', '', $return); - } + /** + * Finds, validates, and trims the URL to be used as the base domain + * for setting up Platform Email. + */ + private function determineDomain(): string + { + $domain = $this->io->ask("What's the domain name you'd like to register?", '', Closure::fromCallable([ + $this, + 'validateUrl', + ])); + + $domainParts = parse_url($domain); + if (array_key_exists('host', $domainParts)) { + $return = $domainParts['host']; + } else { + $return = $domain; + } + return str_replace('www.', '', $return); + } } diff --git a/src/Command/Email/EmailInfoForSubscriptionCommand.php b/src/Command/Email/EmailInfoForSubscriptionCommand.php index 11196015f..f4b6fcd3e 100644 --- a/src/Command/Email/EmailInfoForSubscriptionCommand.php +++ b/src/Command/Email/EmailInfoForSubscriptionCommand.php @@ -1,6 +1,6 @@ addArgument('subscriptionUuid', InputArgument::OPTIONAL, 'The subscription UUID whose Platform Email configuration is to be checked.') - ->setHelp('This command lists information related to Platform Email for a subscription, including which domains have been validated, which have not, and which applications have Platform Email domains associated.'); - } +final class EmailInfoForSubscriptionCommand extends CommandBase +{ + protected function configure(): void + { + $this + ->addArgument('subscriptionUuid', InputArgument::OPTIONAL, 'The subscription UUID whose Platform Email configuration is to be checked.') + ->setHelp('This command lists information related to Platform Email for a subscription, including which domains have been validated, which have not, and which applications have Platform Email domains associated.'); + } - protected function execute(InputInterface $input, OutputInterface $output): int { + protected function execute(InputInterface $input, OutputInterface $output): int + { - $client = $this->cloudApiClientService->getClient(); - $subscription = $this->determineCloudSubscription(); + $client = $this->cloudApiClientService->getClient(); + $subscription = $this->determineCloudSubscription(); - $response = $client->request('get', "/subscriptions/$subscription->uuid/domains"); + $response = $client->request('get', "/subscriptions/$subscription->uuid/domains"); - if (count($response)) { + if (count($response)) { + $this->localMachineHelper->getFilesystem()->remove("./subscription-$subscription->uuid-domains"); + $this->localMachineHelper->getFilesystem()->mkdir("./subscription-$subscription->uuid-domains"); - $this->localMachineHelper->getFilesystem()->remove("./subscription-$subscription->uuid-domains"); - $this->localMachineHelper->getFilesystem()->mkdir("./subscription-$subscription->uuid-domains"); + $this->writeDomainsToTables($output, $subscription, $response); - $this->writeDomainsToTables($output, $subscription, $response); + $subscriptionApplications = $this->validateSubscriptionApplicationCount($client, $subscription); - $subscriptionApplications = $this->validateSubscriptionApplicationCount($client, $subscription); + if (!isset($subscriptionApplications)) { + return 1; + } - if (!isset($subscriptionApplications)) { - return 1; - } + $this->renderApplicationAssociations($output, $client, $subscription, $subscriptionApplications); - $this->renderApplicationAssociations($output, $client, $subscription, $subscriptionApplications); + $this->output->writeln("CSV files with these tables have been exported to /subscription-$subscription->uuid-domains. A detailed breakdown of each domain's DNS records has been exported there as well."); + } else { + $this->io->info("No email domains registered in $subscription->name."); + } - $this->output->writeln("CSV files with these tables have been exported to /subscription-$subscription->uuid-domains. A detailed breakdown of each domain's DNS records has been exported there as well."); - } - else { - $this->io->info("No email domains registered in $subscription->name."); + return Command::SUCCESS; } - return Command::SUCCESS; - } - - /** - * Renders tables showing email domain verification statuses, - * as well as exports these statuses to respective CSV files. - */ - private function writeDomainsToTables(OutputInterface $output, SubscriptionResponse $subscription, array $domainList): void { - - // Initialize tables to be displayed in console. - $allDomainsTable = $this->createTotalDomainTable($output, "Subscription $subscription->name - All Domains"); - $verifiedDomainsTable = $this->createDomainStatusTable($output, "Subscription $subscription->name - Verified Domains"); - $pendingDomainsTable = $this->createDomainStatusTable($output, "Subscription $subscription->name - Pending Domains"); - $failedDomainsTable = $this->createDomainStatusTable($output, "Subscription $subscription->name - Failed Domains"); - - // Initialize csv writers for each file. - $writerAllDomains = Writer::createFromPath("./subscription-$subscription->uuid-domains/all-domains-summary.csv", 'w+'); - $writerVerifiedDomains = Writer::createFromPath("./subscription-$subscription->uuid-domains/verified-domains-summary.csv", 'w+'); - $writerPendingDomains = Writer::createFromPath("./subscription-$subscription->uuid-domains/pending-domains-summary.csv", 'w+'); - $writerFailedDomains = Writer::createFromPath("./subscription-$subscription->uuid-domains/failed-domains-summary.csv", 'w+'); - $writerAllDomainsDnsHealth = Writer::createFromPath("./subscription-$subscription->uuid-domains/all-domains-dns-health.csv", 'w+'); - - $allDomainsSummaryHeader = ['Domain Name', 'Domain UUID', 'Verification Status']; - $writerAllDomains->insertOne($allDomainsSummaryHeader); - - $verifiedDomainsHeader = ['Domain Name', 'Summary']; - $writerVerifiedDomains->insertOne($verifiedDomainsHeader); - - $pendingDomainsHeader = $verifiedDomainsHeader; - $writerPendingDomains->insertOne($pendingDomainsHeader); - - $failedDomainsHeader = $verifiedDomainsHeader; - $writerFailedDomains->insertOne($failedDomainsHeader); - - $allDomainsDnsHealthCsvHeader = ['Domain Name', 'Domain UUID', 'Domain Health', 'DNS Record Name', 'DNS Record Type', 'DNS Record Value', 'DNS Record Health Details']; - $writerAllDomainsDnsHealth->insertOne($allDomainsDnsHealthCsvHeader); - - foreach ($domainList as $domain) { - $domainNameAndSummary = [$domain->domain_name, $domain->health->summary]; - - if ($domain->health->code === '200') { - $verifiedDomainsTable->addRow($domainNameAndSummary); - $writerVerifiedDomains->insertOne($domainNameAndSummary); - } - else if ($domain->health->code === '202') { - $pendingDomainsTable->addRow($domainNameAndSummary); - $writerPendingDomains->insertOne($domainNameAndSummary); - } - else { - $failedDomainsTable->addRow($domainNameAndSummary); - $writerFailedDomains->insertOne($domainNameAndSummary); - } - - $allDomainsTable->addRow([ - $domain->domain_name, - $domain->uuid, - $this->showHumanReadableStatus($domain->health->code) . ' - ' . $domain->health->code, - ]); - - $writerAllDomains->insertOne([ - $domain->domain_name, - $domain->uuid, - $this->showHumanReadableStatus($domain->health->code) . ' - ' . $domain->health->code, - ]); - - foreach ($domain->dns_records as $index => $record) { - if ($index === 0) { - $writerAllDomainsDnsHealth->insertOne([ + /** + * Renders tables showing email domain verification statuses, + * as well as exports these statuses to respective CSV files. + */ + private function writeDomainsToTables(OutputInterface $output, SubscriptionResponse $subscription, array $domainList): void + { + + // Initialize tables to be displayed in console. + $allDomainsTable = $this->createTotalDomainTable($output, "Subscription $subscription->name - All Domains"); + $verifiedDomainsTable = $this->createDomainStatusTable($output, "Subscription $subscription->name - Verified Domains"); + $pendingDomainsTable = $this->createDomainStatusTable($output, "Subscription $subscription->name - Pending Domains"); + $failedDomainsTable = $this->createDomainStatusTable($output, "Subscription $subscription->name - Failed Domains"); + + // Initialize csv writers for each file. + $writerAllDomains = Writer::createFromPath("./subscription-$subscription->uuid-domains/all-domains-summary.csv", 'w+'); + $writerVerifiedDomains = Writer::createFromPath("./subscription-$subscription->uuid-domains/verified-domains-summary.csv", 'w+'); + $writerPendingDomains = Writer::createFromPath("./subscription-$subscription->uuid-domains/pending-domains-summary.csv", 'w+'); + $writerFailedDomains = Writer::createFromPath("./subscription-$subscription->uuid-domains/failed-domains-summary.csv", 'w+'); + $writerAllDomainsDnsHealth = Writer::createFromPath("./subscription-$subscription->uuid-domains/all-domains-dns-health.csv", 'w+'); + + $allDomainsSummaryHeader = ['Domain Name', 'Domain UUID', 'Verification Status']; + $writerAllDomains->insertOne($allDomainsSummaryHeader); + + $verifiedDomainsHeader = ['Domain Name', 'Summary']; + $writerVerifiedDomains->insertOne($verifiedDomainsHeader); + + $pendingDomainsHeader = $verifiedDomainsHeader; + $writerPendingDomains->insertOne($pendingDomainsHeader); + + $failedDomainsHeader = $verifiedDomainsHeader; + $writerFailedDomains->insertOne($failedDomainsHeader); + + $allDomainsDnsHealthCsvHeader = ['Domain Name', 'Domain UUID', 'Domain Health', 'DNS Record Name', 'DNS Record Type', 'DNS Record Value', 'DNS Record Health Details']; + $writerAllDomainsDnsHealth->insertOne($allDomainsDnsHealthCsvHeader); + + foreach ($domainList as $domain) { + $domainNameAndSummary = [$domain->domain_name, $domain->health->summary]; + + if ($domain->health->code === '200') { + $verifiedDomainsTable->addRow($domainNameAndSummary); + $writerVerifiedDomains->insertOne($domainNameAndSummary); + } elseif ($domain->health->code === '202') { + $pendingDomainsTable->addRow($domainNameAndSummary); + $writerPendingDomains->insertOne($domainNameAndSummary); + } else { + $failedDomainsTable->addRow($domainNameAndSummary); + $writerFailedDomains->insertOne($domainNameAndSummary); + } + + $allDomainsTable->addRow([ + $domain->domain_name, + $domain->uuid, + $this->showHumanReadableStatus($domain->health->code) . ' - ' . $domain->health->code, + ]); + + $writerAllDomains->insertOne([ $domain->domain_name, $domain->uuid, $this->showHumanReadableStatus($domain->health->code) . ' - ' . $domain->health->code, - $record->name, - $record->type, - $record->value, - $record->health->details, - ]); + ]); + + foreach ($domain->dns_records as $index => $record) { + if ($index === 0) { + $writerAllDomainsDnsHealth->insertOne([ + $domain->domain_name, + $domain->uuid, + $this->showHumanReadableStatus($domain->health->code) . ' - ' . $domain->health->code, + $record->name, + $record->type, + $record->value, + $record->health->details, + ]); + } else { + $writerAllDomainsDnsHealth->insertOne([ + '', + '', + '', + $record->name, + $record->type, + $record->value, + $record->health->details, + ]); + } + } } - else { - $writerAllDomainsDnsHealth->insertOne([ - '', - '', - '', - $record->name, - $record->type, - $record->value, - $record->health->details, - ]); + + $this->renderDomainInfoTables([$allDomainsTable, $verifiedDomainsTable, $pendingDomainsTable, $failedDomainsTable]); + } + + /** + * Nicely renders a given array of tables. + */ + private function renderDomainInfoTables(array $tables): void + { + foreach ($tables as $table) { + $table->render(); + $this->io->newLine(); } - } } - $this->renderDomainInfoTables([$allDomainsTable, $verifiedDomainsTable, $pendingDomainsTable, $failedDomainsTable]); + /** + * Verifies the number of applications present in a subscription. + * + * @return array|null + */ + private function validateSubscriptionApplicationCount(Client $client, SubscriptionResponse $subscription): ?array + { + $subscriptionApplications = $this->getSubscriptionApplications($client, $subscription); + if (count($subscriptionApplications) > 100) { + $this->io->warning('You have over 100 applications in this subscription. Retrieving the email domains for each could take a while!'); + $continue = $this->io->confirm('Do you wish to continue?'); + if (!$continue) { + return null; + } + } - } + return $subscriptionApplications; + } + + /** + * Renders a table of applications in a subscription and the email domains + * associated or dissociated with each application. + * + * @param $subscription + * @param $subscriptionApplications + */ + private function renderApplicationAssociations(OutputInterface $output, Client $client, \AcquiaCloudApi\Response\SubscriptionResponse $subscription, array $subscriptionApplications): void + { + $appsDomainsTable = $this->createApplicationDomainsTable($output); + $writerAppsDomains = Writer::createFromPath("./subscription-$subscription->uuid-domains/apps-domain-associations.csv", 'w+'); + + $appsDomainsHeader = ['Application', 'Domain Name', 'Associated?']; + $writerAppsDomains->insertOne($appsDomainsHeader); + + foreach ($subscriptionApplications as $index => $app) { + $appDomains = $client->request('get', "/applications/$app->uuid/email/domains"); + + if ($index !== 0) { + $appsDomainsTable->addRow([new TableSeparator(['colspan' => 2])]); + } + $appsDomainsTable->addRow([new TableCell("Application: $app->name", ['colspan' => 2])]); + if (count($appDomains)) { + foreach ($appDomains as $domain) { + $appsDomainsTable->addRow([ + $domain->domain_name, + var_export($domain->flags->associated, true), + ]); + $writerAppsDomains->insertOne([$app->name, $domain->domain_name, var_export($domain->flags->associated, true)]); + } + } else { + $appsDomainsTable->addRow([new TableCell("No domains eligible for association.", [ + 'colspan' => 2, + 'style' => new TableCellStyle([ + 'fg' => 'yellow', + ]), + ]), + ]); + $writerAppsDomains->insertOne([$app->name, 'No domains eligible for association', '']); + } + } + $appsDomainsTable->render(); + } - /** - * Nicely renders a given array of tables. - */ - private function renderDomainInfoTables(array $tables): void { - foreach ($tables as $table) { - $table->render(); - $this->io->newLine(); + /** + * Creates a table of all domains registered in a subscription. + */ + private function createTotalDomainTable(OutputInterface $output, string $title): Table + { + $headers = ['Domain Name', 'Domain UUID', 'Verification Status']; + $widths = [.2, .2, .1]; + return $this->createTable($output, $title, $headers, $widths); } - } - - /** - * Verifies the number of applications present in a subscription. - * - * @return array|null - */ - private function validateSubscriptionApplicationCount(Client $client, SubscriptionResponse $subscription): ?array { - $subscriptionApplications = $this->getSubscriptionApplications($client, $subscription); - if (count($subscriptionApplications) > 100) { - $this->io->warning('You have over 100 applications in this subscription. Retrieving the email domains for each could take a while!'); - $continue = $this->io->confirm('Do you wish to continue?'); - if (!$continue) { - return NULL; - } + + /** + * Creates a table of domains of one verification status in a subscription. + */ + private function createDomainStatusTable(OutputInterface $output, string $title): Table + { + $headers = ['Domain Name', 'Summary']; + $widths = [.2, .2]; + return $this->createTable($output, $title, $headers, $widths); } - return $subscriptionApplications; - } - - /** - * Renders a table of applications in a subscription and the email domains - * associated or dissociated with each application. - * - * @param $subscription - * @param $subscriptionApplications - */ - private function renderApplicationAssociations(OutputInterface $output, Client $client, \AcquiaCloudApi\Response\SubscriptionResponse $subscription, array $subscriptionApplications): void { - $appsDomainsTable = $this->createApplicationDomainsTable($output); - $writerAppsDomains = Writer::createFromPath("./subscription-$subscription->uuid-domains/apps-domain-associations.csv", 'w+'); - - $appsDomainsHeader = ['Application', 'Domain Name', 'Associated?']; - $writerAppsDomains->insertOne($appsDomainsHeader); - - foreach ($subscriptionApplications as $index => $app) { - $appDomains = $client->request('get', "/applications/$app->uuid/email/domains"); - - if ($index !== 0) { - $appsDomainsTable->addRow([new TableSeparator(['colspan' => 2])]); - } - $appsDomainsTable->addRow([new TableCell("Application: $app->name", ['colspan' => 2])]); - if (count($appDomains)) { - foreach ($appDomains as $domain) { - $appsDomainsTable->addRow([ - $domain->domain_name, - var_export($domain->flags->associated, TRUE), - ]); - $writerAppsDomains->insertOne([$app->name, $domain->domain_name, var_export($domain->flags->associated, TRUE)]); - } - } - else { - $appsDomainsTable->addRow([new TableCell("No domains eligible for association.", [ - 'colspan' => 2, - 'style' => new TableCellStyle([ - 'fg' => 'yellow', - ]), - ]), - ]); - $writerAppsDomains->insertOne([$app->name, 'No domains eligible for association', '']); - } + /** + * Creates a table of applications in a subscription and the associated + * or dissociated domains in each application. + */ + private function createApplicationDomainsTable(OutputInterface $output): Table + { + $headers = ['Domain Name', 'Associated?']; + $widths = [.2, .1]; + return $this->createTable($output, 'Domain Association Status', $headers, $widths); } - $appsDomainsTable->render(); - } - - /** - * Creates a table of all domains registered in a subscription. - */ - private function createTotalDomainTable(OutputInterface $output, string $title): Table { - $headers = ['Domain Name', 'Domain UUID', 'Verification Status']; - $widths = [.2, .2, .1]; - return $this->createTable($output, $title, $headers, $widths); - } - - /** - * Creates a table of domains of one verification status in a subscription. - */ - private function createDomainStatusTable(OutputInterface $output, string $title): Table { - $headers = ['Domain Name', 'Summary']; - $widths = [.2, .2]; - return $this->createTable($output, $title, $headers, $widths); - } - - /** - * Creates a table of applications in a subscription and the associated - * or dissociated domains in each application. - */ - private function createApplicationDomainsTable(OutputInterface $output): Table { - $headers = ['Domain Name', 'Associated?']; - $widths = [.2, .1]; - return $this->createTable($output, 'Domain Association Status', $headers, $widths); - } - - /** - * Returns a human-readable string of whether a status code represents - * a failed, pending, or successful domain verification. - */ - private function showHumanReadableStatus(string $code): string { - return match ($code) { - '200' => "Succeeded", - '202' => "Pending", - default => "Failed", - }; - } + /** + * Returns a human-readable string of whether a status code represents + * a failed, pending, or successful domain verification. + */ + private function showHumanReadableStatus(string $code): string + { + return match ($code) { + '200' => "Succeeded", + '202' => "Pending", + default => "Failed", + }; + } } diff --git a/src/Command/Env/EnvCertCreateCommand.php b/src/Command/Env/EnvCertCreateCommand.php index a68051f82..2e94b0a51 100644 --- a/src/Command/Env/EnvCertCreateCommand.php +++ b/src/Command/Env/EnvCertCreateCommand.php @@ -1,6 +1,6 @@ addArgument('certificate', InputArgument::REQUIRED, 'Filename of the SSL certificate being installed') - ->addArgument('private-key', InputArgument::REQUIRED, 'Filename of the SSL private key') - ->addOption('legacy', '', InputOption::VALUE_OPTIONAL, 'True for legacy certificates', FALSE) - ->addOption('ca-certificates', '', InputOption::VALUE_OPTIONAL, 'Filename of the CA intermediary certificates') - ->addOption('csr-id', '', InputOption::VALUE_OPTIONAL, 'The CSR (certificate signing request) to associate with this certificate') - ->addOption('label', '', InputOption::VALUE_OPTIONAL, 'The label for this certificate. Required for standard certificates. Optional for legacy certificates', 'My certificate') - ->acceptEnvironmentId(); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $acquiaCloudClient = $this->cloudApiClientService->getClient(); - $environment = $this->determineEnvironment($input, $output, TRUE, TRUE); - $certificate = $input->getArgument('certificate'); - $privateKey = $input->getArgument('private-key'); - $label = $this->determineOption('label'); - $caCertificates = $this->determineOption('ca-certificates'); - $csrId = (int) $this->determineOption('csr-id'); - $legacy = $this->determineOption('legacy', FALSE, NULL, NULL, 'false'); - $legacy = filter_var($legacy, FILTER_VALIDATE_BOOLEAN); - - $sslCertificates = new SslCertificates($acquiaCloudClient); - $response = $sslCertificates->create( - $environment->uuid, - $label, - $this->localMachineHelper->readFile($certificate), - $this->localMachineHelper->readFile($privateKey), - $caCertificates ? $this->localMachineHelper->readFile($caCertificates) : NULL, - $csrId, - $legacy - ); - $notificationUuid = CommandBase::getNotificationUuidFromResponse($response); - $this->waitForNotificationToComplete($acquiaCloudClient, $notificationUuid, 'Installing certificate'); - return Command::SUCCESS; - } - +final class EnvCertCreateCommand extends CommandBase +{ + protected function configure(): void + { + $this + ->addArgument('certificate', InputArgument::REQUIRED, 'Filename of the SSL certificate being installed') + ->addArgument('private-key', InputArgument::REQUIRED, 'Filename of the SSL private key') + ->addOption('legacy', '', InputOption::VALUE_OPTIONAL, 'True for legacy certificates', false) + ->addOption('ca-certificates', '', InputOption::VALUE_OPTIONAL, 'Filename of the CA intermediary certificates') + ->addOption('csr-id', '', InputOption::VALUE_OPTIONAL, 'The CSR (certificate signing request) to associate with this certificate') + ->addOption('label', '', InputOption::VALUE_OPTIONAL, 'The label for this certificate. Required for standard certificates. Optional for legacy certificates', 'My certificate') + ->acceptEnvironmentId(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $acquiaCloudClient = $this->cloudApiClientService->getClient(); + $environment = $this->determineEnvironment($input, $output, true, true); + $certificate = $input->getArgument('certificate'); + $privateKey = $input->getArgument('private-key'); + $label = $this->determineOption('label'); + $caCertificates = $this->determineOption('ca-certificates'); + $csrId = (int) $this->determineOption('csr-id'); + $legacy = $this->determineOption('legacy', false, null, null, 'false'); + $legacy = filter_var($legacy, FILTER_VALIDATE_BOOLEAN); + + $sslCertificates = new SslCertificates($acquiaCloudClient); + $response = $sslCertificates->create( + $environment->uuid, + $label, + $this->localMachineHelper->readFile($certificate), + $this->localMachineHelper->readFile($privateKey), + $caCertificates ? $this->localMachineHelper->readFile($caCertificates) : null, + $csrId, + $legacy + ); + $notificationUuid = CommandBase::getNotificationUuidFromResponse($response); + $this->waitForNotificationToComplete($acquiaCloudClient, $notificationUuid, 'Installing certificate'); + return Command::SUCCESS; + } } diff --git a/src/Command/Env/EnvCopyCronCommand.php b/src/Command/Env/EnvCopyCronCommand.php index 1f018d268..d6b6923a8 100644 --- a/src/Command/Env/EnvCopyCronCommand.php +++ b/src/Command/Env/EnvCopyCronCommand.php @@ -1,6 +1,6 @@ addArgument('source_env', InputArgument::REQUIRED, 'Alias of the source environment in the format `app-name.env` or the environment uuid') - ->addArgument('dest_env', InputArgument::REQUIRED, 'Alias of the destination environment in the format `app-name.env` or the environment uuid') - ->addUsage(' ') - ->addUsage('myapp.dev myapp.prod') - ->addUsage('abcd1234-1111-2222-3333-0e02b2c3d470 efgh1234-1111-2222-3333-0e02b2c3d470'); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - // If both source and destination env inputs are same. - if ($input->getArgument('source_env') === $input->getArgument('dest_env')) { - $this->io->error('The source and destination environments can not be same.'); - return 1; +final class EnvCopyCronCommand extends CommandBase +{ + protected function configure(): void + { + $this + ->addArgument('source_env', InputArgument::REQUIRED, 'Alias of the source environment in the format `app-name.env` or the environment uuid') + ->addArgument('dest_env', InputArgument::REQUIRED, 'Alias of the destination environment in the format `app-name.env` or the environment uuid') + ->addUsage(' ') + ->addUsage('myapp.dev myapp.prod') + ->addUsage('abcd1234-1111-2222-3333-0e02b2c3d470 efgh1234-1111-2222-3333-0e02b2c3d470'); } - // Get source env alias. - $this->convertEnvironmentAliasToUuid($input, 'source_env'); - $sourceEnvId = $input->getArgument('source_env'); - - // Get destination env alias. - $this->convertEnvironmentAliasToUuid($input, 'dest_env'); - $destEnvId = $input->getArgument('dest_env'); - - // Get the cron resource. - $cronResource = new Crons($this->cloudApiClientService->getClient()); - $sourceEnvCronList = $cronResource->getAll($sourceEnvId); - - // Ask for confirmation before starting the copy. - $answer = $this->io->confirm('Are you sure you\'d like to copy the cron jobs from ' . $sourceEnvId . ' to ' . $destEnvId . '?'); - if (!$answer) { - return Command::SUCCESS; - } + protected function execute(InputInterface $input, OutputInterface $output): int + { + // If both source and destination env inputs are same. + if ($input->getArgument('source_env') === $input->getArgument('dest_env')) { + $this->io->error('The source and destination environments can not be same.'); + return 1; + } - $onlySystemCrons = TRUE; - foreach ($sourceEnvCronList as $cron) { - if (!$cron->flags->system) { - $onlySystemCrons = FALSE; - } - } + // Get source env alias. + $this->convertEnvironmentAliasToUuid($input, 'source_env'); + $sourceEnvId = $input->getArgument('source_env'); - // If source environment doesn't have any cron job or only - // has system crons. - if ($onlySystemCrons || $sourceEnvCronList->count() === 0) { - $this->io->error('There are no cron jobs in the source environment for copying.'); - return 1; - } + // Get destination env alias. + $this->convertEnvironmentAliasToUuid($input, 'dest_env'); + $destEnvId = $input->getArgument('dest_env'); - foreach ($sourceEnvCronList as $cron) { - // We don't copy the system cron as those should already be there - // when environment is provisioned. - if (!$cron->flags->system) { - $cronFrequency = implode(' ', [ - $cron->minute, - $cron->hour, - $cron->dayMonth, - $cron->month, - $cron->dayWeek, - ]); + // Get the cron resource. + $cronResource = new Crons($this->cloudApiClientService->getClient()); + $sourceEnvCronList = $cronResource->getAll($sourceEnvId); - $this->io->info('Copying the cron task "' . $cron->label . '" from ' . $sourceEnvId . ' to ' . $destEnvId); - try { - // Copying the cron on destination environment. - $cronResource->create( - $destEnvId, - $cron->command, - $cronFrequency, - $cron->label, - ); + // Ask for confirmation before starting the copy. + $answer = $this->io->confirm('Are you sure you\'d like to copy the cron jobs from ' . $sourceEnvId . ' to ' . $destEnvId . '?'); + if (!$answer) { + return Command::SUCCESS; + } + $onlySystemCrons = true; + foreach ($sourceEnvCronList as $cron) { + if (!$cron->flags->system) { + $onlySystemCrons = false; + } } - catch (Exception $e) { - $this->io->error('There was some error while copying the cron task "' . $cron->label . '"'); - // Log the error for debugging purpose. - $this->logger->debug('Error @error while copying the cron task @cron from @source env to @dest env', [ - '@cron' => $cron->label, - '@dest' => $destEnvId, - '@error' => $e->getMessage(), - '@source' => $sourceEnvId, - ]); - return 1; + + // If source environment doesn't have any cron job or only + // has system crons. + if ($onlySystemCrons || $sourceEnvCronList->count() === 0) { + $this->io->error('There are no cron jobs in the source environment for copying.'); + return 1; } - } - } - $this->io->success('Cron task copy is completed.'); - return Command::SUCCESS; - } + foreach ($sourceEnvCronList as $cron) { + // We don't copy the system cron as those should already be there + // when environment is provisioned. + if (!$cron->flags->system) { + $cronFrequency = implode(' ', [ + $cron->minute, + $cron->hour, + $cron->dayMonth, + $cron->month, + $cron->dayWeek, + ]); + + $this->io->info('Copying the cron task "' . $cron->label . '" from ' . $sourceEnvId . ' to ' . $destEnvId); + try { + // Copying the cron on destination environment. + $cronResource->create( + $destEnvId, + $cron->command, + $cronFrequency, + $cron->label, + ); + } catch (Exception $e) { + $this->io->error('There was some error while copying the cron task "' . $cron->label . '"'); + // Log the error for debugging purpose. + $this->logger->debug('Error @error while copying the cron task @cron from @source env to @dest env', [ + '@cron' => $cron->label, + '@dest' => $destEnvId, + '@error' => $e->getMessage(), + '@source' => $sourceEnvId, + ]); + return 1; + } + } + } + $this->io->success('Cron task copy is completed.'); + return Command::SUCCESS; + } } diff --git a/src/Command/Env/EnvCreateCommand.php b/src/Command/Env/EnvCreateCommand.php index ac22d29fe..3eb49463c 100644 --- a/src/Command/Env/EnvCreateCommand.php +++ b/src/Command/Env/EnvCreateCommand.php @@ -1,6 +1,6 @@ addArgument('label', InputArgument::REQUIRED, 'The label of the new environment'); - $this->addArgument('branch', InputArgument::OPTIONAL, 'The vcs path (git branch name) to deploy to the new environment'); - $this->acceptApplicationUuid(); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $this->output = $output; - $cloudAppUuid = $this->determineCloudApplication(TRUE); - $label = $input->getArgument('label'); - $acquiaCloudClient = $this->cloudApiClientService->getClient(); - $environmentsResource = new Environments($acquiaCloudClient); - $this->checklist = new Checklist($output); + protected function configure(): void + { + $this->addArgument('label', InputArgument::REQUIRED, 'The label of the new environment'); + $this->addArgument('branch', InputArgument::OPTIONAL, 'The vcs path (git branch name) to deploy to the new environment'); + $this->acceptApplicationUuid(); + } - $this->validateLabel($environmentsResource, $cloudAppUuid, $label); - $branch = $this->getBranch($acquiaCloudClient, $cloudAppUuid, $input); - $databaseNames = $this->getDatabaseNames($acquiaCloudClient, $cloudAppUuid); + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->output = $output; + $cloudAppUuid = $this->determineCloudApplication(true); + $label = $input->getArgument('label'); + $acquiaCloudClient = $this->cloudApiClientService->getClient(); + $environmentsResource = new Environments($acquiaCloudClient); + $this->checklist = new Checklist($output); - $this->checklist->addItem("Initiating environment creation"); - $response = $environmentsResource->create($cloudAppUuid, $label, $branch, $databaseNames); - $notificationUuid = CommandBase::getNotificationUuidFromResponse($response); - $this->checklist->completePreviousItem(); + $this->validateLabel($environmentsResource, $cloudAppUuid, $label); + $branch = $this->getBranch($acquiaCloudClient, $cloudAppUuid, $input); + $databaseNames = $this->getDatabaseNames($acquiaCloudClient, $cloudAppUuid); - $success = function () use ($environmentsResource, $cloudAppUuid, $label): void { - $environments = $environmentsResource->getAll($cloudAppUuid); - foreach ($environments as $environment) { - if ($environment->label === $label) { - break; - } - } - if (isset($environment)) { - $this->output->writeln([ - '', - "Your CDE URL: domains[0]}>{$environment->domains[0]}", - ]); - } - }; - $this->waitForNotificationToComplete($acquiaCloudClient, $notificationUuid, "Waiting for the environment to be ready. This usually takes 2 - 15 minutes.", $success); + $this->checklist->addItem("Initiating environment creation"); + $response = $environmentsResource->create($cloudAppUuid, $label, $branch, $databaseNames); + $notificationUuid = CommandBase::getNotificationUuidFromResponse($response); + $this->checklist->completePreviousItem(); - return Command::SUCCESS; - } + $success = function () use ($environmentsResource, $cloudAppUuid, $label): void { + $environments = $environmentsResource->getAll($cloudAppUuid); + foreach ($environments as $environment) { + if ($environment->label === $label) { + break; + } + } + if (isset($environment)) { + $this->output->writeln([ + '', + "Your CDE URL: domains[0]}>{$environment->domains[0]}", + ]); + } + }; + $this->waitForNotificationToComplete($acquiaCloudClient, $notificationUuid, "Waiting for the environment to be ready. This usually takes 2 - 15 minutes.", $success); - private function validateLabel(Environments $environmentsResource, string $cloudAppUuid, string $title): void { - $this->checklist->addItem("Checking to see that label is unique"); - /** @var \AcquiaCloudApi\Response\EnvironmentResponse[] $environments */ - $environments = $environmentsResource->getAll($cloudAppUuid); - foreach ($environments as $environment) { - if ($environment->label === $title) { - throw new AcquiaCliException("An environment named $title already exists."); - } + return Command::SUCCESS; } - $this->checklist->completePreviousItem(); - } - private function getBranch(Client $acquiaCloudClient, ?string $cloudAppUuid, InputInterface $input): string { - $branchesAndTags = $acquiaCloudClient->request('get', "/applications/$cloudAppUuid/code"); - if ($input->getArgument('branch')) { - $branch = $input->getArgument('branch'); - if (!in_array($branch, array_column($branchesAndTags, 'name'), TRUE)) { - throw new AcquiaCliException("There is no branch or tag with the name $branch on the remote VCS.", ); - } - return $branch; + private function validateLabel(Environments $environmentsResource, string $cloudAppUuid, string $title): void + { + $this->checklist->addItem("Checking to see that label is unique"); + /** @var \AcquiaCloudApi\Response\EnvironmentResponse[] $environments */ + $environments = $environmentsResource->getAll($cloudAppUuid); + foreach ($environments as $environment) { + if ($environment->label === $title) { + throw new AcquiaCliException("An environment named $title already exists."); + } + } + $this->checklist->completePreviousItem(); } - $branchOrTag = $this->promptChooseFromObjectsOrArrays($branchesAndTags, 'name', 'name', "Choose a branch or tag to deploy to the new environment"); - return $branchOrTag->name; - } - /** - * @return array - */ - private function getDatabaseNames(Client $acquiaCloudClient, ?string $cloudAppUuid): array { - $this->checklist->addItem("Determining default database"); - $databasesResource = new Databases($acquiaCloudClient); - $databases = $databasesResource->getNames($cloudAppUuid); - $databaseNames = []; - foreach ($databases as $database) { - $databaseNames[] = $database->name; + private function getBranch(Client $acquiaCloudClient, ?string $cloudAppUuid, InputInterface $input): string + { + $branchesAndTags = $acquiaCloudClient->request('get', "/applications/$cloudAppUuid/code"); + if ($input->getArgument('branch')) { + $branch = $input->getArgument('branch'); + if (!in_array($branch, array_column($branchesAndTags, 'name'), true)) { + throw new AcquiaCliException("There is no branch or tag with the name $branch on the remote VCS.",); + } + return $branch; + } + $branchOrTag = $this->promptChooseFromObjectsOrArrays($branchesAndTags, 'name', 'name', "Choose a branch or tag to deploy to the new environment"); + return $branchOrTag->name; } - $this->checklist->completePreviousItem(); - return $databaseNames; - } + /** + * @return array + */ + private function getDatabaseNames(Client $acquiaCloudClient, ?string $cloudAppUuid): array + { + $this->checklist->addItem("Determining default database"); + $databasesResource = new Databases($acquiaCloudClient); + $databases = $databasesResource->getNames($cloudAppUuid); + $databaseNames = []; + foreach ($databases as $database) { + $databaseNames[] = $database->name; + } + $this->checklist->completePreviousItem(); + return $databaseNames; + } } diff --git a/src/Command/Env/EnvDeleteCommand.php b/src/Command/Env/EnvDeleteCommand.php index d68dafc04..25719c9c1 100644 --- a/src/Command/Env/EnvDeleteCommand.php +++ b/src/Command/Env/EnvDeleteCommand.php @@ -1,6 +1,6 @@ acceptEnvironmentId(); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $this->output = $output; - $cloudAppUuid = $this->determineCloudApplication(TRUE); - $acquiaCloudClient = $this->cloudApiClientService->getClient(); - $environmentsResource = new Environments($acquiaCloudClient); - $environment = $this->determineEnvironmentCde($environmentsResource, $cloudAppUuid); - $environmentsResource->delete($environment->uuid); - - $this->io->success([ - "The {$environment->label} environment is being deleted", - ]); - - return Command::SUCCESS; - } - - private function determineEnvironmentCde(Environments $environmentsResource, string $cloudAppUuid): EnvironmentResponse { - if ($this->input->getArgument('environmentId')) { - // @todo Validate. - $environmentId = $this->input->getArgument('environmentId'); - return $environmentsResource->get($environmentId); +final class EnvDeleteCommand extends CommandBase +{ + protected function configure(): void + { + $this->acceptEnvironmentId(); } - $environments = $environmentsResource->getAll($cloudAppUuid); - $cdes = []; - foreach ($environments as $environment) { - if ($environment->flags->cde) { - $cdes[] = $environment; - } - } - if (!$cdes) { - throw new AcquiaCliException('There are no existing CDEs for Application ' . $cloudAppUuid); + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->output = $output; + $cloudAppUuid = $this->determineCloudApplication(true); + $acquiaCloudClient = $this->cloudApiClientService->getClient(); + $environmentsResource = new Environments($acquiaCloudClient); + $environment = $this->determineEnvironmentCde($environmentsResource, $cloudAppUuid); + $environmentsResource->delete($environment->uuid); + + $this->io->success([ + "The {$environment->label} environment is being deleted", + ]); + + return Command::SUCCESS; } - return $this->promptChooseFromObjectsOrArrays($cdes, 'uuid', 'label', "Which Continuous Delivery Environment (CDE) do you want to delete?"); - } + private function determineEnvironmentCde(Environments $environmentsResource, string $cloudAppUuid): EnvironmentResponse + { + if ($this->input->getArgument('environmentId')) { + // @todo Validate. + $environmentId = $this->input->getArgument('environmentId'); + return $environmentsResource->get($environmentId); + } + $environments = $environmentsResource->getAll($cloudAppUuid); + $cdes = []; + foreach ($environments as $environment) { + if ($environment->flags->cde) { + $cdes[] = $environment; + } + } + if (!$cdes) { + throw new AcquiaCliException('There are no existing CDEs for Application ' . $cloudAppUuid); + } + return $this->promptChooseFromObjectsOrArrays($cdes, 'uuid', 'label', "Which Continuous Delivery Environment (CDE) do you want to delete?"); + } } diff --git a/src/Command/Env/EnvMirrorCommand.php b/src/Command/Env/EnvMirrorCommand.php index 7a2038717..e3860ff7e 100644 --- a/src/Command/Env/EnvMirrorCommand.php +++ b/src/Command/Env/EnvMirrorCommand.php @@ -1,6 +1,6 @@ addArgument('source-environment', InputArgument::REQUIRED, 'The Cloud Platform source environment ID or alias') - ->addUsage('[]') - ->addUsage('myapp.dev') - ->addUsage('12345-abcd1234-1111-2222-3333-0e02b2c3d470'); - $this->addArgument('destination-environment', InputArgument::REQUIRED, 'The Cloud Platform destination environment ID or alias') - ->addUsage('[]') - ->addUsage('myapp.dev') - ->addUsage('12345-abcd1234-1111-2222-3333-0e02b2c3d470'); - $this->addOption('no-code', 'c'); - $this->addOption('no-databases', 'd'); - $this->addOption('no-files', 'f'); - $this->addOption('no-config', 'p'); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $this->checklist = new Checklist($output); - $outputCallback = $this->getOutputCallback($output, $this->checklist); - $acquiaCloudClient = $this->cloudApiClientService->getClient(); - $environmentsResource = new Environments($acquiaCloudClient); - $sourceEnvironmentUuid = $input->getArgument('source-environment'); - $destinationEnvironmentUuid = $input->getArgument('destination-environment'); - - $this->checklist->addItem("Fetching information about source environment"); - $sourceEnvironment = $environmentsResource->get($sourceEnvironmentUuid); - $this->checklist->completePreviousItem(); - - $this->checklist->addItem("Fetching information about destination environment"); - $destinationEnvironment = $environmentsResource->get($destinationEnvironmentUuid); - $this->checklist->completePreviousItem(); - - $answer = $this->io->confirm("Are you sure that you want to overwrite everything on {$destinationEnvironment->label} ({$destinationEnvironment->name}) and replace it with source data from {$sourceEnvironment->label} ({$sourceEnvironment->name})"); - if (!$answer) { - return 1; +final class EnvMirrorCommand extends CommandBase +{ + private Checklist $checklist; + + protected function configure(): void + { + $this->addArgument('source-environment', InputArgument::REQUIRED, 'The Cloud Platform source environment ID or alias') + ->addUsage('[]') + ->addUsage('myapp.dev') + ->addUsage('12345-abcd1234-1111-2222-3333-0e02b2c3d470'); + $this->addArgument('destination-environment', InputArgument::REQUIRED, 'The Cloud Platform destination environment ID or alias') + ->addUsage('[]') + ->addUsage('myapp.dev') + ->addUsage('12345-abcd1234-1111-2222-3333-0e02b2c3d470'); + $this->addOption('no-code', 'c'); + $this->addOption('no-databases', 'd'); + $this->addOption('no-files', 'f'); + $this->addOption('no-config', 'p'); } - if (!$input->getOption('no-code')) { - $codeCopyResponse = $this->mirrorCode($acquiaCloudClient, $destinationEnvironmentUuid, $sourceEnvironment, $outputCallback); + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->checklist = new Checklist($output); + $outputCallback = $this->getOutputCallback($output, $this->checklist); + $acquiaCloudClient = $this->cloudApiClientService->getClient(); + $environmentsResource = new Environments($acquiaCloudClient); + $sourceEnvironmentUuid = $input->getArgument('source-environment'); + $destinationEnvironmentUuid = $input->getArgument('destination-environment'); + + $this->checklist->addItem("Fetching information about source environment"); + $sourceEnvironment = $environmentsResource->get($sourceEnvironmentUuid); + $this->checklist->completePreviousItem(); + + $this->checklist->addItem("Fetching information about destination environment"); + $destinationEnvironment = $environmentsResource->get($destinationEnvironmentUuid); + $this->checklist->completePreviousItem(); + + $answer = $this->io->confirm("Are you sure that you want to overwrite everything on {$destinationEnvironment->label} ({$destinationEnvironment->name}) and replace it with source data from {$sourceEnvironment->label} ({$sourceEnvironment->name})"); + if (!$answer) { + return 1; + } + + if (!$input->getOption('no-code')) { + $codeCopyResponse = $this->mirrorCode($acquiaCloudClient, $destinationEnvironmentUuid, $sourceEnvironment, $outputCallback); + } + + if (!$input->getOption('no-databases')) { + $dbCopyResponse = $this->mirrorDatabase($acquiaCloudClient, $sourceEnvironmentUuid, $destinationEnvironmentUuid, $outputCallback); + } + + if (!$input->getOption('no-files')) { + $filesCopyResponse = $this->mirrorFiles($environmentsResource, $sourceEnvironmentUuid, $destinationEnvironmentUuid); + } + + if (!$input->getOption('no-config')) { + $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($dbCopyResponse)) { + $this->waitForNotificationToComplete($acquiaCloudClient, CommandBase::getNotificationUuidFromResponse($dbCopyResponse), 'Waiting for database copy to complete'); + } + if (isset($filesCopyResponse)) { + $this->waitForNotificationToComplete($acquiaCloudClient, CommandBase::getNotificationUuidFromResponse($filesCopyResponse), 'Waiting for files copy to complete'); + } + if (isset($configCopyResponse)) { + $this->waitForNotificationToComplete($acquiaCloudClient, CommandBase::getNotificationUuidFromResponse($configCopyResponse), 'Waiting for config copy to complete'); + } + + $this->io->success([ + "Done! {$destinationEnvironment->label} now matches {$sourceEnvironment->label}", + "You can visit it here:", + "https://" . $destinationEnvironment->domains[0], + ]); + + return Command::SUCCESS; } - if (!$input->getOption('no-databases')) { - $dbCopyResponse = $this->mirrorDatabase($acquiaCloudClient, $sourceEnvironmentUuid, $destinationEnvironmentUuid, $outputCallback); + private function getDefaultDatabase(array $databases): ?object + { + foreach ($databases as $database) { + if ($database->flags->default) { + return $database; + } + } + return null; } - if (!$input->getOption('no-files')) { - $filesCopyResponse = $this->mirrorFiles($environmentsResource, $sourceEnvironmentUuid, $destinationEnvironmentUuid); + private function mirrorDatabase(Client $acquiaCloudClient, mixed $sourceEnvironmentUuid, mixed $destinationEnvironmentUuid, callable $outputCallback): OperationResponse + { + $this->checklist->addItem("Initiating database copy"); + $outputCallback('out', "Getting a list of databases"); + $databasesResource = new Databases($acquiaCloudClient); + $databases = $acquiaCloudClient->request('get', "/environments/$sourceEnvironmentUuid/databases"); + $defaultDatabase = $this->getDefaultDatabase($databases); + $outputCallback('out', "Copying {$defaultDatabase->name}"); + + // @todo Create database if its missing. + $dbCopyResponse = $databasesResource->copy($sourceEnvironmentUuid, $defaultDatabase->name, $destinationEnvironmentUuid); + $this->checklist->completePreviousItem(); + return $dbCopyResponse; } - if (!$input->getOption('no-config')) { - $configCopyResponse = $this->mirrorConfig($sourceEnvironment, $destinationEnvironment, $environmentsResource, $destinationEnvironmentUuid, $outputCallback); + private function mirrorCode(Client $acquiaCloudClient, mixed $destinationEnvironmentUuid, EnvironmentResponse $sourceEnvironment, callable $outputCallback): mixed + { + $this->checklist->addItem("Initiating code switch"); + $outputCallback('out', "Switching to {$sourceEnvironment->vcs->path}"); + $codeCopyResponse = $acquiaCloudClient->request('post', "/environments/$destinationEnvironmentUuid/code/actions/switch", [ + 'form_params' => [ + 'branch' => $sourceEnvironment->vcs->path, + ], + ]); + $codeCopyResponse->links = $codeCopyResponse->_links; + $this->checklist->completePreviousItem(); + return $codeCopyResponse; } - if (isset($codeCopyResponse)) { - $this->waitForNotificationToComplete($acquiaCloudClient, CommandBase::getNotificationUuidFromResponse($codeCopyResponse), 'Waiting for code copy to complete'); - } - if (isset($dbCopyResponse)) { - $this->waitForNotificationToComplete($acquiaCloudClient, CommandBase::getNotificationUuidFromResponse($dbCopyResponse), 'Waiting for database copy to complete'); - } - if (isset($filesCopyResponse)) { - $this->waitForNotificationToComplete($acquiaCloudClient, CommandBase::getNotificationUuidFromResponse($filesCopyResponse), 'Waiting for files copy to complete'); + private function mirrorFiles(Environments $environmentsResource, mixed $sourceEnvironmentUuid, mixed $destinationEnvironmentUuid): OperationResponse + { + $this->checklist->addItem("Initiating files copy"); + $filesCopyResponse = $environmentsResource->copyFiles($sourceEnvironmentUuid, $destinationEnvironmentUuid); + $this->checklist->completePreviousItem(); + return $filesCopyResponse; } - if (isset($configCopyResponse)) { - $this->waitForNotificationToComplete($acquiaCloudClient, CommandBase::getNotificationUuidFromResponse($configCopyResponse), 'Waiting for config copy to complete'); - } - - $this->io->success([ - "Done! {$destinationEnvironment->label} now matches {$sourceEnvironment->label}", - "You can visit it here:", - "https://" . $destinationEnvironment->domains[0], - ]); - return Command::SUCCESS; - } - - private function getDefaultDatabase(array $databases): ?object { - foreach ($databases as $database) { - if ($database->flags->default) { - return $database; - } - } - return NULL; - } - - private function mirrorDatabase(Client $acquiaCloudClient, mixed $sourceEnvironmentUuid, mixed $destinationEnvironmentUuid, callable $outputCallback): OperationResponse { - $this->checklist->addItem("Initiating database copy"); - $outputCallback('out', "Getting a list of databases"); - $databasesResource = new Databases($acquiaCloudClient); - $databases = $acquiaCloudClient->request('get', "/environments/$sourceEnvironmentUuid/databases"); - $defaultDatabase = $this->getDefaultDatabase($databases); - $outputCallback('out', "Copying {$defaultDatabase->name}"); - - // @todo Create database if its missing. - $dbCopyResponse = $databasesResource->copy($sourceEnvironmentUuid, $defaultDatabase->name, $destinationEnvironmentUuid); - $this->checklist->completePreviousItem(); - return $dbCopyResponse; - } - - private function mirrorCode(Client $acquiaCloudClient, mixed $destinationEnvironmentUuid, EnvironmentResponse $sourceEnvironment, callable $outputCallback): mixed { - $this->checklist->addItem("Initiating code switch"); - $outputCallback('out', "Switching to {$sourceEnvironment->vcs->path}"); - $codeCopyResponse = $acquiaCloudClient->request('post', "/environments/$destinationEnvironmentUuid/code/actions/switch", [ - 'form_params' => [ - 'branch' => $sourceEnvironment->vcs->path, - ], - ]); - $codeCopyResponse->links = $codeCopyResponse->_links; - $this->checklist->completePreviousItem(); - return $codeCopyResponse; - } - - private function mirrorFiles(Environments $environmentsResource, mixed $sourceEnvironmentUuid, mixed $destinationEnvironmentUuid): OperationResponse { - $this->checklist->addItem("Initiating files copy"); - $filesCopyResponse = $environmentsResource->copyFiles($sourceEnvironmentUuid, $destinationEnvironmentUuid); - $this->checklist->completePreviousItem(); - return $filesCopyResponse; - } - - private function mirrorConfig(EnvironmentResponse $sourceEnvironment, EnvironmentResponse $destinationEnvironment, Environments $environmentsResource, mixed $destinationEnvironmentUuid, callable $outputCallback): OperationResponse { - $this->checklist->addItem("Initiating config copy"); - $outputCallback('out', "Copying PHP version, acpu memory limit, etc."); - $config = (array) $sourceEnvironment->configuration->php; - $config['apcu'] = max(32, $sourceEnvironment->configuration->php->apcu); - if ($config['version'] == $destinationEnvironment->configuration->php->version) { - unset($config['version']); + private function mirrorConfig(EnvironmentResponse $sourceEnvironment, EnvironmentResponse $destinationEnvironment, Environments $environmentsResource, mixed $destinationEnvironmentUuid, callable $outputCallback): OperationResponse + { + $this->checklist->addItem("Initiating config copy"); + $outputCallback('out', "Copying PHP version, acpu memory limit, etc."); + $config = (array) $sourceEnvironment->configuration->php; + $config['apcu'] = max(32, $sourceEnvironment->configuration->php->apcu); + if ($config['version'] == $destinationEnvironment->configuration->php->version) { + unset($config['version']); + } + unset($config['memcached_limit']); + $configCopyResponse = $environmentsResource->update($destinationEnvironmentUuid, $config); + $this->checklist->completePreviousItem(); + return $configCopyResponse; } - unset($config['memcached_limit']); - $configCopyResponse = $environmentsResource->update($destinationEnvironmentUuid, $config); - $this->checklist->completePreviousItem(); - return $configCopyResponse; - } - } diff --git a/src/Command/HelloWorldCommand.php b/src/Command/HelloWorldCommand.php index c3409f6cc..d51c07ec2 100644 --- a/src/Command/HelloWorldCommand.php +++ b/src/Command/HelloWorldCommand.php @@ -1,6 +1,6 @@ io->success('Hello world!'); - - return Command::SUCCESS; - } +#[AsCommand(name: 'hello-world', description: 'Test command used for asserting core functionality', hidden: true)] +final class HelloWorldCommand extends CommandBase +{ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->io->success('Hello world!'); + return Command::SUCCESS; + } } diff --git a/src/Command/Ide/IdeCommandBase.php b/src/Command/Ide/IdeCommandBase.php index 3c07ab03d..f3204ccc3 100644 --- a/src/Command/Ide/IdeCommandBase.php +++ b/src/Command/Ide/IdeCommandBase.php @@ -1,6 +1,6 @@ getAll($cloudApplicationUuid)); + if (empty($ides)) { + throw new AcquiaCliException('No IDEs exist for this application.'); + } - protected function promptIdeChoice( - string $questionText, - Ides $idesResource, - string $cloudApplicationUuid - ): IdeResponse { - $ides = iterator_to_array($idesResource->getAll($cloudApplicationUuid)); - if (empty($ides)) { - throw new AcquiaCliException('No IDEs exist for this application.'); - } + $choices = []; + foreach ($ides as $ide) { + $choices[] = "$ide->label ($ide->uuid)"; + } + $choice = $this->io->choice($questionText, $choices, $choices[0]); + $chosenEnvironmentIndex = array_search($choice, $choices, true); - $choices = []; - foreach ($ides as $ide) { - $choices[] = "$ide->label ($ide->uuid)"; + return $ides[$chosenEnvironmentIndex]; } - $choice = $this->io->choice($questionText, $choices, $choices[0]); - $chosenEnvironmentIndex = array_search($choice, $choices, TRUE); - - return $ides[$chosenEnvironmentIndex]; - } - /** - * Start service inside IDE. - */ - protected function startService(string $service): void { - $process = $this->localMachineHelper->execute([ - 'supervisorctl', - 'start', - $service, - ], NULL, NULL, FALSE); - if (!$process->isSuccessful()) { - throw new AcquiaCliException('Unable to start ' . $service . ' in the IDE: {error}', ['error' => $process->getErrorOutput()]); + /** + * Start service inside IDE. + */ + protected function startService(string $service): void + { + $process = $this->localMachineHelper->execute([ + 'supervisorctl', + 'start', + $service, + ], null, null, false); + if (!$process->isSuccessful()) { + throw new AcquiaCliException('Unable to start ' . $service . ' in the IDE: {error}', ['error' => $process->getErrorOutput()]); + } } - } - /** - * Stop service inside IDE. - */ - protected function stopService(string $service): void { - $process = $this->localMachineHelper->execute([ - 'supervisorctl', - 'stop', - $service, - ], NULL, NULL, FALSE); - if (!$process->isSuccessful()) { - throw new AcquiaCliException('Unable to stop ' . $service . ' in the IDE: {error}', ['error' => $process->getErrorOutput()]); + /** + * Stop service inside IDE. + */ + protected function stopService(string $service): void + { + $process = $this->localMachineHelper->execute([ + 'supervisorctl', + 'stop', + $service, + ], null, null, false); + if (!$process->isSuccessful()) { + throw new AcquiaCliException('Unable to stop ' . $service . ' in the IDE: {error}', ['error' => $process->getErrorOutput()]); + } } - } - /** - * Restart service inside IDE. - */ - protected function restartService(string $service): void { - $process = $this->localMachineHelper->execute([ - 'supervisorctl', - 'restart', - $service, - ], NULL, NULL, FALSE); - if (!$process->isSuccessful()) { - throw new AcquiaCliException('Unable to restart ' . $service . ' in the IDE: {error}', ['error' => $process->getErrorOutput()]); + /** + * Restart service inside IDE. + */ + protected function restartService(string $service): void + { + $process = $this->localMachineHelper->execute([ + 'supervisorctl', + 'restart', + $service, + ], null, null, false); + if (!$process->isSuccessful()) { + throw new AcquiaCliException('Unable to restart ' . $service . ' in the IDE: {error}', ['error' => $process->getErrorOutput()]); + } } - } - - public function setXdebugIniFilepath(string $filePath): void { - $this->xdebugIniFilepath = $filePath; - } - protected function getXdebugIniFilePath(): string { - return $this->xdebugIniFilepath; - } + public function setXdebugIniFilepath(string $filePath): void + { + $this->xdebugIniFilepath = $filePath; + } + protected function getXdebugIniFilePath(): string + { + return $this->xdebugIniFilepath; + } } diff --git a/src/Command/Ide/IdeCreateCommand.php b/src/Command/Ide/IdeCreateCommand.php index 932bd073c..2c106e5cd 100644 --- a/src/Command/Ide/IdeCreateCommand.php +++ b/src/Command/Ide/IdeCreateCommand.php @@ -1,6 +1,6 @@ localMachineHelper, $this->datastoreCloud, $this->datastoreAcli, $this->cloudCredentials, $this->telemetryHelper, $this->projectDir, $this->cloudApiClientService, $this->sshHelper, $this->sshDir, $logger); - } - - protected function configure(): void { - $this->acceptApplicationUuid(); - $this->addOption('label', NULL, InputOption::VALUE_REQUIRED, 'The label for the IDE'); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $cloudApplicationUuid = $this->determineCloudApplication(); - $checklist = new Checklist($output); - $acquiaCloudClient = $this->cloudApiClientService->getClient(); - $accountResource = new Account($acquiaCloudClient); - $account = $accountResource->get(); - $default = "$account->first_name $account->last_name's IDE"; - $ideLabel = $this->determineOption('label', FALSE, $this->validateIdeLabel(...), NULL, $default); +final class IdeCreateCommand extends IdeCommandBase +{ + private IdeResponse $ide; + + public function __construct( + public LocalMachineHelper $localMachineHelper, + protected CloudDataStore $datastoreCloud, + protected AcquiaCliDatastore $datastoreAcli, + protected ApiCredentialsInterface $cloudCredentials, + protected TelemetryHelper $telemetryHelper, + protected string $projectDir, + protected ClientService $cloudApiClientService, + public SshHelper $sshHelper, + protected string $sshDir, + LoggerInterface $logger, + protected Client $httpClient + ) { + parent::__construct($this->localMachineHelper, $this->datastoreCloud, $this->datastoreAcli, $this->cloudCredentials, $this->telemetryHelper, $this->projectDir, $this->cloudApiClientService, $this->sshHelper, $this->sshDir, $logger); + } - // Create it. - $checklist->addItem('Creating your Cloud IDE'); - $idesResource = new Ides($acquiaCloudClient); - $response = $idesResource->create($cloudApplicationUuid, $ideLabel); - $checklist->completePreviousItem(); + protected function configure(): void + { + $this->acceptApplicationUuid(); + $this->addOption('label', null, InputOption::VALUE_REQUIRED, 'The label for the IDE'); + } - // Get IDE info. - $checklist->addItem('Getting IDE information'); - $this->ide = $this->getIdeFromResponse($response, $acquiaCloudClient); - $ideUrl = $this->ide->links->ide->href; - $checklist->completePreviousItem(); + protected function execute(InputInterface $input, OutputInterface $output): int + { + $cloudApplicationUuid = $this->determineCloudApplication(); + $checklist = new Checklist($output); + $acquiaCloudClient = $this->cloudApiClientService->getClient(); + $accountResource = new Account($acquiaCloudClient); + $account = $accountResource->get(); + $default = "$account->first_name $account->last_name's IDE"; + $ideLabel = $this->determineOption('label', false, $this->validateIdeLabel(...), null, $default); + + // Create it. + $checklist->addItem('Creating your Cloud IDE'); + $idesResource = new Ides($acquiaCloudClient); + $response = $idesResource->create($cloudApplicationUuid, $ideLabel); + $checklist->completePreviousItem(); + + // Get IDE info. + $checklist->addItem('Getting IDE information'); + $this->ide = $this->getIdeFromResponse($response, $acquiaCloudClient); + $ideUrl = $this->ide->links->ide->href; + $checklist->completePreviousItem(); + + // Wait! + return $this->waitForDnsPropagation($ideUrl); + } - // Wait! - return $this->waitForDnsPropagation($ideUrl); - } + /** + * Keep this public since it's used as a callback and static analysis tools + * think it's unused. + * + * @todo use first-class callable syntax instead once we upgrade to PHP 8.1 + * @see https://www.php.net/manual/en/functions.first_class_callable_syntax.php + */ + public function validateIdeLabel(string $label): string + { + $violations = Validation::createValidator()->validate($label, [ + new Regex(['pattern' => '/^[\w\' ]+$/', 'message' => 'Use only letters, numbers, and spaces']), + ]); + if (count($violations)) { + throw new ValidatorException($violations->get(0)->getMessage()); + } + return $label; + } - /** - * Keep this public since it's used as a callback and static analysis tools - * think it's unused. - * - * @todo use first-class callable syntax instead once we upgrade to PHP 8.1 - * @see https://www.php.net/manual/en/functions.first_class_callable_syntax.php - */ - public function validateIdeLabel(string $label): string { - $violations = Validation::createValidator()->validate($label, [ - new Regex(['pattern' => '/^[\w\' ]+$/', 'message' => 'Use only letters, numbers, and spaces']), - ]); - if (count($violations)) { - throw new ValidatorException($violations->get(0)->getMessage()); + private function waitForDnsPropagation(string $ideUrl): int + { + $ideCreated = false; + $checkIdeStatus = function () use (&$ideCreated, $ideUrl) { + // Ideally we'd set $ideUrl as the Guzzle base_url, but that requires creating a client factory. + // @see https://stackoverflow.com/questions/28277889/guzzlehttp-client-change-base-url-dynamically + $response = $this->httpClient->request('GET', "$ideUrl/health", ['http_errors' => false]); + // Mutating this will result in an infinite loop and timeout. + // @infection-ignore-all + if ($response->getStatusCode() === 200) { + $ideCreated = true; + } + return $ideCreated; + }; + $doneCallback = function () use (&$ideCreated): void { + if ($ideCreated) { + $this->output->writeln(''); + $this->output->writeln('Your IDE is ready!'); + } + $this->writeIdeLinksToScreen(); + }; + $spinnerMessage = 'Waiting for the IDE to be ready. This usually takes 2 - 15 minutes.'; + LoopHelper::getLoopy($this->output, $this->io, $spinnerMessage, $checkIdeStatus, $doneCallback); + + return Command::SUCCESS; } - return $label; - } - private function waitForDnsPropagation(string $ideUrl): int { - $ideCreated = FALSE; - $checkIdeStatus = function () use (&$ideCreated, $ideUrl) { - // Ideally we'd set $ideUrl as the Guzzle base_url, but that requires creating a client factory. - // @see https://stackoverflow.com/questions/28277889/guzzlehttp-client-change-base-url-dynamically - $response = $this->httpClient->request('GET', "$ideUrl/health", ['http_errors' => FALSE]); - // Mutating this will result in an infinite loop and timeout. - // @infection-ignore-all - if ($response->getStatusCode() === 200) { - $ideCreated = TRUE; - } - return $ideCreated; - }; - $doneCallback = function () use (&$ideCreated): void { - if ($ideCreated) { + /** + * Writes the IDE links to screen. + */ + private function writeIdeLinksToScreen(): void + { $this->output->writeln(''); - $this->output->writeln('Your IDE is ready!'); - } - $this->writeIdeLinksToScreen(); - }; - $spinnerMessage = 'Waiting for the IDE to be ready. This usually takes 2 - 15 minutes.'; - LoopHelper::getLoopy($this->output, $this->io, $spinnerMessage, $checkIdeStatus, $doneCallback); - - return Command::SUCCESS; - } - - /** - * Writes the IDE links to screen. - */ - private function writeIdeLinksToScreen(): void { - $this->output->writeln(''); - $this->output->writeln("Your IDE URL: ide->links->ide->href}>{$this->ide->links->ide->href}"); - $this->output->writeln("Your Drupal Site URL: ide->links->web->href}>{$this->ide->links->web->href}"); - // @todo Prompt to open browser. - } - - private function getIdeFromResponse( - OperationResponse $response, - \AcquiaCloudApi\Connector\Client $acquiaCloudClient - ): IdeResponse { - $cloudApiIdeUrl = $response->links->self->href; - $urlParts = explode('/', $cloudApiIdeUrl); - $ideUuid = end($urlParts); - return (new Ides($acquiaCloudClient))->get($ideUuid); - } + $this->output->writeln("Your IDE URL: ide->links->ide->href}>{$this->ide->links->ide->href}"); + $this->output->writeln("Your Drupal Site URL: ide->links->web->href}>{$this->ide->links->web->href}"); + // @todo Prompt to open browser. + } + private function getIdeFromResponse( + OperationResponse $response, + \AcquiaCloudApi\Connector\Client $acquiaCloudClient + ): IdeResponse { + $cloudApiIdeUrl = $response->links->self->href; + $urlParts = explode('/', $cloudApiIdeUrl); + $ideUuid = end($urlParts); + return (new Ides($acquiaCloudClient))->get($ideUuid); + } } diff --git a/src/Command/Ide/IdeDeleteCommand.php b/src/Command/Ide/IdeDeleteCommand.php index e90ea2af4..b54a9aa88 100644 --- a/src/Command/Ide/IdeDeleteCommand.php +++ b/src/Command/Ide/IdeDeleteCommand.php @@ -1,6 +1,6 @@ acceptApplicationUuid(); - // @todo make this an argument - $this->addOption('uuid', NULL, InputOption::VALUE_OPTIONAL, 'UUID of the IDE to delete'); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $acquiaCloudClient = $this->cloudApiClientService->getClient(); - $idesResource = new Ides($acquiaCloudClient); - - $ideUuid = $input->getOption('uuid'); - if ($ideUuid) { - $ide = $idesResource->get($ideUuid); - } - else { - $cloudApplicationUuid = $this->determineCloudApplication(); - $ide = $this->promptIdeChoice("Select the IDE you'd like to delete:", $idesResource, $cloudApplicationUuid); - $answer = $this->io->confirm("Are you sure you want to delete $ide->label"); - if (!$answer) { - $this->io->writeln('Ok, never mind.'); - return Command::FAILURE; - } - } - $response = $idesResource->delete($ide->uuid); - $this->io->writeln($response->message); - - // Check to see if an SSH key for this IDE exists on Cloud. - $cloudKey = $this->findIdeSshKeyOnCloud($ide->label, $ide->uuid); - if ($cloudKey) { - $answer = $this->io->confirm('Would you like to delete the SSH key associated with this IDE from your Cloud Platform account?'); - if ($answer) { - $this->deleteSshKeyFromCloud($output, $cloudKey); - } +final class IdeDeleteCommand extends IdeCommandBase +{ + use SshCommandTrait; + + protected function configure(): void + { + $this->acceptApplicationUuid(); + // @todo make this an argument + $this->addOption('uuid', null, InputOption::VALUE_OPTIONAL, 'UUID of the IDE to delete'); } - return Command::SUCCESS; - } - + protected function execute(InputInterface $input, OutputInterface $output): int + { + $acquiaCloudClient = $this->cloudApiClientService->getClient(); + $idesResource = new Ides($acquiaCloudClient); + + $ideUuid = $input->getOption('uuid'); + if ($ideUuid) { + $ide = $idesResource->get($ideUuid); + } else { + $cloudApplicationUuid = $this->determineCloudApplication(); + $ide = $this->promptIdeChoice("Select the IDE you'd like to delete:", $idesResource, $cloudApplicationUuid); + $answer = $this->io->confirm("Are you sure you want to delete $ide->label"); + if (!$answer) { + $this->io->writeln('Ok, never mind.'); + return Command::FAILURE; + } + } + $response = $idesResource->delete($ide->uuid); + $this->io->writeln($response->message); + + // Check to see if an SSH key for this IDE exists on Cloud. + $cloudKey = $this->findIdeSshKeyOnCloud($ide->label, $ide->uuid); + if ($cloudKey) { + $answer = $this->io->confirm('Would you like to delete the SSH key associated with this IDE from your Cloud Platform account?'); + if ($answer) { + $this->deleteSshKeyFromCloud($output, $cloudKey); + } + } + + return Command::SUCCESS; + } } diff --git a/src/Command/Ide/IdeInfoCommand.php b/src/Command/Ide/IdeInfoCommand.php index 026a4ddf7..9c075081a 100644 --- a/src/Command/Ide/IdeInfoCommand.php +++ b/src/Command/Ide/IdeInfoCommand.php @@ -1,6 +1,6 @@ acceptApplicationUuid(); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $applicationUuid = $this->determineCloudApplication(); - - $acquiaCloudClient = $this->cloudApiClientService->getClient(); - $idesResource = new Ides($acquiaCloudClient); - - $ide = $this->promptIdeChoice("Select an IDE to get more information:", $idesResource, $applicationUuid); - $response = $idesResource->get($ide->uuid); - $this->io->definitionList( - ['IDE property' => 'IDE value'], - new TableSeparator(), - ['UUID' => $response->uuid], - ['Label' => $response->label], - ['Owner name' => $response->owner->first_name . ' ' . $response->owner->last_name], - ['Owner username' => $response->owner->username], - ['Owner email' => $response->owner->mail], - ['Cloud application' => $response->links->application->href], - ['IDE URL' => $response->links->ide->href], - ['Web URL' => $response->links->web->href] - ); - - return Command::SUCCESS; - } - +final class IdeInfoCommand extends IdeCommandBase +{ + protected function configure(): void + { + $this->acceptApplicationUuid(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $applicationUuid = $this->determineCloudApplication(); + + $acquiaCloudClient = $this->cloudApiClientService->getClient(); + $idesResource = new Ides($acquiaCloudClient); + + $ide = $this->promptIdeChoice("Select an IDE to get more information:", $idesResource, $applicationUuid); + $response = $idesResource->get($ide->uuid); + $this->io->definitionList( + ['IDE property' => 'IDE value'], + new TableSeparator(), + ['UUID' => $response->uuid], + ['Label' => $response->label], + ['Owner name' => $response->owner->first_name . ' ' . $response->owner->last_name], + ['Owner username' => $response->owner->username], + ['Owner email' => $response->owner->mail], + ['Cloud application' => $response->links->application->href], + ['IDE URL' => $response->links->ide->href], + ['Web URL' => $response->links->web->href] + ); + + return Command::SUCCESS; + } } diff --git a/src/Command/Ide/IdeListCommand.php b/src/Command/Ide/IdeListCommand.php index ea83d2214..dfa5e9bac 100644 --- a/src/Command/Ide/IdeListCommand.php +++ b/src/Command/Ide/IdeListCommand.php @@ -1,6 +1,6 @@ acceptApplicationUuid(); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $applicationUuid = $this->determineCloudApplication(); - - $acquiaCloudClient = $this->cloudApiClientService->getClient(); - $idesResource = new Ides($acquiaCloudClient); - $applicationIdes = $idesResource->getAll($applicationUuid); - - if ($applicationIdes->count()) { - $table = new Table($output); - $table->setStyle('borderless'); - $table->setHeaders(['IDEs']); - foreach ($applicationIdes as $ide) { - $table->addRows([ - ["{$ide->label} ({$ide->owner->mail})"], - ["IDE URL: links->ide->href}>{$ide->links->ide->href}"], - ["Web URL: links->web->href}>{$ide->links->web->href}"], - new TableSeparator(), - ]); - } - $table->render(); +final class IdeListCommand extends IdeCommandBase +{ + protected function configure(): void + { + $this->acceptApplicationUuid(); } - else { - $output->writeln('No IDE exists for this application.'); - } - - return Command::SUCCESS; - } + protected function execute(InputInterface $input, OutputInterface $output): int + { + $applicationUuid = $this->determineCloudApplication(); + + $acquiaCloudClient = $this->cloudApiClientService->getClient(); + $idesResource = new Ides($acquiaCloudClient); + $applicationIdes = $idesResource->getAll($applicationUuid); + + if ($applicationIdes->count()) { + $table = new Table($output); + $table->setStyle('borderless'); + $table->setHeaders(['IDEs']); + foreach ($applicationIdes as $ide) { + $table->addRows([ + ["{$ide->label} ({$ide->owner->mail})"], + ["IDE URL: links->ide->href}>{$ide->links->ide->href}"], + ["Web URL: links->web->href}>{$ide->links->web->href}"], + new TableSeparator(), + ]); + } + $table->render(); + } else { + $output->writeln('No IDE exists for this application.'); + } + + return Command::SUCCESS; + } } diff --git a/src/Command/Ide/IdeListMineCommand.php b/src/Command/Ide/IdeListMineCommand.php index 7e1409b0f..cb7fb3f87 100644 --- a/src/Command/Ide/IdeListMineCommand.php +++ b/src/Command/Ide/IdeListMineCommand.php @@ -1,6 +1,6 @@ cloudApiClientService->getClient(); - $ides = new Ides($acquiaCloudClient); - $accountIdes = $ides->getMine(); - $applicationResource = new Applications($acquiaCloudClient); - - if (count($accountIdes)) { - $table = new Table($output); - $table->setStyle('borderless'); - $table->setHeaders(['IDEs']); - foreach ($accountIdes as $ide) { - $appUrlParts = explode('/', $ide->links->application->href); - $appUuid = end($appUrlParts); - $application = $applicationResource->get($appUuid); - $applicationUrl = str_replace('/api', '/a', $application->links->self->href); - - $table->addRows([ - ["$ide->label"], - ["UUID: $ide->uuid"], - ["Application: $application->name"], - ["Subscription: {$application->subscription->name}"], - ["IDE URL: links->ide->href}>{$ide->links->ide->href}"], - ["Web URL: links->web->href}>{$ide->links->web->href}"], - new TableSeparator(), - ]); - } - $table->render(); +final class IdeListMineCommand extends IdeCommandBase +{ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $acquiaCloudClient = $this->cloudApiClientService->getClient(); + $ides = new Ides($acquiaCloudClient); + $accountIdes = $ides->getMine(); + $applicationResource = new Applications($acquiaCloudClient); + + if (count($accountIdes)) { + $table = new Table($output); + $table->setStyle('borderless'); + $table->setHeaders(['IDEs']); + foreach ($accountIdes as $ide) { + $appUrlParts = explode('/', $ide->links->application->href); + $appUuid = end($appUrlParts); + $application = $applicationResource->get($appUuid); + $applicationUrl = str_replace('/api', '/a', $application->links->self->href); + + $table->addRows([ + ["$ide->label"], + ["UUID: $ide->uuid"], + ["Application: $application->name"], + ["Subscription: {$application->subscription->name}"], + ["IDE URL: links->ide->href}>{$ide->links->ide->href}"], + ["Web URL: links->web->href}>{$ide->links->web->href}"], + new TableSeparator(), + ]); + } + $table->render(); + } else { + $output->writeln('No IDE exists for your account.'); + } + + return Command::SUCCESS; } - else { - $output->writeln('No IDE exists for your account.'); - } - - return Command::SUCCESS; - } - } diff --git a/src/Command/Ide/IdeOpenCommand.php b/src/Command/Ide/IdeOpenCommand.php index 0d56f7eca..37f375f30 100644 --- a/src/Command/Ide/IdeOpenCommand.php +++ b/src/Command/Ide/IdeOpenCommand.php @@ -1,6 +1,6 @@ setHidden(AcquiaDrupalEnvironmentDetector::isAhIdeEnv()); - $this->acceptApplicationUuid(); - // @todo Add option to accept an ide UUID. - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $acquiaCloudClient = $this->cloudApiClientService->getClient(); - $cloudApplicationUuid = $this->determineCloudApplication(); - $idesResource = new Ides($acquiaCloudClient); - $ide = $this->promptIdeChoice("Select the IDE you'd like to open:", $idesResource, $cloudApplicationUuid); - - $this->output->writeln(''); - $this->output->writeln("Your IDE URL: links->ide->href}>{$ide->links->ide->href}"); - $this->output->writeln("Your Drupal Site URL: links->web->href}>{$ide->links->web->href}"); - $this->output->writeln('Opening your IDE in browser...'); - - $this->localMachineHelper->startBrowser($ide->links->ide->href); - - return Command::SUCCESS; - } - +final class IdeOpenCommand extends IdeCommandBase +{ + protected function configure(): void + { + $this + ->setHidden(AcquiaDrupalEnvironmentDetector::isAhIdeEnv()); + $this->acceptApplicationUuid(); + // @todo Add option to accept an ide UUID. + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $acquiaCloudClient = $this->cloudApiClientService->getClient(); + $cloudApplicationUuid = $this->determineCloudApplication(); + $idesResource = new Ides($acquiaCloudClient); + $ide = $this->promptIdeChoice("Select the IDE you'd like to open:", $idesResource, $cloudApplicationUuid); + + $this->output->writeln(''); + $this->output->writeln("Your IDE URL: links->ide->href}>{$ide->links->ide->href}"); + $this->output->writeln("Your Drupal Site URL: links->web->href}>{$ide->links->web->href}"); + $this->output->writeln('Opening your IDE in browser...'); + + $this->localMachineHelper->startBrowser($ide->links->ide->href); + + return Command::SUCCESS; + } } diff --git a/src/Command/Ide/IdePhpVersionCommand.php b/src/Command/Ide/IdePhpVersionCommand.php index c647eb000..e0194e004 100644 --- a/src/Command/Ide/IdePhpVersionCommand.php +++ b/src/Command/Ide/IdePhpVersionCommand.php @@ -1,6 +1,6 @@ addArgument('version', InputArgument::REQUIRED, 'The PHP version') - ->setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv()); - } +final class IdePhpVersionCommand extends IdeCommandBase +{ + private string $idePhpFilePathPrefix; + + protected function configure(): void + { + $this + ->addArgument('version', InputArgument::REQUIRED, 'The PHP version') + ->setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv()); + } - protected function execute(InputInterface $input, OutputInterface $output): int { - $this->requireCloudIdeEnvironment(); - $version = $input->getArgument('version'); - $this->validatePhpVersion($version); - $this->localMachineHelper->getFilesystem()->dumpFile($this->getIdePhpVersionFilePath(), $version); - $this->restartService('php-fpm'); + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->requireCloudIdeEnvironment(); + $version = $input->getArgument('version'); + $this->validatePhpVersion($version); + $this->localMachineHelper->getFilesystem()->dumpFile($this->getIdePhpVersionFilePath(), $version); + $this->restartService('php-fpm'); - return Command::SUCCESS; - } + return Command::SUCCESS; + } - private function getIdePhpFilePathPrefix(): string { - if (!isset($this->idePhpFilePathPrefix)) { - $this->idePhpFilePathPrefix = '/usr/local/php'; + private function getIdePhpFilePathPrefix(): string + { + if (!isset($this->idePhpFilePathPrefix)) { + $this->idePhpFilePathPrefix = '/usr/local/php'; + } + return $this->idePhpFilePathPrefix; } - return $this->idePhpFilePathPrefix; - } - - public function setIdePhpFilePathPrefix(string $path): void { - $this->idePhpFilePathPrefix = $path; - } - - protected function validatePhpVersion(string $version): string { - parent::validatePhpVersion($version); - $phpFilepath = $this->getIdePhpFilePathPrefix() . $version; - if (!$this->localMachineHelper->getFilesystem()->exists($phpFilepath)) { - throw new AcquiaCliException('The specified PHP version does not exist on this machine.'); + + public function setIdePhpFilePathPrefix(string $path): void + { + $this->idePhpFilePathPrefix = $path; } - return $version; - } + protected function validatePhpVersion(string $version): string + { + parent::validatePhpVersion($version); + $phpFilepath = $this->getIdePhpFilePathPrefix() . $version; + if (!$this->localMachineHelper->getFilesystem()->exists($phpFilepath)) { + throw new AcquiaCliException('The specified PHP version does not exist on this machine.'); + } + return $version; + } } diff --git a/src/Command/Ide/IdeServiceRestartCommand.php b/src/Command/Ide/IdeServiceRestartCommand.php index ca039d8d9..c6a0eee34 100644 --- a/src/Command/Ide/IdeServiceRestartCommand.php +++ b/src/Command/Ide/IdeServiceRestartCommand.php @@ -1,6 +1,6 @@ addArgument('service', InputArgument::REQUIRED, 'The name of the service to restart') - ->addUsage('php') - ->addUsage('apache') - ->addUsage('mysql') - ->setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv()); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $this->requireCloudIdeEnvironment(); - $service = $input->getArgument('service'); - $this->validateService($service); - - $serviceNameMap = [ - 'apache' => 'apache2', - 'apache2' => 'apache2', - 'mysql' => 'mysqld', - 'mysqld' => 'mysqld', - 'php' => 'php-fpm', - 'php-fpm' => 'php-fpm', - ]; - $output->writeln("Restarting $service..."); - $serviceName = $serviceNameMap[$service]; - $this->restartService($serviceName); - $output->writeln("Restarted $service"); - - return Command::SUCCESS; - } - - private function validateService(string $service): void { - $violations = Validation::createValidator()->validate($service, [ - new Choice([ - 'choices' => ['php', 'php-fpm', 'apache', 'apache2', 'mysql', 'mysqld'], - 'message' => 'Specify a valid service name: php, apache, or mysql', - ]), - ]); - if (count($violations)) { - throw new ValidatorException($violations->get(0)->getMessage()); +final class IdeServiceRestartCommand extends IdeCommandBase +{ + protected function configure(): void + { + $this + ->addArgument('service', InputArgument::REQUIRED, 'The name of the service to restart') + ->addUsage('php') + ->addUsage('apache') + ->addUsage('mysql') + ->setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv()); } - } + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->requireCloudIdeEnvironment(); + $service = $input->getArgument('service'); + $this->validateService($service); + + $serviceNameMap = [ + 'apache' => 'apache2', + 'apache2' => 'apache2', + 'mysql' => 'mysqld', + 'mysqld' => 'mysqld', + 'php' => 'php-fpm', + 'php-fpm' => 'php-fpm', + ]; + $output->writeln("Restarting $service..."); + $serviceName = $serviceNameMap[$service]; + $this->restartService($serviceName); + $output->writeln("Restarted $service"); + + return Command::SUCCESS; + } + private function validateService(string $service): void + { + $violations = Validation::createValidator()->validate($service, [ + new Choice([ + 'choices' => ['php', 'php-fpm', 'apache', 'apache2', 'mysql', 'mysqld'], + 'message' => 'Specify a valid service name: php, apache, or mysql', + ]), + ]); + if (count($violations)) { + throw new ValidatorException($violations->get(0)->getMessage()); + } + } } diff --git a/src/Command/Ide/IdeServiceStartCommand.php b/src/Command/Ide/IdeServiceStartCommand.php index 5d3f0d607..71a5e19b0 100644 --- a/src/Command/Ide/IdeServiceStartCommand.php +++ b/src/Command/Ide/IdeServiceStartCommand.php @@ -1,6 +1,6 @@ addArgument('service', InputArgument::REQUIRED, 'The name of the service to start') - ->addUsage('php') - ->addUsage('apache') - ->addUsage('mysql') - ->setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv()); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $this->requireCloudIdeEnvironment(); - $service = $input->getArgument('service'); - $this->validateService($service); - - $serviceNameMap = [ - 'apache' => 'apache2', - 'apache2' => 'apache2', - 'mysql' => 'mysqld', - 'mysqld' => 'mysqld', - 'php' => 'php-fpm', - 'php-fpm' => 'php-fpm', - ]; - $output->writeln("Starting $service..."); - $serviceName = $serviceNameMap[$service]; - $this->startService($serviceName); - $output->writeln("Started $service"); - - return Command::SUCCESS; - } - - private function validateService(string $service): void { - $violations = Validation::createValidator()->validate($service, [ - new Choice([ - 'choices' => ['php', 'php-fpm', 'apache', 'apache2', 'mysql', 'mysqld'], - 'message' => 'Specify a valid service name: php, apache, or mysql', - ]), - ]); - if (count($violations)) { - throw new ValidatorException($violations->get(0)->getMessage()); +final class IdeServiceStartCommand extends IdeCommandBase +{ + protected function configure(): void + { + $this + ->addArgument('service', InputArgument::REQUIRED, 'The name of the service to start') + ->addUsage('php') + ->addUsage('apache') + ->addUsage('mysql') + ->setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv()); } - } + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->requireCloudIdeEnvironment(); + $service = $input->getArgument('service'); + $this->validateService($service); + + $serviceNameMap = [ + 'apache' => 'apache2', + 'apache2' => 'apache2', + 'mysql' => 'mysqld', + 'mysqld' => 'mysqld', + 'php' => 'php-fpm', + 'php-fpm' => 'php-fpm', + ]; + $output->writeln("Starting $service..."); + $serviceName = $serviceNameMap[$service]; + $this->startService($serviceName); + $output->writeln("Started $service"); + + return Command::SUCCESS; + } + private function validateService(string $service): void + { + $violations = Validation::createValidator()->validate($service, [ + new Choice([ + 'choices' => ['php', 'php-fpm', 'apache', 'apache2', 'mysql', 'mysqld'], + 'message' => 'Specify a valid service name: php, apache, or mysql', + ]), + ]); + if (count($violations)) { + throw new ValidatorException($violations->get(0)->getMessage()); + } + } } diff --git a/src/Command/Ide/IdeServiceStopCommand.php b/src/Command/Ide/IdeServiceStopCommand.php index d0e9476d8..039015a1d 100644 --- a/src/Command/Ide/IdeServiceStopCommand.php +++ b/src/Command/Ide/IdeServiceStopCommand.php @@ -1,6 +1,6 @@ addArgument('service', InputArgument::REQUIRED, 'The name of the service to stop') - ->addUsage('php') - ->addUsage('apache') - ->addUsage('mysql') - ->setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv()); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $this->requireCloudIdeEnvironment(); - $service = $input->getArgument('service'); - $this->validateService($service); - - $serviceNameMap = [ - 'apache' => 'apache2', - 'apache2' => 'apache2', - 'mysql' => 'mysqld', - 'mysqld' => 'mysqld', - 'php' => 'php-fpm', - 'php-fpm' => 'php-fpm', - ]; - $output->writeln("Stopping $service..."); - $serviceName = $serviceNameMap[$service]; - $this->stopService($serviceName); - $output->writeln("Stopped $service"); - - return Command::SUCCESS; - } - - private function validateService(string $service): void { - $violations = Validation::createValidator()->validate($service, [ - new Choice([ - 'choices' => ['php', 'php-fpm', 'apache', 'apache2', 'mysql', 'mysqld'], - 'message' => 'Specify a valid service name: php, apache, or mysql', - ]), - ]); - if (count($violations)) { - throw new ValidatorException($violations->get(0)->getMessage()); +final class IdeServiceStopCommand extends IdeCommandBase +{ + protected function configure(): void + { + $this + ->addArgument('service', InputArgument::REQUIRED, 'The name of the service to stop') + ->addUsage('php') + ->addUsage('apache') + ->addUsage('mysql') + ->setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv()); } - } + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->requireCloudIdeEnvironment(); + $service = $input->getArgument('service'); + $this->validateService($service); + + $serviceNameMap = [ + 'apache' => 'apache2', + 'apache2' => 'apache2', + 'mysql' => 'mysqld', + 'mysqld' => 'mysqld', + 'php' => 'php-fpm', + 'php-fpm' => 'php-fpm', + ]; + $output->writeln("Stopping $service..."); + $serviceName = $serviceNameMap[$service]; + $this->stopService($serviceName); + $output->writeln("Stopped $service"); + + return Command::SUCCESS; + } + private function validateService(string $service): void + { + $violations = Validation::createValidator()->validate($service, [ + new Choice([ + 'choices' => ['php', 'php-fpm', 'apache', 'apache2', 'mysql', 'mysqld'], + 'message' => 'Specify a valid service name: php, apache, or mysql', + ]), + ]); + if (count($violations)) { + throw new ValidatorException($violations->get(0)->getMessage()); + } + } } diff --git a/src/Command/Ide/IdeShareCommand.php b/src/Command/Ide/IdeShareCommand.php index 46fdf0d5a..f01240c89 100644 --- a/src/Command/Ide/IdeShareCommand.php +++ b/src/Command/Ide/IdeShareCommand.php @@ -1,6 +1,6 @@ + */ + private array $shareCodeFilepaths; - /** - * @var array - */ - private array $shareCodeFilepaths; - - protected function configure(): void { - $this - ->addOption('regenerate', '', InputOption::VALUE_NONE, 'regenerate the share code') - ->setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv()); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $this->requireCloudIdeEnvironment(); - - if ($input->getOption('regenerate')) { - $this->regenerateShareCode(); + protected function configure(): void + { + $this + ->addOption('regenerate', '', InputOption::VALUE_NONE, 'regenerate the share code') + ->setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv()); } - $shareUuid = $this->localMachineHelper->readFile($this->getShareCodeFilepaths()[0]); - $webUrl = self::getThisCloudIdeWebUrl(); + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->requireCloudIdeEnvironment(); - $this->output->writeln(''); - $this->output->writeln("Your IDE Share URL: https://$webUrl?share=$shareUuid"); + if ($input->getOption('regenerate')) { + $this->regenerateShareCode(); + } - return Command::SUCCESS; - } + $shareUuid = $this->localMachineHelper->readFile($this->getShareCodeFilepaths()[0]); + $webUrl = self::getThisCloudIdeWebUrl(); - public function setShareCodeFilepaths(array $filePath): void { - $this->shareCodeFilepaths = $filePath; - } + $this->output->writeln(''); + $this->output->writeln("Your IDE Share URL: https://$webUrl?share=$shareUuid"); - /** - * @return array - */ - private function getShareCodeFilepaths(): array { - if (!isset($this->shareCodeFilepaths)) { - $this->shareCodeFilepaths = [ - '/usr/local/share/ide/.sharecode', - '/home/ide/.sharecode', - ]; + return Command::SUCCESS; } - return $this->shareCodeFilepaths; - } - private function regenerateShareCode(): void { - $newShareCode = (string) Uuid::uuid4(); - foreach ($this->getShareCodeFilepaths() as $path) { - $this->localMachineHelper->writeFile($path, $newShareCode); + public function setShareCodeFilepaths(array $filePath): void + { + $this->shareCodeFilepaths = $filePath; } - } + /** + * @return array + */ + private function getShareCodeFilepaths(): array + { + if (!isset($this->shareCodeFilepaths)) { + $this->shareCodeFilepaths = [ + '/usr/local/share/ide/.sharecode', + '/home/ide/.sharecode', + ]; + } + return $this->shareCodeFilepaths; + } + + private function regenerateShareCode(): void + { + $newShareCode = (string) Uuid::uuid4(); + foreach ($this->getShareCodeFilepaths() as $path) { + $this->localMachineHelper->writeFile($path, $newShareCode); + } + } } diff --git a/src/Command/Ide/IdeXdebugToggleCommand.php b/src/Command/Ide/IdeXdebugToggleCommand.php index a003ca2ea..14d28f7b8 100644 --- a/src/Command/Ide/IdeXdebugToggleCommand.php +++ b/src/Command/Ide/IdeXdebugToggleCommand.php @@ -1,6 +1,6 @@ setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv()); + } - protected function configure(): void { - $this - ->setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv()); - } + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->requireCloudIdeEnvironment(); + $iniFile = $this->getXdebugIniFilePath(); + $contents = file_get_contents($iniFile); + $this->setXDebugStatus($contents); - protected function execute(InputInterface $input, OutputInterface $output): int { - $this->requireCloudIdeEnvironment(); - $iniFile = $this->getXdebugIniFilePath(); - $contents = file_get_contents($iniFile); - $this->setXDebugStatus($contents); + if ($this->getXDebugStatus() === false) { + $this->enableXDebug($iniFile, $contents); + } elseif ($this->getXDebugStatus() === true) { + $this->disableXDebug($iniFile, $contents); + } else { + throw new AcquiaCliException("Could not find xdebug zend extension in $iniFile!"); + } + $this->restartService('php-fpm'); - if ($this->getXDebugStatus() === FALSE) { - $this->enableXDebug($iniFile, $contents); - } - elseif ($this->getXDebugStatus() === TRUE) { - $this->disableXDebug($iniFile, $contents); - } - else { - throw new AcquiaCliException("Could not find xdebug zend extension in $iniFile!"); + return Command::SUCCESS; } - $this->restartService('php-fpm'); - return Command::SUCCESS; - } - - /** - * Sets $this->xDebugEnabled. - * - * @param string $contents The contents of php.ini. - */ - private function setXDebugStatus(string $contents): void { - if (str_contains($contents, ';zend_extension=xdebug.so')) { - $this->xDebugEnabled = FALSE; - } - elseif (str_contains($contents, 'zend_extension=xdebug.so')) { - $this->xDebugEnabled = TRUE; + /** + * Sets $this->xDebugEnabled. + * + * @param string $contents The contents of php.ini. + */ + private function setXDebugStatus(string $contents): void + { + if (str_contains($contents, ';zend_extension=xdebug.so')) { + $this->xDebugEnabled = false; + } elseif (str_contains($contents, 'zend_extension=xdebug.so')) { + $this->xDebugEnabled = true; + } else { + $this->xDebugEnabled = null; + } } - else { - $this->xDebugEnabled = NULL; - } - } - - private function getXDebugStatus(): ?bool { - return $this->xDebugEnabled; - } - /** - * Enables xDebug. - * - * @param string $contents The contents of php.ini. - */ - private function enableXDebug(string $destinationFile, string $contents): void { - $this->logger->notice("Enabling Xdebug PHP extension in $destinationFile..."); + private function getXDebugStatus(): ?bool + { + return $this->xDebugEnabled; + } - // Note that this replaces 1 or more ";" characters. - $newContents = preg_replace('/(;)+(zend_extension=xdebug\.so)/', '$2', $contents); - file_put_contents($destinationFile, $newContents); - $this->output->writeln("Xdebug PHP extension enabled."); - $this->output->writeln("You must also enable Xdebug listening in your code editor to begin a debugging session."); - } + /** + * Enables xDebug. + * + * @param string $contents The contents of php.ini. + */ + private function enableXDebug(string $destinationFile, string $contents): void + { + $this->logger->notice("Enabling Xdebug PHP extension in $destinationFile..."); - /** - * Disables xDebug. - * - * @param string $contents The contents of php.ini. - */ - private function disableXDebug(string $destinationFile, string $contents): void { - $this->logger->notice("Disabling Xdebug PHP extension in $destinationFile..."); - $newContents = preg_replace('/(;)*(zend_extension=xdebug\.so)/', ';$2', $contents); - file_put_contents($destinationFile, $newContents); - $this->output->writeln("Xdebug PHP extension disabled."); - } + // Note that this replaces 1 or more ";" characters. + $newContents = preg_replace('/(;)+(zend_extension=xdebug\.so)/', '$2', $contents); + file_put_contents($destinationFile, $newContents); + $this->output->writeln("Xdebug PHP extension enabled."); + $this->output->writeln("You must also enable Xdebug listening in your code editor to begin a debugging session."); + } + /** + * Disables xDebug. + * + * @param string $contents The contents of php.ini. + */ + private function disableXDebug(string $destinationFile, string $contents): void + { + $this->logger->notice("Disabling Xdebug PHP extension in $destinationFile..."); + $newContents = preg_replace('/(;)*(zend_extension=xdebug\.so)/', ';$2', $contents); + file_put_contents($destinationFile, $newContents); + $this->output->writeln("Xdebug PHP extension disabled."); + } } diff --git a/src/Command/Ide/Wizard/IdeWizardCommandBase.php b/src/Command/Ide/Wizard/IdeWizardCommandBase.php index 43cccbb19..34f43310f 100644 --- a/src/Command/Ide/Wizard/IdeWizardCommandBase.php +++ b/src/Command/Ide/Wizard/IdeWizardCommandBase.php @@ -1,6 +1,6 @@ setSshKeyFilepath(self::getSshKeyFilename($this::getThisCloudIdeUuid())); - $this->passphraseFilepath = $this->localMachineHelper->getLocalFilepath('~/.passphrase'); - } - - public static function getSshKeyFilename(mixed $ideUuid): string { - return 'id_rsa_acquia_ide_' . $ideUuid; - } + $this->setSshKeyFilepath(self::getSshKeyFilename($this::getThisCloudIdeUuid())); + $this->passphraseFilepath = $this->localMachineHelper->getLocalFilepath('~/.passphrase'); + } - protected function validateEnvironment(): void { - $this->requireCloudIdeEnvironment(); - } + public static function getSshKeyFilename(mixed $ideUuid): string + { + return 'id_rsa_acquia_ide_' . $ideUuid; + } - protected function getSshKeyLabel(): string { - return $this::getIdeSshKeyLabel(self::getThisCloudIdeLabel(), self::getThisCloudIdeUuid()); - } + protected function validateEnvironment(): void + { + $this->requireCloudIdeEnvironment(); + } - protected function deleteThisSshKeyFromCloud(mixed $output): void { - if ($cloudKey = $this->findIdeSshKeyOnCloud($this::getThisCloudIdeLabel(), $this::getThisCloudIdeUuid())) { - $this->deleteSshKeyFromCloud($output, $cloudKey); + protected function getSshKeyLabel(): string + { + return $this::getIdeSshKeyLabel(self::getThisCloudIdeLabel(), self::getThisCloudIdeUuid()); } - } + protected function deleteThisSshKeyFromCloud(mixed $output): void + { + if ($cloudKey = $this->findIdeSshKeyOnCloud($this::getThisCloudIdeLabel(), $this::getThisCloudIdeUuid())) { + $this->deleteSshKeyFromCloud($output, $cloudKey); + } + } } diff --git a/src/Command/Ide/Wizard/IdeWizardCreateSshKeyCommand.php b/src/Command/Ide/Wizard/IdeWizardCreateSshKeyCommand.php index d25b7beae..f8383b732 100644 --- a/src/Command/Ide/Wizard/IdeWizardCreateSshKeyCommand.php +++ b/src/Command/Ide/Wizard/IdeWizardCreateSshKeyCommand.php @@ -1,6 +1,6 @@ setHidden(!CommandBase::isAcquiaCloudIde()); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $checklist = new Checklist($output); - - // Get Cloud account. - $acquiaCloudClient = $this->cloudApiClientService->getClient(); - $accountAdapter = new Account($acquiaCloudClient); - $account = $accountAdapter->get(); - $this->validateRequiredCloudPermissions( - $acquiaCloudClient, - self::getThisCloudIdeCloudAppUuid(), - $account, - [ - // Add SSH key to git repository. - "add ssh key to git", - // Add SSH key to non-production environments. - "add ssh key to non-prod", - ] - ); - - $keyWasUploaded = FALSE; - // Create local SSH key. - if (!$this->localSshKeyExists() || !$this->passPhraseFileExists()) { - // Just in case the public key exists and the private doesn't, remove the public key. - $this->deleteLocalSshKey(); - // Just in case there's an orphaned key on the Cloud Platform for this Cloud IDE. - $this->deleteThisSshKeyFromCloud($output); - - $checklist->addItem('Creating a local SSH key'); - - // Create SSH key. - $password = md5(random_bytes(10)); - $this->savePassPhraseToFile($password); - $this->createSshKey($this->privateSshKeyFilename, $password); - - $checklist->completePreviousItem(); - $keyWasUploaded = TRUE; - } - else { - $checklist->addItem('Already created a local key'); - $checklist->completePreviousItem(); - } - - // Upload SSH key to the Cloud Platform. - if (!$this->userHasUploadedThisKeyToCloud($this->getSshKeyLabel())) { - $checklist->addItem('Uploading the local key to the Cloud Platform'); - - // Just in case there is an uploaded key, but it doesn't actually match - // the local key, delete remote key! - $this->deleteThisSshKeyFromCloud($output); - $publicKey = $this->localMachineHelper->readFile($this->publicSshKeyFilepath); - $this->uploadSshKey($this->getSshKeyLabel(), $publicKey); - - $checklist->completePreviousItem(); - $keyWasUploaded = TRUE; - } - else { - $checklist->addItem('Already uploaded the local key to the Cloud Platform'); - $checklist->completePreviousItem(); +final class IdeWizardCreateSshKeyCommand extends IdeWizardCommandBase +{ + protected function configure(): void + { + $this + ->setHidden(!CommandBase::isAcquiaCloudIde()); } - // Add SSH key to local keychain. - if (!$this->sshKeyIsAddedToKeychain()) { - $checklist->addItem('Adding the SSH key to local keychain'); - $this->addSshKeyToAgent($this->publicSshKeyFilepath, $this->getPassPhraseFromFile()); - } - else { - $checklist->addItem('Already added the SSH key to local keychain'); - } - $checklist->completePreviousItem(); + protected function execute(InputInterface $input, OutputInterface $output): int + { + $checklist = new Checklist($output); + + // Get Cloud account. + $acquiaCloudClient = $this->cloudApiClientService->getClient(); + $accountAdapter = new Account($acquiaCloudClient); + $account = $accountAdapter->get(); + $this->validateRequiredCloudPermissions( + $acquiaCloudClient, + self::getThisCloudIdeCloudAppUuid(), + $account, + [ + // Add SSH key to git repository. + "add ssh key to git", + // Add SSH key to non-production environments. + "add ssh key to non-prod", + ] + ); + + $keyWasUploaded = false; + // Create local SSH key. + if (!$this->localSshKeyExists() || !$this->passPhraseFileExists()) { + // Just in case the public key exists and the private doesn't, remove the public key. + $this->deleteLocalSshKey(); + // Just in case there's an orphaned key on the Cloud Platform for this Cloud IDE. + $this->deleteThisSshKeyFromCloud($output); + + $checklist->addItem('Creating a local SSH key'); + + // Create SSH key. + $password = md5(random_bytes(10)); + $this->savePassPhraseToFile($password); + $this->createSshKey($this->privateSshKeyFilename, $password); + + $checklist->completePreviousItem(); + $keyWasUploaded = true; + } else { + $checklist->addItem('Already created a local key'); + $checklist->completePreviousItem(); + } + + // Upload SSH key to the Cloud Platform. + if (!$this->userHasUploadedThisKeyToCloud($this->getSshKeyLabel())) { + $checklist->addItem('Uploading the local key to the Cloud Platform'); + + // Just in case there is an uploaded key, but it doesn't actually match + // the local key, delete remote key! + $this->deleteThisSshKeyFromCloud($output); + $publicKey = $this->localMachineHelper->readFile($this->publicSshKeyFilepath); + $this->uploadSshKey($this->getSshKeyLabel(), $publicKey); + + $checklist->completePreviousItem(); + $keyWasUploaded = true; + } else { + $checklist->addItem('Already uploaded the local key to the Cloud Platform'); + $checklist->completePreviousItem(); + } + + // Add SSH key to local keychain. + if (!$this->sshKeyIsAddedToKeychain()) { + $checklist->addItem('Adding the SSH key to local keychain'); + $this->addSshKeyToAgent($this->publicSshKeyFilepath, $this->getPassPhraseFromFile()); + } else { + $checklist->addItem('Already added the SSH key to local keychain'); + } + $checklist->completePreviousItem(); + + // Wait for the key to register on the Cloud Platform. + if ($keyWasUploaded) { + if ($this->input->isInteractive() && !$this->promptWaitForSsh($this->io)) { + $this->io->success('Your SSH key has been successfully uploaded to the Cloud Platform.'); + return Command::SUCCESS; + } + $this->pollAcquiaCloudUntilSshSuccess($output); + } - // Wait for the key to register on the Cloud Platform. - if ($keyWasUploaded) { - if ($this->input->isInteractive() && !$this->promptWaitForSsh($this->io)) { - $this->io->success('Your SSH key has been successfully uploaded to the Cloud Platform.'); return Command::SUCCESS; - } - $this->pollAcquiaCloudUntilSshSuccess($output); } - - return Command::SUCCESS; - } - } diff --git a/src/Command/Ide/Wizard/IdeWizardDeleteSshKeyCommand.php b/src/Command/Ide/Wizard/IdeWizardDeleteSshKeyCommand.php index d1a07faa9..6826f4f69 100644 --- a/src/Command/Ide/Wizard/IdeWizardDeleteSshKeyCommand.php +++ b/src/Command/Ide/Wizard/IdeWizardDeleteSshKeyCommand.php @@ -1,6 +1,6 @@ setHidden(!CommandBase::isAcquiaCloudIde()); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $this->requireCloudIdeEnvironment(); - - $cloudKey = $this->findIdeSshKeyOnCloud($this::getThisCloudIdeLabel(), $this::getThisCloudIdeUuid()); - if (!$cloudKey) { - throw new AcquiaCliException('Could not find an SSH key on the Cloud Platform matching any local key in this IDE.'); +final class IdeWizardDeleteSshKeyCommand extends IdeWizardCommandBase +{ + use SshCommandTrait; + + protected function configure(): void + { + $this + ->setHidden(!CommandBase::isAcquiaCloudIde()); } - $this->deleteSshKeyFromCloud($output, $cloudKey); - $this->deleteLocalSshKey(); + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->requireCloudIdeEnvironment(); + + $cloudKey = $this->findIdeSshKeyOnCloud($this::getThisCloudIdeLabel(), $this::getThisCloudIdeUuid()); + if (!$cloudKey) { + throw new AcquiaCliException('Could not find an SSH key on the Cloud Platform matching any local key in this IDE.'); + } - $this->output->writeln("Deleted local files {$this->publicSshKeyFilepath} and {$this->privateSshKeyFilepath}"); + $this->deleteSshKeyFromCloud($output, $cloudKey); + $this->deleteLocalSshKey(); - return Command::SUCCESS; - } + $this->output->writeln("Deleted local files {$this->publicSshKeyFilepath} and {$this->privateSshKeyFilepath}"); + return Command::SUCCESS; + } } diff --git a/src/Command/Pull/PullCodeCommand.php b/src/Command/Pull/PullCodeCommand.php index b4a57409a..add6093b1 100644 --- a/src/Command/Pull/PullCodeCommand.php +++ b/src/Command/Pull/PullCodeCommand.php @@ -1,6 +1,6 @@ acceptEnvironmentId() - ->addOption('dir', NULL, InputArgument::OPTIONAL, 'The directory containing the Drupal project to be refreshed') - ->addOption('no-scripts', NULL, InputOption::VALUE_NONE, - 'Do not run any additional scripts after code is pulled. E.g., composer install , drush cache-rebuild, etc.'); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $this->setDirAndRequireProjectCwd($input); - $clone = $this->determineCloneProject($output); - $sourceEnvironment = $this->determineEnvironment($input, $output, TRUE); - $this->pullCode($input, $output, $clone, $sourceEnvironment); - $this->checkEnvironmentPhpVersions($sourceEnvironment); - $this->matchIdePhpVersion($output, $sourceEnvironment); - if (!$input->getOption('no-scripts')) { - $outputCallback = $this->getOutputCallback($output, $this->checklist); - $this->runComposerScripts($outputCallback, $this->checklist); - $this->runDrushCacheClear($outputCallback, $this->checklist); +final class PullCodeCommand extends PullCommandBase +{ + protected function configure(): void + { + $this + ->acceptEnvironmentId() + ->addOption('dir', null, InputArgument::OPTIONAL, 'The directory containing the Drupal project to be refreshed') + ->addOption( + 'no-scripts', + null, + InputOption::VALUE_NONE, + 'Do not run any additional scripts after code is pulled. E.g., composer install , drush cache-rebuild, etc.' + ); } - return Command::SUCCESS; - } - + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->setDirAndRequireProjectCwd($input); + $clone = $this->determineCloneProject($output); + $sourceEnvironment = $this->determineEnvironment($input, $output, true); + $this->pullCode($input, $output, $clone, $sourceEnvironment); + $this->checkEnvironmentPhpVersions($sourceEnvironment); + $this->matchIdePhpVersion($output, $sourceEnvironment); + if (!$input->getOption('no-scripts')) { + $outputCallback = $this->getOutputCallback($output, $this->checklist); + $this->runComposerScripts($outputCallback, $this->checklist); + $this->runDrushCacheClear($outputCallback, $this->checklist); + } + + return Command::SUCCESS; + } } diff --git a/src/Command/Pull/PullCommand.php b/src/Command/Pull/PullCommand.php index 7c632dac8..b01d0440a 100644 --- a/src/Command/Pull/PullCommand.php +++ b/src/Command/Pull/PullCommand.php @@ -1,6 +1,6 @@ acceptEnvironmentId() - ->acceptSite() - ->addOption('dir', NULL, InputArgument::OPTIONAL, 'The directory containing the Drupal project to be refreshed') - ->addOption('no-code', NULL, InputOption::VALUE_NONE, 'Do not refresh code from remote repository') - ->addOption('no-files', NULL, InputOption::VALUE_NONE, 'Do not refresh files') - ->addOption('no-databases', NULL, InputOption::VALUE_NONE, 'Do not refresh databases') - ->addOption( +final class PullCommand extends PullCommandBase +{ + protected function configure(): void + { + $this + ->acceptEnvironmentId() + ->acceptSite() + ->addOption('dir', null, InputArgument::OPTIONAL, 'The directory containing the Drupal project to be refreshed') + ->addOption('no-code', null, InputOption::VALUE_NONE, 'Do not refresh code from remote repository') + ->addOption('no-files', null, InputOption::VALUE_NONE, 'Do not refresh files') + ->addOption('no-databases', null, InputOption::VALUE_NONE, 'Do not refresh databases') + ->addOption( 'no-scripts', - NULL, + null, InputOption::VALUE_NONE, 'Do not run any additional scripts after code and database are copied. E.g., composer install , drush cache-rebuild, etc.' ); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $this->setDirAndRequireProjectCwd($input); - $clone = $this->determineCloneProject($output); - $sourceEnvironment = $this->determineEnvironment($input, $output, TRUE); - - if (!$input->getOption('no-code')) { - $this->pullCode($input, $output, $clone, $sourceEnvironment); } - if (!$input->getOption('no-files')) { - $this->pullFiles($input, $output, $sourceEnvironment); - } + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->setDirAndRequireProjectCwd($input); + $clone = $this->determineCloneProject($output); + $sourceEnvironment = $this->determineEnvironment($input, $output, true); - if (!$input->getOption('no-databases')) { - $this->pullDatabase($input, $output, $sourceEnvironment); - } + if (!$input->getOption('no-code')) { + $this->pullCode($input, $output, $clone, $sourceEnvironment); + } - if (!$input->getOption('no-scripts')) { - $this->executeAllScripts($this->getOutputCallback($output, $this->checklist), $this->checklist); - } + if (!$input->getOption('no-files')) { + $this->pullFiles($input, $output, $sourceEnvironment); + } + + if (!$input->getOption('no-databases')) { + $this->pullDatabase($input, $output, $sourceEnvironment); + } - return Command::SUCCESS; - } + if (!$input->getOption('no-scripts')) { + $this->executeAllScripts($this->getOutputCallback($output, $this->checklist), $this->checklist); + } + return Command::SUCCESS; + } } diff --git a/src/Command/Pull/PullCommandBase.php b/src/Command/Pull/PullCommandBase.php index ce18ba740..84b93117a 100644 --- a/src/Command/Pull/PullCommandBase.php +++ b/src/Command/Pull/PullCommandBase.php @@ -1,6 +1,6 @@ localMachineHelper, $this->datastoreCloud, $this->datastoreAcli, $this->cloudCredentials, $this->telemetryHelper, $this->projectDir, $this->cloudApiClientService, $this->sshHelper, $this->sshDir, $logger); - } - - /** - * @see https://github.com/drush-ops/drush/blob/c21a5a24a295cc0513bfdecead6f87f1a2cf91a2/src/Sql/SqlMysql.php#L168 - * @return string[] - */ - private function listTables(string $out): array { - $tables = []; - if ($out = trim($out)) { - $tables = explode(PHP_EOL, $out); - } - return $tables; - } - - /** - * @see https://github.com/drush-ops/drush/blob/c21a5a24a295cc0513bfdecead6f87f1a2cf91a2/src/Sql/SqlMysql.php#L178 - * @return string[] - */ - private function listTablesQuoted(string $out): array { - $tables = $this->listTables($out); - foreach ($tables as &$table) { - $table = "`$table`"; - } - return $tables; - } - - public static function getBackupPath(object $environment, DatabaseResponse $database, object $backupResponse): string { - // Databases have a machine name not exposed via the API; we can only - // approximately reconstruct it and match the filename you'd get downloading - // a backup from Cloud UI. - if ($database->flags->default) { - $dbMachineName = $database->name . $environment->name; - } - else { - $dbMachineName = 'db' . $database->id; - } - $filename = implode('-', [ +abstract class PullCommandBase extends CommandBase +{ + use IdeCommandTrait; + + protected Checklist $checklist; + + private string $site; + + private UriInterface $backupDownloadUrl; + + public function __construct( + public LocalMachineHelper $localMachineHelper, + protected CloudDataStore $datastoreCloud, + protected AcquiaCliDatastore $datastoreAcli, + protected ApiCredentialsInterface $cloudCredentials, + protected TelemetryHelper $telemetryHelper, + protected string $projectDir, + protected ClientService $cloudApiClientService, + public SshHelper $sshHelper, + protected string $sshDir, + LoggerInterface $logger, + protected \GuzzleHttp\Client $httpClient + ) { + parent::__construct($this->localMachineHelper, $this->datastoreCloud, $this->datastoreAcli, $this->cloudCredentials, $this->telemetryHelper, $this->projectDir, $this->cloudApiClientService, $this->sshHelper, $this->sshDir, $logger); + } + + /** + * @see https://github.com/drush-ops/drush/blob/c21a5a24a295cc0513bfdecead6f87f1a2cf91a2/src/Sql/SqlMysql.php#L168 + * @return string[] + */ + private function listTables(string $out): array + { + $tables = []; + if ($out = trim($out)) { + $tables = explode(PHP_EOL, $out); + } + return $tables; + } + + /** + * @see https://github.com/drush-ops/drush/blob/c21a5a24a295cc0513bfdecead6f87f1a2cf91a2/src/Sql/SqlMysql.php#L178 + * @return string[] + */ + private function listTablesQuoted(string $out): array + { + $tables = $this->listTables($out); + foreach ($tables as &$table) { + $table = "`$table`"; + } + return $tables; + } + + public static function getBackupPath(object $environment, DatabaseResponse $database, object $backupResponse): string + { + // Databases have a machine name not exposed via the API; we can only + // approximately reconstruct it and match the filename you'd get downloading + // a backup from Cloud UI. + if ($database->flags->default) { + $dbMachineName = $database->name . $environment->name; + } else { + $dbMachineName = 'db' . $database->id; + } + $filename = implode('-', [ $environment->name, $database->name, $dbMachineName, $backupResponse->completedAt, - ]) . '.sql.gz'; - return Path::join(sys_get_temp_dir(), $filename); - } - - protected function initialize(InputInterface $input, OutputInterface $output): void { - parent::initialize($input, $output); - $this->checklist = new Checklist($output); - } - - protected function pullCode(InputInterface $input, OutputInterface $output, bool $clone, EnvironmentResponse $sourceEnvironment): void { - if ($clone) { - $this->checklist->addItem('Cloning git repository from the Cloud Platform'); - $this->cloneFromCloud($sourceEnvironment, $this->getOutputCallback($output, $this->checklist)); - } - else { - $this->checklist->addItem('Pulling code from the Cloud Platform'); - $this->pullCodeFromCloud($sourceEnvironment, $this->getOutputCallback($output, $this->checklist)); + ]) . '.sql.gz'; + return Path::join(sys_get_temp_dir(), $filename); } - $this->checklist->completePreviousItem(); - } - - /** - * @param bool $onDemand Force on-demand backup. - * @param bool $noImport Skip import. - */ - protected function pullDatabase(InputInterface $input, OutputInterface $output, EnvironmentResponse $sourceEnvironment, bool $onDemand = FALSE, bool $noImport = FALSE, bool $multipleDbs = FALSE): void { - if (!$noImport) { - // Verify database connection. - $this->connectToLocalDatabase($this->getLocalDbHost(), $this->getLocalDbUser(), $this->getLocalDbName(), $this->getLocalDbPassword(), $this->getOutputCallback($output, $this->checklist)); + + protected function initialize(InputInterface $input, OutputInterface $output): void + { + parent::initialize($input, $output); + $this->checklist = new Checklist($output); } - $acquiaCloudClient = $this->cloudApiClientService->getClient(); - $site = $this->determineSite($sourceEnvironment, $input); - $databases = $this->determineCloudDatabases($acquiaCloudClient, $sourceEnvironment, $site, $multipleDbs); - - foreach ($databases as $database) { - if ($onDemand) { - $this->checklist->addItem("Creating an on-demand database(s) backup on Cloud Platform"); - $this->createBackup($sourceEnvironment, $database, $acquiaCloudClient); - $this->checklist->completePreviousItem(); - } - $backupResponse = $this->getDatabaseBackup($acquiaCloudClient, $sourceEnvironment, $database); - if (!$onDemand) { - $this->printDatabaseBackupInfo($backupResponse, $sourceEnvironment); - } - - $this->checklist->addItem("Downloading $database->name database copy from the Cloud Platform"); - $localFilepath = $this->downloadDatabaseBackup($sourceEnvironment, $database, $backupResponse, $this->getOutputCallback($output, $this->checklist)); - $this->checklist->completePreviousItem(); - - if ($noImport) { - $this->io->success("$database->name database backup downloaded to $localFilepath"); - } - else { - $this->checklist->addItem("Importing $database->name database download"); - $this->importRemoteDatabase($database, $localFilepath, $this->getOutputCallback($output, $this->checklist)); + + protected function pullCode(InputInterface $input, OutputInterface $output, bool $clone, EnvironmentResponse $sourceEnvironment): void + { + if ($clone) { + $this->checklist->addItem('Cloning git repository from the Cloud Platform'); + $this->cloneFromCloud($sourceEnvironment, $this->getOutputCallback($output, $this->checklist)); + } else { + $this->checklist->addItem('Pulling code from the Cloud Platform'); + $this->pullCodeFromCloud($sourceEnvironment, $this->getOutputCallback($output, $this->checklist)); + } $this->checklist->completePreviousItem(); - } - } - } - - protected function pullFiles(InputInterface $input, OutputInterface $output, EnvironmentResponse $sourceEnvironment): void { - $this->checklist->addItem('Copying Drupal\'s public files from the Cloud Platform'); - $site = $this->determineSite($sourceEnvironment, $input); - $this->rsyncFilesFromCloud($sourceEnvironment, $this->getOutputCallback($output, $this->checklist), $site); - $this->checklist->completePreviousItem(); - } - - private function pullCodeFromCloud(EnvironmentResponse $chosenEnvironment, Closure $outputCallback = NULL): void { - $isDirty = $this->isLocalGitRepoDirty(); - if ($isDirty) { - throw new AcquiaCliException('Pulling code from your Cloud Platform environment was aborted because your local Git repository has uncommitted changes. Either commit, reset, or stash your changes via git.'); - } - // @todo Validate that an Acquia remote is configured for this repository. - $this->localMachineHelper->checkRequiredBinariesExist(['git']); - $this->localMachineHelper->execute([ - 'git', - 'fetch', - '--all', - ], $outputCallback, $this->dir, FALSE); - $this->checkoutBranchFromEnv($chosenEnvironment, $outputCallback); - } - - /** - * Checks out the matching branch from a source environment. - */ - private function checkoutBranchFromEnv(EnvironmentResponse $environment, Closure $outputCallback = NULL): void { - $this->localMachineHelper->checkRequiredBinariesExist(['git']); - $this->localMachineHelper->execute([ - 'git', - 'checkout', - $environment->vcs->path, - ], $outputCallback, $this->dir, FALSE); - } - - private function doImportRemoteDatabase( - string $databaseHost, - string $databaseUser, - string $databaseName, - string $databasePassword, - string $localFilepath, - Closure $outputCallback = NULL - ): void { - $this->dropDbTables($databaseHost, $databaseUser, $databaseName, $databasePassword, $outputCallback); - $this->importDatabaseDump($localFilepath, $databaseHost, $databaseUser, $databaseName, $databasePassword, $outputCallback); - $this->localMachineHelper->getFilesystem()->remove($localFilepath); - } - - private function downloadDatabaseBackup( - EnvironmentResponse $environment, - DatabaseResponse $database, - BackupResponse $backupResponse, - callable $outputCallback = NULL - ): string { - if ($outputCallback) { - $outputCallback('out', "Downloading backup $backupResponse->id"); - } - $localFilepath = self::getBackupPath($environment, $database, $backupResponse); - if ($this->output instanceof ConsoleOutput) { - $output = $this->output->section(); } - else { - $output = $this->output; + + /** + * @param bool $onDemand Force on-demand backup. + * @param bool $noImport Skip import. + */ + protected function pullDatabase(InputInterface $input, OutputInterface $output, EnvironmentResponse $sourceEnvironment, bool $onDemand = false, bool $noImport = false, bool $multipleDbs = false): void + { + if (!$noImport) { + // Verify database connection. + $this->connectToLocalDatabase($this->getLocalDbHost(), $this->getLocalDbUser(), $this->getLocalDbName(), $this->getLocalDbPassword(), $this->getOutputCallback($output, $this->checklist)); + } + $acquiaCloudClient = $this->cloudApiClientService->getClient(); + $site = $this->determineSite($sourceEnvironment, $input); + $databases = $this->determineCloudDatabases($acquiaCloudClient, $sourceEnvironment, $site, $multipleDbs); + + foreach ($databases as $database) { + if ($onDemand) { + $this->checklist->addItem("Creating an on-demand database(s) backup on Cloud Platform"); + $this->createBackup($sourceEnvironment, $database, $acquiaCloudClient); + $this->checklist->completePreviousItem(); + } + $backupResponse = $this->getDatabaseBackup($acquiaCloudClient, $sourceEnvironment, $database); + if (!$onDemand) { + $this->printDatabaseBackupInfo($backupResponse, $sourceEnvironment); + } + + $this->checklist->addItem("Downloading $database->name database copy from the Cloud Platform"); + $localFilepath = $this->downloadDatabaseBackup($sourceEnvironment, $database, $backupResponse, $this->getOutputCallback($output, $this->checklist)); + $this->checklist->completePreviousItem(); + + if ($noImport) { + $this->io->success("$database->name database backup downloaded to $localFilepath"); + } else { + $this->checklist->addItem("Importing $database->name database download"); + $this->importRemoteDatabase($database, $localFilepath, $this->getOutputCallback($output, $this->checklist)); + $this->checklist->completePreviousItem(); + } + } } - // These options tell curl to stream the file to disk rather than loading it into memory. - $acquiaCloudClient = $this->cloudApiClientService->getClient(); - $acquiaCloudClient->addOption('sink', $localFilepath); - $acquiaCloudClient->addOption('curl.options', [ - 'CURLOPT_FILE' => $localFilepath, - 'CURLOPT_RETURNTRANSFER' => FALSE, -]); - $acquiaCloudClient->addOption('progress', static function (mixed $totalBytes, mixed $downloadedBytes) use (&$progress, $output): void { - self::displayDownloadProgress($totalBytes, $downloadedBytes, $progress, $output); - }); - // This is really just used to allow us to inject values for $url during testing. - // It should be empty during normal operations. - $url = $this->getBackupDownloadUrl(); - $acquiaCloudClient->addOption('on_stats', function (TransferStats $stats) use (&$url): void { - $url = $stats->getEffectiveUri(); - }); - - try { - $acquiaCloudClient->stream("get", "/environments/$environment->uuid/databases/$database->name/backups/$backupResponse->id/actions/download", $acquiaCloudClient->getOptions()); - return $localFilepath; + + protected function pullFiles(InputInterface $input, OutputInterface $output, EnvironmentResponse $sourceEnvironment): void + { + $this->checklist->addItem('Copying Drupal\'s public files from the Cloud Platform'); + $site = $this->determineSite($sourceEnvironment, $input); + $this->rsyncFilesFromCloud($sourceEnvironment, $this->getOutputCallback($output, $this->checklist), $site); + $this->checklist->completePreviousItem(); } - catch (RequestException $exception) { - // Deal with broken SSL certificates. - // @see https://timi.eu/docs/anatella/5_1_9_1_list_of_curl_error_codes.html - if (in_array($exception->getHandlerContext()['errno'], [51, 60], TRUE)) { - $outputCallback('out', 'The certificate for ' . $url->getHost() . ' is invalid.'); - assert($url !== NULL); - $domainsResource = new Domains($this->cloudApiClientService->getClient()); - $domains = $domainsResource->getAll($environment->uuid); - foreach ($domains as $domain) { - if ($domain->hostname === $url->getHost()) { - continue; - } - $outputCallback('out', 'Trying alternative host ' . $domain->hostname . ' '); - $downloadUrl = $url->withHost($domain->hostname); - try { - $this->httpClient->request('GET', $downloadUrl, ['sink' => $localFilepath]); + + private function pullCodeFromCloud(EnvironmentResponse $chosenEnvironment, Closure $outputCallback = null): void + { + $isDirty = $this->isLocalGitRepoDirty(); + if ($isDirty) { + throw new AcquiaCliException('Pulling code from your Cloud Platform environment was aborted because your local Git repository has uncommitted changes. Either commit, reset, or stash your changes via git.'); + } + // @todo Validate that an Acquia remote is configured for this repository. + $this->localMachineHelper->checkRequiredBinariesExist(['git']); + $this->localMachineHelper->execute([ + 'git', + 'fetch', + '--all', + ], $outputCallback, $this->dir, false); + $this->checkoutBranchFromEnv($chosenEnvironment, $outputCallback); + } + + /** + * Checks out the matching branch from a source environment. + */ + private function checkoutBranchFromEnv(EnvironmentResponse $environment, Closure $outputCallback = null): void + { + $this->localMachineHelper->checkRequiredBinariesExist(['git']); + $this->localMachineHelper->execute([ + 'git', + 'checkout', + $environment->vcs->path, + ], $outputCallback, $this->dir, false); + } + + private function doImportRemoteDatabase( + string $databaseHost, + string $databaseUser, + string $databaseName, + string $databasePassword, + string $localFilepath, + Closure $outputCallback = null + ): void { + $this->dropDbTables($databaseHost, $databaseUser, $databaseName, $databasePassword, $outputCallback); + $this->importDatabaseDump($localFilepath, $databaseHost, $databaseUser, $databaseName, $databasePassword, $outputCallback); + $this->localMachineHelper->getFilesystem()->remove($localFilepath); + } + + private function downloadDatabaseBackup( + EnvironmentResponse $environment, + DatabaseResponse $database, + BackupResponse $backupResponse, + callable $outputCallback = null + ): string { + if ($outputCallback) { + $outputCallback('out', "Downloading backup $backupResponse->id"); + } + $localFilepath = self::getBackupPath($environment, $database, $backupResponse); + if ($this->output instanceof ConsoleOutput) { + $output = $this->output->section(); + } else { + $output = $this->output; + } + // These options tell curl to stream the file to disk rather than loading it into memory. + $acquiaCloudClient = $this->cloudApiClientService->getClient(); + $acquiaCloudClient->addOption('sink', $localFilepath); + $acquiaCloudClient->addOption('curl.options', [ + 'CURLOPT_FILE' => $localFilepath, + 'CURLOPT_RETURNTRANSFER' => false, + ]); + $acquiaCloudClient->addOption( + 'progress', + static function (mixed $totalBytes, mixed $downloadedBytes) use (&$progress, $output): void { + self::displayDownloadProgress($totalBytes, $downloadedBytes, $progress, $output); + } + ); + // This is really just used to allow us to inject values for $url during testing. + // It should be empty during normal operations. + $url = $this->getBackupDownloadUrl(); + $acquiaCloudClient->addOption('on_stats', function (TransferStats $stats) use (&$url): void { + $url = $stats->getEffectiveUri(); + }); + + try { + $acquiaCloudClient->stream( + "get", + "/environments/$environment->uuid/databases/$database->name/backups/$backupResponse->id/actions/download", + $acquiaCloudClient->getOptions() + ); return $localFilepath; - } - catch (Exception) { - // Continue in the foreach() loop. - } + } catch (RequestException $exception) { + // Deal with broken SSL certificates. + // @see https://timi.eu/docs/anatella/5_1_9_1_list_of_curl_error_codes.html + if (in_array($exception->getHandlerContext()['errno'], [51, 60], true)) { + $outputCallback('out', 'The certificate for ' . $url->getHost() . ' is invalid.'); + assert($url !== null); + $domainsResource = new Domains($this->cloudApiClientService->getClient()); + $domains = $domainsResource->getAll($environment->uuid); + foreach ($domains as $domain) { + if ($domain->hostname === $url->getHost()) { + continue; + } + $outputCallback('out', 'Trying alternative host ' . $domain->hostname . ' '); + $downloadUrl = $url->withHost($domain->hostname); + try { + $this->httpClient->request('GET', $downloadUrl, ['sink' => $localFilepath]); + return $localFilepath; + } catch (Exception) { + // Continue in the foreach() loop. + } + } + } } - } - } - // If we looped through all domains and got here, we didn't download anything. - throw new AcquiaCliException('Could not download backup'); - } - - public function setBackupDownloadUrl(UriInterface $url): void { - $this->backupDownloadUrl = $url; - } - - private function getBackupDownloadUrl(): ?UriInterface { - return $this->backupDownloadUrl ?? NULL; - } - - public static function displayDownloadProgress(mixed $totalBytes, mixed $downloadedBytes, mixed &$progress, OutputInterface $output): void { - if ($totalBytes > 0 && is_null($progress)) { - $progress = new ProgressBar($output, $totalBytes); - $progress->setFormat(' %current%/%max% [%bar%] %percent:3s%%'); - $progress->setProgressCharacter('💧'); - $progress->setOverwrite(TRUE); - $progress->start(); + // If we looped through all domains and got here, we didn't download anything. + throw new AcquiaCliException('Could not download backup'); } - if (!is_null($progress)) { - if ($totalBytes === $downloadedBytes && $progress->getProgressPercent() !== 1.0) { - $progress->finish(); - if ($output instanceof ConsoleSectionOutput) { - $output->clear(); - } - return; - } - $progress->setProgress($downloadedBytes); + public function setBackupDownloadUrl(UriInterface $url): void + { + $this->backupDownloadUrl = $url; } - } - - /** - * Create an on-demand backup and wait for it to become available. - */ - private function createBackup(EnvironmentResponse $environment, DatabaseResponse $database, Client $acquiaCloudClient): void { - $backups = new DatabaseBackups($acquiaCloudClient); - $response = $backups->create($environment->uuid, $database->name); - $urlParts = explode('/', $response->links->notification->href); - $notificationUuid = end($urlParts); - $this->waitForBackup($notificationUuid, $acquiaCloudClient); - } - - /** - * Wait for an on-demand backup to become available (Cloud API notification). - * - * @infection-ignore-all - */ - protected function waitForBackup(string $notificationUuid, Client $acquiaCloudClient): void { - $spinnerMessage = 'Waiting for database backup to complete...'; - $successCallback = function (): void { - $this->output->writeln(''); - $this->output->writeln('Database backup is ready!'); - }; - $this->waitForNotificationToComplete($acquiaCloudClient, $notificationUuid, $spinnerMessage, $successCallback); - Loop::run(); - } - - private function connectToLocalDatabase(string $dbHost, string $dbUser, string $dbName, string $dbPassword, callable $outputCallback = NULL): void { - if ($outputCallback) { - $outputCallback('out', "Connecting to database $dbName"); + + private function getBackupDownloadUrl(): ?UriInterface + { + return $this->backupDownloadUrl ?? null; } - $this->localMachineHelper->checkRequiredBinariesExist(['mysql']); - $command = [ - 'mysql', - '--host', - $dbHost, - '--user', - $dbUser, - $dbName, - ]; - $process = $this->localMachineHelper->execute($command, $outputCallback, NULL, FALSE, NULL, ['MYSQL_PWD' => $dbPassword]); - if (!$process->isSuccessful()) { - throw new AcquiaCliException('Unable to connect to local database using credentials mysql://{user}:{password}@{host}/{database}. {message}', [ - 'database' => $dbName, - 'host' => $dbHost, - 'message' => $process->getErrorOutput(), - 'password' => $dbPassword, - 'user' => $dbUser, - ]); + + public static function displayDownloadProgress(mixed $totalBytes, mixed $downloadedBytes, mixed &$progress, OutputInterface $output): void + { + if ($totalBytes > 0 && is_null($progress)) { + $progress = new ProgressBar($output, $totalBytes); + $progress->setFormat(' %current%/%max% [%bar%] %percent:3s%%'); + $progress->setProgressCharacter('💧'); + $progress->setOverwrite(true); + $progress->start(); + } + + if (!is_null($progress)) { + if ($totalBytes === $downloadedBytes && $progress->getProgressPercent() !== 1.0) { + $progress->finish(); + if ($output instanceof ConsoleSectionOutput) { + $output->clear(); + } + return; + } + $progress->setProgress($downloadedBytes); + } } - } - private function dropDbTables(string $dbHost, string $dbUser, string $dbName, string $dbPassword, ?\Closure $outputCallback = NULL): void { - if ($outputCallback) { - $outputCallback('out', "Dropping tables from database $dbName"); + /** + * Create an on-demand backup and wait for it to become available. + */ + private function createBackup(EnvironmentResponse $environment, DatabaseResponse $database, Client $acquiaCloudClient): void + { + $backups = new DatabaseBackups($acquiaCloudClient); + $response = $backups->create($environment->uuid, $database->name); + $urlParts = explode('/', $response->links->notification->href); + $notificationUuid = end($urlParts); + $this->waitForBackup($notificationUuid, $acquiaCloudClient); + } + + /** + * Wait for an on-demand backup to become available (Cloud API notification). + * + * @infection-ignore-all + */ + protected function waitForBackup(string $notificationUuid, Client $acquiaCloudClient): void + { + $spinnerMessage = 'Waiting for database backup to complete...'; + $successCallback = function (): void { + $this->output->writeln(''); + $this->output->writeln('Database backup is ready!'); + }; + $this->waitForNotificationToComplete($acquiaCloudClient, $notificationUuid, $spinnerMessage, $successCallback); + Loop::run(); + } + + private function connectToLocalDatabase(string $dbHost, string $dbUser, string $dbName, string $dbPassword, callable $outputCallback = null): void + { + if ($outputCallback) { + $outputCallback('out', "Connecting to database $dbName"); + } + $this->localMachineHelper->checkRequiredBinariesExist(['mysql']); + $command = [ + 'mysql', + '--host', + $dbHost, + '--user', + $dbUser, + $dbName, + ]; + $process = $this->localMachineHelper->execute($command, $outputCallback, null, false, null, ['MYSQL_PWD' => $dbPassword]); + if (!$process->isSuccessful()) { + throw new AcquiaCliException('Unable to connect to local database using credentials mysql://{user}:{password}@{host}/{database}. {message}', [ + 'database' => $dbName, + 'host' => $dbHost, + 'message' => $process->getErrorOutput(), + 'password' => $dbPassword, + 'user' => $dbUser, + ]); + } } - $this->localMachineHelper->checkRequiredBinariesExist(['mysql']); - $command = [ - 'mysql', - '--host', - $dbHost, - '--user', - $dbUser, - $dbName, - '--silent', - '-e', - 'SHOW TABLES;', - ]; - $process = $this->localMachineHelper->execute($command, $outputCallback, NULL, FALSE, NULL, ['MYSQL_PWD' => $dbPassword]); - $tables = $this->listTablesQuoted($process->getOutput()); - if ($tables) { - $sql = 'DROP TABLE ' . implode(', ', $tables); - $tempnam = $this->localMachineHelper->getFilesystem()->tempnam(sys_get_temp_dir(), 'acli_drop_table_', '.sql'); - $this->localMachineHelper->getFilesystem()->dumpFile($tempnam, $sql); - $command = [ + + private function dropDbTables(string $dbHost, string $dbUser, string $dbName, string $dbPassword, ?\Closure $outputCallback = null): void + { + if ($outputCallback) { + $outputCallback('out', "Dropping tables from database $dbName"); + } + $this->localMachineHelper->checkRequiredBinariesExist(['mysql']); + $command = [ 'mysql', '--host', $dbHost, '--user', $dbUser, $dbName, + '--silent', '-e', - 'source ' . $tempnam, - ]; - $process = $this->localMachineHelper->execute($command, $outputCallback, NULL, FALSE, NULL, ['MYSQL_PWD' => $dbPassword]); - if (!$process->isSuccessful()) { - throw new AcquiaCliException('Unable to drop tables from database. {message}', ['message' => $process->getErrorOutput()]); - } + 'SHOW TABLES;', + ]; + $process = $this->localMachineHelper->execute($command, $outputCallback, null, false, null, ['MYSQL_PWD' => $dbPassword]); + $tables = $this->listTablesQuoted($process->getOutput()); + if ($tables) { + $sql = 'DROP TABLE ' . implode(', ', $tables); + $tempnam = $this->localMachineHelper->getFilesystem()->tempnam(sys_get_temp_dir(), 'acli_drop_table_', '.sql'); + $this->localMachineHelper->getFilesystem()->dumpFile($tempnam, $sql); + $command = [ + 'mysql', + '--host', + $dbHost, + '--user', + $dbUser, + $dbName, + '-e', + 'source ' . $tempnam, + ]; + $process = $this->localMachineHelper->execute($command, $outputCallback, null, false, null, ['MYSQL_PWD' => $dbPassword]); + if (!$process->isSuccessful()) { + throw new AcquiaCliException('Unable to drop tables from database. {message}', ['message' => $process->getErrorOutput()]); + } + } } - } - private function importDatabaseDump(string $localDumpFilepath, string $dbHost, string $dbUser, string $dbName, string $dbPassword, Closure $outputCallback = NULL): void { - if ($outputCallback) { - $outputCallback('out', "Importing downloaded file to database $dbName"); - } - $this->logger->debug("Importing $localDumpFilepath to MySQL on local machine"); - $this->localMachineHelper->checkRequiredBinariesExist(['gunzip', 'mysql']); - if ($this->localMachineHelper->commandExists('pv')) { - $command = "pv $localDumpFilepath --bytes --rate | gunzip | MYSQL_PWD=$dbPassword mysql --host=$dbHost --user=$dbUser $dbName"; - } - else { - $this->io->warning('Install `pv` to see progress bar'); - $command = "gunzip -c $localDumpFilepath | MYSQL_PWD=$dbPassword mysql --host=$dbHost --user=$dbUser $dbName"; - } + private function importDatabaseDump(string $localDumpFilepath, string $dbHost, string $dbUser, string $dbName, string $dbPassword, Closure $outputCallback = null): void + { + if ($outputCallback) { + $outputCallback('out', "Importing downloaded file to database $dbName"); + } + $this->logger->debug("Importing $localDumpFilepath to MySQL on local machine"); + $this->localMachineHelper->checkRequiredBinariesExist(['gunzip', 'mysql']); + if ($this->localMachineHelper->commandExists('pv')) { + $command = "pv $localDumpFilepath --bytes --rate | gunzip | MYSQL_PWD=$dbPassword mysql --host=$dbHost --user=$dbUser $dbName"; + } else { + $this->io->warning('Install `pv` to see progress bar'); + $command = "gunzip -c $localDumpFilepath | MYSQL_PWD=$dbPassword mysql --host=$dbHost --user=$dbUser $dbName"; + } - $process = $this->localMachineHelper->executeFromCmd($command, $outputCallback, NULL, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL)); - if (!$process->isSuccessful()) { - throw new AcquiaCliException('Unable to import local database. {message}', ['message' => $process->getErrorOutput()]); + $process = $this->localMachineHelper->executeFromCmd($command, $outputCallback, null, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL)); + if (!$process->isSuccessful()) { + throw new AcquiaCliException('Unable to import local database. {message}', ['message' => $process->getErrorOutput()]); + } } - } - private function determineSite(string|EnvironmentResponse|array $environment, InputInterface $input): string { - if (isset($this->site)) { - return $this->site; + private function determineSite(string|EnvironmentResponse|array $environment, InputInterface $input): string + { + if (isset($this->site)) { + return $this->site; + } + + if ($input->hasArgument('site') && $input->getArgument('site')) { + return $input->getArgument('site'); + } + + $this->site = $this->promptChooseDrupalSite($environment); + + return $this->site; } - if ($input->hasArgument('site') && $input->getArgument('site')) { - return $input->getArgument('site'); + private function rsyncFilesFromCloud(EnvironmentResponse $chosenEnvironment, Closure $outputCallback, string $site): void + { + $sourceDir = $chosenEnvironment->sshUrl . ':' . $this->getCloudFilesDir($chosenEnvironment, $site); + $destinationDir = $this->getLocalFilesDir($site); + $this->localMachineHelper->getFilesystem()->mkdir($destinationDir); + + $this->rsyncFiles($sourceDir, $destinationDir, $outputCallback); } - $this->site = $this->promptChooseDrupalSite($environment); + protected function determineCloneProject(OutputInterface $output): bool + { + $finder = $this->localMachineHelper->getFinder()->files()->in($this->dir)->ignoreDotFiles(false); - return $this->site; - } + // If we are in an IDE, assume we should pull into /home/ide/project. + if ($this->dir === '/home/ide/project' && AcquiaDrupalEnvironmentDetector::isAhIdeEnv() && !$finder->hasResults()) { + $output->writeln('Cloning into current directory.'); + return true; + } - private function rsyncFilesFromCloud(EnvironmentResponse $chosenEnvironment, Closure $outputCallback, string $site): void { - $sourceDir = $chosenEnvironment->sshUrl . ':' . $this->getCloudFilesDir($chosenEnvironment, $site); - $destinationDir = $this->getLocalFilesDir($site); - $this->localMachineHelper->getFilesystem()->mkdir($destinationDir); + // If $this->projectDir is set, pull into that dir rather than cloning. + if ($this->projectDir) { + return false; + } - $this->rsyncFiles($sourceDir, $destinationDir, $outputCallback); - } + // If ./.git exists, assume we pull into that dir rather than cloning. + if (file_exists(Path::join($this->dir, '.git'))) { + return false; + } + $output->writeln('Could not find a git repository in the current directory'); - protected function determineCloneProject(OutputInterface $output): bool { - $finder = $this->localMachineHelper->getFinder()->files()->in($this->dir)->ignoreDotFiles(FALSE); + if (!$finder->hasResults() && $this->io->confirm('Would you like to clone a project into the current directory?')) { + return true; + } - // If we are in an IDE, assume we should pull into /home/ide/project. - if ($this->dir === '/home/ide/project' && AcquiaDrupalEnvironmentDetector::isAhIdeEnv() && !$finder->hasResults()) { - $output->writeln('Cloning into current directory.'); - return TRUE; - } + $output->writeln('Could not clone into the current directory because it is not empty'); - // If $this->projectDir is set, pull into that dir rather than cloning. - if ($this->projectDir) { - return FALSE; + throw new AcquiaCliException('Execute this command from within a Drupal project directory or an empty directory'); } - // If ./.git exists, assume we pull into that dir rather than cloning. - if (file_exists(Path::join($this->dir, '.git'))) { - return FALSE; + private function cloneFromCloud(EnvironmentResponse $chosenEnvironment, Closure $outputCallback): void + { + $this->localMachineHelper->checkRequiredBinariesExist(['git']); + $command = [ + 'git', + 'clone', + $chosenEnvironment->vcs->url, + $this->dir, + ]; + $process = $this->localMachineHelper->execute($command, $outputCallback, null, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL), null, ['GIT_SSH_COMMAND' => 'ssh -o StrictHostKeyChecking=no']); + $this->checkoutBranchFromEnv($chosenEnvironment, $outputCallback); + if (!$process->isSuccessful()) { + throw new AcquiaCliException('Failed to clone repository from the Cloud Platform: {message}', ['message' => $process->getErrorOutput()]); + } + $this->projectDir = $this->dir; } - $output->writeln('Could not find a git repository in the current directory'); - if (!$finder->hasResults() && $this->io->confirm('Would you like to clone a project into the current directory?')) { - return TRUE; + protected function checkEnvironmentPhpVersions(EnvironmentResponse $environment): void + { + $version = $this->getIdePhpVersion(); + if (empty($version)) { + $this->io->warning("Could not determine current PHP version. Set it by running acli ide:php-version."); + } elseif (!$this->environmentPhpVersionMatches($environment)) { + $this->io->warning("You are using PHP version $version but the upstream environment $environment->label is using PHP version {$environment->configuration->php->version}"); + } } - $output->writeln('Could not clone into the current directory because it is not empty'); - - throw new AcquiaCliException('Execute this command from within a Drupal project directory or an empty directory'); - } - - private function cloneFromCloud(EnvironmentResponse $chosenEnvironment, Closure $outputCallback): void { - $this->localMachineHelper->checkRequiredBinariesExist(['git']); - $command = [ - 'git', - 'clone', - $chosenEnvironment->vcs->url, - $this->dir, - ]; - $process = $this->localMachineHelper->execute($command, $outputCallback, NULL, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL), NULL, ['GIT_SSH_COMMAND' => 'ssh -o StrictHostKeyChecking=no']); - $this->checkoutBranchFromEnv($chosenEnvironment, $outputCallback); - if (!$process->isSuccessful()) { - throw new AcquiaCliException('Failed to clone repository from the Cloud Platform: {message}', ['message' => $process->getErrorOutput()]); + protected function matchIdePhpVersion( + OutputInterface $output, + EnvironmentResponse $chosenEnvironment + ): void { + if (AcquiaDrupalEnvironmentDetector::isAhIdeEnv() && !$this->environmentPhpVersionMatches($chosenEnvironment)) { + $answer = $this->io->confirm("Would you like to change the PHP version on this IDE to match the PHP version on the $chosenEnvironment->label ({$chosenEnvironment->configuration->php->version}) environment?", false); + if ($answer) { + $command = $this->getApplication()->find('ide:php-version'); + $command->run( + new ArrayInput(['command' => 'ide:php-version', 'version' => $chosenEnvironment->configuration->php->version]), + $output + ); + } + } } - $this->projectDir = $this->dir; - } - protected function checkEnvironmentPhpVersions(EnvironmentResponse $environment): void { - $version = $this->getIdePhpVersion(); - if (empty($version)) { - $this->io->warning("Could not determine current PHP version. Set it by running acli ide:php-version."); - } - else if (!$this->environmentPhpVersionMatches($environment)) { - $this->io->warning("You are using PHP version $version but the upstream environment $environment->label is using PHP version {$environment->configuration->php->version}"); - } - } - - protected function matchIdePhpVersion( - OutputInterface $output, - EnvironmentResponse $chosenEnvironment - ): void { - if (AcquiaDrupalEnvironmentDetector::isAhIdeEnv() && !$this->environmentPhpVersionMatches($chosenEnvironment)) { - $answer = $this->io->confirm("Would you like to change the PHP version on this IDE to match the PHP version on the $chosenEnvironment->label ({$chosenEnvironment->configuration->php->version}) environment?", FALSE); - if ($answer) { - $command = $this->getApplication()->find('ide:php-version'); - $command->run(new ArrayInput(['command' => 'ide:php-version', 'version' => $chosenEnvironment->configuration->php->version]), - $output); - } - } - } - - private function environmentPhpVersionMatches(EnvironmentResponse $environment): bool { - $currentPhpVersion = $this->getIdePhpVersion(); - return $environment->configuration->php->version === $currentPhpVersion; - } - - private function getDatabaseBackup( - Client $acquiaCloudClient, - string|EnvironmentResponse|array $environment, - DatabaseResponse $database - ): BackupResponse { - $databaseBackups = new DatabaseBackups($acquiaCloudClient); - $backupsResponse = $databaseBackups->getAll($environment->uuid, $database->name); - if (!count($backupsResponse)) { - $this->io->warning('No existing backups found, creating an on-demand backup now. This will take some time depending on the size of the database.'); - $this->createBackup($environment, $database, $acquiaCloudClient); - $backupsResponse = $databaseBackups->getAll($environment->uuid, - $database->name); - } - $backupResponse = $backupsResponse[0]; - $this->logger->debug('Using database backup (id #' . $backupResponse->id . ') generated at ' . $backupResponse->completedAt); - - return $backupResponse; - } - - /** - * Print information to the console about the selected database backup. - */ - private function printDatabaseBackupInfo( - BackupResponse $backupResponse, - EnvironmentResponse $sourceEnvironment - ): void { - $interval = time() - strtotime($backupResponse->completedAt); - $hoursInterval = floor($interval / 60 / 60); - $dateFormatted = date("D M j G:i:s T Y", strtotime($backupResponse->completedAt)); - $webLink = "https://cloud.acquia.com/a/environments/{$sourceEnvironment->uuid}/databases"; - $messages = [ - "Using a database backup that is $hoursInterval hours old. Backup #$backupResponse->id was created at {$dateFormatted}.", - "You can view your backups here: $webLink", - "To generate a new backup, re-run this command with the --on-demand option.", - ]; - if ($hoursInterval > 24) { - $this->io->warning($messages); - } - else { - $this->io->info($messages); + private function environmentPhpVersionMatches(EnvironmentResponse $environment): bool + { + $currentPhpVersion = $this->getIdePhpVersion(); + return $environment->configuration->php->version === $currentPhpVersion; + } + + private function getDatabaseBackup( + Client $acquiaCloudClient, + string|EnvironmentResponse|array $environment, + DatabaseResponse $database + ): BackupResponse { + $databaseBackups = new DatabaseBackups($acquiaCloudClient); + $backupsResponse = $databaseBackups->getAll($environment->uuid, $database->name); + if (!count($backupsResponse)) { + $this->io->warning('No existing backups found, creating an on-demand backup now. This will take some time depending on the size of the database.'); + $this->createBackup($environment, $database, $acquiaCloudClient); + $backupsResponse = $databaseBackups->getAll( + $environment->uuid, + $database->name + ); + } + $backupResponse = $backupsResponse[0]; + $this->logger->debug('Using database backup (id #' . $backupResponse->id . ') generated at ' . $backupResponse->completedAt); + + return $backupResponse; + } + + /** + * Print information to the console about the selected database backup. + */ + private function printDatabaseBackupInfo( + BackupResponse $backupResponse, + EnvironmentResponse $sourceEnvironment + ): void { + $interval = time() - strtotime($backupResponse->completedAt); + $hoursInterval = floor($interval / 60 / 60); + $dateFormatted = date("D M j G:i:s T Y", strtotime($backupResponse->completedAt)); + $webLink = "https://cloud.acquia.com/a/environments/{$sourceEnvironment->uuid}/databases"; + $messages = [ + "Using a database backup that is $hoursInterval hours old. Backup #$backupResponse->id was created at {$dateFormatted}.", + "You can view your backups here: $webLink", + "To generate a new backup, re-run this command with the --on-demand option.", + ]; + if ($hoursInterval > 24) { + $this->io->warning($messages); + } else { + $this->io->info($messages); + } } - } - private function importRemoteDatabase(DatabaseResponse $database, string $localFilepath, Closure $outputCallback = NULL): void { - if ($database->flags->default) { - // Easy case, import the default db into the default db. - $this->doImportRemoteDatabase($this->getLocalDbHost(), $this->getLocalDbUser(), $this->getLocalDbName(), $this->getLocalDbPassword(), $localFilepath, $outputCallback); - } - else if (AcquiaDrupalEnvironmentDetector::isAhIdeEnv() && !getenv('IDE_ENABLE_MULTISITE')) { - // Import non-default db into default db. Needed on legacy IDE without multiple dbs. - // @todo remove this case once all IDEs support multiple dbs. - $this->io->note("Cloud IDE only supports importing into the default Drupal database. Acquia CLI will import the NON-DEFAULT database {$database->name} into the DEFAULT database {$this->getLocalDbName()}"); - $this->doImportRemoteDatabase($this->getLocalDbHost(), $this->getLocalDbUser(), $this->getLocalDbName(), $this->getLocalDbPassword(), $localFilepath, $outputCallback); - } - else { - // Import non-default db into non-default db. - $this->io->note("Acquia CLI assumes that the local name for the {$database->name} database is also {$database->name}"); - if (AcquiaDrupalEnvironmentDetector::isLandoEnv() || AcquiaDrupalEnvironmentDetector::isAhIdeEnv()) { - $this->doImportRemoteDatabase($this->getLocalDbHost(), 'root', $database->name, '', $localFilepath, $outputCallback); - } - else { - $this->doImportRemoteDatabase($this->getLocalDbHost(), $this->getLocalDbUser(), $database->name, $this->getLocalDbPassword(), $localFilepath, $outputCallback); - } + private function importRemoteDatabase(DatabaseResponse $database, string $localFilepath, Closure $outputCallback = null): void + { + if ($database->flags->default) { + // Easy case, import the default db into the default db. + $this->doImportRemoteDatabase($this->getLocalDbHost(), $this->getLocalDbUser(), $this->getLocalDbName(), $this->getLocalDbPassword(), $localFilepath, $outputCallback); + } elseif (AcquiaDrupalEnvironmentDetector::isAhIdeEnv() && !getenv('IDE_ENABLE_MULTISITE')) { + // Import non-default db into default db. Needed on legacy IDE without multiple dbs. + // @todo remove this case once all IDEs support multiple dbs. + $this->io->note("Cloud IDE only supports importing into the default Drupal database. Acquia CLI will import the NON-DEFAULT database {$database->name} into the DEFAULT database {$this->getLocalDbName()}"); + $this->doImportRemoteDatabase($this->getLocalDbHost(), $this->getLocalDbUser(), $this->getLocalDbName(), $this->getLocalDbPassword(), $localFilepath, $outputCallback); + } else { + // Import non-default db into non-default db. + $this->io->note("Acquia CLI assumes that the local name for the {$database->name} database is also {$database->name}"); + if (AcquiaDrupalEnvironmentDetector::isLandoEnv() || AcquiaDrupalEnvironmentDetector::isAhIdeEnv()) { + $this->doImportRemoteDatabase($this->getLocalDbHost(), 'root', $database->name, '', $localFilepath, $outputCallback); + } else { + $this->doImportRemoteDatabase($this->getLocalDbHost(), $this->getLocalDbUser(), $database->name, $this->getLocalDbPassword(), $localFilepath, $outputCallback); + } + } } - } - } diff --git a/src/Command/Pull/PullDatabaseCommand.php b/src/Command/Pull/PullDatabaseCommand.php index 15fe38a55..19514c15e 100644 --- a/src/Command/Pull/PullDatabaseCommand.php +++ b/src/Command/Pull/PullDatabaseCommand.php @@ -1,6 +1,6 @@ setHelp('This uses the latest available database backup, which may be up to 24 hours old. If no backup exists, one will be created.') - ->acceptEnvironmentId() - ->acceptSite() - ->addOption('no-scripts', NULL, InputOption::VALUE_NONE, - 'Do not run any additional scripts after the database is pulled. E.g., drush cache-rebuild, drush sql-sanitize, etc.') - ->addOption('on-demand', NULL, InputOption::VALUE_NONE, - 'Force creation of an on-demand backup. This takes much longer than using an existing backup (when one is available)') - ->addOption('no-import', NULL, InputOption::VALUE_NONE, - 'Download the backup but do not import it (implies --no-scripts)') - ->addOption('multiple-dbs', NULL, InputOption::VALUE_NONE, - 'Download multiple dbs. Defaults to FALSE.'); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $noScripts = $input->hasOption('no-scripts') && $input->getOption('no-scripts'); - $onDemand = $input->hasOption('on-demand') && $input->getOption('on-demand'); - $noImport = $input->hasOption('no-import') && $input->getOption('no-import'); - $multipleDbs = $input->hasOption('multiple-dbs') && $input->getOption('multiple-dbs'); - // $noImport implies $noScripts. - $noScripts = $noImport || $noScripts; - $this->setDirAndRequireProjectCwd($input); - $sourceEnvironment = $this->determineEnvironment($input, $output, TRUE); - $this->pullDatabase($input, $output, $sourceEnvironment, $onDemand, $noImport, $multipleDbs); - if (!$noScripts) { - $this->runDrushCacheClear($this->getOutputCallback($output, $this->checklist), $this->checklist); - $this->runDrushSqlSanitize($this->getOutputCallback($output, $this->checklist), $this->checklist); +final class PullDatabaseCommand extends PullCommandBase +{ + protected function configure(): void + { + $this + ->setHelp('This uses the latest available database backup, which may be up to 24 hours old. If no backup exists, one will be created.') + ->acceptEnvironmentId() + ->acceptSite() + ->addOption( + 'no-scripts', + null, + InputOption::VALUE_NONE, + 'Do not run any additional scripts after the database is pulled. E.g., drush cache-rebuild, drush sql-sanitize, etc.' + ) + ->addOption( + 'on-demand', + null, + InputOption::VALUE_NONE, + 'Force creation of an on-demand backup. This takes much longer than using an existing backup (when one is available)' + ) + ->addOption( + 'no-import', + null, + InputOption::VALUE_NONE, + 'Download the backup but do not import it (implies --no-scripts)' + ) + ->addOption( + 'multiple-dbs', + null, + InputOption::VALUE_NONE, + 'Download multiple dbs. Defaults to FALSE.' + ); } - return Command::SUCCESS; - } + protected function execute(InputInterface $input, OutputInterface $output): int + { + $noScripts = $input->hasOption('no-scripts') && $input->getOption('no-scripts'); + $onDemand = $input->hasOption('on-demand') && $input->getOption('on-demand'); + $noImport = $input->hasOption('no-import') && $input->getOption('no-import'); + $multipleDbs = $input->hasOption('multiple-dbs') && $input->getOption('multiple-dbs'); + // $noImport implies $noScripts. + $noScripts = $noImport || $noScripts; + $this->setDirAndRequireProjectCwd($input); + $sourceEnvironment = $this->determineEnvironment($input, $output, true); + $this->pullDatabase($input, $output, $sourceEnvironment, $onDemand, $noImport, $multipleDbs); + if (!$noScripts) { + $this->runDrushCacheClear($this->getOutputCallback($output, $this->checklist), $this->checklist); + $this->runDrushSqlSanitize($this->getOutputCallback($output, $this->checklist), $this->checklist); + } + return Command::SUCCESS; + } } diff --git a/src/Command/Pull/PullFilesCommand.php b/src/Command/Pull/PullFilesCommand.php index abf72f9d3..a42eea055 100644 --- a/src/Command/Pull/PullFilesCommand.php +++ b/src/Command/Pull/PullFilesCommand.php @@ -1,6 +1,6 @@ acceptEnvironmentId() - ->acceptSite(); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $this->setDirAndRequireProjectCwd($input); - $sourceEnvironment = $this->determineEnvironment($input, $output, TRUE); - $this->pullFiles($input, $output, $sourceEnvironment); - - return Command::SUCCESS; - } - +final class PullFilesCommand extends PullCommandBase +{ + protected function configure(): void + { + $this + ->acceptEnvironmentId() + ->acceptSite(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->setDirAndRequireProjectCwd($input); + $sourceEnvironment = $this->determineEnvironment($input, $output, true); + $this->pullFiles($input, $output, $sourceEnvironment); + + return Command::SUCCESS; + } } diff --git a/src/Command/Pull/PullScriptsCommand.php b/src/Command/Pull/PullScriptsCommand.php index 7ff3f3018..4ab34d089 100644 --- a/src/Command/Pull/PullScriptsCommand.php +++ b/src/Command/Pull/PullScriptsCommand.php @@ -1,6 +1,6 @@ acceptEnvironmentId() - ->addOption('dir', NULL, InputArgument::OPTIONAL, 'The directory containing the Drupal project to be refreshed'); - } - - protected function initialize(InputInterface $input, OutputInterface $output): void { - parent::initialize($input, $output); - $this->checklist = new Checklist($output); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $this->setDirAndRequireProjectCwd($input); - $this->executeAllScripts($this->getOutputCallback($output, $this->checklist), $this->checklist); - - return Command::SUCCESS; - } - +final class PullScriptsCommand extends CommandBase +{ + protected Checklist $checklist; + + protected function configure(): void + { + $this + ->acceptEnvironmentId() + ->addOption('dir', null, InputArgument::OPTIONAL, 'The directory containing the Drupal project to be refreshed'); + } + + protected function initialize(InputInterface $input, OutputInterface $output): void + { + parent::initialize($input, $output); + $this->checklist = new Checklist($output); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->setDirAndRequireProjectCwd($input); + $this->executeAllScripts($this->getOutputCallback($output, $this->checklist), $this->checklist); + + return Command::SUCCESS; + } } diff --git a/src/Command/Push/PushArtifactCommand.php b/src/Command/Push/PushArtifactCommand.php index 556f59bfa..afb0a4408 100644 --- a/src/Command/Push/PushArtifactCommand.php +++ b/src/Command/Push/PushArtifactCommand.php @@ -1,6 +1,6 @@ - */ - protected array $vendorDirs; - - /** - * Composer scaffold files. - * - * @var array - */ - protected array $scaffoldFiles; - - private string $composerJsonPath; - - private string $docrootPath; - - private string $destinationGitRef; - - protected Checklist $checklist; - - protected function configure(): void { - $this - ->addOption('dir', NULL, InputArgument::OPTIONAL, 'The directory containing the Drupal project to be pushed') - ->addOption('no-sanitize', NULL, InputOption::VALUE_NONE, 'Do not sanitize the build artifact') - ->addOption('dry-run', NULL, InputOption::VALUE_NONE, 'Deprecated: Use no-push instead') - ->addOption('no-push', NULL, InputOption::VALUE_NONE, 'Do not push changes to Acquia Cloud') - ->addOption('no-commit', NULL, InputOption::VALUE_NONE, 'Do not commit changes. Implies no-push') - ->addOption('no-clone', NULL, InputOption::VALUE_NONE, 'Do not clone repository. Implies no-commit and no-push') - ->addOption('destination-git-urls', 'u', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'The URL(s) of your git repository to which the artifact branch will be pushed') - ->addOption('destination-git-branch', 'b', InputOption::VALUE_REQUIRED, 'The destination branch to push the artifact to') - ->addOption('destination-git-tag', 't', InputOption::VALUE_REQUIRED, 'The destination tag to push the artifact to. Using this option requires also using the --source-git-tag option') - ->addOption('source-git-tag', 's', InputOption::VALUE_REQUIRED, 'The source tag from which to create the tag artifact') - ->acceptEnvironmentId() - ->setHelp('This command builds a sanitized deploy artifact by running composer install, removing sensitive files, and committing vendor directories.' . PHP_EOL . PHP_EOL - . 'Vendor directories and scaffold files are committed to the build artifact even if they are ignored in the source repository.' . PHP_EOL . PHP_EOL - . 'To run additional build or sanitization steps (e.g. npm install), add a post-install-cmd script to your composer.json file: https://getcomposer.org/doc/articles/scripts.md#command-events') - ->addUsage('--destination-git-branch=main-build') - ->addUsage('--source-git-tag=foo-build --destination-git-tag=1.0.0') - ->addUsage('--destination-git-urls=example@svn-1.prod.hosting.acquia.com:example.git --destination-git-branch=main-build'); - } - - protected function initialize(InputInterface $input, OutputInterface $output): void { - parent::initialize($input, $output); - $this->checklist = new Checklist($output); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $this->setDirAndRequireProjectCwd($input); - if ($input->getOption('no-clone')) { - $input->setOption('no-commit', TRUE); - } - if ($input->getOption('no-commit')) { - $input->setOption('no-push', TRUE); - } - $artifactDir = Path::join(sys_get_temp_dir(), 'acli-push-artifact'); - $this->composerJsonPath = Path::join($this->dir, 'composer.json'); - $this->docrootPath = Path::join($this->dir, 'docroot'); - $this->validateSourceCode(); - - $isDirty = $this->isLocalGitRepoDirty(); - $commitHash = $this->getLocalGitCommitHash(); - if ($isDirty) { - throw new AcquiaCliException('Pushing code was aborted because your local Git repository has uncommitted changes. Either commit, reset, or stash your changes via git.'); - } - $this->checklist = new Checklist($output); - $outputCallback = $this->getOutputCallback($output, $this->checklist); - - $destinationGitUrls = []; - $destinationGitRef = ''; - if (!$input->getOption('no-clone')) { - $applicationUuid = $this->determineCloudApplication(); - $destinationGitUrls = $this->determineDestinationGitUrls($applicationUuid); - $destinationGitRef = $this->determineDestinationGitRef(); - $sourceGitBranch = $this->determineSourceGitRef(); - $destinationGitUrlsString = implode(',', $destinationGitUrls); - $refType = $this->input->getOption('destination-git-tag') ? 'tag' : 'branch'; - $this->io->note([ - "Acquia CLI will:", - "- git clone $sourceGitBranch from $destinationGitUrls[0]", - "- Compile the contents of $this->dir into an artifact in a temporary directory", - "- Copy the artifact files into the checked out copy of $sourceGitBranch", - "- Commit changes and push the $destinationGitRef $refType to the following git remote(s):", - " $destinationGitUrlsString", - ]); - - $this->checklist->addItem('Preparing artifact directory'); - $this->cloneSourceBranch($outputCallback, $artifactDir, $destinationGitUrls[0], $sourceGitBranch); - $this->checklist->completePreviousItem(); - } - - $this->checklist->addItem('Generating build artifact'); - $this->buildArtifact($outputCallback, $artifactDir); - $this->checklist->completePreviousItem(); - - if (!$input->getOption('no-sanitize')) { - $this->checklist->addItem('Sanitizing build artifact'); - $this->sanitizeArtifact($outputCallback, $artifactDir); - $this->checklist->completePreviousItem(); - } - - if (!$input->getOption('no-commit')) { - $this->checklist->addItem("Committing changes (commit hash: $commitHash)"); - $this->commit($outputCallback, $artifactDir, $commitHash); - $this->checklist->completePreviousItem(); - } - - if (!$input->getOption('dry-run') && !$input->getOption('no-push')) { - if ($tagName = $input->getOption('destination-git-tag')) { - $this->checklist->addItem("Creating $tagName tag."); - $this->createTag($tagName, $outputCallback, $artifactDir); +final class PushArtifactCommand extends CommandBase +{ + /** + * Composer vendor directories. + * + * @var array + */ + protected array $vendorDirs; + + /** + * Composer scaffold files. + * + * @var array + */ + protected array $scaffoldFiles; + + private string $composerJsonPath; + + private string $docrootPath; + + private string $destinationGitRef; + + protected Checklist $checklist; + + protected function configure(): void + { + $this + ->addOption('dir', null, InputArgument::OPTIONAL, 'The directory containing the Drupal project to be pushed') + ->addOption('no-sanitize', null, InputOption::VALUE_NONE, 'Do not sanitize the build artifact') + ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Deprecated: Use no-push instead') + ->addOption('no-push', null, InputOption::VALUE_NONE, 'Do not push changes to Acquia Cloud') + ->addOption('no-commit', null, InputOption::VALUE_NONE, 'Do not commit changes. Implies no-push') + ->addOption('no-clone', null, InputOption::VALUE_NONE, 'Do not clone repository. Implies no-commit and no-push') + ->addOption('destination-git-urls', 'u', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'The URL(s) of your git repository to which the artifact branch will be pushed') + ->addOption('destination-git-branch', 'b', InputOption::VALUE_REQUIRED, 'The destination branch to push the artifact to') + ->addOption('destination-git-tag', 't', InputOption::VALUE_REQUIRED, 'The destination tag to push the artifact to. Using this option requires also using the --source-git-tag option') + ->addOption('source-git-tag', 's', InputOption::VALUE_REQUIRED, 'The source tag from which to create the tag artifact') + ->acceptEnvironmentId() + ->setHelp('This command builds a sanitized deploy artifact by running composer install, removing sensitive files, and committing vendor directories.' . PHP_EOL . PHP_EOL + . 'Vendor directories and scaffold files are committed to the build artifact even if they are ignored in the source repository.' . PHP_EOL . PHP_EOL + . 'To run additional build or sanitization steps (e.g. npm install), add a post-install-cmd script to your composer.json file: https://getcomposer.org/doc/articles/scripts.md#command-events') + ->addUsage('--destination-git-branch=main-build') + ->addUsage('--source-git-tag=foo-build --destination-git-tag=1.0.0') + ->addUsage('--destination-git-urls=example@svn-1.prod.hosting.acquia.com:example.git --destination-git-branch=main-build'); + } + + protected function initialize(InputInterface $input, OutputInterface $output): void + { + parent::initialize($input, $output); + $this->checklist = new Checklist($output); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->setDirAndRequireProjectCwd($input); + if ($input->getOption('no-clone')) { + $input->setOption('no-commit', true); + } + if ($input->getOption('no-commit')) { + $input->setOption('no-push', true); + } + $artifactDir = Path::join(sys_get_temp_dir(), 'acli-push-artifact'); + $this->composerJsonPath = Path::join($this->dir, 'composer.json'); + $this->docrootPath = Path::join($this->dir, 'docroot'); + $this->validateSourceCode(); + + $isDirty = $this->isLocalGitRepoDirty(); + $commitHash = $this->getLocalGitCommitHash(); + if ($isDirty) { + throw new AcquiaCliException('Pushing code was aborted because your local Git repository has uncommitted changes. Either commit, reset, or stash your changes via git.'); + } + $this->checklist = new Checklist($output); + $outputCallback = $this->getOutputCallback($output, $this->checklist); + + $destinationGitUrls = []; + $destinationGitRef = ''; + if (!$input->getOption('no-clone')) { + $applicationUuid = $this->determineCloudApplication(); + $destinationGitUrls = $this->determineDestinationGitUrls($applicationUuid); + $destinationGitRef = $this->determineDestinationGitRef(); + $sourceGitBranch = $this->determineSourceGitRef(); + $destinationGitUrlsString = implode(',', $destinationGitUrls); + $refType = $this->input->getOption('destination-git-tag') ? 'tag' : 'branch'; + $this->io->note([ + "Acquia CLI will:", + "- git clone $sourceGitBranch from $destinationGitUrls[0]", + "- Compile the contents of $this->dir into an artifact in a temporary directory", + "- Copy the artifact files into the checked out copy of $sourceGitBranch", + "- Commit changes and push the $destinationGitRef $refType to the following git remote(s):", + " $destinationGitUrlsString", + ]); + + $this->checklist->addItem('Preparing artifact directory'); + $this->cloneSourceBranch($outputCallback, $artifactDir, $destinationGitUrls[0], $sourceGitBranch); + $this->checklist->completePreviousItem(); + } + + $this->checklist->addItem('Generating build artifact'); + $this->buildArtifact($outputCallback, $artifactDir); $this->checklist->completePreviousItem(); - $this->checklist->addItem("Pushing changes to $tagName tag."); - $this->pushArtifact($outputCallback, $artifactDir, $destinationGitUrls, $tagName); - } - else { - $this->checklist->addItem("Pushing changes to $destinationGitRef branch."); - $this->pushArtifact($outputCallback, $artifactDir, $destinationGitUrls, $destinationGitRef . ':' . $destinationGitRef); - } - $this->checklist->completePreviousItem(); - } - else { - $this->logger->warning("The --dry-run (deprecated) or --no-push option prevented changes from being pushed to Acquia Cloud. The artifact has been built at $artifactDir"); - } - - return Command::SUCCESS; - } - - /** - * @return string[] - */ - private function determineDestinationGitUrls(?string $applicationUuid): array { - if ($this->input->getOption('destination-git-urls')) { - return $this->input->getOption('destination-git-urls'); - } - if ($envVar = getenv('ACLI_PUSH_ARTIFACT_DESTINATION_GIT_URLS')) { - return explode(',', $envVar); - } - if ($this->datastoreAcli->get('push.artifact.destination-git-urls')) { - return $this->datastoreAcli->get('push.artifact.destination-git-urls'); - } - return [$this->getAnyVcsUrl($applicationUuid)]; - } - - /** - * Prepare a directory to build the artifact. - */ - private function cloneSourceBranch(Closure $outputCallback, string $artifactDir, string $vcsUrl, string $vcsPath): void { - $fs = $this->localMachineHelper->getFilesystem(); - - $outputCallback('out', "Removing $artifactDir if it exists"); - $fs->remove($artifactDir); - - $outputCallback('out', "Initializing Git in $artifactDir"); - $this->localMachineHelper->checkRequiredBinariesExist(['git']); - $process = $this->localMachineHelper->execute(['git', 'clone', '--depth=1', $vcsUrl, $artifactDir], $outputCallback, NULL, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL)); - if (!$process->isSuccessful()) { - throw new AcquiaCliException('Failed to clone repository from the Cloud Platform: {message}', ['message' => $process->getErrorOutput()]); - } - $process = $this->localMachineHelper->execute(['git', 'fetch', '--depth=1', $vcsUrl, $vcsPath . ':' . $vcsPath], $outputCallback, $artifactDir, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL)); - if (!$process->isSuccessful()) { - // Remote branch does not exist. Just create it locally. This will create - // the new branch off of the current commit. - $process = $this->localMachineHelper->execute(['git', 'checkout', '-b', $vcsPath], $outputCallback, $artifactDir, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL)); - } - else { - $process = $this->localMachineHelper->execute(['git', 'checkout', $vcsPath], $outputCallback, $artifactDir, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL)); - } - if (!$process->isSuccessful()) { - throw new AcquiaCliException("Could not checkout $vcsPath branch locally: {message}", ['message' => $process->getErrorOutput() . $process->getOutput()]); - } - - $outputCallback('out', 'Global .gitignore file is temporarily disabled during artifact builds.'); - $this->localMachineHelper->execute(['git', 'config', '--local', 'core.excludesFile', 'false'], $outputCallback, $artifactDir, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL)); - $this->localMachineHelper->execute(['git', 'config', '--local', 'core.fileMode', 'true'], $outputCallback, $artifactDir, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL)); - - // Vendor directories can be "corrupt" (i.e. missing scaffold files due to earlier sanitization) in ways that break composer install. - $outputCallback('out', 'Removing vendor directories'); - foreach ($this->vendorDirs() as $vendorDirectory) { - $fs->remove(Path::join($artifactDir, $vendorDirectory)); - } - } - - /** - * Build the artifact. - */ - private function buildArtifact(Closure $outputCallback, string $artifactDir): void { - // @todo generate a deploy identifier - // @see https://git.drupalcode.org/project/drupal/-/blob/9.1.x/sites/default/default.settings.php#L295 - $outputCallback('out', "Mirroring source files from $this->dir to $artifactDir"); - $originFinder = $this->localMachineHelper->getFinder(); - $originFinder->in($this->dir) - // Include dot files like .htaccess. - ->ignoreDotFiles(FALSE) - // Ignore VCS ignored files (e.g. vendor) to speed up the mirror (Composer will restore them later). - ->ignoreVCSIgnored(TRUE); - $targetFinder = $this->localMachineHelper->getFinder(); - $targetFinder->in($artifactDir)->ignoreDotFiles(FALSE); - $this->localMachineHelper->getFilesystem()->remove($targetFinder); - $this->localMachineHelper->getFilesystem()->mirror($this->dir, $artifactDir, $originFinder, ['override' => TRUE]); - - $this->localMachineHelper->checkRequiredBinariesExist(['composer']); - $outputCallback('out', 'Installing Composer production dependencies'); - $process = $this->localMachineHelper->execute(['composer', 'install', '--no-dev', '--no-interaction', '--optimize-autoloader'], $outputCallback, $artifactDir, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL)); - if (!$process->isSuccessful()) { - throw new AcquiaCliException("Unable to install composer dependencies: {message}", ['message' => $process->getOutput() . $process->getErrorOutput()]); - } - } - - /** - * Sanitize the artifact. - */ - private function sanitizeArtifact(Closure $outputCallback, string $artifactDir): void { - $outputCallback('out', 'Finding Drupal core text files'); - $sanitizeFinder = $this->localMachineHelper->getFinder() - ->files() - ->name('*.txt') - ->notName('LICENSE.txt') - ->in("$artifactDir/docroot/core"); - - $outputCallback('out', 'Finding VCS directories'); - $vcsFinder = $this->localMachineHelper->getFinder() - ->ignoreDotFiles(FALSE) - ->ignoreVCS(FALSE) - ->directories() - ->in(["$artifactDir/docroot", + if (!$input->getOption('no-sanitize')) { + $this->checklist->addItem('Sanitizing build artifact'); + $this->sanitizeArtifact($outputCallback, $artifactDir); + $this->checklist->completePreviousItem(); + } + + if (!$input->getOption('no-commit')) { + $this->checklist->addItem("Committing changes (commit hash: $commitHash)"); + $this->commit($outputCallback, $artifactDir, $commitHash); + $this->checklist->completePreviousItem(); + } + + if (!$input->getOption('dry-run') && !$input->getOption('no-push')) { + if ($tagName = $input->getOption('destination-git-tag')) { + $this->checklist->addItem("Creating $tagName tag."); + $this->createTag($tagName, $outputCallback, $artifactDir); + $this->checklist->completePreviousItem(); + $this->checklist->addItem("Pushing changes to $tagName tag."); + $this->pushArtifact($outputCallback, $artifactDir, $destinationGitUrls, $tagName); + } else { + $this->checklist->addItem("Pushing changes to $destinationGitRef branch."); + $this->pushArtifact($outputCallback, $artifactDir, $destinationGitUrls, $destinationGitRef . ':' . $destinationGitRef); + } + $this->checklist->completePreviousItem(); + } else { + $this->logger->warning("The --dry-run (deprecated) or --no-push option prevented changes from being pushed to Acquia Cloud. The artifact has been built at $artifactDir"); + } + + return Command::SUCCESS; + } + + /** + * @return string[] + */ + private function determineDestinationGitUrls(?string $applicationUuid): array + { + if ($this->input->getOption('destination-git-urls')) { + return $this->input->getOption('destination-git-urls'); + } + if ($envVar = getenv('ACLI_PUSH_ARTIFACT_DESTINATION_GIT_URLS')) { + return explode(',', $envVar); + } + if ($this->datastoreAcli->get('push.artifact.destination-git-urls')) { + return $this->datastoreAcli->get('push.artifact.destination-git-urls'); + } + + return [$this->getAnyVcsUrl($applicationUuid)]; + } + + /** + * Prepare a directory to build the artifact. + */ + private function cloneSourceBranch(Closure $outputCallback, string $artifactDir, string $vcsUrl, string $vcsPath): void + { + $fs = $this->localMachineHelper->getFilesystem(); + + $outputCallback('out', "Removing $artifactDir if it exists"); + $fs->remove($artifactDir); + + $outputCallback('out', "Initializing Git in $artifactDir"); + $this->localMachineHelper->checkRequiredBinariesExist(['git']); + $process = $this->localMachineHelper->execute(['git', 'clone', '--depth=1', $vcsUrl, $artifactDir], $outputCallback, null, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL)); + if (!$process->isSuccessful()) { + throw new AcquiaCliException('Failed to clone repository from the Cloud Platform: {message}', ['message' => $process->getErrorOutput()]); + } + $process = $this->localMachineHelper->execute(['git', 'fetch', '--depth=1', $vcsUrl, $vcsPath . ':' . $vcsPath], $outputCallback, $artifactDir, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL)); + if (!$process->isSuccessful()) { + // Remote branch does not exist. Just create it locally. This will create + // the new branch off of the current commit. + $process = $this->localMachineHelper->execute(['git', 'checkout', '-b', $vcsPath], $outputCallback, $artifactDir, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL)); + } else { + $process = $this->localMachineHelper->execute(['git', 'checkout', $vcsPath], $outputCallback, $artifactDir, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL)); + } + if (!$process->isSuccessful()) { + throw new AcquiaCliException("Could not checkout $vcsPath branch locally: {message}", ['message' => $process->getErrorOutput() . $process->getOutput()]); + } + + $outputCallback('out', 'Global .gitignore file is temporarily disabled during artifact builds.'); + $this->localMachineHelper->execute(['git', 'config', '--local', 'core.excludesFile', 'false'], $outputCallback, $artifactDir, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL)); + $this->localMachineHelper->execute(['git', 'config', '--local', 'core.fileMode', 'true'], $outputCallback, $artifactDir, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL)); + + // Vendor directories can be "corrupt" (i.e. missing scaffold files due to earlier sanitization) in ways that break composer install. + $outputCallback('out', 'Removing vendor directories'); + foreach ($this->vendorDirs() as $vendorDirectory) { + $fs->remove(Path::join($artifactDir, $vendorDirectory)); + } + } + + /** + * Build the artifact. + */ + private function buildArtifact(Closure $outputCallback, string $artifactDir): void + { + // @todo generate a deploy identifier + // @see https://git.drupalcode.org/project/drupal/-/blob/9.1.x/sites/default/default.settings.php#L295 + $outputCallback('out', "Mirroring source files from $this->dir to $artifactDir"); + $originFinder = $this->localMachineHelper->getFinder(); + $originFinder->in($this->dir) + // Include dot files like .htaccess. + ->ignoreDotFiles(false) + // Ignore VCS ignored files (e.g. vendor) to speed up the mirror (Composer will restore them later). + ->ignoreVCSIgnored(true); + $targetFinder = $this->localMachineHelper->getFinder(); + $targetFinder->in($artifactDir)->ignoreDotFiles(false); + $this->localMachineHelper->getFilesystem()->remove($targetFinder); + $this->localMachineHelper->getFilesystem()->mirror($this->dir, $artifactDir, $originFinder, ['override' => true]); + + $this->localMachineHelper->checkRequiredBinariesExist(['composer']); + $outputCallback('out', 'Installing Composer production dependencies'); + $process = $this->localMachineHelper->execute(['composer', 'install', '--no-dev', '--no-interaction', '--optimize-autoloader'], $outputCallback, $artifactDir, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL)); + if (!$process->isSuccessful()) { + throw new AcquiaCliException("Unable to install composer dependencies: {message}", ['message' => $process->getOutput() . $process->getErrorOutput()]); + } + } + + /** + * Sanitize the artifact. + */ + private function sanitizeArtifact(Closure $outputCallback, string $artifactDir): void + { + $outputCallback('out', 'Finding Drupal core text files'); + $sanitizeFinder = $this->localMachineHelper->getFinder() + ->files() + ->name('*.txt') + ->notName('LICENSE.txt') + ->in("$artifactDir/docroot/core"); + + $outputCallback('out', 'Finding VCS directories'); + $vcsFinder = $this->localMachineHelper->getFinder() + ->ignoreDotFiles(false) + ->ignoreVCS(false) + ->directories() + ->in(["$artifactDir/docroot", "$artifactDir/vendor", - ]) - ->name('.git'); - $drushDir = "$artifactDir/drush"; - if (file_exists($drushDir)) { - $vcsFinder->in($drushDir); - } - if ($vcsFinder->hasResults()) { - $sanitizeFinder->append($vcsFinder); - } - - $outputCallback('out', 'Finding INSTALL database text files'); - $dbInstallFinder = $this->localMachineHelper->getFinder() - ->files() - ->in([$artifactDir]) - ->name('/INSTALL\.[a-z]+\.(md|txt)$/'); - if ($dbInstallFinder->hasResults()) { - $sanitizeFinder->append($dbInstallFinder); - } - - $outputCallback('out', 'Finding other common text files'); - $filenames = [ - 'AUTHORS', - 'CHANGELOG', - 'CONDUCT', - 'CONTRIBUTING', - 'INSTALL', - 'MAINTAINERS', - 'PATCHES', - 'TESTING', - 'UPDATE', - ]; - $textFileFinder = $this->localMachineHelper->getFinder() - ->files() - ->in(["$artifactDir/docroot"]) - ->name('/(' . implode('|', $filenames) . ')\.(md|txt)$/'); - if ($textFileFinder->hasResults()) { - $sanitizeFinder->append($textFileFinder); - } - - $outputCallback('out', "Removing sensitive files from build"); - $this->localMachineHelper->getFilesystem()->remove($sanitizeFinder); - } - - /** - * Commit the artifact. - */ - private function commit(Closure $outputCallback, string $artifactDir, string $commitHash): void { - $outputCallback('out', 'Adding and committing changed files'); - $this->localMachineHelper->checkRequiredBinariesExist(['git']); - $process = $this->localMachineHelper->execute(['git', 'add', '-A'], $outputCallback, $artifactDir, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL)); - if (!$process->isSuccessful()) { - throw new AcquiaCliException("Could not add files to artifact via git: {message}", ['message' => $process->getErrorOutput() . $process->getOutput()]); - } - foreach (array_merge($this->vendorDirs(), $this->scaffoldFiles($artifactDir)) as $file) { - $this->logger->debug("Forcibly adding $file"); - $this->localMachineHelper->execute(['git', 'add', '-f', $file], NULL, $artifactDir, FALSE); - if (!$process->isSuccessful()) { - // This will fatally error if the file doesn't exist. Suppress error output. - $this->io->warning("Unable to forcibly add $file to new branch"); - } - } - $commitMessage = $this->generateCommitMessage($commitHash); - $process = $this->localMachineHelper->execute(['git', 'commit', '-m', $commitMessage], $outputCallback, $artifactDir, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL)); - if (!$process->isSuccessful()) { - throw new AcquiaCliException("Could not commit via git: {message}", ['message' => $process->getErrorOutput() . $process->getOutput()]); - } - } - - private function generateCommitMessage(string $commitHash): array|string { - if ($envVar = getenv('ACLI_PUSH_ARTIFACT_COMMIT_MSG')) { - return $envVar; - } - - return "Automated commit by Acquia CLI (source commit: $commitHash)"; - } - - /** - * Push the artifact. - */ - private function pushArtifact(Closure $outputCallback, string $artifactDir, array $vcsUrls, string $destGitBranch): void { - $this->localMachineHelper->checkRequiredBinariesExist(['git']); - foreach ($vcsUrls as $vcsUrl) { - $outputCallback('out', "Pushing changes to Acquia Git ($vcsUrl)"); - $args = [ + ]) + ->name('.git'); + $drushDir = "$artifactDir/drush"; + if (file_exists($drushDir)) { + $vcsFinder->in($drushDir); + } + if ($vcsFinder->hasResults()) { + $sanitizeFinder->append($vcsFinder); + } + + $outputCallback('out', 'Finding INSTALL database text files'); + $dbInstallFinder = $this->localMachineHelper->getFinder() + ->files() + ->in([$artifactDir]) + ->name('/INSTALL\.[a-z]+\.(md|txt)$/'); + if ($dbInstallFinder->hasResults()) { + $sanitizeFinder->append($dbInstallFinder); + } + + $outputCallback('out', 'Finding other common text files'); + $filenames = [ + 'AUTHORS', + 'CHANGELOG', + 'CONDUCT', + 'CONTRIBUTING', + 'INSTALL', + 'MAINTAINERS', + 'PATCHES', + 'TESTING', + 'UPDATE', + ]; + $textFileFinder = $this->localMachineHelper->getFinder() + ->files() + ->in(["$artifactDir/docroot"]) + ->name('/(' . implode('|', $filenames) . ')\.(md|txt)$/'); + if ($textFileFinder->hasResults()) { + $sanitizeFinder->append($textFileFinder); + } + + $outputCallback('out', "Removing sensitive files from build"); + $this->localMachineHelper->getFilesystem()->remove($sanitizeFinder); + } + + /** + * Commit the artifact. + */ + private function commit(Closure $outputCallback, string $artifactDir, string $commitHash): void + { + $outputCallback('out', 'Adding and committing changed files'); + $this->localMachineHelper->checkRequiredBinariesExist(['git']); + $process = $this->localMachineHelper->execute(['git', 'add', '-A'], $outputCallback, $artifactDir, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL)); + if (!$process->isSuccessful()) { + throw new AcquiaCliException("Could not add files to artifact via git: {message}", ['message' => $process->getErrorOutput() . $process->getOutput()]); + } + foreach (array_merge($this->vendorDirs(), $this->scaffoldFiles($artifactDir)) as $file) { + $this->logger->debug("Forcibly adding $file"); + $this->localMachineHelper->execute(['git', 'add', '-f', $file], null, $artifactDir, false); + if (!$process->isSuccessful()) { + // This will fatally error if the file doesn't exist. Suppress error output. + $this->io->warning("Unable to forcibly add $file to new branch"); + } + } + $commitMessage = $this->generateCommitMessage($commitHash); + $process = $this->localMachineHelper->execute(['git', 'commit', '-m', $commitMessage], $outputCallback, $artifactDir, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL)); + if (!$process->isSuccessful()) { + throw new AcquiaCliException("Could not commit via git: {message}", ['message' => $process->getErrorOutput() . $process->getOutput()]); + } + } + + private function generateCommitMessage(string $commitHash): array|string + { + if ($envVar = getenv('ACLI_PUSH_ARTIFACT_COMMIT_MSG')) { + return $envVar; + } + + return "Automated commit by Acquia CLI (source commit: $commitHash)"; + } + + /** + * Push the artifact. + */ + private function pushArtifact(Closure $outputCallback, string $artifactDir, array $vcsUrls, string $destGitBranch): void + { + $this->localMachineHelper->checkRequiredBinariesExist(['git']); + foreach ($vcsUrls as $vcsUrl) { + $outputCallback('out', "Pushing changes to Acquia Git ($vcsUrl)"); + $args = [ + 'git', + 'push', + $vcsUrl, + $destGitBranch, + ]; + $process = $this->localMachineHelper->execute($args, $outputCallback, $artifactDir, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL)); + if (!$process->isSuccessful()) { + throw new AcquiaCliException("Unable to push artifact: {message}", ['message' => $process->getOutput() . $process->getErrorOutput()]); + } + } + } + + /** + * Get a list of Composer vendor directories from the root composer.json. + * + * @return array|string[] + */ + private function vendorDirs(): array + { + if (!empty($this->vendorDirs)) { + return $this->vendorDirs; + } + + $this->vendorDirs = [ + 'vendor', + ]; + if (file_exists($this->composerJsonPath)) { + $composerJson = json_decode($this->localMachineHelper->readFile($this->composerJsonPath), true, 512, JSON_THROW_ON_ERROR); + + foreach ($composerJson['extra']['installer-paths'] as $path => $type) { + $this->vendorDirs[] = str_replace('/{$name}', '', $path); + } + return $this->vendorDirs; + } + return []; + } + + /** + * Get a list of scaffold files from Drupal core's composer.json. + * + * @return array + */ + private function scaffoldFiles(string $artifactDir): array + { + if (!empty($this->scaffoldFiles)) { + return $this->scaffoldFiles; + } + + $this->scaffoldFiles = []; + $composerJson = json_decode($this->localMachineHelper->readFile(Path::join($artifactDir, 'docroot', 'core', 'composer.json')), true, 512, JSON_THROW_ON_ERROR); + foreach ($composerJson['extra']['drupal-scaffold']['file-mapping'] as $file => $assetPath) { + if (str_starts_with($file, '[web-root]')) { + $this->scaffoldFiles[] = str_replace('[web-root]', 'docroot', $file); + } + } + $this->scaffoldFiles[] = 'docroot/autoload.php'; + + return $this->scaffoldFiles; + } + + private function validateSourceCode(): void + { + $requiredPaths = [ + $this->composerJsonPath, + $this->docrootPath, + ]; + foreach ($requiredPaths as $requiredPath) { + if (!file_exists($requiredPath)) { + throw new AcquiaCliException("Your current directory does not look like a valid Drupal application. $requiredPath is missing."); + } + } + } + + private function determineSourceGitRef(): string + { + if ($this->input->getOption('source-git-tag')) { + return $this->input->getOption('source-git-tag'); + } + if ($envVar = getenv('ACLI_PUSH_ARTIFACT_SOURCE_GIT_TAG')) { + return $envVar; + } + if ($this->input->getOption('destination-git-tag')) { + throw new AcquiaCliException('You must also set the --source-git-tag option when setting the --destination-git-tag option.'); + } + + // Assume the source and destination branches are the same. + return $this->destinationGitRef; + } + + private function determineDestinationGitRef(): string + { + if ($this->input->getOption('destination-git-tag')) { + $this->destinationGitRef = $this->input->getOption('destination-git-tag'); + return $this->destinationGitRef; + } + if ($envVar = getenv('ACLI_PUSH_ARTIFACT_DESTINATION_GIT_TAG')) { + $this->destinationGitRef = $envVar; + return $this->destinationGitRef; + } + if ($this->input->getOption('destination-git-branch')) { + $this->destinationGitRef = $this->input->getOption('destination-git-branch'); + return $this->destinationGitRef; + } + if ($envVar = getenv('ACLI_PUSH_ARTIFACT_DESTINATION_GIT_BRANCH')) { + $this->destinationGitRef = $envVar; + return $this->destinationGitRef; + } + + $environment = $this->determineEnvironment($this->input, $this->output); + if (str_starts_with($environment->vcs->path, 'tags')) { + throw new AcquiaCliException("You cannot push to an environment that has a git tag deployed to it. Environment $environment->name has {$environment->vcs->path} deployed. Select a different environment."); + } + + $this->destinationGitRef = $environment->vcs->path; + + return $this->destinationGitRef; + } + + private function createTag(mixed $tagName, Closure $outputCallback, string $artifactDir): void + { + $this->localMachineHelper->checkRequiredBinariesExist(['git']); + $process = $this->localMachineHelper->execute([ 'git', - 'push', - $vcsUrl, - $destGitBranch, - ]; - $process = $this->localMachineHelper->execute($args, $outputCallback, $artifactDir, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL)); - if (!$process->isSuccessful()) { - throw new AcquiaCliException("Unable to push artifact: {message}", ['message' => $process->getOutput() . $process->getErrorOutput()]); - } - } - } - - /** - * Get a list of Composer vendor directories from the root composer.json. - * - * @return array|string[] - */ - private function vendorDirs(): array { - if (!empty($this->vendorDirs)) { - return $this->vendorDirs; + 'tag', + $tagName, + ], $outputCallback, $artifactDir, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL)); + if (!$process->isSuccessful()) { + throw new AcquiaCliException('Failed to create Git tag: {message}', ['message' => $process->getErrorOutput()]); + } } - - $this->vendorDirs = [ - 'vendor', - ]; - if (file_exists($this->composerJsonPath)) { - $composerJson = json_decode($this->localMachineHelper->readFile($this->composerJsonPath), TRUE, 512, JSON_THROW_ON_ERROR); - - foreach ($composerJson['extra']['installer-paths'] as $path => $type) { - $this->vendorDirs[] = str_replace('/{$name}', '', $path); - } - return $this->vendorDirs; - } - return []; - } - - /** - * Get a list of scaffold files from Drupal core's composer.json. - * - * @return array - */ - private function scaffoldFiles(string $artifactDir): array { - if (!empty($this->scaffoldFiles)) { - return $this->scaffoldFiles; - } - - $this->scaffoldFiles = []; - $composerJson = json_decode($this->localMachineHelper->readFile(Path::join($artifactDir, 'docroot', 'core', 'composer.json')), TRUE, 512, JSON_THROW_ON_ERROR); - foreach ($composerJson['extra']['drupal-scaffold']['file-mapping'] as $file => $assetPath) { - if (str_starts_with($file, '[web-root]')) { - $this->scaffoldFiles[] = str_replace('[web-root]', 'docroot', $file); - } - } - $this->scaffoldFiles[] = 'docroot/autoload.php'; - - return $this->scaffoldFiles; - } - - private function validateSourceCode(): void { - $requiredPaths = [ - $this->composerJsonPath, - $this->docrootPath, - ]; - foreach ($requiredPaths as $requiredPath) { - if (!file_exists($requiredPath)) { - throw new AcquiaCliException("Your current directory does not look like a valid Drupal application. $requiredPath is missing."); - } - } - } - - private function determineSourceGitRef(): string { - if ($this->input->getOption('source-git-tag')) { - return $this->input->getOption('source-git-tag'); - } - if ($envVar = getenv('ACLI_PUSH_ARTIFACT_SOURCE_GIT_TAG')) { - return $envVar; - } - if ($this->input->getOption('destination-git-tag')) { - throw new AcquiaCliException('You must also set the --source-git-tag option when setting the --destination-git-tag option.'); - } - - // Assume the source and destination branches are the same. - return $this->destinationGitRef; - } - - private function determineDestinationGitRef(): string { - if ($this->input->getOption('destination-git-tag')) { - $this->destinationGitRef = $this->input->getOption('destination-git-tag'); - return $this->destinationGitRef; - } - if ($envVar = getenv('ACLI_PUSH_ARTIFACT_DESTINATION_GIT_TAG')) { - $this->destinationGitRef = $envVar; - return $this->destinationGitRef; - } - if ($this->input->getOption('destination-git-branch')) { - $this->destinationGitRef = $this->input->getOption('destination-git-branch'); - return $this->destinationGitRef; - } - if ($envVar = getenv('ACLI_PUSH_ARTIFACT_DESTINATION_GIT_BRANCH')) { - $this->destinationGitRef = $envVar; - return $this->destinationGitRef; - } - - $environment = $this->determineEnvironment($this->input, $this->output); - if (str_starts_with($environment->vcs->path, 'tags')) { - throw new AcquiaCliException("You cannot push to an environment that has a git tag deployed to it. Environment $environment->name has {$environment->vcs->path} deployed. Select a different environment."); - } - - $this->destinationGitRef = $environment->vcs->path; - - return $this->destinationGitRef; - } - - private function createTag(mixed $tagName, Closure $outputCallback, string $artifactDir): void { - $this->localMachineHelper->checkRequiredBinariesExist(['git']); - $process = $this->localMachineHelper->execute([ - 'git', - 'tag', - $tagName, - ], $outputCallback, $artifactDir, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL)); - if (!$process->isSuccessful()) { - throw new AcquiaCliException('Failed to create Git tag: {message}', ['message' => $process->getErrorOutput()]); - } - } - } diff --git a/src/Command/Push/PushCodeCommand.php b/src/Command/Push/PushCodeCommand.php index 9f96c0d10..f05b0577e 100644 --- a/src/Command/Push/PushCodeCommand.php +++ b/src/Command/Push/PushCodeCommand.php @@ -1,6 +1,6 @@ setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv() && !self::isLandoEnv()); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $output->writeln("Use git to push code changes upstream."); - - return Command::SUCCESS; - } - +final class PushCodeCommand extends PushCommandBase +{ + protected function configure(): void + { + $this + ->setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv() && !self::isLandoEnv()); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $output->writeln("Use git to push code changes upstream."); + + return Command::SUCCESS; + } } diff --git a/src/Command/Push/PushCommandBase.php b/src/Command/Push/PushCommandBase.php index 2b6c29bd0..94dbb73d7 100644 --- a/src/Command/Push/PushCommandBase.php +++ b/src/Command/Push/PushCommandBase.php @@ -1,14 +1,13 @@ acceptEnvironmentId() - ->acceptSite(); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $destinationEnvironment = $this->determineEnvironment($input, $output); - $acquiaCloudClient = $this->cloudApiClientService->getClient(); - $databases = $this->determineCloudDatabases($acquiaCloudClient, $destinationEnvironment, $input->getArgument('site')); - // We only support pushing a single database. - $database = $databases[0]; - $answer = $this->io->confirm("Overwrite the $database->name database on {$destinationEnvironment->name} with a copy of the database from the current machine?"); - if (!$answer) { - return Command::SUCCESS; +final class PushDatabaseCommand extends PushCommandBase +{ + protected function configure(): void + { + $this + ->acceptEnvironmentId() + ->acceptSite(); } - $this->checklist = new Checklist($output); - $outputCallback = $this->getOutputCallback($output, $this->checklist); - - $this->checklist->addItem('Creating local database dump'); - $localDumpFilepath = $this->createMySqlDumpOnLocal($this->getLocalDbHost(), $this->getLocalDbUser(), $this->getLocalDbName(), $this->getLocalDbPassword(), $outputCallback); - $this->checklist->completePreviousItem(); - - $this->checklist->addItem('Uploading database dump to remote machine'); - $remoteDumpFilepath = $this->uploadDatabaseDump($destinationEnvironment, $localDumpFilepath, $outputCallback); - $this->checklist->completePreviousItem(); - - $this->checklist->addItem('Importing database dump into MySQL on remote machine'); - $this->importDatabaseDumpOnRemote($destinationEnvironment, $remoteDumpFilepath, $database); - $this->checklist->completePreviousItem(); - - return Command::SUCCESS; - } - - private function uploadDatabaseDump( - EnvironmentResponse $environment, - string $localFilepath, - callable $outputCallback - ): string { - $envAlias = self::getEnvironmentAlias($environment); - $remoteFilepath = "/mnt/tmp/$envAlias/" . basename($localFilepath); - $this->logger->debug("Uploading database dump to $remoteFilepath on remote machine"); - $this->localMachineHelper->checkRequiredBinariesExist(['rsync']); - $command = [ - 'rsync', - '-tDvPhe', - 'ssh -o StrictHostKeyChecking=no', - $localFilepath, - $environment->sshUrl . ':' . $remoteFilepath, - ]; - $process = $this->localMachineHelper->execute($command, $outputCallback, NULL, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL)); - if (!$process->isSuccessful()) { - throw new AcquiaCliException('Could not upload local database dump: {message}', - ['message' => $process->getOutput()]); + protected function execute(InputInterface $input, OutputInterface $output): int + { + $destinationEnvironment = $this->determineEnvironment($input, $output); + $acquiaCloudClient = $this->cloudApiClientService->getClient(); + $databases = $this->determineCloudDatabases($acquiaCloudClient, $destinationEnvironment, $input->getArgument('site')); + // We only support pushing a single database. + $database = $databases[0]; + $answer = $this->io->confirm("Overwrite the $database->name database on {$destinationEnvironment->name} with a copy of the database from the current machine?"); + if (!$answer) { + return Command::SUCCESS; + } + + $this->checklist = new Checklist($output); + $outputCallback = $this->getOutputCallback($output, $this->checklist); + + $this->checklist->addItem('Creating local database dump'); + $localDumpFilepath = $this->createMySqlDumpOnLocal($this->getLocalDbHost(), $this->getLocalDbUser(), $this->getLocalDbName(), $this->getLocalDbPassword(), $outputCallback); + $this->checklist->completePreviousItem(); + + $this->checklist->addItem('Uploading database dump to remote machine'); + $remoteDumpFilepath = $this->uploadDatabaseDump($destinationEnvironment, $localDumpFilepath, $outputCallback); + $this->checklist->completePreviousItem(); + + $this->checklist->addItem('Importing database dump into MySQL on remote machine'); + $this->importDatabaseDumpOnRemote($destinationEnvironment, $remoteDumpFilepath, $database); + $this->checklist->completePreviousItem(); + + return Command::SUCCESS; } - return $remoteFilepath; - } - - private function importDatabaseDumpOnRemote(EnvironmentResponse $environment, string $remoteDumpFilepath, DatabaseResponse $database): void { - $this->logger->debug("Importing $remoteDumpFilepath to MySQL on remote machine"); - $command = "pv $remoteDumpFilepath --bytes --rate | gunzip | MYSQL_PWD={$database->password} mysql --host={$this->getHostFromDatabaseResponse($environment, $database)} --user={$database->user_name} {$this->getNameFromDatabaseResponse($database)}"; - $process = $this->sshHelper->executeCommand($environment->sshUrl, [$command], ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL)); - if (!$process->isSuccessful()) { - throw new AcquiaCliException('Unable to import database on remote machine. {message}', ['message' => $process->getErrorOutput()]); + private function uploadDatabaseDump( + EnvironmentResponse $environment, + string $localFilepath, + callable $outputCallback + ): string { + $envAlias = self::getEnvironmentAlias($environment); + $remoteFilepath = "/mnt/tmp/$envAlias/" . basename($localFilepath); + $this->logger->debug("Uploading database dump to $remoteFilepath on remote machine"); + $this->localMachineHelper->checkRequiredBinariesExist(['rsync']); + $command = [ + 'rsync', + '-tDvPhe', + 'ssh -o StrictHostKeyChecking=no', + $localFilepath, + $environment->sshUrl . ':' . $remoteFilepath, + ]; + $process = $this->localMachineHelper->execute($command, $outputCallback, null, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL)); + if (!$process->isSuccessful()) { + throw new AcquiaCliException( + 'Could not upload local database dump: {message}', + ['message' => $process->getOutput()] + ); + } + + return $remoteFilepath; } - } - private function getNameFromDatabaseResponse(DatabaseResponse $database): string { - $dbUrlParts = explode('/', $database->url); - return end($dbUrlParts); - } + private function importDatabaseDumpOnRemote(EnvironmentResponse $environment, string $remoteDumpFilepath, DatabaseResponse $database): void + { + $this->logger->debug("Importing $remoteDumpFilepath to MySQL on remote machine"); + $command = "pv $remoteDumpFilepath --bytes --rate | gunzip | MYSQL_PWD={$database->password} mysql --host={$this->getHostFromDatabaseResponse($environment, $database)} --user={$database->user_name} {$this->getNameFromDatabaseResponse($database)}"; + $process = $this->sshHelper->executeCommand($environment->sshUrl, [$command], ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL)); + if (!$process->isSuccessful()) { + throw new AcquiaCliException('Unable to import database on remote machine. {message}', ['message' => $process->getErrorOutput()]); + } + } + private function getNameFromDatabaseResponse(DatabaseResponse $database): string + { + $dbUrlParts = explode('/', $database->url); + return end($dbUrlParts); + } } diff --git a/src/Command/Push/PushFilesCommand.php b/src/Command/Push/PushFilesCommand.php index 08fe07661..1414bbaf6 100644 --- a/src/Command/Push/PushFilesCommand.php +++ b/src/Command/Push/PushFilesCommand.php @@ -1,6 +1,6 @@ acceptEnvironmentId() - ->acceptSite(); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $this->setDirAndRequireProjectCwd($input); - $destinationEnvironment = $this->determineEnvironment($input, $output); - $chosenSite = $input->getArgument('site'); - if (!$chosenSite) { - $chosenSite = $this->promptChooseDrupalSite($destinationEnvironment); +final class PushFilesCommand extends PushCommandBase +{ + protected function configure(): void + { + $this + ->acceptEnvironmentId() + ->acceptSite(); } - $answer = $this->io->confirm("Overwrite the public files directory on $destinationEnvironment->name with a copy of the files from the current machine?"); - if (!$answer) { - return Command::SUCCESS; - } - - $this->checklist = new Checklist($output); - $this->checklist->addItem('Pushing public files directory to remote machine'); - $this->rsyncFilesToCloud($destinationEnvironment, $this->getOutputCallback($output, $this->checklist), $chosenSite); - $this->checklist->completePreviousItem(); - - return Command::SUCCESS; - } - private function rsyncFilesToCloud(EnvironmentResponse $chosenEnvironment, callable $outputCallback = NULL, string $site = NULL): void { - $sourceDir = $this->getLocalFilesDir($site); - $destinationDir = $chosenEnvironment->sshUrl . ':' . $this->getCloudFilesDir($chosenEnvironment, $site); + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->setDirAndRequireProjectCwd($input); + $destinationEnvironment = $this->determineEnvironment($input, $output); + $chosenSite = $input->getArgument('site'); + if (!$chosenSite) { + $chosenSite = $this->promptChooseDrupalSite($destinationEnvironment); + } + $answer = $this->io->confirm("Overwrite the public files directory on $destinationEnvironment->name with a copy of the files from the current machine?"); + if (!$answer) { + return Command::SUCCESS; + } + + $this->checklist = new Checklist($output); + $this->checklist->addItem('Pushing public files directory to remote machine'); + $this->rsyncFilesToCloud($destinationEnvironment, $this->getOutputCallback($output, $this->checklist), $chosenSite); + $this->checklist->completePreviousItem(); + + return Command::SUCCESS; + } - $this->rsyncFiles($sourceDir, $destinationDir, $outputCallback); - } + private function rsyncFilesToCloud(EnvironmentResponse $chosenEnvironment, callable $outputCallback = null, string $site = null): void + { + $sourceDir = $this->getLocalFilesDir($site); + $destinationDir = $chosenEnvironment->sshUrl . ':' . $this->getCloudFilesDir($chosenEnvironment, $site); + $this->rsyncFiles($sourceDir, $destinationDir, $outputCallback); + } } diff --git a/src/Command/Remote/AliasListCommand.php b/src/Command/Remote/AliasListCommand.php index 21c239f2a..bb769778b 100644 --- a/src/Command/Remote/AliasListCommand.php +++ b/src/Command/Remote/AliasListCommand.php @@ -1,6 +1,6 @@ acceptApplicationUuid(); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $acquiaCloudClient = $this->cloudApiClientService->getClient(); - $applicationsResource = new Applications($acquiaCloudClient); - $cloudApplicationUuid = $this->determineCloudApplication(); - $customerApplication = $applicationsResource->get($cloudApplicationUuid); - $environmentsResource = new Environments($acquiaCloudClient); - - $table = new Table($this->output); - $table->setHeaders(['Application', 'Environment Alias', 'Environment UUID']); - - $siteId = $customerApplication->hosting->id; - $parts = explode(':', $siteId); - $sitePrefix = $parts[1]; - $environments = $environmentsResource->getAll($customerApplication->uuid); - foreach ($environments as $environment) { - $alias = $sitePrefix . '.' . $environment->name; - $table->addRow([$customerApplication->name, $alias, $environment->uuid]); +final class AliasListCommand extends CommandBase +{ + protected function configure(): void + { + $this->acceptApplicationUuid(); } - $table->render(); - - return Command::SUCCESS; - } - + protected function execute(InputInterface $input, OutputInterface $output): int + { + $acquiaCloudClient = $this->cloudApiClientService->getClient(); + $applicationsResource = new Applications($acquiaCloudClient); + $cloudApplicationUuid = $this->determineCloudApplication(); + $customerApplication = $applicationsResource->get($cloudApplicationUuid); + $environmentsResource = new Environments($acquiaCloudClient); + + $table = new Table($this->output); + $table->setHeaders(['Application', 'Environment Alias', 'Environment UUID']); + + $siteId = $customerApplication->hosting->id; + $parts = explode(':', $siteId); + $sitePrefix = $parts[1]; + $environments = $environmentsResource->getAll($customerApplication->uuid); + foreach ($environments as $environment) { + $alias = $sitePrefix . '.' . $environment->name; + $table->addRow([$customerApplication->name, $alias, $environment->uuid]); + } + + $table->render(); + + return Command::SUCCESS; + } } diff --git a/src/Command/Remote/AliasesDownloadCommand.php b/src/Command/Remote/AliasesDownloadCommand.php index 7579e8895..89f7a0978 100644 --- a/src/Command/Remote/AliasesDownloadCommand.php +++ b/src/Command/Remote/AliasesDownloadCommand.php @@ -1,6 +1,6 @@ addOption('destination-dir', NULL, InputOption::VALUE_REQUIRED, 'The directory to which aliases will be downloaded') - ->addOption('all', NULL, InputOption::VALUE_NONE, 'Download the aliases for all applications that you have access to, not just the current one.'); - $this->acceptApplicationUuid(); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $aliasVersion = $this->promptChooseDrushAliasVersion(); - $drushArchiveTempFilepath = $this->getDrushArchiveTempFilepath(); - $drushAliasesDir = $this->getDrushAliasesDir($aliasVersion); - $this->localMachineHelper->getFilesystem()->mkdir($drushAliasesDir); - $this->localMachineHelper->getFilesystem()->chmod($drushAliasesDir, 0700); - - if ($aliasVersion === '9') { - $this->downloadDrush9Aliases($input, $aliasVersion, $drushArchiveTempFilepath, $drushAliasesDir); +final class AliasesDownloadCommand extends SshBaseCommand +{ + private string $drushArchiveFilepath; + + protected function configure(): void + { + $this + ->addOption('destination-dir', null, InputOption::VALUE_REQUIRED, 'The directory to which aliases will be downloaded') + ->addOption('all', null, InputOption::VALUE_NONE, 'Download the aliases for all applications that you have access to, not just the current one.'); + $this->acceptApplicationUuid(); } - else { - $this->downloadDrush8Aliases($aliasVersion, $drushArchiveTempFilepath, $drushAliasesDir); + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $aliasVersion = $this->promptChooseDrushAliasVersion(); + $drushArchiveTempFilepath = $this->getDrushArchiveTempFilepath(); + $drushAliasesDir = $this->getDrushAliasesDir($aliasVersion); + $this->localMachineHelper->getFilesystem()->mkdir($drushAliasesDir); + $this->localMachineHelper->getFilesystem()->chmod($drushAliasesDir, 0700); + + if ($aliasVersion === '9') { + $this->downloadDrush9Aliases($input, $aliasVersion, $drushArchiveTempFilepath, $drushAliasesDir); + } else { + $this->downloadDrush8Aliases($aliasVersion, $drushArchiveTempFilepath, $drushAliasesDir); + } + + $this->output->writeln(sprintf( + 'Cloud Platform Drush aliases installed into %s', + $drushAliasesDir + )); + unlink($drushArchiveTempFilepath); + + return Command::SUCCESS; } - $this->output->writeln(sprintf( - 'Cloud Platform Drush aliases installed into %s', - $drushAliasesDir - )); - unlink($drushArchiveTempFilepath); - - return Command::SUCCESS; - } - - /** - * Prompts the user for their preferred Drush alias version. - */ - protected function promptChooseDrushAliasVersion(): string { - $this->io->writeln('Drush changed how aliases are defined in Drush 9. Drush 8 aliases are PHP-based and stored in your home directory, while Drush 9+ aliases are YAML-based and stored with your project.'); - $question = 'Choose your preferred alias compatibility:'; - $choices = [ - '8' => 'Drush 8 / Drupal 7 (PHP)', - '9' => 'Drush 9+ / Drupal 8+ (YAML)', - ]; - return (string) array_search($this->io->choice($question, $choices, '9'), $choices, TRUE); - } - - public function getDrushArchiveTempFilepath(): string { - if (!isset($this->drushArchiveFilepath)) { - $this->drushArchiveFilepath = tempnam(sys_get_temp_dir(), - 'AcquiaDrushAliases') . '.tar.gz'; + /** + * Prompts the user for their preferred Drush alias version. + */ + protected function promptChooseDrushAliasVersion(): string + { + $this->io->writeln('Drush changed how aliases are defined in Drush 9. Drush 8 aliases are PHP-based and stored in your home directory, while Drush 9+ aliases are YAML-based and stored with your project.'); + $question = 'Choose your preferred alias compatibility:'; + $choices = [ + '8' => 'Drush 8 / Drupal 7 (PHP)', + '9' => 'Drush 9+ / Drupal 8+ (YAML)', + ]; + return (string) array_search($this->io->choice($question, $choices, '9'), $choices, true); } - return $this->drushArchiveFilepath; - } + public function getDrushArchiveTempFilepath(): string + { + if (!isset($this->drushArchiveFilepath)) { + $this->drushArchiveFilepath = tempnam( + sys_get_temp_dir(), + 'AcquiaDrushAliases' + ) . '.tar.gz'; + } - protected function getDrushAliasesDir(string $version): string { - if ($this->input->getOption('destination-dir')) { - return $this->input->getOption('destination-dir'); - } - return match ($version) { - '8' => Path::join($this->localMachineHelper::getHomeDir(), '.drush'), - '9' => Path::join($this->getProjectDir(), 'drush'), - default => throw new AcquiaCliException("Unknown Drush version"), - }; - } - - protected function getAliasesFromCloud(Client $acquiaCloudClient, string $aliasVersion): StreamInterface { - $acquiaCloudClient->addQuery('version', $aliasVersion); - return (new Account($acquiaCloudClient))->getDrushAliases(); - } - - protected function getSitePrefix(bool $singleApplication): string { - $sitePrefix = ''; - if ($singleApplication) { - $cloudApplicationUuid = $this->determineCloudApplication(); - $cloudApplication = $this->getCloudApplication($cloudApplicationUuid); - $parts = explode(':', $cloudApplication->hosting->id); - $sitePrefix = $parts[1]; - } - return $sitePrefix; - } - - protected function downloadArchive(string $aliasVersion, string $drushArchiveTempFilepath, string $baseDir): PharData { - $acquiaCloudClient = $this->cloudApiClientService->getClient(); - $aliases = $this->getAliasesFromCloud($acquiaCloudClient, $aliasVersion); - $this->localMachineHelper->writeFile($drushArchiveTempFilepath, $aliases); - return new PharData($drushArchiveTempFilepath . '/' . $baseDir); - } - - protected function downloadDrush9Aliases(InputInterface $input, string $aliasVersion, string $drushArchiveTempFilepath, string $drushAliasesDir): void { - $this->setDirAndRequireProjectCwd($input); - $all = $input->getOption('all'); - $applicationUuidArgument = $input->getArgument('applicationUuid'); - $singleApplication = !$all || $applicationUuidArgument; - $sitePrefix = $this->getSitePrefix($singleApplication); - $baseDir = 'sites'; - $archive = $this->downloadArchive($aliasVersion, $drushArchiveTempFilepath, $baseDir); - if ($singleApplication) { - $drushFiles = $this->getSingleAliasForSite($archive, $sitePrefix, $baseDir); + return $this->drushArchiveFilepath; } - else { - $drushFiles = []; - foreach (new RecursiveIteratorIterator($archive, RecursiveIteratorIterator::LEAVES_ONLY) as $file) { - $drushFiles[] = $baseDir . '/' . $file->getFileName(); - } + + protected function getDrushAliasesDir(string $version): string + { + if ($this->input->getOption('destination-dir')) { + return $this->input->getOption('destination-dir'); + } + return match ($version) { + '8' => Path::join($this->localMachineHelper::getHomeDir(), '.drush'), + '9' => Path::join($this->getProjectDir(), 'drush'), + default => throw new AcquiaCliException("Unknown Drush version"), + }; } - try { - // Throws warnings on permissions errors. - @$archive->extractTo($drushAliasesDir, $drushFiles, TRUE); + + protected function getAliasesFromCloud(Client $acquiaCloudClient, string $aliasVersion): StreamInterface + { + $acquiaCloudClient->addQuery('version', $aliasVersion); + return (new Account($acquiaCloudClient))->getDrushAliases(); } - catch (\Exception) { - throw new AcquiaCliException('Could not extract aliases to {destination}', ['destination' => $drushAliasesDir]); + + protected function getSitePrefix(bool $singleApplication): string + { + $sitePrefix = ''; + if ($singleApplication) { + $cloudApplicationUuid = $this->determineCloudApplication(); + $cloudApplication = $this->getCloudApplication($cloudApplicationUuid); + $parts = explode(':', $cloudApplication->hosting->id); + $sitePrefix = $parts[1]; + } + return $sitePrefix; } - } - - protected function downloadDrush8Aliases(string $aliasVersion, string $drushArchiveTempFilepath, string $drushAliasesDir): void { - $baseDir = '.drush'; - $archive = $this->downloadArchive($aliasVersion, $drushArchiveTempFilepath, $baseDir); - $drushFiles = []; - foreach (new RecursiveIteratorIterator($archive, RecursiveIteratorIterator::LEAVES_ONLY) as $file) { - $drushFiles[] = $baseDir . '/' . $file->getFileName(); + + protected function downloadArchive(string $aliasVersion, string $drushArchiveTempFilepath, string $baseDir): PharData + { + $acquiaCloudClient = $this->cloudApiClientService->getClient(); + $aliases = $this->getAliasesFromCloud($acquiaCloudClient, $aliasVersion); + $this->localMachineHelper->writeFile($drushArchiveTempFilepath, $aliases); + return new PharData($drushArchiveTempFilepath . '/' . $baseDir); } - $archive->extractTo($drushAliasesDir, $drushFiles, TRUE); - } - - /** - * @return array - */ - protected function getSingleAliasForSite(PharData $archive, string $sitePrefix, string $baseDir): array { - $drushFiles = []; - foreach (new RecursiveIteratorIterator($archive, RecursiveIteratorIterator::LEAVES_ONLY) as $file) { - // Just get the single alias for this single application. - if ($file->getFileName() === $sitePrefix . '.site.yml') { - $drushFiles[] = $baseDir . '/' . $file->getFileName(); - break; - } + + protected function downloadDrush9Aliases(InputInterface $input, string $aliasVersion, string $drushArchiveTempFilepath, string $drushAliasesDir): void + { + $this->setDirAndRequireProjectCwd($input); + $all = $input->getOption('all'); + $applicationUuidArgument = $input->getArgument('applicationUuid'); + $singleApplication = !$all || $applicationUuidArgument; + $sitePrefix = $this->getSitePrefix($singleApplication); + $baseDir = 'sites'; + $archive = $this->downloadArchive($aliasVersion, $drushArchiveTempFilepath, $baseDir); + if ($singleApplication) { + $drushFiles = $this->getSingleAliasForSite($archive, $sitePrefix, $baseDir); + } else { + $drushFiles = []; + foreach (new RecursiveIteratorIterator($archive, RecursiveIteratorIterator::LEAVES_ONLY) as $file) { + $drushFiles[] = $baseDir . '/' . $file->getFileName(); + } + } + try { + // Throws warnings on permissions errors. + @$archive->extractTo($drushAliasesDir, $drushFiles, true); + } catch (\Exception) { + throw new AcquiaCliException('Could not extract aliases to {destination}', ['destination' => $drushAliasesDir]); + } } - if (empty($drushFiles)) { - throw new AcquiaCliException("Could not locate any aliases matching the current site ($sitePrefix)"); + + protected function downloadDrush8Aliases(string $aliasVersion, string $drushArchiveTempFilepath, string $drushAliasesDir): void + { + $baseDir = '.drush'; + $archive = $this->downloadArchive($aliasVersion, $drushArchiveTempFilepath, $baseDir); + $drushFiles = []; + foreach (new RecursiveIteratorIterator($archive, RecursiveIteratorIterator::LEAVES_ONLY) as $file) { + $drushFiles[] = $baseDir . '/' . $file->getFileName(); + } + $archive->extractTo($drushAliasesDir, $drushFiles, true); } - return $drushFiles; - } + /** + * @return array + */ + protected function getSingleAliasForSite(PharData $archive, string $sitePrefix, string $baseDir): array + { + $drushFiles = []; + foreach (new RecursiveIteratorIterator($archive, RecursiveIteratorIterator::LEAVES_ONLY) as $file) { + // Just get the single alias for this single application. + if ($file->getFileName() === $sitePrefix . '.site.yml') { + $drushFiles[] = $baseDir . '/' . $file->getFileName(); + break; + } + } + if (empty($drushFiles)) { + throw new AcquiaCliException("Could not locate any aliases matching the current site ($sitePrefix)"); + } + return $drushFiles; + } } diff --git a/src/Command/Remote/DrushCommand.php b/src/Command/Remote/DrushCommand.php index df1372fa1..dad6feb32 100644 --- a/src/Command/Remote/DrushCommand.php +++ b/src/Command/Remote/DrushCommand.php @@ -1,6 +1,6 @@ setHelp('Pay close attention to the argument syntax! Note the usage of -- to separate the drush command arguments and options.') - ->acceptEnvironmentId() - ->addArgument('drush_command', InputArgument::IS_ARRAY, 'Drush command') - ->addUsage('. -- ') - ->addUsage('myapp.dev -- uli 1') - ->addUsage('myapp.dev -- status --fields=db-status'); - } - - protected function execute(InputInterface $input, OutputInterface $output): ?int { - $environment = $this->determineEnvironment($input, $output); - $alias = self::getEnvironmentAlias($environment); - $acliArguments = $input->getArguments(); - $drushArguments = (array) $acliArguments['drush_command']; - // When available, provide the default domain to drush. - if (!empty($environment->default_domain)) { - // Insert at the beginning so a user-supplied --uri arg will override. - array_unshift($drushArguments, "--uri=http://$environment->default_domain"); +final class DrushCommand extends SshBaseCommand +{ + protected function configure(): void + { + $this + ->setHelp('Pay close attention to the argument syntax! Note the usage of -- to separate the drush command arguments and options.') + ->acceptEnvironmentId() + ->addArgument('drush_command', InputArgument::IS_ARRAY, 'Drush command') + ->addUsage('. -- ') + ->addUsage('myapp.dev -- uli 1') + ->addUsage('myapp.dev -- status --fields=db-status'); } - $drushCommandArguments = [ - "cd /var/www/html/$alias/docroot; ", - 'drush', - implode(' ', $drushArguments), - ]; - - return $this->sshHelper->executeCommand($environment->sshUrl, $drushCommandArguments)->getExitCode(); - } + protected function execute(InputInterface $input, OutputInterface $output): ?int + { + $environment = $this->determineEnvironment($input, $output); + $alias = self::getEnvironmentAlias($environment); + $acliArguments = $input->getArguments(); + $drushArguments = (array) $acliArguments['drush_command']; + // When available, provide the default domain to drush. + if (!empty($environment->default_domain)) { + // Insert at the beginning so a user-supplied --uri arg will override. + array_unshift($drushArguments, "--uri=http://$environment->default_domain"); + } + $drushCommandArguments = [ + "cd /var/www/html/$alias/docroot; ", + 'drush', + implode(' ', $drushArguments), + ]; + + return $this->sshHelper->executeCommand($environment->sshUrl, $drushCommandArguments)->getExitCode(); + } } diff --git a/src/Command/Remote/SshBaseCommand.php b/src/Command/Remote/SshBaseCommand.php index 08cb35dcc..c3ca29905 100644 --- a/src/Command/Remote/SshBaseCommand.php +++ b/src/Command/Remote/SshBaseCommand.php @@ -1,6 +1,6 @@ addArgument('alias', InputArgument::REQUIRED, 'Alias for application & environment in the format `app-name.env`') - ->addArgument('ssh_command', InputArgument::IS_ARRAY, 'Command to run via SSH (if not provided, opens a shell in the site directory)') - ->addUsage("myapp.dev # open a shell in the myapp.dev environment") - ->addUsage("myapp.dev -- ls -al # list files in the myapp.dev environment and return"); - } - - protected function execute(InputInterface $input, OutputInterface $output): ?int { - $alias = $input->getArgument('alias'); - $alias = $this->normalizeAlias($alias); - $alias = self::validateEnvironmentAlias($alias); - $environment = $this->getEnvironmentFromAliasArg($alias); - if (!isset($environment->sshUrl)) { - throw new AcquiaCliException('Cannot determine environment SSH URL. Check that you have SSH permissions on this environment.'); - } - $sshCommand = [ - 'cd /var/www/html/' . $alias, - ]; - $arguments = $input->getArguments(); - if (empty($arguments['ssh_command'])) { - $sshCommand[] = 'exec $SHELL -l'; +final class SshCommand extends SshBaseCommand +{ + protected function configure(): void + { + $this + ->addArgument('alias', InputArgument::REQUIRED, 'Alias for application & environment in the format `app-name.env`') + ->addArgument('ssh_command', InputArgument::IS_ARRAY, 'Command to run via SSH (if not provided, opens a shell in the site directory)') + ->addUsage("myapp.dev # open a shell in the myapp.dev environment") + ->addUsage("myapp.dev -- ls -al # list files in the myapp.dev environment and return"); } - else { - $sshCommand[] = implode(' ', $arguments['ssh_command']); - } - $sshCommand = (array) implode('; ', $sshCommand); - return $this->sshHelper->executeCommand($environment->sshUrl, $sshCommand)->getExitCode(); - } + protected function execute(InputInterface $input, OutputInterface $output): ?int + { + $alias = $input->getArgument('alias'); + $alias = $this->normalizeAlias($alias); + $alias = self::validateEnvironmentAlias($alias); + $environment = $this->getEnvironmentFromAliasArg($alias); + if (!isset($environment->sshUrl)) { + throw new AcquiaCliException('Cannot determine environment SSH URL. Check that you have SSH permissions on this environment.'); + } + $sshCommand = [ + 'cd /var/www/html/' . $alias, + ]; + $arguments = $input->getArguments(); + if (empty($arguments['ssh_command'])) { + $sshCommand[] = 'exec $SHELL -l'; + } else { + $sshCommand[] = implode(' ', $arguments['ssh_command']); + } + $sshCommand = (array) implode('; ', $sshCommand); + return $this->sshHelper->executeCommand($environment->sshUrl, $sshCommand)->getExitCode(); + } } diff --git a/src/Command/Self/ClearCacheCommand.php b/src/Command/Self/ClearCacheCommand.php index 597134964..f68408ef2 100644 --- a/src/Command/Self/ClearCacheCommand.php +++ b/src/Command/Self/ClearCacheCommand.php @@ -1,6 +1,6 @@ writeln('Acquia CLI caches were cleared.'); - - return Command::SUCCESS; - } - - /** - * Clear caches. - */ - public static function clearCaches(): void { - $cache = self::getAliasCache(); - $cache->clear(); - $systemCacheDir = Path::join(sys_get_temp_dir(), 'symphony-cache'); - $fs = new Filesystem(); - $fs->remove([$systemCacheDir]); - } - +final class ClearCacheCommand extends CommandBase +{ + protected function execute(InputInterface $input, OutputInterface $output): int + { + self::clearCaches(); + $output->writeln('Acquia CLI caches were cleared.'); + + return Command::SUCCESS; + } + + /** + * Clear caches. + */ + public static function clearCaches(): void + { + $cache = self::getAliasCache(); + $cache->clear(); + $systemCacheDir = Path::join(sys_get_temp_dir(), 'symphony-cache'); + $fs = new Filesystem(); + $fs->remove([$systemCacheDir]); + } } diff --git a/src/Command/Self/ListCommand.php b/src/Command/Self/ListCommand.php index 836d41058..224302db6 100644 --- a/src/Command/Self/ListCommand.php +++ b/src/Command/Self/ListCommand.php @@ -1,6 +1,6 @@ setName('list') - ->setDefinition([ - new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name', NULL, fn () => array_keys((new ApplicationDescription($this->getApplication()))->getNamespaces())), - new InputOption('raw', NULL, InputOption::VALUE_NONE, 'To output raw command list'), - new InputOption('format', NULL, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt', fn () => (new DescriptorHelper())->getFormats()), - new InputOption('short', NULL, InputOption::VALUE_NONE, 'To skip describing commands\' arguments'), - ]) - ->setDescription('List commands') - ->setHelp(<<<'EOF' +#[AsCommand(name: 'list', description: null, aliases: ['self:list'])] +final class ListCommand extends CommandBase +{ + protected function configure(): void + { + $this + ->setName('list') + ->setDefinition([ + new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name', null, fn () => array_keys((new ApplicationDescription($this->getApplication()))->getNamespaces())), + new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command list'), + new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt', fn () => (new DescriptorHelper())->getFormats()), + new InputOption('short', null, InputOption::VALUE_NONE, 'To skip describing commands\' arguments'), + ]) + ->setDescription('List commands') + ->setHelp(<<<'EOF' The %command.name% command lists all commands: %command.full_name% @@ -46,33 +47,33 @@ protected function configure(): void { %command.full_name% --raw EOF - ); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - foreach (['api', 'acsf'] as $prefix) { - if ($input->getArgument('namespace') !== $prefix) { - $allCommands = $this->getApplication()->all(); - foreach ($allCommands as $command) { - if ( - !is_a($command, ApiListCommandBase::class) - && !is_a($command, AcsfListCommandBase::class) - && str_starts_with($command->getName(), $prefix . ':') - ) { - $command->setHidden(); - } - } - } + ); } - $helper = new DescriptorHelper(); - $helper->describe($output, $this->getApplication(), [ - 'format' => $input->getOption('format'), - 'namespace' => $input->getArgument('namespace'), - 'raw_text' => $input->getOption('raw'), - ]); + protected function execute(InputInterface $input, OutputInterface $output): int + { + foreach (['api', 'acsf'] as $prefix) { + if ($input->getArgument('namespace') !== $prefix) { + $allCommands = $this->getApplication()->all(); + foreach ($allCommands as $command) { + if ( + !is_a($command, ApiListCommandBase::class) + && !is_a($command, AcsfListCommandBase::class) + && str_starts_with($command->getName(), $prefix . ':') + ) { + $command->setHidden(); + } + } + } + } - return Command::SUCCESS; - } + $helper = new DescriptorHelper(); + $helper->describe($output, $this->getApplication(), [ + 'format' => $input->getOption('format'), + 'namespace' => $input->getArgument('namespace'), + 'raw_text' => $input->getOption('raw'), + ]); + return Command::SUCCESS; + } } diff --git a/src/Command/Self/MakeDocsCommand.php b/src/Command/Self/MakeDocsCommand.php index 17795c272..ad9896a5b 100644 --- a/src/Command/Self/MakeDocsCommand.php +++ b/src/Command/Self/MakeDocsCommand.php @@ -1,6 +1,6 @@ addOption('format', 'f', InputOption::VALUE_OPTIONAL, 'The format to describe the docs in.', 'rst'); - $this->addOption('dump', 'd', InputOption::VALUE_OPTIONAL, 'Dump docs to directory'); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $helper = new DescriptorHelper(); - - if (!$input->getOption('dump')) { - $helper->describe($output, $this->getApplication(), [ - 'format' => $input->getOption('format'), - ]); - return Command::SUCCESS; +#[AsCommand(name: 'self:make-docs', description: 'Generate documentation for all ACLI commands', hidden: true)] +final class MakeDocsCommand extends CommandBase +{ + protected function configure(): void + { + $this->addOption('format', 'f', InputOption::VALUE_OPTIONAL, 'The format to describe the docs in.', 'rst'); + $this->addOption('dump', 'd', InputOption::VALUE_OPTIONAL, 'Dump docs to directory'); } - $docs_dir = $input->getOption('dump'); - $this->localMachineHelper->getFilesystem()->mkdir($docs_dir); - $buffer = new BufferedOutput(); - $helper->describe($buffer, $this->getApplication(), [ - 'format' => 'json', - ]); - $commands = json_decode($buffer->fetch(), TRUE); - $index = []; - foreach ($commands['commands'] as $command) { - if ($command['definition']['hidden'] ?? FALSE) { - continue; - } - $filename = $command['name'] . '.json'; - $index[] = [ - 'command' => $command['name'], - 'help' => $command['help'], - 'path' => $filename, - 'usage' => $command['usage'][0], - ]; - file_put_contents("$docs_dir/$filename", json_encode($command)); + protected function execute(InputInterface $input, OutputInterface $output): int + { + $helper = new DescriptorHelper(); + + if (!$input->getOption('dump')) { + $helper->describe($output, $this->getApplication(), [ + 'format' => $input->getOption('format'), + ]); + return Command::SUCCESS; + } + + $docs_dir = $input->getOption('dump'); + $this->localMachineHelper->getFilesystem()->mkdir($docs_dir); + $buffer = new BufferedOutput(); + $helper->describe($buffer, $this->getApplication(), [ + 'format' => 'json', + ]); + $commands = json_decode($buffer->fetch(), true); + $index = []; + foreach ($commands['commands'] as $command) { + if ($command['definition']['hidden'] ?? false) { + continue; + } + $filename = $command['name'] . '.json'; + $index[] = [ + 'command' => $command['name'], + 'help' => $command['help'], + 'path' => $filename, + 'usage' => $command['usage'][0], + ]; + file_put_contents("$docs_dir/$filename", json_encode($command)); + } + file_put_contents("$docs_dir/index.json", json_encode($index)); + return Command::SUCCESS; } - file_put_contents("$docs_dir/index.json", json_encode($index)); - return Command::SUCCESS; - } - } diff --git a/src/Command/Self/TelemetryCommand.php b/src/Command/Self/TelemetryCommand.php index 6ebc29aa2..2e6c71b99 100644 --- a/src/Command/Self/TelemetryCommand.php +++ b/src/Command/Self/TelemetryCommand.php @@ -1,6 +1,6 @@ datastoreCloud; + if ($datastore->get(DataStoreContract::SEND_TELEMETRY)) { + $datastore->set(DataStoreContract::SEND_TELEMETRY, false); + $this->io->success('Telemetry has been disabled.'); + } else { + $datastore->set(DataStoreContract::SEND_TELEMETRY, true); + $this->io->success('Telemetry has been enabled.'); + } + $oppositeVerb = $datastore->get(DataStoreContract::SEND_TELEMETRY) ? 'disable' : 'enable'; + $this->io->writeln("Run this command again to $oppositeVerb telemetry"); - protected function execute(InputInterface $input, OutputInterface $output): int { - $datastore = $this->datastoreCloud; - if ($datastore->get(DataStoreContract::SEND_TELEMETRY)) { - $datastore->set(DataStoreContract::SEND_TELEMETRY, FALSE); - $this->io->success('Telemetry has been disabled.'); + return Command::SUCCESS; } - else { - $datastore->set(DataStoreContract::SEND_TELEMETRY, TRUE); - $this->io->success('Telemetry has been enabled.'); - } - $oppositeVerb = $datastore->get(DataStoreContract::SEND_TELEMETRY) ? 'disable' : 'enable'; - $this->io->writeln("Run this command again to $oppositeVerb telemetry"); - - return Command::SUCCESS; - } - } diff --git a/src/Command/Self/TelemetryDisableCommand.php b/src/Command/Self/TelemetryDisableCommand.php index 2c757f031..91dc83c84 100644 --- a/src/Command/Self/TelemetryDisableCommand.php +++ b/src/Command/Self/TelemetryDisableCommand.php @@ -1,6 +1,6 @@ datastoreCloud; - $datastore->set(DataStoreContract::SEND_TELEMETRY, FALSE); - $this->io->success('Telemetry has been disabled.'); - - return Command::SUCCESS; - } +final class TelemetryDisableCommand extends CommandBase +{ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $datastore = $this->datastoreCloud; + $datastore->set(DataStoreContract::SEND_TELEMETRY, false); + $this->io->success('Telemetry has been disabled.'); + return Command::SUCCESS; + } } diff --git a/src/Command/Self/TelemetryEnableCommand.php b/src/Command/Self/TelemetryEnableCommand.php index aec834d8e..cd9c9ec7f 100644 --- a/src/Command/Self/TelemetryEnableCommand.php +++ b/src/Command/Self/TelemetryEnableCommand.php @@ -1,6 +1,6 @@ datastoreCloud; - $datastore->set(DataStoreContract::SEND_TELEMETRY, TRUE); - $this->io->success('Telemetry has been enabled.'); - - return Command::SUCCESS; - } +final class TelemetryEnableCommand extends CommandBase +{ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $datastore = $this->datastoreCloud; + $datastore->set(DataStoreContract::SEND_TELEMETRY, true); + $this->io->success('Telemetry has been enabled.'); + return Command::SUCCESS; + } } diff --git a/src/Command/Ssh/SshKeyCommandBase.php b/src/Command/Ssh/SshKeyCommandBase.php index 8620f56fd..656ca01b9 100644 --- a/src/Command/Ssh/SshKeyCommandBase.php +++ b/src/Command/Ssh/SshKeyCommandBase.php @@ -1,6 +1,6 @@ privateSshKeyFilename = $privateSshKeyFilename; + $this->privateSshKeyFilepath = $this->sshDir . '/' . $this->privateSshKeyFilename; + $this->publicSshKeyFilepath = $this->privateSshKeyFilepath . '.pub'; + } - protected function setSshKeyFilepath(string $privateSshKeyFilename): void { - $this->privateSshKeyFilename = $privateSshKeyFilename; - $this->privateSshKeyFilepath = $this->sshDir . '/' . $this->privateSshKeyFilename; - $this->publicSshKeyFilepath = $this->privateSshKeyFilepath . '.pub'; - } + protected static function getIdeSshKeyLabel(string $ideLabel, string $ideUuid): string + { + return self::normalizeSshKeyLabel('IDE_' . $ideLabel . '_' . $ideUuid); + } - protected static function getIdeSshKeyLabel(string $ideLabel, string $ideUuid): string { - return self::normalizeSshKeyLabel('IDE_' . $ideLabel . '_' . $ideUuid); - } + private static function normalizeSshKeyLabel(?string $label): string|null + { + if (is_null($label)) { + throw new RuntimeException('The label cannot be empty'); + } + // It may only contain letters, numbers and underscores. + return preg_replace('/\W/', '', $label); + } - private static function normalizeSshKeyLabel(?string $label): string|null { - if (is_null($label)) { - throw new RuntimeException('The label cannot be empty'); + /** + * Normalizes public SSH key by trimming and removing user and machine suffix. + */ + protected function normalizePublicSshKey(string $publicKey): string + { + $parts = explode('== ', $publicKey); + $key = $parts[0]; + + return trim($key); } - // It may only contain letters, numbers and underscores. - return preg_replace('/\W/', '', $label); - } - - /** - * Normalizes public SSH key by trimming and removing user and machine suffix. - */ - protected function normalizePublicSshKey(string $publicKey): string { - $parts = explode('== ', $publicKey); - $key = $parts[0]; - - return trim($key); - } - - /** - * Asserts whether ANY SSH key has been added to the local keychain. - */ - protected function sshKeyIsAddedToKeychain(): bool { - $process = $this->localMachineHelper->execute([ - 'ssh-add', - '-L', - ], NULL, NULL, FALSE); - - if ($process->isSuccessful()) { - $keyContents = $this->normalizePublicSshKey($this->localMachineHelper->readFile($this->publicSshKeyFilepath)); - return str_contains($process->getOutput(), $keyContents); + + /** + * Asserts whether ANY SSH key has been added to the local keychain. + */ + protected function sshKeyIsAddedToKeychain(): bool + { + $process = $this->localMachineHelper->execute([ + 'ssh-add', + '-L', + ], null, null, false); + + if ($process->isSuccessful()) { + $keyContents = $this->normalizePublicSshKey($this->localMachineHelper->readFile($this->publicSshKeyFilepath)); + return str_contains($process->getOutput(), $keyContents); + } + return false; } - return FALSE; - } - - /** - * Adds a given password protected local SSH key to the local keychain. - * - * @param string $filepath The filepath of the private SSH key. - */ - protected function addSshKeyToAgent(string $filepath, string $password): void { - // We must use a separate script to mimic user input due to the limitations of the `ssh-add` command. - // @see https://www.linux.com/topic/networking/manage-ssh-key-file-passphrase/ - $tempFilepath = $this->localMachineHelper->getFilesystem()->tempnam(sys_get_temp_dir(), 'acli'); - $this->localMachineHelper->writeFile($tempFilepath, <<<'EOT' + + /** + * Adds a given password protected local SSH key to the local keychain. + * + * @param string $filepath The filepath of the private SSH key. + */ + protected function addSshKeyToAgent(string $filepath, string $password): void + { + // We must use a separate script to mimic user input due to the limitations of the `ssh-add` command. + // @see https://www.linux.com/topic/networking/manage-ssh-key-file-passphrase/ + $tempFilepath = $this->localMachineHelper->getFilesystem()->tempnam(sys_get_temp_dir(), 'acli'); + $this->localMachineHelper->writeFile($tempFilepath, <<<'EOT' #!/usr/bin/env bash echo $SSH_PASS EOT - ); - $this->localMachineHelper->getFilesystem()->chmod($tempFilepath, 0755); - $privateKeyFilepath = str_replace('.pub', '', $filepath); - $process = $this->localMachineHelper->executeFromCmd('SSH_PASS=' . $password . ' DISPLAY=1 SSH_ASKPASS=' . $tempFilepath . ' ssh-add ' . $privateKeyFilepath, NULL, NULL, FALSE); - $this->localMachineHelper->getFilesystem()->remove($tempFilepath); - if (!$process->isSuccessful()) { - throw new AcquiaCliException('Unable to add the SSH key to local SSH agent:' . $process->getOutput() . $process->getErrorOutput()); - } - } - - /** - * Polls the Cloud Platform until a successful SSH request is made to the dev - * environment. - * - * @infection-ignore-all - */ - protected function pollAcquiaCloudUntilSshSuccess( - OutputInterface $output - ): void { - // Create a loop to periodically poll the Cloud Platform. - $timers = []; - $startTime = time(); - $cloudAppUuid = $this->determineCloudApplication(TRUE); - $permissions = $this->cloudApiClientService->getClient()->request('get', "/applications/$cloudAppUuid/permissions"); - $perms = array_column($permissions, 'name'); - $mappings = $this->checkPermissions($perms, $cloudAppUuid, $output); - foreach ($mappings as $envName => $config) { - $spinner = new Spinner($output, 4); - $spinner->setMessage("Waiting for the key to become available in Cloud Platform $envName environments"); - $spinner->start(); - $mappings[$envName]['timer'] = Loop::addPeriodicTimer($spinner->interval(), - static function () use ($spinner): void { - $spinner->advance(); - }); - $mappings[$envName]['spinner'] = $spinner; - } - $callback = function () use ($output, &$mappings, &$timers, $startTime): void { - foreach ($mappings as $envName => $config) { - try { - $process = $this->sshHelper->executeCommand($config['ssh_target'], ['ls'], FALSE); - if (($process->getExitCode() === 128 && $envName === 'git') || $process->isSuccessful()) { - // SSH key is available on this host, but may be pending on others. - $config['spinner']->finish(); - Loop::cancelTimer($config['timer']); - unset($mappings[$envName]); - } - else { - // SSH key isn't available on this host... yet. - $this->logger->debug($process->getOutput() . $process->getErrorOutput()); - } - } - catch (AcquiaCliException $exception) { - $this->logger->debug($exception->getMessage()); - } - } - if (empty($mappings)) { - // SSH key is available on every host. - Amplitude::getInstance()->queueEvent('SSH key upload', ['result' => 'success', 'duration' => time() - $startTime]); - $output->writeln("\nYour SSH key is ready for use!\n"); - foreach ($timers as $timer) { - Loop::cancelTimer($timer); + ); + $this->localMachineHelper->getFilesystem()->chmod($tempFilepath, 0755); + $privateKeyFilepath = str_replace('.pub', '', $filepath); + $process = $this->localMachineHelper->executeFromCmd('SSH_PASS=' . $password . ' DISPLAY=1 SSH_ASKPASS=' . $tempFilepath . ' ssh-add ' . $privateKeyFilepath, null, null, false); + $this->localMachineHelper->getFilesystem()->remove($tempFilepath); + if (!$process->isSuccessful()) { + throw new AcquiaCliException('Unable to add the SSH key to local SSH agent:' . $process->getOutput() . $process->getErrorOutput()); } + } + + /** + * Polls the Cloud Platform until a successful SSH request is made to the dev + * environment. + * + * @infection-ignore-all + */ + protected function pollAcquiaCloudUntilSshSuccess( + OutputInterface $output + ): void { + // Create a loop to periodically poll the Cloud Platform. $timers = []; - } - }; - // Poll Cloud every 5 seconds. - $timers[] = Loop::addPeriodicTimer(5, $callback); - $timers[] = Loop::addTimer(0.1, $callback); - $timers[] = Loop::addTimer(60 * 60, static function () use ($output, &$timers): void { - // Upload timed out. - $output->writeln("\nThis is taking longer than usual. It will happen eventually!\n"); - Amplitude::getInstance()->queueEvent('SSH key upload', ['result' => 'timeout']); - foreach ($timers as $timer) { - Loop::cancelTimer($timer); - } - $timers = []; - }); - Loop::run(); - } - - /** - * @return array - */ - private function checkPermissions(array $userPerms, string $cloudAppUuid, OutputInterface $output): array { - $mappings = []; - $requiredPerms = ['add ssh key to git', 'add ssh key to non-prod', 'add ssh key to prod']; - foreach ($requiredPerms as $index => $requiredPerm) { - if (in_array($requiredPerm, $userPerms, TRUE)) { - switch ($requiredPerm) { - case 'add ssh key to git': - $fullUrl = $this->getAnyVcsUrl($cloudAppUuid); - $urlParts = explode(':', $fullUrl); - $mappings['git']['ssh_target'] = $urlParts[0]; - break; - case 'add ssh key to non-prod': - if ($nonProdEnv = $this->getAnyNonProdAhEnvironment($cloudAppUuid)) { - $mappings['nonprod']['ssh_target'] = $nonProdEnv->sshUrl; + $startTime = time(); + $cloudAppUuid = $this->determineCloudApplication(true); + $permissions = $this->cloudApiClientService->getClient()->request('get', "/applications/$cloudAppUuid/permissions"); + $perms = array_column($permissions, 'name'); + $mappings = $this->checkPermissions($perms, $cloudAppUuid, $output); + foreach ($mappings as $envName => $config) { + $spinner = new Spinner($output, 4); + $spinner->setMessage("Waiting for the key to become available in Cloud Platform $envName environments"); + $spinner->start(); + $mappings[$envName]['timer'] = Loop::addPeriodicTimer( + $spinner->interval(), + static function () use ($spinner): void { + $spinner->advance(); + } + ); + $mappings[$envName]['spinner'] = $spinner; + } + $callback = function () use ($output, &$mappings, &$timers, $startTime): void { + foreach ($mappings as $envName => $config) { + try { + $process = $this->sshHelper->executeCommand($config['ssh_target'], ['ls'], false); + if (($process->getExitCode() === 128 && $envName === 'git') || $process->isSuccessful()) { + // SSH key is available on this host, but may be pending on others. + $config['spinner']->finish(); + Loop::cancelTimer($config['timer']); + unset($mappings[$envName]); + } else { + // SSH key isn't available on this host... yet. + $this->logger->debug($process->getOutput() . $process->getErrorOutput()); + } + } catch (AcquiaCliException $exception) { + $this->logger->debug($exception->getMessage()); + } } - break; - case 'add ssh key to prod': - if ($prodEnv = $this->getAnyProdAhEnvironment($cloudAppUuid)) { - $mappings['prod']['ssh_target'] = $prodEnv->sshUrl; + if (empty($mappings)) { + // SSH key is available on every host. + Amplitude::getInstance()->queueEvent('SSH key upload', ['result' => 'success', 'duration' => time() - $startTime]); + $output->writeln("\nYour SSH key is ready for use!\n"); + foreach ($timers as $timer) { + Loop::cancelTimer($timer); + } + $timers = []; } - break; - } - unset($requiredPerms[$index]); - } + }; + // Poll Cloud every 5 seconds. + $timers[] = Loop::addPeriodicTimer(5, $callback); + $timers[] = Loop::addTimer(0.1, $callback); + $timers[] = Loop::addTimer(60 * 60, static function () use ($output, &$timers): void { + // Upload timed out. + $output->writeln("\nThis is taking longer than usual. It will happen eventually!\n"); + Amplitude::getInstance()->queueEvent('SSH key upload', ['result' => 'timeout']); + foreach ($timers as $timer) { + Loop::cancelTimer($timer); + } + $timers = []; + }); + Loop::run(); } - if (!empty($requiredPerms)) { - $permString = implode(", ", $requiredPerms); - $output->writeln('You do not have access to some environments on this application.'); - $output->writeln("Check that you have the following permissions: $permString"); + + /** + * @return array + */ + private function checkPermissions(array $userPerms, string $cloudAppUuid, OutputInterface $output): array + { + $mappings = []; + $requiredPerms = ['add ssh key to git', 'add ssh key to non-prod', 'add ssh key to prod']; + foreach ($requiredPerms as $index => $requiredPerm) { + if (in_array($requiredPerm, $userPerms, true)) { + switch ($requiredPerm) { + case 'add ssh key to git': + $fullUrl = $this->getAnyVcsUrl($cloudAppUuid); + $urlParts = explode(':', $fullUrl); + $mappings['git']['ssh_target'] = $urlParts[0]; + break; + case 'add ssh key to non-prod': + if ($nonProdEnv = $this->getAnyNonProdAhEnvironment($cloudAppUuid)) { + $mappings['nonprod']['ssh_target'] = $nonProdEnv->sshUrl; + } + break; + case 'add ssh key to prod': + if ($prodEnv = $this->getAnyProdAhEnvironment($cloudAppUuid)) { + $mappings['prod']['ssh_target'] = $prodEnv->sshUrl; + } + break; + } + unset($requiredPerms[$index]); + } + } + if (!empty($requiredPerms)) { + $permString = implode(", ", $requiredPerms); + $output->writeln('You do not have access to some environments on this application.'); + $output->writeln("Check that you have the following permissions: $permString"); + } + return $mappings; } - return $mappings; - } - - protected function createSshKey(string $filename, string $password): string { - $keyFilePath = $this->doCreateSshKey($filename, $password); - $this->setSshKeyFilepath(basename($keyFilePath)); - if (!$this->sshKeyIsAddedToKeychain()) { - $this->addSshKeyToAgent($this->publicSshKeyFilepath, $password); + + protected function createSshKey(string $filename, string $password): string + { + $keyFilePath = $this->doCreateSshKey($filename, $password); + $this->setSshKeyFilepath(basename($keyFilePath)); + if (!$this->sshKeyIsAddedToKeychain()) { + $this->addSshKeyToAgent($this->publicSshKeyFilepath, $password); + } + return $keyFilePath; } - return $keyFilePath; - } - private function doCreateSshKey(string $filename, string $password): string { - $filepath = $this->sshDir . '/' . $filename; - if (file_exists($filepath)) { - throw new AcquiaCliException('An SSH key with the filename {filepath} already exists. Delete it and retry', ['filepath' => $filename]); + private function doCreateSshKey(string $filename, string $password): string + { + $filepath = $this->sshDir . '/' . $filename; + if (file_exists($filepath)) { + throw new AcquiaCliException('An SSH key with the filename {filepath} already exists. Delete it and retry', ['filepath' => $filename]); + } + + $this->localMachineHelper->checkRequiredBinariesExist(['ssh-keygen']); + $process = $this->localMachineHelper->execute([ + 'ssh-keygen', + '-t', + 'rsa', + '-b', + '4096', + '-f', + $filepath, + '-N', + $password, + ], null, null, false); + if (!$process->isSuccessful()) { + throw new AcquiaCliException($process->getOutput() . $process->getErrorOutput()); + } + + return $filepath; } - $this->localMachineHelper->checkRequiredBinariesExist(['ssh-keygen']); - $process = $this->localMachineHelper->execute([ - 'ssh-keygen', - '-t', - 'rsa', - '-b', - '4096', - '-f', - $filepath, - '-N', - $password, - ], NULL, NULL, FALSE); - if (!$process->isSuccessful()) { - throw new AcquiaCliException($process->getOutput() . $process->getErrorOutput()); + protected function determineFilename(): string + { + return $this->determineOption( + 'filename', + false, + $this->validateFilename(...), + static function (mixed $value) { + return $value ? trim($value) : ''; + }, + 'id_rsa_acquia' + ); } - return $filepath; - } - - protected function determineFilename(): string { - return $this->determineOption( - 'filename', - FALSE, - $this->validateFilename(...), - static function (mixed $value) { - return $value ? trim($value) : '';}, - 'id_rsa_acquia' - ); - } - - private function validateFilename(string $filename): string { - $violations = Validation::createValidator()->validate($filename, [ - new Length(['min' => 5]), - new NotBlank(), - new Regex(['pattern' => '/^\S*$/', 'message' => 'The value may not contain spaces']), - ]); - if (count($violations)) { - throw new ValidatorException($violations->get(0)->getMessage()); + private function validateFilename(string $filename): string + { + $violations = Validation::createValidator()->validate($filename, [ + new Length(['min' => 5]), + new NotBlank(), + new Regex(['pattern' => '/^\S*$/', 'message' => 'The value may not contain spaces']), + ]); + if (count($violations)) { + throw new ValidatorException($violations->get(0)->getMessage()); + } + + return $filename; } - return $filename; - } - - protected function determinePassword(): string { - return $this->determineOption( - 'password', - TRUE, - $this->validatePassword(...), - static function (mixed $value) { - return $value ? trim($value) : ''; - } - ); - } - - private function validatePassword(string $password): string { - $violations = Validation::createValidator()->validate($password, [ - new Length(['min' => 5]), - new NotBlank(), - ]); - if (count($violations)) { - throw new ValidatorException($violations->get(0)->getMessage()); + protected function determinePassword(): string + { + return $this->determineOption( + 'password', + true, + $this->validatePassword(...), + static function (mixed $value) { + return $value ? trim($value) : ''; + } + ); } - return $password; - } + private function validatePassword(string $password): string + { + $violations = Validation::createValidator()->validate($password, [ + new Length(['min' => 5]), + new NotBlank(), + ]); + if (count($violations)) { + throw new ValidatorException($violations->get(0)->getMessage()); + } - private function keyHasUploaded(Client $acquiaCloudClient, string $publicKey): bool { - $sshKeys = new SshKeys($acquiaCloudClient); - foreach ($sshKeys->getAll() as $cloudKey) { - if (trim($cloudKey->public_key) === trim($publicKey)) { - return TRUE; - } + return $password; } - return FALSE; - } - - /** - * @return array - */ - protected function determinePublicSshKey(string $filepath = NULL): array { - if ($filepath) { - $filepath = $this->localMachineHelper->getLocalFilepath($filepath); + + private function keyHasUploaded(Client $acquiaCloudClient, string $publicKey): bool + { + $sshKeys = new SshKeys($acquiaCloudClient); + foreach ($sshKeys->getAll() as $cloudKey) { + if (trim($cloudKey->public_key) === trim($publicKey)) { + return true; + } + } + return false; } - elseif ($this->input->hasOption('filepath') && $this->input->getOption('filepath')) { - $filepath = $this->localMachineHelper->getLocalFilepath($this->input->getOption('filepath')); + + /** + * @return array + */ + protected function determinePublicSshKey(string $filepath = null): array + { + if ($filepath) { + $filepath = $this->localMachineHelper->getLocalFilepath($filepath); + } elseif ($this->input->hasOption('filepath') && $this->input->getOption('filepath')) { + $filepath = $this->localMachineHelper->getLocalFilepath($this->input->getOption('filepath')); + } + + if ($filepath) { + if (!$this->localMachineHelper->getFilesystem()->exists($filepath)) { + throw new AcquiaCliException('The filepath {filepath} is not valid', ['filepath' => $filepath]); + } + if (!str_contains($filepath, '.pub')) { + throw new AcquiaCliException('The filepath {filepath} does not have the .pub extension', ['filepath' => $filepath]); + } + $publicKey = $this->localMachineHelper->readFile($filepath); + $chosenLocalKey = basename($filepath); + } else { + // Get local key and contents. + $localKeys = $this->findLocalSshKeys(); + $chosenLocalKey = $this->promptChooseLocalSshKey($localKeys); + $publicKey = $this->getLocalSshKeyContents($localKeys, $chosenLocalKey); + } + + return [$chosenLocalKey, $publicKey]; } - if ($filepath) { - if (!$this->localMachineHelper->getFilesystem()->exists($filepath)) { - throw new AcquiaCliException('The filepath {filepath} is not valid', ['filepath' => $filepath]); - } - if (!str_contains($filepath, '.pub')) { - throw new AcquiaCliException('The filepath {filepath} does not have the .pub extension', ['filepath' => $filepath]); - } - $publicKey = $this->localMachineHelper->readFile($filepath); - $chosenLocalKey = basename($filepath); + private function promptChooseLocalSshKey(array $localKeys): string + { + $labels = []; + foreach ($localKeys as $localKey) { + $labels[] = $localKey->getFilename(); + } + $question = new ChoiceQuestion( + 'Choose a local SSH key to upload to the Cloud Platform', + $labels + ); + return $this->io->askQuestion($question); } - else { - // Get local key and contents. - $localKeys = $this->findLocalSshKeys(); - $chosenLocalKey = $this->promptChooseLocalSshKey($localKeys); - $publicKey = $this->getLocalSshKeyContents($localKeys, $chosenLocalKey); + + protected function determineSshKeyLabel(): string + { + return $this->determineOption('label', false, $this->validateSshKeyLabel(...), $this->normalizeSshKeyLabel(...)); } - return [$chosenLocalKey, $publicKey]; - } + private function validateSshKeyLabel(mixed $label): mixed + { + if (trim($label) === '') { + throw new RuntimeException('The label cannot be empty'); + } - private function promptChooseLocalSshKey(array $localKeys): string { - $labels = []; - foreach ($localKeys as $localKey) { - $labels[] = $localKey->getFilename(); + return $label; } - $question = new ChoiceQuestion( - 'Choose a local SSH key to upload to the Cloud Platform', - $labels - ); - return $this->io->askQuestion($question); - } - - protected function determineSshKeyLabel(): string { - return $this->determineOption('label', FALSE, $this->validateSshKeyLabel(...), $this->normalizeSshKeyLabel(...)); - } - - private function validateSshKeyLabel(mixed $label): mixed { - if (trim($label) === '') { - throw new RuntimeException('The label cannot be empty'); + + private function getLocalSshKeyContents(array $localKeys, string $chosenLocalKey): string + { + $filepath = ''; + foreach ($localKeys as $localKey) { + if ($localKey->getFilename() === $chosenLocalKey) { + $filepath = $localKey->getRealPath(); + break; + } + } + return $this->localMachineHelper->readFile($filepath); } - return $label; - } + protected function uploadSshKey(string $label, string $publicKey): void + { + // @todo If a key with this label already exists, let the user try again. + $sshKeys = new SshKeys($this->cloudApiClientService->getClient()); + $sshKeys->create($label, $publicKey); + + // Wait for the key to register on the Cloud Platform. + if ($this->input->hasOption('no-wait') && $this->input->getOption('no-wait') === false) { + if ($this->input->isInteractive() && !$this->promptWaitForSsh($this->io)) { + $this->io->success('Your SSH key has been successfully uploaded to the Cloud Platform.'); + return; + } - private function getLocalSshKeyContents(array $localKeys, string $chosenLocalKey): string { - $filepath = ''; - foreach ($localKeys as $localKey) { - if ($localKey->getFilename() === $chosenLocalKey) { - $filepath = $localKey->getRealPath(); - break; - } - } - return $this->localMachineHelper->readFile($filepath); - } - - protected function uploadSshKey(string $label, string $publicKey): void { - // @todo If a key with this label already exists, let the user try again. - $sshKeys = new SshKeys($this->cloudApiClientService->getClient()); - $sshKeys->create($label, $publicKey); - - // Wait for the key to register on the Cloud Platform. - if ($this->input->hasOption('no-wait') && $this->input->getOption('no-wait') === FALSE) { - if ($this->input->isInteractive() && !$this->promptWaitForSsh($this->io)) { - $this->io->success('Your SSH key has been successfully uploaded to the Cloud Platform.'); - return; - } - - if ($this->keyHasUploaded($this->cloudApiClientService->getClient(), $publicKey)) { - $this->pollAcquiaCloudUntilSshSuccess($this->output); - } + if ($this->keyHasUploaded($this->cloudApiClientService->getClient(), $publicKey)) { + $this->pollAcquiaCloudUntilSshSuccess($this->output); + } + } } - } - protected static function getFingerprint(mixed $sshPublicKey): string { - if (!str_starts_with($sshPublicKey, 'ssh-rsa ')) { - throw new AcquiaCliException('SSH keys must start with "ssh-rsa ".'); + protected static function getFingerprint(mixed $sshPublicKey): string + { + if (!str_starts_with($sshPublicKey, 'ssh-rsa ')) { + throw new AcquiaCliException('SSH keys must start with "ssh-rsa ".'); + } + $content = explode(' ', $sshPublicKey, 3); + return base64_encode(hash('sha256', base64_decode($content[1]), true)); } - $content = explode(' ', $sshPublicKey, 3); - return base64_encode(hash('sha256', base64_decode($content[1]), TRUE)); - } - } diff --git a/src/Command/Ssh/SshKeyCreateCommand.php b/src/Command/Ssh/SshKeyCreateCommand.php index 8b8892d91..0c88737a4 100644 --- a/src/Command/Ssh/SshKeyCreateCommand.php +++ b/src/Command/Ssh/SshKeyCreateCommand.php @@ -1,6 +1,6 @@ addOption('filename', NULL, InputOption::VALUE_REQUIRED, 'The filename of the SSH key') - ->addOption('password', NULL, InputOption::VALUE_REQUIRED, 'The password for the SSH key'); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $filename = $this->determineFilename(); - $password = $this->determinePassword(); - $this->createSshKey($filename, $password); - $output->writeln('Created new SSH key. ' . $this->publicSshKeyFilepath); - - return Command::SUCCESS; - } - +final class SshKeyCreateCommand extends SshKeyCommandBase +{ + protected function configure(): void + { + $this + ->addOption('filename', null, InputOption::VALUE_REQUIRED, 'The filename of the SSH key') + ->addOption('password', null, InputOption::VALUE_REQUIRED, 'The password for the SSH key'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $filename = $this->determineFilename(); + $password = $this->determinePassword(); + $this->createSshKey($filename, $password); + $output->writeln('Created new SSH key. ' . $this->publicSshKeyFilepath); + + return Command::SUCCESS; + } } diff --git a/src/Command/Ssh/SshKeyCreateUploadCommand.php b/src/Command/Ssh/SshKeyCreateUploadCommand.php index f49b2c4a5..521f6f362 100644 --- a/src/Command/Ssh/SshKeyCreateUploadCommand.php +++ b/src/Command/Ssh/SshKeyCreateUploadCommand.php @@ -1,6 +1,6 @@ addOption('filename', NULL, InputOption::VALUE_REQUIRED, 'The filename of the SSH key') - ->addOption('password', NULL, InputOption::VALUE_REQUIRED, 'The password for the SSH key') - ->addOption('label', NULL, InputOption::VALUE_REQUIRED, 'The SSH key label to be used with the Cloud Platform') - ->addOption('no-wait', NULL, InputOption::VALUE_NONE, "Don't wait for the SSH key to be uploaded to the Cloud Platform"); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $filename = $this->determineFilename(); - $password = $this->determinePassword(); - $this->createSshKey($filename, $password); - $publicKey = $this->localMachineHelper->readFile($this->publicSshKeyFilepath); - $chosenLocalKey = basename($this->privateSshKeyFilepath); - $label = $this->determineSshKeyLabel(); - $this->uploadSshKey($label, $publicKey); - $this->io->success("Uploaded $chosenLocalKey to the Cloud Platform with label $label"); - - return Command::SUCCESS; - } - +final class SshKeyCreateUploadCommand extends SshKeyCommandBase +{ + protected function configure(): void + { + $this + ->addOption('filename', null, InputOption::VALUE_REQUIRED, 'The filename of the SSH key') + ->addOption('password', null, InputOption::VALUE_REQUIRED, 'The password for the SSH key') + ->addOption('label', null, InputOption::VALUE_REQUIRED, 'The SSH key label to be used with the Cloud Platform') + ->addOption('no-wait', null, InputOption::VALUE_NONE, "Don't wait for the SSH key to be uploaded to the Cloud Platform"); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $filename = $this->determineFilename(); + $password = $this->determinePassword(); + $this->createSshKey($filename, $password); + $publicKey = $this->localMachineHelper->readFile($this->publicSshKeyFilepath); + $chosenLocalKey = basename($this->privateSshKeyFilepath); + $label = $this->determineSshKeyLabel(); + $this->uploadSshKey($label, $publicKey); + $this->io->success("Uploaded $chosenLocalKey to the Cloud Platform with label $label"); + + return Command::SUCCESS; + } } diff --git a/src/Command/Ssh/SshKeyDeleteCommand.php b/src/Command/Ssh/SshKeyDeleteCommand.php index 556a7dc94..47012ef3b 100644 --- a/src/Command/Ssh/SshKeyDeleteCommand.php +++ b/src/Command/Ssh/SshKeyDeleteCommand.php @@ -1,6 +1,6 @@ addOption('cloud-key-uuid', 'uuid', InputOption::VALUE_REQUIRED); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - return $this->deleteSshKeyFromCloud($output); - } - +final class SshKeyDeleteCommand extends SshKeyCommandBase +{ + use SshCommandTrait; + + protected function configure(): void + { + $this + ->addOption('cloud-key-uuid', 'uuid', InputOption::VALUE_REQUIRED); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + return $this->deleteSshKeyFromCloud($output); + } } diff --git a/src/Command/Ssh/SshKeyInfoCommand.php b/src/Command/Ssh/SshKeyInfoCommand.php index 06bf953f4..1c5a1f1a2 100644 --- a/src/Command/Ssh/SshKeyInfoCommand.php +++ b/src/Command/Ssh/SshKeyInfoCommand.php @@ -1,6 +1,6 @@ addOption('fingerprint', NULL, InputOption::VALUE_REQUIRED, 'sha256 fingerprint') - ->addUsage('--fingerprint=pyarUa1mt2ln4fmrp7alWKpv1IPneqFwE+ErTC71IvY='); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $acquiaCloudClient = $this->cloudApiClientService->getClient(); - $key = $this->determineSshKey($acquiaCloudClient); - - $location = 'Local'; - if (array_key_exists('cloud', $key)) { - $location = array_key_exists('local', $key) ? 'Local + Cloud' : 'Cloud'; +final class SshKeyInfoCommand extends SshKeyCommandBase +{ + protected function configure(): void + { + $this + ->addOption('fingerprint', null, InputOption::VALUE_REQUIRED, 'sha256 fingerprint') + ->addUsage('--fingerprint=pyarUa1mt2ln4fmrp7alWKpv1IPneqFwE+ErTC71IvY='); } - $this->io->definitionList( - ['SSH key property' => 'SSH key value'], - new TableSeparator(), - ['Location' => $location], - ['Fingerprint (sha256)' => $key['fingerprint']], - ['Fingerprint (md5)' => array_key_exists('cloud', $key) ? $key['cloud']['fingerprint'] : 'n/a'], - ['UUID' => array_key_exists('cloud', $key) ? $key['cloud']['uuid'] : 'n/a'], - ['Label' => array_key_exists('cloud', $key) ? $key['cloud']['label'] : $key['local']['filename']], - ['Created at' => array_key_exists('cloud', $key) ? $key['cloud']['created_at'] : 'n/a'], - ); - $this->io->writeln('Public key'); - $this->io->writeln('----------'); - $this->io->writeln($key['public_key']); + protected function execute(InputInterface $input, OutputInterface $output): int + { + $acquiaCloudClient = $this->cloudApiClientService->getClient(); + $key = $this->determineSshKey($acquiaCloudClient); - return Command::SUCCESS; - } + $location = 'Local'; + if (array_key_exists('cloud', $key)) { + $location = array_key_exists('local', $key) ? 'Local + Cloud' : 'Cloud'; + } + $this->io->definitionList( + ['SSH key property' => 'SSH key value'], + new TableSeparator(), + ['Location' => $location], + ['Fingerprint (sha256)' => $key['fingerprint']], + ['Fingerprint (md5)' => array_key_exists('cloud', $key) ? $key['cloud']['fingerprint'] : 'n/a'], + ['UUID' => array_key_exists('cloud', $key) ? $key['cloud']['uuid'] : 'n/a'], + ['Label' => array_key_exists('cloud', $key) ? $key['cloud']['label'] : $key['local']['filename']], + ['Created at' => array_key_exists('cloud', $key) ? $key['cloud']['created_at'] : 'n/a'], + ); - /** - * @return array - */ - private function determineSshKey(mixed $acquiaCloudClient): array { - $cloudKeysResponse = new SshKeys($acquiaCloudClient); - $cloudKeys = $cloudKeysResponse->getAll(); - $localKeys = $this->findLocalSshKeys(); - $keys = []; - /** @var \AcquiaCloudApi\Response\SshKeyResponse $key */ - foreach ($cloudKeys as $key) { - $fingerprint = self::getFingerprint($key->public_key); - $keys[$fingerprint]['fingerprint'] = $fingerprint; - $keys[$fingerprint]['public_key'] = $key->public_key; - $keys[$fingerprint]['cloud'] = [ - 'created_at' => $key->created_at, - 'fingerprint' => $key->fingerprint, - 'label' => $key->label, - 'uuid' => $key->uuid, - ]; - } - foreach ($localKeys as $key) { - $fingerprint = self::getFingerprint($key->getContents()); - $keys[$fingerprint]['fingerprint'] = $fingerprint; - $keys[$fingerprint]['public_key'] = $key->getContents(); - $keys[$fingerprint]['local'] = [ - 'filename' => $key->getFilename(), - ]; - } - if ($fingerprint = $this->input->getOption('fingerprint')) { - if (!array_key_exists($fingerprint, $keys)) { - throw new AcquiaCliException('No key exists matching provided fingerprint'); - } - return $keys[$fingerprint]; - } + $this->io->writeln('Public key'); + $this->io->writeln('----------'); + $this->io->writeln($key['public_key']); - return $this->promptChooseFromObjectsOrArrays( - $keys, - 'fingerprint', - 'fingerprint', - 'Choose an SSH key to view' - ); + return Command::SUCCESS; + } - } + /** + * @return array + */ + private function determineSshKey(mixed $acquiaCloudClient): array + { + $cloudKeysResponse = new SshKeys($acquiaCloudClient); + $cloudKeys = $cloudKeysResponse->getAll(); + $localKeys = $this->findLocalSshKeys(); + $keys = []; + /** @var \AcquiaCloudApi\Response\SshKeyResponse $key */ + foreach ($cloudKeys as $key) { + $fingerprint = self::getFingerprint($key->public_key); + $keys[$fingerprint]['fingerprint'] = $fingerprint; + $keys[$fingerprint]['public_key'] = $key->public_key; + $keys[$fingerprint]['cloud'] = [ + 'created_at' => $key->created_at, + 'fingerprint' => $key->fingerprint, + 'label' => $key->label, + 'uuid' => $key->uuid, + ]; + } + foreach ($localKeys as $key) { + $fingerprint = self::getFingerprint($key->getContents()); + $keys[$fingerprint]['fingerprint'] = $fingerprint; + $keys[$fingerprint]['public_key'] = $key->getContents(); + $keys[$fingerprint]['local'] = [ + 'filename' => $key->getFilename(), + ]; + } + if ($fingerprint = $this->input->getOption('fingerprint')) { + if (!array_key_exists($fingerprint, $keys)) { + throw new AcquiaCliException('No key exists matching provided fingerprint'); + } + return $keys[$fingerprint]; + } + return $this->promptChooseFromObjectsOrArrays( + $keys, + 'fingerprint', + 'fingerprint', + 'Choose an SSH key to view' + ); + } } diff --git a/src/Command/Ssh/SshKeyListCommand.php b/src/Command/Ssh/SshKeyListCommand.php index cdb165aa6..3f711a408 100644 --- a/src/Command/Ssh/SshKeyListCommand.php +++ b/src/Command/Ssh/SshKeyListCommand.php @@ -1,6 +1,6 @@ cloudApiClientService->getClient(); + $sshKeys = new SshKeys($acquiaCloudClient); + $cloudKeys = $sshKeys->getAll(); + $localKeys = $this->findLocalSshKeys(); + $table = $this->createSshKeyTable($output, 'Cloud Platform keys with matching local keys'); + foreach ($localKeys as $localIndex => $localFile) { + /** @var \AcquiaCloudApi\Response\SshKeyResponse $cloudKey */ + foreach ($cloudKeys as $index => $cloudKey) { + if (trim($localFile->getContents()) === trim($cloudKey->public_key)) { + $hash = self::getFingerprint($cloudKey->public_key); + $table->addRow([ + $cloudKey->label, + $localFile->getFilename(), + $hash, + ]); + unset($cloudKeys[$index], $localKeys[$localIndex]); + break; + } + } + } + $table->render(); + $this->io->newLine(); - protected function execute(InputInterface $input, OutputInterface $output): int { - $acquiaCloudClient = $this->cloudApiClientService->getClient(); - $sshKeys = new SshKeys($acquiaCloudClient); - $cloudKeys = $sshKeys->getAll(); - $localKeys = $this->findLocalSshKeys(); - $table = $this->createSshKeyTable($output, 'Cloud Platform keys with matching local keys'); - foreach ($localKeys as $localIndex => $localFile) { - /** @var \AcquiaCloudApi\Response\SshKeyResponse $cloudKey */ - foreach ($cloudKeys as $index => $cloudKey) { - if (trim($localFile->getContents()) === trim($cloudKey->public_key)) { - $hash = self::getFingerprint($cloudKey->public_key); - $table->addRow([ + $table = $this->createSshKeyTable($output, 'Cloud Platform keys with no matching local keys'); + foreach ($cloudKeys as $cloudKey) { + $hash = self::getFingerprint($cloudKey->public_key); + $table->addRow([ $cloudKey->label, + 'none', + $hash, + ]); + } + $table->render(); + $this->io->newLine(); + + $table = $this->createSshKeyTable($output, 'Local keys with no matching Cloud Platform keys'); + foreach ($localKeys as $localFile) { + $hash = self::getFingerprint($localFile->getContents()); + $table->addRow([ + 'none', $localFile->getFilename(), $hash, - ]); - unset($cloudKeys[$index], $localKeys[$localIndex]); - break; + ]); } - } - } - $table->render(); - $this->io->newLine(); + $table->render(); - $table = $this->createSshKeyTable($output, 'Cloud Platform keys with no matching local keys'); - foreach ($cloudKeys as $cloudKey) { - $hash = self::getFingerprint($cloudKey->public_key); - $table->addRow([ - $cloudKey->label, - 'none', - $hash, - ]); + return Command::SUCCESS; } - $table->render(); - $this->io->newLine(); - $table = $this->createSshKeyTable($output, 'Local keys with no matching Cloud Platform keys'); - foreach ($localKeys as $localFile) { - $hash = self::getFingerprint($localFile->getContents()); - $table->addRow([ - 'none', - $localFile->getFilename(), - $hash, - ]); + private function createSshKeyTable(OutputInterface $output, string $title): Table + { + $headers = ['Cloud Platform label', 'Local filename', 'Fingerprint (sha256)']; + $widths = [.4, .2, .2]; + return $this->createTable($output, $title, $headers, $widths); } - $table->render(); - - return Command::SUCCESS; - } - - private function createSshKeyTable(OutputInterface $output, string $title): Table { - $headers = ['Cloud Platform label', 'Local filename', 'Fingerprint (sha256)']; - $widths = [.4, .2, .2]; - return $this->createTable($output, $title, $headers, $widths); - } - } diff --git a/src/Command/Ssh/SshKeyUploadCommand.php b/src/Command/Ssh/SshKeyUploadCommand.php index cc55f90a7..191c5e9e2 100644 --- a/src/Command/Ssh/SshKeyUploadCommand.php +++ b/src/Command/Ssh/SshKeyUploadCommand.php @@ -1,6 +1,6 @@ addOption('filepath', NULL, InputOption::VALUE_REQUIRED, 'The filepath of the public SSH key to upload') - ->addOption('label', NULL, InputOption::VALUE_REQUIRED, 'The SSH key label to be used with the Cloud Platform') - ->addOption('no-wait', NULL, InputOption::VALUE_NONE, "Don't wait for the SSH key to be uploaded to the Cloud Platform"); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - [$chosenLocalKey, $publicKey] = $this->determinePublicSshKey(); - $label = $this->determineSshKeyLabel(); - $this->uploadSshKey($label, $publicKey); - $this->io->success("Uploaded $chosenLocalKey to the Cloud Platform with label $label"); - - return Command::SUCCESS; - } - +final class SshKeyUploadCommand extends SshKeyCommandBase +{ + protected function configure(): void + { + $this + ->addOption('filepath', null, InputOption::VALUE_REQUIRED, 'The filepath of the public SSH key to upload') + ->addOption('label', null, InputOption::VALUE_REQUIRED, 'The SSH key label to be used with the Cloud Platform') + ->addOption('no-wait', null, InputOption::VALUE_NONE, "Don't wait for the SSH key to be uploaded to the Cloud Platform"); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + [$chosenLocalKey, $publicKey] = $this->determinePublicSshKey(); + $label = $this->determineSshKeyLabel(); + $this->uploadSshKey($label, $publicKey); + $this->io->success("Uploaded $chosenLocalKey to the Cloud Platform with label $label"); + + return Command::SUCCESS; + } } diff --git a/src/Command/WizardCommandBase.php b/src/Command/WizardCommandBase.php index 25891b497..ccf773f16 100644 --- a/src/Command/WizardCommandBase.php +++ b/src/Command/WizardCommandBase.php @@ -1,6 +1,6 @@ getAttributes(RequireAuth::class) && !$this->cloudApiClientService->isMachineAuthenticated()) { + $commandName = 'auth:login'; + $command = $this->getApplication()->find($commandName); + $arguments = ['command' => $commandName]; + $createInput = new ArrayInput($arguments); + $exitCode = $command->run($createInput, $output); + if ($exitCode !== 0) { + throw new AcquiaCliException("Unable to authenticate with the Cloud Platform."); + } + } + $this->validateEnvironment(); - protected function initialize(InputInterface $input, OutputInterface $output): void { - if ((new \ReflectionClass(static::class))->getAttributes(RequireAuth::class) && !$this->cloudApiClientService->isMachineAuthenticated()) { - $commandName = 'auth:login'; - $command = $this->getApplication()->find($commandName); - $arguments = ['command' => $commandName]; - $createInput = new ArrayInput($arguments); - $exitCode = $command->run($createInput, $output); - if ($exitCode !== 0) { - throw new AcquiaCliException("Unable to authenticate with the Cloud Platform."); - } + parent::initialize($input, $output); } - $this->validateEnvironment(); - parent::initialize($input, $output); - } - - protected function deleteLocalSshKey(): void { - $this->localMachineHelper->getFilesystem()->remove([ - $this->publicSshKeyFilepath, - $this->privateSshKeyFilepath, - ]); - } - - protected function savePassPhraseToFile(string $passphrase): bool|int { - return file_put_contents($this->passphraseFilepath, $passphrase); - } + protected function deleteLocalSshKey(): void + { + $this->localMachineHelper->getFilesystem()->remove([ + $this->publicSshKeyFilepath, + $this->privateSshKeyFilepath, + ]); + } - protected function getPassPhraseFromFile(): string { - return file_get_contents($this->passphraseFilepath); - } + protected function savePassPhraseToFile(string $passphrase): bool|int + { + return file_put_contents($this->passphraseFilepath, $passphrase); + } - /** - * Assert whether ANY local key exists that has a corresponding key on the - * Cloud Platform. - */ - protected function userHasUploadedThisKeyToCloud(string $label): bool { - $acquiaCloudClient = $this->cloudApiClientService->getClient(); - $sshKeys = new SshKeys($acquiaCloudClient); - $cloudKeys = $sshKeys->getAll(); - /** @var \AcquiaCloudApi\Response\SshKeyResponse $cloudKey */ - foreach ($cloudKeys as $index => $cloudKey) { - if ( - $cloudKey->label === $label - // Assert that a corresponding local key exists. - && $this->localSshKeyExists() - // Assert local public key contents match Cloud public key contents. - && $this->normalizePublicSshKey($cloudKey->public_key) === $this->normalizePublicSshKey(file_get_contents($this->publicSshKeyFilepath)) - ) { - return TRUE; - } + protected function getPassPhraseFromFile(): string + { + return file_get_contents($this->passphraseFilepath); } - return FALSE; - } - protected function passPhraseFileExists(): bool { - return file_exists($this->passphraseFilepath); - } + /** + * Assert whether ANY local key exists that has a corresponding key on the + * Cloud Platform. + */ + protected function userHasUploadedThisKeyToCloud(string $label): bool + { + $acquiaCloudClient = $this->cloudApiClientService->getClient(); + $sshKeys = new SshKeys($acquiaCloudClient); + $cloudKeys = $sshKeys->getAll(); + /** @var \AcquiaCloudApi\Response\SshKeyResponse $cloudKey */ + foreach ($cloudKeys as $index => $cloudKey) { + if ( + $cloudKey->label === $label + // Assert that a corresponding local key exists. + && $this->localSshKeyExists() + // Assert local public key contents match Cloud public key contents. + && $this->normalizePublicSshKey($cloudKey->public_key) === $this->normalizePublicSshKey(file_get_contents($this->publicSshKeyFilepath)) + ) { + return true; + } + } + return false; + } - protected function localSshKeyExists(): bool { - return file_exists($this->publicSshKeyFilepath) && file_exists($this->privateSshKeyFilepath); - } + protected function passPhraseFileExists(): bool + { + return file_exists($this->passphraseFilepath); + } + protected function localSshKeyExists(): bool + { + return file_exists($this->publicSshKeyFilepath) && file_exists($this->privateSshKeyFilepath); + } } diff --git a/src/CommandFactoryInterface.php b/src/CommandFactoryInterface.php index 33205ab46..b8d830578 100644 --- a/src/CommandFactoryInterface.php +++ b/src/CommandFactoryInterface.php @@ -1,6 +1,6 @@ getRootNode() + public function getConfigTreeBuilder(): TreeBuilder + { + $treeBuilder = new TreeBuilder('acquia_cli'); + $treeBuilder + ->getRootNode() ->children() ->scalarNode('cloud_app_uuid') ->end() - ->end(); - return $treeBuilder; - } - + ->end(); + return $treeBuilder; + } } diff --git a/src/Config/CloudDataConfig.php b/src/Config/CloudDataConfig.php index d283cebb6..61d89e3f4 100644 --- a/src/Config/CloudDataConfig.php +++ b/src/Config/CloudDataConfig.php @@ -1,23 +1,25 @@ getRootNode(); - $rootNode - ->children() + public function getConfigTreeBuilder(): TreeBuilder + { + $treeBuilder = new TreeBuilder('cloud_api'); + $rootNode = $treeBuilder->getRootNode(); + $rootNode + ->children() // I can't find a better node type that accepts TRUE, FALSE, and NULL. // boolNode() will cast NULL to FALSE and enumNode()->values() will @@ -28,7 +30,7 @@ public function getConfigTreeBuilder(): TreeBuilder { ->arrayNode('keys') ->useAttributeAsKey('uuid') - ->normalizeKeys(FALSE) + ->normalizeKeys(false) ->arrayPrototype() ->children() ->scalarNode('label')->end() @@ -42,14 +44,14 @@ public function getConfigTreeBuilder(): TreeBuilder { ->children() ->scalarNode('uuid')->end() ->booleanNode('is_acquian') - ->defaultValue(FALSE) + ->defaultValue(false) ->end() ->end() ->end() ->arrayNode('acsf_factories') ->useAttributeAsKey('url') - ->normalizeKeys(FALSE) + ->normalizeKeys(false) ->arrayPrototype() ->children() ->arrayNode('users') @@ -68,13 +70,12 @@ public function getConfigTreeBuilder(): TreeBuilder { ->scalarNode('acsf_active_factory')->end() - ->end() - ->validate() - ->ifTrue(function ($config) { - return is_array($config['keys']) && !empty($config['keys']) && !array_key_exists($config['acli_key'], $config['keys']); - }) - ->thenInvalid('acli_key must exist in keys'); - return $treeBuilder; - } - + ->end() + ->validate() + ->ifTrue(function ($config) { + return is_array($config['keys']) && !empty($config['keys']) && !array_key_exists($config['acli_key'], $config['keys']); + }) + ->thenInvalid('acli_key must exist in keys'); + return $treeBuilder; + } } diff --git a/src/ConnectorFactoryInterface.php b/src/ConnectorFactoryInterface.php index 88f5e2d90..ded5c0138 100644 --- a/src/ConnectorFactoryInterface.php +++ b/src/ConnectorFactoryInterface.php @@ -1,14 +1,13 @@ - */ - protected array $config; - - public function __construct( - protected LocalMachineHelper $localMachineHelper, - AcquiaCliConfig $configDefinition, - string $acliConfigFilepath - ) { - $filePath = $localMachineHelper->getLocalFilepath($acliConfigFilepath); - parent::__construct($filePath, $configDefinition); - } +class AcquiaCliDatastore extends YamlStore +{ + /** + * @var array + */ + protected array $config; + public function __construct( + protected LocalMachineHelper $localMachineHelper, + AcquiaCliConfig $configDefinition, + string $acliConfigFilepath + ) { + $filePath = $localMachineHelper->getLocalFilepath($acliConfigFilepath); + parent::__construct($filePath, $configDefinition); + } } diff --git a/src/DataStore/CloudDataStore.php b/src/DataStore/CloudDataStore.php index 22995e4d4..ac8b1aa16 100644 --- a/src/DataStore/CloudDataStore.php +++ b/src/DataStore/CloudDataStore.php @@ -1,25 +1,24 @@ - */ - protected array $config; - - public function __construct( - protected LocalMachineHelper $localMachineHelper, - CloudDataConfig $cloudDataConfig, - string $cloudConfigFilepath - ) { - parent::__construct($cloudConfigFilepath, $cloudDataConfig); - } +class CloudDataStore extends JsonDataStore +{ + /** + * @var array + */ + protected array $config; + public function __construct( + protected LocalMachineHelper $localMachineHelper, + CloudDataConfig $cloudDataConfig, + string $cloudConfigFilepath + ) { + parent::__construct($cloudConfigFilepath, $cloudDataConfig); + } } diff --git a/src/DataStore/DataStoreInterface.php b/src/DataStore/DataStoreInterface.php index 17cc9f359..71bbc8033 100644 --- a/src/DataStore/DataStoreInterface.php +++ b/src/DataStore/DataStoreInterface.php @@ -1,19 +1,18 @@ fileSystem = new Filesystem(); - $this->filepath = $path; - $this->expander = new Expander(); - $this->expander->setStringifier(new Stringifier()); - $this->data = new Data(); - } - - public function set(string $key, mixed $value): void { - $this->data->set($key, $value); - $this->dump(); - } - - public function get(string $key): mixed { - try { - return $this->data->get($key); - } - catch (MissingPathException) { - return NULL; + public function __construct(string $path) + { + $this->fileSystem = new Filesystem(); + $this->filepath = $path; + $this->expander = new Expander(); + $this->expander->setStringifier(new Stringifier()); + $this->data = new Data(); } - } - public function remove(string $key): void { - $this->data->remove($key); - $this->dump(); - } + public function set(string $key, mixed $value): void + { + $this->data->set($key, $value); + $this->dump(); + } - public function exists(string $key): bool { - return $this->data->has($key); - } + public function get(string $key): mixed + { + try { + return $this->data->get($key); + } catch (MissingPathException) { + return null; + } + } - /** - * @param string $path Path to the datastore on disk. - * @return array - */ - protected function processConfig(array $config, ConfigurationInterface $definition, string $path): array { - try { - return (new Processor())->processConfiguration( - $definition, - [$definition->getName() => $config], - ); + public function remove(string $key): void + { + $this->data->remove($key); + $this->dump(); } - catch (InvalidConfigurationException $e) { - throw new AcquiaCliException( - 'Configuration file at the following path contains invalid keys: {path} {error}', - ['path' => $path, 'error' => $e->getMessage()]); + + public function exists(string $key): bool + { + return $this->data->has($key); } - } + /** + * @param string $path Path to the datastore on disk. + * @return array + */ + protected function processConfig(array $config, ConfigurationInterface $definition, string $path): array + { + try { + return (new Processor())->processConfiguration( + $definition, + [$definition->getName() => $config], + ); + } catch (InvalidConfigurationException $e) { + throw new AcquiaCliException( + 'Configuration file at the following path contains invalid keys: {path} {error}', + ['path' => $path, 'error' => $e->getMessage()] + ); + } + } } diff --git a/src/DataStore/JsonDataStore.php b/src/DataStore/JsonDataStore.php index 6c7f90d9f..896e99831 100644 --- a/src/DataStore/JsonDataStore.php +++ b/src/DataStore/JsonDataStore.php @@ -1,49 +1,51 @@ fileSystem->exists($path)) { - $array = json_decode(file_get_contents($path), TRUE, 512, JSON_THROW_ON_ERROR); - $array = $this->expander->expandArrayProperties($array); - $cleaned = $this->cleanLegacyConfig($array); - - if ($configDefinition) { - $array = $this->processConfig($array, $configDefinition, $path); - } - $this->data->import($array); - - // Dump the new values to disk. - if ($cleaned) { - $this->dump(); - } +class JsonDataStore extends Datastore +{ + /** + * Creates a new store. + * + * @param \Symfony\Component\Config\Definition\ConfigurationInterface|null $configDefinition + */ + public function __construct(string $path, ConfigurationInterface $configDefinition = null) + { + parent::__construct($path); + if ($this->fileSystem->exists($path)) { + $array = json_decode(file_get_contents($path), true, 512, JSON_THROW_ON_ERROR); + $array = $this->expander->expandArrayProperties($array); + $cleaned = $this->cleanLegacyConfig($array); + + if ($configDefinition) { + $array = $this->processConfig($array, $configDefinition, $path); + } + $this->data->import($array); + + // Dump the new values to disk. + if ($cleaned) { + $this->dump(); + } + } } - } - - public function dump(): void { - $this->fileSystem->dumpFile($this->filepath, json_encode($this->data->export(), JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT)); - } - - protected function cleanLegacyConfig(array &$array): bool { - // Legacy format of credential storage. - $dump = FALSE; - if (array_key_exists('key', $array) || array_key_exists('secret', $array)) { - unset($array['key'], $array['secret']); - $dump = TRUE; + + public function dump(): void + { + $this->fileSystem->dumpFile($this->filepath, json_encode($this->data->export(), JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT)); } - return $dump; - } + protected function cleanLegacyConfig(array &$array): bool + { + // Legacy format of credential storage. + $dump = false; + if (array_key_exists('key', $array) || array_key_exists('secret', $array)) { + unset($array['key'], $array['secret']); + $dump = true; + } + return $dump; + } } diff --git a/src/DataStore/YamlStore.php b/src/DataStore/YamlStore.php index a2608f03d..c9040bda3 100644 --- a/src/DataStore/YamlStore.php +++ b/src/DataStore/YamlStore.php @@ -1,33 +1,34 @@ fileSystem->exists($path)) { - $array = Yaml::parseFile($path); - $array = $this->expander->expandArrayProperties($array); - if ($configDefinition) { - $array = $this->processConfig($array, $configDefinition, $path); - } - $this->data->import($array); +class YamlStore extends Datastore +{ + /** + * Creates a new store. + * + * @param \Symfony\Component\Config\Definition\ConfigurationInterface|null $configDefinition + */ + public function __construct(string $path, ConfigurationInterface $configDefinition = null) + { + parent::__construct($path); + if ($this->fileSystem->exists($path)) { + $array = Yaml::parseFile($path); + $array = $this->expander->expandArrayProperties($array); + if ($configDefinition) { + $array = $this->processConfig($array, $configDefinition, $path); + } + $this->data->import($array); + } } - } - - public function dump(): void { - $this->fileSystem->dumpFile($this->filepath, Yaml::dump($this->data->export())); - } + public function dump(): void + { + $this->fileSystem->dumpFile($this->filepath, Yaml::dump($this->data->export())); + } } diff --git a/src/EventListener/ComposerScriptsListener.php b/src/EventListener/ComposerScriptsListener.php index de2474acb..b7b3ed796 100644 --- a/src/EventListener/ComposerScriptsListener.php +++ b/src/EventListener/ComposerScriptsListener.php @@ -1,6 +1,6 @@ executeComposerScripts($event, 'pre'); - } - - /** - * When a console command terminates successfully, execute a corresponding - * script from a local composer.json. - */ - public function onConsoleTerminate(ConsoleTerminateEvent $event): void { - if ($event->getExitCode() === 0) { - $this->executeComposerScripts($event, 'post'); +class ComposerScriptsListener +{ + /** + * Before a console command is executed, execute a corresponding script from + * a local composer.json. + */ + public function onConsoleCommand(ConsoleCommandEvent $event): void + { + $this->executeComposerScripts($event, 'pre'); } - } - /** - * @param string $prefix Added to the Composer script name. Expected values - * are 'pre' or 'post'. - */ - private function executeComposerScripts(ConsoleCommandEvent|ConsoleTerminateEvent $event, string $prefix): void { - /** @var CommandBase $command */ - $command = $event->getCommand(); - if ($event->getInput()->hasOption('no-scripts') && $event->getInput()->getOption('no-scripts')) { - return; + /** + * When a console command terminates successfully, execute a corresponding + * script from a local composer.json. + */ + public function onConsoleTerminate(ConsoleTerminateEvent $event): void + { + if ($event->getExitCode() === 0) { + $this->executeComposerScripts($event, 'post'); + } } - // Only successful commands should be executed. - if (is_a($command, CommandBase::class)) { - $composerJsonFilepath = Path::join($command->getProjectDir(), 'composer.json'); - if (file_exists($composerJsonFilepath)) { - $composerJson = json_decode($command->localMachineHelper->readFile($composerJsonFilepath), TRUE, 512, JSON_THROW_ON_ERROR); - $commandName = $command->getName(); - // Replace colons with hyphens. E.g., pull:db becomes pull-db. - $scriptName = $prefix . '-acli-' . str_replace(':', '-', $commandName); - if (array_key_exists('scripts', $composerJson) && array_key_exists($scriptName, $composerJson['scripts'])) { - $event->getOutput()->writeln("Executing composer script `$scriptName` defined in `$composerJsonFilepath`", OutputInterface::VERBOSITY_VERBOSE); - $event->getOutput()->writeln($scriptName); - $command->localMachineHelper->execute(['composer', 'run-script', $scriptName]); + + /** + * @param string $prefix Added to the Composer script name. Expected values + * are 'pre' or 'post'. + */ + private function executeComposerScripts(ConsoleCommandEvent|ConsoleTerminateEvent $event, string $prefix): void + { + /** @var CommandBase $command */ + $command = $event->getCommand(); + if ($event->getInput()->hasOption('no-scripts') && $event->getInput()->getOption('no-scripts')) { + return; } - else { - $event->getOutput()->writeln("Notice: Composer script `$scriptName` does not exist in `$composerJsonFilepath`, skipping. This is not an error.", OutputInterface::VERBOSITY_VERBOSE); + // Only successful commands should be executed. + if (is_a($command, CommandBase::class)) { + $composerJsonFilepath = Path::join($command->getProjectDir(), 'composer.json'); + if (file_exists($composerJsonFilepath)) { + $composerJson = json_decode($command->localMachineHelper->readFile($composerJsonFilepath), true, 512, JSON_THROW_ON_ERROR); + $commandName = $command->getName(); + // Replace colons with hyphens. E.g., pull:db becomes pull-db. + $scriptName = $prefix . '-acli-' . str_replace(':', '-', $commandName); + if (array_key_exists('scripts', $composerJson) && array_key_exists($scriptName, $composerJson['scripts'])) { + $event->getOutput()->writeln("Executing composer script `$scriptName` defined in `$composerJsonFilepath`", OutputInterface::VERBOSITY_VERBOSE); + $event->getOutput()->writeln($scriptName); + $command->localMachineHelper->execute(['composer', 'run-script', $scriptName]); + } else { + $event->getOutput()->writeln("Notice: Composer script `$scriptName` does not exist in `$composerJsonFilepath`, skipping. This is not an error.", OutputInterface::VERBOSITY_VERBOSE); + } + } } - } } - } - } diff --git a/src/EventListener/ExceptionListener.php b/src/EventListener/ExceptionListener.php index 9388291e8..bb59dcb6b 100644 --- a/src/EventListener/ExceptionListener.php +++ b/src/EventListener/ExceptionListener.php @@ -1,6 +1,6 @@ getExitCode(); - $error = $event->getError(); - $errorMessage = $error->getMessage(); - - if ($error instanceof IdentityProviderException && $error->getMessage() === 'invalid_client') { - $newErrorMessage = 'Your Cloud Platform API credentials are invalid.'; - $this->helpMessages[] = "Run messagesBgColor;fg=$this->messagesFgColor;options=bold>acli auth:login to reset your API credentials."; - } - - if ($error instanceof RuntimeException) { - switch ($errorMessage) { - case 'Not enough arguments (missing: "environmentId").': - case 'Not enough arguments (missing: "environmentUuid").': - $this->writeSiteAliasHelp(); - break; - } - } - - if ($error instanceof AcquiaCliException) { - switch ($error->getRawMessage()) { - case 'No applications match the alias {applicationAlias}': - case 'Multiple applications match the alias {applicationAlias}': - $this->writeApplicationAliasHelp(); - break; - case '{environmentId} must be a valid UUID or site alias.': - case '{environmentUuid} must be a valid UUID or site alias.': - $this->writeSiteAliasHelp(); - break; - case 'Access token file not found at {file}': - case 'Access token expiry file not found at {file}': - $this->helpMessages[] = 'Get help for this error at https://docs.acquia.com/ide/known-issues/#the-automated-cloud-platform-api-authentication-might-fail'; - break; - case 'This machine is not yet authenticated with the Cloud Platform.': - $this->helpMessages[] = 'Run `acli auth:login` to re-authenticated with the Cloud Platform.'; - break; - case 'This machine is not yet authenticated with Site Factory.': - $this->helpMessages[] = 'Run `acli auth:acsf-login` to re-authenticate with Site Factory.'; - break; - case 'Could not extract aliases to {destination}': - $this->helpMessages[] = 'Check that you have write access to the directory'; - break; - } - } - - if ($error instanceof ApiErrorException) { - switch ($errorMessage) { - case "There are no available Cloud IDEs for this application.\n": - $this->helpMessages[] = "Delete an existing IDE via messagesBgColor;fg=$this->messagesFgColor;options=bold>acli ide:delete or contact your Account Manager or Acquia Sales to purchase additional IDEs."; - break; - case "This resource requires additional authentication.": - $this->helpMessages[] = "This is likely because you have Federated Authentication required for your organization."; - $this->helpMessages[] = "Run `acli login` to authenticate via API token and then try again."; - break; - default: - $newErrorMessage = 'Cloud Platform API returned an error: ' . $errorMessage; - $this->helpMessages[] = "You can learn more about Cloud Platform API at https://docs.acquia.com/cloud-platform/develop/api/"; - } +class ExceptionListener +{ + private string $messagesBgColor = 'blue'; + + private string $messagesFgColor = 'white'; + + /** + * @var string[] + */ + private array $helpMessages = []; + + public function onConsoleError(ConsoleErrorEvent $event): void + { + $exitCode = $event->getExitCode(); + $error = $event->getError(); + $errorMessage = $error->getMessage(); + + if ($error instanceof IdentityProviderException && $error->getMessage() === 'invalid_client') { + $newErrorMessage = 'Your Cloud Platform API credentials are invalid.'; + $this->helpMessages[] = "Run messagesBgColor;fg=$this->messagesFgColor;options=bold>acli auth:login to reset your API credentials."; + } + + if ($error instanceof RuntimeException) { + switch ($errorMessage) { + case 'Not enough arguments (missing: "environmentId").': + case 'Not enough arguments (missing: "environmentUuid").': + $this->writeSiteAliasHelp(); + break; + } + } + + if ($error instanceof AcquiaCliException) { + switch ($error->getRawMessage()) { + case 'No applications match the alias {applicationAlias}': + case 'Multiple applications match the alias {applicationAlias}': + $this->writeApplicationAliasHelp(); + break; + case '{environmentId} must be a valid UUID or site alias.': + case '{environmentUuid} must be a valid UUID or site alias.': + $this->writeSiteAliasHelp(); + break; + case 'Access token file not found at {file}': + case 'Access token expiry file not found at {file}': + $this->helpMessages[] = 'Get help for this error at https://docs.acquia.com/ide/known-issues/#the-automated-cloud-platform-api-authentication-might-fail'; + break; + case 'This machine is not yet authenticated with the Cloud Platform.': + $this->helpMessages[] = 'Run `acli auth:login` to re-authenticated with the Cloud Platform.'; + break; + case 'This machine is not yet authenticated with Site Factory.': + $this->helpMessages[] = 'Run `acli auth:acsf-login` to re-authenticate with Site Factory.'; + break; + case 'Could not extract aliases to {destination}': + $this->helpMessages[] = 'Check that you have write access to the directory'; + break; + } + } + + if ($error instanceof ApiErrorException) { + switch ($errorMessage) { + case "There are no available Cloud IDEs for this application.\n": + $this->helpMessages[] = "Delete an existing IDE via messagesBgColor;fg=$this->messagesFgColor;options=bold>acli ide:delete or contact your Account Manager or Acquia Sales to purchase additional IDEs."; + break; + case "This resource requires additional authentication.": + $this->helpMessages[] = "This is likely because you have Federated Authentication required for your organization."; + $this->helpMessages[] = "Run `acli login` to authenticate via API token and then try again."; + break; + default: + $newErrorMessage = 'Cloud Platform API returned an error: ' . $errorMessage; + $this->helpMessages[] = "You can learn more about Cloud Platform API at https://docs.acquia.com/cloud-platform/develop/api/"; + } + } + + if ($error instanceof \TypeError && str_contains($error->getMessage(), 'AcquiaCloudApi\Response')) { + $newErrorMessage = 'Cloud Platform API returned an unexpected data type. This is not an issue with Acquia CLI but could indicate a problem with your Cloud Platform application.'; + } + + if (!empty($this->helpMessages)) { + $this->helpMessages[0] = 'How to fix it: ' . $this->helpMessages[0]; + } + $this->helpMessages[] = "You can find Acquia CLI documentation at https://docs.acquia.com/acquia-cli/"; + $this->writeUpdateHelp($event); + $this->writeSupportTicketHelp($event); + + if ($command = $event->getCommand()) { + /** @var \Acquia\Cli\Application $application */ + $application = $command->getApplication(); + $application->setHelpMessages($this->helpMessages); + } + + if (isset($newErrorMessage)) { + $event->setError(new AcquiaCliException($newErrorMessage, [], $exitCode)); + } } - if ($error instanceof \TypeError && str_contains($error->getMessage(), 'AcquiaCloudApi\Response')) { - $newErrorMessage = 'Cloud Platform API returned an unexpected data type. This is not an issue with Acquia CLI but could indicate a problem with your Cloud Platform application.'; + private function writeApplicationAliasHelp(): void + { + $this->helpMessages[] = "The messagesBgColor;options=bold>applicationUuid argument must be a valid UUID or unique application alias accessible to your Cloud Platform user." . PHP_EOL . PHP_EOL + . "An alias consists of an application name optionally prefixed with a hosting realm, e.g. messagesBgColor;fg=$this->messagesFgColor;options=bold>myapp or messagesBgColor;fg=$this->messagesFgColor;options=bold>prod.myapp." . PHP_EOL . PHP_EOL + . "Run messagesBgColor;options=bold>acli remote:aliases:list to see a list of all available aliases."; } - if (!empty($this->helpMessages)) { - $this->helpMessages[0] = 'How to fix it: ' . $this->helpMessages[0]; - } - $this->helpMessages[] = "You can find Acquia CLI documentation at https://docs.acquia.com/acquia-cli/"; - $this->writeUpdateHelp($event); - $this->writeSupportTicketHelp($event); - - if ($command = $event->getCommand()) { - /** @var \Acquia\Cli\Application $application */ - $application = $command->getApplication(); - $application->setHelpMessages($this->helpMessages); + private function writeSiteAliasHelp(): void + { + $this->helpMessages[] = "messagesBgColor;options=bold>environmentId can also be a site alias. E.g. messagesBgColor;fg=$this->messagesFgColor;options=bold>myapp.dev." . PHP_EOL + . "Run messagesBgColor;options=bold>acli remote:aliases:list to see a list of all available aliases."; } - if (isset($newErrorMessage)) { - $event->setError(new AcquiaCliException($newErrorMessage, [], $exitCode)); - } - } - - private function writeApplicationAliasHelp(): void { - $this->helpMessages[] = "The messagesBgColor;options=bold>applicationUuid argument must be a valid UUID or unique application alias accessible to your Cloud Platform user." . PHP_EOL . PHP_EOL - . "An alias consists of an application name optionally prefixed with a hosting realm, e.g. messagesBgColor;fg=$this->messagesFgColor;options=bold>myapp or messagesBgColor;fg=$this->messagesFgColor;options=bold>prod.myapp." . PHP_EOL . PHP_EOL - . "Run messagesBgColor;options=bold>acli remote:aliases:list to see a list of all available aliases."; - } - - private function writeSiteAliasHelp(): void { - $this->helpMessages[] = "messagesBgColor;options=bold>environmentId can also be a site alias. E.g. messagesBgColor;fg=$this->messagesFgColor;options=bold>myapp.dev." . PHP_EOL - . "Run messagesBgColor;options=bold>acli remote:aliases:list to see a list of all available aliases."; - } - - private function writeSupportTicketHelp(ConsoleErrorEvent $event): void { - $message = "You can submit a support ticket at https://support-acquia.force.com/s/contactsupport"; - if (!$event->getOutput()->isVeryVerbose()) { - $message .= PHP_EOL . "Re-run the command with the messagesBgColor;fg=$this->messagesFgColor;options=bold>-vvv flag and include the full command output in your support ticket."; - } - $this->helpMessages[] = $message; - } - - private function writeUpdateHelp(ConsoleErrorEvent $event): void { - try { - $command = $event->getCommand(); - if ($command - && method_exists($command, 'checkForNewVersion') - && $latest = $command->checkForNewVersion() - ) { - $message = "Acquia CLI $latest is available. Try updating via messagesBgColor;fg=$this->messagesFgColor;options=bold>acli self-update and then run the command again."; + private function writeSupportTicketHelp(ConsoleErrorEvent $event): void + { + $message = "You can submit a support ticket at https://support-acquia.force.com/s/contactsupport"; + if (!$event->getOutput()->isVeryVerbose()) { + $message .= PHP_EOL . "Re-run the command with the messagesBgColor;fg=$this->messagesFgColor;options=bold>-vvv flag and include the full command output in your support ticket."; + } $this->helpMessages[] = $message; - } - // This command may not exist during some testing. } - catch (CommandNotFoundException) { - } - } + private function writeUpdateHelp(ConsoleErrorEvent $event): void + { + try { + $command = $event->getCommand(); + if ( + $command + && method_exists($command, 'checkForNewVersion') + && $latest = $command->checkForNewVersion() + ) { + $message = "Acquia CLI $latest is available. Try updating via messagesBgColor;fg=$this->messagesFgColor;options=bold>acli self-update and then run the command again."; + $this->helpMessages[] = $message; + } + // This command may not exist during some testing. + } catch (CommandNotFoundException) { + } + } } diff --git a/src/Exception/AcquiaCliException.php b/src/Exception/AcquiaCliException.php index 52c6125df..28912e050 100644 --- a/src/Exception/AcquiaCliException.php +++ b/src/Exception/AcquiaCliException.php @@ -1,56 +1,57 @@ $code, - 'message' => $rawMessage, -]; - Amplitude::getInstance()->queueEvent('Threw exception', $eventProperties); - - parent::__construct($this->interpolateString($rawMessage, $replacements), $code); - } - - /** - * Returns the replacements context array. - * - * @return string $this->replacements - */ - public function getRawMessage(): string { - return $this->rawMessage; - } - - /** - * Replace the variables into the message string. - * - * @param string $message The raw, uninterpolated message string. - * @param array $replacements The values to replace into the message. - */ - protected function interpolateString(string $message, array $replacements): string { - $tr = []; - foreach ($replacements as $key => $val) { - $tr['{' . $key . '}'] = $val; + $eventProperties = [ + 'code' => $code, + 'message' => $rawMessage, + ]; + Amplitude::getInstance()->queueEvent('Threw exception', $eventProperties); + + parent::__construct($this->interpolateString($rawMessage, $replacements), $code); } - return strtr($message, $tr); - } + /** + * Returns the replacements context array. + * + * @return string $this->replacements + */ + public function getRawMessage(): string + { + return $this->rawMessage; + } + /** + * Replace the variables into the message string. + * + * @param string $message The raw, uninterpolated message string. + * @param array $replacements The values to replace into the message. + */ + protected function interpolateString(string $message, array $replacements): string + { + $tr = []; + foreach ($replacements as $key => $val) { + $tr['{' . $key . '}'] = $val; + } + + return strtr($message, $tr); + } } diff --git a/src/Helpers/AliasCache.php b/src/Helpers/AliasCache.php index f56631067..c507d910f 100644 --- a/src/Helpers/AliasCache.php +++ b/src/Helpers/AliasCache.php @@ -1,17 +1,17 @@ localMachineHelper->readFile($this->getIdePhpVersionFilePath())); - } - catch (FilesystemException) { - return NULL; +trait IdeCommandTrait +{ + private string $phpVersionFilePath; + + private function getIdePhpVersion(): ?string + { + try { + return trim($this->localMachineHelper->readFile($this->getIdePhpVersionFilePath())); + } catch (FilesystemException) { + return null; + } } - } - - public function setPhpVersionFilePath(string $path): void { - $this->phpVersionFilePath = $path; - } - - protected function getIdePhpVersionFilePath(): string { - if (!isset($this->phpVersionFilePath)) { - $this->phpVersionFilePath = '/home/ide/configs/php/.version'; + public function setPhpVersionFilePath(string $path): void + { + $this->phpVersionFilePath = $path; } - return $this->phpVersionFilePath; - } + protected function getIdePhpVersionFilePath(): string + { + if (!isset($this->phpVersionFilePath)) { + $this->phpVersionFilePath = '/home/ide/configs/php/.version'; + } + return $this->phpVersionFilePath; + } } diff --git a/src/Helpers/LocalMachineHelper.php b/src/Helpers/LocalMachineHelper.php index 9e9810a46..5a48dcabd 100644 --- a/src/Helpers/LocalMachineHelper.php +++ b/src/Helpers/LocalMachineHelper.php @@ -1,6 +1,6 @@ - */ - private array $installedBinaries = []; - - private SymfonyStyle $io; - - public function __construct( - private readonly InputInterface $input, - private readonly OutputInterface $output, - LoggerInterface $logger - ) { - $this->setLogger($logger); - $this->io = new SymfonyStyle($input, $output); - } - - /** - * Check if a command exists. - * - * This won't find aliases or shell built-ins, so use it mindfully (e.g. only - * for commands that you _know_ to be system commands). - */ - public function commandExists(string $command): bool { - if (array_key_exists($command, $this->installedBinaries)) { - return $this->installedBinaries[$command]; - } - $osCommand = OsInfo::isWindows() ? ['where', $command] : ['which', $command]; - $exists = $this->execute($osCommand, NULL, NULL, FALSE, NULL, NULL, FALSE)->isSuccessful(); - $this->installedBinaries[$command] = $exists; - return $exists; - } - - public function checkRequiredBinariesExist(array $binaries = []): void { - foreach ($binaries as $binary) { - if (!$this->commandExists($binary)) { - throw new AcquiaCliException("The required binary `$binary` does not exist. Install it and ensure it exists in a location listed in your system \$PATH"); - } - } - } - - /** - * Executes a buffered command. - */ - public function execute(array $cmd, callable $callback = NULL, string $cwd = NULL, ?bool $printOutput = TRUE, float $timeout = NULL, array $env = NULL, bool $stdin = TRUE): Process { - $process = new Process($cmd); - $process = $this->configureProcess($process, $cwd, $printOutput, $timeout, $env, $stdin); - return $this->executeProcess($process, $callback, $printOutput); - } - - /** - * Executes a command directly in a shell (without additional parsing). - * - * Use `execute()` instead whenever possible. `executeFromCmd()` does not - * automatically escape arguments and should only be used for commands with - * pipes or redirects not supported by `execute()`. - * - * Windows does not support prepending commands with environment variables. - * - * @param callable|null $callback - * @param string|null $cwd - * @param int|null $timeout - * @param array|null $env - */ - public function executeFromCmd(string $cmd, callable $callback = NULL, string $cwd = NULL, ?bool $printOutput = TRUE, int $timeout = NULL, array $env = NULL): Process { - $process = Process::fromShellCommandline($cmd); - $process = $this->configureProcess($process, $cwd, $printOutput, $timeout, $env); - - return $this->executeProcess($process, $callback, $printOutput); - } - - /** - * @param string|null $cwd - * @param array|null $env - */ - private function configureProcess(Process $process, string $cwd = NULL, ?bool $printOutput = TRUE, float $timeout = NULL, array $env = NULL, bool $stdin = TRUE): Process { - if (function_exists('posix_isatty') && $stdin && !@posix_isatty(STDIN)) { - $process->setInput(STDIN); - } - if ($cwd) { - $process->setWorkingDirectory($cwd); - } - if ($printOutput) { - $process->setTty($this->useTty()); +class LocalMachineHelper +{ + use LoggerAwareTrait; + + private ?bool $isTty; + + /** + * @var array + */ + private array $installedBinaries = []; + + private SymfonyStyle $io; + + public function __construct( + private readonly InputInterface $input, + private readonly OutputInterface $output, + LoggerInterface $logger + ) { + $this->setLogger($logger); + $this->io = new SymfonyStyle($input, $output); } - if ($env) { - $process->setEnv($env); - } - $process->setTimeout($timeout); - - return $process; - } - private function executeProcess(Process $process, callable $callback = NULL, ?bool $printOutput = TRUE): Process { - if ($callback === NULL && $printOutput !== FALSE) { - $callback = function (mixed $type, mixed $buffer): void { - $this->output->write($buffer); - }; - } - $process->start(); - $process->wait($callback); - - $this->logger->notice('Command: {command} [Exit: {exit}]', [ - 'command' => $process->getCommandLine(), - 'exit' => $process->getExitCode(), - ]); - - return $process; - } - - /** - * Returns a set-up filesystem object. - */ - public function getFilesystem(): Filesystem { - return new Filesystem(); - } - - /** - * Returns a finder object. - */ - public function getFinder(): Finder { - return new Finder(); - } - - /** - * Reads to a file from the local system. - */ - public function readFile(string $filename): string { - // @todo remove this blasphemy once upstream issue is fixed - // @ see https://github.com/thecodingmachine/safe/issues/120 - return @file_get_contents($this->getLocalFilepath($filename)); - } - - public function getLocalFilepath(string $filepath): string { - return $this->fixFilename($filepath); - } - - /** - * Determine whether the use of a tty is appropriate. - */ - public function useTty(): bool { - if (isset($this->isTty)) { - return $this->isTty; + /** + * Check if a command exists. + * + * This won't find aliases or shell built-ins, so use it mindfully (e.g. only + * for commands that you _know_ to be system commands). + */ + public function commandExists(string $command): bool + { + if (array_key_exists($command, $this->installedBinaries)) { + return $this->installedBinaries[$command]; + } + $osCommand = OsInfo::isWindows() ? ['where', $command] : ['which', $command]; + $exists = $this->execute($osCommand, null, null, false, null, null, false)->isSuccessful(); + $this->installedBinaries[$command] = $exists; + return $exists; } - // If we are not in interactive mode, then never use a tty. - if (!$this->input->isInteractive()) { - return FALSE; + public function checkRequiredBinariesExist(array $binaries = []): void + { + foreach ($binaries as $binary) { + if (!$this->commandExists($binary)) { + throw new AcquiaCliException("The required binary `$binary` does not exist. Install it and ensure it exists in a location listed in your system \$PATH"); + } + } } - // If we are in interactive mode (or at least the user did not - // specify -n / --no-interaction), then also prevent the use - // of a tty if stdout is redirected. - // Otherwise, let the local machine helper decide whether to use a tty. - if (function_exists('posix_isatty')) { - return (posix_isatty(STDOUT) && @posix_isatty(STDIN)); + /** + * Executes a buffered command. + */ + public function execute(array $cmd, callable $callback = null, string $cwd = null, ?bool $printOutput = true, float $timeout = null, array $env = null, bool $stdin = true): Process + { + $process = new Process($cmd); + $process = $this->configureProcess($process, $cwd, $printOutput, $timeout, $env, $stdin); + return $this->executeProcess($process, $callback, $printOutput); } - return FALSE; - } - - public function setIsTty(?bool $isTty): void { - $this->isTty = $isTty; - } - - /** - * Writes to a file on the local system. - */ - public function writeFile(string $filename, string|StreamInterface $content): void { - $this->getFilesystem()->dumpFile($this->getLocalFilepath($filename), $content); - } - - /** - * Accepts a filename/full path and localizes it to the user's system. - */ - private function fixFilename(string $filename): string { - // '~' is only an alias for $HOME if it's at the start of the path. - // On Windows, '~' (not as an alias) can appear other places in the path. - return preg_replace('/^~/', self::getHomeDir(), $filename); - } - - /** - * Returns the appropriate home directory. - * - * @see https://github.com/pantheon-systems/terminus/blob/1d89e20dd388dc08979a1bc52dfd142b26c03dcf/src/Config/DefaultsConfig.php#L99 - */ - public static function getHomeDir(): string { - $home = getenv('HOME'); - if (!$home) { - $system = ''; - if (getenv('MSYSTEM')) { - $system = strtoupper(substr(getenv('MSYSTEM'), 0, 4)); - } - if ($system !== 'MING') { - $home = getenv('HOMEPATH'); - } + /** + * Executes a command directly in a shell (without additional parsing). + * + * Use `execute()` instead whenever possible. `executeFromCmd()` does not + * automatically escape arguments and should only be used for commands with + * pipes or redirects not supported by `execute()`. + * + * Windows does not support prepending commands with environment variables. + * + * @param callable|null $callback + * @param string|null $cwd + * @param int|null $timeout + * @param array|null $env + */ + public function executeFromCmd(string $cmd, callable $callback = null, string $cwd = null, ?bool $printOutput = true, int $timeout = null, array $env = null): Process + { + $process = Process::fromShellCommandline($cmd); + $process = $this->configureProcess($process, $cwd, $printOutput, $timeout, $env); + + return $this->executeProcess($process, $callback, $printOutput); } - if (!$home) { - throw new AcquiaCliException('Could not determine $HOME directory. Ensure $HOME is set in your shell.'); + /** + * @param string|null $cwd + * @param array|null $env + */ + private function configureProcess(Process $process, string $cwd = null, ?bool $printOutput = true, float $timeout = null, array $env = null, bool $stdin = true): Process + { + if (function_exists('posix_isatty') && $stdin && !@posix_isatty(STDIN)) { + $process->setInput(STDIN); + } + if ($cwd) { + $process->setWorkingDirectory($cwd); + } + if ($printOutput) { + $process->setTty($this->useTty()); + } + if ($env) { + $process->setEnv($env); + } + $process->setTimeout($timeout); + + return $process; } - return $home; - } + private function executeProcess(Process $process, callable $callback = null, ?bool $printOutput = true): Process + { + if ($callback === null && $printOutput !== false) { + $callback = function (mixed $type, mixed $buffer): void { + $this->output->write($buffer); + }; + } + $process->start(); + $process->wait($callback); + + $this->logger->notice('Command: {command} [Exit: {exit}]', [ + 'command' => $process->getCommandLine(), + 'exit' => $process->getExitCode(), + ]); + + return $process; + } - public static function getConfigDir(): string { - $home = self::getHomeDir(); - $legacyDir = Path::join($home, '.acquia'); - if (file_exists($legacyDir)) { - return $legacyDir; + /** + * Returns a set-up filesystem object. + */ + public function getFilesystem(): Filesystem + { + return new Filesystem(); } - if ($xdgHome = getenv('XDG_CONFIG_HOME')) { - return Path::join($xdgHome, 'acquia'); + + /** + * Returns a finder object. + */ + public function getFinder(): Finder + { + return new Finder(); } - return Path::join($home, '.config', 'acquia'); - } - - /** - * Get the project root directory for the working directory. - * - * This method assumes you are running `acli` in a directory containing a - * Drupal docroot either as a sibling or parent(N) of the working directory. - * - * Typically, the root directory would also be a Git repository root, though it - * doesn't have to be (such as for brand-new projects that haven't initialized - * Git yet). - */ - public static function getProjectDir(): ?string { - $possibleProjectRoots = [ - getcwd(), - ]; - // Check for PWD - some local environments will not have this key. - if (getenv('PWD') && !in_array(getenv('PWD'), $possibleProjectRoots, TRUE)) { - array_unshift($possibleProjectRoots, getenv('PWD')); + + /** + * Reads to a file from the local system. + */ + public function readFile(string $filename): string + { + // @todo remove this blasphemy once upstream issue is fixed + // @ see https://github.com/thecodingmachine/safe/issues/120 + return @file_get_contents($this->getLocalFilepath($filename)); } - foreach ($possibleProjectRoots as $possibleProjectRoot) { - if ($projectRoot = self::findDirectoryContainingFiles($possibleProjectRoot, ['docroot'])) { - return realpath($projectRoot); - } + + public function getLocalFilepath(string $filepath): string + { + return $this->fixFilename($filepath); } - return NULL; - } - - /** - * Traverses file system upwards in search of a given file. - * - * Begins searching for $file in $workingDirectory and climbs up directories - * $maxHeight times, repeating search. - * - * @return bool|string FALSE if file was not found. Otherwise, the directory path containing the file. - */ - private static function findDirectoryContainingFiles(string $workingDirectory, array $files, int $maxHeight = 10): bool|string { - $filePath = $workingDirectory; - for ($i = 0; $i <= $maxHeight; $i++) { - if (self::filesExist($filePath, $files)) { - return $filePath; - } - - $filePath = dirname($filePath); + /** + * Determine whether the use of a tty is appropriate. + */ + public function useTty(): bool + { + if (isset($this->isTty)) { + return $this->isTty; + } + + // If we are not in interactive mode, then never use a tty. + if (!$this->input->isInteractive()) { + return false; + } + + // If we are in interactive mode (or at least the user did not + // specify -n / --no-interaction), then also prevent the use + // of a tty if stdout is redirected. + // Otherwise, let the local machine helper decide whether to use a tty. + if (function_exists('posix_isatty')) { + return (posix_isatty(STDOUT) && @posix_isatty(STDIN)); + } + + return false; } - return FALSE; - } - - /** - * Determines if an array of files exists in a particular directory. - */ - private static function filesExist(string $dir, array $files): bool { - foreach ($files as $file) { - if (file_exists(Path::join($dir, $file))) { - return TRUE; - } + public function setIsTty(?bool $isTty): void + { + $this->isTty = $isTty; } - return FALSE; - } + /** + * Writes to a file on the local system. + */ + public function writeFile(string $filename, string|StreamInterface $content): void + { + $this->getFilesystem()->dumpFile($this->getLocalFilepath($filename), $content); + } - /** - * Determines if a browser is available on the local machine. - */ - public function isBrowserAvailable(): bool { - if (AcquiaDrupalEnvironmentDetector::isAhIdeEnv()) { - return FALSE; + /** + * Accepts a filename/full path and localizes it to the user's system. + */ + private function fixFilename(string $filename): string + { + // '~' is only an alias for $HOME if it's at the start of the path. + // On Windows, '~' (not as an alias) can appear other places in the path. + return preg_replace('/^~/', self::getHomeDir(), $filename); } - if (getenv('DISPLAY')) { - return TRUE; + + /** + * Returns the appropriate home directory. + * + * @see https://github.com/pantheon-systems/terminus/blob/1d89e20dd388dc08979a1bc52dfd142b26c03dcf/src/Config/DefaultsConfig.php#L99 + */ + public static function getHomeDir(): string + { + $home = getenv('HOME'); + if (!$home) { + $system = ''; + if (getenv('MSYSTEM')) { + $system = strtoupper(substr(getenv('MSYSTEM'), 0, 4)); + } + if ($system !== 'MING') { + $home = getenv('HOMEPATH'); + } + } + + if (!$home) { + throw new AcquiaCliException('Could not determine $HOME directory. Ensure $HOME is set in your shell.'); + } + + return $home; } - if (OsInfo::isWindows() || OsInfo::isApple()) { - return TRUE; + + public static function getConfigDir(): string + { + $home = self::getHomeDir(); + $legacyDir = Path::join($home, '.acquia'); + if (file_exists($legacyDir)) { + return $legacyDir; + } + if ($xdgHome = getenv('XDG_CONFIG_HOME')) { + return Path::join($xdgHome, 'acquia'); + } + return Path::join($home, '.config', 'acquia'); } - return FALSE; - } - - /** - * Starts a background browser/tab for the current site or a specified URL. - * - * Exclude from mutation testing as we don't want real browser windows opened. - * - * @param string|null $uri Optional URI or site path to open in browser. If omitted, or if a site path - * is specified, the current site home page uri will be prepended if the site's - * hostname resolves. - * @param string|null $browser The command to run to launch a browser. - * @return bool TRUE if browser was opened. FALSE if browser was disabled by the user or a - * default browser could not be found. - * @infection-ignore-all - */ - public function startBrowser(string $uri = NULL, string $browser = NULL): bool { - // We can only open a browser if we have a DISPLAY environment variable on - // POSIX or are running Windows or OS X. - if (!$this->isBrowserAvailable()) { - $this->logger->info('No graphical display appears to be available, not starting browser.'); - return FALSE; + /** + * Get the project root directory for the working directory. + * + * This method assumes you are running `acli` in a directory containing a + * Drupal docroot either as a sibling or parent(N) of the working directory. + * + * Typically, the root directory would also be a Git repository root, though it + * doesn't have to be (such as for brand-new projects that haven't initialized + * Git yet). + */ + public static function getProjectDir(): ?string + { + $possibleProjectRoots = [ + getcwd(), + ]; + // Check for PWD - some local environments will not have this key. + if (getenv('PWD') && !in_array(getenv('PWD'), $possibleProjectRoots, true)) { + array_unshift($possibleProjectRoots, getenv('PWD')); + } + foreach ($possibleProjectRoots as $possibleProjectRoot) { + if ($projectRoot = self::findDirectoryContainingFiles($possibleProjectRoot, ['docroot'])) { + return realpath($projectRoot); + } + } + + return null; } - $host = parse_url($uri, PHP_URL_HOST); - - // Validate that the host part of the URL resolves, so we don't attempt to - // open the browser for http://default or similar invalid hosts. - $hostError = (gethostbynamel($host) === FALSE); - $ipError = (ip2long($host) && gethostbyaddr($host) === $host); - if ($hostError || $ipError) { - $this->logger->warning( - '!host does not appear to be a resolvable hostname or IP, not starting browser.', - ['!host' => $host] - ); - - return FALSE; + + /** + * Traverses file system upwards in search of a given file. + * + * Begins searching for $file in $workingDirectory and climbs up directories + * $maxHeight times, repeating search. + * + * @return bool|string FALSE if file was not found. Otherwise, the directory path containing the file. + */ + private static function findDirectoryContainingFiles(string $workingDirectory, array $files, int $maxHeight = 10): bool|string + { + $filePath = $workingDirectory; + for ($i = 0; $i <= $maxHeight; $i++) { + if (self::filesExist($filePath, $files)) { + return $filePath; + } + + $filePath = dirname($filePath); + } + + return false; } - if ($browser === NULL) { - // See if we can find an OS helper to open URLs in default browser. - if ($this->commandExists('xdg-open')) { - // Linux. - $browser = 'xdg-open'; - } - else if ($this->commandExists('open')) { - // Darwin. - $browser = 'open'; - } - else if (OsInfo::isWindows()) { - $browser = 'start'; - } - else { - $this->logger->warning('Could not find a browser on your local machine. Check that one of xdg-open, open, or start are installed.'); - return FALSE; - } + + /** + * Determines if an array of files exists in a particular directory. + */ + private static function filesExist(string $dir, array $files): bool + { + foreach ($files as $file) { + if (file_exists(Path::join($dir, $file))) { + return true; + } + } + + return false; } - if ($browser) { - $this->io->info("Opening $uri"); - $this->executeFromCmd("$browser $uri"); - return TRUE; + /** + * Determines if a browser is available on the local machine. + */ + public function isBrowserAvailable(): bool + { + if (AcquiaDrupalEnvironmentDetector::isAhIdeEnv()) { + return false; + } + if (getenv('DISPLAY')) { + return true; + } + if (OsInfo::isWindows() || OsInfo::isApple()) { + return true; + } + + return false; } - return FALSE; - } + /** + * Starts a background browser/tab for the current site or a specified URL. + * + * Exclude from mutation testing as we don't want real browser windows opened. + * + * @param string|null $uri Optional URI or site path to open in browser. If omitted, or if a site path + * is specified, the current site home page uri will be prepended if the site's + * hostname resolves. + * @param string|null $browser The command to run to launch a browser. + * @return bool TRUE if browser was opened. FALSE if browser was disabled by the user or a + * default browser could not be found. + * @infection-ignore-all + */ + public function startBrowser(string $uri = null, string $browser = null): bool + { + // We can only open a browser if we have a DISPLAY environment variable on + // POSIX or are running Windows or OS X. + if (!$this->isBrowserAvailable()) { + $this->logger->info('No graphical display appears to be available, not starting browser.'); + return false; + } + $host = parse_url($uri, PHP_URL_HOST); + + // Validate that the host part of the URL resolves, so we don't attempt to + // open the browser for http://default or similar invalid hosts. + $hostError = (gethostbynamel($host) === false); + $ipError = (ip2long($host) && gethostbyaddr($host) === $host); + if ($hostError || $ipError) { + $this->logger->warning( + '!host does not appear to be a resolvable hostname or IP, not starting browser.', + ['!host' => $host] + ); + + return false; + } + if ($browser === null) { + // See if we can find an OS helper to open URLs in default browser. + if ($this->commandExists('xdg-open')) { + // Linux. + $browser = 'xdg-open'; + } elseif ($this->commandExists('open')) { + // Darwin. + $browser = 'open'; + } elseif (OsInfo::isWindows()) { + $browser = 'start'; + } else { + $this->logger->warning('Could not find a browser on your local machine. Check that one of xdg-open, open, or start are installed.'); + return false; + } + } + if ($browser) { + $this->io->info("Opening $uri"); + $this->executeFromCmd("$browser $uri"); + + return true; + } + return false; + } } diff --git a/src/Helpers/LoopHelper.php b/src/Helpers/LoopHelper.php index 1f15c4b66..5910425a7 100644 --- a/src/Helpers/LoopHelper.php +++ b/src/Helpers/LoopHelper.php @@ -1,6 +1,6 @@ setMessage($spinnerMessage); - $spinner->start(); - - $cancelTimers = static function () use (&$timers, $spinner): void { - // @infection-ignore-all - array_map('\React\EventLoop\Loop::cancelTimer', $timers); - $timers = []; - $spinner->finish(); - }; - $periodicCallback = static function () use ($statusCallback, $doneCallback, $cancelTimers): void { - // @infection-ignore-all - if ($statusCallback()) { - $cancelTimers(); - $doneCallback(); - } - }; - - // Spinner timer. - $timers[] = Loop::addPeriodicTimer($spinner->interval(), - static function () use ($spinner): void { - $spinner->advance(); - }); - - // Primary timer checking for result status. - $timers[] = Loop::addPeriodicTimer(5, $periodicCallback); - // Initial timer to speed up tests. - $timers[] = Loop::addTimer(0.1, $periodicCallback); - - // Watchdog timer. - $timers[] = Loop::addTimer(45 * 60, static function () use ($io, $doneCallback, $cancelTimers): void { - $cancelTimers(); - $io->error("Timed out after 45 minutes!"); - $doneCallback(); - }); - - // Manually run the loop. React EventLoop advises against this and suggests - // using autorun instead, but I'm not sure how to pass the correct exit code - // to Symfony if this isn't blocking. - Loop::run(); - } - +class LoopHelper +{ + /** + * @param callable $statusCallback A TRUE return value will cause the loop to exit and call $doneCallback. + */ + public static function getLoopy(OutputInterface $output, SymfonyStyle $io, string $spinnerMessage, callable $statusCallback, callable $doneCallback): void + { + $timers = []; + $spinner = new Spinner($output, 4); + $spinner->setMessage($spinnerMessage); + $spinner->start(); + + $cancelTimers = static function () use (&$timers, $spinner): void { + // @infection-ignore-all + array_map('\React\EventLoop\Loop::cancelTimer', $timers); + $timers = []; + $spinner->finish(); + }; + $periodicCallback = static function () use ($statusCallback, $doneCallback, $cancelTimers): void { + // @infection-ignore-all + if ($statusCallback()) { + $cancelTimers(); + $doneCallback(); + } + }; + + // Spinner timer. + $timers[] = Loop::addPeriodicTimer( + $spinner->interval(), + static function () use ($spinner): void { + $spinner->advance(); + } + ); + + // Primary timer checking for result status. + $timers[] = Loop::addPeriodicTimer(5, $periodicCallback); + // Initial timer to speed up tests. + $timers[] = Loop::addTimer(0.1, $periodicCallback); + + // Watchdog timer. + $timers[] = Loop::addTimer(45 * 60, static function () use ($io, $doneCallback, $cancelTimers): void { + $cancelTimers(); + $io->error("Timed out after 45 minutes!"); + $doneCallback(); + }); + + // Manually run the loop. React EventLoop advises against this and suggests + // using autorun instead, but I'm not sure how to pass the correct exit code + // to Symfony if this isn't blocking. + Loop::run(); + } } diff --git a/src/Helpers/SshCommandTrait.php b/src/Helpers/SshCommandTrait.php index 255a31968..98f7bbdbb 100644 --- a/src/Helpers/SshCommandTrait.php +++ b/src/Helpers/SshCommandTrait.php @@ -1,6 +1,6 @@ cloudApiClientService->getClient(); + if (!$cloudKey) { + $cloudKey = $this->determineCloudKey($acquiaCloudClient); + } - private function deleteSshKeyFromCloud(mixed $output, mixed $cloudKey = NULL): int { - $acquiaCloudClient = $this->cloudApiClientService->getClient(); - if (!$cloudKey) { - $cloudKey = $this->determineCloudKey($acquiaCloudClient); + $sshKeys = new SshKeys($acquiaCloudClient); + $sshKeys->delete($cloudKey->uuid); + $output->writeln("Successfully deleted SSH key $cloudKey->label from the Cloud Platform."); + $localKeys = $this->findLocalSshKeys(); + foreach ($localKeys as $localFile) { + if (trim($localFile->getContents()) === trim($cloudKey->public_key) && $localFile->getRealPath()) { + $privateKeyPath = str_replace('.pub', '', $localFile->getRealPath()); + $publicKeyPath = $localFile->getRealPath(); + $answer = $this->io->confirm("Do you also want to delete the corresponding local key files {$localFile->getRealPath()} and $privateKeyPath ?", false); + if ($answer) { + $this->localMachineHelper->getFilesystem()->remove([ + $localFile->getRealPath(), + $privateKeyPath, + ]); + $this->io->success("Deleted $publicKeyPath and $privateKeyPath"); + return 0; + } + } + } + return 0; } - $sshKeys = new SshKeys($acquiaCloudClient); - $sshKeys->delete($cloudKey->uuid); - $output->writeln("Successfully deleted SSH key $cloudKey->label from the Cloud Platform."); - $localKeys = $this->findLocalSshKeys(); - foreach ($localKeys as $localFile) { - if (trim($localFile->getContents()) === trim($cloudKey->public_key) && $localFile->getRealPath()) { - $privateKeyPath = str_replace('.pub', '', $localFile->getRealPath()); - $publicKeyPath = $localFile->getRealPath(); - $answer = $this->io->confirm("Do you also want to delete the corresponding local key files {$localFile->getRealPath()} and $privateKeyPath ?", FALSE); - if ($answer) { - $this->localMachineHelper->getFilesystem()->remove([ - $localFile->getRealPath(), - $privateKeyPath, - ]); - $this->io->success("Deleted $publicKeyPath and $privateKeyPath"); - return 0; + private function determineCloudKey(Client $acquiaCloudClient): object|array|null + { + $sshKeys = new SshKeys($acquiaCloudClient); + $cloudKeys = $sshKeys->getAll(); + if (!$cloudKeys->count()) { + throw new AcquiaCliException('There are no SSH keys associated with your account.'); } - } + return $this->promptChooseFromObjectsOrArrays( + $cloudKeys, + 'uuid', + 'label', + 'Choose an SSH key to delete from the Cloud Platform' + ); } - return 0; - } - private function determineCloudKey(Client $acquiaCloudClient): object|array|null { - $sshKeys = new SshKeys($acquiaCloudClient); - $cloudKeys = $sshKeys->getAll(); - if (!$cloudKeys->count()) { - throw new AcquiaCliException('There are no SSH keys associated with your account.'); + /** + * @return \Symfony\Component\Finder\SplFileInfo[] + */ + protected function findLocalSshKeys(): array + { + $finder = $this->localMachineHelper->getFinder(); + $finder->files()->in($this->sshDir)->name('*.pub')->ignoreUnreadableDirs(); + return iterator_to_array($finder); } - return $this->promptChooseFromObjectsOrArrays( - $cloudKeys, - 'uuid', - 'label', - 'Choose an SSH key to delete from the Cloud Platform' - ); - } - - /** - * @return \Symfony\Component\Finder\SplFileInfo[] - */ - protected function findLocalSshKeys(): array { - $finder = $this->localMachineHelper->getFinder(); - $finder->files()->in($this->sshDir)->name('*.pub')->ignoreUnreadableDirs(); - return iterator_to_array($finder); - } - - protected function promptWaitForSsh(SymfonyStyle $io): bool { - $io->note("It may take an hour or more before the SSH key is installed on all of your application's servers. Create a Support ticket for further assistance."); - $wait = $io->confirm("Would you like to wait until your key is installed on all of your application's servers?"); - Amplitude::getInstance()->queueEvent('User waited for SSH key upload', ['wait' => $wait]); - return $wait; - } + protected function promptWaitForSsh(SymfonyStyle $io): bool + { + $io->note("It may take an hour or more before the SSH key is installed on all of your application's servers. Create a Support ticket for further assistance."); + $wait = $io->confirm("Would you like to wait until your key is installed on all of your application's servers?"); + Amplitude::getInstance()->queueEvent('User waited for SSH key upload', ['wait' => $wait]); + return $wait; + } } diff --git a/src/Helpers/SshHelper.php b/src/Helpers/SshHelper.php index 9d2b61934..a62582448 100644 --- a/src/Helpers/SshHelper.php +++ b/src/Helpers/SshHelper.php @@ -1,6 +1,6 @@ setLogger($logger); - } - - /** - * Execute the command in a remote environment. - * - * @param int|null $timeout - */ - public function executeCommand(string $sshUrl, array $commandArgs, bool $printOutput = TRUE, int $timeout = NULL): Process { - $commandSummary = $this->getCommandSummary($commandArgs); - - // Remove site_env arg. - unset($commandArgs['alias']); - $process = $this->sendCommand($sshUrl, $commandArgs, $printOutput, $timeout); - - $this->logger->debug('Command: {command} [Exit: {exit}]', [ - 'command' => $commandSummary, - 'env' => $sshUrl, - 'exit' => $process->getExitCode(), - ]); - - if (!$process->isSuccessful() && $process->getExitCode() === 255) { - throw new AcquiaCliException($process->getOutput() . $process->getErrorOutput()); +class SshHelper implements LoggerAwareInterface +{ + use LoggerAwareTrait; + + /** + * SshHelper constructor. + */ + public function __construct( + private readonly OutputInterface $output, + private readonly LocalMachineHelper $localMachineHelper, + LoggerInterface $logger + ) { + $this->setLogger($logger); } - return $process; - } + /** + * Execute the command in a remote environment. + * + * @param int|null $timeout + */ + public function executeCommand(string $sshUrl, array $commandArgs, bool $printOutput = true, int $timeout = null): Process + { + $commandSummary = $this->getCommandSummary($commandArgs); + + // Remove site_env arg. + unset($commandArgs['alias']); + $process = $this->sendCommand($sshUrl, $commandArgs, $printOutput, $timeout); + + $this->logger->debug('Command: {command} [Exit: {exit}]', [ + 'command' => $commandSummary, + 'env' => $sshUrl, + 'exit' => $process->getExitCode(), + ]); + + if (!$process->isSuccessful() && $process->getExitCode() === 255) { + throw new AcquiaCliException($process->getOutput() . $process->getErrorOutput()); + } + + return $process; + } + + private function sendCommand(string $url, array $command, bool $printOutput, ?int $timeout = null): Process + { + $command = array_values($this->getSshCommand($url, $command)); + $this->localMachineHelper->checkRequiredBinariesExist(['ssh']); - private function sendCommand(string $url, array $command, bool $printOutput, ?int $timeout = NULL): Process { - $command = array_values($this->getSshCommand($url, $command)); - $this->localMachineHelper->checkRequiredBinariesExist(['ssh']); + return $this->localMachineHelper->execute($command, $this->getOutputCallback(), null, $printOutput, $timeout); + } - return $this->localMachineHelper->execute($command, $this->getOutputCallback(), NULL, $printOutput, $timeout); - } + /** + * Return the first item of the $commandArgs that is not an option. + */ + private function firstArguments(array $commandArgs): string + { + $result = ''; + while (!empty($commandArgs)) { + $first = array_shift($commandArgs); + if ($first !== '' && $first[0] === '-') { + return $result; + } + $result .= " $first"; + } - /** - * Return the first item of the $commandArgs that is not an option. - */ - private function firstArguments(array $commandArgs): string { - $result = ''; - while (!empty($commandArgs)) { - $first = array_shift($commandArgs); - if ($first !== '' && $first[0] === '-') { return $result; - } - $result .= " $first"; } - return $result; - } + private function getOutputCallback(): callable + { + if ($this->localMachineHelper->useTty() === false) { + $output = $this->output; - private function getOutputCallback(): callable { - if ($this->localMachineHelper->useTty() === FALSE) { - $output = $this->output; + return static function (mixed $type, mixed $buffer) use ($output): void { + $output->write($buffer); + }; + } - return static function (mixed $type, mixed $buffer) use ($output): void { - $output->write($buffer); - }; + return static function (mixed $type, mixed $buffer): void { + }; } - return static function (mixed $type, mixed $buffer): void {}; - } - - /** - * Return a summary of the command that does not include the - * arguments. This avoids potential information disclosure in - * CI scripts. - */ - private function getCommandSummary(array $commandArgs): string { - return $this->firstArguments($commandArgs); - } - - /** - * @return array - */ - private function getConnectionArgs(string $url): array { - return [ - 'ssh', - $url, - '-t', - '-o StrictHostKeyChecking=no', - '-o AddressFamily inet', - '-o LogLevel=ERROR', - ]; - } - - /** - * @return array - */ - private function getSshCommand(string $url, array $command): array { - return array_merge($this->getConnectionArgs($url), $command); - } + /** + * Return a summary of the command that does not include the + * arguments. This avoids potential information disclosure in + * CI scripts. + */ + private function getCommandSummary(array $commandArgs): string + { + return $this->firstArguments($commandArgs); + } + /** + * @return array + */ + private function getConnectionArgs(string $url): array + { + return [ + 'ssh', + $url, + '-t', + '-o StrictHostKeyChecking=no', + '-o AddressFamily inet', + '-o LogLevel=ERROR', + ]; + } + + /** + * @return array + */ + private function getSshCommand(string $url, array $command): array + { + return array_merge($this->getConnectionArgs($url), $command); + } } diff --git a/src/Helpers/TelemetryHelper.php b/src/Helpers/TelemetryHelper.php index 141479c3d..aef7016e2 100644 --- a/src/Helpers/TelemetryHelper.php +++ b/src/Helpers/TelemetryHelper.php @@ -1,6 +1,6 @@ initializeAmplitude(); - $this->initializeBugsnag(); - } - - public function initializeBugsnag(): void { - if (empty($this->bugSnagKey)) { - return; +class TelemetryHelper +{ + public function __construct( + private readonly ClientService $cloudApiClientService, + private readonly CloudDataStore $datastoreCloud, + private readonly Application $application, + private readonly ?string $amplitudeKey = '', + private readonly ?string $bugSnagKey = '' + ) { } - $sendTelemetry = $this->datastoreCloud->get(DataStoreContract::SEND_TELEMETRY); - if ($sendTelemetry === FALSE) { - return; - } - // It's safe-ish to make this key public. - // @see https://github.com/bugsnag/bugsnag-js/issues/595 - $bugsnag = Client::make($this->bugSnagKey); - $bugsnag->setAppVersion($this->application->getVersion()); - $bugsnag->setProjectRoot(Path::join(__DIR__, '..')); - $bugsnag->registerCallback(function (Report $report): bool { - // Exclude errors that we can't control. - switch (TRUE) { - // Exclude reports from app:from, which bootstraps Drupal. - case str_starts_with($report->getContext(), 'GET'): - // Exclude memory exhaustion errors. - case str_starts_with($report->getContext(), 'Allowed memory size'): - // Exclude i/o errors. - case str_starts_with($report->getContext(), 'fgets'): - return FALSE; - } - // Set user info. - $userId = $this->getUserId(); - if (isset($userId)) { - $report->setUser([ - 'id' => $userId, - ]); - } - $context = $report->getContext(); - // Strip working directory and binary from context. - if (str_contains($context, 'acli ')) { - $context = substr($context, strpos($context, 'acli ') + 5); - } - // Strip sensitive parameters from context. - if (str_contains($context, "--password")) { - $context = substr($context, 0, strpos($context, "--password") + 10) . 'REDACTED'; - } - $report->setContext($context); - return TRUE; - }); - Handler::register($bugsnag); - } - /** - * Initializes Amplitude. - */ - public function initializeAmplitude(): void { - if (empty($this->amplitudeKey)) { - return; + public function initialize(): void + { + $this->initializeAmplitude(); + $this->initializeBugsnag(); } - $sendTelemetry = $this->datastoreCloud->get(DataStoreContract::SEND_TELEMETRY); - $amplitude = Amplitude::getInstance(); - $amplitude->setOptOut($sendTelemetry === FALSE); - if ($sendTelemetry === FALSE) { - return; - } - try { - $amplitude->init($this->amplitudeKey); - // Method chaining breaks Prophecy? - // @see https://github.com/phpspec/prophecy/issues/25 - $amplitude->setDeviceId(OsInfo::uuid()); - $amplitude->setUserProperties($this->getTelemetryUserData()); - $amplitude->setUserId($this->getUserId()); - $amplitude->logQueuedEvents(); + public function initializeBugsnag(): void + { + if (empty($this->bugSnagKey)) { + return; + } + $sendTelemetry = $this->datastoreCloud->get(DataStoreContract::SEND_TELEMETRY); + if ($sendTelemetry === false) { + return; + } + // It's safe-ish to make this key public. + // @see https://github.com/bugsnag/bugsnag-js/issues/595 + $bugsnag = Client::make($this->bugSnagKey); + $bugsnag->setAppVersion($this->application->getVersion()); + $bugsnag->setProjectRoot(Path::join(__DIR__, '..')); + $bugsnag->registerCallback(function (Report $report): bool { + // Exclude errors that we can't control. + switch (true) { + // Exclude reports from app:from, which bootstraps Drupal. + case str_starts_with($report->getContext(), 'GET'): + // Exclude memory exhaustion errors. + case str_starts_with($report->getContext(), 'Allowed memory size'): + // Exclude i/o errors. + case str_starts_with($report->getContext(), 'fgets'): + return false; + } + // Set user info. + $userId = $this->getUserId(); + if (isset($userId)) { + $report->setUser([ + 'id' => $userId, + ]); + } + $context = $report->getContext(); + // Strip working directory and binary from context. + if (str_contains($context, 'acli ')) { + $context = substr($context, strpos($context, 'acli ') + 5); + } + // Strip sensitive parameters from context. + if (str_contains($context, "--password")) { + $context = substr($context, 0, strpos($context, "--password") + 10) . 'REDACTED'; + } + $report->setContext($context); + return true; + }); + Handler::register($bugsnag); } - catch (IdentityProviderException $e) { - // If something is wrong with the Cloud API client, don't bother users. - } - } - /** - * @param string $ah_env - * Environment name from AH_ENV. - * @return string - * Normalized environment name. - */ - public static function normalizeAhEnv(string $ah_env): string { - if (AcquiaDrupalEnvironmentDetector::isAhProdEnv($ah_env)) { - return 'prod'; - } - if (AcquiaDrupalEnvironmentDetector::isAhStageEnv($ah_env)) { - return 'stage'; - } - if (AcquiaDrupalEnvironmentDetector::isAhDevEnv($ah_env)) { - return 'dev'; - } - if (AcquiaDrupalEnvironmentDetector::isAhOdeEnv($ah_env)) { - return 'ode'; - } - if (AcquiaDrupalEnvironmentDetector::isAhIdeEnv($ah_env)) { - return 'ide'; - } - return $ah_env; - } + /** + * Initializes Amplitude. + */ + public function initializeAmplitude(): void + { + if (empty($this->amplitudeKey)) { + return; + } + $sendTelemetry = $this->datastoreCloud->get(DataStoreContract::SEND_TELEMETRY); + $amplitude = Amplitude::getInstance(); + $amplitude->setOptOut($sendTelemetry === false); - /** - * Get telemetry user data. - * - * @return array Telemetry user data. - */ - private function getTelemetryUserData(): array { - $data = [ - 'ah_app_uuid' => getenv('AH_APPLICATION_UUID'), - 'ah_env' => $this->normalizeAhEnv(AcquiaDrupalEnvironmentDetector::getAhEnv()), - 'ah_group' => AcquiaDrupalEnvironmentDetector::getAhGroup(), - 'ah_non_production' => getenv('AH_NON_PRODUCTION'), - 'ah_realm' => getenv('AH_REALM'), - 'CI' => getenv('CI'), - 'env_provider' => $this->getEnvironmentProvider(), - 'php_version' => PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION, - ]; - try { - $user = $this->getUserData(); - if (isset($user['is_acquian'])) { - $data['is_acquian'] = $user['is_acquian']; - } - } - catch (IdentityProviderException $e) { - // If something is wrong with the Cloud API client, don't bother users. + if ($sendTelemetry === false) { + return; + } + try { + $amplitude->init($this->amplitudeKey); + // Method chaining breaks Prophecy? + // @see https://github.com/phpspec/prophecy/issues/25 + $amplitude->setDeviceId(OsInfo::uuid()); + $amplitude->setUserProperties($this->getTelemetryUserData()); + $amplitude->setUserId($this->getUserId()); + $amplitude->logQueuedEvents(); + } catch (IdentityProviderException $e) { + // If something is wrong with the Cloud API client, don't bother users. + } } - return $data; - } - public static function getEnvironmentProvider(): ?string { - $providers = self::getProviders(); + /** + * @param string $ah_env + * Environment name from AH_ENV. + * @return string + * Normalized environment name. + */ + public static function normalizeAhEnv(string $ah_env): string + { + if (AcquiaDrupalEnvironmentDetector::isAhProdEnv($ah_env)) { + return 'prod'; + } + if (AcquiaDrupalEnvironmentDetector::isAhStageEnv($ah_env)) { + return 'stage'; + } + if (AcquiaDrupalEnvironmentDetector::isAhDevEnv($ah_env)) { + return 'dev'; + } + if (AcquiaDrupalEnvironmentDetector::isAhOdeEnv($ah_env)) { + return 'ode'; + } + if (AcquiaDrupalEnvironmentDetector::isAhIdeEnv($ah_env)) { + return 'ide'; + } + return $ah_env; + } - // Check for environment variables. - foreach ($providers as $provider => $vars) { - foreach ($vars as $var) { - if (getenv($var) !== FALSE) - return $provider; - } + /** + * Get telemetry user data. + * + * @return array Telemetry user data. + */ + private function getTelemetryUserData(): array + { + $data = [ + 'ah_app_uuid' => getenv('AH_APPLICATION_UUID'), + 'ah_env' => $this->normalizeAhEnv(AcquiaDrupalEnvironmentDetector::getAhEnv()), + 'ah_group' => AcquiaDrupalEnvironmentDetector::getAhGroup(), + 'ah_non_production' => getenv('AH_NON_PRODUCTION'), + 'ah_realm' => getenv('AH_REALM'), + 'CI' => getenv('CI'), + 'env_provider' => $this->getEnvironmentProvider(), + 'php_version' => PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION, + ]; + try { + $user = $this->getUserData(); + if (isset($user['is_acquian'])) { + $data['is_acquian'] = $user['is_acquian']; + } + } catch (IdentityProviderException $e) { + // If something is wrong with the Cloud API client, don't bother users. + } + return $data; } - return NULL; - } + public static function getEnvironmentProvider(): ?string + { + $providers = self::getProviders(); + + // Check for environment variables. + foreach ($providers as $provider => $vars) { + foreach ($vars as $var) { + if (getenv($var) !== false) { + return $provider; + } + } + } - private function getUserId(): ?string { - $user = $this->getUserData(); - if ($user && isset($user['uuid'])) { - return $user['uuid']; + return null; } - return NULL; - } + private function getUserId(): ?string + { + $user = $this->getUserData(); + if ($user && isset($user['uuid'])) { + return $user['uuid']; + } - /** - * Get user data. - * - * @return array|null User account data from Cloud. - */ - private function getUserData(): ?array { - $user = $this->datastoreCloud->get(DataStoreContract::USER); - if (!$user && $this->cloudApiClientService->isMachineAuthenticated()) { - $this->setDefaultUserData(); - $user = $this->datastoreCloud->get(DataStoreContract::USER); + return null; } - return $user; - } + /** + * Get user data. + * + * @return array|null User account data from Cloud. + */ + private function getUserData(): ?array + { + $user = $this->datastoreCloud->get(DataStoreContract::USER); + if (!$user && $this->cloudApiClientService->isMachineAuthenticated()) { + $this->setDefaultUserData(); + $user = $this->datastoreCloud->get(DataStoreContract::USER); + } - /** - * This requires the machine to be authenticated. - */ - private function setDefaultUserData(): void { - $user = $this->getDefaultUserData(); - $this->datastoreCloud->set(DataStoreContract::USER, $user); - } + return $user; + } - /** - * This requires the machine to be authenticated. - * - * @return array - */ - private function getDefaultUserData(): array { - // @todo Cache this! - $account = new Account($this->cloudApiClientService->getClient()); - return [ - 'is_acquian' => str_ends_with($account->get()->mail, 'acquia.com'), - 'uuid' => $account->get()->uuid, - ]; - } + /** + * This requires the machine to be authenticated. + */ + private function setDefaultUserData(): void + { + $user = $this->getDefaultUserData(); + $this->datastoreCloud->set(DataStoreContract::USER, $user); + } - /** - * @infection-ignore-all - * Skipping infection testing for this because, it most cases, we expect that when a row from this array is changed - * it won't affect the return value. - * @return array - * An array of providers and their associated environment variables. - */ - public static function getProviders(): array { - // Define the environment variables associated with each provider. - // phpcs:ignore SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys.IncorrectKeyOrder - return [ - 'lando' => ['LANDO'], - 'ddev' => ['IS_DDEV_PROJECT'], - // Check Lando and DDEV first because the hijack AH_SITE_ENVIRONMENT. - 'acquia' => ['AH_SITE_ENVIRONMENT'], - 'bamboo' => ['BAMBOO_BUILDNUMBER'], - 'beanstalk' => ['BEANSTALK_ENVIRONMENT'], - 'bitbucket' => ['BITBUCKET_BUILD_NUMBER'], - 'bitrise' => ['BITRISE_IO'], - 'buddy' => ['BUDDY_WORKSPACE_ID'], - 'circleci' => ['CIRCLECI'], - 'codebuild' => ['CODEBUILD_BUILD_ID'], - 'docksal' => ['DOCKSAL_VERSION'], - 'drone' => ['DRONE'], - 'github' => ['GITHUB_ACTIONS'], - 'gitlab' => ['GITLAB_CI'], - 'heroku' => ['HEROKU_TEST_RUN_ID'], - 'jenkins' => ['JENKINS_URL'], - 'pantheon' => ['PANTHEON_ENVIRONMENT'], - 'pipelines' => ['PIPELINE_ENV'], - 'platformsh' => ['PLATFORM_ENVIRONMENT'], - 'teamcity' => ['TEAMCITY_VERSION'], - 'travis' => ['TRAVIS'], - ]; - } + /** + * This requires the machine to be authenticated. + * + * @return array + */ + private function getDefaultUserData(): array + { + // @todo Cache this! + $account = new Account($this->cloudApiClientService->getClient()); + return [ + 'is_acquian' => str_ends_with($account->get()->mail, 'acquia.com'), + 'uuid' => $account->get()->uuid, + ]; + } + /** + * @infection-ignore-all + * Skipping infection testing for this because, it most cases, we expect that when a row from this array is changed + * it won't affect the return value. + * @return array + * An array of providers and their associated environment variables. + */ + public static function getProviders(): array + { + // Define the environment variables associated with each provider. + // phpcs:ignore SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys.IncorrectKeyOrder + return [ + 'lando' => ['LANDO'], + 'ddev' => ['IS_DDEV_PROJECT'], + // Check Lando and DDEV first because the hijack AH_SITE_ENVIRONMENT. + 'acquia' => ['AH_SITE_ENVIRONMENT'], + 'bamboo' => ['BAMBOO_BUILDNUMBER'], + 'beanstalk' => ['BEANSTALK_ENVIRONMENT'], + 'bitbucket' => ['BITBUCKET_BUILD_NUMBER'], + 'bitrise' => ['BITRISE_IO'], + 'buddy' => ['BUDDY_WORKSPACE_ID'], + 'circleci' => ['CIRCLECI'], + 'codebuild' => ['CODEBUILD_BUILD_ID'], + 'docksal' => ['DOCKSAL_VERSION'], + 'drone' => ['DRONE'], + 'github' => ['GITHUB_ACTIONS'], + 'gitlab' => ['GITLAB_CI'], + 'heroku' => ['HEROKU_TEST_RUN_ID'], + 'jenkins' => ['JENKINS_URL'], + 'pantheon' => ['PANTHEON_ENVIRONMENT'], + 'pipelines' => ['PIPELINE_ENV'], + 'platformsh' => ['PLATFORM_ENVIRONMENT'], + 'teamcity' => ['TEAMCITY_VERSION'], + 'travis' => ['TRAVIS'], + ]; + } } diff --git a/src/Kernel.php b/src/Kernel.php index 88b6705f4..77970db95 100644 --- a/src/Kernel.php +++ b/src/Kernel.php @@ -1,6 +1,6 @@ - */ - public function registerBundles(): iterable { - return []; - } - - public function registerContainerConfiguration(LoaderInterface $loader): void { - $loader->load($this->getProjectDir() . '/config/' . $this->getEnvironment() . '/services.yml'); - $this->registerExtensionConfiguration($loader); - } - - /** @infection-ignore-all */ - public function getCacheDir(): string { - $testToken = getenv('TEST_TOKEN') ?? ''; - return parent::getCacheDir() . $testToken; - } - - protected function registerExtensionConfiguration(mixed $loader): void { - // Search for plugins. - $finder = new Finder(); - $extensions = $finder->files() - ->in([ - __DIR__ . '/../../', - ]) - ->depth(1) - ->name('acli.services.yml'); - foreach ($extensions as $extension) { - $loader->load($extension->getRealPath()); +class Kernel extends BaseKernel +{ + /** + * @return array + */ + public function registerBundles(): iterable + { + return []; } - } - - /** - * Returns a loader for the container. - * - * @return \Symfony\Component\Config\Loader\DelegatingLoader The loader - */ - protected function getContainerLoader(ContainerInterface $container): DelegatingLoader { - $locator = new FileLocator([$this->getProjectDir()]); - $resolver = new LoaderResolver([ - new YamlFileLoader($container, $locator), - new DirectoryLoader($container, $locator), - ]); - - return new DelegatingLoader($resolver); - } - - protected function build(ContainerBuilder $containerBuilder): void { - $containerBuilder->addCompilerPass($this->createCollectingCompilerPass()); - } - - /** - * Creates a collecting compiler pass. - */ - private function createCollectingCompilerPass(): CompilerPassInterface { - return new class implements CompilerPassInterface { - - public function process(ContainerBuilder $containerBuilder): void { - $appDefinition = $containerBuilder->findDefinition(Application::class); - $dispatcherDefinition = $containerBuilder->findDefinition(EventDispatcher::class); - - foreach ($containerBuilder->getDefinitions() as $definition) { - // Handle event listeners. - if ($definition->hasTag('kernel.event_listener')) { - foreach ($definition->getTag('kernel.event_listener') as $tag) { - $dispatcherDefinition->addMethodCall('addListener', [ - $tag['event'], - [ - new ServiceClosureArgument(new Reference($definition->getClass())), - $tag['method'], - ], - ]); - } - } - // Handle commands. - if (!is_a($definition->getClass(), Command::class, TRUE)) { - continue; - } + public function registerContainerConfiguration(LoaderInterface $loader): void + { + $loader->load($this->getProjectDir() . '/config/' . $this->getEnvironment() . '/services.yml'); + $this->registerExtensionConfiguration($loader); + } - // Without this, Symfony tries to instantiate our abstract base command. No bueno. - if ($definition->isAbstract()) { - continue; - } + /** @infection-ignore-all */ + public function getCacheDir(): string + { + $testToken = getenv('TEST_TOKEN') ?? ''; + return parent::getCacheDir() . $testToken; + } - $appDefinition->addMethodCall('add', [ - new Reference($definition->getClass()), - ]); + protected function registerExtensionConfiguration(mixed $loader): void + { + // Search for plugins. + $finder = new Finder(); + $extensions = $finder->files() + ->in([ + __DIR__ . '/../../', + ]) + ->depth(1) + ->name('acli.services.yml'); + foreach ($extensions as $extension) { + $loader->load($extension->getRealPath()); } + } - $appDefinition->addMethodCall('setDispatcher', [ - $dispatcherDefinition, + /** + * Returns a loader for the container. + * + * @return \Symfony\Component\Config\Loader\DelegatingLoader The loader + */ + protected function getContainerLoader(ContainerInterface $container): DelegatingLoader + { + $locator = new FileLocator([$this->getProjectDir()]); + $resolver = new LoaderResolver([ + new YamlFileLoader($container, $locator), + new DirectoryLoader($container, $locator), ]); - } - }; - } + return new DelegatingLoader($resolver); + } + + protected function build(ContainerBuilder $containerBuilder): void + { + $containerBuilder->addCompilerPass($this->createCollectingCompilerPass()); + } + /** + * Creates a collecting compiler pass. + */ + private function createCollectingCompilerPass(): CompilerPassInterface + { + return new class implements CompilerPassInterface { + public function process(ContainerBuilder $containerBuilder): void + { + $appDefinition = $containerBuilder->findDefinition(Application::class); + $dispatcherDefinition = $containerBuilder->findDefinition(EventDispatcher::class); + + foreach ($containerBuilder->getDefinitions() as $definition) { + // Handle event listeners. + if ($definition->hasTag('kernel.event_listener')) { + foreach ($definition->getTag('kernel.event_listener') as $tag) { + $dispatcherDefinition->addMethodCall('addListener', [ + $tag['event'], + [ + new ServiceClosureArgument(new Reference($definition->getClass())), + $tag['method'], + ], + ]); + } + } + + // Handle commands. + if (!is_a($definition->getClass(), Command::class, true)) { + continue; + } + + // Without this, Symfony tries to instantiate our abstract base command. No bueno. + if ($definition->isAbstract()) { + continue; + } + + $appDefinition->addMethodCall('add', [ + new Reference($definition->getClass()), + ]); + } + + $appDefinition->addMethodCall('setDispatcher', [ + $dispatcherDefinition, + ]); + } + }; + } } diff --git a/src/Output/Checklist.php b/src/Output/Checklist.php index b586c499e..c9c16ef83 100644 --- a/src/Output/Checklist.php +++ b/src/Output/Checklist.php @@ -1,6 +1,6 @@ + */ + private array $items = []; - /** - * @var array - */ - private array $items = []; + private int $indentLength = 4; - private int $indentLength = 4; + /** + * Checklist constructor. + */ + public function __construct(private OutputInterface $output) + { + } - /** - * Checklist constructor. - */ - public function __construct(private OutputInterface $output) { - } + public function addItem(string $message): void + { + $item = ['message' => $message]; - public function addItem(string $message): void { - $item = ['message' => $message]; + if ($this->useSpinner()) { + $spinner = new Spinner($this->output, $this->indentLength); + $spinner->setMessage($message . '...'); + $spinner->start(); + $item['spinner'] = $spinner; + } - if ($this->useSpinner()) { - $spinner = new Spinner($this->output, $this->indentLength); - $spinner->setMessage($message . '...'); - $spinner->start(); - $item['spinner'] = $spinner; + $this->items[] = $item; } - $this->items[] = $item; - } - - public function completePreviousItem(): void { - if ($this->useSpinner()) { - $item = $this->getLastItem(); - /** @var \Acquia\Cli\Output\Spinner\Spinner $spinner */ - $spinner = $item['spinner']; - $spinner->setMessage('', 'detail'); - $spinner->setMessage($item['message']); - $spinner->advance(); - $spinner->finish(); + public function completePreviousItem(): void + { + if ($this->useSpinner()) { + $item = $this->getLastItem(); + /** @var \Acquia\Cli\Output\Spinner\Spinner $spinner */ + $spinner = $item['spinner']; + $spinner->setMessage('', 'detail'); + $spinner->setMessage($item['message']); + $spinner->advance(); + $spinner->finish(); + } } - } - private function getLastItem(): mixed { - return end($this->items); - } - - public function updateProgressBar(string $updateMessage): void { - $item = $this->getLastItem(); - if (!$item) { - return; - } - if ($this->useSpinner()) { - /** @var \Acquia\Cli\Output\Spinner\Spinner $spinner */ - $spinner = $item['spinner']; + private function getLastItem(): mixed + { + return end($this->items); } - $messageLines = explode(PHP_EOL, $updateMessage); - foreach ($messageLines as $line) { - if (isset($spinner) && $item['spinner']) { - if (trim($line)) { - $spinner->setMessage(str_repeat(' ', $this->indentLength * 2) . $line, 'detail'); + public function updateProgressBar(string $updateMessage): void + { + $item = $this->getLastItem(); + if (!$item) { + return; + } + if ($this->useSpinner()) { + /** @var \Acquia\Cli\Output\Spinner\Spinner $spinner */ + $spinner = $item['spinner']; } - $spinner->advance(); - } - } - // Ensure that the new message is displayed at least once. Sometimes it is - // not displayed if the minimum redraw frequency is not met. - if (isset($spinner) && $item['spinner']) { - $spinner->getProgressBar()->display(); - } - } - private function useSpinner(): bool { - return $this->output instanceof ConsoleOutput - && (getenv('CI') !== 'true' || getenv('PHPUNIT_RUNNING')); - } + $messageLines = explode(PHP_EOL, $updateMessage); + foreach ($messageLines as $line) { + if (isset($spinner) && $item['spinner']) { + if (trim($line)) { + $spinner->setMessage(str_repeat(' ', $this->indentLength * 2) . $line, 'detail'); + } + $spinner->advance(); + } + } + // Ensure that the new message is displayed at least once. Sometimes it is + // not displayed if the minimum redraw frequency is not met. + if (isset($spinner) && $item['spinner']) { + $spinner->getProgressBar()->display(); + } + } - /** - * @return array - */ - public function getItems(): array { - return $this->items; - } + private function useSpinner(): bool + { + return $this->output instanceof ConsoleOutput + && (getenv('CI') !== 'true' || getenv('PHPUNIT_RUNNING')); + } + /** + * @return array + */ + public function getItems(): array + { + return $this->items; + } } diff --git a/src/Output/Spinner/Spinner.php b/src/Output/Spinner/Spinner.php index e9b8671f0..b3ab9ccb6 100644 --- a/src/Output/Spinner/Spinner.php +++ b/src/Output/Spinner/Spinner.php @@ -1,6 +1,6 @@ spinnerIsSupported()) { - return; + if (!$this->spinnerIsSupported()) { + return; + } + $this->section = $output->section(); + $this->colorCount = count(self::COLORS); + + // Create progress bar. + $this->progressBar = new ProgressBar($this->section); + $this->progressBar->setBarCharacter(''); + $this->progressBar->setProgressCharacter('⌛'); + $this->progressBar->setEmptyBarCharacter('⌛'); + $this->progressBar->setFormat($indentString . "%bar% %message%\n%detail%"); + $this->progressBar->setBarWidth(1); + $this->progressBar->setMessage('', 'detail'); + $this->progressBar->setOverwrite($output->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE); } - $this->section = $output->section(); - $this->colorCount = count(self::COLORS); - - // Create progress bar. - $this->progressBar = new ProgressBar($this->section); - $this->progressBar->setBarCharacter(''); - $this->progressBar->setProgressCharacter('⌛'); - $this->progressBar->setEmptyBarCharacter('⌛'); - $this->progressBar->setFormat($indentString . "%bar% %message%\n%detail%"); - $this->progressBar->setBarWidth(1); - $this->progressBar->setMessage('', 'detail'); - $this->progressBar->setOverwrite($output->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE); - } - - public function start(): void { - if (!$this->spinnerIsSupported()) { - return; + + public function start(): void + { + if (!$this->spinnerIsSupported()) { + return; + } + $this->progressBar->start(); } - $this->progressBar->start(); - } - public function advance(): void { - if (!$this->spinnerIsSupported() || $this->progressBar->getProgressPercent() === 1.0) { - return; + public function advance(): void + { + if (!$this->spinnerIsSupported() || $this->progressBar->getProgressPercent() === 1.0) { + return; + } + + ++$this->currentCharIdx; + ++$this->currentColorIdx; + $char = $this->getSpinnerCharacter(); + $this->progressBar->setProgressCharacter($char); + $this->progressBar->advance(); } - ++$this->currentCharIdx; - ++$this->currentColorIdx; - $char = $this->getSpinnerCharacter(); - $this->progressBar->setProgressCharacter($char); - $this->progressBar->advance(); - } + private function getSpinnerCharacter(): string + { + if ($this->currentColorIdx === $this->colorCount) { + $this->currentColorIdx = 0; + } + $char = self::CHARS[$this->currentCharIdx % 8]; + $color = self::COLORS[$this->currentColorIdx]; + return "\033[38;5;{$color}m$char\033[0m"; + } - private function getSpinnerCharacter(): string { - if ($this->currentColorIdx === $this->colorCount) { - $this->currentColorIdx = 0; + public function setMessage(string $message, string $name = 'message'): void + { + if (!$this->spinnerIsSupported()) { + return; + } + if ($name === 'detail') { + $terminalWidth = (new Terminal())->getWidth(); + $messageLength = Helper::length($message) + ($this->indentLength * 2); + if ($messageLength > $terminalWidth) { + $suffix = '...'; + $newMessageLen = ($terminalWidth - ($this->indentLength * 2) - strlen($suffix)); + $message = Helper::substr($message, 0, $newMessageLen); + $message .= $suffix; + } + } + $this->progressBar->setMessage($message, $name); } - $char = self::CHARS[$this->currentCharIdx % 8]; - $color = self::COLORS[$this->currentColorIdx]; - return "\033[38;5;{$color}m$char\033[0m"; - } - - public function setMessage(string $message, string $name = 'message'): void { - if (!$this->spinnerIsSupported()) { - return; + + public function finish(): void + { + if (!$this->spinnerIsSupported()) { + return; + } + $this->progressBar->finish(); + // Clear the %detail% line. + $this->section->clear(1); } - if ($name === 'detail') { - $terminalWidth = (new Terminal())->getWidth(); - $messageLength = Helper::length($message) + ($this->indentLength * 2); - if ($messageLength > $terminalWidth) { - $suffix = '...'; - $newMessageLen = ($terminalWidth - ($this->indentLength * 2) - strlen($suffix)); - $message = Helper::substr($message, 0, $newMessageLen); - $message .= $suffix; - } + + public function fail(): void + { + if (!$this->spinnerIsSupported()) { + return; + } + $this->progressBar->finish(); + // Clear the %detail% line. + $this->section->clear(1); } - $this->progressBar->setMessage($message, $name); - } - public function finish(): void { - if (!$this->spinnerIsSupported()) { - return; + /** + * Returns spinner refresh interval. + */ + public function interval(): float + { + return 0.1; } - $this->progressBar->finish(); - // Clear the %detail% line. - $this->section->clear(1); - } - - public function fail(): void { - if (!$this->spinnerIsSupported()) { - return; + + private function spinnerIsSupported(): bool + { + return $this->output instanceof ConsoleOutput + && (getenv('CI') !== 'true' || getenv('PHPUNIT_RUNNING')); } - $this->progressBar->finish(); - // Clear the %detail% line. - $this->section->clear(1); - } - - /** - * Returns spinner refresh interval. - */ - public function interval(): float { - return 0.1; - } - - private function spinnerIsSupported(): bool { - return $this->output instanceof ConsoleOutput - && (getenv('CI') !== 'true' || getenv('PHPUNIT_RUNNING')); - } - - public function getProgressBar(): ProgressBar { - return $this->progressBar; - } + public function getProgressBar(): ProgressBar + { + return $this->progressBar; + } } diff --git a/tests/phpunit/src/AcsfApi/AcsfServiceTest.php b/tests/phpunit/src/AcsfApi/AcsfServiceTest.php index 7466f875e..4e381ef6b 100644 --- a/tests/phpunit/src/AcsfApi/AcsfServiceTest.php +++ b/tests/phpunit/src/AcsfApi/AcsfServiceTest.php @@ -1,6 +1,6 @@ cloudCredentials = new AcsfCredentials($this->datastoreCloud); - - } - - /** - * @return array - */ - public function providerTestIsMachineAuthenticated(): array { - return [ - [ +class AcsfServiceTest extends TestBase +{ + protected function setUp(): void + { + parent::setUp(); + $this->cloudCredentials = new AcsfCredentials($this->datastoreCloud); + } + + /** + * @return array + */ + public function providerTestIsMachineAuthenticated(): array + { + return [ + [ ['ACSF_USERNAME' => 'key', 'ACSF_KEY' => 'secret'], - TRUE, - ], - [ + true, + ], + [ ['ACSF_USERNAME' => 'key', 'ACSF_KEY' => 'secret'], - TRUE, - ], - [ - ['ACSF_USERNAME' => NULL, 'ACSF_KEY' => NULL], - FALSE, - ], - [ - ['ACSF_USERNAME' => 'key', 'ACSF_KEY' => NULL], - FALSE, - ], - ]; - } - - /** - * @dataProvider providerTestIsMachineAuthenticated - */ - public function testIsMachineAuthenticated(array $envVars, bool $isAuthenticated): void { - self::setEnvVars($envVars); - $clientService = new AcsfClientService(new AcsfConnectorFactory(['key' => NULL, 'secret' => NULL]), $this->prophet->prophesize(Application::class)->reveal(), $this->cloudCredentials); - $this->assertEquals($isAuthenticated, $clientService->isMachineAuthenticated()); - self::unsetEnvVars($envVars); - } - + true, + ], + [ + ['ACSF_USERNAME' => null, 'ACSF_KEY' => null], + false, + ], + [ + ['ACSF_USERNAME' => 'key', 'ACSF_KEY' => null], + false, + ], + ]; + } + + /** + * @dataProvider providerTestIsMachineAuthenticated + */ + public function testIsMachineAuthenticated(array $envVars, bool $isAuthenticated): void + { + self::setEnvVars($envVars); + $clientService = new AcsfClientService(new AcsfConnectorFactory(['key' => null, 'secret' => null]), $this->prophet->prophesize(Application::class)->reveal(), $this->cloudCredentials); + $this->assertEquals($isAuthenticated, $clientService->isMachineAuthenticated()); + self::unsetEnvVars($envVars); + } } diff --git a/tests/phpunit/src/AcsfApi/EnvVarAcsfAuthenticationTest.php b/tests/phpunit/src/AcsfApi/EnvVarAcsfAuthenticationTest.php index e2755f49d..622fb26d1 100644 --- a/tests/phpunit/src/AcsfApi/EnvVarAcsfAuthenticationTest.php +++ b/tests/phpunit/src/AcsfApi/EnvVarAcsfAuthenticationTest.php @@ -1,35 +1,37 @@ cloudCredentials = new AcsfCredentials($this->datastoreCloud); - putenv('ACSF_USERNAME=' . $this->key); - putenv('ACSF_KEY=' . $this->secret); - putenv('ACSF_FACTORY_URI=' . $this->acsfCurrentFactoryUrl); - } - - protected function tearDown(): void { - parent::tearDown(); - putenv('ACSF_USERNAME'); - putenv('ACSF_KEY'); - } - - public function testKeyAndSecret(): void { - $this->removeMockCloudConfigFile(); - self::assertEquals($this->key, $this->cloudCredentials->getCloudKey()); - self::assertEquals($this->secret, $this->cloudCredentials->getCloudSecret()); - self::assertEquals($this->acsfCurrentFactoryUrl, $this->cloudCredentials->getBaseUri()); - } - +class EnvVarAcsfAuthenticationTest extends TestBase +{ + private string $acsfCurrentFactoryUrl = 'https://www.test-something.com'; + + public function setUp(mixed $output = null): void + { + parent::setUp(); + $this->cloudCredentials = new AcsfCredentials($this->datastoreCloud); + putenv('ACSF_USERNAME=' . $this->key); + putenv('ACSF_KEY=' . $this->secret); + putenv('ACSF_FACTORY_URI=' . $this->acsfCurrentFactoryUrl); + } + + protected function tearDown(): void + { + parent::tearDown(); + putenv('ACSF_USERNAME'); + putenv('ACSF_KEY'); + } + + public function testKeyAndSecret(): void + { + $this->removeMockCloudConfigFile(); + self::assertEquals($this->key, $this->cloudCredentials->getCloudKey()); + self::assertEquals($this->secret, $this->cloudCredentials->getCloudSecret()); + self::assertEquals($this->acsfCurrentFactoryUrl, $this->cloudCredentials->getBaseUri()); + } } diff --git a/tests/phpunit/src/Application/ComposerScriptsListenerTest.php b/tests/phpunit/src/Application/ComposerScriptsListenerTest.php index f330bc415..22acc722a 100644 --- a/tests/phpunit/src/Application/ComposerScriptsListenerTest.php +++ b/tests/phpunit/src/Application/ComposerScriptsListenerTest.php @@ -1,6 +1,6 @@ [ +class ComposerScriptsListenerTest extends ApplicationTestBase +{ + /** + * @group serial + */ + public function testPreScripts(): void + { + $json = [ + 'scripts' => [ 'pre-acli-hello-world' => [ - 'echo "good morning world"', + 'echo "good morning world"', + ], ], - ], - ]; - file_put_contents( - Path::join($this->projectDir, 'composer.json'), - json_encode($json, JSON_THROW_ON_ERROR) - ); - $this->mockRequest('getAccount'); - $this->setInput([ - 'command' => 'hello-world', - ]); - $buffer = $this->runApp(); - self::assertStringContainsString('pre-acli-hello-world', $buffer); - } + ]; + file_put_contents( + Path::join($this->projectDir, 'composer.json'), + json_encode($json, JSON_THROW_ON_ERROR) + ); + $this->mockRequest('getAccount'); + $this->setInput([ + 'command' => 'hello-world', + ]); + $buffer = $this->runApp(); + self::assertStringContainsString('pre-acli-hello-world', $buffer); + } - /** - * @group serial - */ - public function testPostScripts(): void { - $json = [ - 'scripts' => [ + /** + * @group serial + */ + public function testPostScripts(): void + { + $json = [ + 'scripts' => [ 'post-acli-hello-world' => [ - 'echo "goodbye world"', + 'echo "goodbye world"', + ], ], - ], - ]; - file_put_contents( - Path::join($this->projectDir, 'composer.json'), - json_encode($json, JSON_THROW_ON_ERROR) - ); - $this->mockRequest('getAccount'); - $this->setInput([ - 'command' => 'hello-world', - ]); - $buffer = $this->runApp(); - self::assertStringContainsString('post-acli-hello-world', $buffer); - } + ]; + file_put_contents( + Path::join($this->projectDir, 'composer.json'), + json_encode($json, JSON_THROW_ON_ERROR) + ); + $this->mockRequest('getAccount'); + $this->setInput([ + 'command' => 'hello-world', + ]); + $buffer = $this->runApp(); + self::assertStringContainsString('post-acli-hello-world', $buffer); + } - public function testNoScripts(): void { - $json = [ - 'scripts' => [ + public function testNoScripts(): void + { + $json = [ + 'scripts' => [ 'pre-acli-pull-code' => [ - 'echo "goodbye world"', + 'echo "goodbye world"', ], - ], - ]; - file_put_contents( - Path::join($this->projectDir, 'composer.json'), - json_encode($json, JSON_THROW_ON_ERROR) - ); - $this->setInput([ - '--no-scripts' => TRUE, - 'command' => 'pull:code', - ]); - $buffer = $this->runApp(); - self::assertStringNotContainsString('pre-acli-pull-code', $buffer); - } - - // Hack to ensure listener methods are recognized as used. - // If we were unit testing properly, we'd make meaningful assertions here instead of the integration tests above. - public function testApi(): void { - $listener = new ComposerScriptsListener(); - $listener->onConsoleCommand(new ConsoleCommandEvent($this->injectCommand(HelloWorldCommand::class), $this->input, $this->output)); - $listener->onConsoleTerminate(new ConsoleTerminateEvent($this->injectCommand(HelloWorldCommand::class), $this->input, $this->output, 0)); - self::assertTrue(TRUE); - } + ], + ]; + file_put_contents( + Path::join($this->projectDir, 'composer.json'), + json_encode($json, JSON_THROW_ON_ERROR) + ); + $this->setInput([ + '--no-scripts' => true, + 'command' => 'pull:code', + ]); + $buffer = $this->runApp(); + self::assertStringNotContainsString('pre-acli-pull-code', $buffer); + } + // Hack to ensure listener methods are recognized as used. + // If we were unit testing properly, we'd make meaningful assertions here instead of the integration tests above. + public function testApi(): void + { + $listener = new ComposerScriptsListener(); + $listener->onConsoleCommand(new ConsoleCommandEvent($this->injectCommand(HelloWorldCommand::class), $this->input, $this->output)); + $listener->onConsoleTerminate(new ConsoleTerminateEvent($this->injectCommand(HelloWorldCommand::class), $this->input, $this->output, 0)); + self::assertTrue(true); + } } diff --git a/tests/phpunit/src/Application/ExceptionApplicationTest.php b/tests/phpunit/src/Application/ExceptionApplicationTest.php index 6b6229481..69300a58a 100644 --- a/tests/phpunit/src/Application/ExceptionApplicationTest.php +++ b/tests/phpunit/src/Application/ExceptionApplicationTest.php @@ -1,6 +1,6 @@ setInput([ - 'applicationUuid' => '2ed281d4-9dec-4cc3-ac63-691c3ba002c2', - 'command' => 'aliases', - ]); - $this->mockUnauthorizedRequest(); - $buffer = $this->runApp(); - // This is sensitive to the display width of the test environment, so that's fun. - self::assertStringContainsString('Your Cloud Platform API credentials are invalid.', $buffer); - } - +class ExceptionApplicationTest extends ApplicationTestBase +{ + /** + * @group serial + */ + public function testInvalidApiCredentials(): void + { + $this->setInput([ + 'applicationUuid' => '2ed281d4-9dec-4cc3-ac63-691c3ba002c2', + 'command' => 'aliases', + ]); + $this->mockUnauthorizedRequest(); + $buffer = $this->runApp(); + // This is sensitive to the display width of the test environment, so that's fun. + self::assertStringContainsString('Your Cloud Platform API credentials are invalid.', $buffer); + } } diff --git a/tests/phpunit/src/Application/HelpApplicationTest.php b/tests/phpunit/src/Application/HelpApplicationTest.php index ffdec56b4..f40dec53f 100644 --- a/tests/phpunit/src/Application/HelpApplicationTest.php +++ b/tests/phpunit/src/Application/HelpApplicationTest.php @@ -1,6 +1,6 @@ setInput([ - 'command' => 'help', - 'command_name' => 'app:link', - ]); - $buffer = $this->runApp(); - $this->assertStringContainsString('The Cloud Platform application UUID or alias (i.e. an application name optionally prefixed with the realm)', $buffer); - $this->assertStringContainsString('Usage: +class HelpApplicationTest extends ApplicationTestBase +{ + /** + * @group serial + */ + public function testApplicationAliasHelp(): void + { + $this->setInput([ + 'command' => 'help', + 'command_name' => 'app:link', + ]); + $buffer = $this->runApp(); + $this->assertStringContainsString('The Cloud Platform application UUID or alias (i.e. an application name optionally prefixed with the realm)', $buffer); + $this->assertStringContainsString('Usage: app:link [] link app:link [] app:link myapp app:link prod:myapp app:link abcd1234-1111-2222-3333-0e02b2c3d470', $buffer); - } + } - /** - * @group serial - */ - public function testEnvironmentAliasHelp(): void { - $this->setInput([ - 'command' => 'help', - 'command_name' => 'log:tail', - ]); - $buffer = $this->runApp(); - $this->assertStringContainsString('The Cloud Platform environment ID or alias (i.e. an application and environment name optionally prefixed with the realm)', $buffer); - $this->assertStringContainsString('Usage: + /** + * @group serial + */ + public function testEnvironmentAliasHelp(): void + { + $this->setInput([ + 'command' => 'help', + 'command_name' => 'log:tail', + ]); + $buffer = $this->runApp(); + $this->assertStringContainsString('The Cloud Platform environment ID or alias (i.e. an application and environment name optionally prefixed with the realm)', $buffer); + $this->assertStringContainsString('Usage: app:log:tail [] tail log:tail @@ -52,6 +54,5 @@ public function testEnvironmentAliasHelp(): void { app:log:tail myapp.dev app:log:tail prod:myapp.dev app:log:tail 12345-abcd1234-1111-2222-3333-0e02b2c3d470', $buffer); - } - + } } diff --git a/tests/phpunit/src/Application/KernelTest.php b/tests/phpunit/src/Application/KernelTest.php index 2b458cd4a..3963f24f5 100644 --- a/tests/phpunit/src/Application/KernelTest.php +++ b/tests/phpunit/src/Application/KernelTest.php @@ -1,29 +1,31 @@ setInput([ + 'command' => 'list', + ]); + $buffer = $this->runApp(); + // A bit dumb that we need to break these up, but the available commands vary based on whether a browser is available or the session is interactive. + // Could probably handle that more intelligently... + $this->assertStringStartsWith($this->getStart(), $buffer); + $this->assertStringEndsWith($this->getEnd(), $buffer); + } - /** - * @group serial - */ - public function testRun(): void { - $this->setInput([ - 'command' => 'list', - ]); - $buffer = $this->runApp(); - // A bit dumb that we need to break these up, but the available commands vary based on whether a browser is available or the session is interactive. - // Could probably handle that more intelligently... - $this->assertStringStartsWith($this->getStart(), $buffer); - $this->assertStringEndsWith($this->getEnd(), $buffer); - } - - private function getStart(): string { - return <<kernel = new Kernel('dev', FALSE); - $this->kernel->boot(); - $this->kernel->getContainer()->set(CloudDataStore::class, $this->datastoreCloud); - $this->kernel->getContainer()->set(ClientService::class, $this->clientServiceProphecy->reveal()); - $output = new BufferedOutput(); - $this->kernel->getContainer()->set(OutputInterface::class, $output); - } - - protected function runApp(): string { - putenv("ACLI_REPO_ROOT=" . $this->projectDir); - $input = $this->kernel->getContainer()->get(InputInterface::class); - $output = $this->kernel->getContainer()->get(OutputInterface::class); - /** @var Application $application */ - $application = $this->kernel->getContainer()->get(Application::class); - $application->setAutoExit(FALSE); - $application->run($input, $output); - return $output->fetch(); - } +class ApplicationTestBase extends TestBase +{ + /** + * Symfony kernel. + */ + protected Kernel $kernel; + + public function setUp(): void + { + parent::setUp(); + $this->kernel = new Kernel('dev', false); + $this->kernel->boot(); + $this->kernel->getContainer()->set(CloudDataStore::class, $this->datastoreCloud); + $this->kernel->getContainer()->set(ClientService::class, $this->clientServiceProphecy->reveal()); + $output = new BufferedOutput(); + $this->kernel->getContainer()->set(OutputInterface::class, $output); + } - protected function setInput(array $args): void { - // ArrayInput requires command to be the first parameter. - if (array_key_exists('command', $args)) { - $newArgs = []; - $newArgs['command'] = $args['command']; - unset($args['command']); - $args = array_merge($newArgs, $args); + protected function runApp(): string + { + putenv("ACLI_REPO_ROOT=" . $this->projectDir); + $input = $this->kernel->getContainer()->get(InputInterface::class); + $output = $this->kernel->getContainer()->get(OutputInterface::class); + /** @var Application $application */ + $application = $this->kernel->getContainer()->get(Application::class); + $application->setAutoExit(false); + $application->run($input, $output); + return $output->fetch(); } - $input = new ArrayInput($args); - $input->setInteractive(FALSE); - $this->kernel->getContainer()->set(InputInterface::class, $input); - } + protected function setInput(array $args): void + { + // ArrayInput requires command to be the first parameter. + if (array_key_exists('command', $args)) { + $newArgs = []; + $newArgs['command'] = $args['command']; + unset($args['command']); + $args = array_merge($newArgs, $args); + } + $input = new ArrayInput($args); + $input->setInteractive(false); + $this->kernel->getContainer()->set(InputInterface::class, $input); + } } diff --git a/tests/phpunit/src/CloudApi/AccessTokenConnectorTest.php b/tests/phpunit/src/CloudApi/AccessTokenConnectorTest.php index 8b4063fcd..560e9c7e4 100644 --- a/tests/phpunit/src/CloudApi/AccessTokenConnectorTest.php +++ b/tests/phpunit/src/CloudApi/AccessTokenConnectorTest.php @@ -1,6 +1,6 @@ cloudCredentials->getCloudAccessToken()); + $connectorFactory = new ConnectorFactory( + [ + 'accessToken' => $this->cloudCredentials->getCloudAccessToken(), + 'accessTokenExpiry' => $this->cloudCredentials->getCloudAccessTokenExpiry(), + 'key' => null, + 'secret' => null, + ] + ); + $connector = $connectorFactory->createConnector(); + self::assertInstanceOf(AccessTokenConnector::class, $connector); + self::assertEquals(self::$accessToken, $connector->getAccessToken()->getToken()); + + $verb = 'get'; + $path = 'api'; + + // Make sure that new access tokens are fetched using the refresh token. + $mockProvider = $this->prophet->prophesize(GenericProvider::class); + $mockProvider->getAuthenticatedRequest($verb, ConnectorInterface::BASE_URI . $path, Argument::type(AccessTokenInterface::class)) + ->willReturn($this->prophet->prophesize(RequestInterface::class)->reveal()) + ->shouldBeCalled(); + $connector->setProvider($mockProvider->reveal()); + $connector->createRequest($verb, $path); + + $this->prophet->checkPredictions(); + } + + public function testTokenFile(): void + { + $accessTokenExpiry = time() + 300; + $directory = [ + 'expiry' => (string) $accessTokenExpiry . "\n", + 'token' => self::$accessToken . "\n", + ]; + $vfs = vfsStream::setup('root', null, $directory); + $tokenFile = Path::join($vfs->url(), 'token'); + $expiryFile = Path::join($vfs->url(), 'expiry'); + putenv('ACLI_ACCESS_TOKEN_FILE=' . $tokenFile); + putenv('ACLI_ACCESS_TOKEN_EXPIRY_FILE=' . $expiryFile); + self::assertEquals(self::$accessToken, $this->cloudCredentials->getCloudAccessToken()); + self::assertEquals($accessTokenExpiry, $this->cloudCredentials->getCloudAccessTokenExpiry()); + } + + public function testMissingTokenFile(): void + { + $accessTokenExpiry = time() + 300; + $directory = [ + 'expiry' => (string) $accessTokenExpiry, + ]; + $vfs = vfsStream::setup('root', null, $directory); + $tokenFile = Path::join($vfs->url(), 'token'); + $expiryFile = Path::join($vfs->url(), 'expiry'); + putenv('ACLI_ACCESS_TOKEN_FILE=' . $tokenFile); + putenv('ACLI_ACCESS_TOKEN_EXPIRY_FILE=' . $expiryFile); + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage('Access token file not found at ' . $tokenFile); + $this->cloudCredentials->getCloudAccessToken(); + } + + public function testMissingExpiryFile(): void + { + $directory = [ + 'token' => self::$accessToken, + ]; + $vfs = vfsStream::setup('root', null, $directory); + $tokenFile = Path::join($vfs->url(), 'token'); + $expiryFile = Path::join($vfs->url(), 'expiry'); + putenv('ACLI_ACCESS_TOKEN_FILE=' . $tokenFile); + putenv('ACLI_ACCESS_TOKEN_EXPIRY_FILE=' . $expiryFile); + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage('Access token expiry file not found at ' . $expiryFile); + $this->cloudCredentials->getCloudAccessTokenExpiry(); + } - protected function tearDown(): void { - parent::tearDown(); - self::unsetAccessTokenEnvVars(); - } + /** + * Validate that if both an access token and API key/secret pair are present, + * the pair is used. + */ + public function testConnector(): void + { + self::setAccessTokenEnvVars(); + // Ensure that ACLI_ACCESS_TOKEN was used to populate the refresh token. + self::assertEquals(self::$accessToken, $this->cloudCredentials->getCloudAccessToken()); + $connectorFactory = new ConnectorFactory( + [ + 'accessToken' => $this->cloudCredentials->getCloudAccessToken(), + 'accessTokenExpiry' => $this->cloudCredentials->getCloudAccessTokenExpiry(), + 'key' => $this->cloudCredentials->getCloudKey(), + 'secret' => $this->cloudCredentials->getCloudSecret(), + ] + ); + $connector = $connectorFactory->createConnector(); + self::assertInstanceOf(Connector::class, $connector); + } - public static function setAccessTokenEnvVars(mixed $expired = FALSE): void { - if ($expired) { - $accessTokenExpiry = time() - 300; + public function testExpiredAccessToken(): void + { + self::setAccessTokenEnvVars(true); + $connectorFactory = new ConnectorFactory( + [ + 'accessToken' => $this->cloudCredentials->getCloudAccessToken(), + 'accessTokenExpiry' => $this->cloudCredentials->getCloudAccessTokenExpiry(), + 'key' => null, + 'secret' => null, + ] + ); + $connector = $connectorFactory->createConnector(); + self::assertInstanceOf(Connector::class, $connector); } - else { - $accessTokenExpiry = time() + 300; + + public function testConnectorConfig(): void + { + self::setAccessTokenEnvVars(); + $connectorFactory = new ConnectorFactory( + [ + 'accessToken' => null, + 'key' => $this->cloudCredentials->getCloudKey(), + 'secret' => $this->cloudCredentials->getCloudSecret(), + ] + ); + $clientService = new ClientService($connectorFactory, $this->application, $this->cloudCredentials); + $client = $clientService->getClient(); + $options = $client->getOptions(); + $this->assertEquals(['User-Agent' => [0 => 'acli/UNKNOWN']], $options['headers']); + + $this->prophet->checkPredictions(); } - putenv('ACLI_ACCESS_TOKEN=' . self::$accessToken); - putenv('ACLI_ACCESS_TOKEN_EXPIRY=' . $accessTokenExpiry); - } - - public static function unsetAccessTokenEnvVars(): void { - putenv('ACLI_ACCESS_TOKEN'); - putenv('ACLI_ACCESS_TOKEN_EXPIRY'); - putenv('ACLI_ACCESS_TOKEN_FILE'); - putenv('ACLI_ACCESS_TOKEN_EXPIRY_FILE'); - } - - public function testAccessToken(): void { - self::setAccessTokenEnvVars(); - // Ensure that ACLI_ACCESS_TOKEN was used to populate the refresh token. - self::assertEquals(self::$accessToken, $this->cloudCredentials->getCloudAccessToken()); - $connectorFactory = new ConnectorFactory( - [ - 'accessToken' => $this->cloudCredentials->getCloudAccessToken(), - 'accessTokenExpiry' => $this->cloudCredentials->getCloudAccessTokenExpiry(), - 'key' => NULL, - 'secret' => NULL, - ]); - $connector = $connectorFactory->createConnector(); - self::assertInstanceOf(AccessTokenConnector::class, $connector); - self::assertEquals(self::$accessToken, $connector->getAccessToken()->getToken()); - - $verb = 'get'; - $path = 'api'; - - // Make sure that new access tokens are fetched using the refresh token. - $mockProvider = $this->prophet->prophesize(GenericProvider::class); - $mockProvider->getAuthenticatedRequest($verb, ConnectorInterface::BASE_URI . $path, Argument::type(AccessTokenInterface::class)) - ->willReturn($this->prophet->prophesize(RequestInterface::class)->reveal()) - ->shouldBeCalled(); - $connector->setProvider($mockProvider->reveal()); - $connector->createRequest($verb, $path); - - $this->prophet->checkPredictions(); - } - - public function testTokenFile(): void { - $accessTokenExpiry = time() + 300; - $directory = [ - 'expiry' => (string) $accessTokenExpiry . "\n", - 'token' => self::$accessToken . "\n", -]; - $vfs = vfsStream::setup('root', NULL, $directory); - $tokenFile = Path::join($vfs->url(), 'token'); - $expiryFile = Path::join($vfs->url(), 'expiry'); - putenv('ACLI_ACCESS_TOKEN_FILE=' . $tokenFile); - putenv('ACLI_ACCESS_TOKEN_EXPIRY_FILE=' . $expiryFile); - self::assertEquals(self::$accessToken, $this->cloudCredentials->getCloudAccessToken()); - self::assertEquals($accessTokenExpiry, $this->cloudCredentials->getCloudAccessTokenExpiry()); - } - - public function testMissingTokenFile(): void { - $accessTokenExpiry = time() + 300; - $directory = [ - 'expiry' => (string) $accessTokenExpiry, - ]; - $vfs = vfsStream::setup('root', NULL, $directory); - $tokenFile = Path::join($vfs->url(), 'token'); - $expiryFile = Path::join($vfs->url(), 'expiry'); - putenv('ACLI_ACCESS_TOKEN_FILE=' . $tokenFile); - putenv('ACLI_ACCESS_TOKEN_EXPIRY_FILE=' . $expiryFile); - $this->expectException(AcquiaCliException::class); - $this->expectExceptionMessage('Access token file not found at ' . $tokenFile); - $this->cloudCredentials->getCloudAccessToken(); - } - - public function testMissingExpiryFile(): void { - $directory = [ - 'token' => self::$accessToken, - ]; - $vfs = vfsStream::setup('root', NULL, $directory); - $tokenFile = Path::join($vfs->url(), 'token'); - $expiryFile = Path::join($vfs->url(), 'expiry'); - putenv('ACLI_ACCESS_TOKEN_FILE=' . $tokenFile); - putenv('ACLI_ACCESS_TOKEN_EXPIRY_FILE=' . $expiryFile); - $this->expectException(AcquiaCliException::class); - $this->expectExceptionMessage('Access token expiry file not found at ' . $expiryFile); - $this->cloudCredentials->getCloudAccessTokenExpiry(); - } - - /** - * Validate that if both an access token and API key/secret pair are present, - * the pair is used. - */ - public function testConnector(): void { - self::setAccessTokenEnvVars(); - // Ensure that ACLI_ACCESS_TOKEN was used to populate the refresh token. - self::assertEquals(self::$accessToken, $this->cloudCredentials->getCloudAccessToken()); - $connectorFactory = new ConnectorFactory( - [ - 'accessToken' => $this->cloudCredentials->getCloudAccessToken(), - 'accessTokenExpiry' => $this->cloudCredentials->getCloudAccessTokenExpiry(), - 'key' => $this->cloudCredentials->getCloudKey(), - 'secret' => $this->cloudCredentials->getCloudSecret(), - ]); - $connector = $connectorFactory->createConnector(); - self::assertInstanceOf(Connector::class, $connector); - } - - public function testExpiredAccessToken(): void { - self::setAccessTokenEnvVars(TRUE); - $connectorFactory = new ConnectorFactory( - [ - 'accessToken' => $this->cloudCredentials->getCloudAccessToken(), - 'accessTokenExpiry' => $this->cloudCredentials->getCloudAccessTokenExpiry(), - 'key' => NULL, - 'secret' => NULL, - ]); - $connector = $connectorFactory->createConnector(); - self::assertInstanceOf(Connector::class, $connector); - } - - public function testConnectorConfig(): void { - self::setAccessTokenEnvVars(); - $connectorFactory = new ConnectorFactory( - [ - 'accessToken' => NULL, - 'key' => $this->cloudCredentials->getCloudKey(), - 'secret' => $this->cloudCredentials->getCloudSecret(), - ]); - $clientService = new ClientService($connectorFactory, $this->application, $this->cloudCredentials); - $client = $clientService->getClient(); - $options = $client->getOptions(); - $this->assertEquals(['User-Agent' => [0 => 'acli/UNKNOWN']], $options['headers']); - - $this->prophet->checkPredictions(); - } - - public function testIdeHeader(): void { - self::setAccessTokenEnvVars(); - IdeHelper::setCloudIdeEnvVars(); - $connectorFactory = new ConnectorFactory( - [ - 'accessToken' => NULL, - 'key' => $this->cloudCredentials->getCloudKey(), - 'secret' => $this->cloudCredentials->getCloudSecret(), - ]); - $clientService = new ClientService($connectorFactory, $this->application, $this->cloudCredentials); - $client = $clientService->getClient(); - $options = $client->getOptions(); - $this->assertEquals(['User-Agent' => [0 => 'acli/UNKNOWN'], 'X-Cloud-IDE-UUID' => IdeHelper::$remoteIdeUuid], $options['headers']); - - $this->prophet->checkPredictions(); - IdeHelper::unsetCloudIdeEnvVars(); - } + public function testIdeHeader(): void + { + self::setAccessTokenEnvVars(); + IdeHelper::setCloudIdeEnvVars(); + $connectorFactory = new ConnectorFactory( + [ + 'accessToken' => null, + 'key' => $this->cloudCredentials->getCloudKey(), + 'secret' => $this->cloudCredentials->getCloudSecret(), + ] + ); + $clientService = new ClientService($connectorFactory, $this->application, $this->cloudCredentials); + $client = $clientService->getClient(); + $options = $client->getOptions(); + $this->assertEquals(['User-Agent' => [0 => 'acli/UNKNOWN'], 'X-Cloud-IDE-UUID' => IdeHelper::$remoteIdeUuid], $options['headers']); + + $this->prophet->checkPredictions(); + IdeHelper::unsetCloudIdeEnvVars(); + } } diff --git a/tests/phpunit/src/CloudApi/AcsfClientServiceTest.php b/tests/phpunit/src/CloudApi/AcsfClientServiceTest.php index fd181a01e..b66b814da 100644 --- a/tests/phpunit/src/CloudApi/AcsfClientServiceTest.php +++ b/tests/phpunit/src/CloudApi/AcsfClientServiceTest.php @@ -1,6 +1,6 @@ + */ + public function providerTestIsMachineAuthenticated(): array + { + return [ + [ + ['ACLI_ACCESS_TOKEN' => null, 'ACLI_KEY' => null, 'ACLI_SECRET' => null], + false, + ], + [ + ['ACLI_ACCESS_TOKEN' => null, 'ACLI_KEY' => 'key', 'ACLI_SECRET' => null], + false, + ], + ]; + } - /** - * @return array - */ - public function providerTestIsMachineAuthenticated(): array { - return [ - [ - ['ACLI_ACCESS_TOKEN' => NULL, 'ACLI_KEY' => NULL, 'ACLI_SECRET' => NULL], - FALSE, - ], - [ - ['ACLI_ACCESS_TOKEN' => NULL, 'ACLI_KEY' => 'key', 'ACLI_SECRET' => NULL], - FALSE, - ], - ]; - } + /** + * @dataProvider providerTestIsMachineAuthenticated + */ + public function testIsMachineAuthenticated(array $envVars, bool $isAuthenticated): void + { + self::setEnvVars($envVars); + $cloudDatastore = $this->prophet->prophesize(CloudDataStore::class); + $clientService = new AcsfClientService(new AcsfConnectorFactory(['key' => null, 'secret' => null, 'accessToken' => null]), $this->application, new AcsfCredentials($cloudDatastore->reveal())); + $this->assertEquals($isAuthenticated, $clientService->isMachineAuthenticated()); + $clientService->getClient(); + self::unsetEnvVars($envVars); + } - /** - * @dataProvider providerTestIsMachineAuthenticated - */ - public function testIsMachineAuthenticated(array $envVars, bool $isAuthenticated): void { - self::setEnvVars($envVars); - $cloudDatastore = $this->prophet->prophesize(CloudDataStore::class); - $clientService = new AcsfClientService(new AcsfConnectorFactory(['key' => NULL, 'secret' => NULL, 'accessToken' => NULL]), $this->application, new AcsfCredentials($cloudDatastore->reveal())); - $this->assertEquals($isAuthenticated, $clientService->isMachineAuthenticated()); - $clientService->getClient(); - self::unsetEnvVars($envVars); - } + public function testEmbeddedItems(): void + { + putenv('ACQUIA_CLI_USE_CLOUD_API_SPEC_CACHE=1'); + $cloudDatastore = $this->prophet->prophesize(CloudDataStore::class); + $clientService = new AcsfClientService(new AcsfConnectorFactory(['key' => null, 'secret' => null, 'accessToken' => null]), $this->application, new AcsfCredentials($cloudDatastore->reveal())); + $client = $clientService->getClient(); + $mockBody = ['_embedded' => ['items' => 'foo']]; + $response = new Response(200, [], json_encode($mockBody)); + $body = $client->processResponse($response); + $this->assertEquals('foo', $body); + } - public function testEmbeddedItems(): void { - putenv('ACQUIA_CLI_USE_CLOUD_API_SPEC_CACHE=1'); - $cloudDatastore = $this->prophet->prophesize(CloudDataStore::class); - $clientService = new AcsfClientService(new AcsfConnectorFactory(['key' => NULL, 'secret' => NULL, 'accessToken' => NULL]), $this->application, new AcsfCredentials($cloudDatastore->reveal())); - $client = $clientService->getClient(); - $mockBody = ['_embedded' => ['items' => 'foo']]; - $response = new Response(200, [], json_encode($mockBody)); - $body = $client->processResponse($response); - $this->assertEquals('foo', $body); - } - - public function testErrorMessage(): void { - putenv('ACQUIA_CLI_USE_CLOUD_API_SPEC_CACHE=1'); - $cloudDatastore = $this->prophet->prophesize(CloudDataStore::class); - $clientService = new AcsfClientService(new AcsfConnectorFactory(['key' => NULL, 'secret' => NULL, 'accessToken' => NULL]), $this->application, new AcsfCredentials($cloudDatastore->reveal())); - $client = $clientService->getClient(); - $mockBody = ['error' => 'foo', 'message' => 'bar']; - $response = new Response(200, [], json_encode($mockBody)); - $this->expectException(ApiErrorException::class); - $this->expectExceptionMessage('bar'); - $client->processResponse($response); - } - - public function testErrorCode(): void { - putenv('ACQUIA_CLI_USE_CLOUD_API_SPEC_CACHE=1'); - $cloudDatastore = $this->prophet->prophesize(CloudDataStore::class); - $clientService = new AcsfClientService(new AcsfConnectorFactory(['key' => NULL, 'secret' => NULL, 'accessToken' => NULL]), $this->application, new AcsfCredentials($cloudDatastore->reveal())); - $client = $clientService->getClient(); - $mockBody = ['message' => 'bar']; - $response = new Response(400, [], json_encode($mockBody)); - $this->expectException(ApiErrorException::class); - $this->expectExceptionMessage('bar'); - $client->processResponse($response); - } + public function testErrorMessage(): void + { + putenv('ACQUIA_CLI_USE_CLOUD_API_SPEC_CACHE=1'); + $cloudDatastore = $this->prophet->prophesize(CloudDataStore::class); + $clientService = new AcsfClientService(new AcsfConnectorFactory(['key' => null, 'secret' => null, 'accessToken' => null]), $this->application, new AcsfCredentials($cloudDatastore->reveal())); + $client = $clientService->getClient(); + $mockBody = ['error' => 'foo', 'message' => 'bar']; + $response = new Response(200, [], json_encode($mockBody)); + $this->expectException(ApiErrorException::class); + $this->expectExceptionMessage('bar'); + $client->processResponse($response); + } + public function testErrorCode(): void + { + putenv('ACQUIA_CLI_USE_CLOUD_API_SPEC_CACHE=1'); + $cloudDatastore = $this->prophet->prophesize(CloudDataStore::class); + $clientService = new AcsfClientService(new AcsfConnectorFactory(['key' => null, 'secret' => null, 'accessToken' => null]), $this->application, new AcsfCredentials($cloudDatastore->reveal())); + $client = $clientService->getClient(); + $mockBody = ['message' => 'bar']; + $response = new Response(400, [], json_encode($mockBody)); + $this->expectException(ApiErrorException::class); + $this->expectExceptionMessage('bar'); + $client->processResponse($response); + } } diff --git a/tests/phpunit/src/CloudApi/ClientServiceTest.php b/tests/phpunit/src/CloudApi/ClientServiceTest.php index ae5d4e993..4ecac1dfa 100644 --- a/tests/phpunit/src/CloudApi/ClientServiceTest.php +++ b/tests/phpunit/src/CloudApi/ClientServiceTest.php @@ -1,6 +1,6 @@ - */ - public function providerTestIsMachineAuthenticated(): array { - return [ - [ +class ClientServiceTest extends TestBase +{ + /** + * @return array + */ + public function providerTestIsMachineAuthenticated(): array + { + return [ + [ ['ACLI_ACCESS_TOKEN' => 'token', 'ACLI_KEY' => 'key', 'ACLI_SECRET' => 'secret'], - TRUE, - ], - [ - ['ACLI_ACCESS_TOKEN' => NULL, 'ACLI_KEY' => 'key', 'ACLI_SECRET' => 'secret'], - TRUE, - ], - [ - ['ACLI_ACCESS_TOKEN' => NULL, 'ACLI_KEY' => NULL, 'ACLI_SECRET' => NULL], - FALSE, - ], - [ - ['ACLI_ACCESS_TOKEN' => NULL, 'ACLI_KEY' => 'key', 'ACLI_SECRET' => NULL], - FALSE, - ], - ]; - } - - /** - * @dataProvider providerTestIsMachineAuthenticated - */ - public function testIsMachineAuthenticated(array $envVars, bool $isAuthenticated): void { - self::setEnvVars($envVars); - $cloudDatastore = $this->prophet->prophesize(CloudDataStore::class); - $clientService = new ClientService(new ConnectorFactory(['key' => NULL, 'secret' => NULL, 'accessToken' => NULL]), $this->application, new CloudCredentials($cloudDatastore->reveal())); - $this->assertEquals($isAuthenticated, $clientService->isMachineAuthenticated()); - self::unsetEnvVars($envVars); - } + true, + ], + [ + ['ACLI_ACCESS_TOKEN' => null, 'ACLI_KEY' => 'key', 'ACLI_SECRET' => 'secret'], + true, + ], + [ + ['ACLI_ACCESS_TOKEN' => null, 'ACLI_KEY' => null, 'ACLI_SECRET' => null], + false, + ], + [ + ['ACLI_ACCESS_TOKEN' => null, 'ACLI_KEY' => 'key', 'ACLI_SECRET' => null], + false, + ], + ]; + } + /** + * @dataProvider providerTestIsMachineAuthenticated + */ + public function testIsMachineAuthenticated(array $envVars, bool $isAuthenticated): void + { + self::setEnvVars($envVars); + $cloudDatastore = $this->prophet->prophesize(CloudDataStore::class); + $clientService = new ClientService(new ConnectorFactory(['key' => null, 'secret' => null, 'accessToken' => null]), $this->application, new CloudCredentials($cloudDatastore->reveal())); + $this->assertEquals($isAuthenticated, $clientService->isMachineAuthenticated()); + self::unsetEnvVars($envVars); + } } diff --git a/tests/phpunit/src/CloudApi/EnvVarAuthenticationTest.php b/tests/phpunit/src/CloudApi/EnvVarAuthenticationTest.php index fcbd28799..4c64d8fed 100644 --- a/tests/phpunit/src/CloudApi/EnvVarAuthenticationTest.php +++ b/tests/phpunit/src/CloudApi/EnvVarAuthenticationTest.php @@ -1,33 +1,35 @@ key); - putenv('ACLI_SECRET=' . $this->secret); - putenv('ACLI_CLOUD_API_BASE_URI=' . $this->cloudApiBaseUri); - } - - protected function tearDown(): void { - parent::tearDown(); - putenv('ACLI_KEY'); - putenv('ACLI_SECRET'); - } - - public function testKeyAndSecret(): void { - $this->removeMockCloudConfigFile(); - self::assertEquals($this->key, $this->cloudCredentials->getCloudKey()); - self::assertEquals($this->secret, $this->cloudCredentials->getCloudSecret()); - self::assertEquals($this->cloudApiBaseUri, $this->cloudCredentials->getBaseUri()); - } - +class EnvVarAuthenticationTest extends TestBase +{ + protected string $cloudApiBaseUri = 'https://www.acquia.com'; + + public function setUp(mixed $output = null): void + { + parent::setUp(); + putenv('ACLI_KEY=' . $this->key); + putenv('ACLI_SECRET=' . $this->secret); + putenv('ACLI_CLOUD_API_BASE_URI=' . $this->cloudApiBaseUri); + } + + protected function tearDown(): void + { + parent::tearDown(); + putenv('ACLI_KEY'); + putenv('ACLI_SECRET'); + } + + public function testKeyAndSecret(): void + { + $this->removeMockCloudConfigFile(); + self::assertEquals($this->key, $this->cloudCredentials->getCloudKey()); + self::assertEquals($this->secret, $this->cloudCredentials->getCloudSecret()); + self::assertEquals($this->cloudApiBaseUri, $this->cloudCredentials->getBaseUri()); + } } diff --git a/tests/phpunit/src/CommandTestBase.php b/tests/phpunit/src/CommandTestBase.php index 0374d1b81..1a7a36ea5 100644 --- a/tests/phpunit/src/CommandTestBase.php +++ b/tests/phpunit/src/CommandTestBase.php @@ -1,6 +1,6 @@ command)) { - $this->command = $this->createCommand(); - } - $this->printTestName(); - } - - protected function tearDown(): void { - parent::tearDown(); - if (!in_array('brokenProphecy', $this->getGroups())) { - $this->prophet->checkPredictions(); - } - } - - protected function setCommand(CommandBase $command): void { - $this->command = $command; - } - - /** - * Executes a given command with the command tester. - * - * @param array $args - * The command arguments. - * @param string[] $inputs - * An array of strings representing each input passed to the command input - * stream. - */ - protected function executeCommand(array $args = [], array $inputs = [], int $verbosity = Output::VERBOSITY_VERY_VERBOSE): void { - $cwd = $this->projectDir; - $tester = $this->getCommandTester(); - $tester->setInputs($inputs); - $commandName = $this->command->getName(); - $args = array_merge(['command' => $commandName], $args); - - if (getenv('ACLI_PRINT_COMMAND_OUTPUT')) { - $this->consoleOutput->writeln(''); - $this->consoleOutput->writeln('Executing ' . $this->command->getName() . ' in ' . $cwd); - $this->consoleOutput->writeln('------Begin command output-------'); - } - - try { - $tester->execute($args, ['verbosity' => $verbosity]); - } - catch (Exception $e) { - if (getenv('ACLI_PRINT_COMMAND_OUTPUT')) { - print $this->getDisplay(); - } - throw $e; - } - - if (getenv('ACLI_PRINT_COMMAND_OUTPUT')) { - $this->consoleOutput->writeln($tester->getDisplay()); - $this->consoleOutput->writeln('------End command output---------'); - $this->consoleOutput->writeln(''); - } - } - - /** - * Gets the command tester. - */ - protected function getCommandTester(): CommandTester { - if (isset($this->commandTester)) { - return $this->commandTester; - } - - $this->application->add($this->command); - $foundCommand = $this->application->find($this->command->getName()); - $this->assertInstanceOf(get_class($this->command), $foundCommand, 'Instantiated class.'); - $this->commandTester = new CommandTester($foundCommand); - - return $this->commandTester; - } - - /** - * Gets the display returned by the last execution of the command. - */ - protected function getDisplay(): string { - return $this->getCommandTester()->getDisplay(); - } - - /** - * Gets the status code returned by the last execution of the command. - */ - protected function getStatusCode(): int { - return $this->getCommandTester()->getStatusCode(); - } - - /** - * Write full width line. - */ - protected function writeFullWidthLine(string $message, OutputInterface $output): void { - $terminalWidth = (new Terminal())->getWidth(); - $paddingLen = (int) (($terminalWidth - strlen($message)) / 2); - $pad = $paddingLen > 0 ? str_repeat('-', $paddingLen) : ''; - $output->writeln("{$pad}{$message}{$pad}"); - } - - /** - * Prints the name of the PHPUnit test to output. - */ - protected function printTestName(): void { - if (getenv('ACLI_PRINT_COMMAND_OUTPUT')) { - $this->consoleOutput->writeln(""); - $this->writeFullWidthLine(get_class($this) . "::" . $this->getName(), $this->consoleOutput); - } - } - - protected function getTargetGitConfigFixture(): string { - return Path::join($this->projectDir, '.git', 'config'); - } - - protected function getSourceGitConfigFixture(): string { - return Path::join($this->realFixtureDir, 'git_config'); - } - - /** - * Creates a mock .git/config. - */ - protected function createMockGitConfigFile(): void { - // Create mock git config file. - $this->fs->copy($this->getSourceGitConfigFixture(), $this->getTargetGitConfigFixture()); - } - - /** - * Remove mock .git/config. - */ - protected function removeMockGitConfig(): void { - $this->fs->remove([$this->getTargetGitConfigFixture(), dirname($this->getTargetGitConfigFixture())]); - } - - protected function mockReadIdePhpVersion(string $phpVersion = '7.1'): LocalMachineHelper|ObjectProphecy { - $localMachineHelper = $this->mockLocalMachineHelper(); - $localMachineHelper->getLocalFilepath(Path::join($this->dataDir, 'acquia-cli.json'))->willReturn(Path::join($this->dataDir, 'acquia-cli.json')); - $localMachineHelper->readFile('/home/ide/configs/php/.version')->willReturn("$phpVersion\n")->shouldBeCalled(); - - return $localMachineHelper; - } - - /** - * @return array - */ - protected static function inputChooseEnvironment(): array { - return [ - // Would you like Acquia CLI to search for a Cloud application that matches your local git config? - 'n', - // Select a Cloud Platform application: - self::$INPUT_DEFAULT_CHOICE, - // Would you like to link the project at ... ? - 'n', - // Choose an Acquia environment: - self::$INPUT_DEFAULT_CHOICE, - ]; - } - - public function mockGetEnvironment(): mixed { - $applications = $this->mockRequest('getApplications'); - $application = $this->mockRequest('getApplicationByUuid', $applications[self::$INPUT_DEFAULT_CHOICE]->uuid); - $environments = $this->mockRequest('getApplicationEnvironments', $application->uuid); - return $environments[self::$INPUT_DEFAULT_CHOICE]; - } - - protected function mockLocalMachineHelper(): LocalMachineHelper|ObjectProphecy { - $localMachineHelper = $this->prophet->prophesize(LocalMachineHelper::class); - $localMachineHelper->useTty()->willReturn(FALSE); - $this->command->localMachineHelper = $localMachineHelper->reveal(); - - return $localMachineHelper; - } - - /** - * @return \Acquia\Cli\Helpers\SshHelper|\Prophecy\Prophecy\ObjectProphecy - */ - protected function mockSshHelper(): SshHelper|ObjectProphecy { - return $this->prophet->prophesize(SshHelper::class); - } - - protected function mockGetEnvironments(): object { - $environmentResponse = $this->getMockEnvironmentResponse(); - $this->clientProphecy->request('get', - "/environments/" . $environmentResponse->id) - ->willReturn($environmentResponse) - ->shouldBeCalled(); - return $environmentResponse; - } - - public function mockAcsfEnvironmentsRequest( - object $applicationsResponse - ): object { - $environmentsResponse = $this->getMockEnvironmentsResponse(); - foreach ($environmentsResponse->_embedded->items as $environment) { - $environment->ssh_url = 'profserv2.01dev@profserv201dev.ssh.enterprise-g1.acquia-sites.com'; - $environment->domains = ["profserv201dev.enterprise-g1.acquia-sites.com"]; - } - $this->clientProphecy->request('get', - "/applications/{$applicationsResponse->{'_embedded'}->items[0]->uuid}/environments") - ->willReturn($environmentsResponse->_embedded->items) - ->shouldBeCalled(); - - return $environmentsResponse; - } - - /** - * @return array - */ - protected function mockGetAcsfSites(mixed $sshHelper): array { - $acsfMultisiteFetchProcess = $this->mockProcess(); - $multisiteConfig = file_get_contents(Path::join($this->realFixtureDir, '/multisite-config.json')); - $acsfMultisiteFetchProcess->getOutput()->willReturn($multisiteConfig)->shouldBeCalled(); - $sshHelper->executeCommand( - Argument::type('string'), - ['cat', '/var/www/site-php/profserv2.01dev/multisite-config.json'], - FALSE - )->willReturn($acsfMultisiteFetchProcess->reveal())->shouldBeCalled(); - return json_decode($multisiteConfig, TRUE); - } - - protected function mockGetCloudSites(mixed $sshHelper, mixed $environment): void { - $cloudMultisiteFetchProcess = $this->mockProcess(); - $cloudMultisiteFetchProcess->getOutput()->willReturn("\nbar\ndefault\nfoo\n")->shouldBeCalled(); - $parts = explode('.', $environment->ssh_url); - $sitegroup = reset($parts); - $sshHelper->executeCommand( - Argument::type('string'), - ['ls', "/mnt/files/$sitegroup.{$environment->name}/sites"], - FALSE - )->willReturn($cloudMultisiteFetchProcess->reveal())->shouldBeCalled(); - } - - protected function mockProcess(bool $success = TRUE): Process|ObjectProphecy { - $process = $this->prophet->prophesize(Process::class); - $process->isSuccessful()->willReturn($success); - $process->getExitCode()->willReturn($success ? 0 : 1); - if (!$success) { - $process->getErrorOutput()->willReturn('error'); - } - else { - $process->getErrorOutput()->willReturn(''); - } - return $process; - } - - /** - * @return \AcquiaCloudApi\Response\DatabaseResponse[] - */ - protected function mockAcsfDatabasesResponse( - object $environmentsResponse - ): array { - $databasesResponseJson = json_decode(file_get_contents(Path::join($this->realFixtureDir, '/acsf_db_response.json')), FALSE, 512, JSON_THROW_ON_ERROR); - $databasesResponse = array_map( - static function (mixed $databaseResponse) { - return new DatabaseResponse($databaseResponse); - }, - $databasesResponseJson - ); - $this->clientProphecy->request('get', - "/environments/{$environmentsResponse->id}/databases") - ->willReturn($databasesResponse) - ->shouldBeCalled(); - - return $databasesResponse; - } - - protected function mockDatabaseBackupsResponse( - object $environmentsResponse, - string $dbName, - int $backupId, - bool $existingBackups = TRUE - ): object { - $databaseBackupsResponse = $this->getMockResponseFromSpec('/environments/{environmentId}/databases/{databaseName}/backups', 'get', 200); - foreach ($databaseBackupsResponse->_embedded->items as $backup) { - $backup->_links->download->href = "/environments/$environmentsResponse->id/databases/$dbName/backups/$backupId/actions/download"; - $backup->database->name = $dbName; - // Acquia PHP SDK mutates the property name. Gross workaround, is there a better way? - $backup->completedAt = $backup->completed_at; - } - - if ($existingBackups) { - $this->clientProphecy->request('get', - "/environments/{$environmentsResponse->id}/databases/$dbName/backups") - ->willReturn($databaseBackupsResponse->_embedded->items) +abstract class CommandTestBase extends TestBase +{ + /** + * The command tester. + */ + private CommandTester $commandTester; + + // Select the application / SSH key / etc. + protected static int $INPUT_DEFAULT_CHOICE = 0; + + protected CommandBase $command; + + protected string $apiCommandPrefix = 'api'; + + abstract protected function createCommand(): CommandBase; + + /** + * This method is called before each test. + */ + protected function setUp(): void + { + parent::setUp(); + if (!isset($this->command)) { + $this->command = $this->createCommand(); + } + $this->printTestName(); + } + + protected function tearDown(): void + { + parent::tearDown(); + if (!in_array('brokenProphecy', $this->getGroups())) { + $this->prophet->checkPredictions(); + } + } + + protected function setCommand(CommandBase $command): void + { + $this->command = $command; + } + + /** + * Executes a given command with the command tester. + * + * @param array $args + * The command arguments. + * @param string[] $inputs + * An array of strings representing each input passed to the command input + * stream. + */ + protected function executeCommand(array $args = [], array $inputs = [], int $verbosity = Output::VERBOSITY_VERY_VERBOSE): void + { + $cwd = $this->projectDir; + $tester = $this->getCommandTester(); + $tester->setInputs($inputs); + $commandName = $this->command->getName(); + $args = array_merge(['command' => $commandName], $args); + + if (getenv('ACLI_PRINT_COMMAND_OUTPUT')) { + $this->consoleOutput->writeln(''); + $this->consoleOutput->writeln('Executing ' . $this->command->getName() . ' in ' . $cwd); + $this->consoleOutput->writeln('------Begin command output-------'); + } + + try { + $tester->execute($args, ['verbosity' => $verbosity]); + } catch (Exception $e) { + if (getenv('ACLI_PRINT_COMMAND_OUTPUT')) { + print $this->getDisplay(); + } + throw $e; + } + + if (getenv('ACLI_PRINT_COMMAND_OUTPUT')) { + $this->consoleOutput->writeln($tester->getDisplay()); + $this->consoleOutput->writeln('------End command output---------'); + $this->consoleOutput->writeln(''); + } + } + + /** + * Gets the command tester. + */ + protected function getCommandTester(): CommandTester + { + if (isset($this->commandTester)) { + return $this->commandTester; + } + + $this->application->add($this->command); + $foundCommand = $this->application->find($this->command->getName()); + $this->assertInstanceOf(get_class($this->command), $foundCommand, 'Instantiated class.'); + $this->commandTester = new CommandTester($foundCommand); + + return $this->commandTester; + } + + /** + * Gets the display returned by the last execution of the command. + */ + protected function getDisplay(): string + { + return $this->getCommandTester()->getDisplay(); + } + + /** + * Gets the status code returned by the last execution of the command. + */ + protected function getStatusCode(): int + { + return $this->getCommandTester()->getStatusCode(); + } + + /** + * Write full width line. + */ + protected function writeFullWidthLine(string $message, OutputInterface $output): void + { + $terminalWidth = (new Terminal())->getWidth(); + $paddingLen = (int) (($terminalWidth - strlen($message)) / 2); + $pad = $paddingLen > 0 ? str_repeat('-', $paddingLen) : ''; + $output->writeln("{$pad}{$message}{$pad}"); + } + + /** + * Prints the name of the PHPUnit test to output. + */ + protected function printTestName(): void + { + if (getenv('ACLI_PRINT_COMMAND_OUTPUT')) { + $this->consoleOutput->writeln(""); + $this->writeFullWidthLine(get_class($this) . "::" . $this->getName(), $this->consoleOutput); + } + } + + protected function getTargetGitConfigFixture(): string + { + return Path::join($this->projectDir, '.git', 'config'); + } + + protected function getSourceGitConfigFixture(): string + { + return Path::join($this->realFixtureDir, 'git_config'); + } + + /** + * Creates a mock .git/config. + */ + protected function createMockGitConfigFile(): void + { + // Create mock git config file. + $this->fs->copy($this->getSourceGitConfigFixture(), $this->getTargetGitConfigFixture()); + } + + /** + * Remove mock .git/config. + */ + protected function removeMockGitConfig(): void + { + $this->fs->remove([$this->getTargetGitConfigFixture(), dirname($this->getTargetGitConfigFixture())]); + } + + protected function mockReadIdePhpVersion(string $phpVersion = '7.1'): LocalMachineHelper|ObjectProphecy + { + $localMachineHelper = $this->mockLocalMachineHelper(); + $localMachineHelper->getLocalFilepath(Path::join($this->dataDir, 'acquia-cli.json'))->willReturn(Path::join($this->dataDir, 'acquia-cli.json')); + $localMachineHelper->readFile('/home/ide/configs/php/.version')->willReturn("$phpVersion\n")->shouldBeCalled(); + + return $localMachineHelper; + } + + /** + * @return array + */ + protected static function inputChooseEnvironment(): array + { + return [ + // Would you like Acquia CLI to search for a Cloud application that matches your local git config? + 'n', + // Select a Cloud Platform application: + self::$INPUT_DEFAULT_CHOICE, + // Would you like to link the project at ... ? + 'n', + // Choose an Acquia environment: + self::$INPUT_DEFAULT_CHOICE, + ]; + } + + public function mockGetEnvironment(): mixed + { + $applications = $this->mockRequest('getApplications'); + $application = $this->mockRequest('getApplicationByUuid', $applications[self::$INPUT_DEFAULT_CHOICE]->uuid); + $environments = $this->mockRequest('getApplicationEnvironments', $application->uuid); + return $environments[self::$INPUT_DEFAULT_CHOICE]; + } + + protected function mockLocalMachineHelper(): LocalMachineHelper|ObjectProphecy + { + $localMachineHelper = $this->prophet->prophesize(LocalMachineHelper::class); + $localMachineHelper->useTty()->willReturn(false); + $this->command->localMachineHelper = $localMachineHelper->reveal(); + + return $localMachineHelper; + } + + /** + * @return \Acquia\Cli\Helpers\SshHelper|\Prophecy\Prophecy\ObjectProphecy + */ + protected function mockSshHelper(): SshHelper|ObjectProphecy + { + return $this->prophet->prophesize(SshHelper::class); + } + + protected function mockGetEnvironments(): object + { + $environmentResponse = $this->getMockEnvironmentResponse(); + $this->clientProphecy->request( + 'get', + "/environments/" . $environmentResponse->id + ) + ->willReturn($environmentResponse) ->shouldBeCalled(); + return $environmentResponse; } - else { - $this->clientProphecy->request('get', - "/environments/{$environmentsResponse->id}/databases/$dbName/backups") - ->willReturn([], $databaseBackupsResponse->_embedded->items) + + public function mockAcsfEnvironmentsRequest( + object $applicationsResponse + ): object { + $environmentsResponse = $this->getMockEnvironmentsResponse(); + foreach ($environmentsResponse->_embedded->items as $environment) { + $environment->ssh_url = 'profserv2.01dev@profserv201dev.ssh.enterprise-g1.acquia-sites.com'; + $environment->domains = ["profserv201dev.enterprise-g1.acquia-sites.com"]; + } + $this->clientProphecy->request( + 'get', + "/applications/{$applicationsResponse->{'_embedded'}->items[0]->uuid}/environments" + ) + ->willReturn($environmentsResponse->_embedded->items) ->shouldBeCalled(); + + return $environmentsResponse; + } + + /** + * @return array + */ + protected function mockGetAcsfSites(mixed $sshHelper): array + { + $acsfMultisiteFetchProcess = $this->mockProcess(); + $multisiteConfig = file_get_contents(Path::join($this->realFixtureDir, '/multisite-config.json')); + $acsfMultisiteFetchProcess->getOutput()->willReturn($multisiteConfig)->shouldBeCalled(); + $sshHelper->executeCommand( + Argument::type('string'), + ['cat', '/var/www/site-php/profserv2.01dev/multisite-config.json'], + false + )->willReturn($acsfMultisiteFetchProcess->reveal())->shouldBeCalled(); + return json_decode($multisiteConfig, true); } - return $databaseBackupsResponse; - } - - protected function mockDownloadBackupResponse( - mixed $environmentsResponse, - mixed $dbName, - mixed $backupId - ): void { - $stream = $this->prophet->prophesize(StreamInterface::class); - $this->clientProphecy->stream('get', "/environments/{$environmentsResponse->id}/databases/{$dbName}/backups/{$backupId}/actions/download", []) - ->willReturn($stream->reveal()) - ->shouldBeCalled(); - } - - protected function mockDatabaseBackupCreateResponse( - mixed $environmentsResponse, - mixed $dbName - ): mixed { - $backupCreateResponse = $this->getMockResponseFromSpec('/environments/{environmentId}/databases/{databaseName}/backups', 'post', 202)->{'Creating backup'}->value; - $this->clientProphecy->request('post', "/environments/$environmentsResponse->id/databases/{$dbName}/backups") - ->willReturn($backupCreateResponse) - ->shouldBeCalled(); - - return $backupCreateResponse; - } - - protected function mockNotificationResponseFromObject(object $responseWithNotificationLink): array|object { - $uuid = substr($responseWithNotificationLink->_links->notification->href, -36); - return $this->mockRequest('getNotificationByUuid', $uuid); - } - - protected function mockCreateMySqlDumpOnLocal(ObjectProphecy $localMachineHelper, bool $printOutput = TRUE): void { - $localMachineHelper->checkRequiredBinariesExist(["mysqldump", "gzip"])->shouldBeCalled(); - $process = $this->mockProcess(); - $process->getOutput()->willReturn(''); - $command = 'MYSQL_PWD=drupal mysqldump --host=localhost --user=drupal drupal | pv --rate --bytes | gzip -9 > ' . sys_get_temp_dir() . '/acli-mysql-dump-drupal.sql.gz'; - $localMachineHelper->executeFromCmd($command, Argument::type('callable'), NULL, $printOutput)->willReturn($process->reveal()) - ->shouldBeCalled(); - } - - protected function mockExecutePvExists( + protected function mockGetCloudSites(mixed $sshHelper, mixed $environment): void + { + $cloudMultisiteFetchProcess = $this->mockProcess(); + $cloudMultisiteFetchProcess->getOutput()->willReturn("\nbar\ndefault\nfoo\n")->shouldBeCalled(); + $parts = explode('.', $environment->ssh_url); + $sitegroup = reset($parts); + $sshHelper->executeCommand( + Argument::type('string'), + ['ls', "/mnt/files/$sitegroup.{$environment->name}/sites"], + false + )->willReturn($cloudMultisiteFetchProcess->reveal())->shouldBeCalled(); + } + + protected function mockProcess(bool $success = true): Process|ObjectProphecy + { + $process = $this->prophet->prophesize(Process::class); + $process->isSuccessful()->willReturn($success); + $process->getExitCode()->willReturn($success ? 0 : 1); + if (!$success) { + $process->getErrorOutput()->willReturn('error'); + } else { + $process->getErrorOutput()->willReturn(''); + } + return $process; + } + + /** + * @return \AcquiaCloudApi\Response\DatabaseResponse[] + */ + protected function mockAcsfDatabasesResponse( + object $environmentsResponse + ): array { + $databasesResponseJson = json_decode(file_get_contents(Path::join($this->realFixtureDir, '/acsf_db_response.json')), false, 512, JSON_THROW_ON_ERROR); + $databasesResponse = array_map( + static function (mixed $databaseResponse) { + return new DatabaseResponse($databaseResponse); + }, + $databasesResponseJson + ); + $this->clientProphecy->request( + 'get', + "/environments/{$environmentsResponse->id}/databases" + ) + ->willReturn($databasesResponse) + ->shouldBeCalled(); + + return $databasesResponse; + } + + protected function mockDatabaseBackupsResponse( + object $environmentsResponse, + string $dbName, + int $backupId, + bool $existingBackups = true + ): object { + $databaseBackupsResponse = $this->getMockResponseFromSpec('/environments/{environmentId}/databases/{databaseName}/backups', 'get', 200); + foreach ($databaseBackupsResponse->_embedded->items as $backup) { + $backup->_links->download->href = "/environments/$environmentsResponse->id/databases/$dbName/backups/$backupId/actions/download"; + $backup->database->name = $dbName; + // Acquia PHP SDK mutates the property name. Gross workaround, is there a better way? + $backup->completedAt = $backup->completed_at; + } + + if ($existingBackups) { + $this->clientProphecy->request( + 'get', + "/environments/{$environmentsResponse->id}/databases/$dbName/backups" + ) + ->willReturn($databaseBackupsResponse->_embedded->items) + ->shouldBeCalled(); + } else { + $this->clientProphecy->request( + 'get', + "/environments/{$environmentsResponse->id}/databases/$dbName/backups" + ) + ->willReturn([], $databaseBackupsResponse->_embedded->items) + ->shouldBeCalled(); + } + + return $databaseBackupsResponse; + } + + protected function mockDownloadBackupResponse( + mixed $environmentsResponse, + mixed $dbName, + mixed $backupId + ): void { + $stream = $this->prophet->prophesize(StreamInterface::class); + $this->clientProphecy->stream('get', "/environments/{$environmentsResponse->id}/databases/{$dbName}/backups/{$backupId}/actions/download", []) + ->willReturn($stream->reveal()) + ->shouldBeCalled(); + } + + protected function mockDatabaseBackupCreateResponse( + mixed $environmentsResponse, + mixed $dbName + ): mixed { + $backupCreateResponse = $this->getMockResponseFromSpec('/environments/{environmentId}/databases/{databaseName}/backups', 'post', 202)->{'Creating backup'}->value; + $this->clientProphecy->request('post', "/environments/$environmentsResponse->id/databases/{$dbName}/backups") + ->willReturn($backupCreateResponse) + ->shouldBeCalled(); + + return $backupCreateResponse; + } + + protected function mockNotificationResponseFromObject(object $responseWithNotificationLink): array|object + { + $uuid = substr($responseWithNotificationLink->_links->notification->href, -36); + return $this->mockRequest('getNotificationByUuid', $uuid); + } + + protected function mockCreateMySqlDumpOnLocal(ObjectProphecy $localMachineHelper, bool $printOutput = true): void + { + $localMachineHelper->checkRequiredBinariesExist(["mysqldump", "gzip"])->shouldBeCalled(); + $process = $this->mockProcess(); + $process->getOutput()->willReturn(''); + $command = 'MYSQL_PWD=drupal mysqldump --host=localhost --user=drupal drupal | pv --rate --bytes | gzip -9 > ' . sys_get_temp_dir() . '/acli-mysql-dump-drupal.sql.gz'; + $localMachineHelper->executeFromCmd($command, Argument::type('callable'), null, $printOutput)->willReturn($process->reveal()) + ->shouldBeCalled(); + } + + protected function mockExecutePvExists( ObjectProphecy $localMachineHelper, - bool $pvExists = TRUE + bool $pvExists = true ): void { - $localMachineHelper + $localMachineHelper ->commandExists('pv') ->willReturn($pvExists) ->shouldBeCalled(); - } - - protected function mockExecuteGlabExists( - ObjectProphecy $localMachineHelper - ): void { - $localMachineHelper - ->commandExists('glab') - ->willReturn(TRUE) - ->shouldBeCalled(); - } - - /** - * Mock guzzle requests for update checks, so we don't actually hit GitHub. - */ - protected function setUpdateClient(int $statusCode = 200): void { - /** @var ObjectProphecy|\GuzzleHttp\Psr7\Response $guzzleResponse */ - $guzzleResponse = $this->prophet->prophesize(Response::class); - $stream = $this->prophet->prophesize(StreamInterface::class); - $stream->__toString()->willReturn(file_get_contents(Path::join(__DIR__, '..', '..', 'fixtures', 'github-releases.json'))); - $guzzleResponse->getBody()->willReturn($stream->reveal()); - $guzzleResponse->getReasonPhrase()->willReturn(''); - $guzzleResponse->getStatusCode()->willReturn($statusCode); - $guzzleClient = $this->prophet->prophesize(Client::class); - $guzzleClient->get('https://api.github.com/repos/acquia/cli/releases') - ->willReturn($guzzleResponse->reveal()); - $this->command->setUpdateClient($guzzleClient->reveal()); - } - - protected function mockPollCloudViaSsh(array $environmentsResponse, bool $ssh = TRUE): ObjectProphecy { - $process = $this->prophet->prophesize(Process::class); - $process->isSuccessful()->willReturn(TRUE); - $process->getExitCode()->willReturn(0); - $gitProcess = $this->prophet->prophesize(Process::class); - $gitProcess->isSuccessful()->willReturn(TRUE); - $gitProcess->getExitCode()->willReturn(128); - $sshHelper = $this->mockSshHelper(); - // Mock Git. - $urlParts = explode(':', $environmentsResponse[0]->vcs->url); - $sshHelper->executeCommand($urlParts[0], ['ls'], FALSE) - ->willReturn($gitProcess->reveal()) - ->shouldBeCalled(); - if ($ssh) { - // Mock non-prod. - $sshHelper->executeCommand($environmentsResponse[0]->ssh_url, ['ls'], FALSE) - ->willReturn($process->reveal()) + } + + protected function mockExecuteGlabExists( + ObjectProphecy $localMachineHelper + ): void { + $localMachineHelper + ->commandExists('glab') + ->willReturn(true) + ->shouldBeCalled(); + } + + /** + * Mock guzzle requests for update checks, so we don't actually hit GitHub. + */ + protected function setUpdateClient(int $statusCode = 200): void + { + /** @var ObjectProphecy|\GuzzleHttp\Psr7\Response $guzzleResponse */ + $guzzleResponse = $this->prophet->prophesize(Response::class); + $stream = $this->prophet->prophesize(StreamInterface::class); + $stream->__toString()->willReturn(file_get_contents(Path::join(__DIR__, '..', '..', 'fixtures', 'github-releases.json'))); + $guzzleResponse->getBody()->willReturn($stream->reveal()); + $guzzleResponse->getReasonPhrase()->willReturn(''); + $guzzleResponse->getStatusCode()->willReturn($statusCode); + $guzzleClient = $this->prophet->prophesize(Client::class); + $guzzleClient->get('https://api.github.com/repos/acquia/cli/releases') + ->willReturn($guzzleResponse->reveal()); + $this->command->setUpdateClient($guzzleClient->reveal()); + } + + protected function mockPollCloudViaSsh(array $environmentsResponse, bool $ssh = true): ObjectProphecy + { + $process = $this->prophet->prophesize(Process::class); + $process->isSuccessful()->willReturn(true); + $process->getExitCode()->willReturn(0); + $gitProcess = $this->prophet->prophesize(Process::class); + $gitProcess->isSuccessful()->willReturn(true); + $gitProcess->getExitCode()->willReturn(128); + $sshHelper = $this->mockSshHelper(); + // Mock Git. + $urlParts = explode(':', $environmentsResponse[0]->vcs->url); + $sshHelper->executeCommand($urlParts[0], ['ls'], false) + ->willReturn($gitProcess->reveal()) ->shouldBeCalled(); - // Mock prod. - $sshHelper->executeCommand($environmentsResponse[1]->ssh_url, ['ls'], FALSE) + if ($ssh) { + // Mock non-prod. + $sshHelper->executeCommand($environmentsResponse[0]->ssh_url, ['ls'], false) + ->willReturn($process->reveal()) + ->shouldBeCalled(); + // Mock prod. + $sshHelper->executeCommand($environmentsResponse[1]->ssh_url, ['ls'], false) + ->willReturn($process->reveal()) + ->shouldBeCalled(); + } + return $sshHelper; + } + + protected function mockPollCloudGitViaSsh(object $environmentResponse): ObjectProphecy + { + $process = $this->prophet->prophesize(Process::class); + $process->isSuccessful()->willReturn(true); + $process->getExitCode()->willReturn(128); + $sshHelper = $this->mockSshHelper(); + $sshHelper->executeCommand($environmentResponse->vcs->url, ['ls'], false) ->willReturn($process->reveal()) ->shouldBeCalled(); + return $sshHelper; + } + + protected function mockGetLocalSshKey(mixed $localMachineHelper, mixed $fileSystem, mixed $publicKey): string + { + $fileSystem->exists(Argument::type('string'))->willReturn(true); + /** @var \Symfony\Component\Finder\Finder|\Prophecy\Prophecy\ObjectProphecy $finder */ + $finder = $this->prophet->prophesize(Finder::class); + $finder->files()->willReturn($finder); + $finder->in(Argument::type('string'))->willReturn($finder); + $finder->name(Argument::type('string'))->willReturn($finder); + $finder->ignoreUnreadableDirs()->willReturn($finder); + $file = $this->prophet->prophesize(SplFileInfo::class); + $fileName = 'id_rsa.pub'; + $file->getFileName()->willReturn($fileName); + $file->getRealPath()->willReturn('somepath'); + $localMachineHelper->readFile('somepath')->willReturn($publicKey); + $finder->getIterator()->willReturn(new \ArrayIterator([$file->reveal()])); + $localMachineHelper->getFinder()->willReturn($finder); + + return $fileName; + } + + /** + * @return \Acquia\Cli\Command\Api\ApiCommandFactory + */ + protected function getCommandFactory(): CommandFactoryInterface + { + return new ApiCommandFactory( + $this->localMachineHelper, + $this->datastoreCloud, + $this->datastoreAcli, + $this->cloudCredentials, + $this->telemetryHelper, + $this->projectDir, + $this->clientServiceProphecy->reveal(), + $this->sshHelper, + $this->sshDir, + $this->logger, + ); + } + + /** + * @return array + */ + protected function getApiCommands(): array + { + $apiCommandHelper = new ApiCommandHelper($this->logger); + $commandFactory = $this->getCommandFactory(); + return $apiCommandHelper->getApiCommands($this->apiSpecFixtureFilePath, $this->apiCommandPrefix, $commandFactory); + } + + protected function getApiCommandByName(string $name): ApiBaseCommand|null + { + $apiCommands = $this->getApiCommands(); + foreach ($apiCommands as $apiCommand) { + if ($apiCommand->getName() === $name) { + return $apiCommand; + } + } + + return null; + } + + /** + * @return array + */ + protected function getMockedGitLabProject(mixed $projectId): array + { + return [ + 'default_branch' => 'master', + 'description' => '', + 'http_url_to_repo' => 'https://code.cloudservices.acquia.io/matthew.grasmick/codestudiodemo.git', + 'id' => $projectId, + 'name' => 'codestudiodemo', + 'name_with_namespace' => 'Matthew Grasmick / codestudiodemo', + 'path' => 'codestudiodemo', + 'path_with_namespace' => 'matthew.grasmick/codestudiodemo', + 'topics' => [ + 0 => 'Acquia Cloud Application', + ], + 'web_url' => 'https://code.cloudservices.acquia.io/matthew.grasmick/codestudiodemo', + ]; + } + + /** + * @return \Prophecy\Prophecy\ObjectProphecy|\Gitlab\Client + */ + protected function mockGitLabAuthenticate(ObjectProphecy|LocalMachineHelper $localMachineHelper, string $gitlabHost, string $gitlabToken): ObjectProphecy|\Gitlab\Client + { + $this->mockGitlabGetHost($localMachineHelper, $gitlabHost); + $this->mockGitlabGetToken($localMachineHelper, $gitlabToken, $gitlabHost); + $gitlabClient = $this->prophet->prophesize(\Gitlab\Client::class); + $gitlabClient->users()->willThrow(RuntimeException::class); + return $gitlabClient; + } + + protected function mockGitlabGetToken(mixed $localMachineHelper, string $gitlabToken, string $gitlabHost, bool $success = true): void + { + $process = $this->mockProcess($success); + $process->getOutput()->willReturn($gitlabToken); + $localMachineHelper->execute([ + 'glab', + 'config', + 'get', + 'token', + '--host=' . $gitlabHost, + ], null, null, false)->willReturn($process->reveal()); + } + + protected function mockGitlabGetHost(mixed $localMachineHelper, string $gitlabHost): void + { + $process = $this->mockProcess(); + $process->getOutput()->willReturn($gitlabHost); + $localMachineHelper->execute([ + 'glab', + 'config', + 'get', + 'host', + ], null, null, false)->willReturn($process->reveal()); + } + + protected function mockGitLabUsersMe(ObjectProphecy|\Gitlab\Client $gitlabClient): void + { + $users = $this->prophet->prophesize(Users::class); + $me = [ + 'avatar_url' => 'https://secure.gravatar.com/avatar/5ee7b8ad954bf7156e6eb57a45d60dec?s=80&d=identicon', + 'bio' => '', + 'bot' => false, + 'can_create_group' => true, + 'can_create_project' => true, + 'color_scheme_id' => 1, + 'commit_email' => 'matthew.grasmick@acquia.com', + 'confirmed_at' => '2021-12-21T02:26:51.898Z', + 'created_at' => '2021-12-21T02:26:52.240Z', + 'current_sign_in_at' => '2022-01-22T01:40:55.418Z', + 'email' => 'matthew.grasmick@acquia.com', + 'external' => false, + 'followers' => 0, + 'following' => 0, + 'id' => 20, + 'identities' => [], + 'is_admin' => true, + 'job_title' => '', + 'last_activity_on' => '2022-01-22', + 'last_sign_in_at' => '2022-01-21T23:00:49.035Z', + 'linkedin' => '', + 'local_time' => '2:00 AM', + 'location' => null, + 'name' => 'Matthew Grasmick', + 'note' => '', + 'organization' => null, + 'private_profile' => false, + 'projects_limit' => 100000, + 'pronouns' => null, + 'public_email' => '', + 'skype' => '', + 'state' => 'active', + 'theme_id' => 1, + 'twitter' => '', + 'two_factor_enabled' => false, + 'username' => 'matthew.grasmick', + 'website_url' => '', + 'web_url' => 'https://code.dev.cloudservices.acquia.io/matthew.grasmick', + 'work_information' => null, + ]; + $users->me()->willReturn($me); + $gitlabClient->users()->willReturn($users->reveal()); + } + + /** + * @param $applicationUuid + * @return array + */ + protected function mockGitLabPermissionsRequest(mixed $applicationUuid): array + { + $permissionsResponse = $this->getMockResponseFromSpec('/applications/{applicationUuid}/permissions', 'get', 200); + $permissions = $permissionsResponse->_embedded->items; + $permission = clone reset($permissions); + $permission->name = "administer environment variables on non-prod"; + $permissions[] = $permission; + $this->clientProphecy->request('get', "/applications/{$applicationUuid}/permissions") + ->willReturn($permissions) + ->shouldBeCalled(); + return $permissions; } - return $sshHelper; - } - - protected function mockPollCloudGitViaSsh(object $environmentResponse): ObjectProphecy { - $process = $this->prophet->prophesize(Process::class); - $process->isSuccessful()->willReturn(TRUE); - $process->getExitCode()->willReturn(128); - $sshHelper = $this->mockSshHelper(); - $sshHelper->executeCommand($environmentResponse->vcs->url, ['ls'], FALSE) - ->willReturn($process->reveal()) - ->shouldBeCalled(); - return $sshHelper; - } - - protected function mockGetLocalSshKey(mixed $localMachineHelper, mixed $fileSystem, mixed $publicKey): string { - $fileSystem->exists(Argument::type('string'))->willReturn(TRUE); - /** @var \Symfony\Component\Finder\Finder|\Prophecy\Prophecy\ObjectProphecy $finder */ - $finder = $this->prophet->prophesize(Finder::class); - $finder->files()->willReturn($finder); - $finder->in(Argument::type('string'))->willReturn($finder); - $finder->name(Argument::type('string'))->willReturn($finder); - $finder->ignoreUnreadableDirs()->willReturn($finder); - $file = $this->prophet->prophesize(SplFileInfo::class); - $fileName = 'id_rsa.pub'; - $file->getFileName()->willReturn($fileName); - $file->getRealPath()->willReturn('somepath'); - $localMachineHelper->readFile('somepath')->willReturn($publicKey); - $finder->getIterator()->willReturn(new \ArrayIterator([$file->reveal()])); - $localMachineHelper->getFinder()->willReturn($finder); - - return $fileName; - } - - /** - * @return \Acquia\Cli\Command\Api\ApiCommandFactory - */ - protected function getCommandFactory(): CommandFactoryInterface { - return new ApiCommandFactory( - $this->localMachineHelper, - $this->datastoreCloud, - $this->datastoreAcli, - $this->cloudCredentials, - $this->telemetryHelper, - $this->projectDir, - $this->clientServiceProphecy->reveal(), - $this->sshHelper, - $this->sshDir, - $this->logger, - ); - } - - /** - * @return array - */ - protected function getApiCommands(): array { - $apiCommandHelper = new ApiCommandHelper($this->logger); - $commandFactory = $this->getCommandFactory(); - return $apiCommandHelper->getApiCommands($this->apiSpecFixtureFilePath, $this->apiCommandPrefix, $commandFactory); - } - - protected function getApiCommandByName(string $name): ApiBaseCommand|null { - $apiCommands = $this->getApiCommands(); - foreach ($apiCommands as $apiCommand) { - if ($apiCommand->getName() === $name) { - return $apiCommand; - } - } - - return NULL; - } - - /** - * @return array - */ - protected function getMockedGitLabProject(mixed $projectId): array { - return [ - 'default_branch' => 'master', - 'description' => '', - 'http_url_to_repo' => 'https://code.cloudservices.acquia.io/matthew.grasmick/codestudiodemo.git', - 'id' => $projectId, - 'name' => 'codestudiodemo', - 'name_with_namespace' => 'Matthew Grasmick / codestudiodemo', - 'path' => 'codestudiodemo', - 'path_with_namespace' => 'matthew.grasmick/codestudiodemo', - 'topics' => [ - 0 => 'Acquia Cloud Application', + + protected function mockGetGitLabProjects(mixed $applicationUuid, mixed $gitlabProjectId, mixed $mockedGitlabProjects): Projects|ObjectProphecy + { + $projects = $this->prophet->prophesize(Projects::class); + $projects->all(['search' => $applicationUuid]) + ->willReturn($mockedGitlabProjects); + $projects->all() + ->willReturn([$this->getMockedGitLabProject($gitlabProjectId)]); + return $projects; + } + + /** + * @return array + */ + protected function getMockGitLabVariables(): array + { + return [ + 0 => [ + 'environment_scope' => '*', + 'key' => 'ACQUIA_APPLICATION_UUID', + 'masked' => true, + 'protected' => false, + 'value' => 'a47ac10b-58cc-4372-a567-0e02b2c3d470', + 'variable_type' => 'env_var', ], - 'web_url' => 'https://code.cloudservices.acquia.io/matthew.grasmick/codestudiodemo', - ]; - } - - /** - * @return \Prophecy\Prophecy\ObjectProphecy|\Gitlab\Client - */ - protected function mockGitLabAuthenticate(ObjectProphecy|LocalMachineHelper $localMachineHelper, string $gitlabHost, string $gitlabToken): ObjectProphecy|\Gitlab\Client { - $this->mockGitlabGetHost($localMachineHelper, $gitlabHost); - $this->mockGitlabGetToken($localMachineHelper, $gitlabToken, $gitlabHost); - $gitlabClient = $this->prophet->prophesize(\Gitlab\Client::class); - $gitlabClient->users()->willThrow(RuntimeException::class); - return $gitlabClient; - } - - protected function mockGitlabGetToken(mixed $localMachineHelper, string $gitlabToken, string $gitlabHost, bool $success = TRUE): void { - $process = $this->mockProcess($success); - $process->getOutput()->willReturn($gitlabToken); - $localMachineHelper->execute([ - 'glab', - 'config', - 'get', - 'token', - '--host=' . $gitlabHost, - ], NULL, NULL, FALSE)->willReturn($process->reveal()); - } - - protected function mockGitlabGetHost(mixed $localMachineHelper, string $gitlabHost): void { - $process = $this->mockProcess(); - $process->getOutput()->willReturn($gitlabHost); - $localMachineHelper->execute([ - 'glab', - 'config', - 'get', - 'host', - ], NULL, NULL, FALSE)->willReturn($process->reveal()); - } - - protected function mockGitLabUsersMe(ObjectProphecy|\Gitlab\Client $gitlabClient): void { - $users = $this->prophet->prophesize(Users::class); - $me = [ - 'avatar_url' => 'https://secure.gravatar.com/avatar/5ee7b8ad954bf7156e6eb57a45d60dec?s=80&d=identicon', - 'bio' => '', - 'bot' => FALSE, - 'can_create_group' => TRUE, - 'can_create_project' => TRUE, - 'color_scheme_id' => 1, - 'commit_email' => 'matthew.grasmick@acquia.com', - 'confirmed_at' => '2021-12-21T02:26:51.898Z', - 'created_at' => '2021-12-21T02:26:52.240Z', - 'current_sign_in_at' => '2022-01-22T01:40:55.418Z', - 'email' => 'matthew.grasmick@acquia.com', - 'external' => FALSE, - 'followers' => 0, - 'following' => 0, - 'id' => 20, - 'identities' => [], - 'is_admin' => TRUE, - 'job_title' => '', - 'last_activity_on' => '2022-01-22', - 'last_sign_in_at' => '2022-01-21T23:00:49.035Z', - 'linkedin' => '', - 'local_time' => '2:00 AM', - 'location' => NULL, - 'name' => 'Matthew Grasmick', - 'note' => '', - 'organization' => NULL, - 'private_profile' => FALSE, - 'projects_limit' => 100000, - 'pronouns' => NULL, - 'public_email' => '', - 'skype' => '', - 'state' => 'active', - 'theme_id' => 1, - 'twitter' => '', - 'two_factor_enabled' => FALSE, - 'username' => 'matthew.grasmick', - 'website_url' => '', - 'web_url' => 'https://code.dev.cloudservices.acquia.io/matthew.grasmick', - 'work_information' => NULL, - ]; - $users->me()->willReturn($me); - $gitlabClient->users()->willReturn($users->reveal()); - } - - /** - * @param $applicationUuid - * @return array - */ - protected function mockGitLabPermissionsRequest(mixed $applicationUuid): array { - $permissionsResponse = $this->getMockResponseFromSpec('/applications/{applicationUuid}/permissions', 'get', 200); - $permissions = $permissionsResponse->_embedded->items; - $permission = clone reset($permissions); - $permission->name = "administer environment variables on non-prod"; - $permissions[] = $permission; - $this->clientProphecy->request('get', "/applications/{$applicationUuid}/permissions") - ->willReturn($permissions) - ->shouldBeCalled(); - return $permissions; - } - - protected function mockGetGitLabProjects(mixed $applicationUuid, mixed $gitlabProjectId, mixed $mockedGitlabProjects): Projects|ObjectProphecy { - $projects = $this->prophet->prophesize(Projects::class); - $projects->all(['search' => $applicationUuid]) - ->willReturn($mockedGitlabProjects); - $projects->all() - ->willReturn([$this->getMockedGitLabProject($gitlabProjectId)]); - return $projects; - } - - /** - * @return array - */ - protected function getMockGitLabVariables(): array { - return [ - 0 => [ - 'environment_scope' => '*', - 'key' => 'ACQUIA_APPLICATION_UUID', - 'masked' => TRUE, - 'protected' => FALSE, - 'value' => 'a47ac10b-58cc-4372-a567-0e02b2c3d470', - 'variable_type' => 'env_var', + 1 => [ + 'environment_scope' => '*', + 'key' => 'ACQUIA_CLOUD_API_TOKEN_KEY', + 'masked' => true, + 'protected' => false, + 'value' => '17feaf34-5d04-402b-9a67-15d5161d24e1', + 'variable_type' => 'env_var', ], - 1 => [ - 'environment_scope' => '*', - 'key' => 'ACQUIA_CLOUD_API_TOKEN_KEY', - 'masked' => TRUE, - 'protected' => FALSE, - 'value' => '17feaf34-5d04-402b-9a67-15d5161d24e1', - 'variable_type' => 'env_var', - ], - 2 => [ - 'key' => 'ACQUIA_CLOUD_API_TOKEN_SECRET', - 'masked' => FALSE, - 'protected' => FALSE, - 'value' => 'X1u\/PIQXtYaoeui.4RJSJpGZjwmWYmfl5AUQkAebYE=', - 'variable_type' => 'env_var', - ], - ]; - } - - /** - * Normalize strings for Windows tests. - * - * @todo Remove for PHPUnit 10. - */ - final public static function assertStringContainsStringIgnoringLineEndings(string $needle, string $haystack, string $message = ''): void { - $haystack = strtr( - $haystack, - [ - "\r" => "\n", - "\r\n" => "\n", - ] - ); - static::assertThat($haystack, new StringContains($needle, FALSE), $message); - } + 2 => [ + 'key' => 'ACQUIA_CLOUD_API_TOKEN_SECRET', + 'masked' => false, + 'protected' => false, + 'value' => 'X1u\/PIQXtYaoeui.4RJSJpGZjwmWYmfl5AUQkAebYE=', + 'variable_type' => 'env_var', + ], + ]; + } + /** + * Normalize strings for Windows tests. + * + * @todo Remove for PHPUnit 10. + */ + final public static function assertStringContainsStringIgnoringLineEndings(string $needle, string $haystack, string $message = ''): void + { + $haystack = strtr( + $haystack, + [ + "\r" => "\n", + "\r\n" => "\n", + ] + ); + static::assertThat($haystack, new StringContains($needle, false), $message); + } } diff --git a/tests/phpunit/src/Commands/Acsf/AcsfApiCommandTest.php b/tests/phpunit/src/Commands/Acsf/AcsfApiCommandTest.php index e4bc4f748..428ed6b02 100644 --- a/tests/phpunit/src/Commands/Acsf/AcsfApiCommandTest.php +++ b/tests/phpunit/src/Commands/Acsf/AcsfApiCommandTest.php @@ -1,6 +1,6 @@ clientProphecy->addOption('headers', ['Accept' => 'application/hal+json, version=2']); - putenv('ACQUIA_CLI_USE_CLOUD_API_SPEC_CACHE=1'); - } - - protected function createCommand(): CommandBase { - $this->createMockCloudConfigFile($this->getAcsfCredentialsFileContents()); - $this->cloudCredentials = new AcsfCredentials($this->datastoreCloud); - $this->setClientProphecies(); - return $this->injectCommand(ApiBaseCommand::class); - } - - public function testAcsfCommandExecutionForHttpPostWithMultipleDataTypes(): void { - $mockBody = $this->getMockResponseFromSpec('/api/v1/groups/{group_id}/members', 'post', '200'); - $this->clientProphecy->request('post', '/api/v1/groups/1/members')->willReturn($mockBody)->shouldBeCalled(); - $this->clientProphecy->addOption('json', ["uids" => ["1", "2", "3"]])->shouldBeCalled(); - $this->command = $this->getApiCommandByName('acsf:groups:add-members'); - $this->executeCommand([ - 'uids' => '1,2,3', - ], [ - // group_id. - '1', - ]); - - // Assert. - $output = $this->getDisplay(); - } - - public function testAcsfCommandExecutionBool(): void { - $mockBody = $this->getMockResponseFromSpec('/api/v1/update/pause', 'post', '200'); - $this->clientProphecy->request('post', '/api/v1/update/pause')->willReturn($mockBody)->shouldBeCalled(); - $this->clientProphecy->addOption('json', ["pause" => TRUE])->shouldBeCalled(); - $this->command = $this->getApiCommandByName('acsf:updates:pause'); - $this->executeCommand([], [ - // Pause. - '1', - ]); - - // Assert. - } - - public function testAcsfCommandExecutionForHttpGet(): void { - $mockBody = $this->getMockResponseFromSpec('/api/v1/audit', 'get', '200'); - $this->clientProphecy->addQuery('limit', '1')->shouldBeCalled(); - $this->clientProphecy->request('get', '/api/v1/audit')->willReturn($mockBody)->shouldBeCalled(); - $this->command = $this->getApiCommandByName('acsf:info:audit-events-find'); - // Our mock Client doesn't actually return a limited dataset, but we still assert it was passed added to the - // client's query correctly. - $this->executeCommand(['--limit' => '1']); - - // Assert. - $output = $this->getDisplay(); - $this->assertNotNull($output); - $this->assertJson($output); - $contents = json_decode($output, TRUE); - $this->assertArrayHasKey('count', $contents); - } - - /** - * @return array - */ - public function providerTestAcsfCommandExecutionForHttpGetMultiple(): array { - return [ - ['get', '/api/v1/audit', '/api/v1/audit', 'acsf:info:audit-events-find', [], []], - ['post', '/api/v1/sites', '/api/v1/sites', 'acsf:sites:create', ['site_name' => 'foobar', '--stack_id' => '1', 'group_ids' => ['91,81']], ['site_name' => 'foobar', 'stack_id' => '1', 'group_ids' => [91, 81]]], - ['post', '/api/v1/sites', '/api/v1/sites', 'acsf:sites:create', ['site_name' => 'foobar', '--stack_id' => '1', 'group_ids' => ['91','81']], ['site_name' => 'foobar', 'stack_id' => '1', 'group_ids' => [91, 81]]], - ['post', '/api/v1/sites/{site_id}/backup', '/api/v1/sites/1/backup', 'acsf:sites:backup', ['--label' => 'foo', 'site_id' => '1'], ['label' => 'foo']], - ['post', '/api/v1/groups/{group_id}/members', '/api/v1/groups/2/members', 'acsf:groups:add-members', ['group_id' => '2', 'uids' => '1'], ['group_id' => 'foo', 'uids' => 1]], - ['post', '/api/v1/groups/{group_id}/members', '/api/v1/groups/2/members', 'acsf:groups:add-members', ['group_id' => '2', 'uids' => '1,3'], ['group_id' => 'foo', 'uids' => [1, 3]]], - ]; - } - - /** - * @dataProvider providerTestAcsfCommandExecutionForHttpGetMultiple - */ - public function testAcsfCommandExecutionForHttpGetMultiple(string $method, string $specPath, string $path, string $command, array $arguments = [], array $jsonArguments = []): void { - $mockBody = $this->getMockResponseFromSpec($specPath, $method, '200'); - $this->clientProphecy->request($method, $path)->willReturn($mockBody)->shouldBeCalled(); - foreach ($jsonArguments as $argumentName => $value) { - $this->clientProphecy->addOption('json', [$argumentName => $value]); +class AcsfApiCommandTest extends AcsfCommandTestBase +{ + public function setUp(): void + { + parent::setUp(); + $this->clientProphecy->addOption('headers', ['Accept' => 'application/hal+json, version=2']); + putenv('ACQUIA_CLI_USE_CLOUD_API_SPEC_CACHE=1'); } - $this->command = $this->getApiCommandByName($command); - $this->executeCommand($arguments); - - // Assert. - $output = $this->getDisplay(); - $this->assertNotNull($output); - $this->assertJson($output); - json_decode($output, TRUE); - } - - public function testAcsfUnauthenticatedFailure(): void { - $this->clientServiceProphecy->isMachineAuthenticated()->willReturn(FALSE); - $this->removeMockConfigFiles(); - - $inputs = [ - // Would you like to share anonymous performance usage and data? - 'n', - ]; - $this->expectException(AcquiaCliException::class); - $this->expectExceptionMessage('This machine is not yet authenticated with Site Factory.'); - $this->executeCommand([], $inputs); - } - - protected function setClientProphecies(): void { - $this->clientProphecy = $this->prophet->prophesize(AcsfClient::class); - $this->clientProphecy->addOption('headers', ['User-Agent' => 'acli/UNKNOWN']); - $this->clientProphecy->addOption('debug', Argument::type(OutputInterface::class)); - $this->clientServiceProphecy = $this->prophet->prophesize(AcsfClientService::class); - $this->clientServiceProphecy->getClient() - ->willReturn($this->clientProphecy->reveal()); - $this->clientServiceProphecy->isMachineAuthenticated() - ->willReturn(TRUE); - } - - protected function getCommandFactory(): CommandFactoryInterface { - return new AcsfCommandFactory( - $this->localMachineHelper, - $this->datastoreCloud, - $this->datastoreAcli, - $this->cloudCredentials, - $this->telemetryHelper, - $this->projectDir, - $this->clientServiceProphecy->reveal(), - $this->sshHelper, - $this->sshDir, - $this->logger, - ); - } + protected function createCommand(): CommandBase + { + $this->createMockCloudConfigFile($this->getAcsfCredentialsFileContents()); + $this->cloudCredentials = new AcsfCredentials($this->datastoreCloud); + $this->setClientProphecies(); + return $this->injectCommand(ApiBaseCommand::class); + } + + public function testAcsfCommandExecutionForHttpPostWithMultipleDataTypes(): void + { + $mockBody = $this->getMockResponseFromSpec('/api/v1/groups/{group_id}/members', 'post', '200'); + $this->clientProphecy->request('post', '/api/v1/groups/1/members')->willReturn($mockBody)->shouldBeCalled(); + $this->clientProphecy->addOption('json', ["uids" => ["1", "2", "3"]])->shouldBeCalled(); + $this->command = $this->getApiCommandByName('acsf:groups:add-members'); + $this->executeCommand([ + 'uids' => '1,2,3', + ], [ + // group_id. + '1', + ]); + + // Assert. + $output = $this->getDisplay(); + } + + public function testAcsfCommandExecutionBool(): void + { + $mockBody = $this->getMockResponseFromSpec('/api/v1/update/pause', 'post', '200'); + $this->clientProphecy->request('post', '/api/v1/update/pause')->willReturn($mockBody)->shouldBeCalled(); + $this->clientProphecy->addOption('json', ["pause" => true])->shouldBeCalled(); + $this->command = $this->getApiCommandByName('acsf:updates:pause'); + $this->executeCommand([], [ + // Pause. + '1', + ]); + + // Assert. + } + + public function testAcsfCommandExecutionForHttpGet(): void + { + $mockBody = $this->getMockResponseFromSpec('/api/v1/audit', 'get', '200'); + $this->clientProphecy->addQuery('limit', '1')->shouldBeCalled(); + $this->clientProphecy->request('get', '/api/v1/audit')->willReturn($mockBody)->shouldBeCalled(); + $this->command = $this->getApiCommandByName('acsf:info:audit-events-find'); + // Our mock Client doesn't actually return a limited dataset, but we still assert it was passed added to the + // client's query correctly. + $this->executeCommand(['--limit' => '1']); + + // Assert. + $output = $this->getDisplay(); + $this->assertNotNull($output); + $this->assertJson($output); + $contents = json_decode($output, true); + $this->assertArrayHasKey('count', $contents); + } + + /** + * @return array + */ + public function providerTestAcsfCommandExecutionForHttpGetMultiple(): array + { + return [ + ['get', '/api/v1/audit', '/api/v1/audit', 'acsf:info:audit-events-find', [], []], + ['post', '/api/v1/sites', '/api/v1/sites', 'acsf:sites:create', ['site_name' => 'foobar', '--stack_id' => '1', 'group_ids' => ['91,81']], ['site_name' => 'foobar', 'stack_id' => '1', 'group_ids' => [91, 81]]], + ['post', '/api/v1/sites', '/api/v1/sites', 'acsf:sites:create', ['site_name' => 'foobar', '--stack_id' => '1', 'group_ids' => ['91','81']], ['site_name' => 'foobar', 'stack_id' => '1', 'group_ids' => [91, 81]]], + ['post', '/api/v1/sites/{site_id}/backup', '/api/v1/sites/1/backup', 'acsf:sites:backup', ['--label' => 'foo', 'site_id' => '1'], ['label' => 'foo']], + ['post', '/api/v1/groups/{group_id}/members', '/api/v1/groups/2/members', 'acsf:groups:add-members', ['group_id' => '2', 'uids' => '1'], ['group_id' => 'foo', 'uids' => 1]], + ['post', '/api/v1/groups/{group_id}/members', '/api/v1/groups/2/members', 'acsf:groups:add-members', ['group_id' => '2', 'uids' => '1,3'], ['group_id' => 'foo', 'uids' => [1, 3]]], + ]; + } + + /** + * @dataProvider providerTestAcsfCommandExecutionForHttpGetMultiple + */ + public function testAcsfCommandExecutionForHttpGetMultiple(string $method, string $specPath, string $path, string $command, array $arguments = [], array $jsonArguments = []): void + { + $mockBody = $this->getMockResponseFromSpec($specPath, $method, '200'); + $this->clientProphecy->request($method, $path)->willReturn($mockBody)->shouldBeCalled(); + foreach ($jsonArguments as $argumentName => $value) { + $this->clientProphecy->addOption('json', [$argumentName => $value]); + } + $this->command = $this->getApiCommandByName($command); + $this->executeCommand($arguments); + + // Assert. + $output = $this->getDisplay(); + $this->assertNotNull($output); + $this->assertJson($output); + json_decode($output, true); + } + + public function testAcsfUnauthenticatedFailure(): void + { + $this->clientServiceProphecy->isMachineAuthenticated()->willReturn(false); + $this->removeMockConfigFiles(); + + $inputs = [ + // Would you like to share anonymous performance usage and data? + 'n', + ]; + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage('This machine is not yet authenticated with Site Factory.'); + $this->executeCommand([], $inputs); + } + + protected function setClientProphecies(): void + { + $this->clientProphecy = $this->prophet->prophesize(AcsfClient::class); + $this->clientProphecy->addOption('headers', ['User-Agent' => 'acli/UNKNOWN']); + $this->clientProphecy->addOption('debug', Argument::type(OutputInterface::class)); + $this->clientServiceProphecy = $this->prophet->prophesize(AcsfClientService::class); + $this->clientServiceProphecy->getClient() + ->willReturn($this->clientProphecy->reveal()); + $this->clientServiceProphecy->isMachineAuthenticated() + ->willReturn(true); + } + + protected function getCommandFactory(): CommandFactoryInterface + { + return new AcsfCommandFactory( + $this->localMachineHelper, + $this->datastoreCloud, + $this->datastoreAcli, + $this->cloudCredentials, + $this->telemetryHelper, + $this->projectDir, + $this->clientServiceProphecy->reveal(), + $this->sshHelper, + $this->sshDir, + $this->logger, + ); + } } diff --git a/tests/phpunit/src/Commands/Acsf/AcsfAuthLoginCommandTest.php b/tests/phpunit/src/Commands/Acsf/AcsfAuthLoginCommandTest.php index 1ba32949a..bab52a8ac 100644 --- a/tests/phpunit/src/Commands/Acsf/AcsfAuthLoginCommandTest.php +++ b/tests/phpunit/src/Commands/Acsf/AcsfAuthLoginCommandTest.php @@ -1,6 +1,6 @@ cloudCredentials = new AcsfCredentials($this->datastoreCloud); - return $this->injectCommand(AuthAcsfLoginCommand::class); - } +class AcsfAuthLoginCommandTest extends AcsfCommandTestBase +{ + protected function createCommand(): CommandBase + { + $this->cloudCredentials = new AcsfCredentials($this->datastoreCloud); + return $this->injectCommand(AuthAcsfLoginCommand::class); + } - /** - * @return array - */ - public function providerTestAuthLoginCommand(): array { - return [ - // Data set 0. - [ + /** + * @return array + */ + public function providerTestAuthLoginCommand(): array + { + return [ + // Data set 0. + [ // $machineIsAuthenticated - FALSE, + false, // $inputs [ - // Would you like to share anonymous performance usage and data? (yes/no) [yes]. - 'yes', - // Enter the full URL of the factory. - $this->acsfCurrentFactoryUrl, - // Enter a value for username. - $this->acsfUsername, - // Enter a value for key. - $this->acsfKey, + // Would you like to share anonymous performance usage and data? (yes/no) [yes]. + 'yes', + // Enter the full URL of the factory. + $this->acsfCurrentFactoryUrl, + // Enter a value for username. + $this->acsfUsername, + // Enter a value for key. + $this->acsfKey, ], // No arguments, all interactive. [], // Output to assert. 'Saved credentials', - ], - // Data set 1. - [ + ], + // Data set 1. + [ // $machineIsAuthenticated - FALSE, + false, // $inputs [], // Arguments. [ - // Enter the full URL of the factory. - '--factory-url' => $this->acsfCurrentFactoryUrl, - // Enter a value for key. - '--key' => $this->acsfKey, - // Enter a value for username. - '--username' => $this->acsfUsername, + // Enter the full URL of the factory. + '--factory-url' => $this->acsfCurrentFactoryUrl, + // Enter a value for key. + '--key' => $this->acsfKey, + // Enter a value for username. + '--username' => $this->acsfUsername, ], // Output to assert. 'Saved credentials', // $config. $this->getAcsfCredentialsFileContents(), - ], - // Data set 2. - [ + ], + // Data set 2. + [ // $machineIsAuthenticated - TRUE, + true, // $inputs [ - // Choose a factory to log in to. - $this->acsfCurrentFactoryUrl, - // Choose which user to log in as. - $this->acsfUsername, + // Choose a factory to log in to. + $this->acsfCurrentFactoryUrl, + // Choose which user to log in as. + $this->acsfUsername, ], // Arguments. [], @@ -82,59 +84,58 @@ public function providerTestAuthLoginCommand(): array { "Acquia CLI is now logged in to $this->acsfCurrentFactoryUrl as $this->acsfUsername", // $config. $this->getAcsfCredentialsFileContents(), - ], - ]; - } - - /** - * @dataProvider providerTestAuthLoginCommand - * @requires OS linux|darwin - */ - public function testAcsfAuthLoginCommand(bool $machineIsAuthenticated, array $inputs, array $args, string $outputToAssert, array $config = []): void { - if (!$machineIsAuthenticated) { - $this->clientServiceProphecy->isMachineAuthenticated()->willReturn(FALSE); - $this->removeMockCloudConfigFile(); - } - else { - $this->removeMockCloudConfigFile(); - $this->createMockCloudConfigFile($config); - } - $this->createDataStores(); - $this->command = $this->createCommand(); - - $this->executeCommand($args, $inputs); - $output = $this->getDisplay(); - $this->assertStringContainsString($outputToAssert, $output); - if (!$machineIsAuthenticated && !array_key_exists('--key', $args)) { - $this->assertStringContainsString('Enter your Site Factory key (option -k, --key) (input will be hidden):', $output); + ], + ]; } - $this->assertKeySavedCorrectly(); - $this->assertEquals($this->acsfActiveUser, $this->cloudCredentials->getCloudKey()); - $this->assertEquals($this->acsfKey, $this->cloudCredentials->getCloudSecret()); - $this->assertEquals($this->acsfCurrentFactoryUrl, $this->cloudCredentials->getBaseUri()); - } + /** + * @dataProvider providerTestAuthLoginCommand + * @requires OS linux|darwin + */ + public function testAcsfAuthLoginCommand(bool $machineIsAuthenticated, array $inputs, array $args, string $outputToAssert, array $config = []): void + { + if (!$machineIsAuthenticated) { + $this->clientServiceProphecy->isMachineAuthenticated()->willReturn(false); + $this->removeMockCloudConfigFile(); + } else { + $this->removeMockCloudConfigFile(); + $this->createMockCloudConfigFile($config); + } + $this->createDataStores(); + $this->command = $this->createCommand(); - protected function assertKeySavedCorrectly(): void { - $credsFile = $this->cloudConfigFilepath; - $this->assertFileExists($credsFile); - $config = new CloudDataStore($this->localMachineHelper, new CloudDataConfig(), $credsFile); - $this->assertTrue($config->exists('acsf_active_factory')); - $factoryUrl = $config->get('acsf_active_factory'); - $this->assertTrue($config->exists('acsf_factories')); - $factories = $config->get('acsf_factories'); - $this->assertArrayHasKey($factoryUrl, $factories); - $factory = $factories[$factoryUrl]; - $this->assertArrayHasKey('users', $factory); - $this->assertArrayHasKey('active_user', $factory); - $this->assertEquals($this->acsfUsername, $factory['active_user']); - $users = $factory['users']; - $this->assertArrayHasKey($factory['active_user'], $users); - $user = $users[$factory['active_user']]; - $this->assertArrayHasKey('username', $user); - $this->assertArrayHasKey('key', $user); - $this->assertEquals($this->acsfUsername, $user['username']); - $this->assertEquals($this->acsfKey, $user['key']); - } + $this->executeCommand($args, $inputs); + $output = $this->getDisplay(); + $this->assertStringContainsString($outputToAssert, $output); + if (!$machineIsAuthenticated && !array_key_exists('--key', $args)) { + $this->assertStringContainsString('Enter your Site Factory key (option -k, --key) (input will be hidden):', $output); + } + $this->assertKeySavedCorrectly(); + $this->assertEquals($this->acsfActiveUser, $this->cloudCredentials->getCloudKey()); + $this->assertEquals($this->acsfKey, $this->cloudCredentials->getCloudSecret()); + $this->assertEquals($this->acsfCurrentFactoryUrl, $this->cloudCredentials->getBaseUri()); + } + protected function assertKeySavedCorrectly(): void + { + $credsFile = $this->cloudConfigFilepath; + $this->assertFileExists($credsFile); + $config = new CloudDataStore($this->localMachineHelper, new CloudDataConfig(), $credsFile); + $this->assertTrue($config->exists('acsf_active_factory')); + $factoryUrl = $config->get('acsf_active_factory'); + $this->assertTrue($config->exists('acsf_factories')); + $factories = $config->get('acsf_factories'); + $this->assertArrayHasKey($factoryUrl, $factories); + $factory = $factories[$factoryUrl]; + $this->assertArrayHasKey('users', $factory); + $this->assertArrayHasKey('active_user', $factory); + $this->assertEquals($this->acsfUsername, $factory['active_user']); + $users = $factory['users']; + $this->assertArrayHasKey($factory['active_user'], $users); + $user = $users[$factory['active_user']]; + $this->assertArrayHasKey('username', $user); + $this->assertArrayHasKey('key', $user); + $this->assertEquals($this->acsfUsername, $user['username']); + $this->assertEquals($this->acsfKey, $user['key']); + } } diff --git a/tests/phpunit/src/Commands/Acsf/AcsfAuthLogoutCommandTest.php b/tests/phpunit/src/Commands/Acsf/AcsfAuthLogoutCommandTest.php index b02959a5a..ca790931a 100644 --- a/tests/phpunit/src/Commands/Acsf/AcsfAuthLogoutCommandTest.php +++ b/tests/phpunit/src/Commands/Acsf/AcsfAuthLogoutCommandTest.php @@ -1,6 +1,6 @@ cloudCredentials = new AcsfCredentials($this->datastoreCloud); - return $this->injectCommand(AuthAcsfLogoutCommand::class); - } +class AcsfAuthLogoutCommandTest extends AcsfCommandTestBase +{ + protected function createCommand(): CommandBase + { + $this->cloudCredentials = new AcsfCredentials($this->datastoreCloud); + return $this->injectCommand(AuthAcsfLogoutCommand::class); + } - /** - * @return array - */ - public function providerTestAuthLogoutCommand(): array { - return [ - // Data set 0. - [ + /** + * @return array + */ + public function providerTestAuthLogoutCommand(): array + { + return [ + // Data set 0. + [ // $machineIsAuthenticated - FALSE, + false, // $inputs [], - ], - // Data set 1. - [ + ], + // Data set 1. + [ // $machineIsAuthenticated - TRUE, + true, // $inputs [ - // Choose a Factory to logout of. - 0, + // Choose a Factory to logout of. + 0, ], // $config. $this->getAcsfCredentialsFileContents(), - ], - ]; - } - - /** - * @dataProvider providerTestAuthLogoutCommand - */ - public function testAcsfAuthLogoutCommand(bool $machineIsAuthenticated, array $inputs, array $config = []): void { - if (!$machineIsAuthenticated) { - $this->clientServiceProphecy->isMachineAuthenticated()->willReturn(FALSE); - $this->removeMockCloudConfigFile(); - } - else { - $this->createMockCloudConfigFile($config); + ], + ]; } - $this->createDataStores(); - $this->command = $this->createCommand(); - $this->executeCommand([], $inputs); - $output = $this->getDisplay(); - // Assert creds are removed locally. - $this->assertFileExists($this->cloudConfigFilepath); - $config = new CloudDataStore($this->localMachineHelper, new CloudDataConfig(), $this->cloudConfigFilepath); - $this->assertFalse($config->exists('acli_key')); - $this->assertNull($config->get('acsf_active_factory')); - } + /** + * @dataProvider providerTestAuthLogoutCommand + */ + public function testAcsfAuthLogoutCommand(bool $machineIsAuthenticated, array $inputs, array $config = []): void + { + if (!$machineIsAuthenticated) { + $this->clientServiceProphecy->isMachineAuthenticated()->willReturn(false); + $this->removeMockCloudConfigFile(); + } else { + $this->createMockCloudConfigFile($config); + } + $this->createDataStores(); + $this->command = $this->createCommand(); + $this->executeCommand([], $inputs); + $output = $this->getDisplay(); + // Assert creds are removed locally. + $this->assertFileExists($this->cloudConfigFilepath); + $config = new CloudDataStore($this->localMachineHelper, new CloudDataConfig(), $this->cloudConfigFilepath); + $this->assertFalse($config->exists('acli_key')); + $this->assertNull($config->get('acsf_active_factory')); + } } diff --git a/tests/phpunit/src/Commands/Acsf/AcsfCommandTestBase.php b/tests/phpunit/src/Commands/Acsf/AcsfCommandTestBase.php index 90f76fb38..826d227c4 100644 --- a/tests/phpunit/src/Commands/Acsf/AcsfCommandTestBase.php +++ b/tests/phpunit/src/Commands/Acsf/AcsfCommandTestBase.php @@ -1,6 +1,6 @@ - */ - protected function getAcsfCredentialsFileContents(): array { - return [ - 'acsf_active_factory' => $this->acsfCurrentFactoryUrl, - 'acsf_factories' => [ + /** + * @return array + */ + protected function getAcsfCredentialsFileContents(): array + { + return [ + 'acsf_active_factory' => $this->acsfCurrentFactoryUrl, + 'acsf_factories' => [ $this->acsfCurrentFactoryUrl => [ - 'active_user' => $this->acsfActiveUser, - 'url' => $this->acsfCurrentFactoryUrl, - 'users' => [ - $this->acsfUsername => [ - 'key' => $this->acsfKey, - 'username' => $this->acsfUsername, - ], - ], + 'active_user' => $this->acsfActiveUser, + 'url' => $this->acsfCurrentFactoryUrl, + 'users' => [ + $this->acsfUsername => [ + 'key' => $this->acsfKey, + 'username' => $this->acsfUsername, ], - ], - DataStoreContract::SEND_TELEMETRY => FALSE, - ]; - } - + ], + ], + ], + DataStoreContract::SEND_TELEMETRY => false, + ]; + } } diff --git a/tests/phpunit/src/Commands/Acsf/AcsfListCommandTest.php b/tests/phpunit/src/Commands/Acsf/AcsfListCommandTest.php index caf262883..375ef7b51 100644 --- a/tests/phpunit/src/Commands/Acsf/AcsfListCommandTest.php +++ b/tests/phpunit/src/Commands/Acsf/AcsfListCommandTest.php @@ -1,6 +1,6 @@ application->addCommands($this->getApiCommands()); - } - - protected function createCommand(): CommandBase { - return $this->injectCommand(AcsfListCommand::class); - } - - public function testAcsfListCommand(): void { - $this->executeCommand(); - $output = $this->getDisplay(); - $this->assertStringContainsString('acsf:api', $output); - $this->assertStringContainsString('acsf:api:ping', $output); - $this->assertStringContainsString('acsf:info:audit-events-find', $output); - } - - public function testApiNamespaceListCommand(): void { - $this->command = $this->injectCommand(AcsfListCommandBase::class); - $name = 'acsf:api'; - $this->command->setName($name); - $this->command->setNamespace($name); - $this->executeCommand(); - $output = $this->getDisplay(); - $this->assertStringContainsString('acsf:api:ping', $output); - $this->assertStringNotContainsString('acsf:groups', $output); - } - - public function testListCommand(): void { - $this->command = $this->injectCommand(ListCommand::class); - $this->executeCommand(); - $output = $this->getDisplay(); - $this->assertStringContainsString('acsf:api', $output); - $this->assertStringNotContainsString('acsf:api:ping', $output); - } - +class AcsfListCommandTest extends AcsfCommandTestBase +{ + public function setUp(): void + { + parent::setUp(); + $this->application->addCommands($this->getApiCommands()); + } + + protected function createCommand(): CommandBase + { + return $this->injectCommand(AcsfListCommand::class); + } + + public function testAcsfListCommand(): void + { + $this->executeCommand(); + $output = $this->getDisplay(); + $this->assertStringContainsString('acsf:api', $output); + $this->assertStringContainsString('acsf:api:ping', $output); + $this->assertStringContainsString('acsf:info:audit-events-find', $output); + } + + public function testApiNamespaceListCommand(): void + { + $this->command = $this->injectCommand(AcsfListCommandBase::class); + $name = 'acsf:api'; + $this->command->setName($name); + $this->command->setNamespace($name); + $this->executeCommand(); + $output = $this->getDisplay(); + $this->assertStringContainsString('acsf:api:ping', $output); + $this->assertStringNotContainsString('acsf:groups', $output); + } + + public function testListCommand(): void + { + $this->command = $this->injectCommand(ListCommand::class); + $this->executeCommand(); + $output = $this->getDisplay(); + $this->assertStringContainsString('acsf:api', $output); + $this->assertStringNotContainsString('acsf:api:ping', $output); + } } diff --git a/tests/phpunit/src/Commands/Api/ApiBaseCommandTest.php b/tests/phpunit/src/Commands/Api/ApiBaseCommandTest.php index 6cf4a8f5f..1f1fa072b 100644 --- a/tests/phpunit/src/Commands/Api/ApiBaseCommandTest.php +++ b/tests/phpunit/src/Commands/Api/ApiBaseCommandTest.php @@ -1,6 +1,6 @@ injectCommand(ApiBaseCommand::class); - } - - public function testApiBaseCommand(): void { - $this->expectException(AcquiaCliException::class); - $this->expectExceptionMessage('api:base is not a valid command'); - $this->executeCommand(); - } +class ApiBaseCommandTest extends CommandTestBase +{ + protected function createCommand(): CommandBase + { + return $this->injectCommand(ApiBaseCommand::class); + } + public function testApiBaseCommand(): void + { + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage('api:base is not a valid command'); + $this->executeCommand(); + } } diff --git a/tests/phpunit/src/Commands/Api/ApiCommandTest.php b/tests/phpunit/src/Commands/Api/ApiCommandTest.php index 4c93a3da3..7bf45b3bf 100644 --- a/tests/phpunit/src/Commands/Api/ApiCommandTest.php +++ b/tests/phpunit/src/Commands/Api/ApiCommandTest.php @@ -1,6 +1,6 @@ injectCommand(ApiBaseCommand::class); - } - - /** - * @group brokenProphecy - */ - public function testArgumentsInteraction(): void { - $this->command = $this->getApiCommandByName('api:environments:log-download'); - $this->executeCommand([], [ - '289576-53785bca-1946-4adc-a022-e50d24686c20', - 'apache-access', - ]); - $output = $this->getDisplay(); - $this->assertStringContainsString('Enter a value for environmentId', $output); - $this->assertStringContainsString('logType is a required argument', $output); - $this->assertStringContainsString('An ID that uniquely identifies a log type.', $output); - $this->assertStringContainsString('apache-access', $output); - $this->assertStringContainsString('Select a value for logType', $output); - } - - public function testArgumentsInteractionValidation(): void { - $this->command = $this->getApiCommandByName('api:environments:variable-update'); - try { - $this->executeCommand([], [ +class ApiCommandTest extends CommandTestBase +{ + public function setUp(): void + { + parent::setUp(); + putenv('ACQUIA_CLI_USE_CLOUD_API_SPEC_CACHE=1'); + } + + protected function createCommand(): CommandBase + { + return $this->injectCommand(ApiBaseCommand::class); + } + + /** + * @group brokenProphecy + */ + public function testArgumentsInteraction(): void + { + $this->command = $this->getApiCommandByName('api:environments:log-download'); + $this->executeCommand([], [ '289576-53785bca-1946-4adc-a022-e50d24686c20', - 'AH_SOMETHING', - 'AH_SOMETHING', - ]); + 'apache-access', + ]); + $output = $this->getDisplay(); + $this->assertStringContainsString('Enter a value for environmentId', $output); + $this->assertStringContainsString('logType is a required argument', $output); + $this->assertStringContainsString('An ID that uniquely identifies a log type.', $output); + $this->assertStringContainsString('apache-access', $output); + $this->assertStringContainsString('Select a value for logType', $output); + } + + public function testArgumentsInteractionValidation(): void + { + $this->command = $this->getApiCommandByName('api:environments:variable-update'); + try { + $this->executeCommand([], [ + '289576-53785bca-1946-4adc-a022-e50d24686c20', + 'AH_SOMETHING', + 'AH_SOMETHING', + ]); + } catch (MissingInputException) { + } + $output = $this->getDisplay(); + $this->assertStringContainsString('It must match the pattern', $output); + } + + public function testArgumentsInteractionValidationFormat(): void + { + $this->command = $this->getApiCommandByName('api:notifications:find'); + try { + $this->executeCommand([], [ + 'test', + ]); + } catch (MissingInputException) { + } + $output = $this->getDisplay(); + $this->assertStringContainsString('This is not a valid UUID', $output); + } + + /** + * Tests invalid UUID. + */ + public function testApiCommandErrorResponse(): void + { + $invalidUuid = '257a5440-22c3-49d1-894d-29497a1cf3b9'; + $this->clientProphecy->addOption('headers', ['Accept' => 'application/hal+json, version=2'])->shouldBeCalled(); + $this->command = $this->getApiCommandByName('api:applications:find'); + $mockBody = $this->getMockResponseFromSpec($this->command->getPath(), $this->command->getMethod(), '404'); + $this->clientProphecy->request('get', '/applications/' . $invalidUuid)->willThrow(new ApiErrorException($mockBody))->shouldBeCalled(); + + // ApiCommandBase::convertApplicationAliastoUuid() will try to convert the invalid string to a uuid: + $this->clientProphecy->addQuery('filter', 'hosting=@*:' . $invalidUuid); + $this->clientProphecy->request('get', '/applications')->willReturn([]); + + $this->executeCommand(['applicationUuid' => $invalidUuid], [ + // Would you like Acquia CLI to search for a Cloud application that matches your local git config? + 'n', + // Select a Cloud Platform application: + '0', + // Would you like to link the Cloud application Sample application to this repository? + 'n', + ]); + + // Assert. + $output = $this->getDisplay(); + $this->assertJson($output); + $this->assertStringContainsString($mockBody->message, $output); + $this->assertEquals(1, $this->getStatusCode()); + } + + public function testApiCommandExecutionForHttpGet(): void + { + $this->clientProphecy->addOption('headers', ['Accept' => 'application/hal+json, version=2'])->shouldBeCalled(); + $mockBody = $this->getMockResponseFromSpec('/account/ssh-keys', 'get', '200'); + $this->clientProphecy->addQuery('limit', '1')->shouldBeCalled(); + $this->clientProphecy->request('get', '/account/ssh-keys')->willReturn($mockBody->{'_embedded'}->items)->shouldBeCalled(); + $this->command = $this->getApiCommandByName('api:accounts:ssh-keys-list'); + // Our mock Client doesn't actually return a limited dataset, but we still assert it was passed added to the + // client's query correctly. + $this->executeCommand(['--limit' => '1']); + + // Assert. + $output = $this->getDisplay(); + $this->assertNotNull($output); + $this->assertJson($output); + $contents = json_decode($output, true); + $this->assertArrayHasKey(0, $contents); + $this->assertArrayHasKey('uuid', $contents[0]); + } + + /** + * @group brokenProphecy + */ + public function testObjectParam(): void + { + $this->mockRequest('putEnvironmentCloudActions', '24-a47ac10b-58cc-4372-a567-0e02b2c3d470'); + $this->command = $this->getApiCommandByName('api:environments:cloud-actions-update'); + $this->executeCommand([ + 'cloud-actions' => '{"fb4aa87a-8be2-42c6-bdf0-ef9d09a3de70":true}', + 'environmentId' => '24-a47ac10b-58cc-4372-a567-0e02b2c3d470', + ]); + $output = $this->getDisplay(); + $this->assertStringContainsString('Cloud Actions have been updated.', $output); + } + + public function testInferApplicationUuidArgument(): void + { + $this->clientProphecy->addOption('headers', ['Accept' => 'application/hal+json, version=2'])->shouldBeCalled(); + $applications = $this->mockRequest('getApplications'); + $application = $this->mockRequest('getApplicationByUuid', $applications[0]->uuid); + $this->command = $this->getApiCommandByName('api:applications:find'); + $this->executeCommand([], [ + // Would you like Acquia CLI to search for a Cloud application that matches your local git config? + 'n', + // Select a Cloud Platform application: + '0', + // Would you like to link the Cloud application Sample application to this repository? + 'n', + ]); + + // Assert. + $output = $this->getDisplay(); + $this->assertStringContainsString('Inferring Cloud Application UUID for this command since none was provided...', $output); + $this->assertStringContainsString('Set application uuid to ' . $application->uuid, $output); + $this->assertEquals(0, $this->getStatusCode()); + } + + /** + * @return bool[][] + */ + public function providerTestConvertApplicationAliasToUuidArgument(): array + { + return [ + [false], + [true], + ]; + } + + /** + * @dataProvider providerTestConvertApplicationAliasToUuidArgument + * @group serial + */ + public function testConvertApplicationAliasToUuidArgument(bool $support): void + { + ClearCacheCommand::clearCaches(); + $this->clientProphecy->addOption('headers', ['Accept' => 'application/hal+json, version=2'])->shouldBeCalled(); + $tamper = function (&$response): void { + unset($response[1]); + }; + $applications = $this->mockRequest('getApplications', null, null, null, $tamper); + $this->mockRequest('getApplicationByUuid', $applications[self::$INPUT_DEFAULT_CHOICE]->uuid); + $this->clientProphecy->addQuery('filter', 'hosting=@*:devcloud2')->shouldBeCalled(); + $this->command = $this->getApiCommandByName('api:applications:find'); + $alias = 'devcloud2'; + $tamper = null; + if ($support) { + $this->clientProphecy->addQuery('all', 'true')->shouldBeCalled(); + $tamper = function (mixed $response): void { + $response->flags->support = true; + }; + } + $this->mockRequest('getAccount', null, null, null, $tamper); + + $this->executeCommand(['applicationUuid' => $alias], [ + // Would you like Acquia CLI to search for a Cloud application that matches your local git config? + 'n', + // Select a Cloud Platform application: + self::$INPUT_DEFAULT_CHOICE, + // Would you like to link the Cloud application Sample application to this repository? + 'n', + ]); + + // Assert. + $this->getDisplay(); + $this->assertEquals(0, $this->getStatusCode()); + } + + public function testConvertInvalidApplicationAliasToUuidArgument(): void + { + $this->mockApplicationsRequest(0); + $this->clientProphecy->addQuery('filter', 'hosting=@*:invalidalias')->shouldBeCalled(); + $this->mockRequest('getAccount'); + $this->command = $this->getApiCommandByName('api:applications:find'); + $alias = 'invalidalias'; + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage('No applications match the alias *:invalidalias'); + $this->executeCommand(['applicationUuid' => $alias], []); + } + + /** + * @serial + */ + public function testConvertNonUniqueApplicationAliasToUuidArgument(): void + { + ClearCacheCommand::clearCaches(); + $this->mockApplicationsRequest(2, false); + $this->clientProphecy->addQuery('filter', 'hosting=@*:devcloud2')->shouldBeCalled(); + $this->mockRequest('getAccount'); + $this->command = $this->getApiCommandByName('api:applications:find'); + $alias = 'devcloud2'; + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage('Multiple applications match the alias *:devcloud2'); + $this->executeCommand(['applicationUuid' => $alias], []); + $output = $this->getDisplay(); + $this->assertStringContainsString('Use a unique application alias: devcloud:devcloud2, devcloud:devcloud2', $output); + } + + /** + * @serial + */ + public function testConvertApplicationAliasWithRealmToUuidArgument(): void + { + $this->clientProphecy->addOption('headers', ['Accept' => 'application/hal+json, version=2'])->shouldBeCalled(); + $this->mockApplicationsRequest(1, false); + $this->clientProphecy->addQuery('filter', 'hosting=@devcloud:devcloud2')->shouldBeCalled(); + $this->mockApplicationRequest(); + $this->mockRequest('getAccount'); + $this->command = $this->getApiCommandByName('api:applications:find'); + $alias = 'devcloud:devcloud2'; + $this->executeCommand(['applicationUuid' => $alias], []); } - catch (MissingInputException) { + + /** + * @serial + */ + public function testConvertEnvironmentAliasToUuidArgument(): void + { + ClearCacheCommand::clearCaches(); + $this->clientProphecy->addOption('headers', ['Accept' => 'application/hal+json, version=2'])->shouldBeCalled(); + $applicationsResponse = $this->mockApplicationsRequest(1); + $this->clientProphecy->addQuery('filter', 'hosting=@*:devcloud2')->shouldBeCalled(); + $this->mockEnvironmentsRequest($applicationsResponse); + $this->mockRequest('getAccount'); + + $response = $this->getMockEnvironmentResponse(); + $this->clientProphecy->request('get', '/environments/24-a47ac10b-58cc-4372-a567-0e02b2c3d470')->willReturn($response)->shouldBeCalled(); + + $this->command = $this->getApiCommandByName('api:environments:find'); + $alias = 'devcloud2.dev'; + + $this->executeCommand(['environmentId' => $alias], [ + // Would you like Acquia CLI to search for a Cloud application that matches your local git config? + 'n', + // Select a Cloud Platform application: + '0', + // Would you like to link the Cloud application Sample application to this repository? + 'n', + ]); + + // Assert. + $this->getDisplay(); + $this->assertEquals(0, $this->getStatusCode()); } - $output = $this->getDisplay(); - $this->assertStringContainsString('It must match the pattern', $output); - } - - public function testArgumentsInteractionValidationFormat(): void { - $this->command = $this->getApiCommandByName('api:notifications:find'); - try { - $this->executeCommand([], [ - 'test', - ]); + + /** + * @group serial + */ + public function testConvertInvalidEnvironmentAliasToUuidArgument(): void + { + ClearCacheCommand::clearCaches(); + $applicationsResponse = $this->mockApplicationsRequest(1); + $this->clientProphecy->addQuery('filter', 'hosting=@*:devcloud2')->shouldBeCalled(); + $this->mockEnvironmentsRequest($applicationsResponse); + $this->mockRequest('getAccount'); + $this->command = $this->getApiCommandByName('api:environments:find'); + $alias = 'devcloud2.invalid'; + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage('{environmentId} must be a valid UUID or site alias.'); + $this->executeCommand(['environmentId' => $alias], []); } - catch (MissingInputException) { + + public function testApiCommandExecutionForHttpPost(): void + { + $this->clientProphecy->addOption('headers', ['Accept' => 'application/hal+json, version=2'])->shouldBeCalled(); + $mockRequestArgs = $this->getMockRequestBodyFromSpec('/account/ssh-keys'); + $mockResponseBody = $this->getMockResponseFromSpec('/account/ssh-keys', 'post', '202'); + foreach ($mockRequestArgs as $name => $value) { + $this->clientProphecy->addOption('json', [$name => $value])->shouldBeCalled(); + } + $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); + $this->assertStringContainsString('Adding SSH key.', $output); } - $output = $this->getDisplay(); - $this->assertStringContainsString('This is not a valid UUID', $output); - } - - /** - * Tests invalid UUID. - */ - public function testApiCommandErrorResponse(): void { - $invalidUuid = '257a5440-22c3-49d1-894d-29497a1cf3b9'; - $this->clientProphecy->addOption('headers', ['Accept' => 'application/hal+json, version=2'])->shouldBeCalled(); - $this->command = $this->getApiCommandByName('api:applications:find'); - $mockBody = $this->getMockResponseFromSpec($this->command->getPath(), $this->command->getMethod(), '404'); - $this->clientProphecy->request('get', '/applications/' . $invalidUuid)->willThrow(new ApiErrorException($mockBody))->shouldBeCalled(); - - // ApiCommandBase::convertApplicationAliastoUuid() will try to convert the invalid string to a uuid: - $this->clientProphecy->addQuery('filter', 'hosting=@*:' . $invalidUuid); - $this->clientProphecy->request('get', '/applications')->willReturn([]); - - $this->executeCommand(['applicationUuid' => $invalidUuid], [ - // Would you like Acquia CLI to search for a Cloud application that matches your local git config? - 'n', - // Select a Cloud Platform application: - '0', - // Would you like to link the Cloud application Sample application to this repository? - 'n', - ]); - - // Assert. - $output = $this->getDisplay(); - $this->assertJson($output); - $this->assertStringContainsString($mockBody->message, $output); - $this->assertEquals(1, $this->getStatusCode()); - } - - public function testApiCommandExecutionForHttpGet(): void { - $this->clientProphecy->addOption('headers', ['Accept' => 'application/hal+json, version=2'])->shouldBeCalled(); - $mockBody = $this->getMockResponseFromSpec('/account/ssh-keys', 'get', '200'); - $this->clientProphecy->addQuery('limit', '1')->shouldBeCalled(); - $this->clientProphecy->request('get', '/account/ssh-keys')->willReturn($mockBody->{'_embedded'}->items)->shouldBeCalled(); - $this->command = $this->getApiCommandByName('api:accounts:ssh-keys-list'); - // Our mock Client doesn't actually return a limited dataset, but we still assert it was passed added to the - // client's query correctly. - $this->executeCommand(['--limit' => '1']); - - // Assert. - $output = $this->getDisplay(); - $this->assertNotNull($output); - $this->assertJson($output); - $contents = json_decode($output, TRUE); - $this->assertArrayHasKey(0, $contents); - $this->assertArrayHasKey('uuid', $contents[0]); - } - - /** - * @group brokenProphecy - */ - public function testObjectParam(): void { - $this->mockRequest('putEnvironmentCloudActions', '24-a47ac10b-58cc-4372-a567-0e02b2c3d470'); - $this->command = $this->getApiCommandByName('api:environments:cloud-actions-update'); - $this->executeCommand([ - 'cloud-actions' => '{"fb4aa87a-8be2-42c6-bdf0-ef9d09a3de70":true}', - 'environmentId' => '24-a47ac10b-58cc-4372-a567-0e02b2c3d470', - ]); - $output = $this->getDisplay(); - $this->assertStringContainsString('Cloud Actions have been updated.', $output); - } - - public function testInferApplicationUuidArgument(): void { - $this->clientProphecy->addOption('headers', ['Accept' => 'application/hal+json, version=2'])->shouldBeCalled(); - $applications = $this->mockRequest('getApplications'); - $application = $this->mockRequest('getApplicationByUuid', $applications[0]->uuid); - $this->command = $this->getApiCommandByName('api:applications:find'); - $this->executeCommand([], [ - // Would you like Acquia CLI to search for a Cloud application that matches your local git config? - 'n', - // Select a Cloud Platform application: - '0', - // Would you like to link the Cloud application Sample application to this repository? - 'n', - ]); - - // Assert. - $output = $this->getDisplay(); - $this->assertStringContainsString('Inferring Cloud Application UUID for this command since none was provided...', $output); - $this->assertStringContainsString('Set application uuid to ' . $application->uuid, $output); - $this->assertEquals(0, $this->getStatusCode()); - } - - /** - * @return bool[][] - */ - public function providerTestConvertApplicationAliasToUuidArgument(): array { - return [ - [FALSE], - [TRUE], - ]; - } - - /** - * @dataProvider providerTestConvertApplicationAliasToUuidArgument - * @group serial - */ - public function testConvertApplicationAliasToUuidArgument(bool $support): void { - ClearCacheCommand::clearCaches(); - $this->clientProphecy->addOption('headers', ['Accept' => 'application/hal+json, version=2'])->shouldBeCalled(); - $tamper = function (&$response): void { - unset($response[1]); - }; - $applications = $this->mockRequest('getApplications', NULL, NULL, NULL, $tamper); - $this->mockRequest('getApplicationByUuid', $applications[self::$INPUT_DEFAULT_CHOICE]->uuid); - $this->clientProphecy->addQuery('filter', 'hosting=@*:devcloud2')->shouldBeCalled(); - $this->command = $this->getApiCommandByName('api:applications:find'); - $alias = 'devcloud2'; - $tamper = NULL; - if ($support) { - $this->clientProphecy->addQuery('all', 'true')->shouldBeCalled(); - $tamper = function (mixed $response): void { - $response->flags->support = TRUE; - }; + + public function testApiCommandExecutionForHttpPut(): void + { + $this->clientProphecy->addOption('headers', ['Accept' => 'application/hal+json, version=2'])->shouldBeCalled(); + $mockRequestOptions = $this->getMockRequestBodyFromSpec('/environments/{environmentId}', 'put'); + $mockRequestOptions['max_input_vars'] = 1001; + $mockResponseBody = $this->getMockEnvironmentResponse('put', '202'); + + foreach ($mockRequestOptions as $name => $value) { + $this->clientProphecy->addOption('json', [$name => $value])->shouldBeCalled(); + } + $this->clientProphecy->request('put', '/environments/24-a47ac10b-58cc-4372-a567-0e02b2c3d470')->willReturn($mockResponseBody)->shouldBeCalled(); + $this->command = $this->getApiCommandByName('api:environments:update'); + + $options = []; + foreach ($mockRequestOptions as $key => $value) { + $options['--' . $key] = $value; + } + $options['--lang_version'] = $options['--version']; + unset($options['--version']); + $args = ['environmentId' => '24-a47ac10b-58cc-4372-a567-0e02b2c3d470'] + $options; + $this->executeCommand($args); + + // Assert. + $output = $this->getDisplay(); + $this->assertNotNull($output); + $this->assertJson($output); + $this->assertStringContainsString('The environment configuration is being updated.', $output); } - $this->mockRequest('getAccount', NULL, NULL, NULL, $tamper); - - $this->executeCommand(['applicationUuid' => $alias], [ - // Would you like Acquia CLI to search for a Cloud application that matches your local git config? - 'n', - // Select a Cloud Platform application: - self::$INPUT_DEFAULT_CHOICE, - // Would you like to link the Cloud application Sample application to this repository? - 'n', - ]); - - // Assert. - $this->getDisplay(); - $this->assertEquals(0, $this->getStatusCode()); - } - - public function testConvertInvalidApplicationAliasToUuidArgument(): void { - $this->mockApplicationsRequest(0); - $this->clientProphecy->addQuery('filter', 'hosting=@*:invalidalias')->shouldBeCalled(); - $this->mockRequest('getAccount'); - $this->command = $this->getApiCommandByName('api:applications:find'); - $alias = 'invalidalias'; - $this->expectException(AcquiaCliException::class); - $this->expectExceptionMessage('No applications match the alias *:invalidalias'); - $this->executeCommand(['applicationUuid' => $alias], []); - - } - - /** - * @serial - */ - public function testConvertNonUniqueApplicationAliasToUuidArgument(): void { - ClearCacheCommand::clearCaches(); - $this->mockApplicationsRequest(2, FALSE); - $this->clientProphecy->addQuery('filter', 'hosting=@*:devcloud2')->shouldBeCalled(); - $this->mockRequest('getAccount'); - $this->command = $this->getApiCommandByName('api:applications:find'); - $alias = 'devcloud2'; - $this->expectException(AcquiaCliException::class); - $this->expectExceptionMessage('Multiple applications match the alias *:devcloud2'); - $this->executeCommand(['applicationUuid' => $alias], []); - $output = $this->getDisplay(); - $this->assertStringContainsString('Use a unique application alias: devcloud:devcloud2, devcloud:devcloud2', $output); - - } - - /** - * @serial - */ - public function testConvertApplicationAliasWithRealmToUuidArgument(): void { - $this->clientProphecy->addOption('headers', ['Accept' => 'application/hal+json, version=2'])->shouldBeCalled(); - $this->mockApplicationsRequest(1, FALSE); - $this->clientProphecy->addQuery('filter', 'hosting=@devcloud:devcloud2')->shouldBeCalled(); - $this->mockApplicationRequest(); - $this->mockRequest('getAccount'); - $this->command = $this->getApiCommandByName('api:applications:find'); - $alias = 'devcloud:devcloud2'; - $this->executeCommand(['applicationUuid' => $alias], []); - - } - - /** - * @serial - */ - public function testConvertEnvironmentAliasToUuidArgument(): void { - ClearCacheCommand::clearCaches(); - $this->clientProphecy->addOption('headers', ['Accept' => 'application/hal+json, version=2'])->shouldBeCalled(); - $applicationsResponse = $this->mockApplicationsRequest(1); - $this->clientProphecy->addQuery('filter', 'hosting=@*:devcloud2')->shouldBeCalled(); - $this->mockEnvironmentsRequest($applicationsResponse); - $this->mockRequest('getAccount'); - - $response = $this->getMockEnvironmentResponse(); - $this->clientProphecy->request('get', '/environments/24-a47ac10b-58cc-4372-a567-0e02b2c3d470')->willReturn($response)->shouldBeCalled(); - - $this->command = $this->getApiCommandByName('api:environments:find'); - $alias = 'devcloud2.dev'; - - $this->executeCommand(['environmentId' => $alias], [ - // Would you like Acquia CLI to search for a Cloud application that matches your local git config? - 'n', - // Select a Cloud Platform application: - '0', - // Would you like to link the Cloud application Sample application to this repository? - 'n', - ]); - - // Assert. - $this->getDisplay(); - $this->assertEquals(0, $this->getStatusCode()); - } - - /** - * @group serial - */ - public function testConvertInvalidEnvironmentAliasToUuidArgument(): void { - ClearCacheCommand::clearCaches(); - $applicationsResponse = $this->mockApplicationsRequest(1); - $this->clientProphecy->addQuery('filter', 'hosting=@*:devcloud2')->shouldBeCalled(); - $this->mockEnvironmentsRequest($applicationsResponse); - $this->mockRequest('getAccount'); - $this->command = $this->getApiCommandByName('api:environments:find'); - $alias = 'devcloud2.invalid'; - $this->expectException(AcquiaCliException::class); - $this->expectExceptionMessage('{environmentId} must be a valid UUID or site alias.'); - $this->executeCommand(['environmentId' => $alias], []); - - } - - public function testApiCommandExecutionForHttpPost(): void { - $this->clientProphecy->addOption('headers', ['Accept' => 'application/hal+json, version=2'])->shouldBeCalled(); - $mockRequestArgs = $this->getMockRequestBodyFromSpec('/account/ssh-keys'); - $mockResponseBody = $this->getMockResponseFromSpec('/account/ssh-keys', 'post', '202'); - foreach ($mockRequestArgs as $name => $value) { - $this->clientProphecy->addOption('json', [$name => $value])->shouldBeCalled(); + + /** + * @return array + */ + public function providerTestApiCommandDefinitionParameters(): array + { + $apiAccountsSshKeysListUsage = '--from="-7d" --to="-1d" --sort="field1,-field2" --limit="10" --offset="10"'; + return [ + ['0', 'api:accounts:ssh-keys-list', 'get', $apiAccountsSshKeysListUsage], + ['1', 'api:accounts:ssh-keys-list', 'get', $apiAccountsSshKeysListUsage], + ['1', 'api:accounts:ssh-keys-list', 'get', $apiAccountsSshKeysListUsage], + ['1', 'api:environments:domain-clear-caches', 'post', '12-d314739e-296f-11e9-b210-d663bd873d93 example.com'], + ['1', 'api:applications:find', 'get', 'da1c0a8e-ff69-45db-88fc-acd6d2affbb7'], + ['1', 'api:applications:find', 'get', 'myapp'], + ]; } - $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); - $this->assertStringContainsString('Adding SSH key.', $output); - } - - public function testApiCommandExecutionForHttpPut(): void { - $this->clientProphecy->addOption('headers', ['Accept' => 'application/hal+json, version=2'])->shouldBeCalled(); - $mockRequestOptions = $this->getMockRequestBodyFromSpec('/environments/{environmentId}', 'put'); - $mockRequestOptions['max_input_vars'] = 1001; - $mockResponseBody = $this->getMockEnvironmentResponse('put', '202'); - - foreach ($mockRequestOptions as $name => $value) { - $this->clientProphecy->addOption('json', [$name => $value])->shouldBeCalled(); + + /** + * @dataProvider providerTestApiCommandDefinitionParameters + */ + public function testApiCommandDefinitionParameters(string $useSpecCache, string $commandName, string $method, string $usage): void + { + putenv('ACQUIA_CLI_USE_CLOUD_API_SPEC_CACHE=' . $useSpecCache); + + $this->command = $this->getApiCommandByName($commandName); + $resource = $this->getResourceFromSpec($this->command->getPath(), $method); + $this->assertEquals($resource['summary'], $this->command->getDescription()); + + $expectedCommandName = 'api:' . $resource['x-cli-name']; + $this->assertEquals($expectedCommandName, $this->command->getName()); + + foreach ($resource['parameters'] as $parameter) { + $paramKey = str_replace('#/components/parameters/', '', $parameter['$ref']); + $param = $this->getCloudApiSpec()['components']['parameters'][$paramKey]; + $this->assertTrue( + $this->command->getDefinition()->hasOption($param['name']) || + $this->command->getDefinition()->hasArgument($param['name']), + "Command $expectedCommandName does not have expected argument or option {$param['name']}" + ); + } + + $usages = $this->command->getUsages(); + $this->assertContains($commandName . ' ' . $usage, $usages); } - $this->clientProphecy->request('put', '/environments/24-a47ac10b-58cc-4372-a567-0e02b2c3d470')->willReturn($mockResponseBody)->shouldBeCalled(); - $this->command = $this->getApiCommandByName('api:environments:update'); - $options = []; - foreach ($mockRequestOptions as $key => $value) { - $options['--' . $key] = $value; + public function testModifiedParameterDescriptions(): void + { + $this->command = $this->getApiCommandByName('api:environments:domain-status-find'); + $this->assertStringContainsString('You may also use an environment alias', $this->command->getDefinition()->getArgument('environmentId')->getDescription()); + + $this->command = $this->getApiCommandByName('api:applications:find'); + $this->assertStringContainsString('You may also use an application alias or omit the argument', $this->command->getDefinition()->getArgument('applicationUuid')->getDescription()); + } + + /** + * @return string[][] + */ + public function providerTestApiCommandDefinitionRequestBody(): array + { + return [ + ['api:accounts:ssh-key-create', 'post', 'api:accounts:ssh-key-create "mykey" "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQChwPHzTTDKDpSbpa2+d22LcbQmsw92eLsUK3Fmei1fiGDkd34NsYCN8m7lsi3NbvdMS83CtPQPWiCveYPzFs1/hHc4PYj8opD2CNnr5iWVVbyaulCYHCgVv4aB/ojcexg8q483A4xJeF15TiCr/gu34rK6ucTvC/tn/rCwJBudczvEwt0klqYwv8Cl/ytaQboSuem5KgSjO3lMrb6CWtfSNhE43ZOw+UBFBqxIninN868vGMkIv9VY34Pwj54rPn/ItQd6Ef4B0KHHaGmzK0vfP+AK7FxNMoHnj3iYT33KZNqtDozdn5tYyH/bThPebEtgqUn+/w5l6wZIC/8zzvls/127ngHk+jNa0PlNyS2TxhPUK4NaPHIEnnrlp07JEYC4ImcBjaYCWAdcTcUkcJjwZQkN4bGmyO9cjICH98SdLD/HxqzTHeaYDbAX/Hu9HfaBb5dXLWsjw3Xc6hoVnUUZbMQyfgb0KgxDLh92eNGxJkpZiL0VDNOWCxDWsNpzwhLNkLqCvI6lyxiLaUzvJAk6dPaRhExmCbU1lDO2eR0FdSwC1TEhJOT9eDIK1r2hztZKs2oa5FNFfB/IFHVWasVFC9N2h/r/egB5zsRxC9MqBLRBq95NBxaRSFng6ML5WZSw41Qi4C/JWVm89rdj2WqScDHYyAdwyyppWU4T5c9Fmw== example@example.com"'], + ['api:environments:file-copy', 'post', '12-d314739e-296f-11e9-b210-d663bd873d93 --source="14-0c7e79ab-1c4a-424e-8446-76ae8be7e851"'], + ]; } - $options['--lang_version'] = $options['--version']; - unset($options['--version']); - $args = ['environmentId' => '24-a47ac10b-58cc-4372-a567-0e02b2c3d470'] + $options; - $this->executeCommand($args); - - // Assert. - $output = $this->getDisplay(); - $this->assertNotNull($output); - $this->assertJson($output); - $this->assertStringContainsString('The environment configuration is being updated.', $output); - } - - /** - * @return array - */ - public function providerTestApiCommandDefinitionParameters(): array { - $apiAccountsSshKeysListUsage = '--from="-7d" --to="-1d" --sort="field1,-field2" --limit="10" --offset="10"'; - return [ - ['0', 'api:accounts:ssh-keys-list', 'get', $apiAccountsSshKeysListUsage], - ['1', 'api:accounts:ssh-keys-list', 'get', $apiAccountsSshKeysListUsage], - ['1', 'api:accounts:ssh-keys-list', 'get', $apiAccountsSshKeysListUsage], - ['1', 'api:environments:domain-clear-caches', 'post', '12-d314739e-296f-11e9-b210-d663bd873d93 example.com'], - ['1', 'api:applications:find', 'get', 'da1c0a8e-ff69-45db-88fc-acd6d2affbb7'], - ['1', 'api:applications:find', 'get', 'myapp'], - ]; - } - - /** - * @dataProvider providerTestApiCommandDefinitionParameters - */ - public function testApiCommandDefinitionParameters(string $useSpecCache, string $commandName, string $method, string $usage): void { - putenv('ACQUIA_CLI_USE_CLOUD_API_SPEC_CACHE=' . $useSpecCache); - - $this->command = $this->getApiCommandByName($commandName); - $resource = $this->getResourceFromSpec($this->command->getPath(), $method); - $this->assertEquals($resource['summary'], $this->command->getDescription()); - - $expectedCommandName = 'api:' . $resource['x-cli-name']; - $this->assertEquals($expectedCommandName, $this->command->getName()); - - foreach ($resource['parameters'] as $parameter) { - $paramKey = str_replace('#/components/parameters/', '', $parameter['$ref']); - $param = $this->getCloudApiSpec()['components']['parameters'][$paramKey]; - $this->assertTrue( - $this->command->getDefinition()->hasOption($param['name']) || - $this->command->getDefinition()->hasArgument($param['name']), - "Command $expectedCommandName does not have expected argument or option {$param['name']}" + + /** + * @dataProvider providerTestApiCommandDefinitionRequestBody + * @param $commandName + * @param $method + * @param $usage + */ + public function testApiCommandDefinitionRequestBody(string $commandName, string $method, string $usage): void + { + $this->command = $this->getApiCommandByName($commandName); + $resource = $this->getResourceFromSpec($this->command->getPath(), $method); + foreach ($resource['requestBody']['content']['application/hal+json']['example'] as $propKey => $value) { + $this->assertTrue( + $this->command->getDefinition()->hasArgument($propKey) || $this->command->getDefinition() + ->hasOption($propKey), + "Command {$this->command->getName()} does not have expected argument or option $propKey" + ); + } + $this->assertStringContainsString($usage, $this->command->getUsages()[0]); + } + + public function testGetApplicationUuidFromBltYml(): void + { + $this->clientProphecy->addOption('headers', ['Accept' => 'application/hal+json, version=2'])->shouldBeCalled(); + $mockBody = $this->getMockResponseFromSpec('/applications/{applicationUuid}', 'get', '200'); + $this->clientProphecy->request('get', '/applications/' . $mockBody->uuid)->willReturn($mockBody)->shouldBeCalled(); + $this->command = $this->getApiCommandByName('api:applications:find'); + $bltConfigFilePath = Path::join($this->projectDir, 'blt', 'blt.yml'); + $this->fs->dumpFile($bltConfigFilePath, Yaml::dump(['cloud' => ['appId' => $mockBody->uuid]])); + $this->executeCommand(); + + $this->getDisplay(); + $this->fs->remove($bltConfigFilePath); + } + + public function testOrganizationMemberDeleteByUserUuid(): void + { + $orgId = 'bfafd31a-83a6-4257-b0ec-afdeff83117a'; + $memberUuid = '26c4af83-545b-45cb-b165-d537adc9e0b4'; + + $this->clientProphecy->addOption('headers', ['Accept' => 'application/hal+json, version=2'])->shouldBeCalled(); + $this->mockRequest('postOrganizationMemberDelete', [$orgId, $memberUuid], null, 'Member removed'); + + $this->command = $this->getApiCommandByName('api:organizations:member-delete'); + $this->executeCommand( + [ + 'organizationUuid' => $orgId, + 'userUuid' => $memberUuid, + ], + ); + + $output = $this->getDisplay(); + $this->assertEquals(0, $this->getStatusCode()); + $this->assertStringContainsString("Organization member removed", $output); + } + + /** + * Test of deletion of the user from organization by user email. + */ + public function testOrganizationMemberDeleteByUserEmail(): void + { + $this->clientProphecy->addOption('headers', ['Accept' => 'application/hal+json, version=2'])->shouldBeCalled(); + $membersResponse = $this->getMockResponseFromSpec('/organizations/{organizationUuid}/members', 'get', 200); + $orgId = 'bfafd31a-83a6-4257-b0ec-afdeff83117a'; + $memberMail = $membersResponse->_embedded->items[0]->mail; + $memberUuid = $membersResponse->_embedded->items[0]->uuid; + $this->clientProphecy->request('get', '/organizations/' . $orgId . '/members') + ->willReturn($membersResponse->_embedded->items)->shouldBeCalled(); + + $this->mockRequest('postOrganizationMemberDelete', [$orgId, $memberUuid], null, 'Member removed'); + + $this->command = $this->getApiCommandByName('api:organizations:member-delete'); + $this->executeCommand( + [ + 'organizationUuid' => $orgId, + 'userUuid' => $memberMail, + ], ); + + $output = $this->getDisplay(); + $this->assertEquals(0, $this->getStatusCode()); + $this->assertStringContainsString("Organization member removed", $output); } - $usages = $this->command->getUsages(); - $this->assertContains($commandName . ' ' . $usage, $usages); - } - - public function testModifiedParameterDescriptions(): void { - $this->command = $this->getApiCommandByName('api:environments:domain-status-find'); - $this->assertStringContainsString('You may also use an environment alias', $this->command->getDefinition()->getArgument('environmentId')->getDescription()); - - $this->command = $this->getApiCommandByName('api:applications:find'); - $this->assertStringContainsString('You may also use an application alias or omit the argument', $this->command->getDefinition()->getArgument('applicationUuid')->getDescription()); - } - - /** - * @return string[][] - */ - public function providerTestApiCommandDefinitionRequestBody(): array { - return [ - ['api:accounts:ssh-key-create', 'post', 'api:accounts:ssh-key-create "mykey" "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQChwPHzTTDKDpSbpa2+d22LcbQmsw92eLsUK3Fmei1fiGDkd34NsYCN8m7lsi3NbvdMS83CtPQPWiCveYPzFs1/hHc4PYj8opD2CNnr5iWVVbyaulCYHCgVv4aB/ojcexg8q483A4xJeF15TiCr/gu34rK6ucTvC/tn/rCwJBudczvEwt0klqYwv8Cl/ytaQboSuem5KgSjO3lMrb6CWtfSNhE43ZOw+UBFBqxIninN868vGMkIv9VY34Pwj54rPn/ItQd6Ef4B0KHHaGmzK0vfP+AK7FxNMoHnj3iYT33KZNqtDozdn5tYyH/bThPebEtgqUn+/w5l6wZIC/8zzvls/127ngHk+jNa0PlNyS2TxhPUK4NaPHIEnnrlp07JEYC4ImcBjaYCWAdcTcUkcJjwZQkN4bGmyO9cjICH98SdLD/HxqzTHeaYDbAX/Hu9HfaBb5dXLWsjw3Xc6hoVnUUZbMQyfgb0KgxDLh92eNGxJkpZiL0VDNOWCxDWsNpzwhLNkLqCvI6lyxiLaUzvJAk6dPaRhExmCbU1lDO2eR0FdSwC1TEhJOT9eDIK1r2hztZKs2oa5FNFfB/IFHVWasVFC9N2h/r/egB5zsRxC9MqBLRBq95NBxaRSFng6ML5WZSw41Qi4C/JWVm89rdj2WqScDHYyAdwyyppWU4T5c9Fmw== example@example.com"'], - ['api:environments:file-copy', 'post', '12-d314739e-296f-11e9-b210-d663bd873d93 --source="14-0c7e79ab-1c4a-424e-8446-76ae8be7e851"'], - ]; - } - - /** - * @dataProvider providerTestApiCommandDefinitionRequestBody - * @param $commandName - * @param $method - * @param $usage - */ - public function testApiCommandDefinitionRequestBody(string $commandName, string $method, string $usage): void { - $this->command = $this->getApiCommandByName($commandName); - $resource = $this->getResourceFromSpec($this->command->getPath(), $method); - foreach ($resource['requestBody']['content']['application/hal+json']['example'] as $propKey => $value) { - $this->assertTrue($this->command->getDefinition()->hasArgument($propKey) || $this->command->getDefinition() - ->hasOption($propKey), - "Command {$this->command->getName()} does not have expected argument or option $propKey"); + /** + * @group brokenProphecy + */ + public function testOrganizationMemberDeleteInvalidEmail(): void + { + $membersResponse = $this->getMockResponseFromSpec('/organizations/{organizationUuid}/members', 'get', 200); + $orgId = 'bfafd31a-83a6-4257-b0ec-afdeff83117a'; + $memberUuid = $membersResponse->_embedded->items[0]->mail . 'INVALID'; + $this->clientProphecy->request('get', '/organizations/' . $orgId . '/members') + ->willReturn($membersResponse->_embedded->items)->shouldBeCalled(); + + $this->mockRequest('postOrganizationMemberDelete', [$orgId, $memberUuid], null, 'Member removed'); + + $this->command = $this->getApiCommandByName('api:organizations:member-delete'); + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage('No matching user found in this organization'); + $this->executeCommand( + [ + 'organizationUuid' => $orgId, + 'userUuid' => $memberUuid, + ], + ); } - $this->assertStringContainsString($usage, $this->command->getUsages()[0]); - } - - public function testGetApplicationUuidFromBltYml(): void { - $this->clientProphecy->addOption('headers', ['Accept' => 'application/hal+json, version=2'])->shouldBeCalled(); - $mockBody = $this->getMockResponseFromSpec('/applications/{applicationUuid}', 'get', '200'); - $this->clientProphecy->request('get', '/applications/' . $mockBody->uuid)->willReturn($mockBody)->shouldBeCalled(); - $this->command = $this->getApiCommandByName('api:applications:find'); - $bltConfigFilePath = Path::join($this->projectDir, 'blt', 'blt.yml'); - $this->fs->dumpFile($bltConfigFilePath, Yaml::dump(['cloud' => ['appId' => $mockBody->uuid]])); - $this->executeCommand(); - - $this->getDisplay(); - $this->fs->remove($bltConfigFilePath); - } - - public function testOrganizationMemberDeleteByUserUuid(): void { - $orgId = 'bfafd31a-83a6-4257-b0ec-afdeff83117a'; - $memberUuid = '26c4af83-545b-45cb-b165-d537adc9e0b4'; - - $this->clientProphecy->addOption('headers', ['Accept' => 'application/hal+json, version=2'])->shouldBeCalled(); - $this->mockRequest('postOrganizationMemberDelete', [$orgId, $memberUuid], NULL, 'Member removed'); - - $this->command = $this->getApiCommandByName('api:organizations:member-delete'); - $this->executeCommand( - [ - 'organizationUuid' => $orgId, - 'userUuid' => $memberUuid, - ], - ); - - $output = $this->getDisplay(); - $this->assertEquals(0, $this->getStatusCode()); - $this->assertStringContainsString("Organization member removed", $output); - } - - /** - * Test of deletion of the user from organization by user email. - */ - public function testOrganizationMemberDeleteByUserEmail(): void { - $this->clientProphecy->addOption('headers', ['Accept' => 'application/hal+json, version=2'])->shouldBeCalled(); - $membersResponse = $this->getMockResponseFromSpec('/organizations/{organizationUuid}/members', 'get', 200); - $orgId = 'bfafd31a-83a6-4257-b0ec-afdeff83117a'; - $memberMail = $membersResponse->_embedded->items[0]->mail; - $memberUuid = $membersResponse->_embedded->items[0]->uuid; - $this->clientProphecy->request('get', '/organizations/' . $orgId . '/members') - ->willReturn($membersResponse->_embedded->items)->shouldBeCalled(); - - $this->mockRequest('postOrganizationMemberDelete', [$orgId, $memberUuid], NULL, 'Member removed'); - - $this->command = $this->getApiCommandByName('api:organizations:member-delete'); - $this->executeCommand( - [ - 'organizationUuid' => $orgId, - 'userUuid' => $memberMail, - ], - ); - - $output = $this->getDisplay(); - $this->assertEquals(0, $this->getStatusCode()); - $this->assertStringContainsString("Organization member removed", $output); - } - - /** - * @group brokenProphecy - */ - public function testOrganizationMemberDeleteInvalidEmail(): void { - $membersResponse = $this->getMockResponseFromSpec('/organizations/{organizationUuid}/members', 'get', 200); - $orgId = 'bfafd31a-83a6-4257-b0ec-afdeff83117a'; - $memberUuid = $membersResponse->_embedded->items[0]->mail . 'INVALID'; - $this->clientProphecy->request('get', '/organizations/' . $orgId . '/members') - ->willReturn($membersResponse->_embedded->items)->shouldBeCalled(); - - $this->mockRequest('postOrganizationMemberDelete', [$orgId, $memberUuid], NULL, 'Member removed'); - - $this->command = $this->getApiCommandByName('api:organizations:member-delete'); - $this->expectException(AcquiaCliException::class); - $this->expectExceptionMessage('No matching user found in this organization'); - $this->executeCommand( - [ - 'organizationUuid' => $orgId, - 'userUuid' => $memberUuid, - ], - ); - } - - /** - * Test of deletion of the user from organization by user email. - */ - public function testOrganizationMemberDeleteNoMembers(): void { - $membersResponse = $this->getMockResponseFromSpec('/organizations/{organizationUuid}/members', 'get', 200); - $orgId = 'bfafd31a-83a6-4257-b0ec-afdeff83117a'; - $memberUuid = $membersResponse->_embedded->items[0]->mail; - $this->clientProphecy->request('get', '/organizations/' . $orgId . '/members') - ->willReturn([])->shouldBeCalled(); - - $this->command = $this->getApiCommandByName('api:organizations:member-delete'); - $this->expectException(AcquiaCliException::class); - $this->expectExceptionMessage('Organization has no members'); - $this->executeCommand( - [ - 'organizationUuid' => $orgId, - 'userUuid' => $memberUuid, - ], - ); - } + /** + * Test of deletion of the user from organization by user email. + */ + public function testOrganizationMemberDeleteNoMembers(): void + { + $membersResponse = $this->getMockResponseFromSpec('/organizations/{organizationUuid}/members', 'get', 200); + $orgId = 'bfafd31a-83a6-4257-b0ec-afdeff83117a'; + $memberUuid = $membersResponse->_embedded->items[0]->mail; + $this->clientProphecy->request('get', '/organizations/' . $orgId . '/members') + ->willReturn([])->shouldBeCalled(); + + $this->command = $this->getApiCommandByName('api:organizations:member-delete'); + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage('Organization has no members'); + $this->executeCommand( + [ + 'organizationUuid' => $orgId, + 'userUuid' => $memberUuid, + ], + ); + } } diff --git a/tests/phpunit/src/Commands/Api/ApiListCommandTest.php b/tests/phpunit/src/Commands/Api/ApiListCommandTest.php index 028aa8f50..c1cea25ca 100644 --- a/tests/phpunit/src/Commands/Api/ApiListCommandTest.php +++ b/tests/phpunit/src/Commands/Api/ApiListCommandTest.php @@ -1,6 +1,6 @@ application->addCommands($this->getApiCommands()); - } - - protected function createCommand(): CommandBase { - return $this->injectCommand(ApiListCommand::class); - } - - public function testApiListCommand(): void { - $this->executeCommand(); - $output = $this->getDisplay(); - $this->assertStringContainsString(' api:accounts:ssh-keys-list', $output); - } - - public function testApiNamespaceListCommand(): void { - $this->command = $this->injectCommand(ApiListCommandBase::class); - $name = 'api:accounts'; - $this->command->setName($name); - $this->command->setNamespace($name); - $this->executeCommand(); - $output = $this->getDisplay(); - $this->assertStringContainsString('api:accounts:', $output); - $this->assertStringContainsString('api:accounts:ssh-keys-list', $output); - $this->assertStringNotContainsString('api:subscriptions', $output); - } - - public function testListCommand(): void { - $this->command = $this->injectCommand(ListCommand::class); - $this->executeCommand(); - $output = $this->getDisplay(); - $this->assertStringContainsString(' api:accounts', $output); - $this->assertStringNotContainsString(' api:accounts:ssh-keys-list', $output); - } - +class ApiListCommandTest extends CommandTestBase +{ + public function setUp(): void + { + parent::setUp(); + $this->application->addCommands($this->getApiCommands()); + } + + protected function createCommand(): CommandBase + { + return $this->injectCommand(ApiListCommand::class); + } + + public function testApiListCommand(): void + { + $this->executeCommand(); + $output = $this->getDisplay(); + $this->assertStringContainsString(' api:accounts:ssh-keys-list', $output); + } + + public function testApiNamespaceListCommand(): void + { + $this->command = $this->injectCommand(ApiListCommandBase::class); + $name = 'api:accounts'; + $this->command->setName($name); + $this->command->setNamespace($name); + $this->executeCommand(); + $output = $this->getDisplay(); + $this->assertStringContainsString('api:accounts:', $output); + $this->assertStringContainsString('api:accounts:ssh-keys-list', $output); + $this->assertStringNotContainsString('api:subscriptions', $output); + } + + public function testListCommand(): void + { + $this->command = $this->injectCommand(ListCommand::class); + $this->executeCommand(); + $output = $this->getDisplay(); + $this->assertStringContainsString(' api:accounts', $output); + $this->assertStringNotContainsString(' api:accounts:ssh-keys-list', $output); + } } diff --git a/tests/phpunit/src/Commands/App/AppOpenCommandTest.php b/tests/phpunit/src/Commands/App/AppOpenCommandTest.php index 98f5bbd6e..67a01ff14 100644 --- a/tests/phpunit/src/Commands/App/AppOpenCommandTest.php +++ b/tests/phpunit/src/Commands/App/AppOpenCommandTest.php @@ -1,6 +1,6 @@ injectCommand(AppOpenCommand::class); - } - - public function testAppOpenCommand(): void { - $applicationUuid = 'a47ac10b-58cc-4372-a567-0e02b2c3d470'; - $localMachineHelper = $this->mockLocalMachineHelper(); - $localMachineHelper->startBrowser('https://cloud.acquia.com/a/applications/' . $applicationUuid)->shouldBeCalled(); - $localMachineHelper->isBrowserAvailable()->willReturn(TRUE); - $this->mockRequest('getApplicationByUuid', $applicationUuid); - $this->executeCommand(['applicationUuid' => $applicationUuid]); - } - - /** - * @group brokenProphecy - */ - public function testAppOpenNoBrowser(): void { - $applicationUuid = 'a47ac10b-58cc-4372-a567-0e02b2c3d470'; - $localMachineHelper = $this->mockLocalMachineHelper(); - $localMachineHelper->isBrowserAvailable()->willReturn(FALSE); - - $this->mockApplicationRequest(); - $this->createMockAcliConfigFile($applicationUuid); - $this->expectException(AcquiaCliException::class); - $this->expectExceptionMessage('No browser is available on this machine'); - $this->executeCommand(); - } - +class AppOpenCommandTest extends CommandTestBase +{ + protected function createCommand(): CommandBase + { + return $this->injectCommand(AppOpenCommand::class); + } + + public function testAppOpenCommand(): void + { + $applicationUuid = 'a47ac10b-58cc-4372-a567-0e02b2c3d470'; + $localMachineHelper = $this->mockLocalMachineHelper(); + $localMachineHelper->startBrowser('https://cloud.acquia.com/a/applications/' . $applicationUuid)->shouldBeCalled(); + $localMachineHelper->isBrowserAvailable()->willReturn(true); + $this->mockRequest('getApplicationByUuid', $applicationUuid); + $this->executeCommand(['applicationUuid' => $applicationUuid]); + } + + /** + * @group brokenProphecy + */ + public function testAppOpenNoBrowser(): void + { + $applicationUuid = 'a47ac10b-58cc-4372-a567-0e02b2c3d470'; + $localMachineHelper = $this->mockLocalMachineHelper(); + $localMachineHelper->isBrowserAvailable()->willReturn(false); + + $this->mockApplicationRequest(); + $this->createMockAcliConfigFile($applicationUuid); + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage('No browser is available on this machine'); + $this->executeCommand(); + } } diff --git a/tests/phpunit/src/Commands/App/AppVcsInfoTest.php b/tests/phpunit/src/Commands/App/AppVcsInfoTest.php index a74eacdb5..97f3b7a47 100644 --- a/tests/phpunit/src/Commands/App/AppVcsInfoTest.php +++ b/tests/phpunit/src/Commands/App/AppVcsInfoTest.php @@ -1,6 +1,6 @@ injectCommand(AppVcsInfo::class); - } - - /** - * @group brokenProphecy - */ - public function testNoEnvAvailableCommand(): void { - $applications = $this->mockRequest('getApplications'); - /** @var \AcquiaCloudApi\Response\ApplicationResponse $application */ - $application = $this->mockRequest('getApplicationByUuid', $applications[self::$INPUT_DEFAULT_CHOICE]->uuid); - $this->clientProphecy->request('get', - "/applications/$application->uuid/environments") - ->willReturn([]) - ->shouldBeCalled(); - $this->mockRequest('getCodeByApplicationUuid', $application->uuid); - - $this->expectException(AcquiaCliException::class); - $this->expectExceptionMessage('There are no environments available with this application.'); - - $this->executeCommand( - [ - 'applicationUuid' => 'a47ac10b-58cc-4372-a567-0e02b2c3d470', - ], - ); - } - - /** - * @group brokenProphecy - */ - public function testNoVcsAvailableCommand(): void { - $applications = $this->mockRequest('getApplications'); - $this->mockRequest('getApplicationByUuid', $applications[0]->uuid); - $this->mockRequest('getApplicationEnvironments', $applications[0]->uuid); - - $this->clientProphecy->request('get', - "/applications/{$applications[0]->uuid}/code") - ->willReturn([]) - ->shouldBeCalled(); - - $this->expectException(AcquiaCliException::class); - $this->expectExceptionMessage('No branch or tag is available with this application.'); - $this->executeCommand( - [ - 'applicationUuid' => 'a47ac10b-58cc-4372-a567-0e02b2c3d470', - ], - ); - } - - /** - * @group brokenProphecy - */ - public function testShowVcsListCommand(): void { - $applications = $this->mockRequest('getApplications'); - /** @var \AcquiaCloudApi\Response\ApplicationResponse $application */ - $application = $this->mockRequest('getApplicationByUuid', $applications[self::$INPUT_DEFAULT_CHOICE]->uuid); - $this->mockRequest('getApplicationEnvironments', $application->uuid); - $this->mockRequest('getCodeByApplicationUuid', $application->uuid); - - $this->executeCommand( - [ - 'applicationUuid' => 'a47ac10b-58cc-4372-a567-0e02b2c3d470', - ], - ); - - $output = $this->getDisplay(); - $expected = <<injectCommand(AppVcsInfo::class); + } + + /** + * @group brokenProphecy + */ + public function testNoEnvAvailableCommand(): void + { + $applications = $this->mockRequest('getApplications'); + /** @var \AcquiaCloudApi\Response\ApplicationResponse $application */ + $application = $this->mockRequest('getApplicationByUuid', $applications[self::$INPUT_DEFAULT_CHOICE]->uuid); + $this->clientProphecy->request( + 'get', + "/applications/$application->uuid/environments" + ) + ->willReturn([]) + ->shouldBeCalled(); + $this->mockRequest('getCodeByApplicationUuid', $application->uuid); + + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage('There are no environments available with this application.'); + + $this->executeCommand( + [ + 'applicationUuid' => 'a47ac10b-58cc-4372-a567-0e02b2c3d470', + ], + ); + } + + /** + * @group brokenProphecy + */ + public function testNoVcsAvailableCommand(): void + { + $applications = $this->mockRequest('getApplications'); + $this->mockRequest('getApplicationByUuid', $applications[0]->uuid); + $this->mockRequest('getApplicationEnvironments', $applications[0]->uuid); + + $this->clientProphecy->request( + 'get', + "/applications/{$applications[0]->uuid}/code" + ) + ->willReturn([]) + ->shouldBeCalled(); + + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage('No branch or tag is available with this application.'); + $this->executeCommand( + [ + 'applicationUuid' => 'a47ac10b-58cc-4372-a567-0e02b2c3d470', + ], + ); + } + + /** + * @group brokenProphecy + */ + public function testShowVcsListCommand(): void + { + $applications = $this->mockRequest('getApplications'); + /** @var \AcquiaCloudApi\Response\ApplicationResponse $application */ + $application = $this->mockRequest('getApplicationByUuid', $applications[self::$INPUT_DEFAULT_CHOICE]->uuid); + $this->mockRequest('getApplicationEnvironments', $application->uuid); + $this->mockRequest('getCodeByApplicationUuid', $application->uuid); + + $this->executeCommand( + [ + 'applicationUuid' => 'a47ac10b-58cc-4372-a567-0e02b2c3d470', + ], + ); + + $output = $this->getDisplay(); + $expected = <<mockRequest('getApplications'); - $application = $this->mockRequest('getApplicationByUuid', $applications[self::$INPUT_DEFAULT_CHOICE]->uuid); - $environments = $this->mockRequest('getApplicationEnvironments', $applications[self::$INPUT_DEFAULT_CHOICE]->uuid); - foreach ($environments as $environment) { - // Empty the VCS. - $environment->vcs = new \stdClass(); + self::assertStringContainsStringIgnoringLineEndings($expected, $output); + } + + /** + * @group brokenProphecy + */ + public function testNoDeployedVcs(): void + { + $applications = $this->mockRequest('getApplications'); + $application = $this->mockRequest('getApplicationByUuid', $applications[self::$INPUT_DEFAULT_CHOICE]->uuid); + $environments = $this->mockRequest('getApplicationEnvironments', $applications[self::$INPUT_DEFAULT_CHOICE]->uuid); + foreach ($environments as $environment) { + // Empty the VCS. + $environment->vcs = new \stdClass(); + } + + $this->clientProphecy->request( + 'get', + "/applications/{$application->uuid}/environments" + ) + ->willReturn($environments) + ->shouldBeCalled(); + $this->mockRequest('getCodeByApplicationUuid', $application->uuid); + + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage('No branch or tag is deployed on any of the environment of this application.'); + $this->executeCommand( + [ + 'applicationUuid' => 'a47ac10b-58cc-4372-a567-0e02b2c3d470', + '--deployed', + ], + ); } - $this->clientProphecy->request('get', - "/applications/{$application->uuid}/environments") - ->willReturn($environments) - ->shouldBeCalled(); - $this->mockRequest('getCodeByApplicationUuid', $application->uuid); - - $this->expectException(AcquiaCliException::class); - $this->expectExceptionMessage('No branch or tag is deployed on any of the environment of this application.'); - $this->executeCommand( - [ - 'applicationUuid' => 'a47ac10b-58cc-4372-a567-0e02b2c3d470', - '--deployed', - ], - ); - } - - /** - * @group brokenProphecy - */ - public function testListOnlyDeployedVcs(): void { - $applications = $this->mockRequest('getApplications'); - /** @var \AcquiaCloudApi\Response\ApplicationResponse $application */ - $application = $this->mockRequest('getApplicationByUuid', $applications[self::$INPUT_DEFAULT_CHOICE]->uuid); - $this->mockRequest('getApplicationEnvironments', $application->uuid); - $this->mockRequest('getCodeByApplicationUuid', $application->uuid); - - $this->executeCommand( - [ - 'applicationUuid' => 'a47ac10b-58cc-4372-a567-0e02b2c3d470', - '--deployed', - ], - ); - - $output = $this->getDisplay(); - $expected = <<mockRequest('getApplications'); + /** @var \AcquiaCloudApi\Response\ApplicationResponse $application */ + $application = $this->mockRequest('getApplicationByUuid', $applications[self::$INPUT_DEFAULT_CHOICE]->uuid); + $this->mockRequest('getApplicationEnvironments', $application->uuid); + $this->mockRequest('getCodeByApplicationUuid', $application->uuid); + + $this->executeCommand( + [ + 'applicationUuid' => 'a47ac10b-58cc-4372-a567-0e02b2c3d470', + '--deployed', + ], + ); + + $output = $this->getDisplay(); + $expected = <<sut = DefinedRecommendation::createFromDefinition([ - 'package' => NULL, - 'note' => 'Example: The module bar is no longer required because its functionality has been incorporated into Drupal core.', - 'replaces' => [ + protected function setUp(): void + { + parent::setUp(); + // @see \Acquia\Cli\Tests\Commands\App\From\DefinedRecommendationTest::getTestConfigurations() + // phpcs:disable SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys + $this->sut = DefinedRecommendation::createFromDefinition([ + 'package' => null, + 'note' => 'Example: The module bar is no longer required because its functionality has been incorporated into Drupal core.', + 'replaces' => [ 'name' => 'bar', - ], - 'vetted' => TRUE, - ]); - // phpcs:enable - } - - /** - * @covers ::getPackageName - */ - public function testPackageName(): void { - $this->expectException(\LogicException::class); - $this->sut->getPackageName(); - } + ], + 'vetted' => true, + ]); + // phpcs:enable + } - /** - * @covers ::getVersionConstraint - */ - public function testVersionConstraint(): void { - $this->expectException(\LogicException::class); - $this->sut->getVersionConstraint(); - } + /** + * @covers ::getPackageName + */ + public function testPackageName(): void + { + $this->expectException(\LogicException::class); + $this->sut->getPackageName(); + } - /** - * @covers ::hasModulesToInstall - */ - public function testHasModulesToInstall(): void { - $this->expectException(\LogicException::class); - $this->sut->hasModulesToInstall(); - } + /** + * @covers ::getVersionConstraint + */ + public function testVersionConstraint(): void + { + $this->expectException(\LogicException::class); + $this->sut->getVersionConstraint(); + } - /** - * @covers ::getModulesToInstall - */ - public function testGetModulesToInstall(): void { - $this->expectException(\LogicException::class); - $this->sut->getModulesToInstall(); - } + /** + * @covers ::hasModulesToInstall + */ + public function testHasModulesToInstall(): void + { + $this->expectException(\LogicException::class); + $this->sut->hasModulesToInstall(); + } - /** - * @covers ::hasPatches - */ - public function testHasPatches(): void { - $this->expectException(\LogicException::class); - $this->sut->hasPatches(); - } + /** + * @covers ::getModulesToInstall + */ + public function testGetModulesToInstall(): void + { + $this->expectException(\LogicException::class); + $this->sut->getModulesToInstall(); + } - /** - * @covers ::isVetted - */ - public function testIsVetted(): void { - $this->expectException(\LogicException::class); - $this->sut->isVetted(); - } + /** + * @covers ::hasPatches + */ + public function testHasPatches(): void + { + $this->expectException(\LogicException::class); + $this->sut->hasPatches(); + } - /** - * @covers ::getPatches - */ - public function testGetPatches(): void { - $this->expectException(\LogicException::class); - $this->sut->getPatches(); - } + /** + * @covers ::isVetted + */ + public function testIsVetted(): void + { + $this->expectException(\LogicException::class); + $this->sut->isVetted(); + } + /** + * @covers ::getPatches + */ + public function testGetPatches(): void + { + $this->expectException(\LogicException::class); + $this->sut->getPatches(); + } } diff --git a/tests/phpunit/src/Commands/App/From/ConfigurationTest.php b/tests/phpunit/src/Commands/App/From/ConfigurationTest.php index cb8af7e21..b7d6071ea 100644 --- a/tests/phpunit/src/Commands/App/From/ConfigurationTest.php +++ b/tests/phpunit/src/Commands/App/From/ConfigurationTest.php @@ -1,6 +1,6 @@ expectExceptionObject($expected_exception); - Configuration::createFromResource($test_stream); - } - - /** - * @return array - */ - public function getTestConfigurations(): array { - return [ - 'bad JSON in configuration file' => ['{,}', new JsonException('Syntax error', JSON_ERROR_SYNTAX)], - 'empty configuration file' => [json_encode((object) []), new DomainException('Missing required key: rootPackageDefinition')], - ]; - } - +class ConfigurationTest extends TestCase +{ + use ProphecyTrait; + + protected Configuration $sut; + + /** + * @param string $configuration + * A JSON string from which to create a configuration object. + * @dataProvider getTestConfigurations + */ + public function test(string $configuration, Exception $expected_exception): void + { + $test_stream = fopen('php://memory', 'rw'); + fwrite($test_stream, $configuration); + rewind($test_stream); + $this->expectExceptionObject($expected_exception); + Configuration::createFromResource($test_stream); + } + + /** + * @return array + */ + public function getTestConfigurations(): array + { + return [ + 'bad JSON in configuration file' => ['{,}', new JsonException('Syntax error', JSON_ERROR_SYNTAX)], + 'empty configuration file' => [json_encode((object) []), new DomainException('Missing required key: rootPackageDefinition')], + ]; + } } diff --git a/tests/phpunit/src/Commands/App/From/DefinedRecommendationTest.php b/tests/phpunit/src/Commands/App/From/DefinedRecommendationTest.php index 7e72e9eba..7f4c1511b 100755 --- a/tests/phpunit/src/Commands/App/From/DefinedRecommendationTest.php +++ b/tests/phpunit/src/Commands/App/From/DefinedRecommendationTest.php @@ -1,6 +1,6 @@ assertInstanceOf(NoRecommendation::class, $actual); - } - else { - $this->assertInstanceOf(RecommendationInterface::class, $actual); - $this->assertNotInstanceOf(NoRecommendation::class, $actual); - $extension_prophecy = $this->prophesize(ExtensionInterface::class); - $extension_prophecy->getName()->willReturn('bar'); - $mock_extension = $extension_prophecy->reveal(); - if (!$actual instanceof UniversalRecommendation) { - $this->assertSame($expected->applies($mock_extension), $actual->applies($mock_extension)); - } - if ($expected->getPackageName() === TestRecommendation::ABANDON) { - $this->assertInstanceOf(AbandonmentRecommendation::class, $actual); - } - else { - $this->assertSame($expected->getPackageName(), $actual->getPackageName()); - $this->assertSame($expected->getVersionConstraint(), $actual->getVersionConstraint()); - $this->assertSame($expected->getPatches(), $actual->getPatches()); - } + /** + * @dataProvider getTestConfigurations + */ + public function test(mixed $configuration, RecommendationInterface $expected): void + { + $actual = DefinedRecommendation::createFromDefinition($configuration); + if ($expected instanceof NoRecommendation) { + $this->assertInstanceOf(NoRecommendation::class, $actual); + } else { + $this->assertInstanceOf(RecommendationInterface::class, $actual); + $this->assertNotInstanceOf(NoRecommendation::class, $actual); + $extension_prophecy = $this->prophesize(ExtensionInterface::class); + $extension_prophecy->getName()->willReturn('bar'); + $mock_extension = $extension_prophecy->reveal(); + if (!$actual instanceof UniversalRecommendation) { + $this->assertSame($expected->applies($mock_extension), $actual->applies($mock_extension)); + } + if ($expected->getPackageName() === TestRecommendation::ABANDON) { + $this->assertInstanceOf(AbandonmentRecommendation::class, $actual); + } else { + $this->assertSame($expected->getPackageName(), $actual->getPackageName()); + $this->assertSame($expected->getVersionConstraint(), $actual->getVersionConstraint()); + $this->assertSame($expected->getPatches(), $actual->getPatches()); + } + } } - } - /** - * @return array - */ - public function getTestConfigurations(): array { - // phpcs:disable SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys - return [ - 'config is not array' => [42, new NoRecommendation()], - 'empty array' => [[], new NoRecommendation()], - 'missing required key' => [ + /** + * @return array + */ + public function getTestConfigurations(): array + { + // phpcs:disable SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys + return [ + 'config is not array' => [42, new NoRecommendation()], + 'empty array' => [[], new NoRecommendation()], + 'missing required key' => [ ['package' => '', 'constraint' => ''], new NoRecommendation(), - ], - 'key value does not match schema' => [ + ], + 'key value does not match schema' => [ ['package' => 42, 'constraint' => '', 'replaces' => ['name' => '']], new NoRecommendation(), - ], - 'nested key value does not match schema' => [ + ], + 'nested key value does not match schema' => [ ['package' => '', 'constraint' => '', 'replaces' => ['name' => 42]], new NoRecommendation(), - ], - 'invalid patches key' => [ + ], + 'invalid patches key' => [ [ - 'package' => 'foo', - 'constraint' => '^1.42', - 'patches' => [ - 0 => 'https://example.com', - ], - 'replaces' => [ - 'name' => 'foo', - ], + 'package' => 'foo', + 'constraint' => '^1.42', + 'patches' => [ + 0 => 'https://example.com', + ], + 'replaces' => [ + 'name' => 'foo', + ], ], new NoRecommendation(), - ], - 'invalid patches key value' => [ + ], + 'invalid patches key value' => [ [ - 'package' => 'foo', - 'constraint' => '^1.42', - 'patches' => [ - 'A patch description' => TRUE, - ], - 'replaces' => [ - 'name' => 'foo', - ], + 'package' => 'foo', + 'constraint' => '^1.42', + 'patches' => [ + 'A patch description' => true, + ], + 'replaces' => [ + 'name' => 'foo', + ], ], new NoRecommendation(), - ], - 'missing replaces key, not universal by default' => [ + ], + 'missing replaces key, not universal by default' => [ [ - 'package' => 'foo', - 'constraint' => '^1.42', + 'package' => 'foo', + 'constraint' => '^1.42', ], new NoRecommendation(), - ], - 'missing replaces key, explicitly not universal' => [ + ], + 'missing replaces key, explicitly not universal' => [ [ - 'universal' => FALSE, - 'package' => 'foo', - 'constraint' => '^1.42', + 'universal' => false, + 'package' => 'foo', + 'constraint' => '^1.42', ], new NoRecommendation(), - ], - 'valid config; does not apply' => [ + ], + 'valid config; does not apply' => [ [ - 'package' => 'foo', - 'constraint' => '^1.42', - 'replaces' => [ - 'name' => 'foo', - ], - ], - new TestRecommendation(FALSE, 'foo', '^1.42'), - ], - 'valid config; does apply; missing replaces key but universal is true' => [ + 'package' => 'foo', + 'constraint' => '^1.42', + 'replaces' => [ + 'name' => 'foo', + ], + ], + new TestRecommendation(false, 'foo', '^1.42'), + ], + 'valid config; does apply; missing replaces key but universal is true' => [ [ - 'universal' => TRUE, - 'package' => 'foo', - 'constraint' => '^1.42', + 'universal' => true, + 'package' => 'foo', + 'constraint' => '^1.42', ], - new TestRecommendation(TRUE, 'foo', '^1.42'), - ], - 'valid config; does apply; no patches key' => [ + new TestRecommendation(true, 'foo', '^1.42'), + ], + 'valid config; does apply; no patches key' => [ [ - 'package' => 'foo', - 'constraint' => '^1.42', - 'replaces' => [ - 'name' => 'bar', - ], - ], - new TestRecommendation(TRUE, 'foo', '^1.42'), - ], - 'valid config; does apply; empty patches value' => [ + 'package' => 'foo', + 'constraint' => '^1.42', + 'replaces' => [ + 'name' => 'bar', + ], + ], + new TestRecommendation(true, 'foo', '^1.42'), + ], + 'valid config; does apply; empty patches value' => [ [ - 'package' => 'foo', - 'constraint' => '^1.42', - 'patches' => [], - 'replaces' => [ - 'name' => 'bar', - ], - ], - new TestRecommendation(TRUE, 'foo', '^1.42'), - ], - 'valid config; does apply; has patches' => [ + 'package' => 'foo', + 'constraint' => '^1.42', + 'patches' => [], + 'replaces' => [ + 'name' => 'bar', + ], + ], + new TestRecommendation(true, 'foo', '^1.42'), + ], + 'valid config; does apply; has patches' => [ [ - 'package' => 'foo', - 'constraint' => '^1.42', - 'patches' => [ - 'A patch description' => 'https://example.com/example.patch', - ], - 'install' => ['foo'], - 'replaces' => [ - 'name' => 'bar', - ], - ], - new TestRecommendation(TRUE, 'foo', '^1.42', ['foo'], FALSE, [ - 'A patch description' => 'https://example.com/example.patch', + 'package' => 'foo', + 'constraint' => '^1.42', + 'patches' => [ + 'A patch description' => 'https://example.com/example.patch', + ], + 'install' => ['foo'], + 'replaces' => [ + 'name' => 'bar', + ], + ], + new TestRecommendation(true, 'foo', '^1.42', ['foo'], false, [ + 'A patch description' => 'https://example.com/example.patch', ]), - ], - 'valid config; does apply; has null package property' => [ + ], + 'valid config; does apply; has null package property' => [ [ - 'package' => NULL, - 'note' => 'Example: The module bar is no longer required because its functionality has been incorporated into Drupal core.', - 'replaces' => [ - 'name' => 'bar', - ], - 'vetted' => TRUE, - ], - new TestRecommendation(TRUE, TestRecommendation::ABANDON), - ], - ]; - // phpcs:enable - } - + 'package' => null, + 'note' => 'Example: The module bar is no longer required because its functionality has been incorporated into Drupal core.', + 'replaces' => [ + 'name' => 'bar', + ], + 'vetted' => true, + ], + new TestRecommendation(true, TestRecommendation::ABANDON), + ], + ]; + // phpcs:enable + } } diff --git a/tests/phpunit/src/Commands/App/From/ProjectBuilderTest.php b/tests/phpunit/src/Commands/App/From/ProjectBuilderTest.php index 19f71f038..f9d1c8f2a 100644 --- a/tests/phpunit/src/Commands/App/From/ProjectBuilderTest.php +++ b/tests/phpunit/src/Commands/App/From/ProjectBuilderTest.php @@ -1,6 +1,6 @@ assertSame($project_builder->buildProject(), $expected_project_definition); + } - /** - * @dataProvider getTestResources - * @param resource $configuration_resource - * @param resource $recommendations_resource - */ - public function test($configuration_resource, $recommendations_resource, array $expected_project_definition): void { - assert(is_resource($configuration_resource)); - assert(is_resource($recommendations_resource)); - $configuration = Configuration::createFromResource($configuration_resource); - $site_inspector = new TestSiteInspector(); - $resolver = new Resolver($site_inspector, Recommendations::createFromResource($recommendations_resource)); - $project_builder = new ProjectBuilder($configuration, $resolver, $site_inspector); - $this->assertSame($project_builder->buildProject(), $expected_project_definition); - } - - /** - * @return array - */ - public function getTestResources(): array { - // phpcs:disable SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys - $test_cases = [ - 'simplest case, sanity check' => [ + /** + * @return array + */ + public function getTestResources(): array + { + // phpcs:disable SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys + $test_cases = [ + 'simplest case, sanity check' => [ json_encode([ - 'sourceModules' => [], - 'filePaths' => [ - 'public' => 'sites/default/files', - 'private' => NULL, - ], - 'rootPackageDefinition' => [], + 'sourceModules' => [], + 'filePaths' => [ + 'public' => 'sites/default/files', + 'private' => null, + ], + 'rootPackageDefinition' => [], ]), json_encode([ - 'data' => [], + 'data' => [], ]), [ - 'installModules' => [], - 'filePaths' => [ - 'public' => 'sites/default/files', - 'private' => NULL, - ], - 'sourceModules' => [], - 'recommendations' => [], - 'rootPackageDefinition' => [], + 'installModules' => [], + 'filePaths' => [ + 'public' => 'sites/default/files', + 'private' => null, ], - ], - ]; - // phpcs:enable - return array_map(function (array $data) { - [$configuration_json, $recommendation_json, $expectation] = $data; - $config_resource = fopen('php://memory', 'rw'); - fwrite($config_resource, $configuration_json); - rewind($config_resource); - $recommendation_resource = fopen('php://memory', 'rw'); - fwrite($recommendation_resource, $recommendation_json); - rewind($recommendation_resource); - return [$config_resource, $recommendation_resource, $expectation]; - }, $test_cases); - } - + 'sourceModules' => [], + 'recommendations' => [], + 'rootPackageDefinition' => [], + ], + ], + ]; + // phpcs:enable + return array_map(function (array $data) { + [$configuration_json, $recommendation_json, $expectation] = $data; + $config_resource = fopen('php://memory', 'rw'); + fwrite($config_resource, $configuration_json); + rewind($config_resource); + $recommendation_resource = fopen('php://memory', 'rw'); + fwrite($recommendation_resource, $recommendation_json); + rewind($recommendation_resource); + return [$config_resource, $recommendation_resource, $expectation]; + }, $test_cases); + } } diff --git a/tests/phpunit/src/Commands/App/From/RecommendationsTest.php b/tests/phpunit/src/Commands/App/From/RecommendationsTest.php index f9b391982..0b1381554 100644 --- a/tests/phpunit/src/Commands/App/From/RecommendationsTest.php +++ b/tests/phpunit/src/Commands/App/From/RecommendationsTest.php @@ -1,6 +1,6 @@ assertEmpty($recommendations); - } - else { - assert($expectation instanceof RecommendationInterface); - $this->assertNotEmpty($recommendations); - $extension_prophecy = $this->prophesize(ExtensionInterface::class); - $extension_prophecy->getName()->willReturn('foo'); - $mock_extension = $extension_prophecy->reveal(); - $actual_recommendation = $recommendations->current(); - $this->assertSame($expectation->applies($mock_extension), $actual_recommendation->applies($mock_extension)); - $this->assertSame($expectation->getPackageName(), $actual_recommendation->getPackageName()); - $this->assertSame($expectation->getVersionConstraint(), $actual_recommendation->getVersionConstraint()); + /** + * @param string $configuration + * A JSON string from which to create a configuration object. + * @param \Acquia\Cli\Command\App\From\Recommendation\RecommendationInterface|\JsonException $expectation + * An expected recommendation or a JSON exception in the case that the given + * $configuration is malformed. + * @dataProvider getTestConfigurations + */ + public function test(string $configuration, mixed $expectation): void + { + $test_stream = fopen('php://memory', 'rw'); + fwrite($test_stream, $configuration); + rewind($test_stream); + $recommendations = Recommendations::createFromResource($test_stream); + if ($expectation === static::NO_RECOMMENDATIONS) { + $this->assertEmpty($recommendations); + } else { + assert($expectation instanceof RecommendationInterface); + $this->assertNotEmpty($recommendations); + $extension_prophecy = $this->prophesize(ExtensionInterface::class); + $extension_prophecy->getName()->willReturn('foo'); + $mock_extension = $extension_prophecy->reveal(); + $actual_recommendation = $recommendations->current(); + $this->assertSame($expectation->applies($mock_extension), $actual_recommendation->applies($mock_extension)); + $this->assertSame($expectation->getPackageName(), $actual_recommendation->getPackageName()); + $this->assertSame($expectation->getVersionConstraint(), $actual_recommendation->getVersionConstraint()); + } } - } - /** - * @return array - */ - public function getTestConfigurations(): array { - // phpcs:disable SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys - return [ - 'bad JSON in configuration file' => [ + /** + * @return array + */ + public function getTestConfigurations(): array + { + // phpcs:disable SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys + return [ + 'bad JSON in configuration file' => [ '{,}', static::NO_RECOMMENDATIONS, - ], - 'empty configuration file' => [ + ], + 'empty configuration file' => [ json_encode((object) []), static::NO_RECOMMENDATIONS, - ], - 'unexpected recommendations value' => [ - json_encode(['data' => TRUE]), + ], + 'unexpected recommendations value' => [ + json_encode(['data' => true]), static::NO_RECOMMENDATIONS, - ], - 'empty recommendations key' => [ + ], + 'empty recommendations key' => [ json_encode(['data' => []]), static::NO_RECOMMENDATIONS, - ], - 'populated recommendations key with invalid item' => [ + ], + 'populated recommendations key with invalid item' => [ json_encode(['recommendations' => [[]]]), static::NO_RECOMMENDATIONS, - ], - 'populated recommendations key with valid item' => [ + ], + 'populated recommendations key with valid item' => [ json_encode([ - 'data' => [ - [ - 'package' => 'foo', - 'constraint' => '^1.42', - 'replaces' => [ - 'name' => 'foo', - ], - ], - ], + 'data' => [ + [ + 'package' => 'foo', + 'constraint' => '^1.42', + 'replaces' => [ + 'name' => 'foo', + ], + ], + ], ]), - new TestRecommendation(TRUE, 'foo', '^1.42'), - ], - ]; - // phpcs:enable - } - + new TestRecommendation(true, 'foo', '^1.42'), + ], + ]; + // phpcs:enable + } } diff --git a/tests/phpunit/src/Commands/App/From/TestRecommendation.php b/tests/phpunit/src/Commands/App/From/TestRecommendation.php index fde9df632..6b6a00e71 100644 --- a/tests/phpunit/src/Commands/App/From/TestRecommendation.php +++ b/tests/phpunit/src/Commands/App/From/TestRecommendation.php @@ -1,6 +1,6 @@ packageName = $package_name ?: self::ABANDON; - } - - public function applies(ExtensionInterface $extension): bool { - return $this->shouldApply; - } - - public function getPackageName(): string { - return $this->packageName; - } - - public function getVersionConstraint(): string { - return $this->versionConstraint; - } - - public function hasModulesToInstall(): bool { - return !empty($this->install); - } - - /** - * {@inheritDoc} - */ - public function getModulesToInstall(): array { - return $this->install; - } - - public function isVetted(): bool { - return $this->vetted; - } - - public function hasPatches(): bool { - return !empty($this->patches); - } - - /** - * {@inheritDoc} - */ - public function getPatches(): array { - return $this->patches; - } - +final class TestRecommendation implements RecommendationInterface +{ + public const ABANDON = '%ABANDON%'; + + protected string $packageName; + + public function __construct( + protected bool $shouldApply, + ?string $package_name, + protected string $versionConstraint = 'n/a', + protected array $install = [], + protected bool $vetted = false, + protected array $patches = [] + ) { + assert(!is_null($package_name) || $versionConstraint === 'n/a'); + $this->packageName = $package_name ?: self::ABANDON; + } + + public function applies(ExtensionInterface $extension): bool + { + return $this->shouldApply; + } + + public function getPackageName(): string + { + return $this->packageName; + } + + public function getVersionConstraint(): string + { + return $this->versionConstraint; + } + + public function hasModulesToInstall(): bool + { + return !empty($this->install); + } + + /** + * {@inheritDoc} + */ + public function getModulesToInstall(): array + { + return $this->install; + } + + public function isVetted(): bool + { + return $this->vetted; + } + + public function hasPatches(): bool + { + return !empty($this->patches); + } + + /** + * {@inheritDoc} + */ + public function getPatches(): array + { + return $this->patches; + } } diff --git a/tests/phpunit/src/Commands/App/From/TestSiteInspector.php b/tests/phpunit/src/Commands/App/From/TestSiteInspector.php index 7082a2eb1..41c712f7e 100644 --- a/tests/phpunit/src/Commands/App/From/TestSiteInspector.php +++ b/tests/phpunit/src/Commands/App/From/TestSiteInspector.php @@ -1,6 +1,6 @@ extensions; - } - - public function getPublicFilePath(): string { - return $this->filePublicPath; - } - - public function getPrivateFilePath(): ?string { - return $this->filePrivatePath; - } - +final class TestSiteInspector extends SiteInspectorBase +{ + /** + * TestSiteInspector constructor. + * + * @param \Acquia\Cli\Command\App\From\SourceSite\ExtensionInterface[] $extensions + * An array of extensions. + */ + public function __construct( + protected array $extensions = [], + protected string $filePublicPath = 'sites/default/files', + protected ?string $filePrivatePath = null + ) { + } + + /** + * {@inheritDoc} + */ + protected function readExtensions(): array + { + return $this->extensions; + } + + public function getPublicFilePath(): string + { + return $this->filePublicPath; + } + + public function getPrivateFilePath(): ?string + { + return $this->filePrivatePath; + } } diff --git a/tests/phpunit/src/Commands/App/LinkCommandTest.php b/tests/phpunit/src/Commands/App/LinkCommandTest.php index 25604aaa7..85a26744c 100644 --- a/tests/phpunit/src/Commands/App/LinkCommandTest.php +++ b/tests/phpunit/src/Commands/App/LinkCommandTest.php @@ -1,6 +1,6 @@ injectCommand(LinkCommand::class); - } - - public function testLinkCommand(): void { - $applications = $this->mockRequest('getApplications'); - $this->mockApplicationRequest(); - - $inputs = [ - // Would you like Acquia CLI to search for a Cloud application that matches your local git config? - 'n', - // Select a Cloud Platform application. - 0, - ]; - $this->executeCommand([], $inputs); - $output = $this->getDisplay(); - $this->assertEquals($applications[self::$INPUT_DEFAULT_CHOICE]->uuid, $this->datastoreAcli->get('cloud_app_uuid')); - $this->assertStringContainsString('There is no Cloud Platform application linked to', $output); - $this->assertStringContainsString('Select a Cloud Platform application', $output); - $this->assertStringContainsString('[0] Sample application 1', $output); - $this->assertStringContainsString('[1] Sample application 2', $output); - $this->assertStringContainsString('The Cloud application Sample application 1 has been linked', $output); - } - - public function testLinkCommandAlreadyLinked(): void { - $this->createMockAcliConfigFile('a47ac10b-58cc-4372-a567-0e02b2c3d470'); - $this->mockApplicationRequest(); - $this->executeCommand(); - $output = $this->getDisplay(); - $this->assertStringContainsString('This repository is already linked to Cloud application', $output); - $this->assertEquals(1, $this->getStatusCode()); - } - - /** - * @group brokenProphecy - */ - public function testLinkCommandInvalidDir(): void { - $this->mockRequest('getApplications'); - $this->command->setProjectDir(''); - $this->expectException(AcquiaCliException::class); - $this->expectExceptionMessage('Could not find a local Drupal project.'); - $this->executeCommand(); - } +class LinkCommandTest extends CommandTestBase +{ + protected function createCommand(): CommandBase + { + return $this->injectCommand(LinkCommand::class); + } + public function testLinkCommand(): void + { + $applications = $this->mockRequest('getApplications'); + $this->mockApplicationRequest(); + + $inputs = [ + // Would you like Acquia CLI to search for a Cloud application that matches your local git config? + 'n', + // Select a Cloud Platform application. + 0, + ]; + $this->executeCommand([], $inputs); + $output = $this->getDisplay(); + $this->assertEquals($applications[self::$INPUT_DEFAULT_CHOICE]->uuid, $this->datastoreAcli->get('cloud_app_uuid')); + $this->assertStringContainsString('There is no Cloud Platform application linked to', $output); + $this->assertStringContainsString('Select a Cloud Platform application', $output); + $this->assertStringContainsString('[0] Sample application 1', $output); + $this->assertStringContainsString('[1] Sample application 2', $output); + $this->assertStringContainsString('The Cloud application Sample application 1 has been linked', $output); + } + + public function testLinkCommandAlreadyLinked(): void + { + $this->createMockAcliConfigFile('a47ac10b-58cc-4372-a567-0e02b2c3d470'); + $this->mockApplicationRequest(); + $this->executeCommand(); + $output = $this->getDisplay(); + $this->assertStringContainsString('This repository is already linked to Cloud application', $output); + $this->assertEquals(1, $this->getStatusCode()); + } + + /** + * @group brokenProphecy + */ + public function testLinkCommandInvalidDir(): void + { + $this->mockRequest('getApplications'); + $this->command->setProjectDir(''); + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage('Could not find a local Drupal project.'); + $this->executeCommand(); + } } diff --git a/tests/phpunit/src/Commands/App/LogTailCommandTest.php b/tests/phpunit/src/Commands/App/LogTailCommandTest.php index 3190abfec..6bd3d3a1e 100644 --- a/tests/phpunit/src/Commands/App/LogTailCommandTest.php +++ b/tests/phpunit/src/Commands/App/LogTailCommandTest.php @@ -1,6 +1,6 @@ logStreamManagerProphecy = $this->prophet->prophesize(LogstreamManager::class); - protected function createCommand(): CommandBase { - // Must initialize this here instead of in setUp() because we need the - // prophet to be initialized first. - $this->logStreamManagerProphecy = $this->prophet->prophesize(LogstreamManager::class); + return new LogTailCommand( + $this->localMachineHelper, + $this->datastoreCloud, + $this->datastoreAcli, + $this->cloudCredentials, + $this->telemetryHelper, + $this->acliRepoRoot, + $this->clientServiceProphecy->reveal(), + $this->sshHelper, + $this->sshDir, + $this->logger, + $this->logStreamManagerProphecy->reveal() + ); + } - return new LogTailCommand( - $this->localMachineHelper, - $this->datastoreCloud, - $this->datastoreAcli, - $this->cloudCredentials, - $this->telemetryHelper, - $this->acliRepoRoot, - $this->clientServiceProphecy->reveal(), - $this->sshHelper, - $this->sshDir, - $this->logger, - $this->logStreamManagerProphecy->reveal() - ); - } + /** + * @dataProvider providerLogTailCommand + */ + public function testLogTailCommand(?int $stream): void + { + $this->logStreamManagerProphecy->setColourise(true)->shouldBeCalled(); + $this->logStreamManagerProphecy->setParams(Argument::type('object'))->shouldBeCalled(); + $this->logStreamManagerProphecy->setLogTypeFilter(["bal-request"])->shouldBeCalled(); + $this->logStreamManagerProphecy->stream()->shouldBeCalled(); + $this->mockGetEnvironment(); + $this->mockLogStreamRequest(); + $this->executeCommand([], [ + // Would you like Acquia CLI to search for a Cloud application that matches your local git config? + 'n', + // Select the application. + 0, + // Would you like to link the project at ... ? + 'y', + // Select environment. + 0, + // Select log. + $stream, + ]); - /** - * @dataProvider providerLogTailCommand - */ - public function testLogTailCommand(?int $stream): void { - $this->logStreamManagerProphecy->setColourise(TRUE)->shouldBeCalled(); - $this->logStreamManagerProphecy->setParams(Argument::type('object'))->shouldBeCalled(); - $this->logStreamManagerProphecy->setLogTypeFilter(["bal-request"])->shouldBeCalled(); - $this->logStreamManagerProphecy->stream()->shouldBeCalled(); - $this->mockGetEnvironment(); - $this->mockLogStreamRequest(); - $this->executeCommand([], [ - // Would you like Acquia CLI to search for a Cloud application that matches your local git config? - 'n', - // Select the application. - 0, - // Would you like to link the project at ... ? - 'y', - // Select environment. - 0, - // Select log. - $stream, - ]); + // Assert. + $output = $this->getDisplay(); + $this->assertStringContainsString('Select a Cloud Platform application:', $output); + $this->assertStringContainsString('[0] Sample application 1', $output); + $this->assertStringContainsString('[1] Sample application 2', $output); + $this->assertStringContainsString('Apache request', $output); + $this->assertStringContainsString('Drupal request', $output); + } - // Assert. - $output = $this->getDisplay(); - $this->assertStringContainsString('Select a Cloud Platform application:', $output); - $this->assertStringContainsString('[0] Sample application 1', $output); - $this->assertStringContainsString('[1] Sample application 2', $output); - $this->assertStringContainsString('Apache request', $output); - $this->assertStringContainsString('Drupal request', $output); - } + public function testLogTailCommandWithEnvArg(): void + { + $this->mockRequest('getEnvironment', '24-a47ac10b-58cc-4372-a567-0e02b2c3d470'); + $this->mockLogStreamRequest(); + $this->executeCommand( + ['environmentId' => '24-a47ac10b-58cc-4372-a567-0e02b2c3d470'], + // Select log. + [0] + ); - public function testLogTailCommandWithEnvArg(): void { - $this->mockRequest('getEnvironment', '24-a47ac10b-58cc-4372-a567-0e02b2c3d470'); - $this->mockLogStreamRequest(); - $this->executeCommand( - ['environmentId' => '24-a47ac10b-58cc-4372-a567-0e02b2c3d470'], - // Select log. - [0] - ); + // Assert. + $output = $this->getDisplay(); + $this->assertStringContainsString('Apache request', $output); + $this->assertStringContainsString('Drupal request', $output); + } - // Assert. - $output = $this->getDisplay(); - $this->assertStringContainsString('Apache request', $output); - $this->assertStringContainsString('Drupal request', $output); - } - - public function testLogTailNode(): void { - $applications = $this->mockRequest('getApplications'); - $application = $this->mockRequest('getApplicationByUuid', $applications[self::$INPUT_DEFAULT_CHOICE]->uuid); - $tamper = function ($responses): void { - foreach ($responses as $response) { - $response->type = 'node'; - } - }; - $this->mockRequest('getApplicationEnvironments', $application->uuid, NULL, NULL, $tamper); - $this->expectException(AcquiaCliException::class); - $this->expectExceptionMessage('No compatible environments found'); - $this->executeCommand([], [ - // Would you like Acquia CLI to search for a Cloud application that matches your local git config? - 'n', - // Select the application. - 0, - // Would you like to link the project at ... ? - 'y', - // Select environment. - 0, - // Select log. - 0, - ]); - } - - private function mockLogStreamRequest(): void { - $response = $this->getMockResponseFromSpec('/environments/{environmentId}/logstream', - 'get', '200'); - $this->clientProphecy->request('get', - '/environments/24-a47ac10b-58cc-4372-a567-0e02b2c3d470/logstream') - ->willReturn($response) - ->shouldBeCalled(); - } + public function testLogTailNode(): void + { + $applications = $this->mockRequest('getApplications'); + $application = $this->mockRequest('getApplicationByUuid', $applications[self::$INPUT_DEFAULT_CHOICE]->uuid); + $tamper = function ($responses): void { + foreach ($responses as $response) { + $response->type = 'node'; + } + }; + $this->mockRequest('getApplicationEnvironments', $application->uuid, null, null, $tamper); + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage('No compatible environments found'); + $this->executeCommand([], [ + // Would you like Acquia CLI to search for a Cloud application that matches your local git config? + 'n', + // Select the application. + 0, + // Would you like to link the project at ... ? + 'y', + // Select environment. + 0, + // Select log. + 0, + ]); + } + private function mockLogStreamRequest(): void + { + $response = $this->getMockResponseFromSpec( + '/environments/{environmentId}/logstream', + 'get', + '200' + ); + $this->clientProphecy->request( + 'get', + '/environments/24-a47ac10b-58cc-4372-a567-0e02b2c3d470/logstream' + ) + ->willReturn($response) + ->shouldBeCalled(); + } } diff --git a/tests/phpunit/src/Commands/App/NewCommandTest.php b/tests/phpunit/src/Commands/App/NewCommandTest.php index cc2171f3e..90b502fda 100644 --- a/tests/phpunit/src/Commands/App/NewCommandTest.php +++ b/tests/phpunit/src/Commands/App/NewCommandTest.php @@ -1,6 +1,6 @@ setupFsFixture(); - } - - protected function createCommand(): CommandBase { - return $this->injectCommand(NewCommand::class); - } - - /** - * @return array - */ - public function provideTestNewDrupalCommand(): array { - return [ - [['acquia_drupal_recommended' => 'acquia/drupal-recommended-project']], - [['acquia_drupal_recommended' => 'acquia/drupal-recommended-project', 'test-dir']], - ]; - } - - /** - * @return array - */ - public function provideTestNewNextJsAppCommand(): array { - return [ - [['acquia_next_acms' => 'acquia/next-acms']], - [['acquia_next_acms' => 'acquia/next-acms'], 'test-dir'], - ]; - } - - /** - * @dataProvider provideTestNewDrupalCommand - */ - public function testNewDrupalCommand(array $package, string $directory = 'drupal'): void { - $this->newProjectDir = Path::makeAbsolute($directory, $this->projectDir); - $projectKey = array_keys($package)[0]; - $project = $package[$projectKey]; - - $process = $this->prophet->prophesize(Process::class); - $process->isSuccessful()->willReturn(TRUE); - $process->getExitCode()->willReturn(0); - - $localMachineHelper = $this->mockLocalMachineHelper(); - - $mockFileSystem = $this->mockGetFilesystem($localMachineHelper); - $localMachineHelper->checkRequiredBinariesExist(["composer"])->shouldBeCalled(); - $this->mockExecuteComposerCreate($this->newProjectDir, $localMachineHelper, $process, $project); - $localMachineHelper->checkRequiredBinariesExist(["git"])->shouldBeCalled(); - $this->mockExecuteGitInit($localMachineHelper, $this->newProjectDir, $process); - $this->mockExecuteGitAdd($localMachineHelper, $this->newProjectDir, $process); - $this->mockExecuteGitCommit($localMachineHelper, $this->newProjectDir, $process); - - $inputs = [ - // Choose a starting project. - $project, - ]; - $this->executeCommand([ - 'directory' => $directory, - ], $inputs); - - $output = $this->getDisplay(); - $this->assertStringContainsString('Acquia recommends most customers use acquia/drupal-recommended-project to setup a Drupal project', $output); - $this->assertStringContainsString('Choose a starting project', $output); - $this->assertStringContainsString($project, $output); - $this->assertTrue($mockFileSystem->isAbsolutePath($this->newProjectDir), 'Directory path is not absolute'); - $this->assertStringContainsString('New 💧 Drupal project created in ' . $this->newProjectDir, $output); - } - - /** - * @dataProvider provideTestNewNextJsAppCommand - */ - public function testNewNextJSAppCommand(array $package, string $directory = 'nextjs'): void { - $this->newProjectDir = Path::makeAbsolute($directory, $this->projectDir); - $projectKey = array_keys($package)[0]; - $project = $package[$projectKey]; - - $process = $this->prophet->prophesize(Process::class); - $process->isSuccessful()->willReturn(TRUE); - $process->getExitCode()->willReturn(0); - - $localMachineHelper = $this->mockLocalMachineHelper(); - - $mockFileSystem = $this->mockGetFilesystem($localMachineHelper); - - $localMachineHelper->checkRequiredBinariesExist(["node"])->shouldBeCalled(); - $this->mockExecuteNpxCreate($this->newProjectDir, $localMachineHelper, $process); - $localMachineHelper->checkRequiredBinariesExist(["git"])->shouldBeCalled(); - $this->mockExecuteGitInit($localMachineHelper, $this->newProjectDir, $process); - $this->mockExecuteGitAdd($localMachineHelper, $this->newProjectDir, $process); - $this->mockExecuteGitCommit($localMachineHelper, $this->newProjectDir, $process); - - $inputs = [ - // Choose a starting project. - $project, - ]; - $this->executeCommand([ - 'directory' => $directory, - ], $inputs); - - $output = $this->getDisplay(); - $this->assertStringContainsString('acquia/next-acms is a starter template for building a headless site', $output); - $this->assertStringContainsString('Choose a starting project', $output); - $this->assertStringContainsString($project, $output); - $this->assertTrue($mockFileSystem->isAbsolutePath($this->newProjectDir), 'Directory path is not absolute'); - $this->assertStringContainsString('New Next JS project created in ' . $this->newProjectDir, $output); - } - - protected function mockExecuteComposerCreate( - string $projectDir, - ObjectProphecy $localMachineHelper, - ObjectProphecy $process, - string $project - ): void { - $command = [ - 'composer', - 'create-project', - $project, - $projectDir, - '--no-interaction', - ]; - $localMachineHelper - ->execute($command) - ->willReturn($process->reveal()) - ->shouldBeCalled(); - } - - protected function mockExecuteNpxCreate( - string $projectDir, - ObjectProphecy $localMachineHelper, - ObjectProphecy $process, - ): void { - $command = [ - 'npx', - 'create-next-app', - '-e', - 'https://github.com/acquia/next-acms/tree/main/starters/basic-starter', - $projectDir, - ]; - $localMachineHelper - ->execute($command) - ->willReturn($process->reveal()) - ->shouldBeCalled(); - } - - protected function mockExecuteGitInit( - ObjectProphecy $localMachineHelper, - string $projectDir, - ObjectProphecy $process - ): void { - $command = [ - 'git', - 'init', - '--initial-branch=main', - ]; - $localMachineHelper - ->execute($command, NULL, $projectDir) - ->willReturn($process->reveal()) - ->shouldBeCalled(); - } - - protected function mockExecuteGitAdd( - ObjectProphecy $localMachineHelper, - string $projectDir, - ObjectProphecy $process - ): void { - $command = [ - 'git', - 'add', - '-A', - ]; - $localMachineHelper - ->execute($command, NULL, $projectDir) - ->willReturn($process->reveal()) - ->shouldBeCalled(); - } - - protected function mockExecuteGitCommit( - ObjectProphecy $localMachineHelper, - string $projectDir, - ObjectProphecy $process - ): void { - $command = [ - 'git', - 'commit', - '--message', - 'Initial commit.', - '--quiet', - ]; - $localMachineHelper - ->execute($command, NULL, $projectDir) - ->willReturn($process->reveal()) - ->shouldBeCalled(); - } - +class NewCommandTest extends CommandTestBase +{ + protected string $newProjectDir; + + public function setUp(): void + { + parent::setUp(); + $this->setupFsFixture(); + } + + protected function createCommand(): CommandBase + { + return $this->injectCommand(NewCommand::class); + } + + /** + * @return array + */ + public function provideTestNewDrupalCommand(): array + { + return [ + [['acquia_drupal_recommended' => 'acquia/drupal-recommended-project']], + [['acquia_drupal_recommended' => 'acquia/drupal-recommended-project', 'test-dir']], + ]; + } + + /** + * @return array + */ + public function provideTestNewNextJsAppCommand(): array + { + return [ + [['acquia_next_acms' => 'acquia/next-acms']], + [['acquia_next_acms' => 'acquia/next-acms'], 'test-dir'], + ]; + } + + /** + * @dataProvider provideTestNewDrupalCommand + */ + public function testNewDrupalCommand(array $package, string $directory = 'drupal'): void + { + $this->newProjectDir = Path::makeAbsolute($directory, $this->projectDir); + $projectKey = array_keys($package)[0]; + $project = $package[$projectKey]; + + $process = $this->prophet->prophesize(Process::class); + $process->isSuccessful()->willReturn(true); + $process->getExitCode()->willReturn(0); + + $localMachineHelper = $this->mockLocalMachineHelper(); + + $mockFileSystem = $this->mockGetFilesystem($localMachineHelper); + $localMachineHelper->checkRequiredBinariesExist(["composer"])->shouldBeCalled(); + $this->mockExecuteComposerCreate($this->newProjectDir, $localMachineHelper, $process, $project); + $localMachineHelper->checkRequiredBinariesExist(["git"])->shouldBeCalled(); + $this->mockExecuteGitInit($localMachineHelper, $this->newProjectDir, $process); + $this->mockExecuteGitAdd($localMachineHelper, $this->newProjectDir, $process); + $this->mockExecuteGitCommit($localMachineHelper, $this->newProjectDir, $process); + + $inputs = [ + // Choose a starting project. + $project, + ]; + $this->executeCommand([ + 'directory' => $directory, + ], $inputs); + + $output = $this->getDisplay(); + $this->assertStringContainsString('Acquia recommends most customers use acquia/drupal-recommended-project to setup a Drupal project', $output); + $this->assertStringContainsString('Choose a starting project', $output); + $this->assertStringContainsString($project, $output); + $this->assertTrue($mockFileSystem->isAbsolutePath($this->newProjectDir), 'Directory path is not absolute'); + $this->assertStringContainsString('New 💧 Drupal project created in ' . $this->newProjectDir, $output); + } + + /** + * @dataProvider provideTestNewNextJsAppCommand + */ + public function testNewNextJSAppCommand(array $package, string $directory = 'nextjs'): void + { + $this->newProjectDir = Path::makeAbsolute($directory, $this->projectDir); + $projectKey = array_keys($package)[0]; + $project = $package[$projectKey]; + + $process = $this->prophet->prophesize(Process::class); + $process->isSuccessful()->willReturn(true); + $process->getExitCode()->willReturn(0); + + $localMachineHelper = $this->mockLocalMachineHelper(); + + $mockFileSystem = $this->mockGetFilesystem($localMachineHelper); + + $localMachineHelper->checkRequiredBinariesExist(["node"])->shouldBeCalled(); + $this->mockExecuteNpxCreate($this->newProjectDir, $localMachineHelper, $process); + $localMachineHelper->checkRequiredBinariesExist(["git"])->shouldBeCalled(); + $this->mockExecuteGitInit($localMachineHelper, $this->newProjectDir, $process); + $this->mockExecuteGitAdd($localMachineHelper, $this->newProjectDir, $process); + $this->mockExecuteGitCommit($localMachineHelper, $this->newProjectDir, $process); + + $inputs = [ + // Choose a starting project. + $project, + ]; + $this->executeCommand([ + 'directory' => $directory, + ], $inputs); + + $output = $this->getDisplay(); + $this->assertStringContainsString('acquia/next-acms is a starter template for building a headless site', $output); + $this->assertStringContainsString('Choose a starting project', $output); + $this->assertStringContainsString($project, $output); + $this->assertTrue($mockFileSystem->isAbsolutePath($this->newProjectDir), 'Directory path is not absolute'); + $this->assertStringContainsString('New Next JS project created in ' . $this->newProjectDir, $output); + } + + protected function mockExecuteComposerCreate( + string $projectDir, + ObjectProphecy $localMachineHelper, + ObjectProphecy $process, + string $project + ): void { + $command = [ + 'composer', + 'create-project', + $project, + $projectDir, + '--no-interaction', + ]; + $localMachineHelper + ->execute($command) + ->willReturn($process->reveal()) + ->shouldBeCalled(); + } + + protected function mockExecuteNpxCreate( + string $projectDir, + ObjectProphecy $localMachineHelper, + ObjectProphecy $process, + ): void { + $command = [ + 'npx', + 'create-next-app', + '-e', + 'https://github.com/acquia/next-acms/tree/main/starters/basic-starter', + $projectDir, + ]; + $localMachineHelper + ->execute($command) + ->willReturn($process->reveal()) + ->shouldBeCalled(); + } + + protected function mockExecuteGitInit( + ObjectProphecy $localMachineHelper, + string $projectDir, + ObjectProphecy $process + ): void { + $command = [ + 'git', + 'init', + '--initial-branch=main', + ]; + $localMachineHelper + ->execute($command, null, $projectDir) + ->willReturn($process->reveal()) + ->shouldBeCalled(); + } + + protected function mockExecuteGitAdd( + ObjectProphecy $localMachineHelper, + string $projectDir, + ObjectProphecy $process + ): void { + $command = [ + 'git', + 'add', + '-A', + ]; + $localMachineHelper + ->execute($command, null, $projectDir) + ->willReturn($process->reveal()) + ->shouldBeCalled(); + } + + protected function mockExecuteGitCommit( + ObjectProphecy $localMachineHelper, + string $projectDir, + ObjectProphecy $process + ): void { + $command = [ + 'git', + 'commit', + '--message', + 'Initial commit.', + '--quiet', + ]; + $localMachineHelper + ->execute($command, null, $projectDir) + ->willReturn($process->reveal()) + ->shouldBeCalled(); + } } diff --git a/tests/phpunit/src/Commands/App/NewFromDrupal7CommandTest.php b/tests/phpunit/src/Commands/App/NewFromDrupal7CommandTest.php index 826dd7ef5..77ce1750e 100644 --- a/tests/phpunit/src/Commands/App/NewFromDrupal7CommandTest.php +++ b/tests/phpunit/src/Commands/App/NewFromDrupal7CommandTest.php @@ -1,6 +1,6 @@ setupFsFixture(); - } - - protected function createCommand(): CommandBase { - return $this->injectCommand(NewFromDrupal7Command::class); - } - - /** - * @return array - */ - public function provideTestNewFromDrupal7Command(): array { - $repo_root = dirname(dirname(dirname(dirname(dirname(dirname(__FILE__)))))); - // Windows accepts paths with either slash (/) or backslash (\), but will - // not accept a path which contains both a slash and a backslash. Since this - // may run on any platform, sanitize everything to use slash which is - // supported on all platforms. - // @see \Drupal\Core\File\FileSystem::getTempDirectory() - // @see https://www.php.net/manual/en/function.dirname.php#123472 - $repo_root = str_replace('\\', '/', $repo_root); - $case_directories = glob($repo_root . '/tests/fixtures/drupal7/*', GLOB_ONLYDIR); - $cases = []; - foreach ($case_directories as $case_directory) { - $cases[basename($case_directory)] = [ - "$case_directory/extensions.json", - "$repo_root/config/from_d7_recommendations.json", - "$case_directory/expected.json", - ]; +class NewFromDrupal7CommandTest extends CommandTestBase +{ + protected string $newProjectDir; + + public function setUp(): void + { + parent::setUp(); + $this->setupFsFixture(); } - return $cases; - } - - protected function assertValidDateFormat(string $date, string $format): void { - $d = \DateTime::createFromFormat($format, $date); - $this->assertTrue($d && $d->format($format) == $date, sprintf("Failed asserting that '%s' matches the format '%s'", $date, DATE_ATOM)); - } - - /** - * Test the app:new:from:drupal7 command. - * - * Since this command inspects an actual Drupal site to determine its enabled - * modules, the inspector must be mocked. A set of Drupal 7 extensions is - * given by the extensions file. This project provides a shell script to help - * generate that file from an existing Drupal 7 site. An example shell command - * is given below. - * - * @code - * drush pm:list --pipe --format=json | /path/to/this/project/tests/fixtures/drupal7/drush_to_extensions_test_file_format.sh > extensions.json - * @endcode - * @param string $extensions_file - * An extensions file. See above. - * @param string $recommendations_json - * A recommendations file. The file should have the same format as a file - * that would be provided to the --recommendations CLI option. - * @param string $expected_output_file - * The expected output. - * @dataProvider provideTestNewFromDrupal7Command - */ - public function testNewFromDrupal7Command(string $extensions_json, string $recommendations_json, string $expected_json): void { - foreach (func_get_args() as $file) { - $this->assertTrue(file_exists($file), sprintf("The %s test file is missing.", basename($file))); + + protected function createCommand(): CommandBase + { + return $this->injectCommand(NewFromDrupal7Command::class); + } + + /** + * @return array + */ + public function provideTestNewFromDrupal7Command(): array + { + $repo_root = dirname(dirname(dirname(dirname(dirname(dirname(__FILE__)))))); + // Windows accepts paths with either slash (/) or backslash (\), but will + // not accept a path which contains both a slash and a backslash. Since this + // may run on any platform, sanitize everything to use slash which is + // supported on all platforms. + // @see \Drupal\Core\File\FileSystem::getTempDirectory() + // @see https://www.php.net/manual/en/function.dirname.php#123472 + $repo_root = str_replace('\\', '/', $repo_root); + $case_directories = glob($repo_root . '/tests/fixtures/drupal7/*', GLOB_ONLYDIR); + $cases = []; + foreach ($case_directories as $case_directory) { + $cases[basename($case_directory)] = [ + "$case_directory/extensions.json", + "$repo_root/config/from_d7_recommendations.json", + "$case_directory/expected.json", + ]; + } + return $cases; } - $race_condition_proof_tmpdir = sys_get_temp_dir() . '/' . getmypid(); - // The same PHP process may run multiple tests: create the directory - // only once. - if (!is_dir($race_condition_proof_tmpdir)) { - mkdir($race_condition_proof_tmpdir); + protected function assertValidDateFormat(string $date, string $format): void + { + $d = \DateTime::createFromFormat($format, $date); + $this->assertTrue($d && $d->format($format) == $date, sprintf("Failed asserting that '%s' matches the format '%s'", $date, DATE_ATOM)); } - $process = $this->prophet->prophesize(Process::class); - $process->isSuccessful()->willReturn(TRUE); - $process->getExitCode()->willReturn(0); - - $localMachineHelper = $this->mockLocalMachineHelper(); - - $mockFileSystem = $this->mockGetFilesystem($localMachineHelper); - $localMachineHelper->checkRequiredBinariesExist(["composer"])->shouldBeCalled(); - $this->mockExecuteComposerCreate($race_condition_proof_tmpdir, $localMachineHelper, $process); - $localMachineHelper->checkRequiredBinariesExist(["git"])->shouldBeCalled(); - $this->mockExecuteGitInit($localMachineHelper, $race_condition_proof_tmpdir, $process); - $this->mockExecuteGitAdd($localMachineHelper, $race_condition_proof_tmpdir, $process); - $this->mockExecuteGitCommit($localMachineHelper, $race_condition_proof_tmpdir, $process); - - $this->executeCommand([ - '--directory' => $race_condition_proof_tmpdir, - '--recommendations' => $recommendations_json, - '--stored-analysis' => $extensions_json, - ]); - - $output = $this->getDisplay(); - $this->assertStringContainsString('Found Drupal 7 site', $output); - $this->assertStringContainsString('Computing recommendations', $output); - $this->assertStringContainsString('Great news: found', $output); - $this->assertStringContainsString('Installing. This may take a few minutes.', $output); - $this->assertStringContainsString('Drupal project created', $output); - $this->assertStringContainsString('New 💧 Drupal project created in ' . $race_condition_proof_tmpdir, $output); - - $expected_json = json_decode(file_get_contents($expected_json), TRUE); - $actual_json = json_decode(file_get_contents($race_condition_proof_tmpdir . '/acli-generated-project-metadata.json'), TRUE); - // Because the generated datetime will be unique for each test, simply - // assert that is in the correct format and then set it to the expected - // value before comparing the actual result with expected result. - $this->assertValidDateFormat($actual_json['generated'], DATE_ATOM); - $this->assertValidDateFormat($expected_json['generated'], DATE_ATOM); - $actual_json['generated'] = $expected_json['generated']; - $this->assertSame($expected_json, $actual_json); - } - - protected function mockExecuteComposerCreate( - string $projectDir, - ObjectProphecy $localMachineHelper, - ObjectProphecy $process - ): void { - $command = [ - 'composer', - 'install', - '--working-dir', - $projectDir, - '--no-interaction', - ]; - $localMachineHelper - ->execute($command) - ->willReturn($process->reveal()) - ->shouldBeCalled(); - } - - protected function mockExecuteGitInit( - ObjectProphecy $localMachineHelper, - string $projectDir, - ObjectProphecy $process - ): void { - $command = [ - 'git', - 'init', - '--initial-branch=main', - '--quiet', - ]; - $localMachineHelper - ->execute($command, NULL, $projectDir) - ->willReturn($process->reveal()) - ->shouldBeCalled(); - } - - protected function mockExecuteGitAdd( - ObjectProphecy $localMachineHelper, - string $projectDir, - ObjectProphecy $process - ): void { - $command = [ - 'git', - 'add', - '-A', - ]; - $localMachineHelper - ->execute($command, NULL, $projectDir) - ->willReturn($process->reveal()) - ->shouldBeCalled(); - } - - protected function mockExecuteGitCommit( - ObjectProphecy $localMachineHelper, - string $projectDir, - ObjectProphecy $process - ): void { - $command = [ - 'git', - 'commit', - '--message', - "Generated by Acquia CLI's app:new:from:drupal7.", - '--quiet', - ]; - $localMachineHelper - ->execute($command, NULL, $projectDir) - ->willReturn($process->reveal()) - ->shouldBeCalled(); - } + /** + * Test the app:new:from:drupal7 command. + * + * Since this command inspects an actual Drupal site to determine its enabled + * modules, the inspector must be mocked. A set of Drupal 7 extensions is + * given by the extensions file. This project provides a shell script to help + * generate that file from an existing Drupal 7 site. An example shell command + * is given below. + * + * @code + * drush pm:list --pipe --format=json | /path/to/this/project/tests/fixtures/drupal7/drush_to_extensions_test_file_format.sh > extensions.json + * @endcode + * @param string $extensions_file + * An extensions file. See above. + * @param string $recommendations_json + * A recommendations file. The file should have the same format as a file + * that would be provided to the --recommendations CLI option. + * @param string $expected_output_file + * The expected output. + * @dataProvider provideTestNewFromDrupal7Command + */ + public function testNewFromDrupal7Command(string $extensions_json, string $recommendations_json, string $expected_json): void + { + foreach (func_get_args() as $file) { + $this->assertTrue(file_exists($file), sprintf("The %s test file is missing.", basename($file))); + } + + $race_condition_proof_tmpdir = sys_get_temp_dir() . '/' . getmypid(); + // The same PHP process may run multiple tests: create the directory + // only once. + if (!is_dir($race_condition_proof_tmpdir)) { + mkdir($race_condition_proof_tmpdir); + } + + $process = $this->prophet->prophesize(Process::class); + $process->isSuccessful()->willReturn(true); + $process->getExitCode()->willReturn(0); + + $localMachineHelper = $this->mockLocalMachineHelper(); + + $mockFileSystem = $this->mockGetFilesystem($localMachineHelper); + $localMachineHelper->checkRequiredBinariesExist(["composer"])->shouldBeCalled(); + $this->mockExecuteComposerCreate($race_condition_proof_tmpdir, $localMachineHelper, $process); + $localMachineHelper->checkRequiredBinariesExist(["git"])->shouldBeCalled(); + $this->mockExecuteGitInit($localMachineHelper, $race_condition_proof_tmpdir, $process); + $this->mockExecuteGitAdd($localMachineHelper, $race_condition_proof_tmpdir, $process); + $this->mockExecuteGitCommit($localMachineHelper, $race_condition_proof_tmpdir, $process); + + $this->executeCommand([ + '--directory' => $race_condition_proof_tmpdir, + '--recommendations' => $recommendations_json, + '--stored-analysis' => $extensions_json, + ]); + + $output = $this->getDisplay(); + $this->assertStringContainsString('Found Drupal 7 site', $output); + $this->assertStringContainsString('Computing recommendations', $output); + $this->assertStringContainsString('Great news: found', $output); + $this->assertStringContainsString('Installing. This may take a few minutes.', $output); + $this->assertStringContainsString('Drupal project created', $output); + $this->assertStringContainsString('New 💧 Drupal project created in ' . $race_condition_proof_tmpdir, $output); + + $expected_json = json_decode(file_get_contents($expected_json), true); + $actual_json = json_decode(file_get_contents($race_condition_proof_tmpdir . '/acli-generated-project-metadata.json'), true); + // Because the generated datetime will be unique for each test, simply + // assert that is in the correct format and then set it to the expected + // value before comparing the actual result with expected result. + $this->assertValidDateFormat($actual_json['generated'], DATE_ATOM); + $this->assertValidDateFormat($expected_json['generated'], DATE_ATOM); + $actual_json['generated'] = $expected_json['generated']; + $this->assertSame($expected_json, $actual_json); + } + + protected function mockExecuteComposerCreate( + string $projectDir, + ObjectProphecy $localMachineHelper, + ObjectProphecy $process + ): void { + $command = [ + 'composer', + 'install', + '--working-dir', + $projectDir, + '--no-interaction', + ]; + $localMachineHelper + ->execute($command) + ->willReturn($process->reveal()) + ->shouldBeCalled(); + } + protected function mockExecuteGitInit( + ObjectProphecy $localMachineHelper, + string $projectDir, + ObjectProphecy $process + ): void { + $command = [ + 'git', + 'init', + '--initial-branch=main', + '--quiet', + ]; + $localMachineHelper + ->execute($command, null, $projectDir) + ->willReturn($process->reveal()) + ->shouldBeCalled(); + } + + protected function mockExecuteGitAdd( + ObjectProphecy $localMachineHelper, + string $projectDir, + ObjectProphecy $process + ): void { + $command = [ + 'git', + 'add', + '-A', + ]; + $localMachineHelper + ->execute($command, null, $projectDir) + ->willReturn($process->reveal()) + ->shouldBeCalled(); + } + + protected function mockExecuteGitCommit( + ObjectProphecy $localMachineHelper, + string $projectDir, + ObjectProphecy $process + ): void { + $command = [ + 'git', + 'commit', + '--message', + "Generated by Acquia CLI's app:new:from:drupal7.", + '--quiet', + ]; + $localMachineHelper + ->execute($command, null, $projectDir) + ->willReturn($process->reveal()) + ->shouldBeCalled(); + } } diff --git a/tests/phpunit/src/Commands/App/TaskWaitCommandTest.php b/tests/phpunit/src/Commands/App/TaskWaitCommandTest.php index 03525a3cd..0549a951f 100644 --- a/tests/phpunit/src/Commands/App/TaskWaitCommandTest.php +++ b/tests/phpunit/src/Commands/App/TaskWaitCommandTest.php @@ -1,6 +1,6 @@ injectCommand(TaskWaitCommand::class); - } - - /** - * @dataProvider providerTestTaskWaitCommand - */ - public function testTaskWaitCommand(string $notification): void { - $notificationUuid = '1bd3487e-71d1-4fca-a2d9-5f969b3d35c1'; - $this->mockRequest('getNotificationByUuid', $notificationUuid); - $this->executeCommand([ - 'notification-uuid' => $notification, - ]); - - $output = $this->getDisplay(); - 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(Command::SUCCESS, $this->getStatusCode()); - } +class TaskWaitCommandTest extends CommandTestBase +{ + protected function createCommand(): CommandBase + { + return $this->injectCommand(TaskWaitCommand::class); + } - 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, - ]); + /** + * @dataProvider providerTestTaskWaitCommand + */ + public function testTaskWaitCommand(string $notification): void + { + $notificationUuid = '1bd3487e-71d1-4fca-a2d9-5f969b3d35c1'; + $this->mockRequest('getNotificationByUuid', $notificationUuid); + $this->executeCommand([ + 'notification-uuid' => $notification, + ]); + + $output = $this->getDisplay(); + 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(Command::SUCCESS, $this->getStatusCode()); + } - self::assertStringContainsString(' [ERROR] The task with notification uuid 1bd3487e-71d1-4fca-a2d9-5f969b3d35c1 failed', $this->getDisplay()); - $this->assertEquals(Command::FAILURE, $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, + ]); + + self::assertStringContainsString(' [ERROR] The task with notification uuid 1bd3487e-71d1-4fca-a2d9-5f969b3d35c1 failed', $this->getDisplay()); + $this->assertEquals(Command::FAILURE, $this->getStatusCode()); + } - /** - * Valid notifications. - * - * @return (string|int)[][] - */ - public function providerTestTaskWaitCommand(): array { - return [ - [ + /** + * Valid notifications. + * + * @return (string|int)[][] + */ + public function providerTestTaskWaitCommand(): array + { + return [ + [ '1bd3487e-71d1-4fca-a2d9-5f969b3d35c1', - ], - [ + ], + [ 'https://cloud.acquia.com/api/notifications/1bd3487e-71d1-4fca-a2d9-5f969b3d35c1', - ], - [ + ], + [ <<<'EOT' { "message": "Caches are being cleared.", @@ -83,45 +88,49 @@ public function providerTestTaskWaitCommand(): array { } } EOT, - ], - [ + ], + [ '"1bd3487e-71d1-4fca-a2d9-5f969b3d35c1"', - ], - ]; - } + ], + ]; + } - 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' => '{}']); + 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. - } + // Assert. + } - public function testTaskWaitCommandWithInvalidUrl(): void { - $this->expectException(AcquiaCliException::class); - $this->expectExceptionMessage('Notification format is not one of UUID, JSON response, or URL'); - $this->executeCommand(['notification-uuid' => 'https://cloud.acquia.com/api/notifications/foo']); + public function testTaskWaitCommandWithInvalidUrl(): void + { + $this->expectException(AcquiaCliException::class); + $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. - } + // Assert. + } - /** - * @dataProvider providerTestTaskWaitCommandWithInvalidJson - */ - public function testTaskWaitCommandWithInvalidJson(string $notification): void { - $this->expectException(AcquiaCliException::class); - $this->executeCommand([ - 'notification-uuid' => $notification, - ]); - } + /** + * @dataProvider providerTestTaskWaitCommandWithInvalidJson + */ + public function testTaskWaitCommandWithInvalidJson(string $notification): void + { + $this->expectException(AcquiaCliException::class); + $this->executeCommand([ + 'notification-uuid' => $notification, + ]); + } - /** - * @return string[] - */ - public function providerTestTaskWaitCommandWithInvalidJson(): array { - return [ - [ + /** + * @return string[] + */ + public function providerTestTaskWaitCommandWithInvalidJson(): array + { + return [ + [ <<<'EOT' { "message": "Caches are being cleared.", @@ -138,8 +147,8 @@ public function providerTestTaskWaitCommandWithInvalidJson(): array { } } EOT, - ], - [ + ], + [ <<<'EOT' { "message": "Caches are being cleared.", @@ -150,11 +159,10 @@ public function providerTestTaskWaitCommandWithInvalidJson(): array { } } EOT, - ], - [ + ], + [ '"11bd3487e-71d1-4fca-a2d9-5f969b3d35c1"', - ], - ]; - } - + ], + ]; + } } diff --git a/tests/phpunit/src/Commands/App/UnlinkCommandTest.php b/tests/phpunit/src/Commands/App/UnlinkCommandTest.php index a36100fa6..067e331f0 100644 --- a/tests/phpunit/src/Commands/App/UnlinkCommandTest.php +++ b/tests/phpunit/src/Commands/App/UnlinkCommandTest.php @@ -1,6 +1,6 @@ injectCommand(UnlinkCommand::class); - } - - public function testUnlinkCommand(): void { - $applicationsResponse = $this->getMockResponseFromSpec('/applications', - 'get', '200'); - $cloudApplication = $applicationsResponse->{'_embedded'}->items[0]; - $cloudApplicationUuid = $cloudApplication->uuid; - $this->createMockAcliConfigFile($cloudApplicationUuid); - $this->mockApplicationRequest(); - - // Assert we set it correctly. - $this->assertEquals($applicationsResponse->{'_embedded'}->items[0]->uuid, $this->datastoreAcli->get('cloud_app_uuid')); - - $this->executeCommand(); - $output = $this->getDisplay(); - - // Assert it's been unset. - $this->assertNull($this->datastoreAcli->get('cloud_app_uuid')); - $this->assertStringContainsString("Unlinked $this->projectDir from Cloud application " . $cloudApplication->name, $output); - } - - public function testUnlinkCommandInvalidDir(): void { - $this->expectException(AcquiaCliException::class); - $this->expectExceptionMessage('There is no Cloud Platform application linked to ' . $this->projectDir); - $this->executeCommand(); - } - +class UnlinkCommandTest extends CommandTestBase +{ + protected function createCommand(): CommandBase + { + return $this->injectCommand(UnlinkCommand::class); + } + + public function testUnlinkCommand(): void + { + $applicationsResponse = $this->getMockResponseFromSpec( + '/applications', + 'get', + '200' + ); + $cloudApplication = $applicationsResponse->{'_embedded'}->items[0]; + $cloudApplicationUuid = $cloudApplication->uuid; + $this->createMockAcliConfigFile($cloudApplicationUuid); + $this->mockApplicationRequest(); + + // Assert we set it correctly. + $this->assertEquals($applicationsResponse->{'_embedded'}->items[0]->uuid, $this->datastoreAcli->get('cloud_app_uuid')); + + $this->executeCommand(); + $output = $this->getDisplay(); + + // Assert it's been unset. + $this->assertNull($this->datastoreAcli->get('cloud_app_uuid')); + $this->assertStringContainsString("Unlinked $this->projectDir from Cloud application " . $cloudApplication->name, $output); + } + + public function testUnlinkCommandInvalidDir(): void + { + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage('There is no Cloud Platform application linked to ' . $this->projectDir); + $this->executeCommand(); + } } diff --git a/tests/phpunit/src/Commands/Archive/ArchiveExporterCommandTest.php b/tests/phpunit/src/Commands/Archive/ArchiveExporterCommandTest.php index e88c7b7c3..2cd9b3791 100644 --- a/tests/phpunit/src/Commands/Archive/ArchiveExporterCommandTest.php +++ b/tests/phpunit/src/Commands/Archive/ArchiveExporterCommandTest.php @@ -1,6 +1,6 @@ injectCommand(ArchiveExportCommand::class); + } - protected function createCommand(): CommandBase { - return $this->injectCommand(ArchiveExportCommand::class); - } + public function setUp(): void + { + self::unsetEnvVars(['ACLI_DB_HOST', 'ACLI_DB_USER', 'ACLI_DB_PASSWORD', 'ACLI_DB_NAME']); + parent::setUp(); + } - public function setUp(): void { - self::unsetEnvVars(['ACLI_DB_HOST', 'ACLI_DB_USER', 'ACLI_DB_PASSWORD', 'ACLI_DB_NAME']); - parent::setUp(); - } + public function testArchiveExport(): void + { + touch(Path::join($this->projectDir, '.gitignore')); + $destinationDir = 'foo'; + $localMachineHelper = $this->mockLocalMachineHelper(); + $fileSystem = $this->mockFileSystem($destinationDir); + $localMachineHelper->getFilesystem()->willReturn($fileSystem->reveal())->shouldBeCalled(); + $this->mockExecutePvExists($localMachineHelper); + $this->mockExecuteDrushExists($localMachineHelper); + $this->mockExecuteDrushStatus($localMachineHelper, $this->projectDir); + $this->mockCreateMySqlDumpOnLocal($localMachineHelper); + $localMachineHelper->checkRequiredBinariesExist(["tar"])->shouldBeCalled(); + $localMachineHelper->execute(Argument::type('array'), Argument::type('callable'), null, true)->willReturn($this->mockProcess())->shouldBeCalled(); - public function testArchiveExport(): void { - touch(Path::join($this->projectDir, '.gitignore')); - $destinationDir = 'foo'; - $localMachineHelper = $this->mockLocalMachineHelper(); - $fileSystem = $this->mockFileSystem($destinationDir); - $localMachineHelper->getFilesystem()->willReturn($fileSystem->reveal())->shouldBeCalled(); - $this->mockExecutePvExists($localMachineHelper); - $this->mockExecuteDrushExists($localMachineHelper); - $this->mockExecuteDrushStatus($localMachineHelper, $this->projectDir); - $this->mockCreateMySqlDumpOnLocal($localMachineHelper); - $localMachineHelper->checkRequiredBinariesExist(["tar"])->shouldBeCalled(); - $localMachineHelper->execute(Argument::type('array'), Argument::type('callable'), NULL, TRUE)->willReturn($this->mockProcess())->shouldBeCalled(); + $finder = $this->mockFinder(); + $localMachineHelper->getFinder()->willReturn($finder->reveal()); - $finder = $this->mockFinder(); - $localMachineHelper->getFinder()->willReturn($finder->reveal()); + $inputs = [ + // ... Do you want to continue? (yes/no) [yes] + 'y', + ]; + $this->executeCommand([ + 'destination-dir' => $destinationDir, + ], $inputs); - $inputs = [ - // ... Do you want to continue? (yes/no) [yes] - 'y', - ]; - $this->executeCommand([ - 'destination-dir' => $destinationDir, - ], $inputs); + $output = $this->getDisplay(); - $output = $this->getDisplay(); - - self::assertStringContainsString('An archive of your Drupal application was created at', $output); - self::assertStringContainsString('foo/acli-archive-project-', $output); - } - - protected function mockFileSystem(string $destinationDir): ObjectProphecy { - $fileSystem = $this->prophet->prophesize(Filesystem::class); - $fileSystem->mirror($this->projectDir, Argument::type('string'), - Argument::type(Finder::class), ['override' => TRUE, 'delete' => TRUE], - Argument::type(Finder::class))->shouldBeCalled(); - $fileSystem->exists($destinationDir)->willReturn(TRUE)->shouldBeCalled(); - $fileSystem->rename(Argument::type('string'), Argument::type('string')) - ->shouldBeCalled(); - $fileSystem->remove(Argument::type('string'))->shouldBeCalled(); - $fileSystem->mkdir(Argument::type('array'))->shouldBeCalled(); - return $fileSystem; - } + self::assertStringContainsString('An archive of your Drupal application was created at', $output); + self::assertStringContainsString('foo/acli-archive-project-', $output); + } + protected function mockFileSystem(string $destinationDir): ObjectProphecy + { + $fileSystem = $this->prophet->prophesize(Filesystem::class); + $fileSystem->mirror( + $this->projectDir, + Argument::type('string'), + Argument::type(Finder::class), + ['override' => true, 'delete' => true], + Argument::type(Finder::class) + )->shouldBeCalled(); + $fileSystem->exists($destinationDir)->willReturn(true)->shouldBeCalled(); + $fileSystem->rename(Argument::type('string'), Argument::type('string')) + ->shouldBeCalled(); + $fileSystem->remove(Argument::type('string'))->shouldBeCalled(); + $fileSystem->mkdir(Argument::type('array'))->shouldBeCalled(); + return $fileSystem; + } } diff --git a/tests/phpunit/src/Commands/Auth/AuthLoginCommandTest.php b/tests/phpunit/src/Commands/Auth/AuthLoginCommandTest.php index 9c0c725b9..32ae7560d 100644 --- a/tests/phpunit/src/Commands/Auth/AuthLoginCommandTest.php +++ b/tests/phpunit/src/Commands/Auth/AuthLoginCommandTest.php @@ -1,6 +1,6 @@ injectCommand(AuthLoginCommand::class); - } - - public function testAuthLoginCommand(): void { - $this->mockRequest('getAccount'); - $this->clientServiceProphecy->setConnector(Argument::type(Connector::class))->shouldBeCalled(); - $this->clientServiceProphecy->isMachineAuthenticated()->willReturn(FALSE); - $this->removeMockCloudConfigFile(); - $this->createDataStores(); - $this->command = $this->createCommand(); - - $this->executeCommand(['--key' => $this->key, '--secret' => $this->secret]); - $output = $this->getDisplay(); - - $this->assertStringContainsString('Saved credentials', $output); - $this->assertKeySavedCorrectly(); - } - - public function testAuthLoginNoKeysCommand(): void { - $this->mockRequest('getAccount'); - $this->clientServiceProphecy->setConnector(Argument::type(Connector::class))->shouldBeCalled(); - $this->clientServiceProphecy->isMachineAuthenticated()->willReturn(FALSE); - $this->removeMockCloudConfigFile(); - $this->fs->dumpFile($this->cloudConfigFilepath, json_encode(['send_telemetry' => FALSE])); - $this->createDataStores(); - $this->command = $this->createCommand(); - - $this->executeCommand(['--key' => $this->key, '--secret' => $this->secret]); - $output = $this->getDisplay(); - - $this->assertStringContainsString('Saved credentials', $output); - $this->assertKeySavedCorrectly(); - } - - public function providerTestAuthLoginInvalidInputCommand(): Generator { - yield - [ +class AuthLoginCommandTest extends CommandTestBase +{ + protected function createCommand(): CommandBase + { + return $this->injectCommand(AuthLoginCommand::class); + } + + public function testAuthLoginCommand(): void + { + $this->mockRequest('getAccount'); + $this->clientServiceProphecy->setConnector(Argument::type(Connector::class))->shouldBeCalled(); + $this->clientServiceProphecy->isMachineAuthenticated()->willReturn(false); + $this->removeMockCloudConfigFile(); + $this->createDataStores(); + $this->command = $this->createCommand(); + + $this->executeCommand(['--key' => $this->key, '--secret' => $this->secret]); + $output = $this->getDisplay(); + + $this->assertStringContainsString('Saved credentials', $output); + $this->assertKeySavedCorrectly(); + } + + public function testAuthLoginNoKeysCommand(): void + { + $this->mockRequest('getAccount'); + $this->clientServiceProphecy->setConnector(Argument::type(Connector::class))->shouldBeCalled(); + $this->clientServiceProphecy->isMachineAuthenticated()->willReturn(false); + $this->removeMockCloudConfigFile(); + $this->fs->dumpFile($this->cloudConfigFilepath, json_encode(['send_telemetry' => false])); + $this->createDataStores(); + $this->command = $this->createCommand(); + + $this->executeCommand(['--key' => $this->key, '--secret' => $this->secret]); + $output = $this->getDisplay(); + + $this->assertStringContainsString('Saved credentials', $output); + $this->assertKeySavedCorrectly(); + } + + public function providerTestAuthLoginInvalidInputCommand(): Generator + { + yield + [ [], ['--key' => 'no spaces are allowed' , '--secret' => $this->secret], - ]; - yield - [ + ]; + yield + [ [], ['--key' => 'shorty' , '--secret' => $this->secret], - ]; - yield - [ + ]; + yield + [ [], ['--key' => ' ', '--secret' => $this->secret], - ]; - } - - /** - * @dataProvider providerTestAuthLoginInvalidInputCommand - */ - public function testAuthLoginInvalidInputCommand(array $inputs, array $args): void { - $this->clientServiceProphecy->isMachineAuthenticated()->willReturn(FALSE); - $this->removeMockCloudConfigFile(); - $this->createDataStores(); - $this->command = $this->createCommand(); - $this->expectException(ValidatorException::class); - $this->executeCommand($args, $inputs); - } - - public function testAuthLoginInvalidDatastore(): void { - $this->clientServiceProphecy->isMachineAuthenticated()->willReturn(FALSE); - $this->removeMockCloudConfigFile(); - $data = [ - 'acli_key' => 'key2', - 'keys' => [ + ]; + } + + /** + * @dataProvider providerTestAuthLoginInvalidInputCommand + */ + public function testAuthLoginInvalidInputCommand(array $inputs, array $args): void + { + $this->clientServiceProphecy->isMachineAuthenticated()->willReturn(false); + $this->removeMockCloudConfigFile(); + $this->createDataStores(); + $this->command = $this->createCommand(); + $this->expectException(ValidatorException::class); + $this->executeCommand($args, $inputs); + } + + public function testAuthLoginInvalidDatastore(): void + { + $this->clientServiceProphecy->isMachineAuthenticated()->willReturn(false); + $this->removeMockCloudConfigFile(); + $data = [ + 'acli_key' => 'key2', + 'keys' => [ 'key1' => [ - 'label' => 'foo', - 'secret' => 'foo', - 'uuid' => 'foo', + 'label' => 'foo', + 'secret' => 'foo', + 'uuid' => 'foo', ], - ], - ]; - $this->fs->dumpFile($this->cloudConfigFilepath, json_encode($data)); - $this->expectException(AcquiaCliException::class); - $this->expectExceptionMessage("Configuration file at the following path contains invalid keys: $this->cloudConfigFilepath Invalid configuration for path \"cloud_api\": acli_key must exist in keys"); - $this->createDataStores(); - } - - protected function assertInteractivePrompts(string $output): void { - // Your machine has already been authenticated with the Cloud Platform API, would you like to re-authenticate? - $this->assertStringContainsString('You will need a Cloud Platform API token from https://cloud.acquia.com/a/profile/tokens', $output); - $this->assertStringContainsString('Do you want to open this page to generate a token now?', $output); - $this->assertStringContainsString('Enter your Cloud API key (option -k, --key):', $output); - $this->assertStringContainsString('Enter your Cloud API secret (option -s, --secret) (input will be hidden):', $output); - } - - protected function assertKeySavedCorrectly(): void { - $credsFile = $this->cloudConfigFilepath; - $this->assertFileExists($credsFile); - $config = new CloudDataStore($this->localMachineHelper, new CloudDataConfig(), $credsFile); - $this->assertTrue($config->exists('acli_key')); - $this->assertEquals($this->key, $config->get('acli_key')); - $this->assertTrue($config->exists('keys')); - $keys = $config->get('keys'); - $this->assertArrayHasKey($this->key, $keys); - $this->assertArrayHasKey('label', $keys[$this->key]); - $this->assertArrayHasKey('secret', $keys[$this->key]); - $this->assertEquals($this->secret, $keys[$this->key]['secret']); - } - + ], + ]; + $this->fs->dumpFile($this->cloudConfigFilepath, json_encode($data)); + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage("Configuration file at the following path contains invalid keys: $this->cloudConfigFilepath Invalid configuration for path \"cloud_api\": acli_key must exist in keys"); + $this->createDataStores(); + } + + protected function assertInteractivePrompts(string $output): void + { + // Your machine has already been authenticated with the Cloud Platform API, would you like to re-authenticate? + $this->assertStringContainsString('You will need a Cloud Platform API token from https://cloud.acquia.com/a/profile/tokens', $output); + $this->assertStringContainsString('Do you want to open this page to generate a token now?', $output); + $this->assertStringContainsString('Enter your Cloud API key (option -k, --key):', $output); + $this->assertStringContainsString('Enter your Cloud API secret (option -s, --secret) (input will be hidden):', $output); + } + + protected function assertKeySavedCorrectly(): void + { + $credsFile = $this->cloudConfigFilepath; + $this->assertFileExists($credsFile); + $config = new CloudDataStore($this->localMachineHelper, new CloudDataConfig(), $credsFile); + $this->assertTrue($config->exists('acli_key')); + $this->assertEquals($this->key, $config->get('acli_key')); + $this->assertTrue($config->exists('keys')); + $keys = $config->get('keys'); + $this->assertArrayHasKey($this->key, $keys); + $this->assertArrayHasKey('label', $keys[$this->key]); + $this->assertArrayHasKey('secret', $keys[$this->key]); + $this->assertEquals($this->secret, $keys[$this->key]['secret']); + } } diff --git a/tests/phpunit/src/Commands/Auth/AuthLogoutCommandTest.php b/tests/phpunit/src/Commands/Auth/AuthLogoutCommandTest.php index 5abdf9ac7..c63b13e59 100644 --- a/tests/phpunit/src/Commands/Auth/AuthLogoutCommandTest.php +++ b/tests/phpunit/src/Commands/Auth/AuthLogoutCommandTest.php @@ -1,6 +1,6 @@ injectCommand(AuthLogoutCommand::class); - } - - public function testAuthLogoutCommand(): void { - $this->executeCommand(); - $output = $this->getDisplay(); - $this->assertFileExists($this->cloudConfigFilepath); - $this->assertStringContainsString('The key Test Key will be deactivated on this machine.', $output); - $this->assertStringContainsString('Do you want to delete the active Cloud Platform API credentials (option --delete)? (yes/no) [no]:', $output); - $this->assertStringContainsString('The active Cloud Platform API credentials were deactivated', $output); - } - - public function testAuthLogoutInvalidDatastore(): void { - $this->clientServiceProphecy->isMachineAuthenticated()->willReturn(FALSE); - $this->removeMockCloudConfigFile(); - $data = [ - 'acli_key' => 'key2', - 'keys' => [ +class AuthLogoutCommandTest extends CommandTestBase +{ + protected function createCommand(): CommandBase + { + return $this->injectCommand(AuthLogoutCommand::class); + } + + public function testAuthLogoutCommand(): void + { + $this->executeCommand(); + $output = $this->getDisplay(); + $this->assertFileExists($this->cloudConfigFilepath); + $this->assertStringContainsString('The key Test Key will be deactivated on this machine.', $output); + $this->assertStringContainsString('Do you want to delete the active Cloud Platform API credentials (option --delete)? (yes/no) [no]:', $output); + $this->assertStringContainsString('The active Cloud Platform API credentials were deactivated', $output); + } + + public function testAuthLogoutInvalidDatastore(): void + { + $this->clientServiceProphecy->isMachineAuthenticated()->willReturn(false); + $this->removeMockCloudConfigFile(); + $data = [ + 'acli_key' => 'key2', + 'keys' => [ 'key1' => [ - 'label' => 'foo', - 'secret' => 'foo', - 'uuid' => 'foo', + 'label' => 'foo', + 'secret' => 'foo', + 'uuid' => 'foo', ], - ], - ]; - $this->fs->dumpFile($this->cloudConfigFilepath, json_encode($data)); - $this->expectException(AcquiaCliException::class); - $this->expectExceptionMessage("Configuration file at the following path contains invalid keys: $this->cloudConfigFilepath Invalid configuration for path \"cloud_api\": acli_key must exist in keys"); - $this->createDataStores(); - } - + ], + ]; + $this->fs->dumpFile($this->cloudConfigFilepath, json_encode($data)); + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage("Configuration file at the following path contains invalid keys: $this->cloudConfigFilepath Invalid configuration for path \"cloud_api\": acli_key must exist in keys"); + $this->createDataStores(); + } } diff --git a/tests/phpunit/src/Commands/ChecklistTest.php b/tests/phpunit/src/Commands/ChecklistTest.php index c1c203449..aa2e1fdba 100644 --- a/tests/phpunit/src/Commands/ChecklistTest.php +++ b/tests/phpunit/src/Commands/ChecklistTest.php @@ -1,6 +1,6 @@ section() - // method which is only available for ConsoleOutput. Could make a custom testing - // output class with the method. - parent::setUp(); - $this->output = new ConsoleOutput(); - } - - public function testSpinner(): void { - putenv('PHPUNIT_RUNNING=1'); - $checklist = new Checklist($this->output); - $checklist->addItem('Testing!'); - - // Make the spinner spin with some output. - $outputCallback = static function (string $type, string $buffer) use ($checklist): void { - $checklist->updateProgressBar($buffer); - }; - $this->localMachineHelper->execute(['echo', 'hello world'], $outputCallback, NULL, FALSE); - - // Complete the item. - $checklist->completePreviousItem(); - $items = $checklist->getItems(); - /** @var \Symfony\Component\Console\Helper\ProgressBar $progressBar */ - $progressBar = $items[0]['spinner']->getProgressBar(); - $this->assertEquals('Testing!', $progressBar->getMessage()); - $this->assertEquals('', $progressBar->getBarCharacter()); - $this->assertEquals('⢸', $progressBar->getProgressCharacter()); - $this->assertEquals('⌛', $progressBar->getEmptyBarCharacter()); - $this->assertEquals(1, $progressBar->getBarWidth()); - $this->assertEquals('', $progressBar->getMessage('detail')); - - putenv('PHPUNIT_RUNNING'); - } - +class ChecklistTest extends TestBase +{ + protected OutputInterface $output; + + public function setUp(): void + { + // Unfortunately this prints to screen. Not sure how else to + // get the spinner and checklist to work. They require the $output->section() + // method which is only available for ConsoleOutput. Could make a custom testing + // output class with the method. + parent::setUp(); + $this->output = new ConsoleOutput(); + } + + public function testSpinner(): void + { + putenv('PHPUNIT_RUNNING=1'); + $checklist = new Checklist($this->output); + $checklist->addItem('Testing!'); + + // Make the spinner spin with some output. + $outputCallback = static function (string $type, string $buffer) use ($checklist): void { + $checklist->updateProgressBar($buffer); + }; + $this->localMachineHelper->execute(['echo', 'hello world'], $outputCallback, null, false); + + // Complete the item. + $checklist->completePreviousItem(); + $items = $checklist->getItems(); + /** @var \Symfony\Component\Console\Helper\ProgressBar $progressBar */ + $progressBar = $items[0]['spinner']->getProgressBar(); + $this->assertEquals('Testing!', $progressBar->getMessage()); + $this->assertEquals('', $progressBar->getBarCharacter()); + $this->assertEquals('⢸', $progressBar->getProgressCharacter()); + $this->assertEquals('⌛', $progressBar->getEmptyBarCharacter()); + $this->assertEquals(1, $progressBar->getBarWidth()); + $this->assertEquals('', $progressBar->getMessage('detail')); + + putenv('PHPUNIT_RUNNING'); + } } diff --git a/tests/phpunit/src/Commands/ClearCacheCommandTest.php b/tests/phpunit/src/Commands/ClearCacheCommandTest.php index 543be2ad0..535d648da 100644 --- a/tests/phpunit/src/Commands/ClearCacheCommandTest.php +++ b/tests/phpunit/src/Commands/ClearCacheCommandTest.php @@ -1,6 +1,6 @@ injectCommand(ClearCacheCommand::class); + } - protected function createCommand(): CommandBase { - return $this->injectCommand(ClearCacheCommand::class); - } + /** + * @group serial + */ + public function testAliasesAreCached(): void + { + ClearCacheCommand::clearCaches(); + $this->command = $this->injectCommand(IdeListCommand::class); - /** - * @group serial - */ - public function testAliasesAreCached(): void { - ClearCacheCommand::clearCaches(); - $this->command = $this->injectCommand(IdeListCommand::class); + // Request for applications. + $applicationsResponse = $this->getMockResponseFromSpec( + '/applications', + 'get', + '200' + ); + $applicationsResponse = $this->filterApplicationsResponse($applicationsResponse, 1, true); + $this->clientProphecy->request('get', '/applications') + ->willReturn($applicationsResponse->{'_embedded'}->items) + // Ensure this is only called once, even though we execute the command twice. + ->shouldBeCalledTimes(1); - // Request for applications. - $applicationsResponse = $this->getMockResponseFromSpec('/applications', - 'get', '200'); - $applicationsResponse = $this->filterApplicationsResponse($applicationsResponse, 1, TRUE); - $this->clientProphecy->request('get', '/applications') - ->willReturn($applicationsResponse->{'_embedded'}->items) - // Ensure this is only called once, even though we execute the command twice. - ->shouldBeCalledTimes(1); + $this->clientProphecy->addQuery('filter', 'hosting=@*:devcloud2')->shouldBeCalled(); + $this->mockApplicationRequest(); + $this->mockRequest('getApplicationIdes', 'a47ac10b-58cc-4372-a567-0e02b2c3d470'); + $this->mockRequest('getAccount'); - $this->clientProphecy->addQuery('filter', 'hosting=@*:devcloud2')->shouldBeCalled(); - $this->mockApplicationRequest(); - $this->mockRequest('getApplicationIdes', 'a47ac10b-58cc-4372-a567-0e02b2c3d470'); - $this->mockRequest('getAccount'); + $alias = 'devcloud2'; + $args = ['applicationUuid' => $alias]; + $inputs = [ + // Would you like to link the Cloud application Sample application to this repository? + 'n', + ]; - $alias = 'devcloud2'; - $args = ['applicationUuid' => $alias]; - $inputs = [ - // Would you like to link the Cloud application Sample application to this repository? - 'n', - ]; + $this->executeCommand($args, $inputs); + // Run it twice, make sure API calls are made only once. + $this->executeCommand($args, $inputs); - $this->executeCommand($args, $inputs); - // Run it twice, make sure API calls are made only once. - $this->executeCommand($args, $inputs); + // Assert. + $output = $this->getDisplay(); + $this->assertEquals(0, $this->getStatusCode()); + } - // Assert. - $output = $this->getDisplay(); - $this->assertEquals(0, $this->getStatusCode()); - } - - public function testClearCaches(): void { - $this->executeCommand(); - $output = $this->getDisplay(); - $this->assertStringContainsString('Acquia CLI caches were cleared', $output); - - $cache = CommandBase::getAliasCache(); - $this->assertCount(0, iterator_to_array($cache->getItems(), FALSE)); - } + public function testClearCaches(): void + { + $this->executeCommand(); + $output = $this->getDisplay(); + $this->assertStringContainsString('Acquia CLI caches were cleared', $output); + $cache = CommandBase::getAliasCache(); + $this->assertCount(0, iterator_to_array($cache->getItems(), false)); + } } diff --git a/tests/phpunit/src/Commands/CodeStudio/CodeStudioCiCdVariablesTest.php b/tests/phpunit/src/Commands/CodeStudio/CodeStudioCiCdVariablesTest.php index b18f7d2fb..917bdc4f2 100644 --- a/tests/phpunit/src/Commands/CodeStudio/CodeStudioCiCdVariablesTest.php +++ b/tests/phpunit/src/Commands/CodeStudio/CodeStudioCiCdVariablesTest.php @@ -1,35 +1,35 @@ getDefaultsForNode(); - $this->testBooleanValues($variables); - $variables = $codeStudioCiCdVariablesObj->getDefaultsForPhp(); - $this->testBooleanValues($variables); - } - - protected function testBooleanValues(array $variables): void { - foreach ($variables as $variable ) { - if ($variable['key'] !== "PHP_VERSION" && $variable['key'] !== "NODE_VERSION") { - $maskedValue = $variable['masked']; - $this->assertEquals(TRUE, $maskedValue); - } - else { - $maskedValue = $variable['masked']; - $this->assertEquals(FALSE, $maskedValue); - } - $protectedValue = $variable['protected']; - $this->assertEquals(FALSE, $protectedValue); +class CodeStudioCiCdVariablesTest extends TestBase +{ + public function testGetDefaultsForNode(): void + { + $codeStudioCiCdVariablesObj = new CodeStudioCiCdVariables(); + $variables = $codeStudioCiCdVariablesObj->getDefaultsForNode(); + $this->testBooleanValues($variables); + $variables = $codeStudioCiCdVariablesObj->getDefaultsForPhp(); + $this->testBooleanValues($variables); } - } + protected function testBooleanValues(array $variables): void + { + foreach ($variables as $variable) { + if ($variable['key'] !== "PHP_VERSION" && $variable['key'] !== "NODE_VERSION") { + $maskedValue = $variable['masked']; + $this->assertEquals(true, $maskedValue); + } else { + $maskedValue = $variable['masked']; + $this->assertEquals(false, $maskedValue); + } + $protectedValue = $variable['protected']; + $this->assertEquals(false, $protectedValue); + } + } } diff --git a/tests/phpunit/src/Commands/CodeStudio/CodeStudioPhpVersionCommandTest.php b/tests/phpunit/src/Commands/CodeStudio/CodeStudioPhpVersionCommandTest.php index a345d6022..8634a089f 100644 --- a/tests/phpunit/src/Commands/CodeStudio/CodeStudioPhpVersionCommandTest.php +++ b/tests/phpunit/src/Commands/CodeStudio/CodeStudioPhpVersionCommandTest.php @@ -1,6 +1,6 @@ injectCommand(CodeStudioPhpVersionCommand::class); - } - - /** - * @return array - */ - public function providerTestPhpVersionFailure(): array { - return [ - ['', ValidatorException::class], - ['8', ValidatorException::class], - ['8 1', ValidatorException::class], - ['ABC', ValidatorException::class], - ]; - } - - /** - * Test for the wrong PHP version passed as argument. - * - * @dataProvider providerTestPhpVersionFailure - */ - public function testPhpVersionFailure(mixed $phpVersion): void { - $this->expectException(ValidatorException::class); - $this->executeCommand([ - 'applicationUuid' => self::$applicationUuid, - 'php-version' => $phpVersion, - ]); - } - - /** - * Test for CI/CD not enabled on the project. - */ - public function testCiCdNotEnabled(): void { - $this->mockApplicationRequest(); - $gitlabClient = $this->prophet->prophesize(Client::class); - $this->mockGitLabUsersMe($gitlabClient); - $projects = $this->mockGetGitLabProjects( - self::$applicationUuid, - $this->gitLabProjectId, - [$this->getMockedGitLabProject($this->gitLabProjectId)], - ); - - $gitlabClient->projects()->willReturn($projects); - - $this->command->setGitLabClient($gitlabClient->reveal()); - $this->executeCommand([ - '--gitlab-host-name' => $this->gitLabHost, - '--gitlab-token' => $this->gitLabToken, - 'applicationUuid' => self::$applicationUuid, - 'php-version' => '8.1', - ]); - - $output = $this->getDisplay(); - $this->assertStringContainsString('CI/CD is not enabled for this application in code studio.', $output); - } - - /** - * Test for failed PHP version add. - */ - public function testPhpVersionAddFail(): void { - $this->mockApplicationRequest(); - $gitlabClient = $this->prophet->prophesize(Client::class); - $this->mockGitLabUsersMe($gitlabClient); - $mockedProject = $this->getMockedGitLabProject($this->gitLabProjectId); - $mockedProject['jobs_enabled'] = TRUE; - $projects = $this->mockGetGitLabProjects( - self::$applicationUuid, - $this->gitLabProjectId, - [$mockedProject], - ); - - $projects->variables($this->gitLabProjectId)->willReturn($this->getMockGitLabVariables()); - $projects->addVariable($this->gitLabProjectId, Argument::type('string'), Argument::type('string')) - ->willThrow(RuntimeException::class); - - $gitlabClient->projects()->willReturn($projects); - $this->command->setGitLabClient($gitlabClient->reveal()); - $this->executeCommand([ - '--gitlab-host-name' => $this->gitLabHost, - '--gitlab-token' => $this->gitLabToken, - 'applicationUuid' => self::$applicationUuid, - 'php-version' => '8.1', - ]); - - $output = $this->getDisplay(); - $this->assertStringContainsString('Unable to update the PHP version to 8.1', $output); - } - - /** - * @group brokenProphecy - */ - public function testPhpVersionAdd(): void { - $this->mockApplicationRequest(); - $gitlabClient = $this->prophet->prophesize(Client::class); - $this->mockGitLabUsersMe($gitlabClient); - $mockedProject = $this->getMockedGitLabProject($this->gitLabProjectId); - $mockedProject['jobs_enabled'] = TRUE; - $projects = $this->mockGetGitLabProjects( - self::$applicationUuid, - $this->gitLabProjectId, - [$mockedProject], - ); - - $projects->variables($this->gitLabProjectId)->willReturn($this->getMockGitLabVariables()); - $projects->addVariable($this->gitLabProjectId, Argument::type('string'), Argument::type('string')); - - $gitlabClient->projects()->willReturn($projects); - - $this->command->setGitLabClient($gitlabClient->reveal()); - $this->executeCommand([ - '--gitlab-host-name' => $this->gitLabHost, - '--gitlab-token' => $this->gitLabToken, - 'applicationUuid' => self::$applicationUuid, - 'php-version' => '8.1', - ]); - - $output = $this->getDisplay(); - $this->assertStringContainsString('PHP version is updated to 8.1 successfully!', $output); - } - - /** - * Test for failed PHP version update. - */ - public function testPhpVersionUpdateFail(): void { - $this->mockApplicationRequest(); - $gitlabClient = $this->prophet->prophesize(Client::class); - $this->mockGitLabUsersMe($gitlabClient); - $mockedProject = $this->getMockedGitLabProject($this->gitLabProjectId); - $mockedProject['jobs_enabled'] = TRUE; - $projects = $this->mockGetGitLabProjects( - self::$applicationUuid, - $this->gitLabProjectId, - [$mockedProject], - ); - - $variables = $this->getMockGitLabVariables(); - $variables[] = [ - 'environment_scope' => '*', - 'key' => 'PHP_VERSION', - 'masked' => FALSE, - 'protected' => FALSE, - 'value' => '8.1', - 'variable_type' => 'env_var', - ]; - $projects->variables($this->gitLabProjectId)->willReturn($variables); - $projects->updateVariable($this->gitLabProjectId, Argument::type('string'), Argument::type('string')) - ->willThrow(RuntimeException::class); - - $gitlabClient->projects()->willReturn($projects); - $this->command->setGitLabClient($gitlabClient->reveal()); - $this->executeCommand([ - '--gitlab-host-name' => $this->gitLabHost, - '--gitlab-token' => $this->gitLabToken, - 'applicationUuid' => self::$applicationUuid, - 'php-version' => '8.1', - ]); - - $output = $this->getDisplay(); - $this->assertStringContainsString('Unable to update the PHP version to 8.1', $output); - } - - /** - * @group brokenProphecy - */ - public function testPhpVersionUpdate(): void { - $this->mockApplicationRequest(); - $gitlabClient = $this->prophet->prophesize(Client::class); - $this->mockGitLabUsersMe($gitlabClient); - $mockedProject = $this->getMockedGitLabProject($this->gitLabProjectId); - $mockedProject['jobs_enabled'] = TRUE; - $projects = $this->mockGetGitLabProjects( - self::$applicationUuid, - $this->gitLabProjectId, - [$mockedProject], - ); - - $variables = $this->getMockGitLabVariables(); - $variables[] = [ - 'environment_scope' => '*', - 'key' => 'PHP_VERSION', - 'masked' => FALSE, - 'protected' => FALSE, - 'value' => '8.1', - 'variable_type' => 'env_var', - ]; - $projects->variables($this->gitLabProjectId)->willReturn($variables); - $projects->updateVariable($this->gitLabProjectId, Argument::type('string'), Argument::type('string')); - - $gitlabClient->projects()->willReturn($projects); - - $this->command->setGitLabClient($gitlabClient->reveal()); - $this->executeCommand([ - '--gitlab-host-name' => $this->gitLabHost, - '--gitlab-token' => $this->gitLabToken, - 'applicationUuid' => self::$applicationUuid, - 'php-version' => '8.1', - ]); - - $output = $this->getDisplay(); - $this->assertStringContainsString('PHP version is updated to 8.1 successfully!', $output); - } - +class CodeStudioPhpVersionCommandTest extends CommandTestBase +{ + private string $gitLabHost = 'gitlabhost'; + private string $gitLabToken = 'gitlabtoken'; + private int $gitLabProjectId = 33; + public static string $applicationUuid = 'a47ac10b-58cc-4372-a567-0e02b2c3d470'; + + protected function createCommand(): CommandBase + { + return $this->injectCommand(CodeStudioPhpVersionCommand::class); + } + + /** + * @return array + */ + public function providerTestPhpVersionFailure(): array + { + return [ + ['', ValidatorException::class], + ['8', ValidatorException::class], + ['8 1', ValidatorException::class], + ['ABC', ValidatorException::class], + ]; + } + + /** + * Test for the wrong PHP version passed as argument. + * + * @dataProvider providerTestPhpVersionFailure + */ + public function testPhpVersionFailure(mixed $phpVersion): void + { + $this->expectException(ValidatorException::class); + $this->executeCommand([ + 'applicationUuid' => self::$applicationUuid, + 'php-version' => $phpVersion, + ]); + } + + /** + * Test for CI/CD not enabled on the project. + */ + public function testCiCdNotEnabled(): void + { + $this->mockApplicationRequest(); + $gitlabClient = $this->prophet->prophesize(Client::class); + $this->mockGitLabUsersMe($gitlabClient); + $projects = $this->mockGetGitLabProjects( + self::$applicationUuid, + $this->gitLabProjectId, + [$this->getMockedGitLabProject($this->gitLabProjectId)], + ); + + $gitlabClient->projects()->willReturn($projects); + + $this->command->setGitLabClient($gitlabClient->reveal()); + $this->executeCommand([ + '--gitlab-host-name' => $this->gitLabHost, + '--gitlab-token' => $this->gitLabToken, + 'applicationUuid' => self::$applicationUuid, + 'php-version' => '8.1', + ]); + + $output = $this->getDisplay(); + $this->assertStringContainsString('CI/CD is not enabled for this application in code studio.', $output); + } + + /** + * Test for failed PHP version add. + */ + public function testPhpVersionAddFail(): void + { + $this->mockApplicationRequest(); + $gitlabClient = $this->prophet->prophesize(Client::class); + $this->mockGitLabUsersMe($gitlabClient); + $mockedProject = $this->getMockedGitLabProject($this->gitLabProjectId); + $mockedProject['jobs_enabled'] = true; + $projects = $this->mockGetGitLabProjects( + self::$applicationUuid, + $this->gitLabProjectId, + [$mockedProject], + ); + + $projects->variables($this->gitLabProjectId)->willReturn($this->getMockGitLabVariables()); + $projects->addVariable($this->gitLabProjectId, Argument::type('string'), Argument::type('string')) + ->willThrow(RuntimeException::class); + + $gitlabClient->projects()->willReturn($projects); + $this->command->setGitLabClient($gitlabClient->reveal()); + $this->executeCommand([ + '--gitlab-host-name' => $this->gitLabHost, + '--gitlab-token' => $this->gitLabToken, + 'applicationUuid' => self::$applicationUuid, + 'php-version' => '8.1', + ]); + + $output = $this->getDisplay(); + $this->assertStringContainsString('Unable to update the PHP version to 8.1', $output); + } + + /** + * @group brokenProphecy + */ + public function testPhpVersionAdd(): void + { + $this->mockApplicationRequest(); + $gitlabClient = $this->prophet->prophesize(Client::class); + $this->mockGitLabUsersMe($gitlabClient); + $mockedProject = $this->getMockedGitLabProject($this->gitLabProjectId); + $mockedProject['jobs_enabled'] = true; + $projects = $this->mockGetGitLabProjects( + self::$applicationUuid, + $this->gitLabProjectId, + [$mockedProject], + ); + + $projects->variables($this->gitLabProjectId)->willReturn($this->getMockGitLabVariables()); + $projects->addVariable($this->gitLabProjectId, Argument::type('string'), Argument::type('string')); + + $gitlabClient->projects()->willReturn($projects); + + $this->command->setGitLabClient($gitlabClient->reveal()); + $this->executeCommand([ + '--gitlab-host-name' => $this->gitLabHost, + '--gitlab-token' => $this->gitLabToken, + 'applicationUuid' => self::$applicationUuid, + 'php-version' => '8.1', + ]); + + $output = $this->getDisplay(); + $this->assertStringContainsString('PHP version is updated to 8.1 successfully!', $output); + } + + /** + * Test for failed PHP version update. + */ + public function testPhpVersionUpdateFail(): void + { + $this->mockApplicationRequest(); + $gitlabClient = $this->prophet->prophesize(Client::class); + $this->mockGitLabUsersMe($gitlabClient); + $mockedProject = $this->getMockedGitLabProject($this->gitLabProjectId); + $mockedProject['jobs_enabled'] = true; + $projects = $this->mockGetGitLabProjects( + self::$applicationUuid, + $this->gitLabProjectId, + [$mockedProject], + ); + + $variables = $this->getMockGitLabVariables(); + $variables[] = [ + 'environment_scope' => '*', + 'key' => 'PHP_VERSION', + 'masked' => false, + 'protected' => false, + 'value' => '8.1', + 'variable_type' => 'env_var', + ]; + $projects->variables($this->gitLabProjectId)->willReturn($variables); + $projects->updateVariable($this->gitLabProjectId, Argument::type('string'), Argument::type('string')) + ->willThrow(RuntimeException::class); + + $gitlabClient->projects()->willReturn($projects); + $this->command->setGitLabClient($gitlabClient->reveal()); + $this->executeCommand([ + '--gitlab-host-name' => $this->gitLabHost, + '--gitlab-token' => $this->gitLabToken, + 'applicationUuid' => self::$applicationUuid, + 'php-version' => '8.1', + ]); + + $output = $this->getDisplay(); + $this->assertStringContainsString('Unable to update the PHP version to 8.1', $output); + } + + /** + * @group brokenProphecy + */ + public function testPhpVersionUpdate(): void + { + $this->mockApplicationRequest(); + $gitlabClient = $this->prophet->prophesize(Client::class); + $this->mockGitLabUsersMe($gitlabClient); + $mockedProject = $this->getMockedGitLabProject($this->gitLabProjectId); + $mockedProject['jobs_enabled'] = true; + $projects = $this->mockGetGitLabProjects( + self::$applicationUuid, + $this->gitLabProjectId, + [$mockedProject], + ); + + $variables = $this->getMockGitLabVariables(); + $variables[] = [ + 'environment_scope' => '*', + 'key' => 'PHP_VERSION', + 'masked' => false, + 'protected' => false, + 'value' => '8.1', + 'variable_type' => 'env_var', + ]; + $projects->variables($this->gitLabProjectId)->willReturn($variables); + $projects->updateVariable($this->gitLabProjectId, Argument::type('string'), Argument::type('string')); + + $gitlabClient->projects()->willReturn($projects); + + $this->command->setGitLabClient($gitlabClient->reveal()); + $this->executeCommand([ + '--gitlab-host-name' => $this->gitLabHost, + '--gitlab-token' => $this->gitLabToken, + 'applicationUuid' => self::$applicationUuid, + 'php-version' => '8.1', + ]); + + $output = $this->getDisplay(); + $this->assertStringContainsString('PHP version is updated to 8.1 successfully!', $output); + } } diff --git a/tests/phpunit/src/Commands/CodeStudio/CodeStudioPipelinesMigrateCommandTest.php b/tests/phpunit/src/Commands/CodeStudio/CodeStudioPipelinesMigrateCommandTest.php index 0e4b9af06..f04927bb5 100644 --- a/tests/phpunit/src/Commands/CodeStudio/CodeStudioPipelinesMigrateCommandTest.php +++ b/tests/phpunit/src/Commands/CodeStudio/CodeStudioPipelinesMigrateCommandTest.php @@ -1,6 +1,6 @@ mockApplicationRequest(); - TestBase::setEnvVars(['GITLAB_HOST' => 'code.cloudservices.acquia.io']); - } + public function setUp(): void + { + parent::setUp(); + $this->mockApplicationRequest(); + TestBase::setEnvVars(['GITLAB_HOST' => 'code.cloudservices.acquia.io']); + } - public function tearDown(): void { - parent::tearDown(); - TestBase::unsetEnvVars(['GITLAB_HOST' => 'code.cloudservices.acquia.io']); - } + public function tearDown(): void + { + parent::tearDown(); + TestBase::unsetEnvVars(['GITLAB_HOST' => 'code.cloudservices.acquia.io']); + } - protected function createCommand(): CommandBase { - return $this->injectCommand(CodeStudioPipelinesMigrateCommand::class); - } + protected function createCommand(): CommandBase + { + return $this->injectCommand(CodeStudioPipelinesMigrateCommand::class); + } - /** - * @return array - */ - public function providerTestCommand(): array { - return [ - [ + /** + * @return array + */ + public function providerTestCommand(): array + { + return [ + [ // One project. [$this->getMockedGitLabProject($this->gitLabProjectId)], // Inputs. [ - // Would you like Acquia CLI to search for a Cloud application that matches your local git config? - 'n', - // @todo - '0', - // Do you want to continue? - 'y', + // Would you like Acquia CLI to search for a Cloud application that matches your local git config? + 'n', + // @todo + '0', + // Do you want to continue? + 'y', ], // Args. [ - '--key' => $this->key, - '--secret' => $this->secret, + '--key' => $this->key, + '--secret' => $this->secret, ], - ], - ]; - } + ], + ]; + } - /** - * @dataProvider providerTestCommand - * @param $mockedGitlabProjects - * @param $args - * @param $inputs - * @group brokenProphecy - */ - public function testCommand(mixed $mockedGitlabProjects, mixed $inputs, mixed $args): void { - copy( - Path::join($this->realFixtureDir, 'acquia-pipelines.yml'), - Path::join($this->projectDir, 'acquia-pipelines.yml') - ); - $localMachineHelper = $this->mockLocalMachineHelper(); - $this->mockExecuteGlabExists($localMachineHelper); - $this->mockGitlabGetHost($localMachineHelper, $this->gitLabHost); - $this->mockGitlabGetToken($localMachineHelper, $this->gitLabToken, $this->gitLabHost); - $gitlabClient = $this->prophet->prophesize(Client::class); - $this->mockGitLabUsersMe($gitlabClient); - $this->mockRequest('getAccount'); - $this->mockGitLabPermissionsRequest($this::$applicationUuid); - $projects = $this->mockGetGitLabProjects($this::$applicationUuid, $this->gitLabProjectId, $mockedGitlabProjects); - $gitlabCicdVariables = [ - [ + /** + * @dataProvider providerTestCommand + * @param $mockedGitlabProjects + * @param $args + * @param $inputs + * @group brokenProphecy + */ + public function testCommand(mixed $mockedGitlabProjects, mixed $inputs, mixed $args): void + { + copy( + Path::join($this->realFixtureDir, 'acquia-pipelines.yml'), + Path::join($this->projectDir, 'acquia-pipelines.yml') + ); + $localMachineHelper = $this->mockLocalMachineHelper(); + $this->mockExecuteGlabExists($localMachineHelper); + $this->mockGitlabGetHost($localMachineHelper, $this->gitLabHost); + $this->mockGitlabGetToken($localMachineHelper, $this->gitLabToken, $this->gitLabHost); + $gitlabClient = $this->prophet->prophesize(Client::class); + $this->mockGitLabUsersMe($gitlabClient); + $this->mockRequest('getAccount'); + $this->mockGitLabPermissionsRequest($this::$applicationUuid); + $projects = $this->mockGetGitLabProjects($this::$applicationUuid, $this->gitLabProjectId, $mockedGitlabProjects); + $gitlabCicdVariables = [ + [ 'key' => 'ACQUIA_APPLICATION_UUID', - 'masked' => TRUE, - 'protected' => FALSE, - 'value' => NULL, + 'masked' => true, + 'protected' => false, + 'value' => null, 'variable_type' => 'env_var', - ], - [ + ], + [ 'key' => 'ACQUIA_CLOUD_API_TOKEN_KEY', - 'masked' => TRUE, - 'protected' => FALSE, - 'value' => NULL, + 'masked' => true, + 'protected' => false, + 'value' => null, 'variable_type' => 'env_var', - ], - [ + ], + [ 'key' => 'ACQUIA_CLOUD_API_TOKEN_SECRET', - 'masked' => TRUE, - 'protected' => FALSE, - 'value' => NULL, + 'masked' => true, + 'protected' => false, + 'value' => null, 'variable_type' => 'env_var', - ], - [ + ], + [ 'key' => 'ACQUIA_GLAB_TOKEN_NAME', - 'masked' => TRUE, - 'protected' => FALSE, - 'value' => NULL, + 'masked' => true, + 'protected' => false, + 'value' => null, 'variable_type' => 'env_var', - ], - [ + ], + [ 'key' => 'ACQUIA_GLAB_TOKEN_SECRET', - 'masked' => TRUE, - 'protected' => FALSE, - 'value' => NULL, + 'masked' => true, + 'protected' => false, + 'value' => null, 'variable_type' => 'env_var', - ], - [ + ], + [ 'key' => 'PHP_VERSION', - 'masked' => FALSE, - 'protected' => FALSE, - 'value' => NULL, + 'masked' => false, + 'protected' => false, + 'value' => null, 'variable_type' => 'env_var', - ], - ]; - $projects->variables($this->gitLabProjectId)->willReturn($gitlabCicdVariables); - $projects->update($this->gitLabProjectId, Argument::type('array')); - $gitlabClient->projects()->willReturn($projects); - $localMachineHelper->getFilesystem()->willReturn(new Filesystem())->shouldBeCalled(); - $this->command->setGitLabClient($gitlabClient->reveal()); + ], + ]; + $projects->variables($this->gitLabProjectId)->willReturn($gitlabCicdVariables); + $projects->update($this->gitLabProjectId, Argument::type('array')); + $gitlabClient->projects()->willReturn($projects); + $localMachineHelper->getFilesystem()->willReturn(new Filesystem())->shouldBeCalled(); + $this->command->setGitLabClient($gitlabClient->reveal()); - $this->mockRequest('getApplications'); - // Set properties and execute. - $this->executeCommand($args, $inputs); + $this->mockRequest('getApplications'); + // Set properties and execute. + $this->executeCommand($args, $inputs); - // Assertions. - $this->assertEquals(0, $this->getStatusCode()); - $gitlabCiYmlFilePath = $this->projectDir . '/.gitlab-ci.yml'; - $this->assertFileExists($gitlabCiYmlFilePath); - // @todo Assert things about skips. Composer install, BLT, launch_ode. - $contents = Yaml::parseFile($gitlabCiYmlFilePath); - $arraySkipMap = ['composer install', '${BLT_DIR}', 'launch_ode']; - foreach ($contents as $values) { - if (array_key_exists('script', $values)) { - foreach ($arraySkipMap as $map) { - $this->assertNotContains($map, $values['script'], "Skip option found"); + // Assertions. + $this->assertEquals(0, $this->getStatusCode()); + $gitlabCiYmlFilePath = $this->projectDir . '/.gitlab-ci.yml'; + $this->assertFileExists($gitlabCiYmlFilePath); + // @todo Assert things about skips. Composer install, BLT, launch_ode. + $contents = Yaml::parseFile($gitlabCiYmlFilePath); + $arraySkipMap = ['composer install', '${BLT_DIR}', 'launch_ode']; + foreach ($contents as $values) { + if (array_key_exists('script', $values)) { + foreach ($arraySkipMap as $map) { + $this->assertNotContains($map, $values['script'], "Skip option found"); + } + } } - } + $this->assertArrayHasKey('include', $contents); + $this->assertArrayHasKey('variables', $contents); + $this->assertArrayHasKey('setup', $contents); + $this->assertArrayHasKey('launch_ode', $contents); + $this->assertArrayHasKey('script', $contents['launch_ode']); + $this->assertNotEmpty($contents['launch_ode']['script']); + $this->assertArrayHasKey('script', $contents['setup']); + $this->assertArrayHasKey('stage', $contents['setup']); + $this->assertEquals('Build Drupal', $contents['setup']['stage']); + $this->assertArrayHasKey('needs', $contents['setup']); + $this->assertIsArray($contents['setup']['needs']); } - $this->assertArrayHasKey('include', $contents); - $this->assertArrayHasKey('variables', $contents); - $this->assertArrayHasKey('setup', $contents); - $this->assertArrayHasKey('launch_ode', $contents); - $this->assertArrayHasKey('script', $contents['launch_ode']); - $this->assertNotEmpty($contents['launch_ode']['script']); - $this->assertArrayHasKey('script', $contents['setup']); - $this->assertArrayHasKey('stage', $contents['setup']); - $this->assertEquals('Build Drupal', $contents['setup']['stage']); - $this->assertArrayHasKey('needs', $contents['setup']); - $this->assertIsArray($contents['setup']['needs']); - } - } diff --git a/tests/phpunit/src/Commands/CodeStudio/CodeStudioWizardCommandTest.php b/tests/phpunit/src/Commands/CodeStudio/CodeStudioWizardCommandTest.php index b312847b1..531a80f0e 100644 --- a/tests/phpunit/src/Commands/CodeStudio/CodeStudioWizardCommandTest.php +++ b/tests/phpunit/src/Commands/CodeStudio/CodeStudioWizardCommandTest.php @@ -1,6 +1,6 @@ mockApplicationRequest(); - TestBase::setEnvVars(['GITLAB_HOST' => 'code.cloudservices.acquia.io']); - } + public function setUp(): void + { + parent::setUp(); + $this->mockApplicationRequest(); + TestBase::setEnvVars(['GITLAB_HOST' => 'code.cloudservices.acquia.io']); + } - public function tearDown(): void { - parent::tearDown(); - TestBase::unsetEnvVars(['GITLAB_HOST' => 'code.cloudservices.acquia.io']); - } + public function tearDown(): void + { + parent::tearDown(); + TestBase::unsetEnvVars(['GITLAB_HOST' => 'code.cloudservices.acquia.io']); + } - protected function createCommand(): CommandBase { - return $this->injectCommand(CodeStudioWizardCommand::class); - } + protected function createCommand(): CommandBase + { + return $this->injectCommand(CodeStudioWizardCommand::class); + } - /** - * @return array - */ - public function providerTestCommand(): array { - return [ - [ + /** + * @return array + */ + public function providerTestCommand(): array + { + return [ + [ // One project. [$this->getMockedGitLabProject($this->gitLabProjectId)], // Inputs. [ - // Do you want to continue? - 'y', - // Would you like to perform a one time push of code from Acquia Cloud to Code Studio now? (yes/no) [yes]: - 'y', + // Do you want to continue? + 'y', + // Would you like to perform a one time push of code from Acquia Cloud to Code Studio now? (yes/no) [yes]: + 'y', ], // Args. [ - '--key' => $this->key, - '--secret' => $this->secret, + '--key' => $this->key, + '--secret' => $this->secret, + ], ], - ], - // Two projects. - [ + // Two projects. + [ [$this->getMockedGitLabProject($this->gitLabProjectId), $this->getMockedGitLabProject($this->gitLabProjectId)], // Inputs. [ - // Found multiple projects that could match the Sample application 1 application. Choose which one to configure. - '0', - // Do you want to continue? - 'y', - // Would you like to perform a one time push of code from Acquia Cloud to Code Studio now? (yes/no) [yes]: - 'y', + // Found multiple projects that could match the Sample application 1 application. Choose which one to configure. + '0', + // Do you want to continue? + 'y', + // Would you like to perform a one time push of code from Acquia Cloud to Code Studio now? (yes/no) [yes]: + 'y', ], // Args. [ - '--key' => $this->key, - '--secret' => $this->secret, + '--key' => $this->key, + '--secret' => $this->secret, + ], ], - ], - [ + [ // No projects. [], // Inputs. [ - // 'Would you like to create a new Code Studio project? - 'y', - // Select a project type Drupal_project. - '0', - // Select PHP version 8.1. - '0', - // Do you want to continue? - 'y', - // Would you like to perform a one time push of code from Acquia Cloud to Code Studio now? (yes/no) [yes]: - 'y', + // 'Would you like to create a new Code Studio project? + 'y', + // Select a project type Drupal_project. + '0', + // Select PHP version 8.1. + '0', + // Do you want to continue? + 'y', + // Would you like to perform a one time push of code from Acquia Cloud to Code Studio now? (yes/no) [yes]: + 'y', ], // Args. [ - '--key' => $this->key, - '--secret' => $this->secret, + '--key' => $this->key, + '--secret' => $this->secret, ], - ], - [ + ], + [ // No projects. [], // Inputs. [ - // 'Would you like to create a new Code Studio project? - 'y', - // Select a project type Drupal_project. - '0', - // Select PHP version 8.2. - '1', - // Do you want to continue? - 'y', - // Would you like to perform a one time push of code from Acquia Cloud to Code Studio now? (yes/no) [yes]: - 'y', + // 'Would you like to create a new Code Studio project? + 'y', + // Select a project type Drupal_project. + '0', + // Select PHP version 8.2. + '1', + // Do you want to continue? + 'y', + // Would you like to perform a one time push of code from Acquia Cloud to Code Studio now? (yes/no) [yes]: + 'y', ], // Args. [ - '--key' => $this->key, - '--secret' => $this->secret, + '--key' => $this->key, + '--secret' => $this->secret, ], - ], - [ + ], + [ // No projects. [], // Inputs. [ - // 'Would you like to create a new Code Studio project? - 'y', - // Select a project type Node_project. - '1', - // Select NODE version 18.17.1. - '0', - // Do you want to continue? - 'y', - // Would you like to perform a one time push of code from Acquia Cloud to Code Studio now? (yes/no) [yes]: - 'y', + // 'Would you like to create a new Code Studio project? + 'y', + // Select a project type Node_project. + '1', + // Select NODE version 18.17.1. + '0', + // Do you want to continue? + 'y', + // Would you like to perform a one time push of code from Acquia Cloud to Code Studio now? (yes/no) [yes]: + 'y', ], // Args. [ - '--key' => $this->key, - '--secret' => $this->secret, + '--key' => $this->key, + '--secret' => $this->secret, + ], ], - ], - [ + [ // No projects. [], // Inputs. [ - // 'Would you like to create a new Code Studio project? - 'y', - // Select a project type Node_project. - '1', - // Select NODE version 20.5.1. - '1', - // Do you want to continue? - 'y', - // Would you like to perform a one time push of code from Acquia Cloud to Code Studio now? (yes/no) [yes]: - 'y', + // 'Would you like to create a new Code Studio project? + 'y', + // Select a project type Node_project. + '1', + // Select NODE version 20.5.1. + '1', + // Do you want to continue? + 'y', + // Would you like to perform a one time push of code from Acquia Cloud to Code Studio now? (yes/no) [yes]: + 'y', ], // Args. [ - '--key' => $this->key, - '--secret' => $this->secret, + '--key' => $this->key, + '--secret' => $this->secret, + ], ], - ], - [ + [ // No projects. [], // Inputs. [ - // 'Would you like to create a new Code Studio project? - 'n', - // Choose project. - '0', - // Do you want to continue? - 'y', + // 'Would you like to create a new Code Studio project? + 'n', + // Choose project. + '0', + // Do you want to continue? + 'y', ], // Args. [ - '--key' => $this->key, - '--secret' => $this->secret, + '--key' => $this->key, + '--secret' => $this->secret, ], - ], - [ + ], + [ // No projects. [], // Inputs. [ - // 'Would you like to create a new Code Studio project? - 'y', - // Enter Cloud Key. - $this->key, - // Enter Cloud secret,. - $this->secret, - // Select a project type Drupal_project. - '0', - // Select PHP version 8.1. - '0', - // Do you want to continue? - 'y', + // 'Would you like to create a new Code Studio project? + 'y', + // Enter Cloud Key. + $this->key, + // Enter Cloud secret,. + $this->secret, + // Select a project type Drupal_project. + '0', + // Select PHP version 8.1. + '0', + // Do you want to continue? + 'y', ], // Args. [], - ], - [ + ], + [ // No projects. [], // Inputs. [ - // 'Would you like to create a new Code Studio project? - 'y', - // Enter Cloud Key. - $this->key, - // Enter Cloud secret,. - $this->secret, - // Select a project type Node_project. - '1', - // Select NODE version 18.17.1. - '0', - // Do you want to continue? - 'y', + // 'Would you like to create a new Code Studio project? + 'y', + // Enter Cloud Key. + $this->key, + // Enter Cloud secret,. + $this->secret, + // Select a project type Node_project. + '1', + // Select NODE version 18.17.1. + '0', + // Do you want to continue? + 'y', ], // Args. [], - ], - [ + ], + [ // No projects. [], // Inputs. [ - // 'Would you like to create a new Code Studio project? - 'y', - // Enter Cloud Key. - $this->key, - // Enter Cloud secret,. - $this->secret, - // Select a project type Drupal_project. - '0', - // Select PHP version 8.2. - '1', - // Do you want to continue? - 'y', + // 'Would you like to create a new Code Studio project? + 'y', + // Enter Cloud Key. + $this->key, + // Enter Cloud secret,. + $this->secret, + // Select a project type Drupal_project. + '0', + // Select PHP version 8.2. + '1', + // Do you want to continue? + 'y', ], // Args. [], - ], - [ + ], + [ // No projects. [], // Inputs. [ - // 'Would you like to create a new Code Studio project? - 'y', - // Enter Cloud Key. - $this->key, - // Enter Cloud secret,. - $this->secret, - // Select a project type Node_project. - '1', - // Select NODE version 20.5.1. - '1', - // Do you want to continue? - 'y', + // 'Would you like to create a new Code Studio project? + 'y', + // Enter Cloud Key. + $this->key, + // Enter Cloud secret,. + $this->secret, + // Select a project type Node_project. + '1', + // Select NODE version 20.5.1. + '1', + // Do you want to continue? + 'y', ], // Args. [], - ], - ]; - } - - /** - * @dataProvider providerTestCommand - */ - public function testCommand(array $mockedGitlabProjects, array $inputs, array $args): void { - $this->clientServiceProphecy->setConnector(Argument::type(Connector::class))->shouldBeCalled(); - $this->mockRequest('getAccount'); - $this->mockGitLabPermissionsRequest($this::$applicationUuid); - - $gitlabClient = $this->prophet->prophesize(Client::class); - $this->mockGitLabUsersMe($gitlabClient); - $this->mockGitLabGroups($gitlabClient); - $this->mockGitLabNamespaces($gitlabClient); + ], + ]; + } - $projects = $this->mockGetGitLabProjects($this::$applicationUuid, $this->gitLabProjectId, $mockedGitlabProjects); - $parameters = [ - 'container_registry_access_level' => 'disabled', - 'default_branch' => 'main', - 'description' => 'Source repository for Acquia Cloud Platform application a47ac10b-58cc-4372-a567-0e02b2c3d470', - 'initialize_with_readme' => TRUE, - 'namespace_id' => 47, - 'topics' => 'Acquia Cloud Application', - ]; - $projects->create('Sample-application-1', $parameters)->willReturn($this->getMockedGitLabProject($this->gitLabProjectId)); - $this->mockGitLabProjectsTokens($projects); - $parameters = [ - 'container_registry_access_level' => 'disabled', - 'default_branch' => 'main', - 'description' => 'Source repository for Acquia Cloud Platform application a47ac10b-58cc-4372-a567-0e02b2c3d470', - 'initialize_with_readme' => TRUE, - 'topics' => 'Acquia Cloud Application', - ]; - $projects->update($this->gitLabProjectId, $parameters)->shouldBeCalled(); - $projects->uploadAvatar( - 33, - Argument::type('string'), - )->shouldBeCalled(); - $this->mockGitLabVariables($this->gitLabProjectId, $projects); + /** + * @dataProvider providerTestCommand + */ + public function testCommand(array $mockedGitlabProjects, array $inputs, array $args): void + { + $this->clientServiceProphecy->setConnector(Argument::type(Connector::class))->shouldBeCalled(); + $this->mockRequest('getAccount'); + $this->mockGitLabPermissionsRequest($this::$applicationUuid); - if ($inputs[0] === 'y' && ($inputs[1] === '1' || (array_key_exists(3, $inputs) && $inputs[3] === '1'))) { - $parameters = [ - 'ci_config_path' => 'gitlab-ci/Auto-DevOps.acquia.gitlab-ci.yml@acquia/node-template', - ]; - $projects->update($this->gitLabProjectId, $parameters)->shouldBeCalled(); - } - else { - $schedules = $this->prophet->prophesize(Schedules::class); - $schedules->showAll($this->gitLabProjectId)->willReturn([]); - $pipeline = ['id' => 1]; - $parameters = [ - // Every Thursday at midnight. - 'cron' => '0 0 * * 4', - 'description' => 'Code Studio Automatic Updates', - 'ref' => 'master', - ]; - $schedules->create($this->gitLabProjectId, $parameters)->willReturn($pipeline); - $schedules->addVariable($this->gitLabProjectId, $pipeline['id'], [ - 'key' => 'ACQUIA_JOBS_DEPRECATED_UPDATE', - 'value' => 'true', - ])->shouldBeCalled(); - $schedules->addVariable($this->gitLabProjectId, $pipeline['id'], [ - 'key' => 'ACQUIA_JOBS_COMPOSER_UPDATE', - 'value' => 'true', - ])->shouldBeCalled(); - $gitlabClient->schedules()->willReturn($schedules->reveal()); - } + $gitlabClient = $this->prophet->prophesize(Client::class); + $this->mockGitLabUsersMe($gitlabClient); + $this->mockGitLabGroups($gitlabClient); + $this->mockGitLabNamespaces($gitlabClient); - $gitlabClient->projects()->willReturn($projects); + $projects = $this->mockGetGitLabProjects($this::$applicationUuid, $this->gitLabProjectId, $mockedGitlabProjects); + $parameters = [ + 'container_registry_access_level' => 'disabled', + 'default_branch' => 'main', + 'description' => 'Source repository for Acquia Cloud Platform application a47ac10b-58cc-4372-a567-0e02b2c3d470', + 'initialize_with_readme' => true, + 'namespace_id' => 47, + 'topics' => 'Acquia Cloud Application', + ]; + $projects->create('Sample-application-1', $parameters)->willReturn($this->getMockedGitLabProject($this->gitLabProjectId)); + $this->mockGitLabProjectsTokens($projects); + $parameters = [ + 'container_registry_access_level' => 'disabled', + 'default_branch' => 'main', + 'description' => 'Source repository for Acquia Cloud Platform application a47ac10b-58cc-4372-a567-0e02b2c3d470', + 'initialize_with_readme' => true, + 'topics' => 'Acquia Cloud Application', + ]; + $projects->update($this->gitLabProjectId, $parameters)->shouldBeCalled(); + $projects->uploadAvatar( + 33, + Argument::type('string'), + )->shouldBeCalled(); + $this->mockGitLabVariables($this->gitLabProjectId, $projects); - $this->command->setGitLabClient($gitlabClient->reveal()); + if ($inputs[0] === 'y' && ($inputs[1] === '1' || (array_key_exists(3, $inputs) && $inputs[3] === '1'))) { + $parameters = [ + 'ci_config_path' => 'gitlab-ci/Auto-DevOps.acquia.gitlab-ci.yml@acquia/node-template', + ]; + $projects->update($this->gitLabProjectId, $parameters)->shouldBeCalled(); + } else { + $schedules = $this->prophet->prophesize(Schedules::class); + $schedules->showAll($this->gitLabProjectId)->willReturn([]); + $pipeline = ['id' => 1]; + $parameters = [ + // Every Thursday at midnight. + 'cron' => '0 0 * * 4', + 'description' => 'Code Studio Automatic Updates', + 'ref' => 'master', + ]; + $schedules->create($this->gitLabProjectId, $parameters)->willReturn($pipeline); + $schedules->addVariable($this->gitLabProjectId, $pipeline['id'], [ + 'key' => 'ACQUIA_JOBS_DEPRECATED_UPDATE', + 'value' => 'true', + ])->shouldBeCalled(); + $schedules->addVariable($this->gitLabProjectId, $pipeline['id'], [ + 'key' => 'ACQUIA_JOBS_COMPOSER_UPDATE', + 'value' => 'true', + ])->shouldBeCalled(); + $gitlabClient->schedules()->willReturn($schedules->reveal()); + } - $localMachineHelper = $this->mockLocalMachineHelper(); - $localMachineHelper->checkRequiredBinariesExist(['git']); - $this->mockExecuteGlabExists($localMachineHelper); - $process = $this->mockProcess(); - $localMachineHelper->execute(Argument::containing('remote'), Argument::type('callable'), '/home/ide/project', FALSE)->willReturn($process->reveal()); - $localMachineHelper->execute(Argument::containing('push'), Argument::type('callable'), '/home/ide/project', FALSE)->willReturn($process->reveal()); + $gitlabClient->projects()->willReturn($projects); - $this->mockGetCurrentBranchName($localMachineHelper); - $this->mockGitlabGetHost($localMachineHelper, $this->gitLabHost); - $this->mockGitlabGetToken($localMachineHelper, $this->gitLabToken, $this->gitLabHost); + $this->command->setGitLabClient($gitlabClient->reveal()); - /** @var Filesystem|ObjectProphecy $fileSystem */ - $fileSystem = $this->prophet->prophesize(Filesystem::class); - // Set properties and execute. - $this->executeCommand($args, $inputs); - $output = $this->getDisplay(); - $output_strings = $this->getOutputStrings(); - foreach ($output_strings as $output_string) { - self::assertStringContainsString($output_string, $output); - } + $localMachineHelper = $this->mockLocalMachineHelper(); + $localMachineHelper->checkRequiredBinariesExist(['git']); + $this->mockExecuteGlabExists($localMachineHelper); + $process = $this->mockProcess(); + $localMachineHelper->execute(Argument::containing('remote'), Argument::type('callable'), '/home/ide/project', false)->willReturn($process->reveal()); + $localMachineHelper->execute(Argument::containing('push'), Argument::type('callable'), '/home/ide/project', false)->willReturn($process->reveal()); - // Assertions. - $this->assertEquals(0, $this->getStatusCode()); - } + $this->mockGetCurrentBranchName($localMachineHelper); + $this->mockGitlabGetHost($localMachineHelper, $this->gitLabHost); + $this->mockGitlabGetToken($localMachineHelper, $this->gitLabToken, $this->gitLabHost); - /** - * @group brokenProphecy - */ - public function testInvalidGitLabCredentials(): void { - $localMachineHelper = $this->mockLocalMachineHelper(); - $this->mockExecuteGlabExists($localMachineHelper); - $gitlabClient = $this->mockGitLabAuthenticate($localMachineHelper, $this->gitLabHost, $this->gitLabToken); - $this->command->setGitLabClient($gitlabClient->reveal()); + /** @var Filesystem|ObjectProphecy $fileSystem */ + $fileSystem = $this->prophet->prophesize(Filesystem::class); + // Set properties and execute. + $this->executeCommand($args, $inputs); + $output = $this->getDisplay(); + $output_strings = $this->getOutputStrings(); + foreach ($output_strings as $output_string) { + self::assertStringContainsString($output_string, $output); + } - $this->expectException(AcquiaCliException::class); - $this->expectExceptionMessage('Unable to authenticate with Code Studio'); - $this->executeCommand([ - '--key' => $this->key, - '--secret' => $this->secret, - ]); - } + // Assertions. + $this->assertEquals(0, $this->getStatusCode()); + } - /** - * @group brokenProphecy - */ - public function testMissingGitLabCredentials(): void { - $localMachineHelper = $this->mockLocalMachineHelper(); - $this->mockExecuteGlabExists($localMachineHelper); - $this->mockGitlabGetHost($localMachineHelper, $this->gitLabHost); - $this->mockGitlabGetToken($localMachineHelper, $this->gitLabToken, $this->gitLabHost, FALSE); + /** + * @group brokenProphecy + */ + public function testInvalidGitLabCredentials(): void + { + $localMachineHelper = $this->mockLocalMachineHelper(); + $this->mockExecuteGlabExists($localMachineHelper); + $gitlabClient = $this->mockGitLabAuthenticate($localMachineHelper, $this->gitLabHost, $this->gitLabToken); + $this->command->setGitLabClient($gitlabClient->reveal()); - $this->expectException(AcquiaCliException::class); - $this->expectExceptionMessage('Could not determine GitLab token'); - $this->executeCommand([ - '--key' => $this->key, - '--secret' => $this->secret, - ]); - } + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage('Unable to authenticate with Code Studio'); + $this->executeCommand([ + '--key' => $this->key, + '--secret' => $this->secret, + ]); + } - protected function mockGitLabProjectsTokens(ObjectProphecy $projects): void { - $tokens = [ - 0 => [ - 'access_level' => 40, - 'active' => TRUE, - 'created_at' => '2021-12-28T20:08:21.629Z', - 'expires_at' => new DateTime('+365 days'), - 'id' => $this->gitLabTokenId, - 'name' => 'acquia-codestudio', - 'revoked' => FALSE, - 'scopes' => [ - 0 => 'api', - 1 => 'write_repository', - ], - 'user_id' => 154, - ], - ]; - $projects->projectAccessTokens($this->gitLabProjectId)->willReturn($tokens)->shouldBeCalled(); - $projects->deleteProjectAccessToken($this->gitLabProjectId, $this->gitLabTokenId)->shouldBeCalled(); - $token = $tokens[0]; - $token['token'] = 'token'; - $projects->createProjectAccessToken($this->gitLabProjectId, Argument::type('array'))->willReturn($token); - } + /** + * @group brokenProphecy + */ + public function testMissingGitLabCredentials(): void + { + $localMachineHelper = $this->mockLocalMachineHelper(); + $this->mockExecuteGlabExists($localMachineHelper); + $this->mockGitlabGetHost($localMachineHelper, $this->gitLabHost); + $this->mockGitlabGetToken($localMachineHelper, $this->gitLabToken, $this->gitLabHost, false); - protected function mockGetCurrentBranchName(mixed $localMachineHelper): void { - $process = $this->mockProcess(); - $process->getOutput()->willReturn('main'); - $localMachineHelper->execute([ - 'git', - 'rev-parse', - '--abbrev-ref', - 'HEAD', - ], NULL, NULL, FALSE)->willReturn($process->reveal()); - } + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage('Could not determine GitLab token'); + $this->executeCommand([ + '--key' => $this->key, + '--secret' => $this->secret, + ]); + } - protected function mockGitLabGroups(ObjectProphecy $gitlabClient): void { - $groups = $this->prophet->prophesize(Groups::class); - $groups->all(Argument::type('array'))->willReturn([ - 0 => [ - 'auto_devops_enabled' => NULL, - 'avatar_url' => NULL, - 'created_at' => '2021-11-16T18:54:31.275Z', - 'default_branch_protection' => 2, - 'description' => '', - 'emails_disabled' => NULL, - 'full_name' => 'awesome-demo', - 'full_path' => 'awesome-demo', - 'id' => 47, - 'ldap_access' => NULL, - 'ldap_cn' => NULL, - 'lfs_enabled' => TRUE, - 'marked_for_deletion_on' => NULL, - 'mentions_disabled' => NULL, - 'name' => 'awesome-demo', - 'parent_id' => NULL, - 'path' => 'awesome-demo', - 'project_creation_level' => 'developer', - 'request_access_enabled' => TRUE, - 'require_two_factor_authentication' => FALSE, - 'share_with_group_lock' => FALSE, - 'subgroup_creation_level' => 'maintainer', - 'two_factor_grace_period' => 48, - 'visibility' => 'private', - 'web_url' => 'https://code.cloudservices.acquia.io/groups/awesome-demo', - ], - 1 => [ - 'auto_devops_enabled' => NULL, - 'avatar_url' => NULL, - 'created_at' => '2021-12-14T18:49:50.724Z', - 'default_branch_protection' => 2, - 'description' => '', - 'emails_disabled' => NULL, - 'full_name' => 'Nestle', - 'full_path' => 'nestle', - 'id' => 68, - 'ldap_access' => NULL, - 'ldap_cn' => NULL, - 'lfs_enabled' => TRUE, - 'marked_for_deletion_on' => NULL, - 'mentions_disabled' => NULL, - 'name' => 'Nestle', - 'parent_id' => NULL, - 'path' => 'nestle', - 'project_creation_level' => 'developer', - 'request_access_enabled' => TRUE, - 'require_two_factor_authentication' => FALSE, - 'share_with_group_lock' => FALSE, - 'subgroup_creation_level' => 'maintainer', - 'two_factor_grace_period' => 48, - 'visibility' => 'private', - 'web_url' => 'https://code.cloudservices.acquia.io/groups/nestle', - ], - ]); - $gitlabClient->groups()->willReturn($groups->reveal()); - } + protected function mockGitLabProjectsTokens(ObjectProphecy $projects): void + { + $tokens = [ + 0 => [ + 'access_level' => 40, + 'active' => true, + 'created_at' => '2021-12-28T20:08:21.629Z', + 'expires_at' => new DateTime('+365 days'), + 'id' => $this->gitLabTokenId, + 'name' => 'acquia-codestudio', + 'revoked' => false, + 'scopes' => [ + 0 => 'api', + 1 => 'write_repository', + ], + 'user_id' => 154, + ], + ]; + $projects->projectAccessTokens($this->gitLabProjectId)->willReturn($tokens)->shouldBeCalled(); + $projects->deleteProjectAccessToken($this->gitLabProjectId, $this->gitLabTokenId)->shouldBeCalled(); + $token = $tokens[0]; + $token['token'] = 'token'; + $projects->createProjectAccessToken($this->gitLabProjectId, Argument::type('array'))->willReturn($token); + } - protected function mockGitLabNamespaces(ObjectProphecy $gitlabClient): void { - $namespaces = $this->prophet->prophesize(ProjectNamespaces::class); - $namespaces->show(Argument::type('string'))->willReturn([ - 'avatar_url' => 'https://secure.gravatar.com/avatar/5ee7b8ad954bf7156e6eb57a45d60dec?s=80&d=identicon', - 'billable_members_count' => 1, - 'full_path' => 'matthew.grasmick', - 'id' => 48, - 'kind' => 'user', - 'max_seats_used' => 0, - 'name' => 'Matthew Grasmick', - 'parent_id' => NULL, - 'path' => 'matthew.grasmick', - 'plan' => 'default', - 'seats_in_use' => 0, - 'trial' => FALSE, - 'trial_ends_on' => NULL, - 'web_url' => 'https://code.cloudservices.acquia.io/matthew.grasmick', - ]); - $gitlabClient->namespaces()->willReturn($namespaces->reveal()); - } + protected function mockGetCurrentBranchName(mixed $localMachineHelper): void + { + $process = $this->mockProcess(); + $process->getOutput()->willReturn('main'); + $localMachineHelper->execute([ + 'git', + 'rev-parse', + '--abbrev-ref', + 'HEAD', + ], null, null, false)->willReturn($process->reveal()); + } - protected function mockGitLabVariables(int $gitlabProjectId, ObjectProphecy $projects): void { - $variables = $this->getMockGitLabVariables(); - $projects->variables($gitlabProjectId)->willReturn($variables); - foreach ($variables as $variable) { - $projects->addVariable($gitlabProjectId, Argument::type('string'), Argument::type('string'), Argument::type('bool'), NULL, ['masked' => $variable['masked'], 'variable_type' => $variable['variable_type']])->shouldBeCalled(); + protected function mockGitLabGroups(ObjectProphecy $gitlabClient): void + { + $groups = $this->prophet->prophesize(Groups::class); + $groups->all(Argument::type('array'))->willReturn([ + 0 => [ + 'auto_devops_enabled' => null, + 'avatar_url' => null, + 'created_at' => '2021-11-16T18:54:31.275Z', + 'default_branch_protection' => 2, + 'description' => '', + 'emails_disabled' => null, + 'full_name' => 'awesome-demo', + 'full_path' => 'awesome-demo', + 'id' => 47, + 'ldap_access' => null, + 'ldap_cn' => null, + 'lfs_enabled' => true, + 'marked_for_deletion_on' => null, + 'mentions_disabled' => null, + 'name' => 'awesome-demo', + 'parent_id' => null, + 'path' => 'awesome-demo', + 'project_creation_level' => 'developer', + 'request_access_enabled' => true, + 'require_two_factor_authentication' => false, + 'share_with_group_lock' => false, + 'subgroup_creation_level' => 'maintainer', + 'two_factor_grace_period' => 48, + 'visibility' => 'private', + 'web_url' => 'https://code.cloudservices.acquia.io/groups/awesome-demo', + ], + 1 => [ + 'auto_devops_enabled' => null, + 'avatar_url' => null, + 'created_at' => '2021-12-14T18:49:50.724Z', + 'default_branch_protection' => 2, + 'description' => '', + 'emails_disabled' => null, + 'full_name' => 'Nestle', + 'full_path' => 'nestle', + 'id' => 68, + 'ldap_access' => null, + 'ldap_cn' => null, + 'lfs_enabled' => true, + 'marked_for_deletion_on' => null, + 'mentions_disabled' => null, + 'name' => 'Nestle', + 'parent_id' => null, + 'path' => 'nestle', + 'project_creation_level' => 'developer', + 'request_access_enabled' => true, + 'require_two_factor_authentication' => false, + 'share_with_group_lock' => false, + 'subgroup_creation_level' => 'maintainer', + 'two_factor_grace_period' => 48, + 'visibility' => 'private', + 'web_url' => 'https://code.cloudservices.acquia.io/groups/nestle', + ], + ]); + $gitlabClient->groups()->willReturn($groups->reveal()); } - foreach ($variables as $variable) { - $projects->updateVariable($this->gitLabProjectId, $variable['key'], $variable['value'], FALSE, NULL, ['masked' => TRUE, 'variable_type' => 'env_var'])->shouldBeCalled(); + + protected function mockGitLabNamespaces(ObjectProphecy $gitlabClient): void + { + $namespaces = $this->prophet->prophesize(ProjectNamespaces::class); + $namespaces->show(Argument::type('string'))->willReturn([ + 'avatar_url' => 'https://secure.gravatar.com/avatar/5ee7b8ad954bf7156e6eb57a45d60dec?s=80&d=identicon', + 'billable_members_count' => 1, + 'full_path' => 'matthew.grasmick', + 'id' => 48, + 'kind' => 'user', + 'max_seats_used' => 0, + 'name' => 'Matthew Grasmick', + 'parent_id' => null, + 'path' => 'matthew.grasmick', + 'plan' => 'default', + 'seats_in_use' => 0, + 'trial' => false, + 'trial_ends_on' => null, + 'web_url' => 'https://code.cloudservices.acquia.io/matthew.grasmick', + ]); + $gitlabClient->namespaces()->willReturn($namespaces->reveal()); } - } + protected function mockGitLabVariables(int $gitlabProjectId, ObjectProphecy $projects): void + { + $variables = $this->getMockGitLabVariables(); + $projects->variables($gitlabProjectId)->willReturn($variables); + foreach ($variables as $variable) { + $projects->addVariable($gitlabProjectId, Argument::type('string'), Argument::type('string'), Argument::type('bool'), null, ['masked' => $variable['masked'], 'variable_type' => $variable['variable_type']])->shouldBeCalled(); + } + foreach ($variables as $variable) { + $projects->updateVariable($this->gitLabProjectId, $variable['key'], $variable['value'], false, null, ['masked' => true, 'variable_type' => 'env_var'])->shouldBeCalled(); + } + } } diff --git a/tests/phpunit/src/Commands/CommandBaseTest.php b/tests/phpunit/src/Commands/CommandBaseTest.php index 79c36c4ba..49e0d7853 100644 --- a/tests/phpunit/src/Commands/CommandBaseTest.php +++ b/tests/phpunit/src/Commands/CommandBaseTest.php @@ -1,6 +1,6 @@ injectCommand(LinkCommand::class); - } - - public function testUnauthenticatedFailure(): void { - $this->clientServiceProphecy->isMachineAuthenticated()->willReturn(FALSE); - $this->removeMockConfigFiles(); - - $inputs = [ - // Would you like to share anonymous performance usage and data? - 'n', - ]; - $this->expectException(AcquiaCliException::class); - $this->expectExceptionMessage('This machine is not yet authenticated with the Cloud Platform.'); - $this->executeCommand([], $inputs); - } - - public function testCloudAppFromLocalConfig(): void { - $this->command = $this->injectCommand(IdeListCommand::class); - $this->mockRequest('getApplicationByUuid', 'a47ac10b-58cc-4372-a567-0e02b2c3d470'); - $this->mockRequest('getApplicationIdes', 'a47ac10b-58cc-4372-a567-0e02b2c3d470'); - $this->createMockAcliConfigFile('a47ac10b-58cc-4372-a567-0e02b2c3d470'); - $this->executeCommand(); - - } - - /** - * @return string[][] - */ - public function providerTestCloudAppUuidArg(): array { - return [ - ['a47ac10b-58cc-4372-a567-0e02b2c3d470'], - ['165c887b-7633-4f64-799d-a5d4669c768e'], - ]; - } - - /** - * @dataProvider providerTestCloudAppUuidArg - * @group brokenProphecy - */ - public function testCloudAppUuidArg(string $uuid): void { - $this->mockApplicationRequest(); - $this->assertEquals($uuid, CommandBase::validateUuid($uuid)); - } - - /** - * @return array - */ - public function providerTestInvalidCloudAppUuidArg(): array { - return [ - ['a47ac10b-58cc-4372-a567-0e02b2c3d4', 'This value should have exactly 36 characters.'], - ['a47ac10b-58cc-4372-a567-0e02b2c3d47z', 'This is not a valid UUID.'], - ]; - } - - /** - * @dataProvider providerTestInvalidCloudAppUuidArg - */ - public function testInvalidCloudAppUuidArg(string $uuid, string $message): void { - $this->expectException(ValidatorException::class); - $this->expectExceptionMessage($message); - CommandBase::validateUuid($uuid); - } - - /** - * @return array - */ - public function providerTestInvalidCloudEnvironmentAlias(): array { - return [ - ['bl.a', 'This value is too short. It should have 5 characters or more.'], - ['blarg', 'You must enter either an environment ID or alias. Environment aliases must match the pattern [app-name].[env]'], - ['12345', 'You must enter either an environment ID or alias. Environment aliases must match the pattern [app-name].[env]'], - ]; - } - - /** - * @dataProvider providerTestInvalidCloudEnvironmentAlias - */ - public function testInvalidCloudEnvironmentAlias(string $alias, string $message): void { - $this->expectException(ValidatorException::class); - $this->expectExceptionMessage($message); - CommandBase::validateEnvironmentAlias($alias); - } - +class CommandBaseTest extends CommandTestBase +{ + /** + * @return \Acquia\Cli\Command\App\LinkCommand + */ + protected function createCommand(): CommandBase + { + return $this->injectCommand(LinkCommand::class); + } + + public function testUnauthenticatedFailure(): void + { + $this->clientServiceProphecy->isMachineAuthenticated()->willReturn(false); + $this->removeMockConfigFiles(); + + $inputs = [ + // Would you like to share anonymous performance usage and data? + 'n', + ]; + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage('This machine is not yet authenticated with the Cloud Platform.'); + $this->executeCommand([], $inputs); + } + + public function testCloudAppFromLocalConfig(): void + { + $this->command = $this->injectCommand(IdeListCommand::class); + $this->mockRequest('getApplicationByUuid', 'a47ac10b-58cc-4372-a567-0e02b2c3d470'); + $this->mockRequest('getApplicationIdes', 'a47ac10b-58cc-4372-a567-0e02b2c3d470'); + $this->createMockAcliConfigFile('a47ac10b-58cc-4372-a567-0e02b2c3d470'); + $this->executeCommand(); + } + + /** + * @return string[][] + */ + public function providerTestCloudAppUuidArg(): array + { + return [ + ['a47ac10b-58cc-4372-a567-0e02b2c3d470'], + ['165c887b-7633-4f64-799d-a5d4669c768e'], + ]; + } + + /** + * @dataProvider providerTestCloudAppUuidArg + * @group brokenProphecy + */ + public function testCloudAppUuidArg(string $uuid): void + { + $this->mockApplicationRequest(); + $this->assertEquals($uuid, CommandBase::validateUuid($uuid)); + } + + /** + * @return array + */ + public function providerTestInvalidCloudAppUuidArg(): array + { + return [ + ['a47ac10b-58cc-4372-a567-0e02b2c3d4', 'This value should have exactly 36 characters.'], + ['a47ac10b-58cc-4372-a567-0e02b2c3d47z', 'This is not a valid UUID.'], + ]; + } + + /** + * @dataProvider providerTestInvalidCloudAppUuidArg + */ + public function testInvalidCloudAppUuidArg(string $uuid, string $message): void + { + $this->expectException(ValidatorException::class); + $this->expectExceptionMessage($message); + CommandBase::validateUuid($uuid); + } + + /** + * @return array + */ + public function providerTestInvalidCloudEnvironmentAlias(): array + { + return [ + ['bl.a', 'This value is too short. It should have 5 characters or more.'], + ['blarg', 'You must enter either an environment ID or alias. Environment aliases must match the pattern [app-name].[env]'], + ['12345', 'You must enter either an environment ID or alias. Environment aliases must match the pattern [app-name].[env]'], + ]; + } + + /** + * @dataProvider providerTestInvalidCloudEnvironmentAlias + */ + public function testInvalidCloudEnvironmentAlias(string $alias, string $message): void + { + $this->expectException(ValidatorException::class); + $this->expectExceptionMessage($message); + CommandBase::validateEnvironmentAlias($alias); + } } diff --git a/tests/phpunit/src/Commands/DocsCommandTest.php b/tests/phpunit/src/Commands/DocsCommandTest.php index cb7f3e1cc..b69936a1a 100644 --- a/tests/phpunit/src/Commands/DocsCommandTest.php +++ b/tests/phpunit/src/Commands/DocsCommandTest.php @@ -1,6 +1,6 @@ injectCommand(DocsCommand::class); + } - protected function createCommand(): CommandBase { - return $this->injectCommand(DocsCommand::class); - } + /** + * @dataProvider providerTestDocsCommand + */ + public function testDocsCommand(int $input, string $expectedOutput): void + { + $localMachineHelper = $this->mockLocalMachineHelper(); + $localMachineHelper->startBrowser(Argument::any())->shouldBeCalled(); - /** - * @dataProvider providerTestDocsCommand - */ - public function testDocsCommand(int $input, string $expectedOutput): void { - $localMachineHelper = $this->mockLocalMachineHelper(); - $localMachineHelper->startBrowser(Argument::any())->shouldBeCalled(); + $this->executeCommand([], [$input]); + $output = $this->getDisplay(); + $this->assertStringContainsString('Select the Acquia Product [Acquia CLI]:', $output); + $this->assertStringContainsString($expectedOutput, $output); + } - $this->executeCommand([], [$input]); - $output = $this->getDisplay(); - $this->assertStringContainsString('Select the Acquia Product [Acquia CLI]:', $output); - $this->assertStringContainsString($expectedOutput, $output); - } - - /** - * @return array - */ - public function providerTestDocsCommand(): array { - return [ - [ + /** + * @return array + */ + public function providerTestDocsCommand(): array + { + return [ + [ 0, '[0 ] Acquia CLI', - ], - [ + ], + [ 1, '[1 ] Acquia CMS', - ], - [ + ], + [ 2, '[2 ] Acquia DAM Classic', - ], - [ + ], + [ 3, '[3 ] Acquia Migrate Accelerate', - ], - [ + ], + [ 4, '[4 ] BLT', - ], - [ + ], + [ 5, '[5 ] Campaign Factory', - ], - [ + ], + [ 6, '[6 ] Campaign Studio', - ], - [ + ], + [ 7, '[7 ] Cloud IDE', - ], - [ + ], + [ 8, '[8 ] Cloud Platform', - ], - [ + ], + [ 9, '[9 ] Code Studio', - ], - [ + ], + [ 10, '[10] Content Hub', - ], - [ + ], + [ 11, '[11] Customer Data Platform', - ], - [ + ], + [ 12, '[12] Edge', - ], - [ + ], + [ 13, '[13] Personalization', - ], - [ + ], + [ 14, '[14] Search', - ], - [ + ], + [ 15, '[15] Shield', - ], - [ + ], + [ 16, '[16] Site Factory', - ], - [ + ], + [ 17, '[17] Site Studio', - ], - ]; - } - + ], + ]; + } } diff --git a/tests/phpunit/src/Commands/Email/ConfigurePlatformEmailCommandTest.php b/tests/phpunit/src/Commands/Email/ConfigurePlatformEmailCommandTest.php index ad5ab4356..bd3caef50 100644 --- a/tests/phpunit/src/Commands/Email/ConfigurePlatformEmailCommandTest.php +++ b/tests/phpunit/src/Commands/Email/ConfigurePlatformEmailCommandTest.php @@ -1,6 +1,6 @@ injectCommand(ConfigurePlatformEmailCommand::class); - } - - public function setUp(): void { - parent::setUp(); - $this->setupFsFixture(); - $this->command = $this->createCommand(); - } - - /** - * @return array - */ - public function providerTestConfigurePlatformEmail(): array { - - return [ - [ + const ZONE_TEST_OUTPUT = "_acquiaplatform.example.com. 3600 IN TXT \"aGh54oW35sd5LMGhas1fWrnRrticnsdndf,43=\"\n" . + "_amazonses.example.com. 3600 IN TXT \"AB/CD4Hef1+c0D7+wYS2xQ+EBr3HZiXRWDJHrjEWOhs=\"\n" . + "abcdefgh1ijkl2mnopq34rstuvwxyz._domainkey.example.com. 3600 IN CNAME abcdefgh1ijkl2mnopq34rstuvwxyz.dkim.amazonses.com.\n" . + "abcdefgh1ijkl2mnopq34rstuvwxyz._domainkey.example.com. 3600 IN CNAME abcdefgh1ijkl2mnopq34rstuvwxyz.dkim.amazonses.com.\n" . + "abcdefgh1ijkl2mnopq34rstuvwxyz._domainkey.example.com. 3600 IN CNAME abcdefgh1ijkl2mnopq34rstuvwxyz.dkim.amazonses.com.\n" . + "mail.example.com. 3600 IN MX 10 feedback-smtp.us-east-1.amazonses.com.\n" . + "mail.example.com. 3600 IN TXT \"v=spf1 include:amazonses.com ~all\""; + + const YAML_TEST_OUTPUT = "-\n type: TXT\n name: _amazonses.example.com\n value: AB/CD4Hef1+c0D7+wYS2xQ+EBr3HZiXRWDJHrjEWOhs=\n" . + "-\n type: TXT\n name: _acquiaplatform.example.com\n value: 'aGh54oW35sd5LMGhas1fWrnRrticnsdndf,43='\n" . + "-\n type: MX\n name: mail.example.com\n value: '10 feedback-smtp.us-east-1.amazonses.com'\n" . + "-\n type: TXT\n name: mail.example.com\n value: 'v=spf1 include:amazonses.com ~all'\n" . + "-\n type: CNAME\n name: abcdefgh1ijkl2mnopq34rstuvwxyz._domainkey.example.com\n value: abcdefgh1ijkl2mnopq34rstuvwxyz.dkim.amazonses.com\n" . + "-\n type: CNAME\n name: abcdefgh1ijkl2mnopq34rstuvwxyz._domainkey.example.com\n value: abcdefgh1ijkl2mnopq34rstuvwxyz.dkim.amazonses.com\n" . + "-\n type: CNAME\n name: abcdefgh1ijkl2mnopq34rstuvwxyz._domainkey.example.com\n value: abcdefgh1ijkl2mnopq34rstuvwxyz.dkim.amazonses.com\n"; + + protected function createCommand(): CommandBase + { + return $this->injectCommand(ConfigurePlatformEmailCommand::class); + } + + public function setUp(): void + { + parent::setUp(); + $this->setupFsFixture(); + $this->command = $this->createCommand(); + } + + /** + * @return array + */ + public function providerTestConfigurePlatformEmail(): array + { + + return [ + [ 'test.com', 'zone', self::ZONE_TEST_OUTPUT, [ - // What's the domain name you'd like to register? - 'test.com', - // Select a Cloud Platform subscription. - '0', - // Would you like your DNS records in BIND Zone File, JSON, or YAML format? - '0', - // Have you finished providing the DNS records to your DNS provider? - 'y', - // What are the environments you'd like to enable email for? You may enter multiple separated by a comma. - '0', + // What's the domain name you'd like to register? + 'test.com', + // Select a Cloud Platform subscription. + '0', + // Would you like your DNS records in BIND Zone File, JSON, or YAML format? + '0', + // Have you finished providing the DNS records to your DNS provider? + 'y', + // What are the environments you'd like to enable email for? You may enter multiple separated by a comma. + '0', ], // Status code. 0, @@ -110,20 +113,20 @@ public function providerTestConfigurePlatformEmail(): array { ["You're all set to start using Platform Email!"], // Domain registration responses. "200", - ], - [ + ], + [ 'test.com', 'yaml', self::YAML_TEST_OUTPUT, [ - // What's the domain name you'd like to register? - 'test.com', - // Select a Cloud Platform subscription. - '0', - // Would you like your DNS records in BIND Zone File, JSON, or YAML format? - '1', - // Have you finished providing the DNS records to your DNS provider? - 'n', + // What's the domain name you'd like to register? + 'test.com', + // Select a Cloud Platform subscription. + '0', + // Would you like your DNS records in BIND Zone File, JSON, or YAML format? + '1', + // Have you finished providing the DNS records to your DNS provider? + 'n', ], // Status code. 1, @@ -131,22 +134,22 @@ public function providerTestConfigurePlatformEmail(): array { ["Make sure to give these records to your DNS provider"], // Domain registration responses. "404", - ], - [ + ], + [ 'test.com', 'json', self::JSON_TEST_OUTPUT, [ - // What's the domain name you'd like to register? - 'test.com', - // Select a Cloud Platform subscription. - '0', - // Would you like your DNS records in BIND Zone File, JSON, or YAML format? - '2', - // Have you finished providing the DNS records to your DNS provider? - 'y', - // Would you like to retry verification? - 'n', + // What's the domain name you'd like to register? + 'test.com', + // Select a Cloud Platform subscription. + '0', + // Would you like your DNS records in BIND Zone File, JSON, or YAML format? + '2', + // Have you finished providing the DNS records to your DNS provider? + 'y', + // Would you like to retry verification? + 'n', ], // Status code. 1, @@ -154,24 +157,24 @@ public function providerTestConfigurePlatformEmail(): array { ["Verification pending...", "Check your DNS records with your DNS provider"], // Domain registration responses. "202", - ], - [ + ], + [ 'test.com', 'zone', self::ZONE_TEST_OUTPUT, [ - // What's the domain name you'd like to register? - 'test.com', - // Select a Cloud Platform subscription. - '0', - // Would you like your DNS records in BIND Zone File, JSON, or YAML format? - '0', - // Have you finished providing the DNS records to your DNS provider? - 'y', - // Would you like to refresh? - 'y', - // Would you like to re-check domain verification? - 'n', + // What's the domain name you'd like to register? + 'test.com', + // Select a Cloud Platform subscription. + '0', + // Would you like your DNS records in BIND Zone File, JSON, or YAML format? + '0', + // Have you finished providing the DNS records to your DNS provider? + 'y', + // Would you like to refresh? + 'y', + // Would you like to re-check domain verification? + 'n', ], // Status code. 1, @@ -179,28 +182,29 @@ public function providerTestConfigurePlatformEmail(): array { ["Refreshing...", "Check your DNS records with your DNS provider"], // Domain registration responses. "404", - ], - ]; - } - - /** - * @return array - */ - public function providerTestConfigurePlatformEmailEnableEnv(): array { - return [ - [ + ], + ]; + } + + /** + * @return array + */ + public function providerTestConfigurePlatformEmailEnableEnv(): array + { + return [ + [ 'example.com', [ - // What's the domain name you'd like to register? - 'example.com', - // Select a Cloud Platform subscription. - '0', - // Would you like your DNS records in BIND Zone File, JSON, or YAML format? - '0', - // Have you finished providing the DNS records to your DNS provider? - 'y', - // What are the environments you'd like to enable email for? You may enter multiple separated by a comma. - '0', + // What's the domain name you'd like to register? + 'example.com', + // Select a Cloud Platform subscription. + '0', + // Would you like your DNS records in BIND Zone File, JSON, or YAML format? + '0', + // Have you finished providing the DNS records to your DNS provider? + 'y', + // What are the environments you'd like to enable email for? You may enter multiple separated by a comma. + '0', ], // Status code. 0, @@ -210,20 +214,20 @@ public function providerTestConfigurePlatformEmailEnableEnv(): array { 'Already enabled', // Expected text. ['already enabled', "You're all set to start using Platform Email!"], - ], - [ + ], + [ 'example.com', [ - // What's the domain name you'd like to register? - 'example.com', - // Select a Cloud Platform subscription. - '0', - // Would you like your DNS records in BIND Zone File, JSON, or YAML format? - '0', - // Have you finished providing the DNS records to your DNS provider? - 'y', - // What are the environments you'd like to enable email for? You may enter multiple separated by a comma. - '0', + // What's the domain name you'd like to register? + 'example.com', + // Select a Cloud Platform subscription. + '0', + // Would you like your DNS records in BIND Zone File, JSON, or YAML format? + '0', + // Have you finished providing the DNS records to your DNS provider? + 'y', + // What are the environments you'd like to enable email for? You may enter multiple separated by a comma. + '0', ], // Status code. 1, @@ -233,284 +237,285 @@ public function providerTestConfigurePlatformEmailEnableEnv(): array { 'No permission', // Expected text. ['You do not have permission', 'Something went wrong'], - ], - ]; - } - - /** - * @dataProvider providerTestConfigurePlatformEmail - */ - public function testConfigurePlatformEmail(mixed $baseDomain, mixed $fileDumpFormat, mixed $fileDump, mixed $inputs, mixed $expectedExitCode, mixed $expectedText, mixed $responseCode): void { - $localMachineHelper = $this->mockLocalMachineHelper(); - $mockFileSystem = $this->mockGetFilesystem($localMachineHelper); - - $subscriptionsResponse = $this->getMockResponseFromSpec('/subscriptions', 'get', '200'); - $this->clientProphecy->request('get', '/subscriptions') - ->willReturn($subscriptionsResponse->{'_embedded'}->items) - ->shouldBeCalledTimes(1); - - $postDomainsResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains', 'post', '200'); - $this->clientProphecy->request('post', "/subscriptions/{$subscriptionsResponse->_embedded->items[0]->uuid}/domains", [ - 'form_params' => [ + ], + ]; + } + + /** + * @dataProvider providerTestConfigurePlatformEmail + */ + public function testConfigurePlatformEmail(mixed $baseDomain, mixed $fileDumpFormat, mixed $fileDump, mixed $inputs, mixed $expectedExitCode, mixed $expectedText, mixed $responseCode): void + { + $localMachineHelper = $this->mockLocalMachineHelper(); + $mockFileSystem = $this->mockGetFilesystem($localMachineHelper); + + $subscriptionsResponse = $this->getMockResponseFromSpec('/subscriptions', 'get', '200'); + $this->clientProphecy->request('get', '/subscriptions') + ->willReturn($subscriptionsResponse->{'_embedded'}->items) + ->shouldBeCalledTimes(1); + + $postDomainsResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains', 'post', '200'); + $this->clientProphecy->request('post', "/subscriptions/{$subscriptionsResponse->_embedded->items[0]->uuid}/domains", [ + 'form_params' => [ 'domain' => $baseDomain, - ], - ])->willReturn($postDomainsResponse); + ], + ])->willReturn($postDomainsResponse); + + $getDomainsResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains', 'get', '200'); + $getDomainsResponse->_embedded->items[0]->domain_name = 'test.com'; + $this->clientProphecy->request('get', "/subscriptions/{$subscriptionsResponse->_embedded->items[0]->uuid}/domains")->willReturn($getDomainsResponse->_embedded->items); + + $domainsRegistrationResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains/{domainRegistrationUuid}', 'get', '200'); + $domainsRegistrationResponse->health->code = $responseCode; + $this->clientProphecy->request('get', "/subscriptions/{$subscriptionsResponse->_embedded->items[0]->uuid}/domains/{$getDomainsResponse->_embedded->items[0]->uuid}") + ->willReturn($domainsRegistrationResponse); + + $mockFileSystem->remove('dns-records.yaml')->shouldBeCalled(); + $mockFileSystem->remove('dns-records.json')->shouldBeCalled(); + $mockFileSystem->remove('dns-records.zone')->shouldBeCalled(); + + $mockFileSystem->dumpFile('dns-records.' . $fileDumpFormat, $fileDump)->shouldBeCalled(); + + if ($responseCode == '404') { + $reverifyResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains/{domainRegistrationUuid}/actions/verify', 'post', '200'); + $this->clientProphecy->request('post', "/subscriptions/{$subscriptionsResponse->_embedded->items[0]->uuid}/domains/{$getDomainsResponse->_embedded->items[0]->uuid}/actions/verify") + ->willReturn($reverifyResponse); + } elseif ($responseCode == '200') { + $applicationsResponse = $this->mockApplicationsRequest(); + // We need the application to belong to the subscription. + $applicationsResponse->_embedded->items[0]->subscription->uuid = $subscriptionsResponse->_embedded->items[0]->uuid; + + $associateResponse = $this->getMockResponseFromSpec('/applications/{applicationUuid}/email/domains/{domainRegistrationUuid}/actions/associate', 'post', '200'); + $this->clientProphecy->request('post', "/applications/{$applicationsResponse->_embedded->items[0]->uuid}/email/domains/{$getDomainsResponse->_embedded->items[0]->uuid}/actions/associate")->willReturn($associateResponse); + $environmentsResponse = $this->mockEnvironmentsRequest($applicationsResponse); + $enableResponse = $this->getMockResponseFromSpec('/environments/{environmentId}/email/actions/enable', 'post', '200'); + $this->clientProphecy->request('post', "/environments/{$environmentsResponse->_embedded->items[0]->id}/email/actions/enable")->willReturn($enableResponse); + } + + $this->executeCommand([], $inputs); + $output = $this->getDisplay(); + + $this->assertEquals($expectedExitCode, $this->getStatusCode()); + foreach ($expectedText as $text) { + $this->assertStringContainsString($text, $output); + } + } + + public function testConfigurePlatformEmailWithMultipleAppsAndEnvs(): void + { + $inputs = [ + // What's the domain name you'd like to register? + 'test.com', + // Select a Cloud Platform subscription. + '0', + // Would you like your DNS records in BIND Zone File, JSON, or YAML format? + '0', + // Have you finished providing the DNS records to your DNS provider? + 'y', + // What are the applications you'd like to associate this domain with? You may enter multiple separated by a comma. + '0,1', + // What are the environments you'd like to enable email for? You may enter multiple separated by a comma. - Application 0. + '0,1', + // What are the environments you'd like to enable email for? You may enter multiple separated by a comma. - Application 1. + '0', + ]; + $localMachineHelper = $this->mockLocalMachineHelper(); + $mockFileSystem = $this->mockGetFilesystem($localMachineHelper); + + $subscriptionsResponse = $this->getMockResponseFromSpec('/subscriptions', 'get', '200'); + $this->clientProphecy->request('get', '/subscriptions') + ->willReturn($subscriptionsResponse->{'_embedded'}->items) + ->shouldBeCalledTimes(1); + + $postDomainsResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains', 'post', '200'); + $this->clientProphecy->request('post', "/subscriptions/{$subscriptionsResponse->_embedded->items[0]->uuid}/domains", [ + 'form_params' => [ + 'domain' => 'test.com', + ], + ])->willReturn($postDomainsResponse); - $getDomainsResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains', 'get', '200'); - $getDomainsResponse->_embedded->items[0]->domain_name = 'test.com'; - $this->clientProphecy->request('get', "/subscriptions/{$subscriptionsResponse->_embedded->items[0]->uuid}/domains")->willReturn($getDomainsResponse->_embedded->items); + $getDomainsResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains', 'get', '200'); + $getDomainsResponse->_embedded->items[0]->domain_name = 'test.com'; + $this->clientProphecy->request('get', "/subscriptions/{$subscriptionsResponse->_embedded->items[0]->uuid}/domains")->willReturn($getDomainsResponse->_embedded->items); - $domainsRegistrationResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains/{domainRegistrationUuid}', 'get', '200'); - $domainsRegistrationResponse->health->code = $responseCode; - $this->clientProphecy->request('get', "/subscriptions/{$subscriptionsResponse->_embedded->items[0]->uuid}/domains/{$getDomainsResponse->_embedded->items[0]->uuid}") - ->willReturn($domainsRegistrationResponse); + $domainsRegistrationResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains/{domainRegistrationUuid}', 'get', '200'); + $domainsRegistrationResponse200 = $domainsRegistrationResponse; + $domainsRegistrationResponse200->health->code = '200'; + // Passing in two responses will return the first response the first time + // that the method is called, the second response the second time it is + // called, etc. + $this->clientProphecy->request('get', "/subscriptions/{$subscriptionsResponse->_embedded->items[0]->uuid}/domains/{$getDomainsResponse->_embedded->items[0]->uuid}")->willReturn($domainsRegistrationResponse, $domainsRegistrationResponse, $domainsRegistrationResponse200); - $mockFileSystem->remove('dns-records.yaml')->shouldBeCalled(); - $mockFileSystem->remove('dns-records.json')->shouldBeCalled(); - $mockFileSystem->remove('dns-records.zone')->shouldBeCalled(); + $mockFileSystem->remove('dns-records.yaml')->shouldBeCalled(); + $mockFileSystem->remove('dns-records.json')->shouldBeCalled(); + $mockFileSystem->remove('dns-records.zone')->shouldBeCalled(); + $mockFileSystem->dumpFile('dns-records.zone', self::ZONE_TEST_OUTPUT)->shouldBeCalled(); - $mockFileSystem->dumpFile('dns-records.' . $fileDumpFormat, $fileDump)->shouldBeCalled(); + $applicationsResponse = $this->mockApplicationsRequest(); + // We need the application to belong to the subscription. + $applicationsResponse->_embedded->items[0]->subscription->uuid = $subscriptionsResponse->_embedded->items[0]->uuid; + $applicationsResponse->_embedded->items[1]->subscription->uuid = $subscriptionsResponse->_embedded->items[0]->uuid; - if ($responseCode == '404') { - $reverifyResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains/{domainRegistrationUuid}/actions/verify', 'post', '200'); - $this->clientProphecy->request('post', "/subscriptions/{$subscriptionsResponse->_embedded->items[0]->uuid}/domains/{$getDomainsResponse->_embedded->items[0]->uuid}/actions/verify") - ->willReturn($reverifyResponse); - } - else if ($responseCode == '200') { - $applicationsResponse = $this->mockApplicationsRequest(); - // We need the application to belong to the subscription. - $applicationsResponse->_embedded->items[0]->subscription->uuid = $subscriptionsResponse->_embedded->items[0]->uuid; - - $associateResponse = $this->getMockResponseFromSpec('/applications/{applicationUuid}/email/domains/{domainRegistrationUuid}/actions/associate', 'post', '200'); - $this->clientProphecy->request('post', "/applications/{$applicationsResponse->_embedded->items[0]->uuid}/email/domains/{$getDomainsResponse->_embedded->items[0]->uuid}/actions/associate")->willReturn($associateResponse); - $environmentsResponse = $this->mockEnvironmentsRequest($applicationsResponse); - $enableResponse = $this->getMockResponseFromSpec('/environments/{environmentId}/email/actions/enable', 'post', '200'); - $this->clientProphecy->request('post', "/environments/{$environmentsResponse->_embedded->items[0]->id}/email/actions/enable")->willReturn($enableResponse); - } + $associateResponse = $this->getMockResponseFromSpec('/applications/{applicationUuid}/email/domains/{domainRegistrationUuid}/actions/associate', 'post', '200'); + $this->clientProphecy->request('post', "/applications/{$applicationsResponse->_embedded->items[0]->uuid}/email/domains/{$getDomainsResponse->_embedded->items[0]->uuid}/actions/associate")->willReturn($associateResponse); + $this->clientProphecy->request('post', "/applications/{$applicationsResponse->_embedded->items[1]->uuid}/email/domains/{$getDomainsResponse->_embedded->items[1]->uuid}/actions/associate")->willReturn($associateResponse); + + $environmentResponseApp1 = $this->getMockEnvironmentsResponse(); + $environmentResponseApp2 = $environmentResponseApp1; + + $this->clientProphecy->request('get', "/applications/{$applicationsResponse->_embedded->items[0]->uuid}/environments")->willReturn($environmentResponseApp1->_embedded->items); + $this->clientProphecy->request('get', "/applications/{$applicationsResponse->_embedded->items[1]->uuid}/environments")->willReturn($environmentResponseApp2->_embedded->items); + + $enableResponse = $this->getMockResponseFromSpec('/environments/{environmentId}/email/actions/enable', 'post', '200'); + $this->clientProphecy->request('post', "/environments/{$environmentResponseApp1->_embedded->items[0]->id}/email/actions/enable")->willReturn($enableResponse); + $this->clientProphecy->request('post', "/environments/{$environmentResponseApp1->_embedded->items[1]->id}/email/actions/enable")->willReturn($enableResponse); + + $this->clientProphecy->request('post', "/environments/{$environmentResponseApp2->_embedded->items[0]->id}/email/actions/enable")->willReturn($enableResponse); - $this->executeCommand([], $inputs); - $output = $this->getDisplay(); + $this->executeCommand([], $inputs); + $output = $this->getDisplay(); - $this->assertEquals($expectedExitCode, $this->getStatusCode()); - foreach ($expectedText as $text) { - $this->assertStringContainsString($text, $output); + $this->assertEquals(0, $this->getStatusCode()); + $this->assertStringContainsString("You're all set to start using Platform Email!", $output); } - } - - public function testConfigurePlatformEmailWithMultipleAppsAndEnvs(): void { - $inputs = [ - // What's the domain name you'd like to register? - 'test.com', - // Select a Cloud Platform subscription. - '0', - // Would you like your DNS records in BIND Zone File, JSON, or YAML format? - '0', - // Have you finished providing the DNS records to your DNS provider? - 'y', - // What are the applications you'd like to associate this domain with? You may enter multiple separated by a comma. - '0,1', - // What are the environments you'd like to enable email for? You may enter multiple separated by a comma. - Application 0. - '0,1', - // What are the environments you'd like to enable email for? You may enter multiple separated by a comma. - Application 1. - '0', - ]; - $localMachineHelper = $this->mockLocalMachineHelper(); - $mockFileSystem = $this->mockGetFilesystem($localMachineHelper); - - $subscriptionsResponse = $this->getMockResponseFromSpec('/subscriptions', 'get', '200'); - $this->clientProphecy->request('get', '/subscriptions') - ->willReturn($subscriptionsResponse->{'_embedded'}->items) - ->shouldBeCalledTimes(1); - - $postDomainsResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains', 'post', '200'); - $this->clientProphecy->request('post', "/subscriptions/{$subscriptionsResponse->_embedded->items[0]->uuid}/domains", [ - 'form_params' => [ - 'domain' => 'test.com', - ], - ])->willReturn($postDomainsResponse); - - $getDomainsResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains', 'get', '200'); - $getDomainsResponse->_embedded->items[0]->domain_name = 'test.com'; - $this->clientProphecy->request('get', "/subscriptions/{$subscriptionsResponse->_embedded->items[0]->uuid}/domains")->willReturn($getDomainsResponse->_embedded->items); - - $domainsRegistrationResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains/{domainRegistrationUuid}', 'get', '200'); - $domainsRegistrationResponse200 = $domainsRegistrationResponse; - $domainsRegistrationResponse200->health->code = '200'; - // Passing in two responses will return the first response the first time - // that the method is called, the second response the second time it is - // called, etc. - $this->clientProphecy->request('get', "/subscriptions/{$subscriptionsResponse->_embedded->items[0]->uuid}/domains/{$getDomainsResponse->_embedded->items[0]->uuid}")->willReturn($domainsRegistrationResponse, $domainsRegistrationResponse, $domainsRegistrationResponse200); - - $mockFileSystem->remove('dns-records.yaml')->shouldBeCalled(); - $mockFileSystem->remove('dns-records.json')->shouldBeCalled(); - $mockFileSystem->remove('dns-records.zone')->shouldBeCalled(); - $mockFileSystem->dumpFile('dns-records.zone', self::ZONE_TEST_OUTPUT)->shouldBeCalled(); - - $applicationsResponse = $this->mockApplicationsRequest(); - // We need the application to belong to the subscription. - $applicationsResponse->_embedded->items[0]->subscription->uuid = $subscriptionsResponse->_embedded->items[0]->uuid; - $applicationsResponse->_embedded->items[1]->subscription->uuid = $subscriptionsResponse->_embedded->items[0]->uuid; - - $associateResponse = $this->getMockResponseFromSpec('/applications/{applicationUuid}/email/domains/{domainRegistrationUuid}/actions/associate', 'post', '200'); - $this->clientProphecy->request('post', "/applications/{$applicationsResponse->_embedded->items[0]->uuid}/email/domains/{$getDomainsResponse->_embedded->items[0]->uuid}/actions/associate")->willReturn($associateResponse); - $this->clientProphecy->request('post', "/applications/{$applicationsResponse->_embedded->items[1]->uuid}/email/domains/{$getDomainsResponse->_embedded->items[1]->uuid}/actions/associate")->willReturn($associateResponse); - - $environmentResponseApp1 = $this->getMockEnvironmentsResponse(); - $environmentResponseApp2 = $environmentResponseApp1; - - $this->clientProphecy->request('get', "/applications/{$applicationsResponse->_embedded->items[0]->uuid}/environments")->willReturn($environmentResponseApp1->_embedded->items); - $this->clientProphecy->request('get', "/applications/{$applicationsResponse->_embedded->items[1]->uuid}/environments")->willReturn($environmentResponseApp2->_embedded->items); - - $enableResponse = $this->getMockResponseFromSpec('/environments/{environmentId}/email/actions/enable', 'post', '200'); - $this->clientProphecy->request('post', "/environments/{$environmentResponseApp1->_embedded->items[0]->id}/email/actions/enable")->willReturn($enableResponse); - $this->clientProphecy->request('post', "/environments/{$environmentResponseApp1->_embedded->items[1]->id}/email/actions/enable")->willReturn($enableResponse); - - $this->clientProphecy->request('post', "/environments/{$environmentResponseApp2->_embedded->items[0]->id}/email/actions/enable")->willReturn($enableResponse); - - $this->executeCommand([], $inputs); - $output = $this->getDisplay(); - - $this->assertEquals(0, $this->getStatusCode()); - $this->assertStringContainsString("You're all set to start using Platform Email!", $output); - - } - - public function testConfigurePlatformEmailNoApps(): void { - $localMachineHelper = $this->mockLocalMachineHelper(); - $mockFileSystem = $this->mockGetFilesystem($localMachineHelper); - - $baseDomain = 'test.com'; - $inputs = [ - // What's the domain name you'd like to register? - $baseDomain, - // Select a Cloud Platform subscription. - '0', - // Would you like your DNS records in BIND Zone File, JSON, or YAML format? - '0', - // Have you finished providing the DNS records to your DNS provider? - 'y', - ]; - - $subscriptionsResponse = $this->getMockResponseFromSpec('/subscriptions', 'get', '200'); - $this->clientProphecy->request('get', '/subscriptions') - ->willReturn($subscriptionsResponse->{'_embedded'}->items) - ->shouldBeCalledTimes(1); - - $postDomainsResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains', 'post', '200'); - $this->clientProphecy->request('post', "/subscriptions/{$subscriptionsResponse->_embedded->items[0]->uuid}/domains", [ - 'form_params' => [ - 'domain' => $baseDomain, - ], - ])->willReturn($postDomainsResponse); - - $getDomainsResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains', 'get', '200'); - $getDomainsResponse->_embedded->items[0]->domain_name = 'test.com'; - $this->clientProphecy->request('get', "/subscriptions/{$subscriptionsResponse->_embedded->items[0]->uuid}/domains")->willReturn($getDomainsResponse->_embedded->items); - - $domainsRegistrationResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains/{domainRegistrationUuid}', 'get', '200'); - $domainsRegistrationResponse200 = $domainsRegistrationResponse; - $domainsRegistrationResponse200->health->code = '200'; - - $this->clientProphecy->request('get', "/subscriptions/{$subscriptionsResponse->_embedded->items[0]->uuid}/domains/{$getDomainsResponse->_embedded->items[0]->uuid}")->willReturn($domainsRegistrationResponse200); - - $mockFileSystem->remove('dns-records.yaml')->shouldBeCalled(); - $mockFileSystem->remove('dns-records.json')->shouldBeCalled(); - $mockFileSystem->remove('dns-records.zone')->shouldBeCalled(); - - $mockFileSystem->dumpFile('dns-records.zone', self::ZONE_TEST_OUTPUT)->shouldBeCalled(); - $applicationsResponse = $this->mockApplicationsRequest(); - - $this->expectException(AcquiaCliException::class); - $this->expectExceptionMessage('You do not have access to any applications'); - $this->executeCommand([], $inputs); - - $output = $this->getDisplay(); - - $this->assertStringNotContainsString("You're all set to start using Platform Email!", $output); - } - - public function testConfigurePlatformEmailWithNoDomainMatch(): void { - $baseDomain = 'test.com'; - $inputs = [ - // What's the domain name you'd like to register? - $baseDomain, - // Select a Cloud Platform subscription. - '0', - // Would you like your DNS records in BIND Zone File, JSON, or YAML format? - '0', - // Have you finished providing the DNS records to your DNS provider? - 'y', - ]; - - $subscriptionsResponse = $this->getMockResponseFromSpec('/subscriptions', 'get', '200'); - $this->clientProphecy->request('get', '/subscriptions') - ->willReturn($subscriptionsResponse->{'_embedded'}->items) - ->shouldBeCalledTimes(1); - - $postDomainsResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains', 'post', '200'); - $this->clientProphecy->request('post', "/subscriptions/{$subscriptionsResponse->_embedded->items[0]->uuid}/domains", [ - 'form_params' => [ + public function testConfigurePlatformEmailNoApps(): void + { + $localMachineHelper = $this->mockLocalMachineHelper(); + $mockFileSystem = $this->mockGetFilesystem($localMachineHelper); + + $baseDomain = 'test.com'; + $inputs = [ + // What's the domain name you'd like to register? + $baseDomain, + // Select a Cloud Platform subscription. + '0', + // Would you like your DNS records in BIND Zone File, JSON, or YAML format? + '0', + // Have you finished providing the DNS records to your DNS provider? + 'y', + ]; + + $subscriptionsResponse = $this->getMockResponseFromSpec('/subscriptions', 'get', '200'); + $this->clientProphecy->request('get', '/subscriptions') + ->willReturn($subscriptionsResponse->{'_embedded'}->items) + ->shouldBeCalledTimes(1); + + $postDomainsResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains', 'post', '200'); + $this->clientProphecy->request('post', "/subscriptions/{$subscriptionsResponse->_embedded->items[0]->uuid}/domains", [ + 'form_params' => [ 'domain' => $baseDomain, - ], - ])->willReturn($postDomainsResponse); - - $getDomainsResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains', 'get', '200'); - $getDomainsResponse->_embedded->items[0]->domain_name = 'mismatch-test.com'; - $this->clientProphecy->request('get', "/subscriptions/{$subscriptionsResponse->_embedded->items[0]->uuid}/domains")->willReturn($getDomainsResponse->_embedded->items); - - $this->expectException(AcquiaCliException::class); - $this->expectExceptionMessage('Could not find domain'); - $this->executeCommand([], $inputs); - - } - - public function testConfigurePlatformEmailWithErrorRetrievingDomainHealth(): void { - $baseDomain = 'test.com'; - $inputs = [ - // What's the domain name you'd like to register? - $baseDomain, - // Select a Cloud Platform subscription. - '0', - // Would you like your DNS records in BIND Zone File, JSON, or YAML format? - '0', - // Have you finished providing the DNS records to your DNS provider? - 'y', - ]; - - $subscriptionsResponse = $this->getMockResponseFromSpec('/subscriptions', 'get', '200'); - $this->clientProphecy->request('get', '/subscriptions') - ->willReturn($subscriptionsResponse->{'_embedded'}->items) - ->shouldBeCalledTimes(1); - - $postDomainsResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains', 'post', '200'); - $this->clientProphecy->request('post', "/subscriptions/{$subscriptionsResponse->_embedded->items[0]->uuid}/domains", [ - 'form_params' => [ + ], + ])->willReturn($postDomainsResponse); + + $getDomainsResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains', 'get', '200'); + $getDomainsResponse->_embedded->items[0]->domain_name = 'test.com'; + $this->clientProphecy->request('get', "/subscriptions/{$subscriptionsResponse->_embedded->items[0]->uuid}/domains")->willReturn($getDomainsResponse->_embedded->items); + + $domainsRegistrationResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains/{domainRegistrationUuid}', 'get', '200'); + $domainsRegistrationResponse200 = $domainsRegistrationResponse; + $domainsRegistrationResponse200->health->code = '200'; + + $this->clientProphecy->request('get', "/subscriptions/{$subscriptionsResponse->_embedded->items[0]->uuid}/domains/{$getDomainsResponse->_embedded->items[0]->uuid}")->willReturn($domainsRegistrationResponse200); + + $mockFileSystem->remove('dns-records.yaml')->shouldBeCalled(); + $mockFileSystem->remove('dns-records.json')->shouldBeCalled(); + $mockFileSystem->remove('dns-records.zone')->shouldBeCalled(); + + $mockFileSystem->dumpFile('dns-records.zone', self::ZONE_TEST_OUTPUT)->shouldBeCalled(); + $applicationsResponse = $this->mockApplicationsRequest(); + + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage('You do not have access to any applications'); + $this->executeCommand([], $inputs); + + $output = $this->getDisplay(); + + $this->assertStringNotContainsString("You're all set to start using Platform Email!", $output); + } + + public function testConfigurePlatformEmailWithNoDomainMatch(): void + { + $baseDomain = 'test.com'; + $inputs = [ + // What's the domain name you'd like to register? + $baseDomain, + // Select a Cloud Platform subscription. + '0', + // Would you like your DNS records in BIND Zone File, JSON, or YAML format? + '0', + // Have you finished providing the DNS records to your DNS provider? + 'y', + ]; + + $subscriptionsResponse = $this->getMockResponseFromSpec('/subscriptions', 'get', '200'); + $this->clientProphecy->request('get', '/subscriptions') + ->willReturn($subscriptionsResponse->{'_embedded'}->items) + ->shouldBeCalledTimes(1); + + $postDomainsResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains', 'post', '200'); + $this->clientProphecy->request('post', "/subscriptions/{$subscriptionsResponse->_embedded->items[0]->uuid}/domains", [ + 'form_params' => [ 'domain' => $baseDomain, - ], - ])->willReturn($postDomainsResponse); + ], + ])->willReturn($postDomainsResponse); + + $getDomainsResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains', 'get', '200'); + $getDomainsResponse->_embedded->items[0]->domain_name = 'mismatch-test.com'; + $this->clientProphecy->request('get', "/subscriptions/{$subscriptionsResponse->_embedded->items[0]->uuid}/domains")->willReturn($getDomainsResponse->_embedded->items); + + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage('Could not find domain'); + $this->executeCommand([], $inputs); + } - $getDomainsResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains', 'get', '200'); - $getDomainsResponse->_embedded->items[0]->domain_name = 'test.com'; - $this->clientProphecy->request('get', "/subscriptions/{$subscriptionsResponse->_embedded->items[0]->uuid}/domains")->willReturn($getDomainsResponse->_embedded->items); + public function testConfigurePlatformEmailWithErrorRetrievingDomainHealth(): void + { + $baseDomain = 'test.com'; + $inputs = [ + // What's the domain name you'd like to register? + $baseDomain, + // Select a Cloud Platform subscription. + '0', + // Would you like your DNS records in BIND Zone File, JSON, or YAML format? + '0', + // Have you finished providing the DNS records to your DNS provider? + 'y', + ]; - $domainsRegistrationResponse404 = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains/{domainRegistrationUuid}', 'get', '404'); + $subscriptionsResponse = $this->getMockResponseFromSpec('/subscriptions', 'get', '200'); + $this->clientProphecy->request('get', '/subscriptions') + ->willReturn($subscriptionsResponse->{'_embedded'}->items) + ->shouldBeCalledTimes(1); - $this->clientProphecy->request('get', "/subscriptions/{$subscriptionsResponse->_embedded->items[0]->uuid}/domains/{$getDomainsResponse->_embedded->items[0]->uuid}")->willReturn($domainsRegistrationResponse404); - $this->expectException(AcquiaCliException::class); - $this->expectExceptionMessage('Could not retrieve DNS records for this domain'); - $this->executeCommand([], $inputs); + $postDomainsResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains', 'post', '200'); + $this->clientProphecy->request('post', "/subscriptions/{$subscriptionsResponse->_embedded->items[0]->uuid}/domains", [ + 'form_params' => [ + 'domain' => $baseDomain, + ], + ])->willReturn($postDomainsResponse); - } + $getDomainsResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains', 'get', '200'); + $getDomainsResponse->_embedded->items[0]->domain_name = 'test.com'; + $this->clientProphecy->request('get', "/subscriptions/{$subscriptionsResponse->_embedded->items[0]->uuid}/domains")->willReturn($getDomainsResponse->_embedded->items); - /** - * Tests the exported JSON file output when running email:configure, ensuring that slashes are encoded correctly. - */ - public function testConfigurePlatformEmailJsonOutput(): void { - $localMachineHelper = $this->mockLocalMachineHelper(); - $mockFileSystem = $this->mockGetFilesystem($localMachineHelper); + $domainsRegistrationResponse404 = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains/{domainRegistrationUuid}', 'get', '404'); - $inputs = [ + $this->clientProphecy->request('get', "/subscriptions/{$subscriptionsResponse->_embedded->items[0]->uuid}/domains/{$getDomainsResponse->_embedded->items[0]->uuid}")->willReturn($domainsRegistrationResponse404); + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage('Could not retrieve DNS records for this domain'); + $this->executeCommand([], $inputs); + } + + /** + * Tests the exported JSON file output when running email:configure, ensuring that slashes are encoded correctly. + */ + public function testConfigurePlatformEmailJsonOutput(): void + { + $localMachineHelper = $this->mockLocalMachineHelper(); + $mockFileSystem = $this->mockGetFilesystem($localMachineHelper); + + $inputs = [ // What's the domain name you'd like to register? 'test.com', // Select a Cloud Platform subscription. @@ -521,115 +526,115 @@ public function testConfigurePlatformEmailJsonOutput(): void { 'y', // What are the environments you'd like to enable email for? You may enter multiple separated by a comma. '0', - ]; - $subscriptionsResponse = $this->getMockResponseFromSpec('/subscriptions', 'get', '200'); - $this->clientProphecy->request('get', '/subscriptions') - ->willReturn($subscriptionsResponse->{'_embedded'}->items); - - $postDomainsResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains', 'post', '200'); - $this->clientProphecy->request('post', "/subscriptions/{$subscriptionsResponse->_embedded->items[0]->uuid}/domains", [ - 'form_params' => [ + ]; + $subscriptionsResponse = $this->getMockResponseFromSpec('/subscriptions', 'get', '200'); + $this->clientProphecy->request('get', '/subscriptions') + ->willReturn($subscriptionsResponse->{'_embedded'}->items); + + $postDomainsResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains', 'post', '200'); + $this->clientProphecy->request('post', "/subscriptions/{$subscriptionsResponse->_embedded->items[0]->uuid}/domains", [ + 'form_params' => [ 'domain' => 'test.com', - ], - ])->willReturn($postDomainsResponse); - - $getDomainsResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains', 'get', '200'); - $getDomainsResponse->_embedded->items[0]->domain_name = 'test.com'; - $this->clientProphecy->request('get', "/subscriptions/{$subscriptionsResponse->_embedded->items[0]->uuid}/domains")->willReturn($getDomainsResponse->_embedded->items); - - $domainsRegistrationResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains/{domainRegistrationUuid}', 'get', '200'); - $domainsRegistrationResponse200 = $domainsRegistrationResponse; - $domainsRegistrationResponse200->health->code = '200'; - - $this->clientProphecy->request('get', "/subscriptions/{$subscriptionsResponse->_embedded->items[0]->uuid}/domains/{$getDomainsResponse->_embedded->items[0]->uuid}")->willReturn($domainsRegistrationResponse200); - $mockFileSystem->remove('dns-records.yaml')->shouldBeCalled(); - $mockFileSystem->remove('dns-records.json')->shouldBeCalled(); - $mockFileSystem->remove('dns-records.zone')->shouldBeCalled(); - $mockFileSystem->dumpFile('dns-records.json', self::JSON_TEST_OUTPUT)->shouldBeCalled(); - $applicationsResponse = $this->mockApplicationsRequest(); - - $appDomainsResponse = $this->getMockResponseFromSpec('/applications/{applicationUuid}/email/domains', 'get', '200'); - $appDomainsResponse->_embedded->items[0]->domain_name = 'test.com'; - $this->clientProphecy->request('get', "/applications/{$applicationsResponse->_embedded->items[0]->uuid}/email/domains")->willReturn($appDomainsResponse->_embedded->items); - // We need the application to belong to the subscription. - $applicationsResponse->_embedded->items[0]->subscription->uuid = $subscriptionsResponse->_embedded->items[0]->uuid; - - $associateResponse = $this->getMockResponseFromSpec('/applications/{applicationUuid}/email/domains/{domainRegistrationUuid}/actions/associate', 'post', '200'); - $this->clientProphecy->request('post', "/applications/{$applicationsResponse->_embedded->items[0]->uuid}/email/domains/{$getDomainsResponse->_embedded->items[0]->uuid}/actions/associate")->willReturn($associateResponse); - - $environmentsResponse = $this->mockEnvironmentsRequest($applicationsResponse); - $enableResponse = $this->getMockResponseFromSpec('/environments/{environmentId}/email/actions/enable', 'post', '200'); - $this->clientProphecy->request('post', "/environments/{$environmentsResponse->_embedded->items[0]->id}/email/actions/enable")->willReturn($enableResponse); - - $this->executeCommand([], $inputs); - $output = $this->getDisplay(); - - $this->assertEquals('0', $this->getStatusCode()); - $this->assertStringContainsString('all set', $output); - } - - /** - * @dataProvider providerTestConfigurePlatformEmailEnableEnv - */ - public function testConfigurePlatformEmailWithAlreadyEnabledEnvs(mixed $baseDomain, mixed $inputs, mixed $expectedExitCode, mixed $responseCode, mixed $specKey, mixed $expectedText): void { - $subscriptionsResponse = $this->getMockResponseFromSpec('/subscriptions', 'get', '200'); - $this->clientProphecy->request('get', '/subscriptions') - ->willReturn($subscriptionsResponse->{'_embedded'}->items) - ->shouldBeCalledTimes(1); - - $postDomainsResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains', 'post', '200'); - $this->clientProphecy->request('post', "/subscriptions/{$subscriptionsResponse->_embedded->items[0]->uuid}/domains", [ - 'form_params' => [ - 'domain' => $baseDomain, - ], - ])->willReturn($postDomainsResponse); + ], + ])->willReturn($postDomainsResponse); - $getDomainsResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains', 'get', '200'); - $getDomainsResponse->_embedded->items[0]->domain_name = 'example.com'; - $this->clientProphecy->request('get', "/subscriptions/{$subscriptionsResponse->_embedded->items[0]->uuid}/domains")->willReturn($getDomainsResponse->_embedded->items); + $getDomainsResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains', 'get', '200'); + $getDomainsResponse->_embedded->items[0]->domain_name = 'test.com'; + $this->clientProphecy->request('get', "/subscriptions/{$subscriptionsResponse->_embedded->items[0]->uuid}/domains")->willReturn($getDomainsResponse->_embedded->items); - $domainsRegistrationResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains/{domainRegistrationUuid}', 'get', '200'); - $domainsRegistrationResponse200 = $domainsRegistrationResponse; - $domainsRegistrationResponse200->health->code = '200'; + $domainsRegistrationResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains/{domainRegistrationUuid}', 'get', '200'); + $domainsRegistrationResponse200 = $domainsRegistrationResponse; + $domainsRegistrationResponse200->health->code = '200'; - $this->clientProphecy->request('get', "/subscriptions/{$subscriptionsResponse->_embedded->items[0]->uuid}/domains/{$getDomainsResponse->_embedded->items[0]->uuid}")->willReturn($domainsRegistrationResponse200); + $this->clientProphecy->request('get', "/subscriptions/{$subscriptionsResponse->_embedded->items[0]->uuid}/domains/{$getDomainsResponse->_embedded->items[0]->uuid}")->willReturn($domainsRegistrationResponse200); + $mockFileSystem->remove('dns-records.yaml')->shouldBeCalled(); + $mockFileSystem->remove('dns-records.json')->shouldBeCalled(); + $mockFileSystem->remove('dns-records.zone')->shouldBeCalled(); + $mockFileSystem->dumpFile('dns-records.json', self::JSON_TEST_OUTPUT)->shouldBeCalled(); + $applicationsResponse = $this->mockApplicationsRequest(); - $applicationsResponse = $this->mockApplicationsRequest(); + $appDomainsResponse = $this->getMockResponseFromSpec('/applications/{applicationUuid}/email/domains', 'get', '200'); + $appDomainsResponse->_embedded->items[0]->domain_name = 'test.com'; + $this->clientProphecy->request('get', "/applications/{$applicationsResponse->_embedded->items[0]->uuid}/email/domains")->willReturn($appDomainsResponse->_embedded->items); + // We need the application to belong to the subscription. + $applicationsResponse->_embedded->items[0]->subscription->uuid = $subscriptionsResponse->_embedded->items[0]->uuid; - $appDomainsResponse = $this->getMockResponseFromSpec('/applications/{applicationUuid}/email/domains', 'get', '200'); - $appDomainsResponse->_embedded->items[0]->domain_name = 'example.com'; - $this->clientProphecy->request('get', "/applications/{$applicationsResponse->_embedded->items[0]->uuid}/email/domains")->willReturn($appDomainsResponse->_embedded->items); - // We need the application to belong to the subscription. - $applicationsResponse->_embedded->items[0]->subscription->uuid = $subscriptionsResponse->_embedded->items[0]->uuid; + $associateResponse = $this->getMockResponseFromSpec('/applications/{applicationUuid}/email/domains/{domainRegistrationUuid}/actions/associate', 'post', '200'); + $this->clientProphecy->request('post', "/applications/{$applicationsResponse->_embedded->items[0]->uuid}/email/domains/{$getDomainsResponse->_embedded->items[0]->uuid}/actions/associate")->willReturn($associateResponse); - $associateResponse = $this->getMockResponseFromSpec('/applications/{applicationUuid}/email/domains/{domainRegistrationUuid}/actions/associate', 'post', '409'); + $environmentsResponse = $this->mockEnvironmentsRequest($applicationsResponse); + $enableResponse = $this->getMockResponseFromSpec('/environments/{environmentId}/email/actions/enable', 'post', '200'); + $this->clientProphecy->request('post', "/environments/{$environmentsResponse->_embedded->items[0]->id}/email/actions/enable")->willReturn($enableResponse); - $this->clientProphecy->request('post', "/applications/{$applicationsResponse->_embedded->items[0]->uuid}/email/domains/{$getDomainsResponse->_embedded->items[0]->uuid}/actions/associate") - ->willThrow(new ApiErrorException($associateResponse->{'Already associated'}->value)); + $this->executeCommand([], $inputs); + $output = $this->getDisplay(); - $environmentsResponse = $this->mockEnvironmentsRequest($applicationsResponse); - $enableResponse = $this->getMockResponseFromSpec('/environments/{environmentId}/email/actions/enable', 'post', $responseCode); - $this->clientProphecy->request('post', "/environments/{$environmentsResponse->_embedded->items[0]->id}/email/actions/enable") - ->willThrow(new ApiErrorException($enableResponse->{$specKey}->value)); + $this->assertEquals('0', $this->getStatusCode()); + $this->assertStringContainsString('all set', $output); + } - $this->executeCommand([], $inputs); - $output = $this->getDisplay(); + /** + * @dataProvider providerTestConfigurePlatformEmailEnableEnv + */ + public function testConfigurePlatformEmailWithAlreadyEnabledEnvs(mixed $baseDomain, mixed $inputs, mixed $expectedExitCode, mixed $responseCode, mixed $specKey, mixed $expectedText): void + { + $subscriptionsResponse = $this->getMockResponseFromSpec('/subscriptions', 'get', '200'); + $this->clientProphecy->request('get', '/subscriptions') + ->willReturn($subscriptionsResponse->{'_embedded'}->items) + ->shouldBeCalledTimes(1); + + $postDomainsResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains', 'post', '200'); + $this->clientProphecy->request('post', "/subscriptions/{$subscriptionsResponse->_embedded->items[0]->uuid}/domains", [ + 'form_params' => [ + 'domain' => $baseDomain, + ], + ])->willReturn($postDomainsResponse); - $this->assertEquals($expectedExitCode, $this->getStatusCode()); - foreach ($expectedText as $text) { - $this->assertStringContainsString($text, $output); - } + $getDomainsResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains', 'get', '200'); + $getDomainsResponse->_embedded->items[0]->domain_name = 'example.com'; + $this->clientProphecy->request('get', "/subscriptions/{$subscriptionsResponse->_embedded->items[0]->uuid}/domains")->willReturn($getDomainsResponse->_embedded->items); - } + $domainsRegistrationResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains/{domainRegistrationUuid}', 'get', '200'); + $domainsRegistrationResponse200 = $domainsRegistrationResponse; + $domainsRegistrationResponse200->health->code = '200'; - /** - * @return \Symfony\Component\Filesystem\Filesystem|\Prophecy\Prophecy\ObjectProphecy - */ - protected function mockGetFilesystem(ObjectProphecy|LocalMachineHelper $localMachineHelper): Filesystem|ObjectProphecy { - $fileSystem = $this->prophet->prophesize(Filesystem::class); - $localMachineHelper->getFilesystem()->willReturn($fileSystem->reveal())->shouldBeCalled(); + $this->clientProphecy->request('get', "/subscriptions/{$subscriptionsResponse->_embedded->items[0]->uuid}/domains/{$getDomainsResponse->_embedded->items[0]->uuid}")->willReturn($domainsRegistrationResponse200); - return $fileSystem; - } + $applicationsResponse = $this->mockApplicationsRequest(); + $appDomainsResponse = $this->getMockResponseFromSpec('/applications/{applicationUuid}/email/domains', 'get', '200'); + $appDomainsResponse->_embedded->items[0]->domain_name = 'example.com'; + $this->clientProphecy->request('get', "/applications/{$applicationsResponse->_embedded->items[0]->uuid}/email/domains")->willReturn($appDomainsResponse->_embedded->items); + // We need the application to belong to the subscription. + $applicationsResponse->_embedded->items[0]->subscription->uuid = $subscriptionsResponse->_embedded->items[0]->uuid; + + $associateResponse = $this->getMockResponseFromSpec('/applications/{applicationUuid}/email/domains/{domainRegistrationUuid}/actions/associate', 'post', '409'); + + $this->clientProphecy->request('post', "/applications/{$applicationsResponse->_embedded->items[0]->uuid}/email/domains/{$getDomainsResponse->_embedded->items[0]->uuid}/actions/associate") + ->willThrow(new ApiErrorException($associateResponse->{'Already associated'}->value)); + + $environmentsResponse = $this->mockEnvironmentsRequest($applicationsResponse); + $enableResponse = $this->getMockResponseFromSpec('/environments/{environmentId}/email/actions/enable', 'post', $responseCode); + $this->clientProphecy->request('post', "/environments/{$environmentsResponse->_embedded->items[0]->id}/email/actions/enable") + ->willThrow(new ApiErrorException($enableResponse->{$specKey}->value)); + + $this->executeCommand([], $inputs); + $output = $this->getDisplay(); + + $this->assertEquals($expectedExitCode, $this->getStatusCode()); + foreach ($expectedText as $text) { + $this->assertStringContainsString($text, $output); + } + } + + /** + * @return \Symfony\Component\Filesystem\Filesystem|\Prophecy\Prophecy\ObjectProphecy + */ + protected function mockGetFilesystem(ObjectProphecy|LocalMachineHelper $localMachineHelper): Filesystem|ObjectProphecy + { + $fileSystem = $this->prophet->prophesize(Filesystem::class); + $localMachineHelper->getFilesystem()->willReturn($fileSystem->reveal())->shouldBeCalled(); + + return $fileSystem; + } } diff --git a/tests/phpunit/src/Commands/Email/EmailInfoForSubscriptionCommandTest.php b/tests/phpunit/src/Commands/Email/EmailInfoForSubscriptionCommandTest.php index 54df9b283..0214457a6 100644 --- a/tests/phpunit/src/Commands/Email/EmailInfoForSubscriptionCommandTest.php +++ b/tests/phpunit/src/Commands/Email/EmailInfoForSubscriptionCommandTest.php @@ -1,6 +1,6 @@ injectCommand(EmailInfoForSubscriptionCommand::class); - } - - public function setUp(): void { - parent::setUp(); - $this->setupFsFixture(); - $this->command = $this->createCommand(); - } - - public function testEmailInfoForSubscription(): void { - $inputs = [ - // Select a Cloud Platform subscription. - 0, - ]; - $subscriptions = $this->mockRequest('getSubscriptions'); - - $getDomainsResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains', 'get', '200'); - // Duplicating the request to ensure there is at least one domain with a successful, pending, and failed health code. - $getDomainsResponse2 = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains', 'get', '200'); - $totalDomainsList = array_merge($getDomainsResponse->_embedded->items, $getDomainsResponse2->_embedded->items); - $this->clientProphecy->request('get', "/subscriptions/{$subscriptions[0]->uuid}/domains")->willReturn($totalDomainsList); - - $totalDomainsList[2]->domain_name = 'example3.com'; - $totalDomainsList[2]->health->code = '200'; - - $totalDomainsList[3]->domain_name = 'example4.com'; - $totalDomainsList[3]->health->code = '202'; - - $applicationsResponse = $this->mockApplicationsRequest(); - - $applicationsResponse->_embedded->items[0]->subscription->uuid = $subscriptions[0]->uuid; - - $getAppDomainsResponse = $this->getMockResponseFromSpec('/applications/{applicationUuid}/email/domains', 'get', '200'); - // Duplicating the request to ensure added domains are included in association list. - $getAppDomainsResponse2 = $this->getMockResponseFromSpec('/applications/{applicationUuid}/email/domains', 'get', '200'); - $totalAppDomainsList = array_merge($getAppDomainsResponse->_embedded->items, $getAppDomainsResponse2->_embedded->items); - $this->clientProphecy->request('get', "/applications/{$applicationsResponse->_embedded->items[0]->uuid}/email/domains")->willReturn($totalAppDomainsList); - - $totalAppDomainsList[2]->domain_name = 'example3.com'; - $totalAppDomainsList[2]->flags->associated = TRUE; - - $totalAppDomainsList[3]->domain_name = 'example4.com'; - $totalAppDomainsList[3]->flags->associated = FALSE; - - $this->executeCommand([], $inputs); - $output = $this->getDisplay(); - $this->assertEquals(0, $this->getStatusCode()); - $this->assertStringContainsString('Application: ', $output); - foreach ($getAppDomainsResponse->_embedded->items as $appDomain) { - $this->assertEquals(3, substr_count($output, $appDomain->domain_name)); +class EmailInfoForSubscriptionCommandTest extends CommandTestBase +{ + protected function createCommand(): CommandBase + { + return $this->injectCommand(EmailInfoForSubscriptionCommand::class); } - $this->assertEquals(2, substr_count($output, 'Failed - 404')); - $this->assertEquals(1, substr_count($output, 'Pending - 202')); - $this->assertEquals(1, substr_count($output, 'Succeeded - 200')); - - $this->assertEquals(3, substr_count($output, 'true')); - $this->assertEquals(1, substr_count($output, 'false')); - } - - public function testEmailInfoForSubscriptionNoApps(): void { - $inputs = [ - // Select a Cloud Platform subscription. - 0, - ]; - $subscriptions = $this->mockRequest('getSubscriptions'); - - $getDomainsResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains', 'get', '200'); - $this->clientProphecy->request('get', "/subscriptions/{$subscriptions[0]->uuid}/domains")->willReturn($getDomainsResponse->_embedded->items); - - $this->mockRequest('getApplications'); - - $this->expectException(AcquiaCliException::class); - $this->expectExceptionMessage('You do not have access'); - $this->executeCommand([], $inputs); - } - - public function testEmailInfoForSubscriptionWith101Apps(): void { - $inputs = [ - // Select a Cloud Platform subscription. - 0, - // Do you wish to continue? - 'no', - ]; - $subscriptions = $this->mockRequest('getSubscriptions'); - - $getDomainsResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains', 'get', '200'); - $this->clientProphecy->request('get', "/subscriptions/{$subscriptions[0]->uuid}/domains")->willReturn($getDomainsResponse->_embedded->items); - - $applicationsResponse = $this->getMockResponseFromSpec('/applications', 'get', '200'); - $applicationsResponse->_embedded->items[0]->subscription->uuid = $subscriptions[0]->uuid; - $applicationsResponse->_embedded->items[1]->subscription->uuid = $subscriptions[0]->uuid; - - $app = $this->getMockResponseFromSpec('/applications/{applicationUuid}', 'get', '200'); - for ($i = 2; $i < 101; $i++) { - $applicationsResponse->_embedded->items[$i] = $app; - $applicationsResponse->_embedded->items[$i]->subscription->uuid = $subscriptions[0]->uuid; + public function setUp(): void + { + parent::setUp(); + $this->setupFsFixture(); + $this->command = $this->createCommand(); } - $this->clientProphecy->request('get', '/applications')->willReturn($applicationsResponse->_embedded->items); + public function testEmailInfoForSubscription(): void + { + $inputs = [ + // Select a Cloud Platform subscription. + 0, + ]; + $subscriptions = $this->mockRequest('getSubscriptions'); - foreach ($applicationsResponse->_embedded->items as $app) { - $this->clientProphecy->request('get', "/applications/{$app->uuid}/email/domains")->willReturn([]); + $getDomainsResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains', 'get', '200'); + // Duplicating the request to ensure there is at least one domain with a successful, pending, and failed health code. + $getDomainsResponse2 = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains', 'get', '200'); + $totalDomainsList = array_merge($getDomainsResponse->_embedded->items, $getDomainsResponse2->_embedded->items); + $this->clientProphecy->request('get', "/subscriptions/{$subscriptions[0]->uuid}/domains")->willReturn($totalDomainsList); + + $totalDomainsList[2]->domain_name = 'example3.com'; + $totalDomainsList[2]->health->code = '200'; + + $totalDomainsList[3]->domain_name = 'example4.com'; + $totalDomainsList[3]->health->code = '202'; + + $applicationsResponse = $this->mockApplicationsRequest(); + + $applicationsResponse->_embedded->items[0]->subscription->uuid = $subscriptions[0]->uuid; + + $getAppDomainsResponse = $this->getMockResponseFromSpec('/applications/{applicationUuid}/email/domains', 'get', '200'); + // Duplicating the request to ensure added domains are included in association list. + $getAppDomainsResponse2 = $this->getMockResponseFromSpec('/applications/{applicationUuid}/email/domains', 'get', '200'); + $totalAppDomainsList = array_merge($getAppDomainsResponse->_embedded->items, $getAppDomainsResponse2->_embedded->items); + $this->clientProphecy->request('get', "/applications/{$applicationsResponse->_embedded->items[0]->uuid}/email/domains")->willReturn($totalAppDomainsList); + + $totalAppDomainsList[2]->domain_name = 'example3.com'; + $totalAppDomainsList[2]->flags->associated = true; + + $totalAppDomainsList[3]->domain_name = 'example4.com'; + $totalAppDomainsList[3]->flags->associated = false; + + $this->executeCommand([], $inputs); + $output = $this->getDisplay(); + $this->assertEquals(0, $this->getStatusCode()); + $this->assertStringContainsString('Application: ', $output); + foreach ($getAppDomainsResponse->_embedded->items as $appDomain) { + $this->assertEquals(3, substr_count($output, $appDomain->domain_name)); + } + + $this->assertEquals(2, substr_count($output, 'Failed - 404')); + $this->assertEquals(1, substr_count($output, 'Pending - 202')); + $this->assertEquals(1, substr_count($output, 'Succeeded - 200')); + + $this->assertEquals(3, substr_count($output, 'true')); + $this->assertEquals(1, substr_count($output, 'false')); } - $this->executeCommand([], $inputs); - $output = $this->getDisplay(); - $this->assertEquals(1, $this->getStatusCode()); + public function testEmailInfoForSubscriptionNoApps(): void + { + $inputs = [ + // Select a Cloud Platform subscription. + 0, + ]; + $subscriptions = $this->mockRequest('getSubscriptions'); + + $getDomainsResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains', 'get', '200'); + $this->clientProphecy->request('get', "/subscriptions/{$subscriptions[0]->uuid}/domains")->willReturn($getDomainsResponse->_embedded->items); - } + $this->mockRequest('getApplications'); + + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage('You do not have access'); + $this->executeCommand([], $inputs); + } - public function testEmailInfoForSubscriptionNoDomains(): void { - $inputs = [ - // Select a Cloud Platform subscription. - 0, - ]; - $subscriptions = $this->mockRequest('getSubscriptions'); + public function testEmailInfoForSubscriptionWith101Apps(): void + { + $inputs = [ + // Select a Cloud Platform subscription. + 0, + // Do you wish to continue? + 'no', + ]; + $subscriptions = $this->mockRequest('getSubscriptions'); + + $getDomainsResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains', 'get', '200'); + $this->clientProphecy->request('get', "/subscriptions/{$subscriptions[0]->uuid}/domains")->willReturn($getDomainsResponse->_embedded->items); + + $applicationsResponse = $this->getMockResponseFromSpec('/applications', 'get', '200'); + $applicationsResponse->_embedded->items[0]->subscription->uuid = $subscriptions[0]->uuid; + $applicationsResponse->_embedded->items[1]->subscription->uuid = $subscriptions[0]->uuid; + + $app = $this->getMockResponseFromSpec('/applications/{applicationUuid}', 'get', '200'); + for ($i = 2; $i < 101; $i++) { + $applicationsResponse->_embedded->items[$i] = $app; + $applicationsResponse->_embedded->items[$i]->subscription->uuid = $subscriptions[0]->uuid; + } + + $this->clientProphecy->request('get', '/applications')->willReturn($applicationsResponse->_embedded->items); + + foreach ($applicationsResponse->_embedded->items as $app) { + $this->clientProphecy->request('get', "/applications/{$app->uuid}/email/domains")->willReturn([]); + } + + $this->executeCommand([], $inputs); + $output = $this->getDisplay(); + $this->assertEquals(1, $this->getStatusCode()); + } - $this->clientProphecy->request('get', "/subscriptions/{$subscriptions[0]->uuid}/domains")->willReturn([]); + public function testEmailInfoForSubscriptionNoDomains(): void + { + $inputs = [ + // Select a Cloud Platform subscription. + 0, + ]; + $subscriptions = $this->mockRequest('getSubscriptions'); - $this->executeCommand([], $inputs); - $output = $this->getDisplay(); - $this->assertStringContainsString('No email domains', $output); - } + $this->clientProphecy->request('get', "/subscriptions/{$subscriptions[0]->uuid}/domains")->willReturn([]); - public function testEmailInfoForSubscriptionNoAppDomains(): void { - $inputs = [ - // Select a Cloud Platform subscription. - 0, - ]; - $subscriptions = $this->mockRequest('getSubscriptions'); + $this->executeCommand([], $inputs); + $output = $this->getDisplay(); + $this->assertStringContainsString('No email domains', $output); + } - $getDomainsResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains', 'get', '200'); - $this->clientProphecy->request('get', "/subscriptions/{$subscriptions[0]->uuid}/domains")->willReturn($getDomainsResponse->_embedded->items); + public function testEmailInfoForSubscriptionNoAppDomains(): void + { + $inputs = [ + // Select a Cloud Platform subscription. + 0, + ]; + $subscriptions = $this->mockRequest('getSubscriptions'); - $applicationsResponse = $this->mockApplicationsRequest(); + $getDomainsResponse = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains', 'get', '200'); + $this->clientProphecy->request('get', "/subscriptions/{$subscriptions[0]->uuid}/domains")->willReturn($getDomainsResponse->_embedded->items); - $applicationsResponse->_embedded->items[0]->subscription->uuid = $subscriptions[0]->uuid; + $applicationsResponse = $this->mockApplicationsRequest(); - $this->clientProphecy->request('get', "/applications/{$applicationsResponse->_embedded->items[0]->uuid}/email/domains")->willReturn([]); + $applicationsResponse->_embedded->items[0]->subscription->uuid = $subscriptions[0]->uuid; - $this->executeCommand([], $inputs); - $output = $this->getDisplay(); - $this->assertStringContainsString('No domains eligible', $output); - } + $this->clientProphecy->request('get', "/applications/{$applicationsResponse->_embedded->items[0]->uuid}/email/domains")->willReturn([]); - public function testEmailInfoForSubscriptionNoSubscriptions(): void { - $inputs = [ - // Select a Cloud Platform subscription. - 0, - ]; - $this->clientProphecy->request('get', '/subscriptions') - ->willReturn([]) - ->shouldBeCalledTimes(1); - $this->expectException(AcquiaCliException::class); - $this->expectExceptionMessage('You have no Cloud subscriptions.'); - $this->executeCommand([], $inputs); - } + $this->executeCommand([], $inputs); + $output = $this->getDisplay(); + $this->assertStringContainsString('No domains eligible', $output); + } + public function testEmailInfoForSubscriptionNoSubscriptions(): void + { + $inputs = [ + // Select a Cloud Platform subscription. + 0, + ]; + $this->clientProphecy->request('get', '/subscriptions') + ->willReturn([]) + ->shouldBeCalledTimes(1); + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage('You have no Cloud subscriptions.'); + $this->executeCommand([], $inputs); + } } diff --git a/tests/phpunit/src/Commands/Env/EnvCertCreateCommandTest.php b/tests/phpunit/src/Commands/Env/EnvCertCreateCommandTest.php index 73d5cea5c..89dc4ab63 100644 --- a/tests/phpunit/src/Commands/Env/EnvCertCreateCommandTest.php +++ b/tests/phpunit/src/Commands/Env/EnvCertCreateCommandTest.php @@ -1,6 +1,6 @@ injectCommand(EnvCertCreateCommand::class); + } - protected function createCommand(): CommandBase { - return $this->injectCommand(EnvCertCreateCommand::class); - } + public function testCreateCert(): 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(); - public function testCreateCert(): 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, + $sslResponse = $this->getMockResponseFromSpec( + '/environments/{environmentId}/ssl/certificates', + 'post', + '202' + ); + $options = [ + 'json' => [ + 'ca_certificates' => null, 'certificate' => $certContents, 'csr_id' => $csrId, 'label' => $label, - 'legacy' => FALSE, + '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); - - $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, - '', - ] - ); + ]; + $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); - } + $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'); - $application = $this->mockRequest('getApplicationByUuid', $applications[self::$INPUT_DEFAULT_CHOICE]->uuid); - $tamper = function ($responses): void { - foreach ($responses as $response) { - $response->type = 'node'; - } - }; - $environments = $this->mockRequest('getApplicationEnvironments', $application->uuid, NULL, NULL, $tamper); - $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(); + public function testCreateCertNode(): void + { + $applications = $this->mockRequest('getApplications'); + $application = $this->mockRequest('getApplicationByUuid', $applications[self::$INPUT_DEFAULT_CHOICE]->uuid); + $tamper = function ($responses): void { + foreach ($responses as $response) { + $response->type = 'node'; + } + }; + $environments = $this->mockRequest('getApplicationEnvironments', $application->uuid, null, null, $tamper); + $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, + $sslResponse = $this->getMockResponseFromSpec( + '/environments/{environmentId}/ssl/certificates', + 'post', + '202' + ); + $options = [ + 'json' => [ + 'ca_certificates' => null, 'certificate' => $certContents, 'csr_id' => $csrId, 'label' => $label, - 'legacy' => FALSE, + 'legacy' => false, 'private_key' => $keyContents, - ], - ]; - $this->clientProphecy->request('post', "/environments/{$environments[0]->id}/ssl/certificates", $options) - ->willReturn($sslResponse->{'Site is being imported'}->value) - ->shouldBeCalled(); - $this->mockNotificationResponseFromObject($sslResponse->{'Site is being imported'}->value); - - $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, - ] - ); - - } + ], + ]; + $this->clientProphecy->request('post', "/environments/{$environments[0]->id}/ssl/certificates", $options) + ->willReturn($sslResponse->{'Site is being imported'}->value) + ->shouldBeCalled(); + $this->mockNotificationResponseFromObject($sslResponse->{'Site is being imported'}->value); + $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, + ] + ); + } } diff --git a/tests/phpunit/src/Commands/Env/EnvCopyCronCommandTest.php b/tests/phpunit/src/Commands/Env/EnvCopyCronCommandTest.php index fbea13de7..6bee7ed66 100644 --- a/tests/phpunit/src/Commands/Env/EnvCopyCronCommandTest.php +++ b/tests/phpunit/src/Commands/Env/EnvCopyCronCommandTest.php @@ -1,6 +1,6 @@ injectCommand(EnvCopyCronCommand::class); - } - - public function testCopyCronTasksCommandTest(): void { - $environmentsResponse = $this->getMockEnvironmentsResponse(); - $sourceCronsListResponse = $this->getMockResponseFromSpec('/environments/{environmentId}/crons', 'get', '200'); - $this->clientProphecy->request('get', - '/environments/' . $environmentsResponse->{'_embedded'}->items[0]->id . '/crons') - ->willReturn($sourceCronsListResponse->{'_embedded'}->items) - ->shouldBeCalled(); - - $createCronResponse = $this->getMockResponseFromSpec('/environments/{environmentId}/crons', 'post', '202'); - $this->clientProphecy->request('post', - '/environments/' . $environmentsResponse->{'_embedded'}->items[2]->id . '/crons', Argument::type('array')) - ->willReturn($createCronResponse->{'Adding cron'}->value) - ->shouldBeCalled(); - - $source = '24-a47ac10b-58cc-4372-a567-0e02b2c3d470'; - $dest = '32-a47ac10b-58cc-4372-a567-0e02b2c3d470'; - $this->executeCommand( - [ - 'dest_env' => $dest, - 'source_env' => $source, - ], - [ - 'y', - ] - ); - - $output = $this->getDisplay(); - $this->assertStringContainsString('Are you sure you\'d like to copy the cron jobs from ' . $source . ' to ' . $dest . '? (yes/no) [yes]:', $output); - $this->assertStringContainsString('Copying the cron task "Clear drush caches" from ', $output); - $this->assertStringContainsString($source . ' to', $output); - $this->assertStringContainsString($dest, $output); - $this->assertStringContainsString('[OK] Cron task copy is completed.', $output); - } - - public function testCopyCronTasksCommandTestFail(): void { - $this->executeCommand([ +class EnvCopyCronCommandTest extends CommandTestBase +{ + protected function createCommand(): CommandBase + { + return $this->injectCommand(EnvCopyCronCommand::class); + } + + public function testCopyCronTasksCommandTest(): void + { + $environmentsResponse = $this->getMockEnvironmentsResponse(); + $sourceCronsListResponse = $this->getMockResponseFromSpec('/environments/{environmentId}/crons', 'get', '200'); + $this->clientProphecy->request( + 'get', + '/environments/' . $environmentsResponse->{'_embedded'}->items[0]->id . '/crons' + ) + ->willReturn($sourceCronsListResponse->{'_embedded'}->items) + ->shouldBeCalled(); + + $createCronResponse = $this->getMockResponseFromSpec('/environments/{environmentId}/crons', 'post', '202'); + $this->clientProphecy->request( + 'post', + '/environments/' . $environmentsResponse->{'_embedded'}->items[2]->id . '/crons', + Argument::type('array') + ) + ->willReturn($createCronResponse->{'Adding cron'}->value) + ->shouldBeCalled(); + + $source = '24-a47ac10b-58cc-4372-a567-0e02b2c3d470'; + $dest = '32-a47ac10b-58cc-4372-a567-0e02b2c3d470'; + $this->executeCommand( + [ + 'dest_env' => $dest, + 'source_env' => $source, + ], + [ + 'y', + ] + ); + + $output = $this->getDisplay(); + $this->assertStringContainsString('Are you sure you\'d like to copy the cron jobs from ' . $source . ' to ' . $dest . '? (yes/no) [yes]:', $output); + $this->assertStringContainsString('Copying the cron task "Clear drush caches" from ', $output); + $this->assertStringContainsString($source . ' to', $output); + $this->assertStringContainsString($dest, $output); + $this->assertStringContainsString('[OK] Cron task copy is completed.', $output); + } + + public function testCopyCronTasksCommandTestFail(): void + { + $this->executeCommand([ 'dest_env' => 'app.test', 'source_env' => 'app.test', -], - ); - $output = $this->getDisplay(); - $this->assertStringContainsString('The source and destination environments can not be same', $output); - } - - /** - * Tests for no cron job available on source environment to copy. - */ - public function testNoCronJobOnSource(): void { - $environmentsResponse = $this->getMockEnvironmentsResponse(); - $this->clientProphecy->request('get', - '/environments/' . $environmentsResponse->{'_embedded'}->items[0]->id . '/crons') - ->willReturn([]) - ->shouldBeCalled(); - - $source = '24-a47ac10b-58cc-4372-a567-0e02b2c3d470'; - $dest = '32-a47ac10b-58cc-4372-a567-0e02b2c3d470'; - $this->executeCommand( - [ - 'dest_env' => $dest, - 'source_env' => $source, - ], - [ - 'y', - ] - ); - - $output = $this->getDisplay(); - $this->assertStringContainsString('There are no cron jobs in the source environment for copying.', $output); - } - - /** - * Tests for exception during the cron job copy. - */ - public function testExceptionOnCronJobCopy(): void { - $environmentsResponse = $this->getMockEnvironmentsResponse(); - $sourceCronsListResponse = $this->getMockResponseFromSpec('/environments/{environmentId}/crons', 'get', '200'); - $this->clientProphecy->request('get', - '/environments/' . $environmentsResponse->{'_embedded'}->items[0]->id . '/crons') - ->willReturn($sourceCronsListResponse->{'_embedded'}->items) - ->shouldBeCalled(); - - $this->getMockResponseFromSpec('/environments/{environmentId}/crons', 'post', '202'); - $this->clientProphecy->request('post', - '/environments/' . $environmentsResponse->{'_embedded'}->items[2]->id . '/crons', Argument::type('array')) - ->willThrow(Exception::class); - - $source = '24-a47ac10b-58cc-4372-a567-0e02b2c3d470'; - $dest = '32-a47ac10b-58cc-4372-a567-0e02b2c3d470'; - $this->executeCommand( - [ - 'dest_env' => $dest, - 'source_env' => $source, - ], - [ - 'y', - ] - ); - - $output = $this->getDisplay(); - $this->assertStringContainsString('There was some error while copying the cron task', $output); - } - + ],); + $output = $this->getDisplay(); + $this->assertStringContainsString('The source and destination environments can not be same', $output); + } + + /** + * Tests for no cron job available on source environment to copy. + */ + public function testNoCronJobOnSource(): void + { + $environmentsResponse = $this->getMockEnvironmentsResponse(); + $this->clientProphecy->request( + 'get', + '/environments/' . $environmentsResponse->{'_embedded'}->items[0]->id . '/crons' + ) + ->willReturn([]) + ->shouldBeCalled(); + + $source = '24-a47ac10b-58cc-4372-a567-0e02b2c3d470'; + $dest = '32-a47ac10b-58cc-4372-a567-0e02b2c3d470'; + $this->executeCommand( + [ + 'dest_env' => $dest, + 'source_env' => $source, + ], + [ + 'y', + ] + ); + + $output = $this->getDisplay(); + $this->assertStringContainsString('There are no cron jobs in the source environment for copying.', $output); + } + + /** + * Tests for exception during the cron job copy. + */ + public function testExceptionOnCronJobCopy(): void + { + $environmentsResponse = $this->getMockEnvironmentsResponse(); + $sourceCronsListResponse = $this->getMockResponseFromSpec('/environments/{environmentId}/crons', 'get', '200'); + $this->clientProphecy->request( + 'get', + '/environments/' . $environmentsResponse->{'_embedded'}->items[0]->id . '/crons' + ) + ->willReturn($sourceCronsListResponse->{'_embedded'}->items) + ->shouldBeCalled(); + + $this->getMockResponseFromSpec('/environments/{environmentId}/crons', 'post', '202'); + $this->clientProphecy->request( + 'post', + '/environments/' . $environmentsResponse->{'_embedded'}->items[2]->id . '/crons', + Argument::type('array') + ) + ->willThrow(Exception::class); + + $source = '24-a47ac10b-58cc-4372-a567-0e02b2c3d470'; + $dest = '32-a47ac10b-58cc-4372-a567-0e02b2c3d470'; + $this->executeCommand( + [ + 'dest_env' => $dest, + 'source_env' => $source, + ], + [ + 'y', + ] + ); + + $output = $this->getDisplay(); + $this->assertStringContainsString('There was some error while copying the cron task', $output); + } } diff --git a/tests/phpunit/src/Commands/Env/EnvCreateCommandTest.php b/tests/phpunit/src/Commands/Env/EnvCreateCommandTest.php index 25f551f82..f7e3cdd87 100644 --- a/tests/phpunit/src/Commands/Env/EnvCreateCommandTest.php +++ b/tests/phpunit/src/Commands/Env/EnvCreateCommandTest.php @@ -1,6 +1,6 @@ mockApplicationsRequest(); - $applicationResponse = $this->mockApplicationRequest(); - $this->mockEnvironmentsRequest($applicationsResponse); - - $response1 = $this->getMockEnvironmentsResponse(); - $response2 = $this->getMockEnvironmentsResponse(); - $cde = $response2->_embedded->items[0]; - $cde->label = $label; - $response2->_embedded->items[3] = $cde; - $this->clientProphecy->request('get', - "/applications/{$applicationsResponse->{'_embedded'}->items[0]->uuid}/environments") - ->willReturn($response1->_embedded->items, $response2->_embedded->items) - ->shouldBeCalled(); - - $codeResponse = $this->getMockResponseFromSpec("/applications/{applicationUuid}/code", 'get', '200'); - $this->clientProphecy->request('get', - "/applications/$applicationResponse->uuid/code") - ->willReturn($codeResponse->_embedded->items) - ->shouldBeCalled(); - - $databasesResponse = $this->getMockResponseFromSpec("/applications/{applicationUuid}/databases", 'get', '200'); - $this->clientProphecy->request('get', - "/applications/$applicationResponse->uuid/databases") - ->willReturn($databasesResponse->_embedded->items) - ->shouldBeCalled(); - - $environmentsResponse = $this->getMockResponseFromSpec('/applications/{applicationUuid}/environments', - 'post', 202); - $this->clientProphecy->request('post', "/applications/$applicationResponse->uuid/environments", Argument::type('array')) - ->willReturn($environmentsResponse->{'Adding environment'}->value) - ->shouldBeCalled(); - - $this->mockNotificationResponseFromObject($environmentsResponse->{'Adding environment'}->value); - return $response2->_embedded->items[3]->domains[0]; - } - - private function getBranch(): string { - $codeResponse = $this->getMockResponseFromSpec("/applications/{applicationUuid}/code", 'get', '200'); - return $codeResponse->_embedded->items[0]->name; - } - - private function getApplication(): string { - $applicationsResponse = $this->getMockResponseFromSpec('/applications', - 'get', '200'); - return $applicationsResponse->{'_embedded'}->items[0]->uuid; - } - - protected function createCommand(): CommandBase { - return $this->injectCommand(EnvCreateCommand::class); - } - - /** - * @return array - */ - public function providerTestCreateCde(): array { - $application = $this->getApplication(); - $branch = $this->getBranch(); - return [ - // No args, only interactive input. - [[NULL, NULL], ['n', 0, 0]], - // Branch as arg. - [[$branch, NULL], ['n', 0]], - // Branch and app id as args. - [[$branch, $application], []], - ]; - } - - /** - * @dataProvider providerTestCreateCde - * @group brokenProphecy - */ - public function testCreateCde(mixed $args, mixed $input): void { - $domain = $this->setupCdeTest(self::$validLabel); - - $this->executeCommand( - [ - 'applicationUuid' => $args[1], - 'branch' => $args[0], - 'label' => self::$validLabel, - ], - $input - ); - - $output = $this->getDisplay(); - $this->assertEquals(0, $this->getStatusCode()); - $this->assertStringContainsString("Your CDE URL: $domain", $output); - } - - /** - * @group brokenProphecy - */ - public function testCreateCdeNonUniqueLabel(): void { - $label = 'Dev'; - $this->setupCdeTest($label); - - $this->expectException(AcquiaCliException::class); - $this->expectExceptionMessage('An environment named Dev already exists.'); - $this->executeCommand( - [ - 'applicationUuid' => $this->getApplication(), - 'branch' => $this->getBranch(), - 'label' => $label, - ] - ); - } - - /** - * @group brokenProphecy - */ - public function testCreateCdeInvalidTag(): void { - $this->setupCdeTest(self::$validLabel); - - $this->expectException(AcquiaCliException::class); - $this->expectExceptionMessage('There is no branch or tag with the name bogus on the remote VCS.'); - $this->executeCommand( - [ - 'applicationUuid' => $this->getApplication(), - 'branch' => 'bogus', - 'label' => self::$validLabel, - ] - ); - } - +class EnvCreateCommandTest extends CommandTestBase +{ + private static string $validLabel = 'New CDE'; + + private function setupCdeTest(string $label): string + { + $applicationsResponse = $this->mockApplicationsRequest(); + $applicationResponse = $this->mockApplicationRequest(); + $this->mockEnvironmentsRequest($applicationsResponse); + + $response1 = $this->getMockEnvironmentsResponse(); + $response2 = $this->getMockEnvironmentsResponse(); + $cde = $response2->_embedded->items[0]; + $cde->label = $label; + $response2->_embedded->items[3] = $cde; + $this->clientProphecy->request( + 'get', + "/applications/{$applicationsResponse->{'_embedded'}->items[0]->uuid}/environments" + ) + ->willReturn($response1->_embedded->items, $response2->_embedded->items) + ->shouldBeCalled(); + + $codeResponse = $this->getMockResponseFromSpec("/applications/{applicationUuid}/code", 'get', '200'); + $this->clientProphecy->request( + 'get', + "/applications/$applicationResponse->uuid/code" + ) + ->willReturn($codeResponse->_embedded->items) + ->shouldBeCalled(); + + $databasesResponse = $this->getMockResponseFromSpec("/applications/{applicationUuid}/databases", 'get', '200'); + $this->clientProphecy->request( + 'get', + "/applications/$applicationResponse->uuid/databases" + ) + ->willReturn($databasesResponse->_embedded->items) + ->shouldBeCalled(); + + $environmentsResponse = $this->getMockResponseFromSpec( + '/applications/{applicationUuid}/environments', + 'post', + 202 + ); + $this->clientProphecy->request('post', "/applications/$applicationResponse->uuid/environments", Argument::type('array')) + ->willReturn($environmentsResponse->{'Adding environment'}->value) + ->shouldBeCalled(); + + $this->mockNotificationResponseFromObject($environmentsResponse->{'Adding environment'}->value); + return $response2->_embedded->items[3]->domains[0]; + } + + private function getBranch(): string + { + $codeResponse = $this->getMockResponseFromSpec("/applications/{applicationUuid}/code", 'get', '200'); + return $codeResponse->_embedded->items[0]->name; + } + + private function getApplication(): string + { + $applicationsResponse = $this->getMockResponseFromSpec( + '/applications', + 'get', + '200' + ); + return $applicationsResponse->{'_embedded'}->items[0]->uuid; + } + + protected function createCommand(): CommandBase + { + return $this->injectCommand(EnvCreateCommand::class); + } + + /** + * @return array + */ + public function providerTestCreateCde(): array + { + $application = $this->getApplication(); + $branch = $this->getBranch(); + return [ + // No args, only interactive input. + [[null, null], ['n', 0, 0]], + // Branch as arg. + [[$branch, null], ['n', 0]], + // Branch and app id as args. + [[$branch, $application], []], + ]; + } + + /** + * @dataProvider providerTestCreateCde + * @group brokenProphecy + */ + public function testCreateCde(mixed $args, mixed $input): void + { + $domain = $this->setupCdeTest(self::$validLabel); + + $this->executeCommand( + [ + 'applicationUuid' => $args[1], + 'branch' => $args[0], + 'label' => self::$validLabel, + ], + $input + ); + + $output = $this->getDisplay(); + $this->assertEquals(0, $this->getStatusCode()); + $this->assertStringContainsString("Your CDE URL: $domain", $output); + } + + /** + * @group brokenProphecy + */ + public function testCreateCdeNonUniqueLabel(): void + { + $label = 'Dev'; + $this->setupCdeTest($label); + + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage('An environment named Dev already exists.'); + $this->executeCommand( + [ + 'applicationUuid' => $this->getApplication(), + 'branch' => $this->getBranch(), + 'label' => $label, + ] + ); + } + + /** + * @group brokenProphecy + */ + public function testCreateCdeInvalidTag(): void + { + $this->setupCdeTest(self::$validLabel); + + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage('There is no branch or tag with the name bogus on the remote VCS.'); + $this->executeCommand( + [ + 'applicationUuid' => $this->getApplication(), + 'branch' => 'bogus', + 'label' => self::$validLabel, + ] + ); + } } diff --git a/tests/phpunit/src/Commands/Env/EnvDeleteCommandTest.php b/tests/phpunit/src/Commands/Env/EnvDeleteCommandTest.php index fda51ec9c..b51c46d0b 100644 --- a/tests/phpunit/src/Commands/Env/EnvDeleteCommandTest.php +++ b/tests/phpunit/src/Commands/Env/EnvDeleteCommandTest.php @@ -1,6 +1,6 @@ injectCommand(EnvDeleteCommand::class); - } - - /** - * @return array - */ - public function providerTestDeleteCde(): array { - $environmentResponse = $this->getMockEnvironmentsResponse(); - $environment = $environmentResponse->_embedded->items[0]; - return [ - [$environment->id], - [NULL], - ]; - } - - /** - * @dataProvider providerTestDeleteCde - * @group brokenProphecy - */ - public function testDeleteCde(mixed $environmentId): void { - $applicationsResponse = $this->mockApplicationsRequest(); - $this->mockApplicationRequest(); - $this->mockEnvironmentsRequest($applicationsResponse); - - $this->getMockEnvironmentsResponse(); - $response2 = $this->getMockEnvironmentsResponse(); - $cde = $response2->_embedded->items[0]; - $cde->flags->cde = TRUE; - $label = "New CDE"; - $cde->label = $label; - $response2->_embedded->items[3] = $cde; - $this->clientProphecy->request('get', - "/applications/{$applicationsResponse->{'_embedded'}->items[0]->uuid}/environments") - ->willReturn($response2->_embedded->items) - ->shouldBeCalled(); - - $environmentsResponse = $this->getMockResponseFromSpec('/environments/{environmentId}', - 'delete', 202); - $this->clientProphecy->request('delete', "/environments/" . $cde->id) - ->willReturn($environmentsResponse) - ->shouldBeCalled(); - - $this->getMockResponseFromSpec('/environments/{environmentId}', - 'get', 200); - $this->clientProphecy->request('get', "/environments/" . $cde->id) - ->willReturn($cde) - ->shouldBeCalled(); - - $this->executeCommand( - [ - 'environmentId' => $environmentId, - ], - [ - // 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, - ] - ); - $output = $this->getDisplay(); - $this->assertEquals(0, $this->getStatusCode()); - $this->assertStringContainsString("The $cde->label environment is being deleted", $output); - - } - - /** - * @group brokenProphecy - */ - public function testNoExistingCDEEnvironment(): void { - $applicationsResponse = $this->mockApplicationsRequest(); - $this->mockApplicationRequest(); - $this->mockEnvironmentsRequest($applicationsResponse); - - $this->expectException(AcquiaCliException::class); - $this->expectExceptionMessage('There are no existing CDEs for Application'); - - $this->executeCommand([], - [ - // 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, - ] - ); - } - - /** - * @group brokenProphecy - */ - public function testNoEnvironmentArgumentPassed(): void { - $applications = $this->mockRequest('getApplications'); - $application = $this->mockRequest('getApplicationByUuid', $applications[self::$INPUT_DEFAULT_CHOICE]->uuid); - $environments = $this->mockRequest('getApplicationEnvironments', $applications[self::$INPUT_DEFAULT_CHOICE]->uuid); - foreach ($environments as $environment) { - $environment->flags->cde = TRUE; +class EnvDeleteCommandTest extends CommandTestBase +{ + protected function createCommand(): CommandBase + { + return $this->injectCommand(EnvDeleteCommand::class); } - $this->clientProphecy->request('get', - "/applications/{$application->uuid}/environments") - ->willReturn($environments) - ->shouldBeCalled(); - - $cde = $environments[0]; - $environmentsResponse = $this->getMockResponseFromSpec('/environments/{environmentId}', - 'delete', 202); - $this->clientProphecy->request('delete', "/environments/" . $cde->id) - ->willReturn($environmentsResponse) - ->shouldBeCalled(); - - $this->executeCommand([], - [ - // 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, - ] - ); - - $output = $this->getDisplay(); - - $expected = << + */ + public function providerTestDeleteCde(): array + { + $environmentResponse = $this->getMockEnvironmentsResponse(); + $environment = $environmentResponse->_embedded->items[0]; + return [ + [$environment->id], + [null], + ]; + } + + /** + * @dataProvider providerTestDeleteCde + * @group brokenProphecy + */ + public function testDeleteCde(mixed $environmentId): void + { + $applicationsResponse = $this->mockApplicationsRequest(); + $this->mockApplicationRequest(); + $this->mockEnvironmentsRequest($applicationsResponse); + + $this->getMockEnvironmentsResponse(); + $response2 = $this->getMockEnvironmentsResponse(); + $cde = $response2->_embedded->items[0]; + $cde->flags->cde = true; + $label = "New CDE"; + $cde->label = $label; + $response2->_embedded->items[3] = $cde; + $this->clientProphecy->request( + 'get', + "/applications/{$applicationsResponse->{'_embedded'}->items[0]->uuid}/environments" + ) + ->willReturn($response2->_embedded->items) + ->shouldBeCalled(); + + $environmentsResponse = $this->getMockResponseFromSpec( + '/environments/{environmentId}', + 'delete', + 202 + ); + $this->clientProphecy->request('delete', "/environments/" . $cde->id) + ->willReturn($environmentsResponse) + ->shouldBeCalled(); + + $this->getMockResponseFromSpec( + '/environments/{environmentId}', + 'get', + 200 + ); + $this->clientProphecy->request('get', "/environments/" . $cde->id) + ->willReturn($cde) + ->shouldBeCalled(); + + $this->executeCommand( + [ + 'environmentId' => $environmentId, + ], + [ + // 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, + ] + ); + $output = $this->getDisplay(); + $this->assertEquals(0, $this->getStatusCode()); + $this->assertStringContainsString("The $cde->label environment is being deleted", $output); + } + + /** + * @group brokenProphecy + */ + public function testNoExistingCDEEnvironment(): void + { + $applicationsResponse = $this->mockApplicationsRequest(); + $this->mockApplicationRequest(); + $this->mockEnvironmentsRequest($applicationsResponse); + + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage('There are no existing CDEs for Application'); + + $this->executeCommand( + [], + [ + // 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, + ] + ); + } + + /** + * @group brokenProphecy + */ + public function testNoEnvironmentArgumentPassed(): void + { + $applications = $this->mockRequest('getApplications'); + $application = $this->mockRequest('getApplicationByUuid', $applications[self::$INPUT_DEFAULT_CHOICE]->uuid); + $environments = $this->mockRequest('getApplicationEnvironments', $applications[self::$INPUT_DEFAULT_CHOICE]->uuid); + foreach ($environments as $environment) { + $environment->flags->cde = true; + } + $this->clientProphecy->request( + 'get', + "/applications/{$application->uuid}/environments" + ) + ->willReturn($environments) + ->shouldBeCalled(); + + $cde = $environments[0]; + $environmentsResponse = $this->getMockResponseFromSpec( + '/environments/{environmentId}', + 'delete', + 202 + ); + $this->clientProphecy->request('delete', "/environments/" . $cde->id) + ->willReturn($environmentsResponse) + ->shouldBeCalled(); + + $this->executeCommand( + [], + [ + // 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, + ] + ); + + $output = $this->getDisplay(); + + $expected = <<injectCommand(EnvMirrorCommand::class); + } - protected function createCommand(): CommandBase { - return $this->injectCommand(EnvMirrorCommand::class); - } + public function testEnvironmentMirror(): 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(); - public function testEnvironmentMirror(): 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(); + $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' => [ + $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(); + ], + ]) + ->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' => [ + $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); + ], + ]) + ->willReturn($response) + ->shouldBeCalled(); - $this->executeCommand( - [ - 'destination-environment' => $environmentResponse->id, - 'source-environment' => $environmentResponse->id, - ], - [ - // Are you sure that you want to overwrite everything ... - 'y', - ] - ); + $environmentUpdateResponse = $this->getMockResponseFromSpec("/environments/{environmentId}", 'put', '202'); + $this->clientProphecy->request('put', "/environments/{$environmentResponse->id}", Argument::type('array')) + ->willReturn($environmentUpdateResponse) + ->shouldBeCalled(); + $this->mockNotificationResponseFromObject($environmentUpdateResponse); - $output = $this->getDisplay(); - $this->assertEquals(0, $this->getStatusCode()); - $this->assertStringContainsString('Are you sure that you want to overwrite everything on Dev (dev) and replace it with source data from Dev (dev)', $output); - $this->assertStringContainsString("Switching to {$environmentResponse->vcs->path}", $output); - $this->assertStringContainsString("Copying {$databasesResponse->_embedded->items[0]->name}", $output); - $this->assertStringContainsString("Copying PHP version, acpu memory limit, etc.", $output); - $this->assertStringContainsString("[OK] Done! {$environmentResponse->label} now matches {$environmentResponse->label}", $output); - } + $this->executeCommand( + [ + 'destination-environment' => $environmentResponse->id, + 'source-environment' => $environmentResponse->id, + ], + [ + // Are you sure that you want to overwrite everything ... + 'y', + ] + ); + $output = $this->getDisplay(); + $this->assertEquals(0, $this->getStatusCode()); + $this->assertStringContainsString('Are you sure that you want to overwrite everything on Dev (dev) and replace it with source data from Dev (dev)', $output); + $this->assertStringContainsString("Switching to {$environmentResponse->vcs->path}", $output); + $this->assertStringContainsString("Copying {$databasesResponse->_embedded->items[0]->name}", $output); + $this->assertStringContainsString("Copying PHP version, acpu memory limit, etc.", $output); + $this->assertStringContainsString("[OK] Done! {$environmentResponse->label} now matches {$environmentResponse->label}", $output); + } } diff --git a/tests/phpunit/src/Commands/Ide/IdeCreateCommandTest.php b/tests/phpunit/src/Commands/Ide/IdeCreateCommandTest.php index 77ec3f4bd..df555137e 100644 --- a/tests/phpunit/src/Commands/Ide/IdeCreateCommandTest.php +++ b/tests/phpunit/src/Commands/Ide/IdeCreateCommandTest.php @@ -1,6 +1,6 @@ httpClientProphecy = $this->prophet->prophesize(Client::class); - protected function createCommand(): CommandBase { - $this->httpClientProphecy = $this->prophet->prophesize(Client::class); + return new IdeCreateCommand( + $this->localMachineHelper, + $this->datastoreCloud, + $this->datastoreAcli, + $this->cloudCredentials, + $this->telemetryHelper, + $this->acliRepoRoot, + $this->clientServiceProphecy->reveal(), + $this->sshHelper, + $this->sshDir, + $this->logger, + $this->httpClientProphecy->reveal() + ); + } - return new IdeCreateCommand( - $this->localMachineHelper, - $this->datastoreCloud, - $this->datastoreAcli, - $this->cloudCredentials, - $this->telemetryHelper, - $this->acliRepoRoot, - $this->clientServiceProphecy->reveal(), - $this->sshHelper, - $this->sshDir, - $this->logger, - $this->httpClientProphecy->reveal() - ); - } + /** + * @group brokenProphecy + */ + public function testCreate(): void + { + $applicationsResponse = $this->mockRequest('getApplications'); + $applicationUuid = $applicationsResponse[self::$INPUT_DEFAULT_CHOICE]->uuid; + $this->mockRequest('getApplicationByUuid', $applicationUuid); + $this->mockRequest('getAccount'); + $this->mockRequest( + 'postApplicationsIde', + $applicationUuid, + ['json' => ['label' => 'Example IDE']], + 'IDE created' + ); + $this->mockRequest('getIde', '1792767d-1ee3-4b5f-83a8-334dfdc2b8a3'); - /** - * @group brokenProphecy - */ - public function testCreate(): void { - $applicationsResponse = $this->mockRequest('getApplications'); - $applicationUuid = $applicationsResponse[self::$INPUT_DEFAULT_CHOICE]->uuid; - $this->mockRequest('getApplicationByUuid', $applicationUuid); - $this->mockRequest('getAccount'); - $this->mockRequest( - 'postApplicationsIde', - $applicationUuid, - ['json' => ['label' => 'Example IDE']], - 'IDE created' - ); - $this->mockRequest('getIde', '1792767d-1ee3-4b5f-83a8-334dfdc2b8a3'); + /** @var \Prophecy\Prophecy\ObjectProphecy|\GuzzleHttp\Psr7\Response $guzzleResponse */ + $guzzleResponse = $this->prophet->prophesize(Response::class); + $guzzleResponse->getStatusCode()->willReturn(200); + $this->httpClientProphecy->request('GET', 'https://215824ff-272a-4a8c-9027-df32ed1d68a9.ides.acquia.com/health', ['http_errors' => false])->willReturn($guzzleResponse->reveal())->shouldBeCalled(); - /** @var \Prophecy\Prophecy\ObjectProphecy|\GuzzleHttp\Psr7\Response $guzzleResponse */ - $guzzleResponse = $this->prophet->prophesize(Response::class); - $guzzleResponse->getStatusCode()->willReturn(200); - $this->httpClientProphecy->request('GET', 'https://215824ff-272a-4a8c-9027-df32ed1d68a9.ides.acquia.com/health', ['http_errors' => FALSE])->willReturn($guzzleResponse->reveal())->shouldBeCalled(); - - $inputs = [ - // Would you like Acquia CLI to search for a Cloud application that matches your local git config? - // Would you like to link the project at ... ? - 'n', - 0, - self::$INPUT_DEFAULT_CHOICE, - // Enter a label for your Cloud IDE: - 'Example IDE', - ]; - $this->executeCommand([], $inputs); - - // Assert. - $output = $this->getDisplay(); - $this->assertStringContainsString('Select a Cloud Platform application:', $output); - $this->assertStringContainsString(' [0] Sample application 1', $output); - $this->assertStringContainsString(' [1] Sample application 2', $output); - $this->assertStringContainsString("Enter the label for the IDE (option --label) [Jane Doe's IDE]:", $output); - $this->assertStringContainsString('Your IDE is ready!', $output); - $this->assertStringContainsString('Your IDE URL: https://215824ff-272a-4a8c-9027-df32ed1d68a9.ides.acquia.com', $output); - $this->assertStringContainsString('Your Drupal Site URL: https://ide-215824ff-272a-4a8c-9027-df32ed1d68a9.prod.acquia-sites.com', $output); - } + $inputs = [ + // Would you like Acquia CLI to search for a Cloud application that matches your local git config? + // Would you like to link the project at ... ? + 'n', + 0, + self::$INPUT_DEFAULT_CHOICE, + // Enter a label for your Cloud IDE: + 'Example IDE', + ]; + $this->executeCommand([], $inputs); + // Assert. + $output = $this->getDisplay(); + $this->assertStringContainsString('Select a Cloud Platform application:', $output); + $this->assertStringContainsString(' [0] Sample application 1', $output); + $this->assertStringContainsString(' [1] Sample application 2', $output); + $this->assertStringContainsString("Enter the label for the IDE (option --label) [Jane Doe's IDE]:", $output); + $this->assertStringContainsString('Your IDE is ready!', $output); + $this->assertStringContainsString('Your IDE URL: https://215824ff-272a-4a8c-9027-df32ed1d68a9.ides.acquia.com', $output); + $this->assertStringContainsString('Your Drupal Site URL: https://ide-215824ff-272a-4a8c-9027-df32ed1d68a9.prod.acquia-sites.com', $output); + } } diff --git a/tests/phpunit/src/Commands/Ide/IdeDeleteCommandTest.php b/tests/phpunit/src/Commands/Ide/IdeDeleteCommandTest.php index fc446f66f..59c186bd1 100644 --- a/tests/phpunit/src/Commands/Ide/IdeDeleteCommandTest.php +++ b/tests/phpunit/src/Commands/Ide/IdeDeleteCommandTest.php @@ -1,6 +1,6 @@ getCommandTester(); - $this->application->addCommands([ - $this->injectCommand(SshKeyDeleteCommand::class), - ]); - } - - protected function createCommand(): CommandBase { - return $this->injectCommand(IdeDeleteCommand::class); - } - - public function testIdeDeleteCommand(): void { - $applications = $this->mockRequest('getApplications'); - $this->mockRequest('getApplicationByUuid', $applications[0]->uuid); - $ides = $this->mockRequest('getApplicationIdes', $applications[0]->uuid); - $this->mockRequest('deleteIde', $ides[0]->uuid, NULL, 'De-provisioning IDE'); - $sshKeyGetResponse = $this->mockListSshKeysRequestWithIdeKey($ides[0]->label, $ides[0]->uuid); - - $this->mockDeleteSshKeyRequest($sshKeyGetResponse->{'_embedded'}->items[0]->uuid); - - $inputs = [ - // Would you like Acquia CLI to search for a Cloud application that matches your local git config? - 'n', - // Select the application for which you'd like to create a new IDE. - 0, - // Would you like to link the project at ... ? - 'y', - // Select the IDE you'd like to delete: - 0, - // Are you sure you want to delete ExampleIDE? - 'y', - ]; - - $this->executeCommand([], $inputs); - - // Assert. - $output = $this->getDisplay(); - $this->assertStringContainsString('The Cloud IDE is being deleted.', $output); - } - - public function testIdeDeleteByUuid(): void { - $this->mockRequest('getIde', IdeHelper::$remoteIdeUuid); - $this->mockRequest('deleteIde', IdeHelper::$remoteIdeUuid, NULL, 'De-provisioning IDE'); - $sshKeyGetResponse = $this->mockListSshKeysRequestWithIdeKey(IdeHelper::$remoteIdeLabel, IdeHelper::$remoteIdeUuid); - - $this->mockDeleteSshKeyRequest($sshKeyGetResponse->{'_embedded'}->items[0]->uuid); - - $inputs = [ - // Would you like to delete the SSH key associated with this IDE from your Cloud Platform account? - 'y', - ]; - - $this->executeCommand(['--uuid' => IdeHelper::$remoteIdeUuid], $inputs); - - // Assert. - $output = $this->getDisplay(); - $this->assertStringContainsString('The Cloud IDE is being deleted.', $output); - } - - public function testIdeDeleteNeverMind(): void { - $applications = $this->mockRequest('getApplications'); - $this->mockRequest('getApplicationByUuid', $applications[0]->uuid); - $this->mockRequest('getApplicationIdes', $applications[0]->uuid); - $inputs = [ - // Would you like Acquia CLI to search for a Cloud application that matches your local git config? - 'n', - // Select the application for which you'd like to create a new IDE. - 0, - // Would you like to link the project at ... ? - 'y', - // Select the IDE you'd like to delete: - 0, - // Are you sure you want to delete ExampleIDE? - 'n', - ]; - - $this->executeCommand([], $inputs); - - // Assert. - $output = $this->getDisplay(); - $this->assertStringContainsString('Ok, never mind.', $output); - } - +class IdeDeleteCommandTest extends CommandTestBase +{ + /** + * This method is called before each test. + */ + public function setUp(OutputInterface $output = null): void + { + parent::setUp(); + $this->getCommandTester(); + $this->application->addCommands([ + $this->injectCommand(SshKeyDeleteCommand::class), + ]); + } + + protected function createCommand(): CommandBase + { + return $this->injectCommand(IdeDeleteCommand::class); + } + + public function testIdeDeleteCommand(): void + { + $applications = $this->mockRequest('getApplications'); + $this->mockRequest('getApplicationByUuid', $applications[0]->uuid); + $ides = $this->mockRequest('getApplicationIdes', $applications[0]->uuid); + $this->mockRequest('deleteIde', $ides[0]->uuid, null, 'De-provisioning IDE'); + $sshKeyGetResponse = $this->mockListSshKeysRequestWithIdeKey($ides[0]->label, $ides[0]->uuid); + + $this->mockDeleteSshKeyRequest($sshKeyGetResponse->{'_embedded'}->items[0]->uuid); + + $inputs = [ + // Would you like Acquia CLI to search for a Cloud application that matches your local git config? + 'n', + // Select the application for which you'd like to create a new IDE. + 0, + // Would you like to link the project at ... ? + 'y', + // Select the IDE you'd like to delete: + 0, + // Are you sure you want to delete ExampleIDE? + 'y', + ]; + + $this->executeCommand([], $inputs); + + // Assert. + $output = $this->getDisplay(); + $this->assertStringContainsString('The Cloud IDE is being deleted.', $output); + } + + public function testIdeDeleteByUuid(): void + { + $this->mockRequest('getIde', IdeHelper::$remoteIdeUuid); + $this->mockRequest('deleteIde', IdeHelper::$remoteIdeUuid, null, 'De-provisioning IDE'); + $sshKeyGetResponse = $this->mockListSshKeysRequestWithIdeKey(IdeHelper::$remoteIdeLabel, IdeHelper::$remoteIdeUuid); + + $this->mockDeleteSshKeyRequest($sshKeyGetResponse->{'_embedded'}->items[0]->uuid); + + $inputs = [ + // Would you like to delete the SSH key associated with this IDE from your Cloud Platform account? + 'y', + ]; + + $this->executeCommand(['--uuid' => IdeHelper::$remoteIdeUuid], $inputs); + + // Assert. + $output = $this->getDisplay(); + $this->assertStringContainsString('The Cloud IDE is being deleted.', $output); + } + + public function testIdeDeleteNeverMind(): void + { + $applications = $this->mockRequest('getApplications'); + $this->mockRequest('getApplicationByUuid', $applications[0]->uuid); + $this->mockRequest('getApplicationIdes', $applications[0]->uuid); + $inputs = [ + // Would you like Acquia CLI to search for a Cloud application that matches your local git config? + 'n', + // Select the application for which you'd like to create a new IDE. + 0, + // Would you like to link the project at ... ? + 'y', + // Select the IDE you'd like to delete: + 0, + // Are you sure you want to delete ExampleIDE? + 'n', + ]; + + $this->executeCommand([], $inputs); + + // Assert. + $output = $this->getDisplay(); + $this->assertStringContainsString('Ok, never mind.', $output); + } } diff --git a/tests/phpunit/src/Commands/Ide/IdeHelper.php b/tests/phpunit/src/Commands/Ide/IdeHelper.php index 3f261723a..f5f9773a7 100644 --- a/tests/phpunit/src/Commands/Ide/IdeHelper.php +++ b/tests/phpunit/src/Commands/Ide/IdeHelper.php @@ -1,34 +1,36 @@ - */ - public static function getEnvVars(): array { - return [ - 'ACQUIA_USER_UUID' => '4acf8956-45df-3cf4-5106-065b62cf1ac8', - 'AH_SITE_ENVIRONMENT' => 'IDE', - 'REMOTEIDE_LABEL' => self::$remoteIdeLabel, - 'REMOTEIDE_UUID' => self::$remoteIdeUuid, - ]; - } - +class IdeHelper +{ + public static string $remoteIdeUuid = '215824ff-272a-4a8c-9027-df32ed1d68a9'; + public static string $remoteIdeLabel = 'ExampleIDE'; + + public static function setCloudIdeEnvVars(): void + { + TestBase::setEnvVars(self::getEnvVars()); + } + + public static function unsetCloudIdeEnvVars(): void + { + TestBase::unsetEnvVars(self::getEnvVars()); + } + + /** + * @return array + */ + public static function getEnvVars(): array + { + return [ + 'ACQUIA_USER_UUID' => '4acf8956-45df-3cf4-5106-065b62cf1ac8', + 'AH_SITE_ENVIRONMENT' => 'IDE', + 'REMOTEIDE_LABEL' => self::$remoteIdeLabel, + 'REMOTEIDE_UUID' => self::$remoteIdeUuid, + ]; + } } diff --git a/tests/phpunit/src/Commands/Ide/IdeInfoCommandTest.php b/tests/phpunit/src/Commands/Ide/IdeInfoCommandTest.php index f7db8153d..6fca529b1 100644 --- a/tests/phpunit/src/Commands/Ide/IdeInfoCommandTest.php +++ b/tests/phpunit/src/Commands/Ide/IdeInfoCommandTest.php @@ -1,6 +1,6 @@ injectCommand(IdeInfoCommand::class); - } - - /** - * @group brokenProphecy - */ - public function testIdeInfoCommand(): void { - $applications = $this->mockRequest('getApplications'); - $this->mockRequest('getApplicationByUuid', $applications[0]->uuid); - $ides = $this->mockRequest('getApplicationIdes', $applications[0]->uuid); - $this->mockRequest('getIde', $ides[0]->uuid); - $inputs = [ - // Would you like Acquia CLI to search for a Cloud application that matches your local git config? - 'n', - // Select the application. - 0, - // Would you like to link the project at ... ? - 'y', - // Select an IDE ... - 0, - ]; - $this->executeCommand([], $inputs); - - // Assert. - $output = $this->getDisplay(); - $this->assertStringContainsString('Select a Cloud Platform application:', $output); - $this->assertStringContainsString('[0] Sample application 1', $output); - $this->assertStringContainsString('[1] Sample application 2', $output); - $this->assertStringContainsString('IDE property IDE value', $output); - $this->assertStringContainsString('UUID 215824ff-272a-4a8c-9027-df32ed1d68a9', $output); - $this->assertStringContainsString('Label Example IDE', $output); - } - +class IdeInfoCommandTest extends CommandTestBase +{ + protected function createCommand(): CommandBase + { + return $this->injectCommand(IdeInfoCommand::class); + } + + /** + * @group brokenProphecy + */ + public function testIdeInfoCommand(): void + { + $applications = $this->mockRequest('getApplications'); + $this->mockRequest('getApplicationByUuid', $applications[0]->uuid); + $ides = $this->mockRequest('getApplicationIdes', $applications[0]->uuid); + $this->mockRequest('getIde', $ides[0]->uuid); + $inputs = [ + // Would you like Acquia CLI to search for a Cloud application that matches your local git config? + 'n', + // Select the application. + 0, + // Would you like to link the project at ... ? + 'y', + // Select an IDE ... + 0, + ]; + $this->executeCommand([], $inputs); + + // Assert. + $output = $this->getDisplay(); + $this->assertStringContainsString('Select a Cloud Platform application:', $output); + $this->assertStringContainsString('[0] Sample application 1', $output); + $this->assertStringContainsString('[1] Sample application 2', $output); + $this->assertStringContainsString('IDE property IDE value', $output); + $this->assertStringContainsString('UUID 215824ff-272a-4a8c-9027-df32ed1d68a9', $output); + $this->assertStringContainsString('Label Example IDE', $output); + } } diff --git a/tests/phpunit/src/Commands/Ide/IdeListCommandMineTest.php b/tests/phpunit/src/Commands/Ide/IdeListCommandMineTest.php index 589f0e030..4aebefff6 100644 --- a/tests/phpunit/src/Commands/Ide/IdeListCommandMineTest.php +++ b/tests/phpunit/src/Commands/Ide/IdeListCommandMineTest.php @@ -1,6 +1,6 @@ injectCommand(IdeListMineCommand::class); - } - - public function testIdeListMineCommand(): void { - $applicationsResponse = $this->getMockResponseFromSpec('/applications', 'get', '200'); - $idesResponse = $this->mockAccountIdeListRequest(); - foreach ($idesResponse->{'_embedded'}->items as $key => $ide) { - $applicationResponse = $applicationsResponse->{'_embedded'}->items[$key]; - $appUrlParts = explode('/', $ide->_links->application->href); - $appUuid = end($appUrlParts); - $applicationResponse->uuid = $appUuid; - $this->clientProphecy->request('get', '/applications/' . $appUuid) - ->willReturn($applicationResponse) - ->shouldBeCalled(); +class IdeListCommandMineTest extends CommandTestBase +{ + protected function createCommand(): CommandBase + { + return $this->injectCommand(IdeListMineCommand::class); } - $inputs = [ - // Would you like Acquia CLI to search for a Cloud application that matches your local git config? - 'n', - // Select the application. - 0, - // Would you like to link the project at ... ? - 'y', - ]; - $this->executeCommand([], $inputs); + public function testIdeListMineCommand(): void + { + $applicationsResponse = $this->getMockResponseFromSpec('/applications', 'get', '200'); + $idesResponse = $this->mockAccountIdeListRequest(); + foreach ($idesResponse->{'_embedded'}->items as $key => $ide) { + $applicationResponse = $applicationsResponse->{'_embedded'}->items[$key]; + $appUrlParts = explode('/', $ide->_links->application->href); + $appUuid = end($appUrlParts); + $applicationResponse->uuid = $appUuid; + $this->clientProphecy->request('get', '/applications/' . $appUuid) + ->willReturn($applicationResponse) + ->shouldBeCalled(); + } - // Assert. - $output = $this->getDisplay(); - $this->assertStringContainsString('IDE Label 1', $output); - $this->assertStringContainsString('UUID: 9a83c081-ef78-4dbd-8852-11cc3eb248f7', $output); - $this->assertStringContainsString('Application: Sample application 1', $output); - $this->assertStringContainsString('Subscription: Sample subscription', $output); - $this->assertStringContainsString('IDE URL: https://9a83c081-ef78-4dbd-8852-11cc3eb248f7.ide.ahdev.cloud', $output); - $this->assertStringContainsString('Web URL: https://9a83c081-ef78-4dbd-8852-11cc3eb248f7.web.ahdev.cloud', $output); + $inputs = [ + // Would you like Acquia CLI to search for a Cloud application that matches your local git config? + 'n', + // Select the application. + 0, + // Would you like to link the project at ... ? + 'y', + ]; + $this->executeCommand([], $inputs); - $this->assertStringContainsString('IDE Label 2', $output); - $this->assertStringContainsString('UUID: 9a83c081-ef78-4dbd-8852-11cc3eb248f7', $output); - $this->assertStringContainsString('Application: Sample application 2', $output); - $this->assertStringContainsString('Subscription: Sample subscription', $output); - $this->assertStringContainsString('IDE URL: https://feea197a-9503-4441-9f49-b4d420b0ecf8.ide.ahdev.cloud', $output); - $this->assertStringContainsString('Web URL: https://feea197a-9503-4441-9f49-b4d420b0ecf8.web.ahdev.cloud', $output); - } + // Assert. + $output = $this->getDisplay(); + $this->assertStringContainsString('IDE Label 1', $output); + $this->assertStringContainsString('UUID: 9a83c081-ef78-4dbd-8852-11cc3eb248f7', $output); + $this->assertStringContainsString('Application: Sample application 1', $output); + $this->assertStringContainsString('Subscription: Sample subscription', $output); + $this->assertStringContainsString('IDE URL: https://9a83c081-ef78-4dbd-8852-11cc3eb248f7.ide.ahdev.cloud', $output); + $this->assertStringContainsString('Web URL: https://9a83c081-ef78-4dbd-8852-11cc3eb248f7.web.ahdev.cloud', $output); - protected function mockAccountIdeListRequest(): object { - $response = $this->getMockResponseFromSpec('/account/ides', - 'get', '200'); - $this->clientProphecy->request('get', - '/account/ides') - ->willReturn($response->{'_embedded'}->items) - ->shouldBeCalled(); + $this->assertStringContainsString('IDE Label 2', $output); + $this->assertStringContainsString('UUID: 9a83c081-ef78-4dbd-8852-11cc3eb248f7', $output); + $this->assertStringContainsString('Application: Sample application 2', $output); + $this->assertStringContainsString('Subscription: Sample subscription', $output); + $this->assertStringContainsString('IDE URL: https://feea197a-9503-4441-9f49-b4d420b0ecf8.ide.ahdev.cloud', $output); + $this->assertStringContainsString('Web URL: https://feea197a-9503-4441-9f49-b4d420b0ecf8.web.ahdev.cloud', $output); + } - return $response; - } + protected function mockAccountIdeListRequest(): object + { + $response = $this->getMockResponseFromSpec( + '/account/ides', + 'get', + '200' + ); + $this->clientProphecy->request( + 'get', + '/account/ides' + ) + ->willReturn($response->{'_embedded'}->items) + ->shouldBeCalled(); + return $response; + } } diff --git a/tests/phpunit/src/Commands/Ide/IdeListCommandTest.php b/tests/phpunit/src/Commands/Ide/IdeListCommandTest.php index 5f94791ff..9e975cb3b 100644 --- a/tests/phpunit/src/Commands/Ide/IdeListCommandTest.php +++ b/tests/phpunit/src/Commands/Ide/IdeListCommandTest.php @@ -1,6 +1,6 @@ injectCommand(IdeListCommand::class); + } - protected function createCommand(): CommandBase { - return $this->injectCommand(IdeListCommand::class); - } + /** + * @group brokenProphecy + */ + public function testIdeListCommand(): void + { + $applications = $this->mockRequest('getApplications'); + $application = $this->mockRequest('getApplicationByUuid', $applications[self::$INPUT_DEFAULT_CHOICE]->uuid); + $this->mockRequest('getApplicationIdes', $application->uuid); + $inputs = [ + // Would you like Acquia CLI to search for a Cloud application that matches your local git config? + 'n', + // Select the application. + self::$INPUT_DEFAULT_CHOICE, + // Would you like to link the project at ... ? + 'y', + ]; + $this->executeCommand([], $inputs); - /** - * @group brokenProphecy - */ - public function testIdeListCommand(): void { - $applications = $this->mockRequest('getApplications'); - $application = $this->mockRequest('getApplicationByUuid', $applications[self::$INPUT_DEFAULT_CHOICE]->uuid); - $this->mockRequest('getApplicationIdes', $application->uuid); - $inputs = [ - // Would you like Acquia CLI to search for a Cloud application that matches your local git config? - 'n', - // Select the application. - self::$INPUT_DEFAULT_CHOICE, - // Would you like to link the project at ... ? - 'y', - ]; - $this->executeCommand([], $inputs); + // Assert. + $output = $this->getDisplay(); + $this->assertStringContainsString('Select a Cloud Platform application:', $output); + $this->assertStringContainsString('[0] Sample application 1', $output); + $this->assertStringContainsString('[1] Sample application 2', $output); + $this->assertStringContainsString('IDE Label 1 (user.name@example.com)', $output); + $this->assertStringContainsString('Web URL: https://9a83c081-ef78-4dbd-8852-11cc3eb248f7.web.ahdev.cloud', $output); + $this->assertStringContainsString('IDE URL: https://9a83c081-ef78-4dbd-8852-11cc3eb248f7.ides.acquia.com', $output); + $this->assertStringContainsString('IDE Label 2 (user.name@example.com)', $output); + $this->assertStringContainsString('Web URL: https://feea197a-9503-4441-9f49-b4d420b0ecf8.web.ahdev.cloud', $output); + $this->assertStringContainsString('IDE URL: https://feea197a-9503-4441-9f49-b4d420b0ecf8.ides.acquia.com', $output); + } - // Assert. - $output = $this->getDisplay(); - $this->assertStringContainsString('Select a Cloud Platform application:', $output); - $this->assertStringContainsString('[0] Sample application 1', $output); - $this->assertStringContainsString('[1] Sample application 2', $output); - $this->assertStringContainsString('IDE Label 1 (user.name@example.com)', $output); - $this->assertStringContainsString('Web URL: https://9a83c081-ef78-4dbd-8852-11cc3eb248f7.web.ahdev.cloud', $output); - $this->assertStringContainsString('IDE URL: https://9a83c081-ef78-4dbd-8852-11cc3eb248f7.ides.acquia.com', $output); - $this->assertStringContainsString('IDE Label 2 (user.name@example.com)', $output); - $this->assertStringContainsString('Web URL: https://feea197a-9503-4441-9f49-b4d420b0ecf8.web.ahdev.cloud', $output); - $this->assertStringContainsString('IDE URL: https://feea197a-9503-4441-9f49-b4d420b0ecf8.ides.acquia.com', $output); - } + /** + * @group brokenProphecy + */ + public function testIdeListEmptyCommand(): void + { + $this->mockRequest('getApplications'); + $this->mockApplicationRequest(); + $this->clientProphecy->request( + 'get', + '/applications/a47ac10b-58cc-4372-a567-0e02b2c3d470/ides' + ) + ->willReturn([]) + ->shouldBeCalled(); - /** - * @group brokenProphecy - */ - public function testIdeListEmptyCommand(): void { - $this->mockRequest('getApplications'); - $this->mockApplicationRequest(); - $this->clientProphecy->request('get', - '/applications/a47ac10b-58cc-4372-a567-0e02b2c3d470/ides') - ->willReturn([]) - ->shouldBeCalled(); - - $inputs = [ - // Would you like Acquia CLI to search for a Cloud application that matches your local git config? - 'n', - // Select the application. - self::$INPUT_DEFAULT_CHOICE, - // Would you like to link the project at ... ? - 'y', - ]; - $this->executeCommand([], $inputs); - - // Assert. - $output = $this->getDisplay(); - $this->assertStringContainsString('Select a Cloud Platform application:', $output); - $this->assertStringContainsString('[0] Sample application 1', $output); - $this->assertStringContainsString('[1] Sample application 2', $output); - $this->assertStringContainsString('No IDE exists for this application.', $output); - } + $inputs = [ + // Would you like Acquia CLI to search for a Cloud application that matches your local git config? + 'n', + // Select the application. + self::$INPUT_DEFAULT_CHOICE, + // Would you like to link the project at ... ? + 'y', + ]; + $this->executeCommand([], $inputs); + // Assert. + $output = $this->getDisplay(); + $this->assertStringContainsString('Select a Cloud Platform application:', $output); + $this->assertStringContainsString('[0] Sample application 1', $output); + $this->assertStringContainsString('[1] Sample application 2', $output); + $this->assertStringContainsString('No IDE exists for this application.', $output); + } } diff --git a/tests/phpunit/src/Commands/Ide/IdeOpenCommandTest.php b/tests/phpunit/src/Commands/Ide/IdeOpenCommandTest.php index 9f6c4388d..fdad33d2c 100644 --- a/tests/phpunit/src/Commands/Ide/IdeOpenCommandTest.php +++ b/tests/phpunit/src/Commands/Ide/IdeOpenCommandTest.php @@ -1,6 +1,6 @@ injectCommand(IdeOpenCommand::class); - } - - /** - * @group brokenProphecy - */ - public function testIdeOpenCommand(): void { - $applications = $this->mockRequest('getApplications'); - $this->mockRequest('getApplicationByUuid', $applications[0]->uuid); - $this->mockRequest('getApplicationIdes', $applications[0]->uuid); - $localMachineHelper = $this->mockLocalMachineHelper(); - $localMachineHelper->isBrowserAvailable()->willReturn(TRUE); - $localMachineHelper->startBrowser('https://9a83c081-ef78-4dbd-8852-11cc3eb248f7.ides.acquia.com')->willReturn(TRUE); - - $inputs = [ - // Would you like Acquia CLI to search for a Cloud application that matches your local git config? - 'n', - // Select a Cloud Platform application: - 0, - // Would you like to link the project at ... ? - 'y', - // Select the IDE you'd like to open: - 0, - ]; - $this->executeCommand([], $inputs); - - // Assert. - $output = $this->getDisplay(); - $this->assertStringContainsString('Select a Cloud Platform application:', $output); - $this->assertStringContainsString('[0] Sample application 1', $output); - $this->assertStringContainsString('Select the IDE you\'d like to open:', $output); - $this->assertStringContainsString('[0] IDE Label 1', $output); - $this->assertStringContainsString('Your IDE URL: https://9a83c081-ef78-4dbd-8852-11cc3eb248f7.ides.acquia.com', $output); - $this->assertStringContainsString('Your Drupal Site URL: https://9a83c081-ef78-4dbd-8852-11cc3eb248f7.web.ahdev.cloud', $output); - $this->assertStringContainsString('Opening your IDE in browser...', $output); - } - +class IdeOpenCommandTest extends CommandTestBase +{ + protected function createCommand(): CommandBase + { + return $this->injectCommand(IdeOpenCommand::class); + } + + /** + * @group brokenProphecy + */ + public function testIdeOpenCommand(): void + { + $applications = $this->mockRequest('getApplications'); + $this->mockRequest('getApplicationByUuid', $applications[0]->uuid); + $this->mockRequest('getApplicationIdes', $applications[0]->uuid); + $localMachineHelper = $this->mockLocalMachineHelper(); + $localMachineHelper->isBrowserAvailable()->willReturn(true); + $localMachineHelper->startBrowser('https://9a83c081-ef78-4dbd-8852-11cc3eb248f7.ides.acquia.com')->willReturn(true); + + $inputs = [ + // Would you like Acquia CLI to search for a Cloud application that matches your local git config? + 'n', + // Select a Cloud Platform application: + 0, + // Would you like to link the project at ... ? + 'y', + // Select the IDE you'd like to open: + 0, + ]; + $this->executeCommand([], $inputs); + + // Assert. + $output = $this->getDisplay(); + $this->assertStringContainsString('Select a Cloud Platform application:', $output); + $this->assertStringContainsString('[0] Sample application 1', $output); + $this->assertStringContainsString('Select the IDE you\'d like to open:', $output); + $this->assertStringContainsString('[0] IDE Label 1', $output); + $this->assertStringContainsString('Your IDE URL: https://9a83c081-ef78-4dbd-8852-11cc3eb248f7.ides.acquia.com', $output); + $this->assertStringContainsString('Your Drupal Site URL: https://9a83c081-ef78-4dbd-8852-11cc3eb248f7.web.ahdev.cloud', $output); + $this->assertStringContainsString('Opening your IDE in browser...', $output); + } } diff --git a/tests/phpunit/src/Commands/Ide/IdePhpVersionCommandTest.php b/tests/phpunit/src/Commands/Ide/IdePhpVersionCommandTest.php index f7e0337fd..e9f258b3b 100644 --- a/tests/phpunit/src/Commands/Ide/IdePhpVersionCommandTest.php +++ b/tests/phpunit/src/Commands/Ide/IdePhpVersionCommandTest.php @@ -1,6 +1,6 @@ injectCommand(IdePhpVersionCommand::class); - } - - /** - * @return array - */ - public function providerTestIdePhpVersionCommand(): array { - return [ - ['7.4'], - ['8.0'], - ['8.1'], - ]; - } - - /** - * @dataProvider providerTestIdePhpVersionCommand - */ - public function testIdePhpVersionCommand(string $version): void { - $localMachineHelper = $this->mockLocalMachineHelper(); - $this->mockRestartPhp($localMachineHelper); - $mockFileSystem = $this->mockGetFilesystem($localMachineHelper); - $phpFilepathPrefix = $this->fs->tempnam(sys_get_temp_dir(), 'acli_php_stub_'); - $phpStubFilepath = $phpFilepathPrefix . $version; - $mockFileSystem->exists($phpStubFilepath)->willReturn(TRUE); - $phpVersionFilePath = $this->fs->tempnam(sys_get_temp_dir(), 'acli_php_version_file_'); - $mockFileSystem->dumpFile($phpVersionFilePath, $version)->shouldBeCalled(); - - $this->command->setPhpVersionFilePath($phpVersionFilePath); - $this->command->setIdePhpFilePathPrefix($phpFilepathPrefix); - $this->executeCommand([ - 'version' => $version, - ], []); - - } - - /** - * @return array - */ - public function providerTestIdePhpVersionCommandFailure(): array { - return [ - ['6.3', AcquiaCliException::class], - ['6', ValidatorException::class], - ['7', ValidatorException::class], - ['7.', ValidatorException::class], - ]; - } - - /** - * @dataProvider providerTestIdePhpVersionCommandFailure - */ - public function testIdePhpVersionCommandFailure(string $version, string $exceptionClass): void { - $this->expectException($exceptionClass); - $this->executeCommand([ - 'version' => $version, - ]); - } - - public function testIdePhpVersionCommandOutsideIde(): void { - IdeHelper::unsetCloudIdeEnvVars(); - $this->expectException(AcquiaCliException::class); - $this->expectExceptionMessage('This command can only be run inside of an Acquia Cloud IDE'); - $this->executeCommand([ - 'version' => '7.3', - ]); - } - - protected function mockRestartPhp(ObjectProphecy|LocalMachineHelper $localMachineHelper): ObjectProphecy { - $process = $this->prophet->prophesize(Process::class); - $process->isSuccessful()->willReturn(TRUE); - $process->getExitCode()->willReturn(0); - $localMachineHelper->execute([ +class IdePhpVersionCommandTest extends CommandTestBase +{ + use IdeRequiredTestTrait; + + protected function createCommand(): CommandBase + { + return $this->injectCommand(IdePhpVersionCommand::class); + } + + /** + * @return array + */ + public function providerTestIdePhpVersionCommand(): array + { + return [ + ['7.4'], + ['8.0'], + ['8.1'], + ]; + } + + /** + * @dataProvider providerTestIdePhpVersionCommand + */ + public function testIdePhpVersionCommand(string $version): void + { + $localMachineHelper = $this->mockLocalMachineHelper(); + $this->mockRestartPhp($localMachineHelper); + $mockFileSystem = $this->mockGetFilesystem($localMachineHelper); + $phpFilepathPrefix = $this->fs->tempnam(sys_get_temp_dir(), 'acli_php_stub_'); + $phpStubFilepath = $phpFilepathPrefix . $version; + $mockFileSystem->exists($phpStubFilepath)->willReturn(true); + $phpVersionFilePath = $this->fs->tempnam(sys_get_temp_dir(), 'acli_php_version_file_'); + $mockFileSystem->dumpFile($phpVersionFilePath, $version)->shouldBeCalled(); + + $this->command->setPhpVersionFilePath($phpVersionFilePath); + $this->command->setIdePhpFilePathPrefix($phpFilepathPrefix); + $this->executeCommand([ + 'version' => $version, + ], []); + } + + /** + * @return array + */ + public function providerTestIdePhpVersionCommandFailure(): array + { + return [ + ['6.3', AcquiaCliException::class], + ['6', ValidatorException::class], + ['7', ValidatorException::class], + ['7.', ValidatorException::class], + ]; + } + + /** + * @dataProvider providerTestIdePhpVersionCommandFailure + */ + public function testIdePhpVersionCommandFailure(string $version, string $exceptionClass): void + { + $this->expectException($exceptionClass); + $this->executeCommand([ + 'version' => $version, + ]); + } + + public function testIdePhpVersionCommandOutsideIde(): void + { + IdeHelper::unsetCloudIdeEnvVars(); + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage('This command can only be run inside of an Acquia Cloud IDE'); + $this->executeCommand([ + 'version' => '7.3', + ]); + } + + protected function mockRestartPhp(ObjectProphecy|LocalMachineHelper $localMachineHelper): ObjectProphecy + { + $process = $this->prophet->prophesize(Process::class); + $process->isSuccessful()->willReturn(true); + $process->getExitCode()->willReturn(0); + $localMachineHelper->execute([ 'supervisorctl', 'restart', 'php-fpm', - ], NULL, NULL, FALSE)->willReturn($process->reveal())->shouldBeCalled(); - return $process; - } - - /** - * @return \Prophecy\Prophecy\ObjectProphecy|\Symfony\Component\Filesystem\Filesystem - */ - protected function mockGetFilesystem(ObjectProphecy|LocalMachineHelper $localMachineHelper): ObjectProphecy|Filesystem { - $fileSystem = $this->prophet->prophesize(Filesystem::class); - $localMachineHelper->getFilesystem()->willReturn($fileSystem)->shouldBeCalled(); - - return $fileSystem; - } - + ], null, null, false)->willReturn($process->reveal())->shouldBeCalled(); + return $process; + } + + /** + * @return \Prophecy\Prophecy\ObjectProphecy|\Symfony\Component\Filesystem\Filesystem + */ + protected function mockGetFilesystem(ObjectProphecy|LocalMachineHelper $localMachineHelper): ObjectProphecy|Filesystem + { + $fileSystem = $this->prophet->prophesize(Filesystem::class); + $localMachineHelper->getFilesystem()->willReturn($fileSystem)->shouldBeCalled(); + + return $fileSystem; + } } diff --git a/tests/phpunit/src/Commands/Ide/IdeRequiredTestTrait.php b/tests/phpunit/src/Commands/Ide/IdeRequiredTestTrait.php index db1024dfa..d1acda600 100644 --- a/tests/phpunit/src/Commands/Ide/IdeRequiredTestTrait.php +++ b/tests/phpunit/src/Commands/Ide/IdeRequiredTestTrait.php @@ -1,22 +1,23 @@ injectCommand(IdeServiceRestartCommand::class); - } - - public function testIdeServiceRestartCommand(): void { - $localMachineHelper = $this->mockLocalMachineHelper(); - $this->mockRestartPhp($localMachineHelper); - - $this->executeCommand(['service' => 'php'], []); - - // Assert. - $output = $this->getDisplay(); - $this->assertStringContainsString('Restarted php', $output); - } - - /** - * @group brokenProphecy - */ - public function testIdeServiceRestartCommandInvalid(): void { - $localMachineHelper = $this->mockLocalMachineHelper(); - $this->mockRestartPhp($localMachineHelper); - - $this->expectException(ValidatorException::class); - $this->expectExceptionMessage('Specify a valid service name'); - $this->executeCommand(['service' => 'rambulator'], []); - } - +class IdeServiceRestartCommandTest extends CommandTestBase +{ + use IdeRequiredTestTrait; + + protected function createCommand(): CommandBase + { + return $this->injectCommand(IdeServiceRestartCommand::class); + } + + public function testIdeServiceRestartCommand(): void + { + $localMachineHelper = $this->mockLocalMachineHelper(); + $this->mockRestartPhp($localMachineHelper); + + $this->executeCommand(['service' => 'php'], []); + + // Assert. + $output = $this->getDisplay(); + $this->assertStringContainsString('Restarted php', $output); + } + + /** + * @group brokenProphecy + */ + public function testIdeServiceRestartCommandInvalid(): void + { + $localMachineHelper = $this->mockLocalMachineHelper(); + $this->mockRestartPhp($localMachineHelper); + + $this->expectException(ValidatorException::class); + $this->expectExceptionMessage('Specify a valid service name'); + $this->executeCommand(['service' => 'rambulator'], []); + } } diff --git a/tests/phpunit/src/Commands/Ide/IdeServiceStartCommandTest.php b/tests/phpunit/src/Commands/Ide/IdeServiceStartCommandTest.php index 88c19d6f5..bcc2842a8 100644 --- a/tests/phpunit/src/Commands/Ide/IdeServiceStartCommandTest.php +++ b/tests/phpunit/src/Commands/Ide/IdeServiceStartCommandTest.php @@ -1,6 +1,6 @@ injectCommand(IdeServiceStartCommand::class); - } - - public function testIdeServiceStartCommand(): void { - $localMachineHelper = $this->mockLocalMachineHelper(); - $this->mockStartPhp($localMachineHelper); - - $this->executeCommand(['service' => 'php'], []); - - // Assert. - $output = $this->getDisplay(); - $this->assertStringContainsString('Starting php', $output); - } - - /** - * @group brokenProphecy - */ - public function testIdeServiceStartCommandInvalid(): void { - $localMachineHelper = $this->mockLocalMachineHelper(); - $this->mockStartPhp($localMachineHelper); - - $this->expectException(ValidatorException::class); - $this->expectExceptionMessage('Specify a valid service name'); - $this->executeCommand(['service' => 'rambulator'], []); - } - +class IdeServiceStartCommandTest extends CommandTestBase +{ + use IdeRequiredTestTrait; + + protected function createCommand(): CommandBase + { + return $this->injectCommand(IdeServiceStartCommand::class); + } + + public function testIdeServiceStartCommand(): void + { + $localMachineHelper = $this->mockLocalMachineHelper(); + $this->mockStartPhp($localMachineHelper); + + $this->executeCommand(['service' => 'php'], []); + + // Assert. + $output = $this->getDisplay(); + $this->assertStringContainsString('Starting php', $output); + } + + /** + * @group brokenProphecy + */ + public function testIdeServiceStartCommandInvalid(): void + { + $localMachineHelper = $this->mockLocalMachineHelper(); + $this->mockStartPhp($localMachineHelper); + + $this->expectException(ValidatorException::class); + $this->expectExceptionMessage('Specify a valid service name'); + $this->executeCommand(['service' => 'rambulator'], []); + } } diff --git a/tests/phpunit/src/Commands/Ide/IdeServiceStopCommandTest.php b/tests/phpunit/src/Commands/Ide/IdeServiceStopCommandTest.php index 96eda23be..00964e835 100644 --- a/tests/phpunit/src/Commands/Ide/IdeServiceStopCommandTest.php +++ b/tests/phpunit/src/Commands/Ide/IdeServiceStopCommandTest.php @@ -1,6 +1,6 @@ injectCommand(IdeServiceStopCommand::class); - } - - public function testIdeServiceStopCommand(): void { - $localMachineHelper = $this->mockLocalMachineHelper(); - $this->mockStopPhp($localMachineHelper); - - $this->executeCommand(['service' => 'php'], []); - - // Assert. - $output = $this->getDisplay(); - $this->assertStringContainsString('Stopping php', $output); - } - - /** - * @group brokenProphecy - */ - public function testIdeServiceStopCommandInvalid(): void { - $localMachineHelper = $this->mockLocalMachineHelper(); - $this->mockStopPhp($localMachineHelper); - - $this->expectException(ValidatorException::class); - $this->expectExceptionMessage('Specify a valid service name'); - $this->executeCommand(['service' => 'rambulator'], []); - } - +class IdeServiceStopCommandTest extends CommandTestBase +{ + use IdeRequiredTestTrait; + + protected function createCommand(): CommandBase + { + return $this->injectCommand(IdeServiceStopCommand::class); + } + + public function testIdeServiceStopCommand(): void + { + $localMachineHelper = $this->mockLocalMachineHelper(); + $this->mockStopPhp($localMachineHelper); + + $this->executeCommand(['service' => 'php'], []); + + // Assert. + $output = $this->getDisplay(); + $this->assertStringContainsString('Stopping php', $output); + } + + /** + * @group brokenProphecy + */ + public function testIdeServiceStopCommandInvalid(): void + { + $localMachineHelper = $this->mockLocalMachineHelper(); + $this->mockStopPhp($localMachineHelper); + + $this->expectException(ValidatorException::class); + $this->expectExceptionMessage('Specify a valid service name'); + $this->executeCommand(['service' => 'rambulator'], []); + } } diff --git a/tests/phpunit/src/Commands/Ide/IdeShareCommandTest.php b/tests/phpunit/src/Commands/Ide/IdeShareCommandTest.php index f0bbf6e0a..09f76aa57 100644 --- a/tests/phpunit/src/Commands/Ide/IdeShareCommandTest.php +++ b/tests/phpunit/src/Commands/Ide/IdeShareCommandTest.php @@ -1,6 +1,6 @@ - */ - private array $shareCodeFilepaths; - - private string $shareCode; - - /** - * This method is called before each test. - */ - public function setUp(OutputInterface $output = NULL): void { - parent::setUp(); - $this->shareCode = 'a47ac10b-58cc-4372-a567-0e02b2c3d470'; - $shareCodeFilepath = $this->fs->tempnam(sys_get_temp_dir(), 'acli_share_uuid_'); - $this->fs->dumpFile($shareCodeFilepath, $this->shareCode); - $this->command->setShareCodeFilepaths([$shareCodeFilepath]); - IdeHelper::setCloudIdeEnvVars(); - } - - protected function createCommand(): CommandBase { - return $this->injectCommand(IdeShareCommand::class); - } - - public function testIdeShareCommand(): void { - $this->executeCommand(); - $output = $this->getDisplay(); - $this->assertStringContainsString('Your IDE Share URL: ', $output); - $this->assertStringContainsString($this->shareCode, $output); - } - - public function testIdeShareRegenerateCommand(): void { - $this->executeCommand(['--regenerate' => TRUE]); - $output = $this->getDisplay(); - $this->assertStringContainsString('Your IDE Share URL: ', $output); - $this->assertStringNotContainsString($this->shareCode, $output); - } - +class IdeShareCommandTest extends CommandTestBase +{ + use IdeRequiredTestTrait; + + /** + * @var array + */ + private array $shareCodeFilepaths; + + private string $shareCode; + + /** + * This method is called before each test. + */ + public function setUp(OutputInterface $output = null): void + { + parent::setUp(); + $this->shareCode = 'a47ac10b-58cc-4372-a567-0e02b2c3d470'; + $shareCodeFilepath = $this->fs->tempnam(sys_get_temp_dir(), 'acli_share_uuid_'); + $this->fs->dumpFile($shareCodeFilepath, $this->shareCode); + $this->command->setShareCodeFilepaths([$shareCodeFilepath]); + IdeHelper::setCloudIdeEnvVars(); + } + + protected function createCommand(): CommandBase + { + return $this->injectCommand(IdeShareCommand::class); + } + + public function testIdeShareCommand(): void + { + $this->executeCommand(); + $output = $this->getDisplay(); + $this->assertStringContainsString('Your IDE Share URL: ', $output); + $this->assertStringContainsString($this->shareCode, $output); + } + + public function testIdeShareRegenerateCommand(): void + { + $this->executeCommand(['--regenerate' => true]); + $output = $this->getDisplay(); + $this->assertStringContainsString('Your IDE Share URL: ', $output); + $this->assertStringNotContainsString($this->shareCode, $output); + } } diff --git a/tests/phpunit/src/Commands/Ide/IdeXdebugToggleCommandTest.php b/tests/phpunit/src/Commands/Ide/IdeXdebugToggleCommandTest.php index 976281515..89da557cb 100644 --- a/tests/phpunit/src/Commands/Ide/IdeXdebugToggleCommandTest.php +++ b/tests/phpunit/src/Commands/Ide/IdeXdebugToggleCommandTest.php @@ -1,6 +1,6 @@ xdebugFilePath = $this->fs->tempnam(sys_get_temp_dir(), 'acli_xdebug_ini_'); + $this->fs->copy($this->realFixtureDir . '/xdebug.ini', $this->xdebugFilePath, true); + $this->command->setXdebugIniFilepath($this->xdebugFilePath); - public function setUpXdebug(string $phpVersion): void { - $this->xdebugFilePath = $this->fs->tempnam(sys_get_temp_dir(), 'acli_xdebug_ini_'); - $this->fs->copy($this->realFixtureDir . '/xdebug.ini', $this->xdebugFilePath, TRUE); - $this->command->setXdebugIniFilepath($this->xdebugFilePath); - - $process = $this->prophet->prophesize(Process::class); - $process->isSuccessful()->willReturn(TRUE); - $process->getExitCode()->willReturn(0); - $localMachineHelper = $this->mockLocalMachineHelper(); - $localMachineHelper - ->execute([ + $process = $this->prophet->prophesize(Process::class); + $process->isSuccessful()->willReturn(true); + $process->getExitCode()->willReturn(0); + $localMachineHelper = $this->mockLocalMachineHelper(); + $localMachineHelper + ->execute([ 'supervisorctl', 'restart', 'php-fpm', - ], NULL, NULL, FALSE) - ->willReturn($process->reveal()) - ->shouldBeCalled(); - - } - - protected function createCommand(): CommandBase { - return $this->injectCommand(IdeXdebugToggleCommand::class); - } + ], null, null, false) + ->willReturn($process->reveal()) + ->shouldBeCalled(); + } - /** - * @return array - */ - public function providerTestXdebugCommandEnable(): array { - return [ - ['7.4'], - ['8.0'], - ['8.1'], - ]; - } + protected function createCommand(): CommandBase + { + return $this->injectCommand(IdeXdebugToggleCommand::class); + } - /** - * @dataProvider providerTestXdebugCommandEnable - */ - public function testXdebugCommandEnable(mixed $phpVersion): void { - $this->setUpXdebug($phpVersion); - $this->executeCommand(); + /** + * @return array + */ + public function providerTestXdebugCommandEnable(): array + { + return [ + ['7.4'], + ['8.0'], + ['8.1'], + ]; + } - $this->assertFileExists($this->xdebugFilePath); - $this->assertStringContainsString('zend_extension=xdebug.so', file_get_contents($this->xdebugFilePath)); - $this->assertStringNotContainsString(';zend_extension=xdebug.so', file_get_contents($this->xdebugFilePath)); - $this->assertStringContainsString("Xdebug PHP extension enabled", $this->getDisplay()); - } + /** + * @dataProvider providerTestXdebugCommandEnable + */ + public function testXdebugCommandEnable(mixed $phpVersion): void + { + $this->setUpXdebug($phpVersion); + $this->executeCommand(); - /** - * @dataProvider providerTestXdebugCommandEnable - */ - public function testXdebugCommandDisable(mixed $phpVersion): void { - $this->setUpXdebug($phpVersion); - // Modify fixture to disable xdebug. - file_put_contents($this->xdebugFilePath, str_replace(';zend_extension=xdebug.so', 'zend_extension=xdebug.so', file_get_contents($this->xdebugFilePath))); - $this->executeCommand(); - $this->assertFileExists($this->xdebugFilePath); - $this->assertStringContainsString(';zend_extension=xdebug.so', file_get_contents($this->xdebugFilePath)); - $this->assertStringContainsString("Xdebug PHP extension disabled", $this->getDisplay()); - } + $this->assertFileExists($this->xdebugFilePath); + $this->assertStringContainsString('zend_extension=xdebug.so', file_get_contents($this->xdebugFilePath)); + $this->assertStringNotContainsString(';zend_extension=xdebug.so', file_get_contents($this->xdebugFilePath)); + $this->assertStringContainsString("Xdebug PHP extension enabled", $this->getDisplay()); + } + /** + * @dataProvider providerTestXdebugCommandEnable + */ + public function testXdebugCommandDisable(mixed $phpVersion): void + { + $this->setUpXdebug($phpVersion); + // Modify fixture to disable xdebug. + file_put_contents($this->xdebugFilePath, str_replace(';zend_extension=xdebug.so', 'zend_extension=xdebug.so', file_get_contents($this->xdebugFilePath))); + $this->executeCommand(); + $this->assertFileExists($this->xdebugFilePath); + $this->assertStringContainsString(';zend_extension=xdebug.so', file_get_contents($this->xdebugFilePath)); + $this->assertStringContainsString("Xdebug PHP extension disabled", $this->getDisplay()); + } } diff --git a/tests/phpunit/src/Commands/Ide/Wizard/IdeWizardCreateSshKeyCommandTest.php b/tests/phpunit/src/Commands/Ide/Wizard/IdeWizardCreateSshKeyCommandTest.php index 334e65b56..ac0575bca 100644 --- a/tests/phpunit/src/Commands/Ide/Wizard/IdeWizardCreateSshKeyCommandTest.php +++ b/tests/phpunit/src/Commands/Ide/Wizard/IdeWizardCreateSshKeyCommandTest.php @@ -1,6 +1,6 @@ mockApplicationRequest(); - $this->mockListSshKeysRequest(); - $this->mockRequest('getAccount'); - $this->mockPermissionsRequest($applicationResponse); - $this->sshKeyFileName = IdeWizardCreateSshKeyCommand::getSshKeyFilename(IdeHelper::$remoteIdeUuid); - } - - /** - * @return \Acquia\Cli\Command\Ide\Wizard\IdeWizardCreateSshKeyCommand - */ - protected function createCommand(): CommandBase { - return $this->injectCommand(IdeWizardCreateSshKeyCommand::class); - } - - public function testCreate(): void { - $this->runTestCreate(); - } - - /** - * @group brokenProphecy - */ - public function testSshKeyAlreadyUploaded(): void { - $this->runTestSshKeyAlreadyUploaded(); - } - +class IdeWizardCreateSshKeyCommandTest extends IdeWizardTestBase +{ + public function setUp(): void + { + parent::setUp(); + $applicationResponse = $this->mockApplicationRequest(); + $this->mockListSshKeysRequest(); + $this->mockRequest('getAccount'); + $this->mockPermissionsRequest($applicationResponse); + $this->sshKeyFileName = IdeWizardCreateSshKeyCommand::getSshKeyFilename(IdeHelper::$remoteIdeUuid); + } + + /** + * @return \Acquia\Cli\Command\Ide\Wizard\IdeWizardCreateSshKeyCommand + */ + protected function createCommand(): CommandBase + { + return $this->injectCommand(IdeWizardCreateSshKeyCommand::class); + } + + public function testCreate(): void + { + $this->runTestCreate(); + } + + /** + * @group brokenProphecy + */ + public function testSshKeyAlreadyUploaded(): void + { + $this->runTestSshKeyAlreadyUploaded(); + } } diff --git a/tests/phpunit/src/Commands/Ide/Wizard/IdeWizardDeleteSshKeyCommandTest.php b/tests/phpunit/src/Commands/Ide/Wizard/IdeWizardDeleteSshKeyCommandTest.php index 046139177..13a1ae503 100644 --- a/tests/phpunit/src/Commands/Ide/Wizard/IdeWizardDeleteSshKeyCommandTest.php +++ b/tests/phpunit/src/Commands/Ide/Wizard/IdeWizardDeleteSshKeyCommandTest.php @@ -1,6 +1,6 @@ mockListSshKeysRequestWithIdeKey(IdeHelper::$remoteIdeLabel, IdeHelper::$remoteIdeUuid); - - $this->mockDeleteSshKeyRequest($mockBody->{'_embedded'}->items[0]->uuid); - - // Create the file so it can be deleted. - $sshKeyFilename = $this->command::getSshKeyFilename(IdeHelper::$remoteIdeUuid); - $this->fs->touch($this->sshDir . '/' . $sshKeyFilename); - $this->fs->dumpFile($this->sshDir . '/' . $sshKeyFilename . '.pub', $mockBody->{'_embedded'}->items[0]->public_key); - - // Run it! - $this->executeCommand(); - - $this->assertFileDoesNotExist($this->sshDir . '/' . $sshKeyFilename); - } - - /** - * @return \Acquia\Cli\Command\Ide\Wizard\IdeWizardCreateSshKeyCommand - */ - protected function createCommand(): CommandBase { - return $this->injectCommand(IdeWizardDeleteSshKeyCommand::class); - } - +class IdeWizardDeleteSshKeyCommandTest extends IdeWizardTestBase +{ + public function testDelete(): void + { + $mockBody = $this->mockListSshKeysRequestWithIdeKey(IdeHelper::$remoteIdeLabel, IdeHelper::$remoteIdeUuid); + + $this->mockDeleteSshKeyRequest($mockBody->{'_embedded'}->items[0]->uuid); + + // Create the file so it can be deleted. + $sshKeyFilename = $this->command::getSshKeyFilename(IdeHelper::$remoteIdeUuid); + $this->fs->touch($this->sshDir . '/' . $sshKeyFilename); + $this->fs->dumpFile($this->sshDir . '/' . $sshKeyFilename . '.pub', $mockBody->{'_embedded'}->items[0]->public_key); + + // Run it! + $this->executeCommand(); + + $this->assertFileDoesNotExist($this->sshDir . '/' . $sshKeyFilename); + } + + /** + * @return \Acquia\Cli\Command\Ide\Wizard\IdeWizardCreateSshKeyCommand + */ + protected function createCommand(): CommandBase + { + return $this->injectCommand(IdeWizardDeleteSshKeyCommand::class); + } } diff --git a/tests/phpunit/src/Commands/Ide/Wizard/IdeWizardTestBase.php b/tests/phpunit/src/Commands/Ide/Wizard/IdeWizardTestBase.php index f95850741..adf516ef7 100644 --- a/tests/phpunit/src/Commands/Ide/Wizard/IdeWizardTestBase.php +++ b/tests/phpunit/src/Commands/Ide/Wizard/IdeWizardTestBase.php @@ -1,13 +1,13 @@ injectCommand(LinkCommand::class); - } - - public function testInfer(): void { - $this->createMockGitConfigFile(); - $applicationsResponse = $this->mockApplicationsRequest(); - $this->mockApplicationRequest(); - $environmentResponse = $this->getMockEnvironmentResponse(); - // The searchApplicationEnvironmentsForGitUrl() method will only look - // for a match of the vcs url on the prod env. So, we mock a prod env. - $environmentResponse2 = $environmentResponse; - $environmentResponse2->flags->production = TRUE; - $this->clientProphecy->request('get', - "/applications/{$applicationsResponse->{'_embedded'}->items[0]->uuid}/environments") - ->willReturn([$environmentResponse, $environmentResponse2]) - ->shouldBeCalled(); - - $this->executeCommand([], [ - // Would you like Acquia CLI to search for a Cloud application that matches your local git config? - 'y', - // Would you like to link the project at ... - 'y', - ]); - - $output = $this->getDisplay(); - - $this->assertStringContainsString('There is no Cloud Platform application linked to', $output); - $this->assertStringContainsString('Searching for a matching Cloud application...', $output); - $this->assertStringContainsString('Found a matching application!', $output); - $this->assertStringContainsString('The Cloud application Sample application 1 has been linked', $output); - } - - public function testInferFailure(): void { - $this->createMockGitConfigFile(); - $applicationsResponse = $this->mockApplicationsRequest(); - $this->mockApplicationRequest(); - - $environmentResponse = $this->getMockEnvironmentResponse(); - $this->clientProphecy->request('get', - "/applications/{$applicationsResponse->{'_embedded'}->items[0]->uuid}/environments") - ->willReturn([$environmentResponse, $environmentResponse]) - ->shouldBeCalled(); - $this->clientProphecy->request('get', - "/applications/{$applicationsResponse->{'_embedded'}->items[1]->uuid}/environments") - ->willReturn([$environmentResponse, $environmentResponse]) - ->shouldBeCalled(); - - $this->executeCommand([], [ - // Would you like Acquia CLI to search for a Cloud application that matches your local git config? - 'y', - // Select a Cloud Platform application: - 0, - // Would you like to link the project at ... - 'y', - ]); - - $output = $this->getDisplay(); - - $this->assertStringContainsString('There is no Cloud Platform application linked to', $output); - $this->assertStringContainsString('Searching for a matching Cloud application...', $output); - $this->assertStringContainsString('Could not find a matching Cloud application.', $output); - $this->assertStringContainsString('The Cloud application Sample application 1 has been linked', $output); - } - - public function testInferInvalidGitConfig(): void { - $this->expectException(AcquiaCliException::class); - $this->executeCommand([], [ - 'y', - ]); - } - +class InferApplicationTest extends CommandTestBase +{ + /** + * @return \Acquia\Cli\Command\App\LinkCommand + */ + protected function createCommand(): CommandBase + { + return $this->injectCommand(LinkCommand::class); + } + + public function testInfer(): void + { + $this->createMockGitConfigFile(); + $applicationsResponse = $this->mockApplicationsRequest(); + $this->mockApplicationRequest(); + $environmentResponse = $this->getMockEnvironmentResponse(); + // The searchApplicationEnvironmentsForGitUrl() method will only look + // for a match of the vcs url on the prod env. So, we mock a prod env. + $environmentResponse2 = $environmentResponse; + $environmentResponse2->flags->production = true; + $this->clientProphecy->request( + 'get', + "/applications/{$applicationsResponse->{'_embedded'}->items[0]->uuid}/environments" + ) + ->willReturn([$environmentResponse, $environmentResponse2]) + ->shouldBeCalled(); + + $this->executeCommand([], [ + // Would you like Acquia CLI to search for a Cloud application that matches your local git config? + 'y', + // Would you like to link the project at ... + 'y', + ]); + + $output = $this->getDisplay(); + + $this->assertStringContainsString('There is no Cloud Platform application linked to', $output); + $this->assertStringContainsString('Searching for a matching Cloud application...', $output); + $this->assertStringContainsString('Found a matching application!', $output); + $this->assertStringContainsString('The Cloud application Sample application 1 has been linked', $output); + } + + public function testInferFailure(): void + { + $this->createMockGitConfigFile(); + $applicationsResponse = $this->mockApplicationsRequest(); + $this->mockApplicationRequest(); + + $environmentResponse = $this->getMockEnvironmentResponse(); + $this->clientProphecy->request( + 'get', + "/applications/{$applicationsResponse->{'_embedded'}->items[0]->uuid}/environments" + ) + ->willReturn([$environmentResponse, $environmentResponse]) + ->shouldBeCalled(); + $this->clientProphecy->request( + 'get', + "/applications/{$applicationsResponse->{'_embedded'}->items[1]->uuid}/environments" + ) + ->willReturn([$environmentResponse, $environmentResponse]) + ->shouldBeCalled(); + + $this->executeCommand([], [ + // Would you like Acquia CLI to search for a Cloud application that matches your local git config? + 'y', + // Select a Cloud Platform application: + 0, + // Would you like to link the project at ... + 'y', + ]); + + $output = $this->getDisplay(); + + $this->assertStringContainsString('There is no Cloud Platform application linked to', $output); + $this->assertStringContainsString('Searching for a matching Cloud application...', $output); + $this->assertStringContainsString('Could not find a matching Cloud application.', $output); + $this->assertStringContainsString('The Cloud application Sample application 1 has been linked', $output); + } + + public function testInferInvalidGitConfig(): void + { + $this->expectException(AcquiaCliException::class); + $this->executeCommand([], [ + 'y', + ]); + } } diff --git a/tests/phpunit/src/Commands/Pull/PullCodeCommandTest.php b/tests/phpunit/src/Commands/Pull/PullCodeCommandTest.php index 9aba199a0..2726ccf6d 100644 --- a/tests/phpunit/src/Commands/Pull/PullCodeCommandTest.php +++ b/tests/phpunit/src/Commands/Pull/PullCodeCommandTest.php @@ -1,6 +1,6 @@ httpClientProphecy = $this->prophet->prophesize(Client::class); - - return new PullCodeCommand( - $this->localMachineHelper, - $this->datastoreCloud, - $this->datastoreAcli, - $this->cloudCredentials, - $this->telemetryHelper, - $this->acliRepoRoot, - $this->clientServiceProphecy->reveal(), - $this->sshHelper, - $this->sshDir, - $this->logger, - $this->httpClientProphecy->reveal() - ); - } - - public function testCloneRepo(): void { - // Unset repo root. Mimics failing to find local git repo. Command must be re-created - // to re-inject the parameter into the command. - $this->acliRepoRoot = ''; - $this->command = $this->createCommand(); - // Client responses. - $environment = $this->mockGetEnvironment(); - $localMachineHelper = $this->mockReadIdePhpVersion(); - $process = $this->mockProcess(); - $dir = Path::join($this->vfsRoot->url(), 'empty-dir'); - mkdir($dir); - $localMachineHelper->checkRequiredBinariesExist(["git"])->shouldBeCalled(); - $this->mockExecuteGitClone($localMachineHelper, $environment, $process, $dir); - $this->mockExecuteGitCheckout($localMachineHelper, $environment->vcs->path, $dir, $process); - $localMachineHelper->getFinder()->willReturn(new Finder()); - - $inputs = [ - // Would you like to clone a project into the current directory? - 'y', - // Select a Cloud Platform application: - self::$INPUT_DEFAULT_CHOICE, - // Would you like to link the project at ... ? - 'n', - // Choose an Acquia environment: - self::$INPUT_DEFAULT_CHOICE, - ]; - $this->executeCommand([ - '--dir' => $dir, - '--no-scripts' => TRUE, - ], $inputs); - - } - - public function testPullCode(): void { - $environment = $this->mockGetEnvironment(); - $this->createMockGitConfigFile(); - - $localMachineHelper = $this->mockReadIdePhpVersion(); - $localMachineHelper->checkRequiredBinariesExist(["git"])->shouldBeCalled(); - $finder = $this->mockFinder(); - $localMachineHelper->getFinder()->willReturn($finder->reveal()); - - $process = $this->mockProcess(); - $this->mockExecuteGitFetchAndCheckout($localMachineHelper, $process, $this->projectDir, $environment->vcs->path); - $this->mockExecuteGitStatus(FALSE, $localMachineHelper, $this->projectDir); - - $this->executeCommand([ - '--no-scripts' => TRUE, - ], self::inputChooseEnvironment()); - - $output = $this->getDisplay(); - - $this->assertStringContainsString('Select a Cloud Platform application:', $output); - $this->assertStringContainsString('[0] Sample application 1', $output); - $this->assertStringContainsString('Choose a Cloud Platform environment', $output); - $this->assertStringContainsString('[0] Dev, dev (vcs: master)', $output); - } - - public function testWithScripts(): void { - touch(Path::join($this->projectDir, 'composer.json')); - $environment = $this->mockGetEnvironment(); - $this->createMockGitConfigFile(); - - $localMachineHelper = $this->mockReadIdePhpVersion(); - $localMachineHelper->checkRequiredBinariesExist(["git"])->shouldBeCalled(); - $finder = $this->mockFinder(); - $localMachineHelper->getFinder()->willReturn($finder->reveal()); - - $process = $this->mockProcess(); - $this->mockExecuteGitFetchAndCheckout($localMachineHelper, $process, $this->projectDir, $environment->vcs->path); - $this->mockExecuteGitStatus(FALSE, $localMachineHelper, $this->projectDir); - $process = $this->mockProcess(); - $this->mockExecuteComposerExists($localMachineHelper); - $this->mockExecuteComposerInstall($localMachineHelper, $process); - $this->mockExecuteDrushExists($localMachineHelper); - $this->mockExecuteDrushStatus($localMachineHelper, $this->projectDir); - $this->mockExecuteDrushCacheRebuild($localMachineHelper, $process); - - $this->executeCommand([], self::inputChooseEnvironment()); - - $output = $this->getDisplay(); - - $this->assertStringContainsString('Select a Cloud Platform application:', $output); - $this->assertStringContainsString('[0] Sample application 1', $output); - $this->assertStringContainsString('Choose a Cloud Platform environment', $output); - $this->assertStringContainsString('[0] Dev, dev (vcs: master)', $output); - } - - public function testNoComposerJson(): void { - $environment = $this->mockGetEnvironment(); - $this->createMockGitConfigFile(); - - $localMachineHelper = $this->mockReadIdePhpVersion(); - $localMachineHelper->checkRequiredBinariesExist(["git"])->shouldBeCalled(); - $finder = $this->mockFinder(); - $localMachineHelper->getFinder()->willReturn($finder->reveal()); - - $process = $this->mockProcess(); - $this->mockExecuteGitFetchAndCheckout($localMachineHelper, $process, $this->projectDir, $environment->vcs->path); - $this->mockExecuteGitStatus(FALSE, $localMachineHelper, $this->projectDir); - $process = $this->mockProcess(); - $this->mockExecuteDrushExists($localMachineHelper); - $this->mockExecuteDrushStatus($localMachineHelper, $this->projectDir); - $this->mockExecuteDrushCacheRebuild($localMachineHelper, $process); - - $this->executeCommand([], self::inputChooseEnvironment()); - - $output = $this->getDisplay(); - $this->assertStringContainsString('composer.json file not found. Skipping composer install.', $output); - } - - public function testNoComposer(): void { - touch(Path::join($this->projectDir, 'composer.json')); - $applications = $this->mockRequest('getApplications'); - $this->mockRequest('getApplicationByUuid', $applications[self::$INPUT_DEFAULT_CHOICE]->uuid); - $environments = $this->mockRequest('getApplicationEnvironments', $applications[self::$INPUT_DEFAULT_CHOICE]->uuid); - $this->createMockGitConfigFile(); - - $localMachineHelper = $this->mockReadIdePhpVersion(); - $localMachineHelper->checkRequiredBinariesExist(["git"])->shouldBeCalled(); - $finder = $this->mockFinder(); - $localMachineHelper->getFinder()->willReturn($finder->reveal()); - - $process = $this->mockProcess(); - $this->mockExecuteGitFetchAndCheckout($localMachineHelper, $process, $this->projectDir, $environments[self::$INPUT_DEFAULT_CHOICE]->vcs->path); - $this->mockExecuteGitStatus(FALSE, $localMachineHelper, $this->projectDir); - $process = $this->mockProcess(); - $localMachineHelper - ->commandExists('composer') - ->willReturn(FALSE) - ->shouldBeCalled(); - $this->mockExecuteDrushExists($localMachineHelper); - $this->mockExecuteDrushStatus($localMachineHelper, $this->projectDir); - $this->mockExecuteDrushCacheRebuild($localMachineHelper, $process); - - $this->executeCommand([], self::inputChooseEnvironment()); - - $output = $this->getDisplay(); - - $this->assertStringContainsString('Composer not found. Skipping composer install.', $output); - } - - public function testWithVendorDir(): void { - touch(Path::join($this->projectDir, 'composer.json')); - touch(Path::join($this->projectDir, 'vendor')); - $applications = $this->mockRequest('getApplications'); - $this->mockRequest('getApplicationByUuid', $applications[self::$INPUT_DEFAULT_CHOICE]->uuid); - $environments = $this->mockRequest('getApplicationEnvironments', $applications[self::$INPUT_DEFAULT_CHOICE]->uuid); - $this->createMockGitConfigFile(); - - $localMachineHelper = $this->mockReadIdePhpVersion(); - $localMachineHelper->checkRequiredBinariesExist(["git"])->shouldBeCalled(); - $finder = $this->mockFinder(); - $localMachineHelper->getFinder()->willReturn($finder->reveal()); - - $process = $this->mockProcess(); - $this->mockExecuteGitFetchAndCheckout($localMachineHelper, $process, $this->projectDir, $environments[self::$INPUT_DEFAULT_CHOICE]->vcs->path); - $this->mockExecuteGitStatus(FALSE, $localMachineHelper, $this->projectDir); - $process = $this->mockProcess(); - $this->mockExecuteComposerExists($localMachineHelper); - $this->mockExecuteDrushExists($localMachineHelper); - $this->mockExecuteDrushStatus($localMachineHelper, $this->projectDir); - $this->mockExecuteDrushCacheRebuild($localMachineHelper, $process); - - $this->executeCommand([], self::inputChooseEnvironment()); - - $output = $this->getDisplay(); - - $this->assertStringContainsString('Composer dependencies already installed. Skipping composer install.', $output); - } - - /** - * @return string[][] - */ - public function providerTestMatchPhpVersion(): array { - return [ - ['7.1'], - ['7.2'], - [''], - ]; - } - - /** - * @dataProvider providerTestMatchPhpVersion - */ - public function testMatchPhpVersion(string $phpVersion): void { - IdeHelper::setCloudIdeEnvVars(); - $this->application->addCommands([ - $this->injectCommand(IdePhpVersionCommand::class), - ]); - $this->command = $this->createCommand(); - $dir = '/home/ide/project'; - $this->createMockGitConfigFile(); - - $localMachineHelper = $this->mockReadIdePhpVersion($phpVersion); - $localMachineHelper->checkRequiredBinariesExist(["git"]) - ->shouldBeCalled(); - $finder = $this->mockFinder(); - $localMachineHelper->getFinder()->willReturn($finder->reveal()); - - $process = $this->mockProcess(); - $this->mockExecuteGitFetchAndCheckout($localMachineHelper, $process, $dir, 'master'); - $this->mockExecuteGitStatus(FALSE, $localMachineHelper, $dir); - - $environmentResponse = $this->getMockEnvironmentResponse(); - $environmentResponse->configuration->php->version = '7.1'; - $environmentResponse->sshUrl = $environmentResponse->ssh_url; - $this->clientProphecy->request('get', - "/environments/" . $environmentResponse->id) - ->willReturn($environmentResponse) - ->shouldBeCalled(); - - $this->executeCommand([ - '--dir' => $dir, - '--no-scripts' => TRUE, - // @todo Execute ONLY match php aspect, not the code pull. - 'environmentId' => $environmentResponse->id, - ], [ - // Choose an Acquia environment: - self::$INPUT_DEFAULT_CHOICE, - // Would you like to change the PHP version on this IDE to match the PHP version on ... ? - 'n', - ]); - - $output = $this->getDisplay(); - IdeHelper::unsetCloudIdeEnvVars(); - $message = "Would you like to change the PHP version on this IDE to match the PHP version on the {$environmentResponse->label} ({$environmentResponse->configuration->php->version}) environment?"; - if ($phpVersion === '7.1') { - $this->assertStringNotContainsString($message, $output); +class PullCodeCommandTest extends PullCommandTestBase +{ + protected function createCommand(): CommandBase + { + $this->httpClientProphecy = $this->prophet->prophesize(Client::class); + + return new PullCodeCommand( + $this->localMachineHelper, + $this->datastoreCloud, + $this->datastoreAcli, + $this->cloudCredentials, + $this->telemetryHelper, + $this->acliRepoRoot, + $this->clientServiceProphecy->reveal(), + $this->sshHelper, + $this->sshDir, + $this->logger, + $this->httpClientProphecy->reveal() + ); } - else { - $this->assertStringContainsString($message, $output); + + public function testCloneRepo(): void + { + // Unset repo root. Mimics failing to find local git repo. Command must be re-created + // to re-inject the parameter into the command. + $this->acliRepoRoot = ''; + $this->command = $this->createCommand(); + // Client responses. + $environment = $this->mockGetEnvironment(); + $localMachineHelper = $this->mockReadIdePhpVersion(); + $process = $this->mockProcess(); + $dir = Path::join($this->vfsRoot->url(), 'empty-dir'); + mkdir($dir); + $localMachineHelper->checkRequiredBinariesExist(["git"])->shouldBeCalled(); + $this->mockExecuteGitClone($localMachineHelper, $environment, $process, $dir); + $this->mockExecuteGitCheckout($localMachineHelper, $environment->vcs->path, $dir, $process); + $localMachineHelper->getFinder()->willReturn(new Finder()); + + $inputs = [ + // Would you like to clone a project into the current directory? + 'y', + // Select a Cloud Platform application: + self::$INPUT_DEFAULT_CHOICE, + // Would you like to link the project at ... ? + 'n', + // Choose an Acquia environment: + self::$INPUT_DEFAULT_CHOICE, + ]; + $this->executeCommand([ + '--dir' => $dir, + '--no-scripts' => true, + ], $inputs); } - } - - protected function mockExecuteGitClone( - ObjectProphecy $localMachineHelper, - object $environmentsResponse, - ObjectProphecy $process, - mixed $dir - ): void { - $command = [ - 'git', - 'clone', - $environmentsResponse->vcs->url, - $dir, - ]; - $localMachineHelper->execute($command, Argument::type('callable'), NULL, TRUE, NULL, ['GIT_SSH_COMMAND' => 'ssh -o StrictHostKeyChecking=no']) - ->willReturn($process->reveal()) - ->shouldBeCalled(); - } + public function testPullCode(): void + { + $environment = $this->mockGetEnvironment(); + $this->createMockGitConfigFile(); + + $localMachineHelper = $this->mockReadIdePhpVersion(); + $localMachineHelper->checkRequiredBinariesExist(["git"])->shouldBeCalled(); + $finder = $this->mockFinder(); + $localMachineHelper->getFinder()->willReturn($finder->reveal()); + + $process = $this->mockProcess(); + $this->mockExecuteGitFetchAndCheckout($localMachineHelper, $process, $this->projectDir, $environment->vcs->path); + $this->mockExecuteGitStatus(false, $localMachineHelper, $this->projectDir); + + $this->executeCommand([ + '--no-scripts' => true, + ], self::inputChooseEnvironment()); + + $output = $this->getDisplay(); + + $this->assertStringContainsString('Select a Cloud Platform application:', $output); + $this->assertStringContainsString('[0] Sample application 1', $output); + $this->assertStringContainsString('Choose a Cloud Platform environment', $output); + $this->assertStringContainsString('[0] Dev, dev (vcs: master)', $output); + } + + public function testWithScripts(): void + { + touch(Path::join($this->projectDir, 'composer.json')); + $environment = $this->mockGetEnvironment(); + $this->createMockGitConfigFile(); + + $localMachineHelper = $this->mockReadIdePhpVersion(); + $localMachineHelper->checkRequiredBinariesExist(["git"])->shouldBeCalled(); + $finder = $this->mockFinder(); + $localMachineHelper->getFinder()->willReturn($finder->reveal()); + + $process = $this->mockProcess(); + $this->mockExecuteGitFetchAndCheckout($localMachineHelper, $process, $this->projectDir, $environment->vcs->path); + $this->mockExecuteGitStatus(false, $localMachineHelper, $this->projectDir); + $process = $this->mockProcess(); + $this->mockExecuteComposerExists($localMachineHelper); + $this->mockExecuteComposerInstall($localMachineHelper, $process); + $this->mockExecuteDrushExists($localMachineHelper); + $this->mockExecuteDrushStatus($localMachineHelper, $this->projectDir); + $this->mockExecuteDrushCacheRebuild($localMachineHelper, $process); + + $this->executeCommand([], self::inputChooseEnvironment()); + + $output = $this->getDisplay(); + + $this->assertStringContainsString('Select a Cloud Platform application:', $output); + $this->assertStringContainsString('[0] Sample application 1', $output); + $this->assertStringContainsString('Choose a Cloud Platform environment', $output); + $this->assertStringContainsString('[0] Dev, dev (vcs: master)', $output); + } + + public function testNoComposerJson(): void + { + $environment = $this->mockGetEnvironment(); + $this->createMockGitConfigFile(); + + $localMachineHelper = $this->mockReadIdePhpVersion(); + $localMachineHelper->checkRequiredBinariesExist(["git"])->shouldBeCalled(); + $finder = $this->mockFinder(); + $localMachineHelper->getFinder()->willReturn($finder->reveal()); + + $process = $this->mockProcess(); + $this->mockExecuteGitFetchAndCheckout($localMachineHelper, $process, $this->projectDir, $environment->vcs->path); + $this->mockExecuteGitStatus(false, $localMachineHelper, $this->projectDir); + $process = $this->mockProcess(); + $this->mockExecuteDrushExists($localMachineHelper); + $this->mockExecuteDrushStatus($localMachineHelper, $this->projectDir); + $this->mockExecuteDrushCacheRebuild($localMachineHelper, $process); + + $this->executeCommand([], self::inputChooseEnvironment()); + + $output = $this->getDisplay(); + $this->assertStringContainsString('composer.json file not found. Skipping composer install.', $output); + } + + public function testNoComposer(): void + { + touch(Path::join($this->projectDir, 'composer.json')); + $applications = $this->mockRequest('getApplications'); + $this->mockRequest('getApplicationByUuid', $applications[self::$INPUT_DEFAULT_CHOICE]->uuid); + $environments = $this->mockRequest('getApplicationEnvironments', $applications[self::$INPUT_DEFAULT_CHOICE]->uuid); + $this->createMockGitConfigFile(); + + $localMachineHelper = $this->mockReadIdePhpVersion(); + $localMachineHelper->checkRequiredBinariesExist(["git"])->shouldBeCalled(); + $finder = $this->mockFinder(); + $localMachineHelper->getFinder()->willReturn($finder->reveal()); + + $process = $this->mockProcess(); + $this->mockExecuteGitFetchAndCheckout($localMachineHelper, $process, $this->projectDir, $environments[self::$INPUT_DEFAULT_CHOICE]->vcs->path); + $this->mockExecuteGitStatus(false, $localMachineHelper, $this->projectDir); + $process = $this->mockProcess(); + $localMachineHelper + ->commandExists('composer') + ->willReturn(false) + ->shouldBeCalled(); + $this->mockExecuteDrushExists($localMachineHelper); + $this->mockExecuteDrushStatus($localMachineHelper, $this->projectDir); + $this->mockExecuteDrushCacheRebuild($localMachineHelper, $process); + + $this->executeCommand([], self::inputChooseEnvironment()); + + $output = $this->getDisplay(); + + $this->assertStringContainsString('Composer not found. Skipping composer install.', $output); + } + + public function testWithVendorDir(): void + { + touch(Path::join($this->projectDir, 'composer.json')); + touch(Path::join($this->projectDir, 'vendor')); + $applications = $this->mockRequest('getApplications'); + $this->mockRequest('getApplicationByUuid', $applications[self::$INPUT_DEFAULT_CHOICE]->uuid); + $environments = $this->mockRequest('getApplicationEnvironments', $applications[self::$INPUT_DEFAULT_CHOICE]->uuid); + $this->createMockGitConfigFile(); + + $localMachineHelper = $this->mockReadIdePhpVersion(); + $localMachineHelper->checkRequiredBinariesExist(["git"])->shouldBeCalled(); + $finder = $this->mockFinder(); + $localMachineHelper->getFinder()->willReturn($finder->reveal()); + + $process = $this->mockProcess(); + $this->mockExecuteGitFetchAndCheckout($localMachineHelper, $process, $this->projectDir, $environments[self::$INPUT_DEFAULT_CHOICE]->vcs->path); + $this->mockExecuteGitStatus(false, $localMachineHelper, $this->projectDir); + $process = $this->mockProcess(); + $this->mockExecuteComposerExists($localMachineHelper); + $this->mockExecuteDrushExists($localMachineHelper); + $this->mockExecuteDrushStatus($localMachineHelper, $this->projectDir); + $this->mockExecuteDrushCacheRebuild($localMachineHelper, $process); + + $this->executeCommand([], self::inputChooseEnvironment()); + + $output = $this->getDisplay(); + + $this->assertStringContainsString('Composer dependencies already installed. Skipping composer install.', $output); + } + + /** + * @return string[][] + */ + public function providerTestMatchPhpVersion(): array + { + return [ + ['7.1'], + ['7.2'], + [''], + ]; + } + + /** + * @dataProvider providerTestMatchPhpVersion + */ + public function testMatchPhpVersion(string $phpVersion): void + { + IdeHelper::setCloudIdeEnvVars(); + $this->application->addCommands([ + $this->injectCommand(IdePhpVersionCommand::class), + ]); + $this->command = $this->createCommand(); + $dir = '/home/ide/project'; + $this->createMockGitConfigFile(); + + $localMachineHelper = $this->mockReadIdePhpVersion($phpVersion); + $localMachineHelper->checkRequiredBinariesExist(["git"]) + ->shouldBeCalled(); + $finder = $this->mockFinder(); + $localMachineHelper->getFinder()->willReturn($finder->reveal()); + + $process = $this->mockProcess(); + $this->mockExecuteGitFetchAndCheckout($localMachineHelper, $process, $dir, 'master'); + $this->mockExecuteGitStatus(false, $localMachineHelper, $dir); + + $environmentResponse = $this->getMockEnvironmentResponse(); + $environmentResponse->configuration->php->version = '7.1'; + $environmentResponse->sshUrl = $environmentResponse->ssh_url; + $this->clientProphecy->request( + 'get', + "/environments/" . $environmentResponse->id + ) + ->willReturn($environmentResponse) + ->shouldBeCalled(); + + $this->executeCommand([ + '--dir' => $dir, + '--no-scripts' => true, + // @todo Execute ONLY match php aspect, not the code pull. + 'environmentId' => $environmentResponse->id, + ], [ + // Choose an Acquia environment: + self::$INPUT_DEFAULT_CHOICE, + // Would you like to change the PHP version on this IDE to match the PHP version on ... ? + 'n', + ]); + + $output = $this->getDisplay(); + IdeHelper::unsetCloudIdeEnvVars(); + $message = "Would you like to change the PHP version on this IDE to match the PHP version on the {$environmentResponse->label} ({$environmentResponse->configuration->php->version}) environment?"; + if ($phpVersion === '7.1') { + $this->assertStringNotContainsString($message, $output); + } else { + $this->assertStringContainsString($message, $output); + } + } + + protected function mockExecuteGitClone( + ObjectProphecy $localMachineHelper, + object $environmentsResponse, + ObjectProphecy $process, + mixed $dir + ): void { + $command = [ + 'git', + 'clone', + $environmentsResponse->vcs->url, + $dir, + ]; + $localMachineHelper->execute($command, Argument::type('callable'), null, true, null, ['GIT_SSH_COMMAND' => 'ssh -o StrictHostKeyChecking=no']) + ->willReturn($process->reveal()) + ->shouldBeCalled(); + } } diff --git a/tests/phpunit/src/Commands/Pull/PullCommandTest.php b/tests/phpunit/src/Commands/Pull/PullCommandTest.php index 1118245cf..cf04096d1 100644 --- a/tests/phpunit/src/Commands/Pull/PullCommandTest.php +++ b/tests/phpunit/src/Commands/Pull/PullCommandTest.php @@ -1,6 +1,6 @@ httpClientProphecy = $this->prophet->prophesize(Client::class); - protected function createCommand(): CommandBase { - $this->httpClientProphecy = $this->prophet->prophesize(Client::class); + return new PullCommand( + $this->localMachineHelper, + $this->datastoreCloud, + $this->datastoreAcli, + $this->cloudCredentials, + $this->telemetryHelper, + $this->acliRepoRoot, + $this->clientServiceProphecy->reveal(), + $this->sshHelper, + $this->sshDir, + $this->logger, + $this->httpClientProphecy->reveal() + ); + } - return new PullCommand( - $this->localMachineHelper, - $this->datastoreCloud, - $this->datastoreAcli, - $this->cloudCredentials, - $this->telemetryHelper, - $this->acliRepoRoot, - $this->clientServiceProphecy->reveal(), - $this->sshHelper, - $this->sshDir, - $this->logger, - $this->httpClientProphecy->reveal() - ); - } + public function testPull(): void + { + // Pull code. + $environment = $this->mockGetEnvironment(); + $this->createMockGitConfigFile(); + $localMachineHelper = $this->mockLocalMachineHelper(); + $localMachineHelper->checkRequiredBinariesExist(["git"])->shouldBeCalled(); + $finder = $this->mockFinder(); + $localMachineHelper->getFinder()->willReturn($finder->reveal()); + $process = $this->mockProcess(); + $this->mockExecuteGitFetchAndCheckout($localMachineHelper, $process, $this->projectDir, $environment->vcs->path); + $this->mockExecuteGitStatus(false, $localMachineHelper, $this->projectDir); - public function testPull(): void { - // Pull code. - $environment = $this->mockGetEnvironment(); - $this->createMockGitConfigFile(); - $localMachineHelper = $this->mockLocalMachineHelper(); - $localMachineHelper->checkRequiredBinariesExist(["git"])->shouldBeCalled(); - $finder = $this->mockFinder(); - $localMachineHelper->getFinder()->willReturn($finder->reveal()); - $process = $this->mockProcess(); - $this->mockExecuteGitFetchAndCheckout($localMachineHelper, $process, $this->projectDir, $environment->vcs->path); - $this->mockExecuteGitStatus(FALSE, $localMachineHelper, $this->projectDir); + // Pull files. + $sshHelper = $this->mockSshHelper(); + $this->mockGetCloudSites($sshHelper, $environment); + $this->mockGetFilesystem($localMachineHelper); + $parts = explode('.', $environment->ssh_url); + $sitegroup = reset($parts); + $this->mockExecuteRsync($localMachineHelper, $environment, '/mnt/files/' . $sitegroup . '.' . $environment->name . '/sites/bar/files/', $this->projectDir . '/docroot/sites/bar/files'); + $this->command->sshHelper = $sshHelper->reveal(); - // Pull files. - $sshHelper = $this->mockSshHelper(); - $this->mockGetCloudSites($sshHelper, $environment); - $this->mockGetFilesystem($localMachineHelper); - $parts = explode('.', $environment->ssh_url); - $sitegroup = reset($parts); - $this->mockExecuteRsync($localMachineHelper, $environment, '/mnt/files/' . $sitegroup . '.' . $environment->name . '/sites/bar/files/', $this->projectDir . '/docroot/sites/bar/files'); - $this->command->sshHelper = $sshHelper->reveal(); + // Pull database. + $this->mockExecuteMySqlConnect($localMachineHelper, true); + $this->mockGetBackup($environment); + $this->mockExecuteMySqlListTables($localMachineHelper, 'drupal'); + $process = $this->mockProcess(); + $localMachineHelper + ->execute(Argument::type('array'), Argument::type('callable'), null, false, null, ['MYSQL_PWD' => $this->dbPassword]) + ->willReturn($process->reveal()) + ->shouldBeCalled(); + $this->mockExecuteMySqlImport($localMachineHelper, true, true, 'my_db', 'my_dbdev', 'drupal'); + $this->executeCommand([ + '--no-scripts' => true, + ], [ + // Would you like Acquia CLI to search for a Cloud application that matches your local git config? + 'n', + // Select a Cloud Platform application: + self::$INPUT_DEFAULT_CHOICE, + // Would you like to link the project at ... ? + 'n', + // Choose an Acquia environment: + self::$INPUT_DEFAULT_CHOICE, + self::$INPUT_DEFAULT_CHOICE, + ]); - // Pull database. - $this->mockExecuteMySqlConnect($localMachineHelper, TRUE); - $this->mockGetBackup($environment); - $this->mockExecuteMySqlListTables($localMachineHelper, 'drupal'); - $process = $this->mockProcess(); - $localMachineHelper - ->execute(Argument::type('array'), Argument::type('callable'), NULL, FALSE, NULL, ['MYSQL_PWD' => $this->dbPassword]) - ->willReturn($process->reveal()) - ->shouldBeCalled(); - $this->mockExecuteMySqlImport($localMachineHelper, TRUE, TRUE, 'my_db', 'my_dbdev', 'drupal'); - $this->executeCommand([ - '--no-scripts' => TRUE, - ], [ - // Would you like Acquia CLI to search for a Cloud application that matches your local git config? - 'n', - // Select a Cloud Platform application: - self::$INPUT_DEFAULT_CHOICE, - // Would you like to link the project at ... ? - 'n', - // Choose an Acquia environment: - self::$INPUT_DEFAULT_CHOICE, - self::$INPUT_DEFAULT_CHOICE, - ]); + $output = $this->getDisplay(); - $output = $this->getDisplay(); - - $this->assertStringContainsString('Select a Cloud Platform application:', $output); - $this->assertStringContainsString('[0] Sample application 1', $output); - $this->assertStringContainsString('Choose a Cloud Platform environment', $output); - $this->assertStringContainsString('[0] Dev, dev (vcs: master)', $output); - $this->assertStringContainsString('Choose a database [my_db (default)]:', $output); - } - - public function testMissingLocalRepo(): void { - $this->setupFsFixture(); - // Unset repo root. Mimics failing to find local git repo. Command must be re-created - // to re-inject the parameter into the command. - $this->acliRepoRoot = ''; - $this->removeMockGitConfig(); - $this->command = $this->createCommand(); - $this->expectException(AcquiaCliException::class); - $this->expectExceptionMessage('Execute this command from within a Drupal project directory or an empty directory'); - $inputs = [ - // Would you like to clone a project into the current directory? - 'n', - ]; - $this->executeCommand([], $inputs); - } + $this->assertStringContainsString('Select a Cloud Platform application:', $output); + $this->assertStringContainsString('[0] Sample application 1', $output); + $this->assertStringContainsString('Choose a Cloud Platform environment', $output); + $this->assertStringContainsString('[0] Dev, dev (vcs: master)', $output); + $this->assertStringContainsString('Choose a database [my_db (default)]:', $output); + } + public function testMissingLocalRepo(): void + { + $this->setupFsFixture(); + // Unset repo root. Mimics failing to find local git repo. Command must be re-created + // to re-inject the parameter into the command. + $this->acliRepoRoot = ''; + $this->removeMockGitConfig(); + $this->command = $this->createCommand(); + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage('Execute this command from within a Drupal project directory or an empty directory'); + $inputs = [ + // Would you like to clone a project into the current directory? + 'n', + ]; + $this->executeCommand([], $inputs); + } } diff --git a/tests/phpunit/src/Commands/Pull/PullCommandTestBase.php b/tests/phpunit/src/Commands/Pull/PullCommandTestBase.php index 7f37384a4..5a38c923c 100644 --- a/tests/phpunit/src/Commands/Pull/PullCommandTestBase.php +++ b/tests/phpunit/src/Commands/Pull/PullCommandTestBase.php @@ -1,6 +1,6 @@ commandExists('drush') - ->willReturn(TRUE) - ->shouldBeCalled(); - } + protected function mockExecuteDrushExists( + ObjectProphecy $localMachineHelper + ): void { + $localMachineHelper + ->commandExists('drush') + ->willReturn(true) + ->shouldBeCalled(); + } - protected function mockExecuteDrushStatus( - ObjectProphecy $localMachineHelper, - string $dir = NULL - ): void { - $drushStatusProcess = $this->prophet->prophesize(Process::class); - $drushStatusProcess->isSuccessful()->willReturn(TRUE); - $drushStatusProcess->getExitCode()->willReturn(0); - $drushStatusProcess->getOutput() - ->willReturn(json_encode(['db-status' => 'Connected'])); - $localMachineHelper - ->execute([ + protected function mockExecuteDrushStatus( + ObjectProphecy $localMachineHelper, + string $dir = null + ): void { + $drushStatusProcess = $this->prophet->prophesize(Process::class); + $drushStatusProcess->isSuccessful()->willReturn(true); + $drushStatusProcess->getExitCode()->willReturn(0); + $drushStatusProcess->getOutput() + ->willReturn(json_encode(['db-status' => 'Connected'])); + $localMachineHelper + ->execute([ 'drush', 'status', '--fields=db-status,drush-version', '--format=json', '--no-interaction', - ], Argument::any(), $dir, FALSE) - ->willReturn($drushStatusProcess->reveal()) - ->shouldBeCalled(); - } + ], Argument::any(), $dir, false) + ->willReturn($drushStatusProcess->reveal()) + ->shouldBeCalled(); + } - protected function mockExecuteDrushCacheRebuild( - ObjectProphecy $localMachineHelper, - ObjectProphecy $process - ): void { - $localMachineHelper - ->execute([ + protected function mockExecuteDrushCacheRebuild( + ObjectProphecy $localMachineHelper, + ObjectProphecy $process + ): void { + $localMachineHelper + ->execute([ 'drush', 'cache:rebuild', '--yes', '--no-interaction', '--verbose', - ], Argument::type('callable'), $this->projectDir, FALSE) - ->willReturn($process->reveal()) - ->shouldBeCalled(); - } + ], Argument::type('callable'), $this->projectDir, false) + ->willReturn($process->reveal()) + ->shouldBeCalled(); + } - protected function mockExecuteDrushSqlSanitize( - ObjectProphecy $localMachineHelper, - ObjectProphecy $process - ): void { - $localMachineHelper - ->execute([ + protected function mockExecuteDrushSqlSanitize( + ObjectProphecy $localMachineHelper, + ObjectProphecy $process + ): void { + $localMachineHelper + ->execute([ 'drush', 'sql:sanitize', '--yes', '--no-interaction', '--verbose', - ], Argument::type('callable'), $this->projectDir, FALSE) - ->willReturn($process->reveal()) - ->shouldBeCalled(); - } + ], Argument::type('callable'), $this->projectDir, false) + ->willReturn($process->reveal()) + ->shouldBeCalled(); + } - protected function mockExecuteComposerExists( - ObjectProphecy $localMachineHelper - ): void { - $localMachineHelper - ->commandExists('composer') - ->willReturn(TRUE) - ->shouldBeCalled(); - } + protected function mockExecuteComposerExists( + ObjectProphecy $localMachineHelper + ): void { + $localMachineHelper + ->commandExists('composer') + ->willReturn(true) + ->shouldBeCalled(); + } - protected function mockExecuteComposerInstall( - ObjectProphecy $localMachineHelper, - ObjectProphecy $process - ): void { - $localMachineHelper - ->execute([ + protected function mockExecuteComposerInstall( + ObjectProphecy $localMachineHelper, + ObjectProphecy $process + ): void { + $localMachineHelper + ->execute([ 'composer', 'install', '--no-interaction', - ], Argument::type('callable'), $this->projectDir, FALSE, NULL) - ->willReturn($process->reveal()) - ->shouldBeCalled(); - } + ], Argument::type('callable'), $this->projectDir, false, null) + ->willReturn($process->reveal()) + ->shouldBeCalled(); + } - protected function mockDrupalSettingsRefresh( - ObjectProphecy $localMachineHelper - ): void { - $localMachineHelper - ->execute([ + protected function mockDrupalSettingsRefresh( + ObjectProphecy $localMachineHelper + ): void { + $localMachineHelper + ->execute([ '/ide/drupal-setup.sh', - ]); - } - - protected function mockExecuteGitStatus( - mixed $failed, - ObjectProphecy $localMachineHelper, - mixed $cwd - ): void { - $process = $this->prophet->prophesize(Process::class); - $process->isSuccessful()->willReturn(!$failed)->shouldBeCalled(); - $localMachineHelper->executeFromCmd('git add . && git diff-index --cached --quiet HEAD', NULL, $cwd, FALSE)->willReturn($process->reveal())->shouldBeCalled(); - } + ]); + } - protected function mockGetLocalCommitHash( - ObjectProphecy $localMachineHelper, - mixed $cwd, - mixed $commitHash - ): void { - $process = $this->prophet->prophesize(Process::class); - $process->isSuccessful()->willReturn(TRUE)->shouldBeCalled(); - $process->getOutput()->willReturn($commitHash)->shouldBeCalled(); - $localMachineHelper->execute([ - 'git', - 'rev-parse', - 'HEAD', - ], NULL, $cwd, FALSE)->willReturn($process->reveal())->shouldBeCalled(); - } + protected function mockExecuteGitStatus( + mixed $failed, + ObjectProphecy $localMachineHelper, + mixed $cwd + ): void { + $process = $this->prophet->prophesize(Process::class); + $process->isSuccessful()->willReturn(!$failed)->shouldBeCalled(); + $localMachineHelper->executeFromCmd('git add . && git diff-index --cached --quiet HEAD', null, $cwd, false)->willReturn($process->reveal())->shouldBeCalled(); + } - protected function mockFinder(): ObjectProphecy { - $finder = $this->prophet->prophesize(Finder::class); - $finder->files()->willReturn($finder); - $finder->in(Argument::type('string'))->willReturn($finder); - $finder->in(Argument::type('array'))->willReturn($finder); - $finder->ignoreDotFiles(FALSE)->willReturn($finder); - $finder->ignoreVCS(FALSE)->willReturn($finder); - $finder->ignoreVCSIgnored(TRUE)->willReturn($finder); - $finder->hasResults()->willReturn(TRUE); - $finder->name(Argument::type('string'))->willReturn($finder); - $finder->notName(Argument::type('string'))->willReturn($finder); - $finder->directories()->willReturn($finder); - $finder->append(Argument::type(Finder::class))->willReturn($finder); + protected function mockGetLocalCommitHash( + ObjectProphecy $localMachineHelper, + mixed $cwd, + mixed $commitHash + ): void { + $process = $this->prophet->prophesize(Process::class); + $process->isSuccessful()->willReturn(true)->shouldBeCalled(); + $process->getOutput()->willReturn($commitHash)->shouldBeCalled(); + $localMachineHelper->execute([ + 'git', + 'rev-parse', + 'HEAD', + ], null, $cwd, false)->willReturn($process->reveal())->shouldBeCalled(); + } - return $finder; - } + protected function mockFinder(): ObjectProphecy + { + $finder = $this->prophet->prophesize(Finder::class); + $finder->files()->willReturn($finder); + $finder->in(Argument::type('string'))->willReturn($finder); + $finder->in(Argument::type('array'))->willReturn($finder); + $finder->ignoreDotFiles(false)->willReturn($finder); + $finder->ignoreVCS(false)->willReturn($finder); + $finder->ignoreVCSIgnored(true)->willReturn($finder); + $finder->hasResults()->willReturn(true); + $finder->name(Argument::type('string'))->willReturn($finder); + $finder->notName(Argument::type('string'))->willReturn($finder); + $finder->directories()->willReturn($finder); + $finder->append(Argument::type(Finder::class))->willReturn($finder); + + return $finder; + } - protected function mockExecuteGitFetchAndCheckout( - ObjectProphecy $localMachineHelper, - ObjectProphecy $process, - string $cwd, - string $vcsPath - ): void { - $localMachineHelper->execute([ - 'git', - 'fetch', - '--all', - ], Argument::type('callable'), $cwd, FALSE) - ->willReturn($process->reveal()) - ->shouldBeCalled(); - $this->mockExecuteGitCheckout($localMachineHelper, $vcsPath, $cwd, $process); - } + protected function mockExecuteGitFetchAndCheckout( + ObjectProphecy $localMachineHelper, + ObjectProphecy $process, + string $cwd, + string $vcsPath + ): void { + $localMachineHelper->execute([ + 'git', + 'fetch', + '--all', + ], Argument::type('callable'), $cwd, false) + ->willReturn($process->reveal()) + ->shouldBeCalled(); + $this->mockExecuteGitCheckout($localMachineHelper, $vcsPath, $cwd, $process); + } - protected function mockExecuteGitCheckout(ObjectProphecy $localMachineHelper, string $vcsPath, string $cwd, ObjectProphecy $process): void { - $localMachineHelper->execute([ - 'git', - 'checkout', - $vcsPath, - ], Argument::type('callable'), $cwd, FALSE) - ->willReturn($process->reveal()) - ->shouldBeCalled(); - } + protected function mockExecuteGitCheckout(ObjectProphecy $localMachineHelper, string $vcsPath, string $cwd, ObjectProphecy $process): void + { + $localMachineHelper->execute([ + 'git', + 'checkout', + $vcsPath, + ], Argument::type('callable'), $cwd, false) + ->willReturn($process->reveal()) + ->shouldBeCalled(); + } - protected function mockExecuteRsync( - LocalMachineHelper|ObjectProphecy $localMachineHelper, - mixed $environment, - string $sourceDir, - string $destinationDir - ): void { - $process = $this->mockProcess(); - $localMachineHelper->checkRequiredBinariesExist(['rsync'])->shouldBeCalled(); - $command = [ - 'rsync', - '-avPhze', - 'ssh -o StrictHostKeyChecking=no', - $environment->ssh_url . ':' . $sourceDir, - $destinationDir, - ]; - $localMachineHelper->execute($command, Argument::type('callable'), NULL, TRUE) - ->willReturn($process->reveal()) - ->shouldBeCalled(); - } + protected function mockExecuteRsync( + LocalMachineHelper|ObjectProphecy $localMachineHelper, + mixed $environment, + string $sourceDir, + string $destinationDir + ): void { + $process = $this->mockProcess(); + $localMachineHelper->checkRequiredBinariesExist(['rsync'])->shouldBeCalled(); + $command = [ + 'rsync', + '-avPhze', + 'ssh -o StrictHostKeyChecking=no', + $environment->ssh_url . ':' . $sourceDir, + $destinationDir, + ]; + $localMachineHelper->execute($command, Argument::type('callable'), null, true) + ->willReturn($process->reveal()) + ->shouldBeCalled(); + } - protected function mockExecuteMySqlConnect( - ObjectProphecy $localMachineHelper, - bool $success - ): void { - $localMachineHelper->checkRequiredBinariesExist(["mysql"])->shouldBeCalled(); - $process = $this->mockProcess($success); - $localMachineHelper - ->execute([ + protected function mockExecuteMySqlConnect( + ObjectProphecy $localMachineHelper, + bool $success + ): void { + $localMachineHelper->checkRequiredBinariesExist(["mysql"])->shouldBeCalled(); + $process = $this->mockProcess($success); + $localMachineHelper + ->execute([ 'mysql', '--host', $this->dbHost, '--user', 'drupal', 'drupal', - ], Argument::type('callable'), NULL, FALSE, NULL, ['MYSQL_PWD' => 'drupal']) - ->willReturn($process->reveal()) - ->shouldBeCalled(); - } - - protected function mockExecuteMySqlListTables( - LocalMachineHelper|ObjectProphecy $localMachineHelper, - string $dbName = 'jxr5000596dev', - ): void { - $localMachineHelper->checkRequiredBinariesExist(["mysql"])->shouldBeCalled(); - $process = $this->mockProcess(); - $process->getOutput()->willReturn('table1'); - $command = [ - 'mysql', - '--host', - 'localhost', - '--user', - 'drupal', - $dbName, - '--silent', - '-e', - 'SHOW TABLES;', - ]; - $localMachineHelper - ->execute($command, Argument::type('callable'), NULL, FALSE, NULL, ['MYSQL_PWD' => $this->dbPassword]) - ->willReturn($process->reveal()) - ->shouldBeCalled(); - } - - protected function mockExecuteMySqlDropDb( - LocalMachineHelper|ObjectProphecy $localMachineHelper, - bool $success, - ObjectProphecy $fs - ): void { - $localMachineHelper->checkRequiredBinariesExist(["mysql"])->shouldBeCalled(); - $process = $this->mockProcess($success); - $fs->tempnam(Argument::type('string'), 'acli_drop_table_', '.sql')->willReturn('something')->shouldBeCalled(); - $fs->dumpfile('something', Argument::type('string'))->shouldBeCalled(); - $localMachineHelper - ->execute(Argument::type('array'), Argument::type('callable'), NULL, FALSE, NULL, ['MYSQL_PWD' => $this->dbPassword]) - ->willReturn($process->reveal()) - ->shouldBeCalled(); - } - - protected function mockExecuteMySqlImport( - ObjectProphecy $localMachineHelper, - bool $success, - bool $pvExists, - string $dbName = 'jxr5000596dev', - string $dbMachineName = 'db554675', - string $localDbName = 'jxr5000596dev', - string $env = 'dev' - ): void { - $localMachineHelper->checkRequiredBinariesExist(['gunzip', 'mysql'])->shouldBeCalled(); - $this->mockExecutePvExists($localMachineHelper, $pvExists); - $process = $this->mockProcess($success); - $filePath = Path::join(sys_get_temp_dir(), "$env-$dbName-$dbMachineName-2012-05-15T12:00:00.000Z.sql.gz"); - $command = $pvExists ? "pv $filePath --bytes --rate | gunzip | MYSQL_PWD=drupal mysql --host=localhost --user=drupal $localDbName" : "gunzip -c $filePath | MYSQL_PWD=drupal mysql --host=localhost --user=drupal $localDbName"; - // MySQL import command. - $localMachineHelper - ->executeFromCmd($command, Argument::type('callable'), - NULL, TRUE, NULL) - ->willReturn($process->reveal()) - ->shouldBeCalled(); - } - - protected function mockDownloadMySqlDump(ObjectProphecy $localMachineHelper, mixed $success): void { - $this->mockProcess($success); - $localMachineHelper->writeFile( - Argument::containingString("dev-profserv2-profserv201dev-something.sql.gz"), - 'backupfilecontents' - ) - ->shouldBeCalled(); - } + ], Argument::type('callable'), null, false, null, ['MYSQL_PWD' => 'drupal']) + ->willReturn($process->reveal()) + ->shouldBeCalled(); + } - protected function mockSettingsFiles(ObjectProphecy $fs): void { - $fs->remove(Argument::type('string')) - ->willReturn() - ->shouldBeCalled(); - } + protected function mockExecuteMySqlListTables( + LocalMachineHelper|ObjectProphecy $localMachineHelper, + string $dbName = 'jxr5000596dev', + ): void { + $localMachineHelper->checkRequiredBinariesExist(["mysql"])->shouldBeCalled(); + $process = $this->mockProcess(); + $process->getOutput()->willReturn('table1'); + $command = [ + 'mysql', + '--host', + 'localhost', + '--user', + 'drupal', + $dbName, + '--silent', + '-e', + 'SHOW TABLES;', + ]; + $localMachineHelper + ->execute($command, Argument::type('callable'), null, false, null, ['MYSQL_PWD' => $this->dbPassword]) + ->willReturn($process->reveal()) + ->shouldBeCalled(); + } - protected function mockListSites(SshHelper|ObjectProphecy $sshHelper): void { - $process = $this->mockProcess(); - $process->getOutput()->willReturn('default')->shouldBeCalled(); - $sshHelper->executeCommand(Argument::type('string'), ['ls', '/mnt/files/site.dev/sites'], FALSE) - ->willReturn($process->reveal())->shouldBeCalled(); - } + protected function mockExecuteMySqlDropDb( + LocalMachineHelper|ObjectProphecy $localMachineHelper, + bool $success, + ObjectProphecy $fs + ): void { + $localMachineHelper->checkRequiredBinariesExist(["mysql"])->shouldBeCalled(); + $process = $this->mockProcess($success); + $fs->tempnam(Argument::type('string'), 'acli_drop_table_', '.sql')->willReturn('something')->shouldBeCalled(); + $fs->dumpfile('something', Argument::type('string'))->shouldBeCalled(); + $localMachineHelper + ->execute(Argument::type('array'), Argument::type('callable'), null, false, null, ['MYSQL_PWD' => $this->dbPassword]) + ->willReturn($process->reveal()) + ->shouldBeCalled(); + } - public function mockGetBackup(mixed $environment): void { - $databases = $this->mockRequest('getEnvironmentsDatabases', $environment->id); - $tamper = function ($backups): void { - $backups[0]->completedAt = $backups[0]->completed_at; - }; - $backups = new BackupsResponse( - $this->mockRequest('getEnvironmentsDatabaseBackups', [ - $environment->id, - 'my_db', - ], NULL, NULL, $tamper) - ); - $this->mockDownloadBackup($databases[0], $environment, $backups[0]); - } + protected function mockExecuteMySqlImport( + ObjectProphecy $localMachineHelper, + bool $success, + bool $pvExists, + string $dbName = 'jxr5000596dev', + string $dbMachineName = 'db554675', + string $localDbName = 'jxr5000596dev', + string $env = 'dev' + ): void { + $localMachineHelper->checkRequiredBinariesExist(['gunzip', 'mysql'])->shouldBeCalled(); + $this->mockExecutePvExists($localMachineHelper, $pvExists); + $process = $this->mockProcess($success); + $filePath = Path::join(sys_get_temp_dir(), "$env-$dbName-$dbMachineName-2012-05-15T12:00:00.000Z.sql.gz"); + $command = $pvExists ? "pv $filePath --bytes --rate | gunzip | MYSQL_PWD=drupal mysql --host=localhost --user=drupal $localDbName" : "gunzip -c $filePath | MYSQL_PWD=drupal mysql --host=localhost --user=drupal $localDbName"; + // MySQL import command. + $localMachineHelper + ->executeFromCmd( + $command, + Argument::type('callable'), + null, + true, + null + ) + ->willReturn($process->reveal()) + ->shouldBeCalled(); + } - protected function mockDownloadBackup(object $database, object $environment, object $backup, int $curlCode = 0): object { - if ($curlCode) { - $this->prophet->prophesize(StreamInterface::class); - /** @var RequestException|ObjectProphecy $requestException */ - $requestException = $this->prophet->prophesize(RequestException::class); - $requestException->getHandlerContext()->willReturn(['errno' => $curlCode]); - $this->clientProphecy->stream('get', "/environments/{$environment->id}/databases/{$database->name}/backups/1/actions/download", []) - ->willThrow($requestException->reveal()) + protected function mockDownloadMySqlDump(ObjectProphecy $localMachineHelper, mixed $success): void + { + $this->mockProcess($success); + $localMachineHelper->writeFile( + Argument::containingString("dev-profserv2-profserv201dev-something.sql.gz"), + 'backupfilecontents' + ) ->shouldBeCalled(); - $response = $this->prophet->prophesize(ResponseInterface::class); - $this->httpClientProphecy->request('GET', 'https://other.example.com/download-backup', Argument::type('array'))->willReturn($response->reveal())->shouldBeCalled(); - $domainsResponse = $this->getMockResponseFromSpec('/environments/{environmentId}/domains', 'get', 200); - $this->clientProphecy->request('get', "/environments/{$environment->id}/domains")->willReturn($domainsResponse->_embedded->items); - $this->command->setBackupDownloadUrl(new Uri( 'https://www.example.com/download-backup')); } - else { - $this->mockDownloadBackupResponse($environment, $database->name, 1); + + protected function mockSettingsFiles(ObjectProphecy $fs): void + { + $fs->remove(Argument::type('string')) + ->willReturn() + ->shouldBeCalled(); } - if ($database->flags->default) { - $dbMachineName = $database->name . $environment->name; + + protected function mockListSites(SshHelper|ObjectProphecy $sshHelper): void + { + $process = $this->mockProcess(); + $process->getOutput()->willReturn('default')->shouldBeCalled(); + $sshHelper->executeCommand(Argument::type('string'), ['ls', '/mnt/files/site.dev/sites'], false) + ->willReturn($process->reveal())->shouldBeCalled(); } - else { - $dbMachineName = 'db' . $database->id; + + public function mockGetBackup(mixed $environment): void + { + $databases = $this->mockRequest('getEnvironmentsDatabases', $environment->id); + $tamper = function ($backups): void { + $backups[0]->completedAt = $backups[0]->completed_at; + }; + $backups = new BackupsResponse( + $this->mockRequest('getEnvironmentsDatabaseBackups', [ + $environment->id, + 'my_db', + ], null, null, $tamper) + ); + $this->mockDownloadBackup($databases[0], $environment, $backups[0]); } - $filename = implode('-', [ + + protected function mockDownloadBackup(object $database, object $environment, object $backup, int $curlCode = 0): object + { + if ($curlCode) { + $this->prophet->prophesize(StreamInterface::class); + /** @var RequestException|ObjectProphecy $requestException */ + $requestException = $this->prophet->prophesize(RequestException::class); + $requestException->getHandlerContext()->willReturn(['errno' => $curlCode]); + $this->clientProphecy->stream('get', "/environments/{$environment->id}/databases/{$database->name}/backups/1/actions/download", []) + ->willThrow($requestException->reveal()) + ->shouldBeCalled(); + $response = $this->prophet->prophesize(ResponseInterface::class); + $this->httpClientProphecy->request('GET', 'https://other.example.com/download-backup', Argument::type('array'))->willReturn($response->reveal())->shouldBeCalled(); + $domainsResponse = $this->getMockResponseFromSpec('/environments/{environmentId}/domains', 'get', 200); + $this->clientProphecy->request('get', "/environments/{$environment->id}/domains")->willReturn($domainsResponse->_embedded->items); + $this->command->setBackupDownloadUrl(new Uri('https://www.example.com/download-backup')); + } else { + $this->mockDownloadBackupResponse($environment, $database->name, 1); + } + if ($database->flags->default) { + $dbMachineName = $database->name . $environment->name; + } else { + $dbMachineName = 'db' . $database->id; + } + $filename = implode('-', [ $environment->name, $database->name, $dbMachineName, $backup->completedAt, - ]) . '.sql.gz'; - $localFilepath = Path::join(sys_get_temp_dir(), $filename); - $this->clientProphecy->addOption('sink', $localFilepath)->shouldBeCalled(); - $this->clientProphecy->addOption('curl.options', [ - 'CURLOPT_FILE' => $localFilepath, - 'CURLOPT_RETURNTRANSFER' => FALSE, - ])->shouldBeCalled(); - $this->clientProphecy->addOption('progress', Argument::type('Closure'))->shouldBeCalled(); - $this->clientProphecy->addOption('on_stats', Argument::type('Closure'))->shouldBeCalled(); - $this->clientProphecy->getOptions()->willReturn([]); - - return $database; - } - + ]) . '.sql.gz'; + $localFilepath = Path::join(sys_get_temp_dir(), $filename); + $this->clientProphecy->addOption('sink', $localFilepath)->shouldBeCalled(); + $this->clientProphecy->addOption('curl.options', [ + 'CURLOPT_FILE' => $localFilepath, + 'CURLOPT_RETURNTRANSFER' => false, + ])->shouldBeCalled(); + $this->clientProphecy->addOption('progress', Argument::type('Closure'))->shouldBeCalled(); + $this->clientProphecy->addOption('on_stats', Argument::type('Closure'))->shouldBeCalled(); + $this->clientProphecy->getOptions()->willReturn([]); + + return $database; + } } diff --git a/tests/phpunit/src/Commands/Pull/PullDatabaseCommandTest.php b/tests/phpunit/src/Commands/Pull/PullDatabaseCommandTest.php index 865a3b1a2..0a6ede37d 100644 --- a/tests/phpunit/src/Commands/Pull/PullDatabaseCommandTest.php +++ b/tests/phpunit/src/Commands/Pull/PullDatabaseCommandTest.php @@ -1,6 +1,6 @@ httpClientProphecy = $this->prophet->prophesize(Client::class); - - return new PullDatabaseCommand( - $this->localMachineHelper, - $this->datastoreCloud, - $this->datastoreAcli, - $this->cloudCredentials, - $this->telemetryHelper, - $this->acliRepoRoot, - $this->clientServiceProphecy->reveal(), - $this->sshHelper, - $this->sshDir, - $this->logger, - $this->httpClientProphecy->reveal() - ); - } - - public function testPullDatabases(): void { - $localMachineHelper = $this->mockLocalMachineHelper(); - $this->mockExecuteMySqlConnect($localMachineHelper, TRUE); - $environment = $this->mockGetEnvironment(); - $sshHelper = $this->mockSshHelper(); - $this->mockListSites($sshHelper); - $this->mockGetBackup($environment); - $this->mockExecuteMySqlListTables($localMachineHelper, 'drupal'); - $fs = $this->prophet->prophesize(Filesystem::class); - $this->mockExecuteMySqlDropDb($localMachineHelper, TRUE, $fs); - $this->mockExecuteMySqlImport($localMachineHelper, TRUE, TRUE, 'my_db', 'my_dbdev', 'drupal'); - $fs->remove(Argument::type('string'))->shouldBeCalled(); - $localMachineHelper->getFilesystem()->willReturn($fs)->shouldBeCalled(); - $this->mockExecuteDrushExists($localMachineHelper); - $this->mockExecuteDrushStatus($localMachineHelper, $this->projectDir); - $process = $this->mockProcess(); - $this->mockExecuteDrushCacheRebuild($localMachineHelper, $process); - $this->mockExecuteDrushSqlSanitize($localMachineHelper, $process); - - $this->executeCommand([ - '--no-scripts' => FALSE, - ], self::inputChooseEnvironment()); - - $output = $this->getDisplay(); - - $this->assertStringContainsString('Select a Cloud Platform application:', $output); - $this->assertStringContainsString('[0] Sample application 1', $output); - $this->assertStringContainsString('Choose a Cloud Platform environment', $output); - $this->assertStringContainsString('[0] Dev, dev (vcs: master)', $output); - $this->assertStringContainsString('Choose a database [my_db (default)]:', $output); - $this->assertStringContainsString('Downloading backup 1', $output); - } - - public function testPullProdDatabase(): void { - $localMachineHelper = $this->mockLocalMachineHelper(); - $this->mockExecuteMySqlConnect($localMachineHelper, TRUE); - $applications = $this->mockRequest('getApplications'); - $application = $this->mockRequest('getApplicationByUuid', $applications[self::$INPUT_DEFAULT_CHOICE]->uuid); - $environments = $this->mockRequest('getApplicationEnvironments', $application->uuid); - $environment = $environments[1]; - $sshHelper = $this->mockSshHelper(); - $process = $this->mockProcess(); - $process->getOutput()->willReturn('default')->shouldBeCalled(); - $sshHelper->executeCommand(Argument::type('string'), ['ls', '/mnt/files/site.prod/sites'], FALSE) - ->willReturn($process->reveal())->shouldBeCalled(); - $this->mockGetBackup($environment); - $this->mockExecuteMySqlListTables($localMachineHelper, 'drupal'); - $fs = $this->prophet->prophesize(Filesystem::class); - $this->mockExecuteMySqlDropDb($localMachineHelper, TRUE, $fs); - $this->mockExecuteMySqlImport($localMachineHelper, TRUE, TRUE, 'my_db', 'my_dbprod', 'drupal', 'prod'); - $fs->remove(Argument::type('string'))->shouldBeCalled(); - $localMachineHelper->getFilesystem()->willReturn($fs)->shouldBeCalled(); - - $this->executeCommand([ - '--no-scripts' => TRUE, - ], [ - // Would you like Acquia CLI to search for a Cloud application that matches your local git config? - 'n', - // Select a Cloud Platform application: - self::$INPUT_DEFAULT_CHOICE, - // Would you like to link the project at ... ? - 'n', - // Choose an Acquia environment: - 1, - ]); - - $output = $this->getDisplay(); - - $this->assertStringContainsString('Select a Cloud Platform application:', $output); - $this->assertStringContainsString('[0] Sample application 1', $output); - $this->assertStringContainsString('Choose a Cloud Platform environment', $output); - $this->assertStringContainsString('[0] Dev, dev (vcs: master)', $output); - $this->assertStringContainsString('Choose a database [my_db (default)]:', $output); - $this->assertStringContainsString('Downloading backup 1', $output); - } - - public function testPullDatabasesLocalConnectionFailure(): void { - $this->mockGetEnvironment(); - $localMachineHelper = $this->mockLocalMachineHelper(); - $this->mockExecuteMySqlConnect($localMachineHelper, FALSE); - - $this->expectException(AcquiaCliException::class); - $this->expectExceptionMessage('Unable to connect'); - $this->executeCommand([ - '--no-scripts' => TRUE, - ], self::inputChooseEnvironment()); - } - - public function testPullDatabaseNoPv(): void { - $localMachineHelper = $this->mockLocalMachineHelper(); - $this->mockExecuteMySqlConnect($localMachineHelper, TRUE); - $environment = $this->mockGetEnvironment(); - $sshHelper = $this->mockSshHelper(); - $this->mockListSites($sshHelper); - $this->mockGetBackup($environment); - $this->mockExecuteMySqlListTables($localMachineHelper, 'drupal'); - $fs = $this->prophet->prophesize(Filesystem::class); - $localMachineHelper->getFilesystem()->willReturn($fs)->shouldBeCalled(); - $this->mockExecuteMySqlDropDb($localMachineHelper, TRUE, $fs); - $this->mockExecuteMySqlImport($localMachineHelper, TRUE, FALSE, 'my_db', 'my_dbdev', 'drupal'); - $fs->remove(Argument::type('string'))->shouldBeCalled(); - - $this->executeCommand([ - '--no-scripts' => TRUE, - ], self::inputChooseEnvironment()); - - $output = $this->getDisplay(); - - $this->assertStringContainsString(' [WARNING] Install `pv` to see progress bar', $output); - } - - public function testPullMultipleDatabases(): void { - $this->setupPullDatabase(TRUE, TRUE, FALSE, TRUE, TRUE); - $inputs = [ - // Would you like Acquia CLI to search for a Cloud application that matches your local git config? - 'n', - // Select a Cloud Platform application: - 0, - // Would you like to link the project at ... ? - 'n', - // Choose a Cloud Platform environment [Dev, dev (vcs: master)]: - 0, - // Choose a site [jxr5000596dev (oracletest1.dev-profserv2.acsitefactory.com)]: - 0, - // Choose databases. You may choose multiple. Use commas to separate choices. [profserv2 (default)]: - '10,27', - ]; - $this->executeCommand([ - '--multiple-dbs' => TRUE, - '--no-scripts' => TRUE, - ], $inputs); - - } - - public function testPullDatabasesOnDemand(): void { - $this->setupPullDatabase(TRUE, TRUE, TRUE); - $inputs = self::inputChooseEnvironment(); - - $this->executeCommand([ - '--no-scripts' => TRUE, - '--on-demand' => TRUE, - ], $inputs); - - $output = $this->getDisplay(); - - $this->assertStringContainsString('Select a Cloud Platform application', $output); - $this->assertStringContainsString('[0] Sample application 1', $output); - $this->assertStringContainsString('Choose a Cloud Platform environment', $output); - $this->assertStringContainsString('[0] Dev, dev (vcs: master)', $output); - $this->assertStringContainsString('Choose a site [jxr5000596dev (oracletest1.dev-profserv2.acsitefactory.com)]:', $output); - $this->assertStringContainsString('jxr5000596dev (oracletest1.dev-profserv2.acsitefactory.com)', $output); - } - - public function testPullDatabasesNoExistingBackup(): void { - $this->setupPullDatabase(TRUE, TRUE, TRUE, TRUE, FALSE, 0, FALSE); - $inputs = self::inputChooseEnvironment(); - - $this->executeCommand([ - '--no-scripts' => TRUE, - ], $inputs); - - $output = $this->getDisplay(); - - $this->assertStringContainsString('Select a Cloud Platform application', $output); - $this->assertStringContainsString('[0] Sample application 1', $output); - $this->assertStringContainsString('Choose a Cloud Platform environment', $output); - $this->assertStringContainsString('[0] Dev, dev (vcs: master)', $output); - $this->assertStringContainsString('Choose a site [jxr5000596dev (oracletest1.dev-profserv2.acsitefactory.com)]:', $output); - $this->assertStringContainsString('jxr5000596dev (oracletest1.dev-profserv2.acsitefactory.com)', $output); - $this->assertStringContainsString('No existing backups found, creating an on-demand backup now.', $output); - } - - public function testPullDatabasesSiteArgument(): void { - $this->setupPullDatabase(TRUE, TRUE, FALSE, FALSE); - $inputs = self::inputChooseEnvironment(); - - $this->executeCommand([ - '--no-scripts' => TRUE, - 'site' => 'jxr5000596dev', - ], $inputs); - - $output = $this->getDisplay(); - - $this->assertStringContainsString('Select a Cloud Platform application', $output); - $this->assertStringContainsString('[0] Sample application 1', $output); - $this->assertStringContainsString('Choose a Cloud Platform environment', $output); - $this->assertStringContainsString('[0] Dev, dev (vcs: master)', $output); - $this->assertStringNotContainsString('Choose a database', $output); - } - - public function testPullDatabaseWithMySqlDropError(): void { - $localMachineHelper = $this->mockLocalMachineHelper(); - $this->mockExecuteMySqlConnect($localMachineHelper, TRUE); - $environment = $this->mockGetEnvironment(); - $sshHelper = $this->mockSshHelper(); - $this->mockListSites($sshHelper); - $this->mockGetBackup($environment); - $this->mockExecuteMySqlListTables($localMachineHelper, 'drupal'); - $fs = $this->prophet->prophesize(Filesystem::class); - $localMachineHelper->getFilesystem()->willReturn($fs)->shouldBeCalled(); - $this->mockExecuteMySqlDropDb($localMachineHelper, FALSE, $fs); - - $this->expectException(AcquiaCliException::class); - $this->expectExceptionMessage('Unable to drop tables from database'); - $this->executeCommand([ - '--no-scripts' => TRUE, - ], self::inputChooseEnvironment()); - } - - public function testPullDatabaseWithMySqlImportError(): void { - $localMachineHelper = $this->mockLocalMachineHelper(); - $this->mockExecuteMySqlConnect($localMachineHelper, TRUE); - $environment = $this->mockGetEnvironment(); - $sshHelper = $this->mockSshHelper(); - $this->mockListSites($sshHelper); - $this->mockGetBackup($environment); - $this->mockExecuteMySqlListTables($localMachineHelper, 'drupal'); - $fs = $this->prophet->prophesize(Filesystem::class); - $localMachineHelper->getFilesystem()->willReturn($fs)->shouldBeCalled(); - $this->mockExecuteMySqlDropDb($localMachineHelper, TRUE, $fs); - $this->mockExecuteMySqlImport($localMachineHelper, FALSE, TRUE, 'my_db', 'my_dbdev', 'drupal'); - - $this->expectException(AcquiaCliException::class); - $this->expectExceptionMessage('Unable to import local database'); - $this->executeCommand([ - '--no-scripts' => TRUE, - ], self::inputChooseEnvironment()); - } - - /** - * @dataProvider providerTestPullDatabaseWithInvalidSslCertificate - */ - public function testPullDatabaseWithInvalidSslCertificate(int $errorCode): void { - $this->setupPullDatabase(TRUE, FALSE, FALSE, TRUE, FALSE, $errorCode); - $inputs = self::inputChooseEnvironment(); - - $this->executeCommand(['--no-scripts' => TRUE], $inputs); - $output = $this->getDisplay(); - $this->assertStringContainsString('The certificate for www.example.com is invalid.', $output); - $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 { - $applicationsResponse = $this->mockApplicationsRequest(); - $this->mockApplicationRequest(); - $environmentsResponse = $this->mockAcsfEnvironmentsRequest($applicationsResponse); - $selectedEnvironment = $environmentsResponse->_embedded->items[0]; - $this->createMockGitConfigFile(); - - $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); - - if ($multiDb) { - $databaseResponse2 = $databasesResponse[array_search('profserv2', array_column($databasesResponse, 'name'), TRUE)]; - $databaseBackupsResponse2 = $this->mockDatabaseBackupsResponse($selectedEnvironment, $databaseResponse2->name, 1, $existingBackups); - $this->mockDownloadBackup($databaseResponse2, $selectedEnvironment, $databaseBackupsResponse2->_embedded->items[0], $curlCode); +class PullDatabaseCommandTest extends PullCommandTestBase +{ + /** + * @return int[][] + */ + public function providerTestPullDatabaseWithInvalidSslCertificate(): array + { + return [[51], [60]]; } - $sshHelper = $this->mockSshHelper(); - if ($mockGetAcsfSites) { - $this->mockGetAcsfSites($sshHelper); + protected function createCommand(): CommandBase + { + $this->httpClientProphecy = $this->prophet->prophesize(Client::class); + + return new PullDatabaseCommand( + $this->localMachineHelper, + $this->datastoreCloud, + $this->datastoreAcli, + $this->cloudCredentials, + $this->telemetryHelper, + $this->acliRepoRoot, + $this->clientServiceProphecy->reveal(), + $this->sshHelper, + $this->sshDir, + $this->logger, + $this->httpClientProphecy->reveal() + ); } - 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); + public function testPullDatabases(): void + { + $localMachineHelper = $this->mockLocalMachineHelper(); + $this->mockExecuteMySqlConnect($localMachineHelper, true); + $environment = $this->mockGetEnvironment(); + $sshHelper = $this->mockSshHelper(); + $this->mockListSites($sshHelper); + $this->mockGetBackup($environment); + $this->mockExecuteMySqlListTables($localMachineHelper, 'drupal'); + $fs = $this->prophet->prophesize(Filesystem::class); + $this->mockExecuteMySqlDropDb($localMachineHelper, true, $fs); + $this->mockExecuteMySqlImport($localMachineHelper, true, true, 'my_db', 'my_dbdev', 'drupal'); + $fs->remove(Argument::type('string'))->shouldBeCalled(); + $localMachineHelper->getFilesystem()->willReturn($fs)->shouldBeCalled(); + $this->mockExecuteDrushExists($localMachineHelper); + $this->mockExecuteDrushStatus($localMachineHelper, $this->projectDir); + $process = $this->mockProcess(); + $this->mockExecuteDrushCacheRebuild($localMachineHelper, $process); + $this->mockExecuteDrushSqlSanitize($localMachineHelper, $process); + + $this->executeCommand([ + '--no-scripts' => false, + ], self::inputChooseEnvironment()); + + $output = $this->getDisplay(); + + $this->assertStringContainsString('Select a Cloud Platform application:', $output); + $this->assertStringContainsString('[0] Sample application 1', $output); + $this->assertStringContainsString('Choose a Cloud Platform environment', $output); + $this->assertStringContainsString('[0] Dev, dev (vcs: master)', $output); + $this->assertStringContainsString('Choose a database [my_db (default)]:', $output); + $this->assertStringContainsString('Downloading backup 1', $output); } - $fs = $this->prophet->prophesize(Filesystem::class); - $localMachineHelper = $this->mockLocalMachineHelper(); - $this->mockExecuteMySqlConnect($localMachineHelper, $mysqlConnectSuccessful); - // Set up file system. - $localMachineHelper->getFilesystem()->willReturn($fs)->shouldBeCalled(); + public function testPullProdDatabase(): void + { + $localMachineHelper = $this->mockLocalMachineHelper(); + $this->mockExecuteMySqlConnect($localMachineHelper, true); + $applications = $this->mockRequest('getApplications'); + $application = $this->mockRequest('getApplicationByUuid', $applications[self::$INPUT_DEFAULT_CHOICE]->uuid); + $environments = $this->mockRequest('getApplicationEnvironments', $application->uuid); + $environment = $environments[1]; + $sshHelper = $this->mockSshHelper(); + $process = $this->mockProcess(); + $process->getOutput()->willReturn('default')->shouldBeCalled(); + $sshHelper->executeCommand(Argument::type('string'), ['ls', '/mnt/files/site.prod/sites'], false) + ->willReturn($process->reveal())->shouldBeCalled(); + $this->mockGetBackup($environment); + $this->mockExecuteMySqlListTables($localMachineHelper, 'drupal'); + $fs = $this->prophet->prophesize(Filesystem::class); + $this->mockExecuteMySqlDropDb($localMachineHelper, true, $fs); + $this->mockExecuteMySqlImport($localMachineHelper, true, true, 'my_db', 'my_dbprod', 'drupal', 'prod'); + $fs->remove(Argument::type('string'))->shouldBeCalled(); + $localMachineHelper->getFilesystem()->willReturn($fs)->shouldBeCalled(); + + $this->executeCommand([ + '--no-scripts' => true, + ], [ + // Would you like Acquia CLI to search for a Cloud application that matches your local git config? + 'n', + // Select a Cloud Platform application: + self::$INPUT_DEFAULT_CHOICE, + // Would you like to link the project at ... ? + 'n', + // Choose an Acquia environment: + 1, + ]); + + $output = $this->getDisplay(); + + $this->assertStringContainsString('Select a Cloud Platform application:', $output); + $this->assertStringContainsString('[0] Sample application 1', $output); + $this->assertStringContainsString('Choose a Cloud Platform environment', $output); + $this->assertStringContainsString('[0] Dev, dev (vcs: master)', $output); + $this->assertStringContainsString('Choose a database [my_db (default)]:', $output); + $this->assertStringContainsString('Downloading backup 1', $output); + } + + public function testPullDatabasesLocalConnectionFailure(): void + { + $this->mockGetEnvironment(); + $localMachineHelper = $this->mockLocalMachineHelper(); + $this->mockExecuteMySqlConnect($localMachineHelper, false); + + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage('Unable to connect'); + $this->executeCommand([ + '--no-scripts' => true, + ], self::inputChooseEnvironment()); + } + + public function testPullDatabaseNoPv(): void + { + $localMachineHelper = $this->mockLocalMachineHelper(); + $this->mockExecuteMySqlConnect($localMachineHelper, true); + $environment = $this->mockGetEnvironment(); + $sshHelper = $this->mockSshHelper(); + $this->mockListSites($sshHelper); + $this->mockGetBackup($environment); + $this->mockExecuteMySqlListTables($localMachineHelper, 'drupal'); + $fs = $this->prophet->prophesize(Filesystem::class); + $localMachineHelper->getFilesystem()->willReturn($fs)->shouldBeCalled(); + $this->mockExecuteMySqlDropDb($localMachineHelper, true, $fs); + $this->mockExecuteMySqlImport($localMachineHelper, true, false, 'my_db', 'my_dbdev', 'drupal'); + $fs->remove(Argument::type('string'))->shouldBeCalled(); + + $this->executeCommand([ + '--no-scripts' => true, + ], self::inputChooseEnvironment()); + + $output = $this->getDisplay(); + + $this->assertStringContainsString(' [WARNING] Install `pv` to see progress bar', $output); + } + + public function testPullMultipleDatabases(): void + { + $this->setupPullDatabase(true, true, false, true, true); + $inputs = [ + // Would you like Acquia CLI to search for a Cloud application that matches your local git config? + 'n', + // Select a Cloud Platform application: + 0, + // Would you like to link the project at ... ? + 'n', + // Choose a Cloud Platform environment [Dev, dev (vcs: master)]: + 0, + // Choose a site [jxr5000596dev (oracletest1.dev-profserv2.acsitefactory.com)]: + 0, + // Choose databases. You may choose multiple. Use commas to separate choices. [profserv2 (default)]: + '10,27', + ]; + $this->executeCommand([ + '--multiple-dbs' => true, + '--no-scripts' => true, + ], $inputs); + } + + public function testPullDatabasesOnDemand(): void + { + $this->setupPullDatabase(true, true, true); + $inputs = self::inputChooseEnvironment(); + + $this->executeCommand([ + '--no-scripts' => true, + '--on-demand' => true, + ], $inputs); + + $output = $this->getDisplay(); + + $this->assertStringContainsString('Select a Cloud Platform application', $output); + $this->assertStringContainsString('[0] Sample application 1', $output); + $this->assertStringContainsString('Choose a Cloud Platform environment', $output); + $this->assertStringContainsString('[0] Dev, dev (vcs: master)', $output); + $this->assertStringContainsString('Choose a site [jxr5000596dev (oracletest1.dev-profserv2.acsitefactory.com)]:', $output); + $this->assertStringContainsString('jxr5000596dev (oracletest1.dev-profserv2.acsitefactory.com)', $output); + } + + public function testPullDatabasesNoExistingBackup(): void + { + $this->setupPullDatabase(true, true, true, true, false, 0, false); + $inputs = self::inputChooseEnvironment(); + + $this->executeCommand([ + '--no-scripts' => true, + ], $inputs); + + $output = $this->getDisplay(); + + $this->assertStringContainsString('Select a Cloud Platform application', $output); + $this->assertStringContainsString('[0] Sample application 1', $output); + $this->assertStringContainsString('Choose a Cloud Platform environment', $output); + $this->assertStringContainsString('[0] Dev, dev (vcs: master)', $output); + $this->assertStringContainsString('Choose a site [jxr5000596dev (oracletest1.dev-profserv2.acsitefactory.com)]:', $output); + $this->assertStringContainsString('jxr5000596dev (oracletest1.dev-profserv2.acsitefactory.com)', $output); + $this->assertStringContainsString('No existing backups found, creating an on-demand backup now.', $output); + } - // Mock IDE filesystem. - if ($mockIdeFs) { - $this->mockDrupalSettingsRefresh($localMachineHelper); + public function testPullDatabasesSiteArgument(): void + { + $this->setupPullDatabase(true, true, false, false); + $inputs = self::inputChooseEnvironment(); + + $this->executeCommand([ + '--no-scripts' => true, + 'site' => 'jxr5000596dev', + ], $inputs); + + $output = $this->getDisplay(); + + $this->assertStringContainsString('Select a Cloud Platform application', $output); + $this->assertStringContainsString('[0] Sample application 1', $output); + $this->assertStringContainsString('Choose a Cloud Platform environment', $output); + $this->assertStringContainsString('[0] Dev, dev (vcs: master)', $output); + $this->assertStringNotContainsString('Choose a database', $output); } - $this->mockSettingsFiles($fs); - - // Database. - $this->mockExecuteMySqlListTables($localMachineHelper); - $this->mockExecuteMySqlDropDb($localMachineHelper, TRUE, $fs); - $this->mockExecuteMySqlImport($localMachineHelper, TRUE, TRUE); - if ($multiDb) { - $this->mockExecuteMySqlListTables($localMachineHelper, 'drupal'); - $this->mockExecuteMySqlImport($localMachineHelper, TRUE, TRUE, 'profserv2', 'profserv2dev', 'drupal'); + + public function testPullDatabaseWithMySqlDropError(): void + { + $localMachineHelper = $this->mockLocalMachineHelper(); + $this->mockExecuteMySqlConnect($localMachineHelper, true); + $environment = $this->mockGetEnvironment(); + $sshHelper = $this->mockSshHelper(); + $this->mockListSites($sshHelper); + $this->mockGetBackup($environment); + $this->mockExecuteMySqlListTables($localMachineHelper, 'drupal'); + $fs = $this->prophet->prophesize(Filesystem::class); + $localMachineHelper->getFilesystem()->willReturn($fs)->shouldBeCalled(); + $this->mockExecuteMySqlDropDb($localMachineHelper, false, $fs); + + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage('Unable to drop tables from database'); + $this->executeCommand([ + '--no-scripts' => true, + ], self::inputChooseEnvironment()); } - } - - protected function mockSshHelper(): ObjectProphecy|SshHelper { - $sshHelper = parent::mockSshHelper(); - $this->command->sshHelper = $sshHelper->reveal(); - return $sshHelper; - } - - public function testDownloadProgressDisplay(): void { - $output = new BufferedOutput(); - $progress = NULL; - PullCommandBase::displayDownloadProgress(100, 0, $progress, $output); - $this->assertStringContainsString('0/100 [💧---------------------------] 0%', $output->fetch()); - - // Need to sleep to prevent the default redraw frequency from skipping display. - sleep(1); - PullCommandBase::displayDownloadProgress(100, 50, $progress, $output); - $this->assertStringContainsString('50/100 [==============💧-------------] 50%', $output->fetch()); - - PullCommandBase::displayDownloadProgress(100, 100, $progress, $output); - $this->assertStringContainsString('100/100 [============================] 100%', $output->fetch()); - } - - public function testPullNode(): void { - $applications = $this->mockRequest('getApplications'); - $application = $this->mockRequest('getApplicationByUuid', $applications[self::$INPUT_DEFAULT_CHOICE]->uuid); - $tamper = function ($responses): void { - foreach ($responses as $response) { - $response->type = 'node'; - } - }; - $this->mockRequest('getApplicationEnvironments', $application->uuid, NULL, NULL, $tamper); - - $this->expectException(AcquiaCliException::class); - $this->expectExceptionMessage('No compatible environments found'); - $this->executeCommand([ - '--no-scripts' => TRUE, - ], [ - // Would you like Acquia CLI to search for a Cloud application that matches your local git config? - 'n', - // Select a Cloud Platform application: - self::$INPUT_DEFAULT_CHOICE, - // Would you like to link the project at ... ? - 'n', - // Choose an Acquia environment: - 1, - ]); - } + public function testPullDatabaseWithMySqlImportError(): void + { + $localMachineHelper = $this->mockLocalMachineHelper(); + $this->mockExecuteMySqlConnect($localMachineHelper, true); + $environment = $this->mockGetEnvironment(); + $sshHelper = $this->mockSshHelper(); + $this->mockListSites($sshHelper); + $this->mockGetBackup($environment); + $this->mockExecuteMySqlListTables($localMachineHelper, 'drupal'); + $fs = $this->prophet->prophesize(Filesystem::class); + $localMachineHelper->getFilesystem()->willReturn($fs)->shouldBeCalled(); + $this->mockExecuteMySqlDropDb($localMachineHelper, true, $fs); + $this->mockExecuteMySqlImport($localMachineHelper, false, true, 'my_db', 'my_dbdev', 'drupal'); + + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage('Unable to import local database'); + $this->executeCommand([ + '--no-scripts' => true, + ], self::inputChooseEnvironment()); + } + + /** + * @dataProvider providerTestPullDatabaseWithInvalidSslCertificate + */ + public function testPullDatabaseWithInvalidSslCertificate(int $errorCode): void + { + $this->setupPullDatabase(true, false, false, true, false, $errorCode); + $inputs = self::inputChooseEnvironment(); + + $this->executeCommand(['--no-scripts' => true], $inputs); + $output = $this->getDisplay(); + $this->assertStringContainsString('The certificate for www.example.com is invalid.', $output); + $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 + { + $applicationsResponse = $this->mockApplicationsRequest(); + $this->mockApplicationRequest(); + $environmentsResponse = $this->mockAcsfEnvironmentsRequest($applicationsResponse); + $selectedEnvironment = $environmentsResponse->_embedded->items[0]; + $this->createMockGitConfigFile(); + + $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); + + if ($multiDb) { + $databaseResponse2 = $databasesResponse[array_search('profserv2', array_column($databasesResponse, 'name'), true)]; + $databaseBackupsResponse2 = $this->mockDatabaseBackupsResponse($selectedEnvironment, $databaseResponse2->name, 1, $existingBackups); + $this->mockDownloadBackup($databaseResponse2, $selectedEnvironment, $databaseBackupsResponse2->_embedded->items[0], $curlCode); + } + + $sshHelper = $this->mockSshHelper(); + if ($mockGetAcsfSites) { + $this->mockGetAcsfSites($sshHelper); + } + + 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); + } + + $fs = $this->prophet->prophesize(Filesystem::class); + $localMachineHelper = $this->mockLocalMachineHelper(); + $this->mockExecuteMySqlConnect($localMachineHelper, $mysqlConnectSuccessful); + // Set up file system. + $localMachineHelper->getFilesystem()->willReturn($fs)->shouldBeCalled(); + + // Mock IDE filesystem. + if ($mockIdeFs) { + $this->mockDrupalSettingsRefresh($localMachineHelper); + } + $this->mockSettingsFiles($fs); + + // Database. + $this->mockExecuteMySqlListTables($localMachineHelper); + $this->mockExecuteMySqlDropDb($localMachineHelper, true, $fs); + $this->mockExecuteMySqlImport($localMachineHelper, true, true); + if ($multiDb) { + $this->mockExecuteMySqlListTables($localMachineHelper, 'drupal'); + $this->mockExecuteMySqlImport($localMachineHelper, true, true, 'profserv2', 'profserv2dev', 'drupal'); + } + } + + protected function mockSshHelper(): ObjectProphecy|SshHelper + { + $sshHelper = parent::mockSshHelper(); + $this->command->sshHelper = $sshHelper->reveal(); + return $sshHelper; + } + + public function testDownloadProgressDisplay(): void + { + $output = new BufferedOutput(); + $progress = null; + PullCommandBase::displayDownloadProgress(100, 0, $progress, $output); + $this->assertStringContainsString('0/100 [💧---------------------------] 0%', $output->fetch()); + + // Need to sleep to prevent the default redraw frequency from skipping display. + sleep(1); + PullCommandBase::displayDownloadProgress(100, 50, $progress, $output); + $this->assertStringContainsString('50/100 [==============💧-------------] 50%', $output->fetch()); + + PullCommandBase::displayDownloadProgress(100, 100, $progress, $output); + $this->assertStringContainsString('100/100 [============================] 100%', $output->fetch()); + } + + public function testPullNode(): void + { + $applications = $this->mockRequest('getApplications'); + $application = $this->mockRequest('getApplicationByUuid', $applications[self::$INPUT_DEFAULT_CHOICE]->uuid); + $tamper = function ($responses): void { + foreach ($responses as $response) { + $response->type = 'node'; + } + }; + $this->mockRequest('getApplicationEnvironments', $application->uuid, null, null, $tamper); + + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage('No compatible environments found'); + $this->executeCommand([ + '--no-scripts' => true, + ], [ + // Would you like Acquia CLI to search for a Cloud application that matches your local git config? + 'n', + // Select a Cloud Platform application: + self::$INPUT_DEFAULT_CHOICE, + // Would you like to link the project at ... ? + 'n', + // Choose an Acquia environment: + 1, + ]); + } } diff --git a/tests/phpunit/src/Commands/Pull/PullFilesCommandTest.php b/tests/phpunit/src/Commands/Pull/PullFilesCommandTest.php index 2dbc70905..f1acadef2 100644 --- a/tests/phpunit/src/Commands/Pull/PullFilesCommandTest.php +++ b/tests/phpunit/src/Commands/Pull/PullFilesCommandTest.php @@ -1,6 +1,6 @@ httpClientProphecy = $this->prophet->prophesize(Client::class); - - return new PullFilesCommand( - $this->localMachineHelper, - $this->datastoreCloud, - $this->datastoreAcli, - $this->cloudCredentials, - $this->telemetryHelper, - $this->acliRepoRoot, - $this->clientServiceProphecy->reveal(), - $this->sshHelper, - $this->sshDir, - $this->logger, - $this->httpClientProphecy->reveal() - ); - } - - public function testRefreshAcsfFiles(): void { - $applicationsResponse = $this->mockApplicationsRequest(); - $this->mockApplicationRequest(); - $environmentsResponse = $this->mockAcsfEnvironmentsRequest($applicationsResponse); - $selectedEnvironment = $environmentsResponse->_embedded->items[0]; - $sshHelper = $this->mockSshHelper(); - $this->mockGetAcsfSites($sshHelper); - $localMachineHelper = $this->mockLocalMachineHelper(); - $this->mockGetFilesystem($localMachineHelper); - $this->mockExecuteRsync($localMachineHelper, $selectedEnvironment, '/mnt/files/profserv2.01dev/sites/g/files/jxr5000596dev/files/', $this->projectDir . '/docroot/sites/jxr5000596dev/files'); - - $this->command->sshHelper = $sshHelper->reveal(); - - $inputs = [ - // Would you like Acquia CLI to search for a Cloud application that matches your local git config? - 'n', - // Select a Cloud Platform application: - 0, - // Would you like to link the project at ... ? - 'n', - // Choose an Acquia environment: - 0, - // Choose site from which to copy files: - 0, - ]; - - $this->executeCommand([], $inputs); - - $output = $this->getDisplay(); - - $this->assertStringContainsString('Select a Cloud Platform application:', $output); - $this->assertStringContainsString('[0] Sample application 1', $output); - $this->assertStringContainsString('Choose a Cloud Platform environment', $output); - $this->assertStringContainsString('[0] Dev, dev (vcs: master)', $output); - } - - public function testRefreshCloudFiles(): void { - $applicationsResponse = $this->mockApplicationsRequest(); - $this->mockApplicationRequest(); - $environmentsResponse = $this->mockEnvironmentsRequest($applicationsResponse); - $selectedEnvironment = $environmentsResponse->_embedded->items[0]; - $sshHelper = $this->mockSshHelper(); - $this->mockGetCloudSites($sshHelper, $selectedEnvironment); - $localMachineHelper = $this->mockLocalMachineHelper(); - $this->mockGetFilesystem($localMachineHelper); - $parts = explode('.', $selectedEnvironment->ssh_url); - $sitegroup = reset($parts); - $this->mockExecuteRsync($localMachineHelper, $selectedEnvironment, '/mnt/files/' . $sitegroup . '.' . $selectedEnvironment->name . '/sites/bar/files/', $this->projectDir . '/docroot/sites/bar/files'); - - $this->command->sshHelper = $sshHelper->reveal(); - - $inputs = [ - // Would you like Acquia CLI to search for a Cloud application that matches your local git config? - 'n', - // Select a Cloud Platform application: - 0, - // Would you like to link the project at ... ? - 'n', - // Choose an Acquia environment: - 0, - // Choose site from which to copy files: - 0, - ]; - - $this->executeCommand([], $inputs); - - $output = $this->getDisplay(); - - $this->assertStringContainsString('Select a Cloud Platform application', $output); - $this->assertStringContainsString('[0] Sample application 1', $output); - $this->assertStringContainsString('Choose a Cloud Platform environment', $output); - $this->assertStringContainsString('[0] Dev, dev (vcs: master)', $output); - } - - public function testInvalidCwd(): void { - IdeHelper::setCloudIdeEnvVars(); - $localMachineHelper = $this->mockLocalMachineHelper(); - $this->mockDrupalSettingsRefresh($localMachineHelper); - - $this->expectException(AcquiaCliException::class); - $this->expectExceptionMessage('Run this command from the '); - $this->executeCommand(); - IdeHelper::unsetCloudIdeEnvVars(); - } - +class PullFilesCommandTest extends PullCommandTestBase +{ + protected function createCommand(): CommandBase + { + $this->httpClientProphecy = $this->prophet->prophesize(Client::class); + + return new PullFilesCommand( + $this->localMachineHelper, + $this->datastoreCloud, + $this->datastoreAcli, + $this->cloudCredentials, + $this->telemetryHelper, + $this->acliRepoRoot, + $this->clientServiceProphecy->reveal(), + $this->sshHelper, + $this->sshDir, + $this->logger, + $this->httpClientProphecy->reveal() + ); + } + + public function testRefreshAcsfFiles(): void + { + $applicationsResponse = $this->mockApplicationsRequest(); + $this->mockApplicationRequest(); + $environmentsResponse = $this->mockAcsfEnvironmentsRequest($applicationsResponse); + $selectedEnvironment = $environmentsResponse->_embedded->items[0]; + $sshHelper = $this->mockSshHelper(); + $this->mockGetAcsfSites($sshHelper); + $localMachineHelper = $this->mockLocalMachineHelper(); + $this->mockGetFilesystem($localMachineHelper); + $this->mockExecuteRsync($localMachineHelper, $selectedEnvironment, '/mnt/files/profserv2.01dev/sites/g/files/jxr5000596dev/files/', $this->projectDir . '/docroot/sites/jxr5000596dev/files'); + + $this->command->sshHelper = $sshHelper->reveal(); + + $inputs = [ + // Would you like Acquia CLI to search for a Cloud application that matches your local git config? + 'n', + // Select a Cloud Platform application: + 0, + // Would you like to link the project at ... ? + 'n', + // Choose an Acquia environment: + 0, + // Choose site from which to copy files: + 0, + ]; + + $this->executeCommand([], $inputs); + + $output = $this->getDisplay(); + + $this->assertStringContainsString('Select a Cloud Platform application:', $output); + $this->assertStringContainsString('[0] Sample application 1', $output); + $this->assertStringContainsString('Choose a Cloud Platform environment', $output); + $this->assertStringContainsString('[0] Dev, dev (vcs: master)', $output); + } + + public function testRefreshCloudFiles(): void + { + $applicationsResponse = $this->mockApplicationsRequest(); + $this->mockApplicationRequest(); + $environmentsResponse = $this->mockEnvironmentsRequest($applicationsResponse); + $selectedEnvironment = $environmentsResponse->_embedded->items[0]; + $sshHelper = $this->mockSshHelper(); + $this->mockGetCloudSites($sshHelper, $selectedEnvironment); + $localMachineHelper = $this->mockLocalMachineHelper(); + $this->mockGetFilesystem($localMachineHelper); + $parts = explode('.', $selectedEnvironment->ssh_url); + $sitegroup = reset($parts); + $this->mockExecuteRsync($localMachineHelper, $selectedEnvironment, '/mnt/files/' . $sitegroup . '.' . $selectedEnvironment->name . '/sites/bar/files/', $this->projectDir . '/docroot/sites/bar/files'); + + $this->command->sshHelper = $sshHelper->reveal(); + + $inputs = [ + // Would you like Acquia CLI to search for a Cloud application that matches your local git config? + 'n', + // Select a Cloud Platform application: + 0, + // Would you like to link the project at ... ? + 'n', + // Choose an Acquia environment: + 0, + // Choose site from which to copy files: + 0, + ]; + + $this->executeCommand([], $inputs); + + $output = $this->getDisplay(); + + $this->assertStringContainsString('Select a Cloud Platform application', $output); + $this->assertStringContainsString('[0] Sample application 1', $output); + $this->assertStringContainsString('Choose a Cloud Platform environment', $output); + $this->assertStringContainsString('[0] Dev, dev (vcs: master)', $output); + } + + public function testInvalidCwd(): void + { + IdeHelper::setCloudIdeEnvVars(); + $localMachineHelper = $this->mockLocalMachineHelper(); + $this->mockDrupalSettingsRefresh($localMachineHelper); + + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage('Run this command from the '); + $this->executeCommand(); + IdeHelper::unsetCloudIdeEnvVars(); + } } diff --git a/tests/phpunit/src/Commands/Pull/PullScriptsCommandTest.php b/tests/phpunit/src/Commands/Pull/PullScriptsCommandTest.php index cfd042e5d..ccd4602a2 100644 --- a/tests/phpunit/src/Commands/Pull/PullScriptsCommandTest.php +++ b/tests/phpunit/src/Commands/Pull/PullScriptsCommandTest.php @@ -1,6 +1,6 @@ injectCommand(PullScriptsCommand::class); - } - - public function testRefreshScripts(): void { - touch(Path::join($this->projectDir, 'composer.json')); - $localMachineHelper = $this->mockLocalMachineHelper(); - $process = $this->mockProcess(); - - // Composer. - $this->mockExecuteComposerExists($localMachineHelper); - $this->mockExecuteComposerInstall($localMachineHelper, $process); - - // Drush. - $this->mockExecuteDrushExists($localMachineHelper); - $this->mockExecuteDrushStatus($localMachineHelper, $this->projectDir); - $this->mockExecuteDrushCacheRebuild($localMachineHelper, $process); - $this->mockExecuteDrushSqlSanitize($localMachineHelper, $process); - - $inputs = [ - // Would you like Acquia CLI to search for a Cloud application that matches your local git config? - 'n', - // Select a Cloud Platform application: - 0, - // Would you like to link the project at ... ? - 'n', - // Choose an Acquia environment: - 0, - ]; - - $this->executeCommand([ - '--dir' => $this->projectDir, - ], $inputs); - - $this->getDisplay(); - } - +class PullScriptsCommandTest extends PullCommandTestBase +{ + protected function createCommand(): CommandBase + { + return $this->injectCommand(PullScriptsCommand::class); + } + + public function testRefreshScripts(): void + { + touch(Path::join($this->projectDir, 'composer.json')); + $localMachineHelper = $this->mockLocalMachineHelper(); + $process = $this->mockProcess(); + + // Composer. + $this->mockExecuteComposerExists($localMachineHelper); + $this->mockExecuteComposerInstall($localMachineHelper, $process); + + // Drush. + $this->mockExecuteDrushExists($localMachineHelper); + $this->mockExecuteDrushStatus($localMachineHelper, $this->projectDir); + $this->mockExecuteDrushCacheRebuild($localMachineHelper, $process); + $this->mockExecuteDrushSqlSanitize($localMachineHelper, $process); + + $inputs = [ + // Would you like Acquia CLI to search for a Cloud application that matches your local git config? + 'n', + // Select a Cloud Platform application: + 0, + // Would you like to link the project at ... ? + 'n', + // Choose an Acquia environment: + 0, + ]; + + $this->executeCommand([ + '--dir' => $this->projectDir, + ], $inputs); + + $this->getDisplay(); + } } diff --git a/tests/phpunit/src/Commands/Push/PushArtifactCommandTest.php b/tests/phpunit/src/Commands/Push/PushArtifactCommandTest.php index fbef27e9a..f1aaeaac4 100644 --- a/tests/phpunit/src/Commands/Push/PushArtifactCommandTest.php +++ b/tests/phpunit/src/Commands/Push/PushArtifactCommandTest.php @@ -1,6 +1,6 @@ injectCommand(PushArtifactCommand::class); - } - - public function testNoAuthenticationRequired(): void { - $help = $this->command->getHelp(); - $this->assertStringNotContainsString('This command requires authentication', $help); - } - - public function testPushArtifact(): void { - $applications = $this->mockRequest('getApplications'); - $this->mockRequest('getApplicationByUuid', $applications[0]->uuid); - $environments = $this->mockRequest('getApplicationEnvironments', $applications[0]->uuid); - $localMachineHelper = $this->mockLocalMachineHelper(); - $this->setUpPushArtifact($localMachineHelper, $environments[0]->vcs->path, [$environments[0]->vcs->url]); - $inputs = [ - // Would you like Acquia CLI to search for a Cloud application that matches your local git config? - 'n', - // Select a Cloud Platform application: - 0, - // Would you like to link the project at ... ? - 'y', - // Choose an Acquia environment: - 0, - ]; - $this->executeCommand([], $inputs); - - $output = $this->getDisplay(); - - $this->assertStringContainsString('Select a Cloud Platform application:', $output); - $this->assertStringContainsString('[0] Sample application 1', $output); - $this->assertStringContainsString('Choose a Cloud Platform environment', $output); - $this->assertStringContainsString('[0] Dev, dev (vcs: master)', $output); - $this->assertStringNotContainsString('Production, prod', $output); - $this->assertStringContainsString('Acquia CLI will:', $output); - $this->assertStringContainsString('- git clone master from site@svn-3.hosted.acquia-sites.com:site.git', $output); - $this->assertStringContainsString('- Compile the contents of vfs://root/project into an artifact', $output); - $this->assertStringContainsString('- Copy the artifact files into the checked out copy of master', $output); - $this->assertStringContainsString('- Commit changes and push the master branch', $output); - $this->assertStringContainsString('Removing', $output); - $this->assertStringContainsString('Initializing Git', $output); - $this->assertStringContainsString('Global .gitignore file', $output); - $this->assertStringContainsString('Removing vendor', $output); - $this->assertStringContainsString('Mirroring source', $output); - $this->assertStringContainsString('Installing Composer', $output); - $this->assertStringContainsString('Finding Drupal', $output); - $this->assertStringContainsString('Removing sensitive', $output); - $this->assertStringContainsString('Adding and committing', $output); - $this->assertStringContainsString('Pushing changes to Acquia Git (site@svn-3.hosted.acquia-sites.com:site.git)', $output); - } - - public function testPushTagArtifact(): void { - $applications = $this->mockRequest('getApplications'); - $this->mockRequest('getApplicationByUuid', $applications[0]->uuid); - $environments = $this->mockRequest('getApplicationEnvironments', $applications[0]->uuid); - $localMachineHelper = $this->mockLocalMachineHelper(); - $gitTag = '1.2.0-build'; - $this->setUpPushArtifact($localMachineHelper, '1.2.0', [$environments[0]->vcs->url], $gitTag); - $artifactDir = Path::join(sys_get_temp_dir(), 'acli-push-artifact'); - $this->mockGitTag($localMachineHelper, $gitTag, $artifactDir); - $inputs = [ - // Would you like Acquia CLI to search for a Cloud application that matches your local git config? - 'n', - // Select a Cloud Platform application: - 0, - // Would you like to link the project at ... ? - 'n', - ]; - $this->executeCommand([ - '--destination-git-tag' => $gitTag, - '--source-git-tag' => '1.2.0', - ], $inputs); - - $output = $this->getDisplay(); - - $this->assertStringContainsString('Select a Cloud Platform application:', $output); - $this->assertStringContainsString('[0] Sample application 1', $output); - $this->assertStringContainsString('Pushing changes to Acquia Git (site@svn-3.hosted.acquia-sites.com:site.git)', $output); - $this->assertStringContainsString('Commit changes and push the 1.2.0-build tag', $output); - } - - public function testPushArtifactWithAcquiaCliFile(): void { - $applications = $this->mockRequest('getApplications'); - $this->mockRequest('getApplicationByUuid', $applications[0]->uuid); - $this->mockRequest('getApplicationEnvironments', $applications[0]->uuid); - $this->datastoreAcli->set('push.artifact.destination-git-urls', [ - 'https://github.com/example1/cli.git', - 'https://github.com/example2/cli.git', - ]); - $localMachineHelper = $this->mockLocalMachineHelper(); - $this->setUpPushArtifact($localMachineHelper, 'master', $this->datastoreAcli->get('push.artifact.destination-git-urls')); - $this->executeCommand([ - '--destination-git-branch' => 'master', - ]); - - $output = $this->getDisplay(); - - $this->assertStringContainsString('Pushing changes to Acquia Git (https://github.com/example1/cli.git)', $output); - $this->assertStringContainsString('Pushing changes to Acquia Git (https://github.com/example2/cli.git)', $output); - } - - public function testPushArtifactWithArgs(): void { - $applications = $this->mockRequest('getApplications'); - $this->mockRequest('getApplicationByUuid', $applications[0]->uuid); - $this->mockRequest('getApplicationEnvironments', $applications[0]->uuid); - $destinationGitUrls = [ - 'https://github.com/example1/cli.git', - 'https://github.com/example2/cli.git', - ]; - $localMachineHelper = $this->mockLocalMachineHelper(); - $this->setUpPushArtifact($localMachineHelper, 'master', $destinationGitUrls); - $this->executeCommand([ - '--destination-git-branch' => 'master', - '--destination-git-urls' => $destinationGitUrls, - ]); - - $output = $this->getDisplay(); - - $this->assertStringContainsString('Pushing changes to Acquia Git (https://github.com/example1/cli.git)', $output); - $this->assertStringContainsString('Pushing changes to Acquia Git (https://github.com/example2/cli.git)', $output); - } - - public function testPushArtifactNoPush(): void { - $applications = $this->mockRequest('getApplications'); - $this->mockRequest('getApplicationByUuid', $applications[0]->uuid); - $environments = $this->mockRequest('getApplicationEnvironments', $applications[0]->uuid); - $localMachineHelper = $this->mockLocalMachineHelper(); - $this->setUpPushArtifact($localMachineHelper, $environments[0]->vcs->path, [$environments[0]->vcs->url], 'master:master', TRUE, TRUE, FALSE); - $inputs = [ - // Would you like Acquia CLI to search for a Cloud application that matches your local git config? - 'n', - // Select a Cloud Platform application: - 0, - // Would you like to link the project at ... ? - 'y', - // Choose an Acquia environment: - 0, - ]; - $this->executeCommand(['--no-push' => TRUE], $inputs); - - $output = $this->getDisplay(); - - $this->assertStringContainsString('Initializing Git', $output); - $this->assertStringContainsString('Adding and committing changed files', $output); - $this->assertStringNotContainsString('Pushing changes to Acquia Git (site@svn-3.hosted.acquia-sites.com:site.git)', $output); - } - - public function testPushArtifactNoCommit(): void { - $applications = $this->mockRequest('getApplications'); - $this->mockRequest('getApplicationByUuid', $applications[0]->uuid); - $environments = $this->mockRequest('getApplicationEnvironments', $applications[0]->uuid); - $localMachineHelper = $this->mockLocalMachineHelper(); - $this->setUpPushArtifact($localMachineHelper, $environments[0]->vcs->path, [$environments[0]->vcs->url], 'master:master', TRUE, FALSE, FALSE); - $inputs = [ - // Would you like Acquia CLI to search for a Cloud application that matches your local git config? - 'n', - // Select a Cloud Platform application: - 0, - // Would you like to link the project at ... ? - 'y', - // Choose an Acquia environment: - 0, - ]; - $this->executeCommand(['--no-commit' => TRUE], $inputs); - - $output = $this->getDisplay(); - - $this->assertStringContainsString('Initializing Git', $output); - $this->assertStringNotContainsString('Adding and committing changed files', $output); - $this->assertStringNotContainsString('Pushing changes to Acquia Git (site@svn-3.hosted.acquia-sites.com:site.git)', $output); - } - - public function testPushArtifactNoClone(): void { - $localMachineHelper = $this->mockLocalMachineHelper(); - $this->setUpPushArtifact($localMachineHelper, 'nothing', [], 'something', FALSE, FALSE, FALSE); - $inputs = [ - // Would you like Acquia CLI to search for a Cloud application that matches your local git config? - 'n', - // Select a Cloud Platform application: - 0, - // Would you like to link the project at ... ? - 'y', - // Choose an Acquia environment: - 0, - ]; - $this->executeCommand(['--no-clone' => TRUE], $inputs); - - $output = $this->getDisplay(); - - $this->assertStringNotContainsString('Initializing Git', $output); - $this->assertStringNotContainsString('Adding and committing changed files', $output); - $this->assertStringNotContainsString('Pushing changes to Acquia Git (site@svn-3.hosted.acquia-sites.com:site.git)', $output); - } - - protected function setUpPushArtifact(ObjectProphecy $localMachineHelper, string $vcsPath, array $vcsUrls, string $destGitRef = 'master:master', bool $clone = TRUE, bool $commit = TRUE, bool $push = TRUE): void { - touch(Path::join($this->projectDir, 'composer.json')); - mkdir(Path::join($this->projectDir, 'docroot')); - $artifactDir = Path::join(sys_get_temp_dir(), 'acli-push-artifact'); - $this->createMockGitConfigFile(); - $finder = $this->mockFinder(); - $localMachineHelper->getFinder()->willReturn($finder->reveal()); - $fs = $this->prophet->prophesize(Filesystem::class); - $localMachineHelper->getFilesystem()->willReturn($fs)->shouldBeCalled(); - - $this->mockExecuteGitStatus(FALSE, $localMachineHelper, $this->projectDir); - $commitHash = 'abc123'; - $this->mockGetLocalCommitHash($localMachineHelper, $this->projectDir, $commitHash); - $this->mockComposerInstall($localMachineHelper, $artifactDir); - $this->mockReadComposerJson($localMachineHelper, $artifactDir); - $localMachineHelper->checkRequiredBinariesExist(['git'])->shouldBeCalled(); - - if ($clone) { - $this->mockLocalGitConfig($localMachineHelper, $artifactDir); - $this->mockCloneShallow($localMachineHelper, $vcsPath, $vcsUrls[0], $artifactDir); +class PushArtifactCommandTest extends PullCommandTestBase +{ + protected function createCommand(): CommandBase + { + return $this->injectCommand(PushArtifactCommand::class); } - if ($commit) { - $this->mockGitAddCommit($localMachineHelper, $artifactDir, $commitHash); + + public function testNoAuthenticationRequired(): void + { + $help = $this->command->getHelp(); + $this->assertStringNotContainsString('This command requires authentication', $help); + } + + public function testPushArtifact(): void + { + $applications = $this->mockRequest('getApplications'); + $this->mockRequest('getApplicationByUuid', $applications[0]->uuid); + $environments = $this->mockRequest('getApplicationEnvironments', $applications[0]->uuid); + $localMachineHelper = $this->mockLocalMachineHelper(); + $this->setUpPushArtifact($localMachineHelper, $environments[0]->vcs->path, [$environments[0]->vcs->url]); + $inputs = [ + // Would you like Acquia CLI to search for a Cloud application that matches your local git config? + 'n', + // Select a Cloud Platform application: + 0, + // Would you like to link the project at ... ? + 'y', + // Choose an Acquia environment: + 0, + ]; + $this->executeCommand([], $inputs); + + $output = $this->getDisplay(); + + $this->assertStringContainsString('Select a Cloud Platform application:', $output); + $this->assertStringContainsString('[0] Sample application 1', $output); + $this->assertStringContainsString('Choose a Cloud Platform environment', $output); + $this->assertStringContainsString('[0] Dev, dev (vcs: master)', $output); + $this->assertStringNotContainsString('Production, prod', $output); + $this->assertStringContainsString('Acquia CLI will:', $output); + $this->assertStringContainsString('- git clone master from site@svn-3.hosted.acquia-sites.com:site.git', $output); + $this->assertStringContainsString('- Compile the contents of vfs://root/project into an artifact', $output); + $this->assertStringContainsString('- Copy the artifact files into the checked out copy of master', $output); + $this->assertStringContainsString('- Commit changes and push the master branch', $output); + $this->assertStringContainsString('Removing', $output); + $this->assertStringContainsString('Initializing Git', $output); + $this->assertStringContainsString('Global .gitignore file', $output); + $this->assertStringContainsString('Removing vendor', $output); + $this->assertStringContainsString('Mirroring source', $output); + $this->assertStringContainsString('Installing Composer', $output); + $this->assertStringContainsString('Finding Drupal', $output); + $this->assertStringContainsString('Removing sensitive', $output); + $this->assertStringContainsString('Adding and committing', $output); + $this->assertStringContainsString('Pushing changes to Acquia Git (site@svn-3.hosted.acquia-sites.com:site.git)', $output); + } + + public function testPushTagArtifact(): void + { + $applications = $this->mockRequest('getApplications'); + $this->mockRequest('getApplicationByUuid', $applications[0]->uuid); + $environments = $this->mockRequest('getApplicationEnvironments', $applications[0]->uuid); + $localMachineHelper = $this->mockLocalMachineHelper(); + $gitTag = '1.2.0-build'; + $this->setUpPushArtifact($localMachineHelper, '1.2.0', [$environments[0]->vcs->url], $gitTag); + $artifactDir = Path::join(sys_get_temp_dir(), 'acli-push-artifact'); + $this->mockGitTag($localMachineHelper, $gitTag, $artifactDir); + $inputs = [ + // Would you like Acquia CLI to search for a Cloud application that matches your local git config? + 'n', + // Select a Cloud Platform application: + 0, + // Would you like to link the project at ... ? + 'n', + ]; + $this->executeCommand([ + '--destination-git-tag' => $gitTag, + '--source-git-tag' => '1.2.0', + ], $inputs); + + $output = $this->getDisplay(); + + $this->assertStringContainsString('Select a Cloud Platform application:', $output); + $this->assertStringContainsString('[0] Sample application 1', $output); + $this->assertStringContainsString('Pushing changes to Acquia Git (site@svn-3.hosted.acquia-sites.com:site.git)', $output); + $this->assertStringContainsString('Commit changes and push the 1.2.0-build tag', $output); + } + + public function testPushArtifactWithAcquiaCliFile(): void + { + $applications = $this->mockRequest('getApplications'); + $this->mockRequest('getApplicationByUuid', $applications[0]->uuid); + $this->mockRequest('getApplicationEnvironments', $applications[0]->uuid); + $this->datastoreAcli->set('push.artifact.destination-git-urls', [ + 'https://github.com/example1/cli.git', + 'https://github.com/example2/cli.git', + ]); + $localMachineHelper = $this->mockLocalMachineHelper(); + $this->setUpPushArtifact($localMachineHelper, 'master', $this->datastoreAcli->get('push.artifact.destination-git-urls')); + $this->executeCommand([ + '--destination-git-branch' => 'master', + ]); + + $output = $this->getDisplay(); + + $this->assertStringContainsString('Pushing changes to Acquia Git (https://github.com/example1/cli.git)', $output); + $this->assertStringContainsString('Pushing changes to Acquia Git (https://github.com/example2/cli.git)', $output); + } + + public function testPushArtifactWithArgs(): void + { + $applications = $this->mockRequest('getApplications'); + $this->mockRequest('getApplicationByUuid', $applications[0]->uuid); + $this->mockRequest('getApplicationEnvironments', $applications[0]->uuid); + $destinationGitUrls = [ + 'https://github.com/example1/cli.git', + 'https://github.com/example2/cli.git', + ]; + $localMachineHelper = $this->mockLocalMachineHelper(); + $this->setUpPushArtifact($localMachineHelper, 'master', $destinationGitUrls); + $this->executeCommand([ + '--destination-git-branch' => 'master', + '--destination-git-urls' => $destinationGitUrls, + ]); + + $output = $this->getDisplay(); + + $this->assertStringContainsString('Pushing changes to Acquia Git (https://github.com/example1/cli.git)', $output); + $this->assertStringContainsString('Pushing changes to Acquia Git (https://github.com/example2/cli.git)', $output); + } + + public function testPushArtifactNoPush(): void + { + $applications = $this->mockRequest('getApplications'); + $this->mockRequest('getApplicationByUuid', $applications[0]->uuid); + $environments = $this->mockRequest('getApplicationEnvironments', $applications[0]->uuid); + $localMachineHelper = $this->mockLocalMachineHelper(); + $this->setUpPushArtifact($localMachineHelper, $environments[0]->vcs->path, [$environments[0]->vcs->url], 'master:master', true, true, false); + $inputs = [ + // Would you like Acquia CLI to search for a Cloud application that matches your local git config? + 'n', + // Select a Cloud Platform application: + 0, + // Would you like to link the project at ... ? + 'y', + // Choose an Acquia environment: + 0, + ]; + $this->executeCommand(['--no-push' => true], $inputs); + + $output = $this->getDisplay(); + + $this->assertStringContainsString('Initializing Git', $output); + $this->assertStringContainsString('Adding and committing changed files', $output); + $this->assertStringNotContainsString('Pushing changes to Acquia Git (site@svn-3.hosted.acquia-sites.com:site.git)', $output); + } + + public function testPushArtifactNoCommit(): void + { + $applications = $this->mockRequest('getApplications'); + $this->mockRequest('getApplicationByUuid', $applications[0]->uuid); + $environments = $this->mockRequest('getApplicationEnvironments', $applications[0]->uuid); + $localMachineHelper = $this->mockLocalMachineHelper(); + $this->setUpPushArtifact($localMachineHelper, $environments[0]->vcs->path, [$environments[0]->vcs->url], 'master:master', true, false, false); + $inputs = [ + // Would you like Acquia CLI to search for a Cloud application that matches your local git config? + 'n', + // Select a Cloud Platform application: + 0, + // Would you like to link the project at ... ? + 'y', + // Choose an Acquia environment: + 0, + ]; + $this->executeCommand(['--no-commit' => true], $inputs); + + $output = $this->getDisplay(); + + $this->assertStringContainsString('Initializing Git', $output); + $this->assertStringNotContainsString('Adding and committing changed files', $output); + $this->assertStringNotContainsString('Pushing changes to Acquia Git (site@svn-3.hosted.acquia-sites.com:site.git)', $output); + } + + public function testPushArtifactNoClone(): void + { + $localMachineHelper = $this->mockLocalMachineHelper(); + $this->setUpPushArtifact($localMachineHelper, 'nothing', [], 'something', false, false, false); + $inputs = [ + // Would you like Acquia CLI to search for a Cloud application that matches your local git config? + 'n', + // Select a Cloud Platform application: + 0, + // Would you like to link the project at ... ? + 'y', + // Choose an Acquia environment: + 0, + ]; + $this->executeCommand(['--no-clone' => true], $inputs); + + $output = $this->getDisplay(); + + $this->assertStringNotContainsString('Initializing Git', $output); + $this->assertStringNotContainsString('Adding and committing changed files', $output); + $this->assertStringNotContainsString('Pushing changes to Acquia Git (site@svn-3.hosted.acquia-sites.com:site.git)', $output); + } + + protected function setUpPushArtifact(ObjectProphecy $localMachineHelper, string $vcsPath, array $vcsUrls, string $destGitRef = 'master:master', bool $clone = true, bool $commit = true, bool $push = true): void + { + touch(Path::join($this->projectDir, 'composer.json')); + mkdir(Path::join($this->projectDir, 'docroot')); + $artifactDir = Path::join(sys_get_temp_dir(), 'acli-push-artifact'); + $this->createMockGitConfigFile(); + $finder = $this->mockFinder(); + $localMachineHelper->getFinder()->willReturn($finder->reveal()); + $fs = $this->prophet->prophesize(Filesystem::class); + $localMachineHelper->getFilesystem()->willReturn($fs)->shouldBeCalled(); + + $this->mockExecuteGitStatus(false, $localMachineHelper, $this->projectDir); + $commitHash = 'abc123'; + $this->mockGetLocalCommitHash($localMachineHelper, $this->projectDir, $commitHash); + $this->mockComposerInstall($localMachineHelper, $artifactDir); + $this->mockReadComposerJson($localMachineHelper, $artifactDir); + $localMachineHelper->checkRequiredBinariesExist(['git'])->shouldBeCalled(); + + if ($clone) { + $this->mockLocalGitConfig($localMachineHelper, $artifactDir); + $this->mockCloneShallow($localMachineHelper, $vcsPath, $vcsUrls[0], $artifactDir); + } + if ($commit) { + $this->mockGitAddCommit($localMachineHelper, $artifactDir, $commitHash); + } + if ($push) { + $this->mockGitPush($vcsUrls, $localMachineHelper, $artifactDir, $destGitRef); + } + } + + protected function mockCloneShallow(ObjectProphecy $localMachineHelper, string $vcsPath, string $vcsUrl, mixed $artifactDir): void + { + $process = $this->prophet->prophesize(Process::class); + $process->isSuccessful()->willReturn(true)->shouldBeCalled(); + $localMachineHelper->execute(['git', 'clone', '--depth=1', $vcsUrl, $artifactDir], Argument::type('callable'), null, true) + ->willReturn($process->reveal())->shouldBeCalled(); + $localMachineHelper->execute(['git', 'fetch', '--depth=1', $vcsUrl, $vcsPath . ':' . $vcsPath], Argument::type('callable'), Argument::type('string'), true) + ->willReturn($process->reveal())->shouldBeCalled(); + $localMachineHelper->execute(['git', 'checkout', $vcsPath], Argument::type('callable'), Argument::type('string'), true) + ->willReturn($process->reveal())->shouldBeCalled(); } - if ($push) { - $this->mockGitPush($vcsUrls, $localMachineHelper, $artifactDir, $destGitRef); + + protected function mockLocalGitConfig(ObjectProphecy $localMachineHelper, string $artifactDir): void + { + $process = $this->prophet->prophesize(Process::class); + $localMachineHelper->execute(['git', 'config', '--local', 'core.excludesFile', 'false'], Argument::type('callable'), $artifactDir, true) + ->willReturn($process->reveal())->shouldBeCalled(); + $localMachineHelper->execute(['git', 'config', '--local', 'core.fileMode', 'true'], Argument::type('callable'), $artifactDir, true) + ->willReturn($process->reveal())->shouldBeCalled(); + } + + protected function mockComposerInstall(ObjectProphecy $localMachineHelper, mixed $artifactDir): void + { + $localMachineHelper->checkRequiredBinariesExist(['composer'])->shouldBeCalled(); + $process = $this->prophet->prophesize(Process::class); + $process->isSuccessful()->willReturn(true); + $localMachineHelper->execute(['composer', 'install', '--no-dev', '--no-interaction', '--optimize-autoloader'], Argument::type('callable'), $artifactDir, true) + ->willReturn($process->reveal())->shouldBeCalled(); + } + + protected function mockGitAddCommit(ObjectProphecy $localMachineHelper, mixed $artifactDir, mixed $commitHash): void + { + $process = $this->mockProcess(); + $localMachineHelper->execute(['git', 'add', '-A'], Argument::type('callable'), $artifactDir, true) + ->willReturn($process->reveal())->shouldBeCalled(); + $localMachineHelper->execute(['git', 'add', '-f', 'docroot/index.php'], null, $artifactDir, false) + ->willReturn($process->reveal())->shouldBeCalled(); + $localMachineHelper->execute(['git', 'add', '-f', 'docroot/autoload.php'], null, $artifactDir, false) + ->willReturn($process->reveal())->shouldBeCalled(); + $localMachineHelper->execute(['git', 'add', '-f', 'docroot/core'], null, $artifactDir, false) + ->willReturn($process->reveal())->shouldBeCalled(); + $localMachineHelper->execute(['git', 'add', '-f', 'vendor'], null, $artifactDir, false) + ->willReturn($process->reveal())->shouldBeCalled(); + $localMachineHelper->execute(['git', 'commit', '-m', "Automated commit by Acquia CLI (source commit: $commitHash)"], Argument::type('callable'), $artifactDir, true) + ->willReturn($process->reveal())->shouldBeCalled(); } - } - - protected function mockCloneShallow(ObjectProphecy $localMachineHelper, string $vcsPath, string $vcsUrl, mixed $artifactDir): void { - $process = $this->prophet->prophesize(Process::class); - $process->isSuccessful()->willReturn(TRUE)->shouldBeCalled(); - $localMachineHelper->execute(['git', 'clone', '--depth=1', $vcsUrl, $artifactDir], Argument::type('callable'), NULL, TRUE) - ->willReturn($process->reveal())->shouldBeCalled(); - $localMachineHelper->execute(['git', 'fetch', '--depth=1', $vcsUrl, $vcsPath . ':' . $vcsPath], Argument::type('callable'), Argument::type('string'), TRUE) - ->willReturn($process->reveal())->shouldBeCalled(); - $localMachineHelper->execute(['git', 'checkout', $vcsPath], Argument::type('callable'), Argument::type('string'), TRUE) - ->willReturn($process->reveal())->shouldBeCalled(); - } - - protected function mockLocalGitConfig(ObjectProphecy $localMachineHelper, string $artifactDir): void { - $process = $this->prophet->prophesize(Process::class); - $localMachineHelper->execute(['git', 'config', '--local', 'core.excludesFile', 'false'], Argument::type('callable'), $artifactDir, TRUE) - ->willReturn($process->reveal())->shouldBeCalled(); - $localMachineHelper->execute(['git', 'config', '--local', 'core.fileMode', 'true'], Argument::type('callable'), $artifactDir, TRUE) - ->willReturn($process->reveal())->shouldBeCalled(); - } - - protected function mockComposerInstall(ObjectProphecy $localMachineHelper, mixed $artifactDir): void { - $localMachineHelper->checkRequiredBinariesExist(['composer'])->shouldBeCalled(); - $process = $this->prophet->prophesize(Process::class); - $process->isSuccessful()->willReturn(TRUE); - $localMachineHelper->execute(['composer', 'install', '--no-dev', '--no-interaction', '--optimize-autoloader'], Argument::type('callable'), $artifactDir, TRUE) - ->willReturn($process->reveal())->shouldBeCalled(); - } - - protected function mockGitAddCommit(ObjectProphecy $localMachineHelper, mixed $artifactDir, mixed $commitHash): void { - $process = $this->mockProcess(); - $localMachineHelper->execute(['git', 'add', '-A'], Argument::type('callable'), $artifactDir, TRUE) - ->willReturn($process->reveal())->shouldBeCalled(); - $localMachineHelper->execute(['git', 'add', '-f', 'docroot/index.php'], NULL, $artifactDir, FALSE) - ->willReturn($process->reveal())->shouldBeCalled(); - $localMachineHelper->execute(['git', 'add', '-f', 'docroot/autoload.php'], NULL, $artifactDir, FALSE) - ->willReturn($process->reveal())->shouldBeCalled(); - $localMachineHelper->execute(['git', 'add', '-f', 'docroot/core'], NULL, $artifactDir, FALSE) - ->willReturn($process->reveal())->shouldBeCalled(); - $localMachineHelper->execute(['git', 'add', '-f', 'vendor'], NULL, $artifactDir, FALSE) - ->willReturn($process->reveal())->shouldBeCalled(); - $localMachineHelper->execute(['git', 'commit', '-m', "Automated commit by Acquia CLI (source commit: $commitHash)"], Argument::type('callable'), $artifactDir, TRUE) - ->willReturn($process->reveal())->shouldBeCalled(); - } - - protected function mockReadComposerJson(ObjectProphecy $localMachineHelper, string $artifactDir): void { - $composerJson = json_encode([ - 'extra' => [ + + protected function mockReadComposerJson(ObjectProphecy $localMachineHelper, string $artifactDir): void + { + $composerJson = json_encode([ + 'extra' => [ 'drupal-scaffold' => [ - 'file-mapping' => [ - '[web-root]/index.php' => [], - ], + 'file-mapping' => [ + '[web-root]/index.php' => [], + ], ], 'installer-paths' => [ - 'docroot/core' => [], + 'docroot/core' => [], ], -], - ]); - $localMachineHelper->readFile(Path::join($this->projectDir, 'composer.json')) - ->willReturn($composerJson); - $localMachineHelper->readFile(Path::join($artifactDir, 'docroot', 'core', 'composer.json')) - ->willReturn($composerJson); - } - - protected function mockGitPush(array $gitUrls, ObjectProphecy $localMachineHelper, string $artifactDir, string $destGitRef): void { - $process = $this->mockProcess(); - foreach ($gitUrls as $gitUrl) { - $localMachineHelper->execute(['git', 'push', $gitUrl, $destGitRef], Argument::type('callable'), $artifactDir, TRUE) - ->willReturn($process->reveal())->shouldBeCalled(); + ], + ]); + $localMachineHelper->readFile(Path::join($this->projectDir, 'composer.json')) + ->willReturn($composerJson); + $localMachineHelper->readFile(Path::join($artifactDir, 'docroot', 'core', 'composer.json')) + ->willReturn($composerJson); + } + + protected function mockGitPush(array $gitUrls, ObjectProphecy $localMachineHelper, string $artifactDir, string $destGitRef): void + { + $process = $this->mockProcess(); + foreach ($gitUrls as $gitUrl) { + $localMachineHelper->execute(['git', 'push', $gitUrl, $destGitRef], Argument::type('callable'), $artifactDir, true) + ->willReturn($process->reveal())->shouldBeCalled(); + } } - } - - protected function mockGitTag(ObjectProphecy $localMachineHelper, string $gitTag, string $artifactDir): void { - $process = $this->mockProcess(); - $localMachineHelper->execute([ - 'git', - 'tag', - $gitTag, - ], Argument::type('callable'), $artifactDir, TRUE) - ->willReturn($process->reveal())->shouldBeCalled(); - } + protected function mockGitTag(ObjectProphecy $localMachineHelper, string $gitTag, string $artifactDir): void + { + $process = $this->mockProcess(); + $localMachineHelper->execute([ + 'git', + 'tag', + $gitTag, + ], Argument::type('callable'), $artifactDir, true) + ->willReturn($process->reveal())->shouldBeCalled(); + } } diff --git a/tests/phpunit/src/Commands/Push/PushCodeCommandTest.php b/tests/phpunit/src/Commands/Push/PushCodeCommandTest.php index cc65e14cb..8489116f0 100644 --- a/tests/phpunit/src/Commands/Push/PushCodeCommandTest.php +++ b/tests/phpunit/src/Commands/Push/PushCodeCommandTest.php @@ -1,6 +1,6 @@ injectCommand(PushCodeCommand::class); + } - protected function createCommand(): CommandBase { - return $this->injectCommand(PushCodeCommand::class); - } + public function testPushCode(): void + { + $this->executeCommand(); - public function testPushCode(): void { - $this->executeCommand(); - - $output = $this->getDisplay(); - - $this->assertStringContainsString('Use git to push code changes upstream.', $output); - } + $output = $this->getDisplay(); + $this->assertStringContainsString('Use git to push code changes upstream.', $output); + } } diff --git a/tests/phpunit/src/Commands/Push/PushDatabaseCommandTest.php b/tests/phpunit/src/Commands/Push/PushDatabaseCommandTest.php index 03568a58c..605dbbf70 100644 --- a/tests/phpunit/src/Commands/Push/PushDatabaseCommandTest.php +++ b/tests/phpunit/src/Commands/Push/PushDatabaseCommandTest.php @@ -1,6 +1,6 @@ injectCommand(PushDatabaseCommand::class); - } - - public function setUp(): void { - self::unsetEnvVars(['ACLI_DB_HOST', 'ACLI_DB_USER', 'ACLI_DB_PASSWORD', 'ACLI_DB_NAME']); - parent::setUp(); - } - - /** - * @dataProvider providerTestPushDatabase - */ - public function testPushDatabase(int $verbosity, bool $printOutput): void { - $applications = $this->mockRequest('getApplications'); - $application = $this->mockRequest('getApplicationByUuid', $applications[self::$INPUT_DEFAULT_CHOICE]->uuid); - $tamper = function ($responses): void { - foreach ($responses as $response) { - $response->ssh_url = 'profserv2.01dev@profserv201dev.ssh.enterprise-g1.acquia-sites.com'; - $response->domains = ["profserv201dev.enterprise-g1.acquia-sites.com"]; - } - }; - $environments = $this->mockRequest('getApplicationEnvironments', $application->uuid, NULL, NULL, $tamper); - $this->createMockGitConfigFile(); - $this->mockAcsfDatabasesResponse($environments[self::$INPUT_DEFAULT_CHOICE]); - $process = $this->mockProcess(); - - $localMachineHelper = $this->mockLocalMachineHelper(); - $localMachineHelper->checkRequiredBinariesExist(['ssh'])->shouldBeCalled(); - $this->mockGetAcsfSitesLMH($localMachineHelper); - - // Database. - $this->mockExecutePvExists($localMachineHelper); - $this->mockCreateMySqlDumpOnLocal($localMachineHelper, $printOutput); - $this->mockUploadDatabaseDump($localMachineHelper, $process, $printOutput); - $this->mockImportDatabaseDumpOnRemote($localMachineHelper, $process, $printOutput); - - $this->command->sshHelper = new SshHelper($this->output, $localMachineHelper->reveal(), $this->logger); - - $inputs = [ - // Would you like Acquia CLI to search for a Cloud application that matches your local git config? - 'n', - // Select a Cloud Platform application: - 0, - // Would you like to link the project at ... ? - 'n', - // Choose a Cloud Platform environment. - 0, - // Choose a database. - 0, - // Overwrite the profserv2 database on dev with a copy of the database from the current machine? - 'y', - ]; - - $this->executeCommand([], $inputs, $verbosity); - - $output = $this->getDisplay(); - - $this->assertStringContainsString('Select a Cloud Platform application:', $output); - $this->assertStringContainsString('[0] Sample application 1', $output); - $this->assertStringContainsString('Choose a Cloud Platform environment', $output); - $this->assertStringContainsString('[0] Dev, dev (vcs: master)', $output); - $this->assertStringContainsString('Choose a database', $output); - $this->assertStringContainsString('jxr5000596dev (oracletest1.dev-profserv2.acsitefactory.com)', $output); - $this->assertStringContainsString('profserv2 (default)', $output); - $this->assertStringContainsString('Overwrite the jxr136 database on dev with a copy of the database from the current machine?', $output); - } - - protected function mockUploadDatabaseDump( - ObjectProphecy $localMachineHelper, - ObjectProphecy $process, - bool $printOutput = TRUE, - ): void { - $localMachineHelper->checkRequiredBinariesExist(['rsync'])->shouldBeCalled(); - $command = [ - 'rsync', - '-tDvPhe', - 'ssh -o StrictHostKeyChecking=no', - sys_get_temp_dir() . '/acli-mysql-dump-drupal.sql.gz', - 'profserv2.01dev@profserv201dev.ssh.enterprise-g1.acquia-sites.com:/mnt/tmp/profserv2.01dev/acli-mysql-dump-drupal.sql.gz', - ]; - $localMachineHelper->execute($command, Argument::type('callable'), NULL, $printOutput, NULL) - ->willReturn($process->reveal()) - ->shouldBeCalled(); - } - - protected function mockExecuteMySqlImport( - ObjectProphecy $localMachineHelper, - ObjectProphecy $process - ): void { - // MySQL import command. - $localMachineHelper - ->executeFromCmd(Argument::type('string'), Argument::type('callable'), - NULL, TRUE, NULL) - ->willReturn($process->reveal()) - ->shouldBeCalled(); - } - - protected function mockGetAcsfSitesLMH(ObjectProphecy $localMachineHelper): void { - $acsfMultisiteFetchProcess = $this->mockProcess(); - $multisiteConfig = file_get_contents(Path::join($this->realFixtureDir, '/multisite-config.json')); - $acsfMultisiteFetchProcess->getOutput()->willReturn($multisiteConfig)->shouldBeCalled(); - $cmd = [ - 0 => 'ssh', - 1 => 'profserv2.01dev@profserv201dev.ssh.enterprise-g1.acquia-sites.com', - 2 => '-t', - 3 => '-o StrictHostKeyChecking=no', - 4 => '-o AddressFamily inet', - 5 => '-o LogLevel=ERROR', - 6 => 'cat', - 7 => '/var/www/site-php/profserv2.01dev/multisite-config.json', - ]; - $localMachineHelper->execute($cmd, Argument::type('callable'), NULL, FALSE, NULL)->willReturn($acsfMultisiteFetchProcess->reveal())->shouldBeCalled(); - } - - private function mockImportDatabaseDumpOnRemote(ObjectProphecy|LocalMachineHelper $localMachineHelper, Process|ObjectProphecy $process, bool $printOutput = TRUE): void { - $cmd = [ - 0 => 'ssh', - 1 => 'profserv2.01dev@profserv201dev.ssh.enterprise-g1.acquia-sites.com', - 2 => '-t', - 3 => '-o StrictHostKeyChecking=no', - 4 => '-o AddressFamily inet', - 5 => '-o LogLevel=ERROR', - 6 => 'pv /mnt/tmp/profserv2.01dev/acli-mysql-dump-drupal.sql.gz --bytes --rate | gunzip | MYSQL_PWD=password mysql --host=fsdb-74.enterprise-g1.hosting.acquia.com.enterprise-g1.hosting.acquia.com --user=s164 profserv2db14390', - ]; - $localMachineHelper->execute($cmd, Argument::type('callable'), NULL, $printOutput, NULL) - ->willReturn($process->reveal()) - ->shouldBeCalled(); - } - +class PushDatabaseCommandTest extends CommandTestBase +{ + /** + * @return mixed[] + */ + public function providerTestPushDatabase(): array + { + return [ + [OutputInterface::VERBOSITY_NORMAL, false], + [OutputInterface::VERBOSITY_VERY_VERBOSE, true], + ]; + } + + protected function createCommand(): CommandBase + { + + return $this->injectCommand(PushDatabaseCommand::class); + } + + public function setUp(): void + { + self::unsetEnvVars(['ACLI_DB_HOST', 'ACLI_DB_USER', 'ACLI_DB_PASSWORD', 'ACLI_DB_NAME']); + parent::setUp(); + } + + /** + * @dataProvider providerTestPushDatabase + */ + public function testPushDatabase(int $verbosity, bool $printOutput): void + { + $applications = $this->mockRequest('getApplications'); + $application = $this->mockRequest('getApplicationByUuid', $applications[self::$INPUT_DEFAULT_CHOICE]->uuid); + $tamper = function ($responses): void { + foreach ($responses as $response) { + $response->ssh_url = 'profserv2.01dev@profserv201dev.ssh.enterprise-g1.acquia-sites.com'; + $response->domains = ["profserv201dev.enterprise-g1.acquia-sites.com"]; + } + }; + $environments = $this->mockRequest('getApplicationEnvironments', $application->uuid, null, null, $tamper); + $this->createMockGitConfigFile(); + $this->mockAcsfDatabasesResponse($environments[self::$INPUT_DEFAULT_CHOICE]); + $process = $this->mockProcess(); + + $localMachineHelper = $this->mockLocalMachineHelper(); + $localMachineHelper->checkRequiredBinariesExist(['ssh'])->shouldBeCalled(); + $this->mockGetAcsfSitesLMH($localMachineHelper); + + // Database. + $this->mockExecutePvExists($localMachineHelper); + $this->mockCreateMySqlDumpOnLocal($localMachineHelper, $printOutput); + $this->mockUploadDatabaseDump($localMachineHelper, $process, $printOutput); + $this->mockImportDatabaseDumpOnRemote($localMachineHelper, $process, $printOutput); + + $this->command->sshHelper = new SshHelper($this->output, $localMachineHelper->reveal(), $this->logger); + + $inputs = [ + // Would you like Acquia CLI to search for a Cloud application that matches your local git config? + 'n', + // Select a Cloud Platform application: + 0, + // Would you like to link the project at ... ? + 'n', + // Choose a Cloud Platform environment. + 0, + // Choose a database. + 0, + // Overwrite the profserv2 database on dev with a copy of the database from the current machine? + 'y', + ]; + + $this->executeCommand([], $inputs, $verbosity); + + $output = $this->getDisplay(); + + $this->assertStringContainsString('Select a Cloud Platform application:', $output); + $this->assertStringContainsString('[0] Sample application 1', $output); + $this->assertStringContainsString('Choose a Cloud Platform environment', $output); + $this->assertStringContainsString('[0] Dev, dev (vcs: master)', $output); + $this->assertStringContainsString('Choose a database', $output); + $this->assertStringContainsString('jxr5000596dev (oracletest1.dev-profserv2.acsitefactory.com)', $output); + $this->assertStringContainsString('profserv2 (default)', $output); + $this->assertStringContainsString('Overwrite the jxr136 database on dev with a copy of the database from the current machine?', $output); + } + + protected function mockUploadDatabaseDump( + ObjectProphecy $localMachineHelper, + ObjectProphecy $process, + bool $printOutput = true, + ): void { + $localMachineHelper->checkRequiredBinariesExist(['rsync'])->shouldBeCalled(); + $command = [ + 'rsync', + '-tDvPhe', + 'ssh -o StrictHostKeyChecking=no', + sys_get_temp_dir() . '/acli-mysql-dump-drupal.sql.gz', + 'profserv2.01dev@profserv201dev.ssh.enterprise-g1.acquia-sites.com:/mnt/tmp/profserv2.01dev/acli-mysql-dump-drupal.sql.gz', + ]; + $localMachineHelper->execute($command, Argument::type('callable'), null, $printOutput, null) + ->willReturn($process->reveal()) + ->shouldBeCalled(); + } + + protected function mockExecuteMySqlImport( + ObjectProphecy $localMachineHelper, + ObjectProphecy $process + ): void { + // MySQL import command. + $localMachineHelper + ->executeFromCmd( + Argument::type('string'), + Argument::type('callable'), + null, + true, + null + ) + ->willReturn($process->reveal()) + ->shouldBeCalled(); + } + + protected function mockGetAcsfSitesLMH(ObjectProphecy $localMachineHelper): void + { + $acsfMultisiteFetchProcess = $this->mockProcess(); + $multisiteConfig = file_get_contents(Path::join($this->realFixtureDir, '/multisite-config.json')); + $acsfMultisiteFetchProcess->getOutput()->willReturn($multisiteConfig)->shouldBeCalled(); + $cmd = [ + 0 => 'ssh', + 1 => 'profserv2.01dev@profserv201dev.ssh.enterprise-g1.acquia-sites.com', + 2 => '-t', + 3 => '-o StrictHostKeyChecking=no', + 4 => '-o AddressFamily inet', + 5 => '-o LogLevel=ERROR', + 6 => 'cat', + 7 => '/var/www/site-php/profserv2.01dev/multisite-config.json', + ]; + $localMachineHelper->execute($cmd, Argument::type('callable'), null, false, null)->willReturn($acsfMultisiteFetchProcess->reveal())->shouldBeCalled(); + } + + private function mockImportDatabaseDumpOnRemote(ObjectProphecy|LocalMachineHelper $localMachineHelper, Process|ObjectProphecy $process, bool $printOutput = true): void + { + $cmd = [ + 0 => 'ssh', + 1 => 'profserv2.01dev@profserv201dev.ssh.enterprise-g1.acquia-sites.com', + 2 => '-t', + 3 => '-o StrictHostKeyChecking=no', + 4 => '-o AddressFamily inet', + 5 => '-o LogLevel=ERROR', + 6 => 'pv /mnt/tmp/profserv2.01dev/acli-mysql-dump-drupal.sql.gz --bytes --rate | gunzip | MYSQL_PWD=password mysql --host=fsdb-74.enterprise-g1.hosting.acquia.com.enterprise-g1.hosting.acquia.com --user=s164 profserv2db14390', + ]; + $localMachineHelper->execute($cmd, Argument::type('callable'), null, $printOutput, null) + ->willReturn($process->reveal()) + ->shouldBeCalled(); + } } diff --git a/tests/phpunit/src/Commands/Push/PushFilesCommandTest.php b/tests/phpunit/src/Commands/Push/PushFilesCommandTest.php index 2c5c81ac2..6bb9e5d34 100644 --- a/tests/phpunit/src/Commands/Push/PushFilesCommandTest.php +++ b/tests/phpunit/src/Commands/Push/PushFilesCommandTest.php @@ -1,6 +1,6 @@ injectCommand(PushFilesCommand::class); - } - - public function testPushFilesAcsf(): void { - $applicationsResponse = $this->mockApplicationsRequest(); - $this->mockApplicationRequest(); - $this->mockAcsfEnvironmentsRequest($applicationsResponse); - $sshHelper = $this->mockSshHelper(); - $multisiteConfig = $this->mockGetAcsfSites($sshHelper); - $localMachineHelper = $this->mockLocalMachineHelper(); - $process = $this->mockProcess(); - $this->mockExecuteAcsfRsync($localMachineHelper, $process, reset($multisiteConfig['sites'])['name']); - - $this->command->sshHelper = $sshHelper->reveal(); - - $inputs = [ - // Would you like Acquia CLI to search for a Cloud application that matches your local git config? - 'n', - // Select a Cloud Platform application: - 0, - // Would you like to link the project at ... ? - 'n', - // Choose a Cloud Platform environment. - 0, - // Choose a site. - 0, - // Overwrite the public files directory. - 'y', - ]; - - $this->executeCommand([], $inputs); - - $output = $this->getDisplay(); - - $this->assertStringContainsString('Select a Cloud Platform application:', $output); - $this->assertStringContainsString('[0] Sample application 1', $output); - $this->assertStringContainsString('Choose a Cloud Platform environment', $output); - $this->assertStringContainsString('[0] Dev, dev (vcs: master)', $output); - } - - public function testPushFilesCloud(): void { - $applicationsResponse = $this->mockApplicationsRequest(); - $this->mockApplicationRequest(); - $environmentsResponse = $this->mockEnvironmentsRequest($applicationsResponse); - $selectedEnvironment = $environmentsResponse->_embedded->items[0]; - $sshHelper = $this->mockSshHelper(); - $this->mockGetCloudSites($sshHelper, $selectedEnvironment); - $localMachineHelper = $this->mockLocalMachineHelper(); - $process = $this->mockProcess(); - $this->mockExecuteCloudRsync($localMachineHelper, $process, $selectedEnvironment); - - $this->command->sshHelper = $sshHelper->reveal(); - - $inputs = [ - // Would you like Acquia CLI to search for a Cloud application that matches your local git config? - 'n', - // Select a Cloud Platform application: - 0, - // Would you like to link the project at ... ? - 'n', - // Choose a Cloud Platform environment. - 0, - // Choose a site. - 0, - // Overwrite the public files directory. - 'y', - ]; - - $this->executeCommand([], $inputs); - - $output = $this->getDisplay(); - - $this->assertStringContainsString('Select a Cloud Platform application:', $output); - $this->assertStringContainsString('[0] Sample application 1', $output); - $this->assertStringContainsString('Choose a Cloud Platform environment', $output); - $this->assertStringContainsString('[0] Dev, dev (vcs: master)', $output); - } - - public function testPushFilesNoOverwrite(): void { - $applicationsResponse = $this->mockApplicationsRequest(); - $this->mockApplicationRequest(); - $environmentsResponse = $this->mockEnvironmentsRequest($applicationsResponse); - $selectedEnvironment = $environmentsResponse->_embedded->items[0]; - $sshHelper = $this->mockSshHelper(); - $this->mockGetCloudSites($sshHelper, $selectedEnvironment); - $localMachineHelper = $this->mockLocalMachineHelper(); - - $this->command->sshHelper = $sshHelper->reveal(); - - $inputs = [ - // Would you like Acquia CLI to search for a Cloud application that matches your local git config? - 'n', - // Select a Cloud Platform application: - 0, - // Would you like to link the project at ... ? - 'n', - // Choose a Cloud Platform environment. - 0, - // Choose a site. - 0, - // Overwrite the public files directory. - 'n', - ]; - - $this->executeCommand([], $inputs); - - $output = $this->getDisplay(); - - $this->assertStringContainsString('Select a Cloud Platform application:', $output); - $this->assertStringContainsString('[0] Sample application 1', $output); - $this->assertStringContainsString('Choose a Cloud Platform environment', $output); - $this->assertStringContainsString('[0] Dev, dev (vcs: master)', $output); - $this->assertStringNotContainsString('Pushing public files', $output); - } - - protected function mockExecuteCloudRsync( - ObjectProphecy $localMachineHelper, - ObjectProphecy $process, - mixed $environment - ): void { - $localMachineHelper->checkRequiredBinariesExist(['rsync'])->shouldBeCalled(); - $parts = explode('.', $environment->ssh_url); - $sitegroup = reset($parts); - $command = [ - 'rsync', - '-avPhze', - 'ssh -o StrictHostKeyChecking=no', - $this->projectDir . '/docroot/sites/bar/files/', - $environment->ssh_url . ':/mnt/files/' . $sitegroup . '.' . $environment->name . '/sites/bar/files', - ]; - $localMachineHelper->execute($command, Argument::type('callable'), NULL, TRUE) - ->willReturn($process->reveal()) - ->shouldBeCalled(); - } - - protected function mockExecuteAcsfRsync( - ObjectProphecy $localMachineHelper, - ObjectProphecy $process, - string $site - ): void { - $localMachineHelper->checkRequiredBinariesExist(['rsync'])->shouldBeCalled(); - $command = [ - 'rsync', - '-avPhze', - 'ssh -o StrictHostKeyChecking=no', - $this->projectDir . '/docroot/sites/' . $site . '/files/', - 'profserv2.01dev@profserv201dev.ssh.enterprise-g1.acquia-sites.com:/mnt/files/profserv2.01dev/sites/g/files/' . $site . '/files', - ]; - $localMachineHelper->execute($command, Argument::type('callable'), NULL, TRUE) - ->willReturn($process->reveal()) - ->shouldBeCalled(); - } - +class PushFilesCommandTest extends CommandTestBase +{ + protected function createCommand(): CommandBase + { + return $this->injectCommand(PushFilesCommand::class); + } + + public function testPushFilesAcsf(): void + { + $applicationsResponse = $this->mockApplicationsRequest(); + $this->mockApplicationRequest(); + $this->mockAcsfEnvironmentsRequest($applicationsResponse); + $sshHelper = $this->mockSshHelper(); + $multisiteConfig = $this->mockGetAcsfSites($sshHelper); + $localMachineHelper = $this->mockLocalMachineHelper(); + $process = $this->mockProcess(); + $this->mockExecuteAcsfRsync($localMachineHelper, $process, reset($multisiteConfig['sites'])['name']); + + $this->command->sshHelper = $sshHelper->reveal(); + + $inputs = [ + // Would you like Acquia CLI to search for a Cloud application that matches your local git config? + 'n', + // Select a Cloud Platform application: + 0, + // Would you like to link the project at ... ? + 'n', + // Choose a Cloud Platform environment. + 0, + // Choose a site. + 0, + // Overwrite the public files directory. + 'y', + ]; + + $this->executeCommand([], $inputs); + + $output = $this->getDisplay(); + + $this->assertStringContainsString('Select a Cloud Platform application:', $output); + $this->assertStringContainsString('[0] Sample application 1', $output); + $this->assertStringContainsString('Choose a Cloud Platform environment', $output); + $this->assertStringContainsString('[0] Dev, dev (vcs: master)', $output); + } + + public function testPushFilesCloud(): void + { + $applicationsResponse = $this->mockApplicationsRequest(); + $this->mockApplicationRequest(); + $environmentsResponse = $this->mockEnvironmentsRequest($applicationsResponse); + $selectedEnvironment = $environmentsResponse->_embedded->items[0]; + $sshHelper = $this->mockSshHelper(); + $this->mockGetCloudSites($sshHelper, $selectedEnvironment); + $localMachineHelper = $this->mockLocalMachineHelper(); + $process = $this->mockProcess(); + $this->mockExecuteCloudRsync($localMachineHelper, $process, $selectedEnvironment); + + $this->command->sshHelper = $sshHelper->reveal(); + + $inputs = [ + // Would you like Acquia CLI to search for a Cloud application that matches your local git config? + 'n', + // Select a Cloud Platform application: + 0, + // Would you like to link the project at ... ? + 'n', + // Choose a Cloud Platform environment. + 0, + // Choose a site. + 0, + // Overwrite the public files directory. + 'y', + ]; + + $this->executeCommand([], $inputs); + + $output = $this->getDisplay(); + + $this->assertStringContainsString('Select a Cloud Platform application:', $output); + $this->assertStringContainsString('[0] Sample application 1', $output); + $this->assertStringContainsString('Choose a Cloud Platform environment', $output); + $this->assertStringContainsString('[0] Dev, dev (vcs: master)', $output); + } + + public function testPushFilesNoOverwrite(): void + { + $applicationsResponse = $this->mockApplicationsRequest(); + $this->mockApplicationRequest(); + $environmentsResponse = $this->mockEnvironmentsRequest($applicationsResponse); + $selectedEnvironment = $environmentsResponse->_embedded->items[0]; + $sshHelper = $this->mockSshHelper(); + $this->mockGetCloudSites($sshHelper, $selectedEnvironment); + $localMachineHelper = $this->mockLocalMachineHelper(); + + $this->command->sshHelper = $sshHelper->reveal(); + + $inputs = [ + // Would you like Acquia CLI to search for a Cloud application that matches your local git config? + 'n', + // Select a Cloud Platform application: + 0, + // Would you like to link the project at ... ? + 'n', + // Choose a Cloud Platform environment. + 0, + // Choose a site. + 0, + // Overwrite the public files directory. + 'n', + ]; + + $this->executeCommand([], $inputs); + + $output = $this->getDisplay(); + + $this->assertStringContainsString('Select a Cloud Platform application:', $output); + $this->assertStringContainsString('[0] Sample application 1', $output); + $this->assertStringContainsString('Choose a Cloud Platform environment', $output); + $this->assertStringContainsString('[0] Dev, dev (vcs: master)', $output); + $this->assertStringNotContainsString('Pushing public files', $output); + } + + protected function mockExecuteCloudRsync( + ObjectProphecy $localMachineHelper, + ObjectProphecy $process, + mixed $environment + ): void { + $localMachineHelper->checkRequiredBinariesExist(['rsync'])->shouldBeCalled(); + $parts = explode('.', $environment->ssh_url); + $sitegroup = reset($parts); + $command = [ + 'rsync', + '-avPhze', + 'ssh -o StrictHostKeyChecking=no', + $this->projectDir . '/docroot/sites/bar/files/', + $environment->ssh_url . ':/mnt/files/' . $sitegroup . '.' . $environment->name . '/sites/bar/files', + ]; + $localMachineHelper->execute($command, Argument::type('callable'), null, true) + ->willReturn($process->reveal()) + ->shouldBeCalled(); + } + + protected function mockExecuteAcsfRsync( + ObjectProphecy $localMachineHelper, + ObjectProphecy $process, + string $site + ): void { + $localMachineHelper->checkRequiredBinariesExist(['rsync'])->shouldBeCalled(); + $command = [ + 'rsync', + '-avPhze', + 'ssh -o StrictHostKeyChecking=no', + $this->projectDir . '/docroot/sites/' . $site . '/files/', + 'profserv2.01dev@profserv201dev.ssh.enterprise-g1.acquia-sites.com:/mnt/files/profserv2.01dev/sites/g/files/' . $site . '/files', + ]; + $localMachineHelper->execute($command, Argument::type('callable'), null, true) + ->willReturn($process->reveal()) + ->shouldBeCalled(); + } } diff --git a/tests/phpunit/src/Commands/Remote/AliasesDownloadCommandTest.php b/tests/phpunit/src/Commands/Remote/AliasesDownloadCommandTest.php index 78eb58c9f..62c479b2f 100644 --- a/tests/phpunit/src/Commands/Remote/AliasesDownloadCommandTest.php +++ b/tests/phpunit/src/Commands/Remote/AliasesDownloadCommandTest.php @@ -1,6 +1,6 @@ injectCommand(AliasesDownloadCommand::class); - } - - public function setUp(): void { - parent::setUp(); - $this->setupFsFixture(); - $this->command = $this->createCommand(); - } - - /** - * Test all Drush alias versions. - * - * @return array> - */ - public function providerTestRemoteAliasesDownloadCommand(): array { - return [ - [['9'], []], - [['9'], ['--destination-dir' => 'testdir'], 'testdir'], - [['9'], ['--all' => TRUE], NULL, TRUE], - [['8'], []], - ]; - } - - /** - * @param string|null $destinationDir - * @param bool $all Download aliases for all applications. - * @dataProvider providerTestRemoteAliasesDownloadCommand - */ - public function testRemoteAliasesDownloadCommand(array $inputs, array $args, string $destinationDir = NULL, bool $all = FALSE): void { - $aliasVersion = $inputs[0]; - - $drushAliasesFixture = Path::canonicalize(__DIR__ . '/../../../../fixtures/drush-aliases'); - $drushAliasesTarballFixtureFilepath = tempnam(sys_get_temp_dir(), 'AcquiaDrushAliases'); - $archiveFixture = new PharData($drushAliasesTarballFixtureFilepath . '.tar'); - $archiveFixture->buildFromDirectory($drushAliasesFixture); - $archiveFixture->compress(Phar::GZ); - - $stream = Utils::streamFor(file_get_contents($drushAliasesTarballFixtureFilepath . '.tar.gz')); - $this->clientProphecy->addQuery('version', $aliasVersion); - $this->clientProphecy->stream('get', '/account/drush-aliases/download')->willReturn($stream); - $drushArchiveFilepath = $this->command->getDrushArchiveTempFilepath(); - - $destinationDir = $destinationDir ?? Path::join($this->acliRepoRoot, 'drush'); - if ($aliasVersion === '8') { - $homeDir = $this->getTempDir(); - putenv('HOME=' . $homeDir); - $destinationDir = Path::join($homeDir, '.drush'); +class AliasesDownloadCommandTest extends CommandTestBase +{ + protected function createCommand(): CommandBase + { + return $this->injectCommand(AliasesDownloadCommand::class); } - if ($aliasVersion === '9' && !$all) { - $applicationsResponse = $this->getMockResponseFromSpec('/applications', 'get', '200'); - $cloudApplication = $applicationsResponse->{'_embedded'}->items[0]; - $cloudApplicationUuid = $cloudApplication->uuid; - $this->createMockAcliConfigFile($cloudApplicationUuid); - $this->mockApplicationRequest(); + + public function setUp(): void + { + parent::setUp(); + $this->setupFsFixture(); + $this->command = $this->createCommand(); } - $this->executeCommand($args, $inputs); - - // Assert. - $output = $this->getDisplay(); - - $this->assertFileDoesNotExist($drushArchiveFilepath); - $this->assertFileExists($destinationDir); - $this->assertStringContainsString('Cloud Platform Drush aliases installed into ' . $destinationDir, $output); - } - - /** - * @requires OS linux|darwin - */ - public function testRemoteAliasesDownloadFailed(): void { - $drushAliasesFixture = Path::canonicalize(__DIR__ . '/../../../../fixtures/drush-aliases'); - $drushAliasesTarballFixtureFilepath = tempnam(sys_get_temp_dir(), 'AcquiaDrushAliases'); - $archiveFixture = new PharData($drushAliasesTarballFixtureFilepath . '.tar'); - $archiveFixture->buildFromDirectory($drushAliasesFixture); - $archiveFixture->compress(Phar::GZ); - - $stream = Utils::streamFor(file_get_contents($drushAliasesTarballFixtureFilepath . '.tar.gz')); - $this->clientProphecy->addQuery('version', '9'); - $this->clientProphecy->stream('get', '/account/drush-aliases/download')->willReturn($stream); - - $destinationDir = Path::join($this->acliRepoRoot, 'drush'); - $sitesDir = Path::join($destinationDir, 'sites'); - mkdir($sitesDir, 0777, TRUE); - chmod($sitesDir, 000); - $this->expectException(AcquiaCliException::class); - $this->expectExceptionMessage("Could not extract aliases to $destinationDir"); - $this->executeCommand(['--all' => TRUE, '--destination-dir' => $destinationDir], ['9']); - } + /** + * Test all Drush alias versions. + * + * @return array> + */ + public function providerTestRemoteAliasesDownloadCommand(): array + { + return [ + [['9'], []], + [['9'], ['--destination-dir' => 'testdir'], 'testdir'], + [['9'], ['--all' => true], null, true], + [['8'], []], + ]; + } + /** + * @param string|null $destinationDir + * @param bool $all Download aliases for all applications. + * @dataProvider providerTestRemoteAliasesDownloadCommand + */ + public function testRemoteAliasesDownloadCommand(array $inputs, array $args, string $destinationDir = null, bool $all = false): void + { + $aliasVersion = $inputs[0]; + + $drushAliasesFixture = Path::canonicalize(__DIR__ . '/../../../../fixtures/drush-aliases'); + $drushAliasesTarballFixtureFilepath = tempnam(sys_get_temp_dir(), 'AcquiaDrushAliases'); + $archiveFixture = new PharData($drushAliasesTarballFixtureFilepath . '.tar'); + $archiveFixture->buildFromDirectory($drushAliasesFixture); + $archiveFixture->compress(Phar::GZ); + + $stream = Utils::streamFor(file_get_contents($drushAliasesTarballFixtureFilepath . '.tar.gz')); + $this->clientProphecy->addQuery('version', $aliasVersion); + $this->clientProphecy->stream('get', '/account/drush-aliases/download')->willReturn($stream); + $drushArchiveFilepath = $this->command->getDrushArchiveTempFilepath(); + + $destinationDir = $destinationDir ?? Path::join($this->acliRepoRoot, 'drush'); + if ($aliasVersion === '8') { + $homeDir = $this->getTempDir(); + putenv('HOME=' . $homeDir); + $destinationDir = Path::join($homeDir, '.drush'); + } + if ($aliasVersion === '9' && !$all) { + $applicationsResponse = $this->getMockResponseFromSpec('/applications', 'get', '200'); + $cloudApplication = $applicationsResponse->{'_embedded'}->items[0]; + $cloudApplicationUuid = $cloudApplication->uuid; + $this->createMockAcliConfigFile($cloudApplicationUuid); + $this->mockApplicationRequest(); + } + + $this->executeCommand($args, $inputs); + + // Assert. + $output = $this->getDisplay(); + + $this->assertFileDoesNotExist($drushArchiveFilepath); + $this->assertFileExists($destinationDir); + $this->assertStringContainsString('Cloud Platform Drush aliases installed into ' . $destinationDir, $output); + } + + /** + * @requires OS linux|darwin + */ + public function testRemoteAliasesDownloadFailed(): void + { + $drushAliasesFixture = Path::canonicalize(__DIR__ . '/../../../../fixtures/drush-aliases'); + $drushAliasesTarballFixtureFilepath = tempnam(sys_get_temp_dir(), 'AcquiaDrushAliases'); + $archiveFixture = new PharData($drushAliasesTarballFixtureFilepath . '.tar'); + $archiveFixture->buildFromDirectory($drushAliasesFixture); + $archiveFixture->compress(Phar::GZ); + + $stream = Utils::streamFor(file_get_contents($drushAliasesTarballFixtureFilepath . '.tar.gz')); + $this->clientProphecy->addQuery('version', '9'); + $this->clientProphecy->stream('get', '/account/drush-aliases/download')->willReturn($stream); + + $destinationDir = Path::join($this->acliRepoRoot, 'drush'); + $sitesDir = Path::join($destinationDir, 'sites'); + mkdir($sitesDir, 0777, true); + chmod($sitesDir, 000); + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage("Could not extract aliases to $destinationDir"); + $this->executeCommand(['--all' => true, '--destination-dir' => $destinationDir], ['9']); + } } diff --git a/tests/phpunit/src/Commands/Remote/AliasesListCommandTest.php b/tests/phpunit/src/Commands/Remote/AliasesListCommandTest.php index 1bffca17a..41fd60534 100644 --- a/tests/phpunit/src/Commands/Remote/AliasesListCommandTest.php +++ b/tests/phpunit/src/Commands/Remote/AliasesListCommandTest.php @@ -1,6 +1,6 @@ injectCommand(AliasListCommand::class); - } - - public function testRemoteAliasesListCommand(): void { - $applicationsResponse = $this->mockApplicationsRequest(); - $this->mockApplicationRequest(); - $this->mockEnvironmentsRequest($applicationsResponse); - - $inputs = [ - // Would you like Acquia CLI to search for a Cloud application that matches your local git config? - 'n', - // Select a Cloud Platform application: - '0', - // Would you like to link the project at ... - 'n', - ]; - $this->executeCommand([], $inputs); - - // Assert. - $output = $this->getDisplay(); - - $this->assertStringContainsString('| Sample application 1 | devcloud2.dev | 24-a47ac10b-58cc-4372-a567-0e02b2c3d470 |', $output); - } - +class AliasesListCommandTest extends CommandTestBase +{ + protected function createCommand(): CommandBase + { + return $this->injectCommand(AliasListCommand::class); + } + + public function testRemoteAliasesListCommand(): void + { + $applicationsResponse = $this->mockApplicationsRequest(); + $this->mockApplicationRequest(); + $this->mockEnvironmentsRequest($applicationsResponse); + + $inputs = [ + // Would you like Acquia CLI to search for a Cloud application that matches your local git config? + 'n', + // Select a Cloud Platform application: + '0', + // Would you like to link the project at ... + 'n', + ]; + $this->executeCommand([], $inputs); + + // Assert. + $output = $this->getDisplay(); + + $this->assertStringContainsString('| Sample application 1 | devcloud2.dev | 24-a47ac10b-58cc-4372-a567-0e02b2c3d470 |', $output); + } } diff --git a/tests/phpunit/src/Commands/Remote/DrushCommandTest.php b/tests/phpunit/src/Commands/Remote/DrushCommandTest.php index 2376421e3..8eeef3751 100644 --- a/tests/phpunit/src/Commands/Remote/DrushCommandTest.php +++ b/tests/phpunit/src/Commands/Remote/DrushCommandTest.php @@ -1,6 +1,6 @@ injectCommand(DrushCommand::class); + } - protected function createCommand(): CommandBase { - return $this->injectCommand(DrushCommand::class); - } - - /** - * @return array>> - */ - public function providerTestRemoteDrushCommand(): array { - return [ - [ + /** + * @return array>> + */ + public function providerTestRemoteDrushCommand(): array + { + return [ + [ [ - '-vvv' => '', - 'drush_command' => 'status --fields=db-status', + '-vvv' => '', + 'drush_command' => 'status --fields=db-status', + ], ], - ], - [ [ - '-vvv' => '', - 'drush_command' => 'status --fields=db-status', + [ + '-vvv' => '', + 'drush_command' => 'status --fields=db-status', ], - ], - ]; - } - - /** - * @dataProvider providerTestRemoteDrushCommand - * @group serial - */ - public function testRemoteDrushCommand(array $args): void { - $this->mockGetEnvironment(); - [$process, $localMachineHelper] = $this->mockForExecuteCommand(); - $localMachineHelper->checkRequiredBinariesExist(['ssh'])->shouldBeCalled(); - $sshCommand = [ - 'ssh', - 'site.dev@sitedev.ssh.hosted.acquia-sites.com', - '-t', - '-o StrictHostKeyChecking=no', - '-o AddressFamily inet', - '-o LogLevel=ERROR', - 'cd /var/www/html/site.dev/docroot; ', - 'drush', - '--uri=http://sitedev.hosted.acquia-sites.com status --fields=db-status', - ]; - $localMachineHelper - ->execute($sshCommand, Argument::type('callable'), NULL, TRUE, NULL) - ->willReturn($process->reveal()) - ->shouldBeCalled(); + ], + ]; + } - $this->command->sshHelper = new SshHelper($this->output, $localMachineHelper->reveal(), $this->logger); - $this->executeCommand($args, self::inputChooseEnvironment()); + /** + * @dataProvider providerTestRemoteDrushCommand + * @group serial + */ + public function testRemoteDrushCommand(array $args): void + { + $this->mockGetEnvironment(); + [$process, $localMachineHelper] = $this->mockForExecuteCommand(); + $localMachineHelper->checkRequiredBinariesExist(['ssh'])->shouldBeCalled(); + $sshCommand = [ + 'ssh', + 'site.dev@sitedev.ssh.hosted.acquia-sites.com', + '-t', + '-o StrictHostKeyChecking=no', + '-o AddressFamily inet', + '-o LogLevel=ERROR', + 'cd /var/www/html/site.dev/docroot; ', + 'drush', + '--uri=http://sitedev.hosted.acquia-sites.com status --fields=db-status', + ]; + $localMachineHelper + ->execute($sshCommand, Argument::type('callable'), null, true, null) + ->willReturn($process->reveal()) + ->shouldBeCalled(); - // Assert. - $this->getDisplay(); - } + $this->command->sshHelper = new SshHelper($this->output, $localMachineHelper->reveal(), $this->logger); + $this->executeCommand($args, self::inputChooseEnvironment()); + // Assert. + $this->getDisplay(); + } } diff --git a/tests/phpunit/src/Commands/Remote/SshCommandTest.php b/tests/phpunit/src/Commands/Remote/SshCommandTest.php index 57a37c46a..6d21e03ec 100644 --- a/tests/phpunit/src/Commands/Remote/SshCommandTest.php +++ b/tests/phpunit/src/Commands/Remote/SshCommandTest.php @@ -1,6 +1,6 @@ injectCommand(SshCommand::class); - } - - /** - * @group serial - */ - public function testRemoteAliasesDownloadCommand(): void { - ClearCacheCommand::clearCaches(); - $this->mockForGetEnvironmentFromAliasArg(); - [$process, $localMachineHelper] = $this->mockForExecuteCommand(); - $localMachineHelper->checkRequiredBinariesExist(['ssh'])->shouldBeCalled(); - $sshCommand = [ - 'ssh', - 'site.dev@sitedev.ssh.hosted.acquia-sites.com', - '-t', - '-o StrictHostKeyChecking=no', - '-o AddressFamily inet', - '-o LogLevel=ERROR', - 'cd /var/www/html/devcloud2.dev; exec $SHELL -l', - ]; - $localMachineHelper - ->execute($sshCommand, Argument::type('callable'), NULL, TRUE, NULL) - ->willReturn($process->reveal()) - ->shouldBeCalled(); - - $this->command->sshHelper = new SshHelper($this->output, $localMachineHelper->reveal(), $this->logger); - - $args = [ - 'alias' => 'devcloud2.dev', - ]; - $this->executeCommand($args); - - // Assert. - $output = $this->getDisplay(); - } - +class SshCommandTest extends SshCommandTestBase +{ + protected function createCommand(): CommandBase + { + return $this->injectCommand(SshCommand::class); + } + + /** + * @group serial + */ + public function testRemoteAliasesDownloadCommand(): void + { + ClearCacheCommand::clearCaches(); + $this->mockForGetEnvironmentFromAliasArg(); + [$process, $localMachineHelper] = $this->mockForExecuteCommand(); + $localMachineHelper->checkRequiredBinariesExist(['ssh'])->shouldBeCalled(); + $sshCommand = [ + 'ssh', + 'site.dev@sitedev.ssh.hosted.acquia-sites.com', + '-t', + '-o StrictHostKeyChecking=no', + '-o AddressFamily inet', + '-o LogLevel=ERROR', + 'cd /var/www/html/devcloud2.dev; exec $SHELL -l', + ]; + $localMachineHelper + ->execute($sshCommand, Argument::type('callable'), null, true, null) + ->willReturn($process->reveal()) + ->shouldBeCalled(); + + $this->command->sshHelper = new SshHelper($this->output, $localMachineHelper->reveal(), $this->logger); + + $args = [ + 'alias' => 'devcloud2.dev', + ]; + $this->executeCommand($args); + + // Assert. + $output = $this->getDisplay(); + } } diff --git a/tests/phpunit/src/Commands/Remote/SshCommandTestBase.php b/tests/phpunit/src/Commands/Remote/SshCommandTestBase.php index 0ba1b816a..a9e075baa 100644 --- a/tests/phpunit/src/Commands/Remote/SshCommandTestBase.php +++ b/tests/phpunit/src/Commands/Remote/SshCommandTestBase.php @@ -1,6 +1,6 @@ mockApplicationsRequest(1); - $this->mockEnvironmentsRequest($applicationsResponse); - $this->clientProphecy->addQuery('filter', 'hosting=@*:devcloud2')->shouldBeCalled(); - $this->mockRequest('getAccount'); - } - - /** - * @return array - */ - protected function mockForExecuteCommand(): array { - $process = $this->prophet->prophesize(Process::class); - $process->isSuccessful()->willReturn(TRUE); - $process->getExitCode()->willReturn(0); - $localMachineHelper = $this->prophet->prophesize(LocalMachineHelper::class); - $localMachineHelper->useTty()->willReturn(FALSE)->shouldBeCalled(); - return [$process, $localMachineHelper]; - } +abstract class SshCommandTestBase extends CommandTestBase +{ + protected function mockForGetEnvironmentFromAliasArg(): void + { + $applicationsResponse = $this->mockApplicationsRequest(1); + $this->mockEnvironmentsRequest($applicationsResponse); + $this->clientProphecy->addQuery('filter', 'hosting=@*:devcloud2')->shouldBeCalled(); + $this->mockRequest('getAccount'); + } + /** + * @return array + */ + protected function mockForExecuteCommand(): array + { + $process = $this->prophet->prophesize(Process::class); + $process->isSuccessful()->willReturn(true); + $process->getExitCode()->willReturn(0); + $localMachineHelper = $this->prophet->prophesize(LocalMachineHelper::class); + $localMachineHelper->useTty()->willReturn(false)->shouldBeCalled(); + return [$process, $localMachineHelper]; + } } diff --git a/tests/phpunit/src/Commands/Self/MakeDocsCommandTest.php b/tests/phpunit/src/Commands/Self/MakeDocsCommandTest.php index 803637b0d..6ae7a534b 100644 --- a/tests/phpunit/src/Commands/Self/MakeDocsCommandTest.php +++ b/tests/phpunit/src/Commands/Self/MakeDocsCommandTest.php @@ -1,6 +1,6 @@ injectCommand(MakeDocsCommand::class); - } - - public function testMakeDocsCommand(): void { - $this->executeCommand(); - $output = $this->getDisplay(); - $this->assertStringContainsString('Console Tool', $output); - $this->assertStringContainsString('============', $output); - $this->assertStringContainsString('- `help`_', $output); - } +class MakeDocsCommandTest extends CommandTestBase +{ + protected function createCommand(): CommandBase + { + return $this->injectCommand(MakeDocsCommand::class); + } + public function testMakeDocsCommand(): void + { + $this->executeCommand(); + $output = $this->getDisplay(); + $this->assertStringContainsString('Console Tool', $output); + $this->assertStringContainsString('============', $output); + $this->assertStringContainsString('- `help`_', $output); + } } diff --git a/tests/phpunit/src/Commands/Ssh/SshKeyCreateCommandTest.php b/tests/phpunit/src/Commands/Ssh/SshKeyCreateCommandTest.php index 23d69be25..8c856d7a4 100644 --- a/tests/phpunit/src/Commands/Ssh/SshKeyCreateCommandTest.php +++ b/tests/phpunit/src/Commands/Ssh/SshKeyCreateCommandTest.php @@ -1,6 +1,6 @@ injectCommand(SshKeyCreateCommand::class); + } - protected function createCommand(): CommandBase { - return $this->injectCommand(SshKeyCreateCommand::class); - } - - /** - * @return array - */ - public function providerTestCreate(): array { - return [ - [ - TRUE, + /** + * @return array + */ + public function providerTestCreate(): array + { + return [ + [ + true, // Args. [ - '--filename' => $this->filename, - '--password' => 'acli123', + '--filename' => $this->filename, + '--password' => 'acli123', ], // Inputs. [], - ], - [ - TRUE, + ], + [ + true, // Args. [], // Inputs. [ - // Enter a filename for your new local SSH key: - $this->filename, - // Enter a password for your SSH key: - 'acli123', + // Enter a filename for your new local SSH key: + $this->filename, + // Enter a password for your SSH key: + 'acli123', ], - ], - [ - FALSE, + ], + [ + false, // Args. [], // Inputs. [ - // Enter a filename for your new local SSH key: - $this->filename, - // Enter a password for your SSH key: - 'acli123', + // Enter a filename for your new local SSH key: + $this->filename, + // Enter a password for your SSH key: + 'acli123', ], - ], - ]; - } - - /** - * @dataProvider providerTestCreate - * @group brokenProphecy - */ - public function testCreate(mixed $sshAddSuccess, mixed $args, mixed $inputs): void { - $sshKeyFilepath = Path::join($this->sshDir, '/' . $this->filename); - $this->fs->remove($sshKeyFilepath); - $localMachineHelper = $this->mockLocalMachineHelper(); - $localMachineHelper->getLocalFilepath('~/.passphrase')->willReturn('~/.passphrase'); - $fileSystem = $this->prophet->prophesize(Filesystem::class); - $this->mockAddSshKeyToAgent($localMachineHelper, $fileSystem); - $this->mockSshAgentList($localMachineHelper, $sshAddSuccess); - $this->mockGenerateSshKey($localMachineHelper); + ], + ]; + } - $localMachineHelper->getFilesystem()->willReturn($fileSystem->reveal())->shouldBeCalled(); + /** + * @dataProvider providerTestCreate + * @group brokenProphecy + */ + public function testCreate(mixed $sshAddSuccess, mixed $args, mixed $inputs): void + { + $sshKeyFilepath = Path::join($this->sshDir, '/' . $this->filename); + $this->fs->remove($sshKeyFilepath); + $localMachineHelper = $this->mockLocalMachineHelper(); + $localMachineHelper->getLocalFilepath('~/.passphrase')->willReturn('~/.passphrase'); + $fileSystem = $this->prophet->prophesize(Filesystem::class); + $this->mockAddSshKeyToAgent($localMachineHelper, $fileSystem); + $this->mockSshAgentList($localMachineHelper, $sshAddSuccess); + $this->mockGenerateSshKey($localMachineHelper); - $this->executeCommand($args, $inputs); - } + $localMachineHelper->getFilesystem()->willReturn($fileSystem->reveal())->shouldBeCalled(); + $this->executeCommand($args, $inputs); + } } diff --git a/tests/phpunit/src/Commands/Ssh/SshKeyCreateUploadCommandTest.php b/tests/phpunit/src/Commands/Ssh/SshKeyCreateUploadCommandTest.php index 566307ef8..8c11bf4c8 100644 --- a/tests/phpunit/src/Commands/Ssh/SshKeyCreateUploadCommandTest.php +++ b/tests/phpunit/src/Commands/Ssh/SshKeyCreateUploadCommandTest.php @@ -1,6 +1,6 @@ getCommandTester(); + $this->application->addCommands([ + $this->injectCommand(SshKeyCreateCommand::class), + $this->injectCommand(SshKeyUploadCommand::class), + ]); + } - $this->getCommandTester(); - $this->application->addCommands([ - $this->injectCommand(SshKeyCreateCommand::class), - $this->injectCommand(SshKeyUploadCommand::class), - ]); - } + protected function createCommand(): CommandBase + { + return $this->injectCommand(SshKeyCreateUploadCommand::class); + } - protected function createCommand(): CommandBase { - return $this->injectCommand(SshKeyCreateUploadCommand::class); - } + public function testCreateUpload(): void + { + $mockRequestArgs = $this->getMockRequestBodyFromSpec('/account/ssh-keys'); - public function testCreateUpload(): void { - $mockRequestArgs = $this->getMockRequestBodyFromSpec('/account/ssh-keys'); + $sshKeyFilename = 'id_rsa'; + $localMachineHelper = $this->mockLocalMachineHelper(); + $localMachineHelper->getLocalFilepath('~/.passphrase')->willReturn('~/.passphrase'); + $fileSystem = $this->prophet->prophesize(Filesystem::class); + $this->mockAddSshKeyToAgent($localMachineHelper, $fileSystem); + $this->mockSshAgentList($localMachineHelper); + $this->mockGenerateSshKey($localMachineHelper, $mockRequestArgs['public_key']); - $sshKeyFilename = 'id_rsa'; - $localMachineHelper = $this->mockLocalMachineHelper(); - $localMachineHelper->getLocalFilepath('~/.passphrase')->willReturn('~/.passphrase'); - $fileSystem = $this->prophet->prophesize(Filesystem::class); - $this->mockAddSshKeyToAgent($localMachineHelper, $fileSystem); - $this->mockSshAgentList($localMachineHelper); - $this->mockGenerateSshKey($localMachineHelper, $mockRequestArgs['public_key']); - - $body = [ - 'json' => [ + $body = [ + 'json' => [ 'label' => $mockRequestArgs['label'], 'public_key' => $mockRequestArgs['public_key'], - ], - ]; - $this->mockRequest('postAccountSshKeys', NULL, $body); - $this->mockGetLocalSshKey($localMachineHelper, $fileSystem, $mockRequestArgs['public_key']); - - $localMachineHelper->getFilesystem()->willReturn($fileSystem->reveal())->shouldBeCalled(); + ], + ]; + $this->mockRequest('postAccountSshKeys', null, $body); + $this->mockGetLocalSshKey($localMachineHelper, $fileSystem, $mockRequestArgs['public_key']); - /** @var SshKeyCreateCommand $sshKeyCreateCommand */ - $sshKeyCreateCommand = $this->application->find(SshKeyCreateCommand::getDefaultName()); - $sshKeyCreateCommand->localMachineHelper = $this->command->localMachineHelper; - /** @var SshKeyUploadCommand $sshKeyUploadCommand */ - $sshKeyUploadCommand = $this->application->find(SshKeyUploadCommand::getDefaultName()); - $sshKeyUploadCommand->localMachineHelper = $this->command->localMachineHelper; + $localMachineHelper->getFilesystem()->willReturn($fileSystem->reveal())->shouldBeCalled(); - $inputs = [ - // Enter a filename for your new local SSH key: - $sshKeyFilename, - // Enter a password for your SSH key: - 'acli123', - // Label. - $mockRequestArgs['label'], - ]; - $this->executeCommand(['--no-wait' => ''], $inputs); + /** @var SshKeyCreateCommand $sshKeyCreateCommand */ + $sshKeyCreateCommand = $this->application->find(SshKeyCreateCommand::getDefaultName()); + $sshKeyCreateCommand->localMachineHelper = $this->command->localMachineHelper; + /** @var SshKeyUploadCommand $sshKeyUploadCommand */ + $sshKeyUploadCommand = $this->application->find(SshKeyUploadCommand::getDefaultName()); + $sshKeyUploadCommand->localMachineHelper = $this->command->localMachineHelper; - $output = $this->getDisplay(); - $this->assertStringContainsString('Enter the filename of the SSH key (option --filename) [id_rsa_acquia]:', $output); - $this->assertStringContainsString('Enter the password for the SSH key (option --password) (input will be hidden):', $output); - $this->assertStringContainsString('Enter the SSH key label to be used with the Cloud Platform (option --label):', $output); - } + $inputs = [ + // Enter a filename for your new local SSH key: + $sshKeyFilename, + // Enter a password for your SSH key: + 'acli123', + // Label. + $mockRequestArgs['label'], + ]; + $this->executeCommand(['--no-wait' => ''], $inputs); + $output = $this->getDisplay(); + $this->assertStringContainsString('Enter the filename of the SSH key (option --filename) [id_rsa_acquia]:', $output); + $this->assertStringContainsString('Enter the password for the SSH key (option --password) (input will be hidden):', $output); + $this->assertStringContainsString('Enter the SSH key label to be used with the Cloud Platform (option --label):', $output); + } } diff --git a/tests/phpunit/src/Commands/Ssh/SshKeyDeleteCommandTest.php b/tests/phpunit/src/Commands/Ssh/SshKeyDeleteCommandTest.php index 39e7645e9..7df5b9621 100644 --- a/tests/phpunit/src/Commands/Ssh/SshKeyDeleteCommandTest.php +++ b/tests/phpunit/src/Commands/Ssh/SshKeyDeleteCommandTest.php @@ -1,6 +1,6 @@ injectCommand(SshKeyDeleteCommand::class); - } - - public function testDelete(): void { - $sshKeyListResponse = $this->mockListSshKeysRequest(); - $this->mockRequest('deleteAccountSshKey', $sshKeyListResponse[self::$INPUT_DEFAULT_CHOICE]->uuid, NULL, 'Removed key'); - - $inputs = [ - // Choose key. - self::$INPUT_DEFAULT_CHOICE, - // Do you also want to delete the corresponding local key files? - 'n', - ]; - $this->executeCommand([], $inputs); - - // Assert. - $output = $this->getDisplay(); - $this->assertStringContainsString('Choose an SSH key to delete from the Cloud Platform', $output); - $this->assertStringContainsString($sshKeyListResponse[self::$INPUT_DEFAULT_CHOICE]->label, $output); - } - +class SshKeyDeleteCommandTest extends CommandTestBase +{ + protected function createCommand(): CommandBase + { + return $this->injectCommand(SshKeyDeleteCommand::class); + } + + public function testDelete(): void + { + $sshKeyListResponse = $this->mockListSshKeysRequest(); + $this->mockRequest('deleteAccountSshKey', $sshKeyListResponse[self::$INPUT_DEFAULT_CHOICE]->uuid, null, 'Removed key'); + + $inputs = [ + // Choose key. + self::$INPUT_DEFAULT_CHOICE, + // Do you also want to delete the corresponding local key files? + 'n', + ]; + $this->executeCommand([], $inputs); + + // Assert. + $output = $this->getDisplay(); + $this->assertStringContainsString('Choose an SSH key to delete from the Cloud Platform', $output); + $this->assertStringContainsString($sshKeyListResponse[self::$INPUT_DEFAULT_CHOICE]->label, $output); + } } diff --git a/tests/phpunit/src/Commands/Ssh/SshKeyInfoCommandTest.php b/tests/phpunit/src/Commands/Ssh/SshKeyInfoCommandTest.php index 75f871742..5f8fcf7eb 100644 --- a/tests/phpunit/src/Commands/Ssh/SshKeyInfoCommandTest.php +++ b/tests/phpunit/src/Commands/Ssh/SshKeyInfoCommandTest.php @@ -1,6 +1,6 @@ injectCommand(SshKeyInfoCommand::class); - } - - public function setUp(): void { - parent::setUp(); - $this->setupFsFixture(); - $this->command = $this->createCommand(); - } - - public function testInfo(): void { - $this->mockListSshKeysRequest(); - - $inputs = [ - // Choose key. - '0', - ]; - $this->executeCommand([], $inputs); - - // Assert. - $output = $this->getDisplay(); - $this->assertStringContainsString('Choose an SSH key to view', $output); - $this->assertStringContainsString('SSH key property SSH key value', $output); - $this->assertStringContainsString('UUID 02905393-65d7-4bef-873b-24593f73d273', $output); - $this->assertStringContainsString('Label PC Home', $output); - $this->assertStringContainsString('Fingerprint (md5) 5d:23:fb:45:70:df:ef:ad:ca:bf:81:93:cd:50:26:28', $output); - $this->assertStringContainsString('Created at 2017-05-09T20:30:35.000Z', $output); - } - +class SshKeyInfoCommandTest extends CommandTestBase +{ + protected function createCommand(): CommandBase + { + return $this->injectCommand(SshKeyInfoCommand::class); + } + + public function setUp(): void + { + parent::setUp(); + $this->setupFsFixture(); + $this->command = $this->createCommand(); + } + + public function testInfo(): void + { + $this->mockListSshKeysRequest(); + + $inputs = [ + // Choose key. + '0', + ]; + $this->executeCommand([], $inputs); + + // Assert. + $output = $this->getDisplay(); + $this->assertStringContainsString('Choose an SSH key to view', $output); + $this->assertStringContainsString('SSH key property SSH key value', $output); + $this->assertStringContainsString('UUID 02905393-65d7-4bef-873b-24593f73d273', $output); + $this->assertStringContainsString('Label PC Home', $output); + $this->assertStringContainsString('Fingerprint (md5) 5d:23:fb:45:70:df:ef:ad:ca:bf:81:93:cd:50:26:28', $output); + $this->assertStringContainsString('Created at 2017-05-09T20:30:35.000Z', $output); + } } diff --git a/tests/phpunit/src/Commands/Ssh/SshKeyListCommandTest.php b/tests/phpunit/src/Commands/Ssh/SshKeyListCommandTest.php index d52ba54f0..984857383 100644 --- a/tests/phpunit/src/Commands/Ssh/SshKeyListCommandTest.php +++ b/tests/phpunit/src/Commands/Ssh/SshKeyListCommandTest.php @@ -1,6 +1,6 @@ injectCommand(SshKeyListCommand::class); - } - - public function setUp(): void { - parent::setUp(); - $this->setupFsFixture(); - $this->command = $this->createCommand(); - } - - public function testList(): void { - - $mockBody = $this->getMockResponseFromSpec('/account/ssh-keys', 'get', '200'); - $this->clientProphecy->request('get', '/account/ssh-keys')->willReturn($mockBody->{'_embedded'}->items)->shouldBeCalled(); - $mockRequestArgs = $this->getMockRequestBodyFromSpec('/account/ssh-keys'); - $tempFileName = $this->createLocalSshKey($mockRequestArgs['public_key']); - $baseFilename = basename($tempFileName); - $this->executeCommand(); - - // Assert. - $output = $this->getDisplay(); - $this->assertStringContainsString('Local filename', $output); - $this->assertStringContainsString('Cloud Platform label', $output); - $this->assertStringContainsString('Fingerprint', $output); - $this->assertStringContainsString($baseFilename, $output); - $this->assertStringContainsString($mockBody->_embedded->items[0]->label, $output); - $this->assertStringContainsString($mockBody->_embedded->items[1]->label, $output); - $this->assertStringContainsString($mockBody->_embedded->items[2]->label, $output); - } - +class SshKeyListCommandTest extends CommandTestBase +{ + protected function createCommand(): CommandBase + { + return $this->injectCommand(SshKeyListCommand::class); + } + + public function setUp(): void + { + parent::setUp(); + $this->setupFsFixture(); + $this->command = $this->createCommand(); + } + + public function testList(): void + { + + $mockBody = $this->getMockResponseFromSpec('/account/ssh-keys', 'get', '200'); + $this->clientProphecy->request('get', '/account/ssh-keys')->willReturn($mockBody->{'_embedded'}->items)->shouldBeCalled(); + $mockRequestArgs = $this->getMockRequestBodyFromSpec('/account/ssh-keys'); + $tempFileName = $this->createLocalSshKey($mockRequestArgs['public_key']); + $baseFilename = basename($tempFileName); + $this->executeCommand(); + + // Assert. + $output = $this->getDisplay(); + $this->assertStringContainsString('Local filename', $output); + $this->assertStringContainsString('Cloud Platform label', $output); + $this->assertStringContainsString('Fingerprint', $output); + $this->assertStringContainsString($baseFilename, $output); + $this->assertStringContainsString($mockBody->_embedded->items[0]->label, $output); + $this->assertStringContainsString($mockBody->_embedded->items[1]->label, $output); + $this->assertStringContainsString($mockBody->_embedded->items[2]->label, $output); + } } diff --git a/tests/phpunit/src/Commands/Ssh/SshKeyUploadCommandTest.php b/tests/phpunit/src/Commands/Ssh/SshKeyUploadCommandTest.php index edeb76a61..7c8726054 100644 --- a/tests/phpunit/src/Commands/Ssh/SshKeyUploadCommandTest.php +++ b/tests/phpunit/src/Commands/Ssh/SshKeyUploadCommandTest.php @@ -1,6 +1,6 @@ injectCommand(SshKeyUploadCommand::class); - } +class SshKeyUploadCommandTest extends CommandTestBase +{ + protected function createCommand(): CommandBase + { + return $this->injectCommand(SshKeyUploadCommand::class); + } - /** - * @return array - */ - public function providerTestUpload(): array { - $sshKeysRequestBody = $this->getMockRequestBodyFromSpec('/account/ssh-keys'); - return [ - [ + /** + * @return array + */ + public function providerTestUpload(): array + { + $sshKeysRequestBody = $this->getMockRequestBodyFromSpec('/account/ssh-keys'); + return [ + [ // Args. [], // Inputs. [ - // Choose key. - '0', - // Enter a Cloud Platform label for this SSH key: - $sshKeysRequestBody['label'], - // Would you like to wait until Cloud Platform is ready? (yes/no) - 'y', - // Would you like Acquia CLI to search for a Cloud application that matches your local git config? (yes/no) - 'n', + // Choose key. + '0', + // Enter a Cloud Platform label for this SSH key: + $sshKeysRequestBody['label'], + // Would you like to wait until Cloud Platform is ready? (yes/no) + 'y', + // Would you like Acquia CLI to search for a Cloud application that matches your local git config? (yes/no) + 'n', ], // Perms. - TRUE, - ], - [ + true, + ], + [ // Args. [ - '--filepath' => 'id_rsa.pub', - '--label' => $sshKeysRequestBody['label'], + '--filepath' => 'id_rsa.pub', + '--label' => $sshKeysRequestBody['label'], ], // Inputs. [ - // Would you like to wait until Cloud Platform is ready? (yes/no) - 'y', - // Would you like Acquia CLI to search for a Cloud application that matches your local git config? (yes/no) - 'n', + // Would you like to wait until Cloud Platform is ready? (yes/no) + 'y', + // Would you like Acquia CLI to search for a Cloud application that matches your local git config? (yes/no) + 'n', ], // Perms. - FALSE, - ], - ]; - } - - /** - * @dataProvider providerTestUpload - */ - public function testUpload(array $args, array $inputs, bool $perms): void { - $sshKeysRequestBody = $this->getMockRequestBodyFromSpec('/account/ssh-keys'); - $body = [ - 'json' => [ + false, + ], + ]; + } + + /** + * @dataProvider providerTestUpload + */ + public function testUpload(array $args, array $inputs, bool $perms): void + { + $sshKeysRequestBody = $this->getMockRequestBodyFromSpec('/account/ssh-keys'); + $body = [ + 'json' => [ 'label' => $sshKeysRequestBody['label'], 'public_key' => $sshKeysRequestBody['public_key'], - ], - ]; - $this->mockRequest('postAccountSshKeys', NULL, $body); - $this->mockListSshKeyRequestWithUploadedKey($sshKeysRequestBody); - $applicationsResponse = $this->mockApplicationsRequest(); - $applicationResponse = $this->mockApplicationRequest(); - $this->mockPermissionsRequest($applicationResponse, $perms); - - $localMachineHelper = $this->mockLocalMachineHelper(); - /** @var Filesystem|\Prophecy\Prophecy\ObjectProphecy $fileSystem */ - $fileSystem = $this->prophet->prophesize(Filesystem::class); - $fileName = $this->mockGetLocalSshKey($localMachineHelper, $fileSystem, $sshKeysRequestBody['public_key']); - - $localMachineHelper->getFilesystem()->willReturn($fileSystem); - $fileSystem->exists(Argument::type('string'))->willReturn(TRUE); - $localMachineHelper->getLocalFilepath(Argument::containingString('id_rsa'))->willReturn('id_rsa.pub'); - $localMachineHelper->readFile(Argument::type('string'))->willReturn($sshKeysRequestBody['public_key']); - - if ($perms) { - $environmentsResponse = $this->mockEnvironmentsRequest($applicationsResponse); - $sshHelper = $this->mockPollCloudViaSsh($environmentsResponse->_embedded->items); - $this->command->sshHelper = $sshHelper->reveal(); + ], + ]; + $this->mockRequest('postAccountSshKeys', null, $body); + $this->mockListSshKeyRequestWithUploadedKey($sshKeysRequestBody); + $applicationsResponse = $this->mockApplicationsRequest(); + $applicationResponse = $this->mockApplicationRequest(); + $this->mockPermissionsRequest($applicationResponse, $perms); + + $localMachineHelper = $this->mockLocalMachineHelper(); + /** @var Filesystem|\Prophecy\Prophecy\ObjectProphecy $fileSystem */ + $fileSystem = $this->prophet->prophesize(Filesystem::class); + $fileName = $this->mockGetLocalSshKey($localMachineHelper, $fileSystem, $sshKeysRequestBody['public_key']); + + $localMachineHelper->getFilesystem()->willReturn($fileSystem); + $fileSystem->exists(Argument::type('string'))->willReturn(true); + $localMachineHelper->getLocalFilepath(Argument::containingString('id_rsa'))->willReturn('id_rsa.pub'); + $localMachineHelper->readFile(Argument::type('string'))->willReturn($sshKeysRequestBody['public_key']); + + if ($perms) { + $environmentsResponse = $this->mockEnvironmentsRequest($applicationsResponse); + $sshHelper = $this->mockPollCloudViaSsh($environmentsResponse->_embedded->items); + $this->command->sshHelper = $sshHelper->reveal(); + } + + // Choose a local SSH key to upload to the Cloud Platform. + $this->executeCommand($args, $inputs); + + // Assert. + $output = $this->getDisplay(); + $this->assertStringContainsString("Uploaded $fileName to the Cloud Platform with label " . $sshKeysRequestBody['label'], $output); + $this->assertStringContainsString('Would you like to wait until your key is installed on all of your application\'s servers?', $output); + $this->assertStringContainsString('Your SSH key is ready for use!', $output); } - // Choose a local SSH key to upload to the Cloud Platform. - $this->executeCommand($args, $inputs); - - // Assert. - $output = $this->getDisplay(); - $this->assertStringContainsString("Uploaded $fileName to the Cloud Platform with label " . $sshKeysRequestBody['label'], $output); - $this->assertStringContainsString('Would you like to wait until your key is installed on all of your application\'s servers?', $output); - $this->assertStringContainsString('Your SSH key is ready for use!', $output); - } - - // Ensure permission checks aren't against a Node environment. - public function testUploadNode(): void { - $sshKeysRequestBody = $this->getMockRequestBodyFromSpec('/account/ssh-keys'); - $body = [ - 'json' => [ + // Ensure permission checks aren't against a Node environment. + public function testUploadNode(): void + { + $sshKeysRequestBody = $this->getMockRequestBodyFromSpec('/account/ssh-keys'); + $body = [ + 'json' => [ 'label' => $sshKeysRequestBody['label'], 'public_key' => $sshKeysRequestBody['public_key'], - ], - ]; - $this->mockRequest('postAccountSshKeys', NULL, $body); - $this->mockListSshKeyRequestWithUploadedKey($sshKeysRequestBody); - $applicationsResponse = $this->mockApplicationsRequest(); - $applicationResponse = $this->mockApplicationRequest(); - $this->mockPermissionsRequest($applicationResponse, TRUE); - - $localMachineHelper = $this->mockLocalMachineHelper(); - /** @var Filesystem|\Prophecy\Prophecy\ObjectProphecy $fileSystem */ - $fileSystem = $this->prophet->prophesize(Filesystem::class); - $fileName = $this->mockGetLocalSshKey($localMachineHelper, $fileSystem, $sshKeysRequestBody['public_key']); - - $localMachineHelper->getFilesystem()->willReturn($fileSystem); - $fileSystem->exists(Argument::type('string'))->willReturn(TRUE); - $localMachineHelper->getLocalFilepath(Argument::containingString('id_rsa'))->willReturn('id_rsa.pub'); - $localMachineHelper->readFile(Argument::type('string'))->willReturn($sshKeysRequestBody['public_key']); - - $tamper = function ($responses): void { - foreach ($responses as $response) { - $response->type = 'node'; - } - }; - $environmentsResponse = $this->mockRequest('getApplicationEnvironments', $applicationsResponse->_embedded->items[0]->uuid, NULL, NULL, $tamper); - $sshHelper = $this->mockPollCloudViaSsh($environmentsResponse, FALSE); - $this->command->sshHelper = $sshHelper->reveal(); - - // Choose a local SSH key to upload to the Cloud Platform. - $inputs = [ - // Choose key. - '0', - // Enter a Cloud Platform label for this SSH key: - $sshKeysRequestBody['label'], - // Would you like to wait until Cloud Platform is ready? (yes/no) - 'y', - // Would you like Acquia CLI to search for a Cloud application that matches your local git config? (yes/no) - 'n', - ]; - $this->executeCommand([], $inputs); - - // Assert. - $output = $this->getDisplay(); - $this->assertStringContainsString("Uploaded $fileName to the Cloud Platform with label " . $sshKeysRequestBody['label'], $output); - $this->assertStringContainsString('Would you like to wait until your key is installed on all of your application\'s servers?', $output); - $this->assertStringContainsString('Your SSH key is ready for use!', $output); - } - - public function testInvalidFilepath(): void { - $inputs = [ - // Choose key. - '0', - // Label. - 'Test', - ]; - $filepath = Path::join(sys_get_temp_dir(), 'notarealfile'); - $args = ['--filepath' => $filepath]; - $this->expectException(AcquiaCliException::class); - $this->expectExceptionMessage("The filepath $filepath is not valid"); - $this->executeCommand($args, $inputs); - } + ], + ]; + $this->mockRequest('postAccountSshKeys', null, $body); + $this->mockListSshKeyRequestWithUploadedKey($sshKeysRequestBody); + $applicationsResponse = $this->mockApplicationsRequest(); + $applicationResponse = $this->mockApplicationRequest(); + $this->mockPermissionsRequest($applicationResponse, true); + + $localMachineHelper = $this->mockLocalMachineHelper(); + /** @var Filesystem|\Prophecy\Prophecy\ObjectProphecy $fileSystem */ + $fileSystem = $this->prophet->prophesize(Filesystem::class); + $fileName = $this->mockGetLocalSshKey($localMachineHelper, $fileSystem, $sshKeysRequestBody['public_key']); + + $localMachineHelper->getFilesystem()->willReturn($fileSystem); + $fileSystem->exists(Argument::type('string'))->willReturn(true); + $localMachineHelper->getLocalFilepath(Argument::containingString('id_rsa'))->willReturn('id_rsa.pub'); + $localMachineHelper->readFile(Argument::type('string'))->willReturn($sshKeysRequestBody['public_key']); + + $tamper = function ($responses): void { + foreach ($responses as $response) { + $response->type = 'node'; + } + }; + $environmentsResponse = $this->mockRequest('getApplicationEnvironments', $applicationsResponse->_embedded->items[0]->uuid, null, null, $tamper); + $sshHelper = $this->mockPollCloudViaSsh($environmentsResponse, false); + $this->command->sshHelper = $sshHelper->reveal(); + + // Choose a local SSH key to upload to the Cloud Platform. + $inputs = [ + // Choose key. + '0', + // Enter a Cloud Platform label for this SSH key: + $sshKeysRequestBody['label'], + // Would you like to wait until Cloud Platform is ready? (yes/no) + 'y', + // Would you like Acquia CLI to search for a Cloud application that matches your local git config? (yes/no) + 'n', + ]; + $this->executeCommand([], $inputs); + + // Assert. + $output = $this->getDisplay(); + $this->assertStringContainsString("Uploaded $fileName to the Cloud Platform with label " . $sshKeysRequestBody['label'], $output); + $this->assertStringContainsString('Would you like to wait until your key is installed on all of your application\'s servers?', $output); + $this->assertStringContainsString('Your SSH key is ready for use!', $output); + } + public function testInvalidFilepath(): void + { + $inputs = [ + // Choose key. + '0', + // Label. + 'Test', + ]; + $filepath = Path::join(sys_get_temp_dir(), 'notarealfile'); + $args = ['--filepath' => $filepath]; + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage("The filepath $filepath is not valid"); + $this->executeCommand($args, $inputs); + } } diff --git a/tests/phpunit/src/Commands/TelemetryCommandTest.php b/tests/phpunit/src/Commands/TelemetryCommandTest.php index b7ac1897a..3b94f8fe4 100644 --- a/tests/phpunit/src/Commands/TelemetryCommandTest.php +++ b/tests/phpunit/src/Commands/TelemetryCommandTest.php @@ -1,6 +1,6 @@ legacyAcliConfigFilepath = Path::join($this->dataDir, 'acquia-cli.json'); - $this->fs->remove($this->legacyAcliConfigFilepath); - } - - /**b - */ - protected function createCommand(): CommandBase { - return $this->injectCommand(TelemetryCommand::class); - } - - /** - * @group brokenProphecy - */ - public function testTelemetryCommand(): void { - $this->mockRequest('getAccount'); - $this->executeCommand(); - $output = $this->getDisplay(); - $this->assertStringContainsString('Telemetry has been enabled.', $output); - $this->executeCommand(); - $output = $this->getDisplay(); - $this->assertStringContainsString('Telemetry has been disabled.', $output); - } - - /** - * @return string[][] - */ - public function providerTestTelemetryPrompt(): array { - return [ - [ +class TelemetryCommandTest extends CommandTestBase +{ + protected string $legacyAcliConfigFilepath; + + public function setUp(): void + { + parent::setUp(); + $this->legacyAcliConfigFilepath = Path::join($this->dataDir, 'acquia-cli.json'); + $this->fs->remove($this->legacyAcliConfigFilepath); + } + + /**b + */ + protected function createCommand(): CommandBase + { + return $this->injectCommand(TelemetryCommand::class); + } + + /** + * @group brokenProphecy + */ + public function testTelemetryCommand(): void + { + $this->mockRequest('getAccount'); + $this->executeCommand(); + $output = $this->getDisplay(); + $this->assertStringContainsString('Telemetry has been enabled.', $output); + $this->executeCommand(); + $output = $this->getDisplay(); + $this->assertStringContainsString('Telemetry has been disabled.', $output); + } + + /** + * @return string[][] + */ + public function providerTestTelemetryPrompt(): array + { + return [ + [ // Would you like to share anonymous performance usage and data? ['n'], 'Ok, no data will be collected and shared with us.', - ], - ]; - } - - /** - * Tests telemetry prompt. - * - * @dataProvider providerTestTelemetryPrompt - * @param $message - */ - public function testTelemetryPrompt(array $inputs, mixed $message): void { - $this->cloudConfig = [DataStoreContract::SEND_TELEMETRY => NULL]; - $this->createMockConfigFiles(); - $this->createMockAcliConfigFile('a47ac10b-58cc-4372-a567-0e02b2c3d470'); - $this->createDataStores(); - $this->mockApplicationRequest(); - $this->command = $this->injectCommand(LinkCommand::class); - $this->executeCommand([], $inputs); - $output = $this->getDisplay(); - - $this->assertStringContainsString('Would you like to share anonymous performance usage and data?', $output); - $this->assertStringContainsString($message, $output); - } - - /** - * Opted out by default. - */ - public function testAmplitudeDisabled(): void { - $this->cloudConfig = [DataStoreContract::SEND_TELEMETRY => FALSE]; - $this->createMockConfigFiles(); - $this->executeCommand(); - - $this->assertEquals(0, $this->getStatusCode()); - - } - - public function testMigrateLegacyTelemetryPreference(): void { - $this->cloudConfig = [DataStoreContract::SEND_TELEMETRY => NULL]; - $this->createMockConfigFiles(); - $this->fs->remove($this->legacyAcliConfigFilepath); - $legacyAcliConfig = ['send_telemetry' => FALSE]; - $contents = json_encode($legacyAcliConfig); - $this->fs->dumpFile($this->legacyAcliConfigFilepath, $contents); - $this->executeCommand(); - - $this->assertEquals(0, $this->getStatusCode()); - $this->fs->remove($this->legacyAcliConfigFilepath); - } - + ], + ]; + } + + /** + * Tests telemetry prompt. + * + * @dataProvider providerTestTelemetryPrompt + * @param $message + */ + public function testTelemetryPrompt(array $inputs, mixed $message): void + { + $this->cloudConfig = [DataStoreContract::SEND_TELEMETRY => null]; + $this->createMockConfigFiles(); + $this->createMockAcliConfigFile('a47ac10b-58cc-4372-a567-0e02b2c3d470'); + $this->createDataStores(); + $this->mockApplicationRequest(); + $this->command = $this->injectCommand(LinkCommand::class); + $this->executeCommand([], $inputs); + $output = $this->getDisplay(); + + $this->assertStringContainsString('Would you like to share anonymous performance usage and data?', $output); + $this->assertStringContainsString($message, $output); + } + + /** + * Opted out by default. + */ + public function testAmplitudeDisabled(): void + { + $this->cloudConfig = [DataStoreContract::SEND_TELEMETRY => false]; + $this->createMockConfigFiles(); + $this->executeCommand(); + + $this->assertEquals(0, $this->getStatusCode()); + } + + public function testMigrateLegacyTelemetryPreference(): void + { + $this->cloudConfig = [DataStoreContract::SEND_TELEMETRY => null]; + $this->createMockConfigFiles(); + $this->fs->remove($this->legacyAcliConfigFilepath); + $legacyAcliConfig = ['send_telemetry' => false]; + $contents = json_encode($legacyAcliConfig); + $this->fs->dumpFile($this->legacyAcliConfigFilepath, $contents); + $this->executeCommand(); + + $this->assertEquals(0, $this->getStatusCode()); + $this->fs->remove($this->legacyAcliConfigFilepath); + } } diff --git a/tests/phpunit/src/Commands/TelemetryDisableCommandTest.php b/tests/phpunit/src/Commands/TelemetryDisableCommandTest.php index 25647d72d..69797251a 100644 --- a/tests/phpunit/src/Commands/TelemetryDisableCommandTest.php +++ b/tests/phpunit/src/Commands/TelemetryDisableCommandTest.php @@ -1,6 +1,6 @@ injectCommand(TelemetryDisableCommand::class); - } - - public function testTelemetryDisableCommand(): void { - $this->executeCommand(); - $output = $this->getDisplay(); - $this->assertStringContainsString('Telemetry has been disabled.', $output); - - $settings = json_decode(file_get_contents($this->cloudConfigFilepath), TRUE); - $this->assertFalse($settings['send_telemetry']); - } - +class TelemetryDisableCommandTest extends CommandTestBase +{ + protected function createCommand(): CommandBase + { + return $this->injectCommand(TelemetryDisableCommand::class); + } + + public function testTelemetryDisableCommand(): void + { + $this->executeCommand(); + $output = $this->getDisplay(); + $this->assertStringContainsString('Telemetry has been disabled.', $output); + + $settings = json_decode(file_get_contents($this->cloudConfigFilepath), true); + $this->assertFalse($settings['send_telemetry']); + } } diff --git a/tests/phpunit/src/Commands/TelemetryEnableCommandTest.php b/tests/phpunit/src/Commands/TelemetryEnableCommandTest.php index 5cfc79953..05dcd6cb3 100644 --- a/tests/phpunit/src/Commands/TelemetryEnableCommandTest.php +++ b/tests/phpunit/src/Commands/TelemetryEnableCommandTest.php @@ -1,6 +1,6 @@ injectCommand(TelemetryEnableCommand::class); - } - - public function testTelemetryEnableCommand(): void { - $this->executeCommand(); - $output = $this->getDisplay(); - $this->assertStringContainsString('Telemetry has been enabled.', $output); - - $settings = json_decode(file_get_contents($this->cloudConfigFilepath), TRUE); - $this->assertTrue($settings['send_telemetry']); - } - +class TelemetryEnableCommandTest extends CommandTestBase +{ + /**b + */ + protected function createCommand(): CommandBase + { + return $this->injectCommand(TelemetryEnableCommand::class); + } + + public function testTelemetryEnableCommand(): void + { + $this->executeCommand(); + $output = $this->getDisplay(); + $this->assertStringContainsString('Telemetry has been enabled.', $output); + + $settings = json_decode(file_get_contents($this->cloudConfigFilepath), true); + $this->assertTrue($settings['send_telemetry']); + } } diff --git a/tests/phpunit/src/Commands/UpdateCommandTest.php b/tests/phpunit/src/Commands/UpdateCommandTest.php index 11c4d3da9..5dfad94b4 100644 --- a/tests/phpunit/src/Commands/UpdateCommandTest.php +++ b/tests/phpunit/src/Commands/UpdateCommandTest.php @@ -1,6 +1,6 @@ injectCommand(HelloWorldCommand::class); - } - - public function testSelfUpdate(): void { - $this->setUpdateClient(); - $this->application->setVersion('2.8.4'); - $this->executeCommand(); - self::assertEquals(0, $this->getStatusCode()); - self::assertStringContainsString('Acquia CLI 2.8.5 is available', $this->getDisplay()); - } - - public function testBadResponseFailsSilently(): void { - $this->setUpdateClient(403); - $this->application->setVersion('2.8.4'); - $this->executeCommand(); - self::assertEquals(0, $this->getStatusCode()); - self::assertStringNotContainsString('Acquia CLI 2.8.5 is available', $this->getDisplay()); - } - - public function testNetworkErrorFailsSilently(): void { - $guzzleClient = $this->prophet->prophesize(Client::class); - $guzzleClient->get('https://api.github.com/repos/acquia/cli/releases') - ->willThrow(RequestException::class); - $this->command->setUpdateClient($guzzleClient->reveal()); - $this->application->setVersion('2.8.4.9999s'); - $this->executeCommand(); - self::assertEquals(0, $this->getStatusCode()); - self::assertStringNotContainsString('Acquia CLI 2.8.5 is available', $this->getDisplay()); - } - +class UpdateCommandTest extends CommandTestBase +{ + protected function createCommand(): CommandBase + { + return $this->injectCommand(HelloWorldCommand::class); + } + + public function testSelfUpdate(): void + { + $this->setUpdateClient(); + $this->application->setVersion('2.8.4'); + $this->executeCommand(); + self::assertEquals(0, $this->getStatusCode()); + self::assertStringContainsString('Acquia CLI 2.8.5 is available', $this->getDisplay()); + } + + public function testBadResponseFailsSilently(): void + { + $this->setUpdateClient(403); + $this->application->setVersion('2.8.4'); + $this->executeCommand(); + self::assertEquals(0, $this->getStatusCode()); + self::assertStringNotContainsString('Acquia CLI 2.8.5 is available', $this->getDisplay()); + } + + public function testNetworkErrorFailsSilently(): void + { + $guzzleClient = $this->prophet->prophesize(Client::class); + $guzzleClient->get('https://api.github.com/repos/acquia/cli/releases') + ->willThrow(RequestException::class); + $this->command->setUpdateClient($guzzleClient->reveal()); + $this->application->setVersion('2.8.4.9999s'); + $this->executeCommand(); + self::assertEquals(0, $this->getStatusCode()); + self::assertStringNotContainsString('Acquia CLI 2.8.5 is available', $this->getDisplay()); + } } diff --git a/tests/phpunit/src/Commands/WizardTestBase.php b/tests/phpunit/src/Commands/WizardTestBase.php index 5b2142571..63f39ecd8 100644 --- a/tests/phpunit/src/Commands/WizardTestBase.php +++ b/tests/phpunit/src/Commands/WizardTestBase.php @@ -1,6 +1,6 @@ getCommandTester(); - $this->application->addCommands([ - $this->injectCommand(SshKeyCreateCommand::class), - $this->injectCommand(SshKeyDeleteCommand::class), - $this->injectCommand(SshKeyUploadCommand::class), - ]); - } - - protected function tearDown(): void { - parent::tearDown(); - TestBase::unsetEnvVars(self::getEnvVars()); - } - - /** - * @return array - */ - public static function getEnvVars(): array { - return [ - 'ACQUIA_APPLICATION_UUID' => self::$applicationUuid, - ]; - } - - protected function runTestCreate(): void { - $environmentsResponse = $this->getMockEnvironmentsResponse(); - $this->clientProphecy->request('get', "/applications/{$this::$applicationUuid}/environments")->willReturn($environmentsResponse->_embedded->items)->shouldBeCalled(); - $request = $this->getMockRequestBodyFromSpec('/account/ssh-keys'); - - $body = [ - 'json' => [ +abstract class WizardTestBase extends CommandTestBase +{ + public static string $applicationUuid = 'a47ac10b-58cc-4372-a567-0e02b2c3d470'; + + protected string $sshKeyFileName; + + /** + * This method is called before each test. + */ + public function setUp(): void + { + TestBase::setEnvVars(self::getEnvVars()); + parent::setUp(); + $this->getCommandTester(); + $this->application->addCommands([ + $this->injectCommand(SshKeyCreateCommand::class), + $this->injectCommand(SshKeyDeleteCommand::class), + $this->injectCommand(SshKeyUploadCommand::class), + ]); + } + + protected function tearDown(): void + { + parent::tearDown(); + TestBase::unsetEnvVars(self::getEnvVars()); + } + + /** + * @return array + */ + public static function getEnvVars(): array + { + return [ + 'ACQUIA_APPLICATION_UUID' => self::$applicationUuid, + ]; + } + + protected function runTestCreate(): void + { + $environmentsResponse = $this->getMockEnvironmentsResponse(); + $this->clientProphecy->request('get', "/applications/{$this::$applicationUuid}/environments")->willReturn($environmentsResponse->_embedded->items)->shouldBeCalled(); + $request = $this->getMockRequestBodyFromSpec('/account/ssh-keys'); + + $body = [ + 'json' => [ 'label' => 'IDE_ExampleIDE_215824ff272a4a8c9027df32ed1d68a9', 'public_key' => $request['public_key'], - ], - ]; - $this->mockRequest('postAccountSshKeys', NULL, $body); - - $localMachineHelper = $this->mockLocalMachineHelper(); - - // Poll Cloud. - $sshHelper = $this->mockPollCloudViaSsh($environmentsResponse->_embedded->items); - $this->command->sshHelper = $sshHelper->reveal(); - - $fileSystem = $this->prophet->prophesize(Filesystem::class); - $this->mockGenerateSshKey($localMachineHelper, $request['public_key']); - $localMachineHelper->getLocalFilepath($this->passphraseFilepath)->willReturn($this->passphraseFilepath); - $fileSystem->remove(Argument::size(2))->shouldBeCalled(); - $this->mockAddSshKeyToAgent($localMachineHelper, $fileSystem); - $this->mockSshAgentList($localMachineHelper); - $localMachineHelper->getFilesystem()->willReturn($fileSystem->reveal())->shouldBeCalled(); - - /** @var SshKeyCreateCommand $sshKeyCreateCommand */ - $sshKeyCreateCommand = $this->application->find(SshKeyCreateCommand::getDefaultName()); - $sshKeyCreateCommand->localMachineHelper = $this->command->localMachineHelper; - /** @var SshKeyUploadCommand $sshKeyUploadCommand */ - $sshKeyUploadCommand = $this->application->find(SshKeyUploadCommand::getDefaultName()); - $sshKeyUploadCommand->localMachineHelper = $this->command->localMachineHelper; - /** @var SshKeyDeleteCommand $sshKeyDeleteCommand */ - $sshKeyDeleteCommand = $this->application->find(SshKeyDeleteCommand::getDefaultName()); - $sshKeyDeleteCommand->localMachineHelper = $this->command->localMachineHelper; - - // Remove SSH key if it exists. - $this->fs->remove(Path::join(sys_get_temp_dir(), $this->sshKeyFileName)); - - // Set properties and execute. - $this->executeCommand([], [ - // Would you like to link the project at ... ? - 'y', - ]); - - // Assertions. - } - - protected function runTestSshKeyAlreadyUploaded(): void { - $mockRequestArgs = $this->getMockRequestBodyFromSpec('/account/ssh-keys'); - $sshKeysResponse = $this->getMockResponseFromSpec('/account/ssh-keys', 'get', '200'); - // Make the uploaded key match the created one. - $sshKeysResponse->_embedded->items[0]->public_key = $mockRequestArgs['public_key']; - $this->clientProphecy->request('get', '/account/ssh-keys') - ->willReturn($sshKeysResponse->{'_embedded'}->items) - ->shouldBeCalled(); - - $this->clientProphecy->request('get', '/account/ssh-keys/' . $sshKeysResponse->_embedded->items[0]->uuid) - ->willReturn($sshKeysResponse->{'_embedded'}->items[0]) - ->shouldBeCalled(); - - $deleteResponse = $this->prophet->prophesize(ResponseInterface::class); - $deleteResponse->getStatusCode()->willReturn(202); - $this->clientProphecy->makeRequest('delete', '/account/ssh-keys/' . $sshKeysResponse->_embedded->items[0]->uuid) - ->willReturn($deleteResponse->reveal()) - ->shouldBeCalled(); - - $environmentsResponse = $this->getMockEnvironmentsResponse(); - $this->clientProphecy->request('get', "/applications/{$this::$applicationUuid}/environments") - ->willReturn($environmentsResponse->_embedded->items) - ->shouldBeCalled(); - - $localMachineHelper = $this->mockLocalMachineHelper(); - - $body = [ - 'json' => [ + ], + ]; + $this->mockRequest('postAccountSshKeys', null, $body); + + $localMachineHelper = $this->mockLocalMachineHelper(); + + // Poll Cloud. + $sshHelper = $this->mockPollCloudViaSsh($environmentsResponse->_embedded->items); + $this->command->sshHelper = $sshHelper->reveal(); + + $fileSystem = $this->prophet->prophesize(Filesystem::class); + $this->mockGenerateSshKey($localMachineHelper, $request['public_key']); + $localMachineHelper->getLocalFilepath($this->passphraseFilepath)->willReturn($this->passphraseFilepath); + $fileSystem->remove(Argument::size(2))->shouldBeCalled(); + $this->mockAddSshKeyToAgent($localMachineHelper, $fileSystem); + $this->mockSshAgentList($localMachineHelper); + $localMachineHelper->getFilesystem()->willReturn($fileSystem->reveal())->shouldBeCalled(); + + /** @var SshKeyCreateCommand $sshKeyCreateCommand */ + $sshKeyCreateCommand = $this->application->find(SshKeyCreateCommand::getDefaultName()); + $sshKeyCreateCommand->localMachineHelper = $this->command->localMachineHelper; + /** @var SshKeyUploadCommand $sshKeyUploadCommand */ + $sshKeyUploadCommand = $this->application->find(SshKeyUploadCommand::getDefaultName()); + $sshKeyUploadCommand->localMachineHelper = $this->command->localMachineHelper; + /** @var SshKeyDeleteCommand $sshKeyDeleteCommand */ + $sshKeyDeleteCommand = $this->application->find(SshKeyDeleteCommand::getDefaultName()); + $sshKeyDeleteCommand->localMachineHelper = $this->command->localMachineHelper; + + // Remove SSH key if it exists. + $this->fs->remove(Path::join(sys_get_temp_dir(), $this->sshKeyFileName)); + + // Set properties and execute. + $this->executeCommand([], [ + // Would you like to link the project at ... ? + 'y', + ]); + + // Assertions. + } + + protected function runTestSshKeyAlreadyUploaded(): void + { + $mockRequestArgs = $this->getMockRequestBodyFromSpec('/account/ssh-keys'); + $sshKeysResponse = $this->getMockResponseFromSpec('/account/ssh-keys', 'get', '200'); + // Make the uploaded key match the created one. + $sshKeysResponse->_embedded->items[0]->public_key = $mockRequestArgs['public_key']; + $this->clientProphecy->request('get', '/account/ssh-keys') + ->willReturn($sshKeysResponse->{'_embedded'}->items) + ->shouldBeCalled(); + + $this->clientProphecy->request('get', '/account/ssh-keys/' . $sshKeysResponse->_embedded->items[0]->uuid) + ->willReturn($sshKeysResponse->{'_embedded'}->items[0]) + ->shouldBeCalled(); + + $deleteResponse = $this->prophet->prophesize(ResponseInterface::class); + $deleteResponse->getStatusCode()->willReturn(202); + $this->clientProphecy->makeRequest('delete', '/account/ssh-keys/' . $sshKeysResponse->_embedded->items[0]->uuid) + ->willReturn($deleteResponse->reveal()) + ->shouldBeCalled(); + + $environmentsResponse = $this->getMockEnvironmentsResponse(); + $this->clientProphecy->request('get', "/applications/{$this::$applicationUuid}/environments") + ->willReturn($environmentsResponse->_embedded->items) + ->shouldBeCalled(); + + $localMachineHelper = $this->mockLocalMachineHelper(); + + $body = [ + 'json' => [ 'label' => 'IDE_ExampleIDE_215824ff272a4a8c9027df32ed1d68a9', 'public_key' => $mockRequestArgs['public_key'], - ], - ]; - $this->mockRequest('postAccountSshKeys', NULL, $body); - - // Poll Cloud. - $sshHelper = $this->mockPollCloudViaSsh($environmentsResponse->_embedded->items); - $this->command->sshHelper = $sshHelper->reveal(); - - $fileSystem = $this->prophet->prophesize(Filesystem::class); - $this->mockGenerateSshKey($localMachineHelper, $mockRequestArgs['public_key']); - $fileSystem->remove(Argument::size(2))->shouldBeCalled(); - $this->mockAddSshKeyToAgent($localMachineHelper, $fileSystem); - $localMachineHelper->getFilesystem() - ->willReturn($fileSystem->reveal()) - ->shouldBeCalled(); - $this->mockSshAgentList($localMachineHelper); - - $this->application->find(SshKeyCreateCommand::getDefaultName())->localMachineHelper = $this->command->localMachineHelper; - $this->application->find(SshKeyUploadCommand::getDefaultName())->localMachineHelper = $this->command->localMachineHelper; - $this->application->find(SshKeyDeleteCommand::getDefaultName())->localMachineHelper = $this->command->localMachineHelper; - - $this->createLocalSshKey($mockRequestArgs['public_key']); - $this->executeCommand(); - } - - /** - * @return string[] - * An array of strings to inspect the output for. - */ - protected function getOutputStrings(): array { - return [ - "Setting GitLab CI/CD variables for", - ]; - } - + ], + ]; + $this->mockRequest('postAccountSshKeys', null, $body); + + // Poll Cloud. + $sshHelper = $this->mockPollCloudViaSsh($environmentsResponse->_embedded->items); + $this->command->sshHelper = $sshHelper->reveal(); + + $fileSystem = $this->prophet->prophesize(Filesystem::class); + $this->mockGenerateSshKey($localMachineHelper, $mockRequestArgs['public_key']); + $fileSystem->remove(Argument::size(2))->shouldBeCalled(); + $this->mockAddSshKeyToAgent($localMachineHelper, $fileSystem); + $localMachineHelper->getFilesystem() + ->willReturn($fileSystem->reveal()) + ->shouldBeCalled(); + $this->mockSshAgentList($localMachineHelper); + + $this->application->find(SshKeyCreateCommand::getDefaultName())->localMachineHelper = $this->command->localMachineHelper; + $this->application->find(SshKeyUploadCommand::getDefaultName())->localMachineHelper = $this->command->localMachineHelper; + $this->application->find(SshKeyDeleteCommand::getDefaultName())->localMachineHelper = $this->command->localMachineHelper; + + $this->createLocalSshKey($mockRequestArgs['public_key']); + $this->executeCommand(); + } + + /** + * @return string[] + * An array of strings to inspect the output for. + */ + protected function getOutputStrings(): array + { + return [ + "Setting GitLab CI/CD variables for", + ]; + } } diff --git a/tests/phpunit/src/Misc/ApiSpecTest.php b/tests/phpunit/src/Misc/ApiSpecTest.php index de7df9115..03874041a 100644 --- a/tests/phpunit/src/Misc/ApiSpecTest.php +++ b/tests/phpunit/src/Misc/ApiSpecTest.php @@ -1,21 +1,21 @@ assertFileExists($apiSpecFile); - $apiSpec = file_get_contents($apiSpecFile); - $this->assertStringNotContainsString('x-internal', $apiSpec); - $this->assertStringNotContainsString('cloud.acquia.dev', $apiSpec); - $this->assertStringNotContainsString('network.acquia-sites.com', $apiSpec); - } - +class ApiSpecTest extends TestCase +{ + public function testApiSpec(): void + { + $apiSpecFile = Path::canonicalize(__DIR__ . '/../../../../assets/acquia-spec.json'); + $this->assertFileExists($apiSpecFile); + $apiSpec = file_get_contents($apiSpecFile); + $this->assertStringNotContainsString('x-internal', $apiSpec); + $this->assertStringNotContainsString('cloud.acquia.dev', $apiSpec); + $this->assertStringNotContainsString('network.acquia-sites.com', $apiSpec); + } } diff --git a/tests/phpunit/src/Misc/EnvDbCredsTest.php b/tests/phpunit/src/Misc/EnvDbCredsTest.php index fb02fd52d..3bbff0c8b 100644 --- a/tests/phpunit/src/Misc/EnvDbCredsTest.php +++ b/tests/phpunit/src/Misc/EnvDbCredsTest.php @@ -1,6 +1,6 @@ dbUser = 'myuserisgood'; - $this->dbPassword = 'mypasswordisgreat'; - $this->dbName = 'mynameisgrand'; - $this->dbHost = 'myhostismeh'; - TestBase::setEnvVars($this->getEnvVars()); - parent::setUp(); - } - - public function tearDown(): void { - parent::tearDown(); - TestBase::unsetEnvVars($this->getEnvVars()); - } - - /** - * @return array - */ - protected function getEnvVars(): array { - return [ - 'ACLI_DB_HOST' => $this->dbHost, - 'ACLI_DB_NAME' => $this->dbName, - 'ACLI_DB_PASSWORD' => $this->dbPassword, - 'ACLI_DB_USER' => $this->dbUser, - ]; - } - - protected function createCommand(): CommandBase { - return $this->injectCommand(ClearCacheCommand::class); - } - - public function testEnvDbCreds(): void { - $this->assertEquals($this->dbUser, $this->command->getLocalDbUser()); - $this->assertEquals($this->dbPassword, $this->command->getLocalDbPassword()); - $this->assertEquals($this->dbName, $this->command->getLocalDbName()); - $this->assertEquals($this->dbHost, $this->command->getLocalDbHost()); - } - +class EnvDbCredsTest extends CommandTestBase +{ + private string $dbUser; + + private string $dbPassword; + + private string $dbName; + + private string $dbHost; + + public function setUp(mixed $output = null): void + { + $this->dbUser = 'myuserisgood'; + $this->dbPassword = 'mypasswordisgreat'; + $this->dbName = 'mynameisgrand'; + $this->dbHost = 'myhostismeh'; + TestBase::setEnvVars($this->getEnvVars()); + parent::setUp(); + } + + public function tearDown(): void + { + parent::tearDown(); + TestBase::unsetEnvVars($this->getEnvVars()); + } + + /** + * @return array + */ + protected function getEnvVars(): array + { + return [ + 'ACLI_DB_HOST' => $this->dbHost, + 'ACLI_DB_NAME' => $this->dbName, + 'ACLI_DB_PASSWORD' => $this->dbPassword, + 'ACLI_DB_USER' => $this->dbUser, + ]; + } + + protected function createCommand(): CommandBase + { + return $this->injectCommand(ClearCacheCommand::class); + } + + public function testEnvDbCreds(): void + { + $this->assertEquals($this->dbUser, $this->command->getLocalDbUser()); + $this->assertEquals($this->dbPassword, $this->command->getLocalDbPassword()); + $this->assertEquals($this->dbName, $this->command->getLocalDbName()); + $this->assertEquals($this->dbHost, $this->command->getLocalDbHost()); + } } diff --git a/tests/phpunit/src/Misc/ExceptionListenerTest.php b/tests/phpunit/src/Misc/ExceptionListenerTest.php index 3275c9e54..95517e376 100644 --- a/tests/phpunit/src/Misc/ExceptionListenerTest.php +++ b/tests/phpunit/src/Misc/ExceptionListenerTest.php @@ -1,6 +1,6 @@ environmentId can also be a site alias. E.g. myapp.dev.' . PHP_EOL . 'Run acli remote:aliases:list to see a list of all available aliases.'; - private static string $appAliasHelp = 'The applicationUuid argument must be a valid UUID or unique application alias accessible to your Cloud Platform user.' . PHP_EOL . PHP_EOL . 'An alias consists of an application name optionally prefixed with a hosting realm, e.g. myapp or prod.myapp.' . PHP_EOL . PHP_EOL . 'Run acli remote:aliases:list to see a list of all available aliases.'; +class ExceptionListenerTest extends TestBase +{ + private static string $siteAliasHelp = 'environmentId can also be a site alias. E.g. myapp.dev.' . PHP_EOL . 'Run acli remote:aliases:list to see a list of all available aliases.'; + private static string $appAliasHelp = 'The applicationUuid argument must be a valid UUID or unique application alias accessible to your Cloud Platform user.' . PHP_EOL . PHP_EOL . 'An alias consists of an application name optionally prefixed with a hosting realm, e.g. myapp or prod.myapp.' . PHP_EOL . PHP_EOL . 'Run acli remote:aliases:list to see a list of all available aliases.'; - /** - * @dataProvider providerTestHelp - */ - public function testHelp(Throwable $error, string|array $helpText): void { - $exceptionListener = new ExceptionListener(); - $commandProphecy = $this->prophet->prophesize(Command::class); - $applicationProphecy = $this->prophet->prophesize(Application::class); - $messages1 = ['You can find Acquia CLI documentation at https://docs.acquia.com/acquia-cli/', 'You can submit a support ticket at https://support-acquia.force.com/s/contactsupport' . PHP_EOL . 'Re-run the command with the -vvv flag and include the full command output in your support ticket.']; - if (is_array($helpText)) { - $messages = array_merge($helpText, $messages1); + /** + * @dataProvider providerTestHelp + */ + public function testHelp(Throwable $error, string|array $helpText): void + { + $exceptionListener = new ExceptionListener(); + $commandProphecy = $this->prophet->prophesize(Command::class); + $applicationProphecy = $this->prophet->prophesize(Application::class); + $messages1 = ['You can find Acquia CLI documentation at https://docs.acquia.com/acquia-cli/', 'You can submit a support ticket at https://support-acquia.force.com/s/contactsupport' . PHP_EOL . 'Re-run the command with the -vvv flag and include the full command output in your support ticket.']; + if (is_array($helpText)) { + $messages = array_merge($helpText, $messages1); + } else { + $messages = array_merge([$helpText], $messages1); + } + $messages[0] = "How to fix it: $messages[0]"; + $applicationProphecy->setHelpMessages($messages)->shouldBeCalled(); + $commandProphecy->getApplication()->willReturn($applicationProphecy->reveal()); + $consoleErrorEvent = new ConsoleErrorEvent($this->input, $this->output, $error, $commandProphecy->reveal()); + $exceptionListener->onConsoleError($consoleErrorEvent); + $this->prophet->checkPredictions(); + self::assertTrue(true); } - else { - $messages = array_merge([$helpText], $messages1); - } - $messages[0] = "How to fix it: $messages[0]"; - $applicationProphecy->setHelpMessages($messages)->shouldBeCalled(); - $commandProphecy->getApplication()->willReturn($applicationProphecy->reveal()); - $consoleErrorEvent = new ConsoleErrorEvent($this->input, $this->output, $error, $commandProphecy->reveal()); - $exceptionListener->onConsoleError($consoleErrorEvent); - $this->prophet->checkPredictions(); - self::assertTrue(TRUE); - } - /** - * @return string[][] - */ - public function providerTestHelp(): array { - return [ - [ + /** + * @return string[][] + */ + public function providerTestHelp(): array + { + return [ + [ new IdentityProviderException('invalid_client', 0, ''), 'Run acli auth:login to reset your API credentials.', - ], - [ + ], + [ new RuntimeException('Not enough arguments (missing: "environmentId").'), self::$siteAliasHelp, - ], - [ + ], + [ new RuntimeException('Not enough arguments (missing: "environmentUuid").'), self::$siteAliasHelp, - ], - [ + ], + [ new AcquiaCliException('No applications match the alias {applicationAlias}'), self::$appAliasHelp, - ], - [ + ], + [ new AcquiaCliException('Multiple applications match the alias {applicationAlias}'), self::$appAliasHelp, - ], - [ + ], + [ new AcquiaCliException('{environmentId} must be a valid UUID or site alias.'), self::$siteAliasHelp, - ], - [ + ], + [ new AcquiaCliException('{environmentUuid} must be a valid UUID or site alias.'), self::$siteAliasHelp, - ], - [ + ], + [ new AcquiaCliException('Access token file not found at {file}'), 'Get help for this error at https://docs.acquia.com/ide/known-issues/#the-automated-cloud-platform-api-authentication-might-fail', - ], - [ + ], + [ new AcquiaCliException('Access token expiry file not found at {file}'), 'Get help for this error at https://docs.acquia.com/ide/known-issues/#the-automated-cloud-platform-api-authentication-might-fail', - ], - [ + ], + [ new AcquiaCliException('This machine is not yet authenticated with the Cloud Platform.'), 'Run `acli auth:login` to re-authenticated with the Cloud Platform.', - ], - [ + ], + [ new AcquiaCliException('This machine is not yet authenticated with Site Factory.'), 'Run `acli auth:acsf-login` to re-authenticate with Site Factory.', - ], - [ + ], + [ new AcquiaCliException('Could not extract aliases to {destination}'), 'Check that you have write access to the directory', - ], - [ + ], + [ new ApiErrorException((object) ['error' => '', 'message' => "There are no available Cloud IDEs for this application.\n"]), 'Delete an existing IDE via acli ide:delete or contact your Account Manager or Acquia Sales to purchase additional IDEs.', - ], - [ + ], + [ new ApiErrorException((object) ['error' => '', 'message' => 'This resource requires additional authentication.']), ['This is likely because you have Federated Authentication required for your organization.', 'Run `acli login` to authenticate via API token and then try again.'], - ], - [ + ], + [ new ApiErrorException((object) ['error' => 'asdf', 'message' => 'fdsa']), 'You can learn more about Cloud Platform API at https://docs.acquia.com/cloud-platform/develop/api/', - ], - ]; - } - + ], + ]; + } } diff --git a/tests/phpunit/src/Misc/LocalMachineHelperTest.php b/tests/phpunit/src/Misc/LocalMachineHelperTest.php index 9036737ac..a9ac9b51d 100644 --- a/tests/phpunit/src/Misc/LocalMachineHelperTest.php +++ b/tests/phpunit/src/Misc/LocalMachineHelperTest.php @@ -1,6 +1,6 @@ localMachineHelper; - $opened = $localMachineHelper->startBrowser('https://google.com', 'cat'); - $this->assertTrue($opened, 'Failed to open browser'); - putenv('DISPLAY'); - } - - /** - * @return bool[][] - */ - public function providerTestExecuteFromCmd(): array { - return [ - [FALSE, NULL, NULL], - [FALSE, FALSE, FALSE], - [TRUE, FALSE, FALSE], - ]; - } - - /** - * @dataProvider providerTestExecuteFromCmd() - */ - public function testExecuteFromCmd(bool $interactive, bool|NULL $isTty, bool|NULL $printOutput): void { - $localMachineHelper = $this->localMachineHelper; - $localMachineHelper->setIsTty($isTty); - $this->input->setInteractive($interactive); - $process = $localMachineHelper->executeFromCmd('echo "hello world"', NULL, NULL, $printOutput); - $this->assertTrue($process->isSuccessful()); - assert(is_a($this->output, BufferedOutput::class)); - $buffer = $this->output->fetch(); - if ($printOutput === FALSE) { - $this->assertEmpty($buffer); +class LocalMachineHelperTest extends TestBase +{ + public function testStartBrowser(): void + { + putenv('DISPLAY=1'); + $localMachineHelper = $this->localMachineHelper; + $opened = $localMachineHelper->startBrowser('https://google.com', 'cat'); + $this->assertTrue($opened, 'Failed to open browser'); + putenv('DISPLAY'); } - else { - $this->assertStringContainsString("hello world", $buffer); + + /** + * @return bool[][] + */ + public function providerTestExecuteFromCmd(): array + { + return [ + [false, null, null], + [false, false, false], + [true, false, false], + ]; } - } - public function testExecuteWithCwd(): void { - $this->setupFsFixture(); - $localMachineHelper = $this->localMachineHelper; - $process = $localMachineHelper->execute(['ls', '-lash'], NULL, $this->fixtureDir, FALSE); - $this->assertTrue($process->isSuccessful()); - $this->assertStringContainsString('xdebug.ini', $process->getOutput()); - } + /** + * @dataProvider providerTestExecuteFromCmd() + */ + public function testExecuteFromCmd(bool $interactive, bool|null $isTty, bool|null $printOutput): void + { + $localMachineHelper = $this->localMachineHelper; + $localMachineHelper->setIsTty($isTty); + $this->input->setInteractive($interactive); + $process = $localMachineHelper->executeFromCmd('echo "hello world"', null, null, $printOutput); + $this->assertTrue($process->isSuccessful()); + assert(is_a($this->output, BufferedOutput::class)); + $buffer = $this->output->fetch(); + if ($printOutput === false) { + $this->assertEmpty($buffer); + } else { + $this->assertStringContainsString("hello world", $buffer); + } + } - public function testCommandExists(): void { - $localMachineHelper = $this->localMachineHelper; - $exists = $localMachineHelper->commandExists('cat'); - $this->assertIsBool($exists); - } + public function testExecuteWithCwd(): void + { + $this->setupFsFixture(); + $localMachineHelper = $this->localMachineHelper; + $process = $localMachineHelper->execute(['ls', '-lash'], null, $this->fixtureDir, false); + $this->assertTrue($process->isSuccessful()); + $this->assertStringContainsString('xdebug.ini', $process->getOutput()); + } - public function testHomeDirWindowsCmd(): void { - self::setEnvVars([ - 'HOMEPATH' => 'something', - ]); - self::unsetEnvVars([ - 'MSYSTEM', - 'HOME', - ]); - $home = LocalMachineHelper::getHomeDir(); - $this->assertEquals('something', $home); - } + public function testCommandExists(): void + { + $localMachineHelper = $this->localMachineHelper; + $exists = $localMachineHelper->commandExists('cat'); + $this->assertIsBool($exists); + } - public function testHomeDirWindowsMsys2(): void { - self::setEnvVars([ - 'HOMEPATH' => 'something', - 'MSYSTEM' => 'MSYS2', - ]); - self::unsetEnvVars(['HOME']); - $home = LocalMachineHelper::getHomeDir(); - $this->assertEquals('something', $home); - } + public function testHomeDirWindowsCmd(): void + { + self::setEnvVars([ + 'HOMEPATH' => 'something', + ]); + self::unsetEnvVars([ + 'MSYSTEM', + 'HOME', + ]); + $home = LocalMachineHelper::getHomeDir(); + $this->assertEquals('something', $home); + } - /** - * I don't know why, but apparently Ming is unsupported ¯\_(ツ)_/¯. - */ - public function testHomeDirWindowsMing(): void { - self::setEnvVars(['MSYSTEM' => 'MING']); - self::unsetEnvVars(['HOME']); - $this->expectException(AcquiaCliException::class); - $this->expectExceptionMessage('Could not determine $HOME directory. Ensure $HOME is set in your shell.'); - LocalMachineHelper::getHomeDir(); - } + public function testHomeDirWindowsMsys2(): void + { + self::setEnvVars([ + 'HOMEPATH' => 'something', + 'MSYSTEM' => 'MSYS2', + ]); + self::unsetEnvVars(['HOME']); + $home = LocalMachineHelper::getHomeDir(); + $this->assertEquals('something', $home); + } - public function testConfigDirLegacy(): void { - self::setEnvVars(['HOME' => 'vfs://root']); - $configDir = LocalMachineHelper::getConfigDir(); - $this->assertEquals('vfs://root/.acquia', $configDir); - } + /** + * I don't know why, but apparently Ming is unsupported ¯\_(ツ)_/¯. + */ + public function testHomeDirWindowsMing(): void + { + self::setEnvVars(['MSYSTEM' => 'MING']); + self::unsetEnvVars(['HOME']); + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage('Could not determine $HOME directory. Ensure $HOME is set in your shell.'); + LocalMachineHelper::getHomeDir(); + } - public function testConfigDirFromXdg(): void { - self::setEnvVars(['XDG_CONFIG_HOME' => 'vfs://root/.config']); - $configDir = LocalMachineHelper::getConfigDir(); - $this->assertEquals('vfs://root/.config/acquia', $configDir); - } + public function testConfigDirLegacy(): void + { + self::setEnvVars(['HOME' => 'vfs://root']); + $configDir = LocalMachineHelper::getConfigDir(); + $this->assertEquals('vfs://root/.acquia', $configDir); + } - public function testConfigDirDefault(): void { - self::setEnvVars(['HOME' => 'vfs://root']); - self::unsetEnvVars(['XDG_CONFIG_HOME']); - unlink('vfs://root/.acquia/cloud_api.conf'); - rmdir('vfs://root/.acquia'); - $configDir = LocalMachineHelper::getConfigDir(); - $this->assertEquals('vfs://root/.config/acquia', $configDir); - } + public function testConfigDirFromXdg(): void + { + self::setEnvVars(['XDG_CONFIG_HOME' => 'vfs://root/.config']); + $configDir = LocalMachineHelper::getConfigDir(); + $this->assertEquals('vfs://root/.config/acquia', $configDir); + } + public function testConfigDirDefault(): void + { + self::setEnvVars(['HOME' => 'vfs://root']); + self::unsetEnvVars(['XDG_CONFIG_HOME']); + unlink('vfs://root/.acquia/cloud_api.conf'); + rmdir('vfs://root/.acquia'); + $configDir = LocalMachineHelper::getConfigDir(); + $this->assertEquals('vfs://root/.config/acquia', $configDir); + } } diff --git a/tests/phpunit/src/Misc/TelemetryHelperTest.php b/tests/phpunit/src/Misc/TelemetryHelperTest.php index 0c3058a84..2bfc60a42 100644 --- a/tests/phpunit/src/Misc/TelemetryHelperTest.php +++ b/tests/phpunit/src/Misc/TelemetryHelperTest.php @@ -1,100 +1,106 @@ providerTestEnvironmentProvider() as $args) { + $envVars = array_merge($envVars, $args[1]); + } - public function tearDown(): void { - parent::tearDown(); - $envVars = []; - foreach ($this->providerTestEnvironmentProvider() as $args) { - $envVars = array_merge($envVars, $args[1]); + TestBase::unsetEnvVars($envVars); } - TestBase::unsetEnvVars($envVars); - } + public function unsetGitHubEnvVars(): void + { + $providers = TelemetryHelper::getProviders(); - public function unsetGitHubEnvVars(): void { - $providers = TelemetryHelper::getProviders(); - - // Since we actually run our own tests on GitHub, getEnvironmentProvider() will return 'github' unless we unset it. - $github_env_vars = []; - foreach ($providers['github'] as $var) { - $github_env_vars[$var] = self::ENV_VAR_DEFAULT_VALUE; + // Since we actually run our own tests on GitHub, getEnvironmentProvider() will return 'github' unless we unset it. + $github_env_vars = []; + foreach ($providers['github'] as $var) { + $github_env_vars[$var] = self::ENV_VAR_DEFAULT_VALUE; + } + TestBase::unsetEnvVars($github_env_vars); } - TestBase::unsetEnvVars($github_env_vars); - } - /** - * @return array - */ - public function providerTestEnvironmentProvider(): array { - $providersList = TelemetryHelper::getProviders(); - $providersArray = []; - foreach ($providersList as $provider => $envVars) { - $env_vars_with_values = []; - foreach ($envVars as $var_name) { - $env_vars_with_values[$var_name] = self::ENV_VAR_DEFAULT_VALUE; - } - $providersArray[] = [$provider, $env_vars_with_values]; + /** + * @return array + */ + public function providerTestEnvironmentProvider(): array + { + $providersList = TelemetryHelper::getProviders(); + $providersArray = []; + foreach ($providersList as $provider => $envVars) { + $env_vars_with_values = []; + foreach ($envVars as $var_name) { + $env_vars_with_values[$var_name] = self::ENV_VAR_DEFAULT_VALUE; + } + $providersArray[] = [$provider, $env_vars_with_values]; + } + + return $providersArray; } - return $providersArray; - } - - /** - * @dataProvider providerTestEnvironmentProvider() - */ - public function testEnvironmentProvider(string $provider, array $envVars): void { - $this->unsetGitHubEnvVars(); - TestBase::setEnvVars($envVars); - $this->assertEquals($provider, TelemetryHelper::getEnvironmentProvider()); - } - - /** - * Test the getEnvironmentProvider method when no environment provider is detected. - */ - public function testGetEnvironmentProviderWithoutAnyEnvSet(): void { - $this->unsetGitHubEnvVars(); + /** + * @dataProvider providerTestEnvironmentProvider() + */ + public function testEnvironmentProvider(string $provider, array $envVars): void + { + $this->unsetGitHubEnvVars(); + TestBase::setEnvVars($envVars); + $this->assertEquals($provider, TelemetryHelper::getEnvironmentProvider()); + } - // Expect null since no provider environment variables are set. - $this->assertNull(TelemetryHelper::getEnvironmentProvider()); - } + /** + * Test the getEnvironmentProvider method when no environment provider is detected. + */ + public function testGetEnvironmentProviderWithoutAnyEnvSet(): void + { + $this->unsetGitHubEnvVars(); - /** - * @return mixed[] - * The data provider. - */ - public function providerTestAhEnvNormalization(): array { - return [ - ['prod', 'prod'], - ['01live', 'prod'], - ['stage', 'stage'], - ['stg', 'stage'], - ['dev1', 'dev'], - ['ode1', 'ode'], - ['ide', 'ide'], - ['fake', 'fake'], - ]; - } + // Expect null since no provider environment variables are set. + $this->assertNull(TelemetryHelper::getEnvironmentProvider()); + } - /** - * @dataProvider providerTestAhEnvNormalization - * @param string $ah_env - * The Acquia hosting environment. - * @param string $expected - * The expected normalized environment. - */ - public function testAhEnvNormalization(string $ah_env, string $expected): void { - $normalized_ah_env = TelemetryHelper::normalizeAhEnv($ah_env); - $this->assertEquals($expected, $normalized_ah_env); - } + /** + * @return mixed[] + * The data provider. + */ + public function providerTestAhEnvNormalization(): array + { + return [ + ['prod', 'prod'], + ['01live', 'prod'], + ['stage', 'stage'], + ['stg', 'stage'], + ['dev1', 'dev'], + ['ode1', 'ode'], + ['ide', 'ide'], + ['fake', 'fake'], + ]; + } + /** + * @dataProvider providerTestAhEnvNormalization + * @param string $ah_env + * The Acquia hosting environment. + * @param string $expected + * The expected normalized environment. + */ + public function testAhEnvNormalization(string $ah_env, string $expected): void + { + $normalized_ah_env = TelemetryHelper::normalizeAhEnv($ah_env); + $this->assertEquals($expected, $normalized_ah_env); + } } diff --git a/tests/phpunit/src/TestBase.php b/tests/phpunit/src/TestBase.php index f8c5956f2..efe15ed6c 100644 --- a/tests/phpunit/src/TestBase.php +++ b/tests/phpunit/src/TestBase.php @@ -1,6 +1,6 @@ + */ + protected array $acliConfig = []; - /** - * @var array - */ - protected array $acliConfig = []; + /** + * @var array + */ + protected array $cloudConfig = []; - /** - * @var array - */ - protected array $cloudConfig = []; + protected string $key = '17feaf34-5d04-402b-9a67-15d5161d24e1'; - protected string $key = '17feaf34-5d04-402b-9a67-15d5161d24e1'; + protected string $secret = 'X1u\/PIQXtYaoeui.4RJSJpGZjwmWYmfl5AUQkAebYE='; - protected string $secret = 'X1u\/PIQXtYaoeui.4RJSJpGZjwmWYmfl5AUQkAebYE='; + protected string $dataDir; - protected string $dataDir; + protected string $cloudConfigFilepath; - protected string $cloudConfigFilepath; + protected string $acliConfigFilepath; - protected string $acliConfigFilepath; + protected AcquiaCliDatastore $datastoreAcli; - protected AcquiaCliDatastore $datastoreAcli; + protected CloudDataStore $datastoreCloud; - protected CloudDataStore $datastoreCloud; + protected ApiCredentialsInterface $cloudCredentials; - protected ApiCredentialsInterface $cloudCredentials; + protected LocalMachineHelper $localMachineHelper; - protected LocalMachineHelper $localMachineHelper; + protected TelemetryHelper $telemetryHelper; - protected TelemetryHelper $telemetryHelper; + protected string $acliConfigFilename; - protected string $acliConfigFilename; + protected ClientService|ObjectProphecy $clientServiceProphecy; - protected ClientService|ObjectProphecy $clientServiceProphecy; + protected SshHelper $sshHelper; - protected SshHelper $sshHelper; + protected string $sshDir; - protected string $sshDir; + protected string $acliRepoRoot; - protected string $acliRepoRoot; + protected ConsoleLogger $logger; - protected ConsoleLogger $logger; + protected string $passphraseFilepath = '~/.passphrase'; - protected string $passphraseFilepath = '~/.passphrase'; + protected vfsStreamDirectory $vfsRoot; - protected vfsStreamDirectory $vfsRoot; + protected string $realFixtureDir; - protected string $realFixtureDir; + /** + * Filter an applications response in order to simulate query filters. + * + * The CXAPI spec returns two sample applications with identical hosting ids. + * While hosting ids are not guaranteed to be unique, in practice they are + * unique. This renames one of the applications to be unique. + * + * @see CXAPI-9647 + */ + public function filterApplicationsResponse(object $applicationsResponse, int $count, bool $unique): object + { + if ($unique) { + $applicationsResponse->{'_embedded'}->items[1]->hosting->id = 'devcloud:devcloud3'; + } + $applicationsResponse->total = $count; + $applicationsResponse->{'_embedded'}->items = array_slice($applicationsResponse->{'_embedded'}->items, 0, $count); + return $applicationsResponse; + } - /** - * Filter an applications response in order to simulate query filters. - * - * The CXAPI spec returns two sample applications with identical hosting ids. - * While hosting ids are not guaranteed to be unique, in practice they are - * unique. This renames one of the applications to be unique. - * - * @see CXAPI-9647 - */ - public function filterApplicationsResponse(object $applicationsResponse, int $count, bool $unique): object { - if ($unique) { - $applicationsResponse->{'_embedded'}->items[1]->hosting->id = 'devcloud:devcloud3'; + /** + * @todo get rid of this method and use virtual file systems (setupVfsFixture) + */ + public function setupFsFixture(): void + { + $this->fixtureDir = $this->getTempDir(); + $this->fs->mirror(realpath(__DIR__ . '/../../fixtures'), $this->fixtureDir); + $this->projectDir = $this->fixtureDir . '/project'; + $this->acliRepoRoot = $this->projectDir; + $this->dataDir = $this->fixtureDir . '/.acquia'; + $this->sshDir = $this->getTempDir(); + $this->acliConfigFilename = '.acquia-cli.yml'; + $this->cloudConfigFilepath = $this->dataDir . '/cloud_api.conf'; + $this->acliConfigFilepath = $this->projectDir . '/' . $this->acliConfigFilename; + $this->createMockConfigFiles(); + $this->createDataStores(); + $this->cloudCredentials = new CloudCredentials($this->datastoreCloud); + $this->telemetryHelper = new TelemetryHelper($this->clientServiceProphecy->reveal(), $this->datastoreCloud, $this->application); + chdir($this->projectDir); } - $applicationsResponse->total = $count; - $applicationsResponse->{'_embedded'}->items = array_slice($applicationsResponse->{'_embedded'}->items, 0, $count); - return $applicationsResponse; - } - /** - * @todo get rid of this method and use virtual file systems (setupVfsFixture) - */ - public function setupFsFixture(): void { - $this->fixtureDir = $this->getTempDir(); - $this->fs->mirror(realpath(__DIR__ . '/../../fixtures'), $this->fixtureDir); - $this->projectDir = $this->fixtureDir . '/project'; - $this->acliRepoRoot = $this->projectDir; - $this->dataDir = $this->fixtureDir . '/.acquia'; - $this->sshDir = $this->getTempDir(); - $this->acliConfigFilename = '.acquia-cli.yml'; - $this->cloudConfigFilepath = $this->dataDir . '/cloud_api.conf'; - $this->acliConfigFilepath = $this->projectDir . '/' . $this->acliConfigFilename; - $this->createMockConfigFiles(); - $this->createDataStores(); - $this->cloudCredentials = new CloudCredentials($this->datastoreCloud); - $this->telemetryHelper = new TelemetryHelper($this->clientServiceProphecy->reveal(), $this->datastoreCloud, $this->application); - chdir($this->projectDir); - } + /** + * This method is called before each test. + */ + protected function setUp(): void + { + self::setEnvVars([ + 'COLUMNS' => '85', + 'HOME' => '/home/test', + ]); + $this->output = new BufferedOutput(); + $this->input = new ArrayInput([]); + + $this->application = new Application(); + $this->fs = new Filesystem(); + $this->prophet = new Prophet(); + $this->consoleOutput = new ConsoleOutput(); + $this->setClientProphecies(); + $this->setIo(); + + $this->vfsRoot = vfsStream::setup(); + $this->projectDir = vfsStream::newDirectory('project')->at($this->vfsRoot)->url(); + $this->sshDir = vfsStream::newDirectory('ssh')->at($this->vfsRoot)->url(); + $this->dataDir = vfsStream::newDirectory('.acquia')->at($this->vfsRoot)->url(); + $this->cloudConfigFilepath = Path::join($this->dataDir, 'cloud_api.conf'); + $this->acliConfigFilename = '.acquia-cli.yml'; + $this->acliConfigFilepath = Path::join($this->projectDir, $this->acliConfigFilename); + $this->acliRepoRoot = $this->projectDir; + $this->createMockConfigFiles(); + $this->createDataStores(); + $this->cloudCredentials = new CloudCredentials($this->datastoreCloud); + $this->telemetryHelper = new TelemetryHelper($this->clientServiceProphecy->reveal(), $this->datastoreCloud, $this->application); + + $this->realFixtureDir = realpath(Path::join(__DIR__, '..', '..', 'fixtures')); + + parent::setUp(); + } - /** - * This method is called before each test. - */ - protected function setUp(): void { - self::setEnvVars([ - 'COLUMNS' => '85', - 'HOME' => '/home/test', - ]); - $this->output = new BufferedOutput(); - $this->input = new ArrayInput([]); - - $this->application = new Application(); - $this->fs = new Filesystem(); - $this->prophet = new Prophet(); - $this->consoleOutput = new ConsoleOutput(); - $this->setClientProphecies(); - $this->setIo(); - - $this->vfsRoot = vfsStream::setup(); - $this->projectDir = vfsStream::newDirectory('project')->at($this->vfsRoot)->url(); - $this->sshDir = vfsStream::newDirectory('ssh')->at($this->vfsRoot)->url(); - $this->dataDir = vfsStream::newDirectory('.acquia')->at($this->vfsRoot)->url(); - $this->cloudConfigFilepath = Path::join($this->dataDir, 'cloud_api.conf'); - $this->acliConfigFilename = '.acquia-cli.yml'; - $this->acliConfigFilepath = Path::join($this->projectDir, $this->acliConfigFilename); - $this->acliRepoRoot = $this->projectDir; - $this->createMockConfigFiles(); - $this->createDataStores(); - $this->cloudCredentials = new CloudCredentials($this->datastoreCloud); - $this->telemetryHelper = new TelemetryHelper($this->clientServiceProphecy->reveal(), $this->datastoreCloud, $this->application); - - $this->realFixtureDir = realpath(Path::join(__DIR__, '..', '..', 'fixtures')); - - parent::setUp(); - } - - /** - * Create a guaranteed-unique temporary directory. - */ - protected function getTempDir(): string { - // sys_get_temp_dir() is not thread-safe, but it's okay to use here since we are specifically creating a thread-safe temporary directory. - // phpcs:ignore - $dir = sys_get_temp_dir(); - - // /tmp is a symlink to /private/tmp on Mac, which causes inconsistency when - // normalizing paths. - if (PHP_OS_FAMILY === 'Darwin') { - $dir = Path::join('/private', $dir); - } - - /* If we don't have permission to create a directory, fail, otherwise we will - * be stuck in an endless loop. + /** + * Create a guaranteed-unique temporary directory. */ - if (!is_dir($dir) || !is_writable($dir)) { - throw new AcquiaCliException('Cannot write to temporary directory'); + protected function getTempDir(): string + { + // sys_get_temp_dir() is not thread-safe, but it's okay to use here since we are specifically creating a thread-safe temporary directory. + // phpcs:ignore + $dir = sys_get_temp_dir(); + + // /tmp is a symlink to /private/tmp on Mac, which causes inconsistency when + // normalizing paths. + if (PHP_OS_FAMILY === 'Darwin') { + $dir = Path::join('/private', $dir); + } + + /* If we don't have permission to create a directory, fail, otherwise we will + * be stuck in an endless loop. + */ + if (!is_dir($dir) || !is_writable($dir)) { + throw new AcquiaCliException('Cannot write to temporary directory'); + } + + /* Attempt to create a random directory until it works. Abort if we reach + * $maxAttempts. Something screwy could be happening with the filesystem + * and our loop could otherwise become endless. + */ + $attempts = 0; + do { + $path = sprintf('%s%s%s%s', $dir, DIRECTORY_SEPARATOR, 'tmp_', random_int(100000, mt_getrandmax())); + } while ( + !mkdir($path, 0700) && + $attempts++ < 10 + ); + + return $path; } - /* Attempt to create a random directory until it works. Abort if we reach - * $maxAttempts. Something screwy could be happening with the filesystem - * and our loop could otherwise become endless. + public static function setEnvVars(array $envVars): void + { + foreach ($envVars as $key => $value) { + putenv($key . '=' . $value); + } + } + + public static function unsetEnvVars(array $envVars): void + { + foreach ($envVars as $key => $value) { + if (is_int($key)) { + putenv($value); + } else { + putenv($key); + } + } + } + + private function setIo(): void + { + $this->logger = new ConsoleLogger($this->output); + $this->localMachineHelper = new LocalMachineHelper($this->input, $this->output, $this->logger); + // TTY should never be used for tests. + $this->localMachineHelper->setIsTty(false); + $this->sshHelper = new SshHelper($this->output, $this->localMachineHelper, $this->logger); + } + + protected function getResourceFromSpec(mixed $path, mixed $method): mixed + { + $acquiaCloudSpec = $this->getCloudApiSpec(); + return $acquiaCloudSpec['paths'][$path][$method]; + } + + /** + * Returns a mock response from acquia-spec.yaml. + * + * This assumes you want a JSON or HTML response. If you want something less + * common (i.e. an octet-stream for file downloads), don't use this method. + * + * @param $path + * @param $method + * @param $httpCode + * @see CXAPI-7208 */ - $attempts = 0; - do { - $path = sprintf('%s%s%s%s', $dir, DIRECTORY_SEPARATOR, 'tmp_', random_int(100000, mt_getrandmax())); - } while ( - !mkdir($path, 0700) && - $attempts++ < 10 - ); - - return $path; - } - - public static function setEnvVars(array $envVars): void { - foreach ($envVars as $key => $value) { - putenv($key . '=' . $value); - } - } - - public static function unsetEnvVars(array $envVars): void { - foreach ($envVars as $key => $value) { - if (is_int($key)) { - putenv($value); - } - else { - putenv($key); - } - } - } - - private function setIo(): void { - $this->logger = new ConsoleLogger($this->output); - $this->localMachineHelper = new LocalMachineHelper($this->input, $this->output, $this->logger); - // TTY should never be used for tests. - $this->localMachineHelper->setIsTty(FALSE); - $this->sshHelper = new SshHelper($this->output, $this->localMachineHelper, $this->logger); - } - - protected function getResourceFromSpec(mixed $path, mixed $method): mixed { - $acquiaCloudSpec = $this->getCloudApiSpec(); - return $acquiaCloudSpec['paths'][$path][$method]; - } - - /** - * Returns a mock response from acquia-spec.yaml. - * - * This assumes you want a JSON or HTML response. If you want something less - * common (i.e. an octet-stream for file downloads), don't use this method. - * - * @param $path - * @param $method - * @param $httpCode - * @see CXAPI-7208 - */ - public function getMockResponseFromSpec(mixed $path, mixed $method, mixed $httpCode): object { - $endpoint = $this->getResourceFromSpec($path, $method); - $response = $endpoint['responses'][$httpCode]; - if (array_key_exists('application/hal+json', $response['content'])) { - $content = $response['content']['application/hal+json']; - } - else { - $content = $response['content']['application/json']; - } - - if (array_key_exists('example', $content)) { - $responseBody = json_encode($content['example'], JSON_THROW_ON_ERROR); - } - elseif (array_key_exists('examples', $content)) { - $responseBody = json_encode($content['examples'], JSON_THROW_ON_ERROR); - } - elseif (array_key_exists('schema', $content) - && array_key_exists('$ref', $content['schema'])) { - $ref = $content['schema']['$ref']; - $paramKey = str_replace('#/components/schemas/', '', $ref); - $spec = $this->getCloudApiSpec(); - return (object) $spec['components']['schemas'][$paramKey]['properties']; - } - else { - return (object) []; - } - - return json_decode($responseBody, FALSE, 512, JSON_THROW_ON_ERROR); - } - - /** - * @return array - */ - protected function getPathMethodCodeFromSpec(string $operationId): array { - $acquiaCloudSpec = $this->getCloudApiSpec(); - foreach ($acquiaCloudSpec['paths'] as $path => $methodEndpoint) { - foreach ($methodEndpoint as $method => $endpoint) { - if ($endpoint['operationId'] === $operationId) { - foreach ($endpoint['responses'] as $code => $response) { - if ($code >= 200 && $code < 300) { - return [$path, $method, $code]; + public function getMockResponseFromSpec(mixed $path, mixed $method, mixed $httpCode): object + { + $endpoint = $this->getResourceFromSpec($path, $method); + $response = $endpoint['responses'][$httpCode]; + if (array_key_exists('application/hal+json', $response['content'])) { + $content = $response['content']['application/hal+json']; + } else { + $content = $response['content']['application/json']; + } + + if (array_key_exists('example', $content)) { + $responseBody = json_encode($content['example'], JSON_THROW_ON_ERROR); + } elseif (array_key_exists('examples', $content)) { + $responseBody = json_encode($content['examples'], JSON_THROW_ON_ERROR); + } elseif ( + array_key_exists('schema', $content) + && array_key_exists('$ref', $content['schema']) + ) { + $ref = $content['schema']['$ref']; + $paramKey = str_replace('#/components/schemas/', '', $ref); + $spec = $this->getCloudApiSpec(); + return (object) $spec['components']['schemas'][$paramKey]['properties']; + } else { + return (object) []; + } + + return json_decode($responseBody, false, 512, JSON_THROW_ON_ERROR); + } + + /** + * @return array + */ + protected function getPathMethodCodeFromSpec(string $operationId): array + { + $acquiaCloudSpec = $this->getCloudApiSpec(); + foreach ($acquiaCloudSpec['paths'] as $path => $methodEndpoint) { + foreach ($methodEndpoint as $method => $endpoint) { + if ($endpoint['operationId'] === $operationId) { + foreach ($endpoint['responses'] as $code => $response) { + if ($code >= 200 && $code < 300) { + return [$path, $method, $code]; + } + } + } } - } } - } - } - throw new \Exception('operationId not found'); - } - - /** - * Build and return a command with common dependencies. - * - * All commands inherit from a common base and use the same constructor with a - * bunch of dependencies injected. It would be tedious for every command test - * to inject every dependency as part of createCommand(). They can use this - * instead. - */ - protected function injectCommand(string $commandName): Command { - return new $commandName( - $this->localMachineHelper, - $this->datastoreCloud, - $this->datastoreAcli, - $this->cloudCredentials, - $this->telemetryHelper, - $this->acliRepoRoot, - $this->clientServiceProphecy->reveal(), - $this->sshHelper, - $this->sshDir, - $this->logger, - ); - } - - public function getMockRequestBodyFromSpec(string $path, string $method = 'post'): mixed { - $endpoint = $this->getResourceFromSpec($path, $method); - if (array_key_exists('application/json', $endpoint['requestBody']['content'])) { - return $endpoint['requestBody']['content']['application/json']['example']; - } - - return $endpoint['requestBody']['content']['application/hal+json']['example']; - } - - protected function getCloudApiSpec(): mixed { - // We cache the yaml file because it's 20k+ lines and takes FOREVER - // to parse when xDebug is enabled. - $acquiaCloudSpecFile = $this->apiSpecFixtureFilePath; - $acquiaCloudSpecFileChecksum = md5_file($acquiaCloudSpecFile); - - $cacheKey = basename($acquiaCloudSpecFile); - $cache = new PhpArrayAdapter(__DIR__ . '/../../../var/cache/' . $cacheKey . '.cache', new FilesystemAdapter()); - $isCommandCacheValid = $this->isApiSpecCacheValid($cache, $cacheKey, $acquiaCloudSpecFileChecksum); - $apiSpecCacheItem = $cache->getItem($cacheKey); - if ($isCommandCacheValid && $apiSpecCacheItem->isHit()) { - return $apiSpecCacheItem->get(); - } - $apiSpec = json_decode(file_get_contents($acquiaCloudSpecFile), TRUE); - $this->saveApiSpecCacheItems($cache, $acquiaCloudSpecFileChecksum, $apiSpecCacheItem, $apiSpec); - - return $apiSpec; - } - - private function isApiSpecCacheValid(PhpArrayAdapter $cache, mixed $cacheKey, string $acquiaCloudSpecFileChecksum): bool { - $apiSpecChecksumItem = $cache->getItem($cacheKey . '.checksum'); - // If there's an invalid entry OR there's no entry, return false. - return !(!$apiSpecChecksumItem->isHit() || ($apiSpecChecksumItem->isHit() + throw new \Exception('operationId not found'); + } + + /** + * Build and return a command with common dependencies. + * + * All commands inherit from a common base and use the same constructor with a + * bunch of dependencies injected. It would be tedious for every command test + * to inject every dependency as part of createCommand(). They can use this + * instead. + */ + protected function injectCommand(string $commandName): Command + { + return new $commandName( + $this->localMachineHelper, + $this->datastoreCloud, + $this->datastoreAcli, + $this->cloudCredentials, + $this->telemetryHelper, + $this->acliRepoRoot, + $this->clientServiceProphecy->reveal(), + $this->sshHelper, + $this->sshDir, + $this->logger, + ); + } + + public function getMockRequestBodyFromSpec(string $path, string $method = 'post'): mixed + { + $endpoint = $this->getResourceFromSpec($path, $method); + if (array_key_exists('application/json', $endpoint['requestBody']['content'])) { + return $endpoint['requestBody']['content']['application/json']['example']; + } + + return $endpoint['requestBody']['content']['application/hal+json']['example']; + } + + protected function getCloudApiSpec(): mixed + { + // We cache the yaml file because it's 20k+ lines and takes FOREVER + // to parse when xDebug is enabled. + $acquiaCloudSpecFile = $this->apiSpecFixtureFilePath; + $acquiaCloudSpecFileChecksum = md5_file($acquiaCloudSpecFile); + + $cacheKey = basename($acquiaCloudSpecFile); + $cache = new PhpArrayAdapter(__DIR__ . '/../../../var/cache/' . $cacheKey . '.cache', new FilesystemAdapter()); + $isCommandCacheValid = $this->isApiSpecCacheValid($cache, $cacheKey, $acquiaCloudSpecFileChecksum); + $apiSpecCacheItem = $cache->getItem($cacheKey); + if ($isCommandCacheValid && $apiSpecCacheItem->isHit()) { + return $apiSpecCacheItem->get(); + } + $apiSpec = json_decode(file_get_contents($acquiaCloudSpecFile), true); + $this->saveApiSpecCacheItems($cache, $acquiaCloudSpecFileChecksum, $apiSpecCacheItem, $apiSpec); + + return $apiSpec; + } + + private function isApiSpecCacheValid(PhpArrayAdapter $cache, mixed $cacheKey, string $acquiaCloudSpecFileChecksum): bool + { + $apiSpecChecksumItem = $cache->getItem($cacheKey . '.checksum'); + // If there's an invalid entry OR there's no entry, return false. + return !(!$apiSpecChecksumItem->isHit() || ($apiSpecChecksumItem->isHit() && $apiSpecChecksumItem->get() !== $acquiaCloudSpecFileChecksum)); - } - - private function saveApiSpecCacheItems( - PhpArrayAdapter $cache, - string $acquiaCloudSpecFileChecksum, - CacheItem $apiSpecCacheItem, - mixed $apiSpec - ): void { - $apiSpecChecksumItem = $cache->getItem('api_spec.checksum'); - $apiSpecChecksumItem->set($acquiaCloudSpecFileChecksum); - $cache->save($apiSpecChecksumItem); - $apiSpecCacheItem->set($apiSpec); - $cache->save($apiSpecCacheItem); - } - - protected function createLocalSshKey(mixed $contents): string { - $privateKeyFilepath = $this->fs->tempnam($this->sshDir, 'acli'); - $this->fs->touch($privateKeyFilepath); - $publicKeyFilepath = $privateKeyFilepath . '.pub'; - $this->fs->dumpFile($publicKeyFilepath, $contents); - - return $publicKeyFilepath; - } - - protected function createMockConfigFiles(): void { - $this->createMockCloudConfigFile(); - - $defaultValues = []; - $acliConfig = array_merge($defaultValues, $this->acliConfig); - $contents = json_encode($acliConfig, JSON_THROW_ON_ERROR); - $filepath = $this->acliConfigFilepath; - $this->fs->dumpFile($filepath, $contents); - } - - protected function createMockCloudConfigFile(mixed $defaultValues = []): void { - if (!$defaultValues) { - $defaultValues = [ - 'acli_key' => $this->key, - 'keys' => [ - (string) ($this->key) => [ + } + + private function saveApiSpecCacheItems( + PhpArrayAdapter $cache, + string $acquiaCloudSpecFileChecksum, + CacheItem $apiSpecCacheItem, + mixed $apiSpec + ): void { + $apiSpecChecksumItem = $cache->getItem('api_spec.checksum'); + $apiSpecChecksumItem->set($acquiaCloudSpecFileChecksum); + $cache->save($apiSpecChecksumItem); + $apiSpecCacheItem->set($apiSpec); + $cache->save($apiSpecCacheItem); + } + + protected function createLocalSshKey(mixed $contents): string + { + $privateKeyFilepath = $this->fs->tempnam($this->sshDir, 'acli'); + $this->fs->touch($privateKeyFilepath); + $publicKeyFilepath = $privateKeyFilepath . '.pub'; + $this->fs->dumpFile($publicKeyFilepath, $contents); + + return $publicKeyFilepath; + } + + protected function createMockConfigFiles(): void + { + $this->createMockCloudConfigFile(); + + $defaultValues = []; + $acliConfig = array_merge($defaultValues, $this->acliConfig); + $contents = json_encode($acliConfig, JSON_THROW_ON_ERROR); + $filepath = $this->acliConfigFilepath; + $this->fs->dumpFile($filepath, $contents); + } + + protected function createMockCloudConfigFile(mixed $defaultValues = []): void + { + if (!$defaultValues) { + $defaultValues = [ + 'acli_key' => $this->key, + 'keys' => [ + (string) ($this->key) => [ 'label' => 'Test Key', 'secret' => $this->secret, 'uuid' => $this->key, - ], - ], - DataStoreContract::SEND_TELEMETRY => FALSE, - ]; - } - $cloudConfig = array_merge($defaultValues, $this->cloudConfig); - $contents = json_encode($cloudConfig, JSON_THROW_ON_ERROR); - $filepath = $this->cloudConfigFilepath; - $this->fs->dumpFile($filepath, $contents); - } - - protected function createMockAcliConfigFile(string $cloudAppUuid): void { - $this->datastoreAcli->set('cloud_app_uuid', $cloudAppUuid); - } - - /** - * This is the preferred generic way of mocking requests and responses. We still maintain a lot of boilerplate mocking methods for legacy reasons. - * - * Auto-completion and return type inferencing is provided by .phpstorm.meta.php. - */ - protected function mockRequest(string $operationId, string|array|null $params = NULL, ?array $body = NULL, ?string $exampleResponse = NULL, Closure $tamper = NULL): object|array { - if (is_string($params)) { - $params = [$params]; - } - else if (is_null($params)) { - $params = []; - } - [$path, $method, $code] = $this->getPathMethodCodeFromSpec($operationId); - if (count($params) !== substr_count($path, '{')) { - throw new RuntimeException('Invalid number of parameters'); - } - $response = $this->getMockResponseFromSpec($path, $method, $code); - - // This is a set of example responses. - if (isset($exampleResponse) && property_exists($response, $exampleResponse)) { - $response = $response->$exampleResponse->value; - } - // This has multiple responses. - if (property_exists($response, '_embedded') && property_exists($response->_embedded, 'items')) { - $response = $response->_embedded->items; - } - if (isset($tamper)) { - $tamper($response); - } - foreach ($params as $param) { - $path = preg_replace('/\{\w*}/', $param, $path, 1); - } - $this->clientProphecy->request($method, $path, $body) - ->willReturn($response) - ->shouldBeCalled(); - return $response; - } - - /** - * @param int $count The number of applications to return. Use this to simulate query filters. - */ - public function mockApplicationsRequest(int $count = 2, bool $unique = TRUE): object { - // Request for applications. - $applicationsResponse = $this->getMockResponseFromSpec('/applications', - 'get', '200'); - $applicationsResponse = $this->filterApplicationsResponse($applicationsResponse, $count, $unique); - $this->clientProphecy->request('get', '/applications') - ->willReturn($applicationsResponse->{'_embedded'}->items) - ->shouldBeCalled(); - return $applicationsResponse; - } - - public function mockUnauthorizedRequest(): void { - $response = [ - 'error' => 'invalid_client', - 'error_description' => 'Client credentials were not found in the headers or body', - ]; - $this->clientProphecy->request('get', Argument::type('string')) - ->willThrow(new IdentityProviderException($response['error'], 0, $response)); - } - - public function mockApiError(): void { - $response = (object) [ - 'error' => 'some error', - 'message' => 'some error', - ]; - $this->clientProphecy->request('get', Argument::type('string')) - ->willThrow(new ApiErrorException($response, $response->message)); - } - - public function mockNoAvailableIdes(): void { - $response = (object) [ - 'error' => "There are no available Cloud IDEs for this application.\n", - 'message' => "There are no available Cloud IDEs for this application.\n", - ]; - $this->clientProphecy->request('get', Argument::type('string')) - ->willThrow(new ApiErrorException($response, $response->message)); - } - - protected function mockApplicationRequest(): object { - $applicationsResponse = $this->getMockResponseFromSpec('/applications', - 'get', '200'); - $applicationResponse = $applicationsResponse->{'_embedded'}->items[0]; - $this->clientProphecy->request('get', - '/applications/' . $applicationsResponse->{'_embedded'}->items[0]->uuid) - ->willReturn($applicationResponse) - ->shouldBeCalled(); - - return $applicationResponse; - } - - protected function mockPermissionsRequest(mixed $applicationResponse, mixed $perms = TRUE): object { - $permissionsResponse = $this->getMockResponseFromSpec("/applications/{applicationUuid}/permissions", - 'get', '200'); - if (!$perms) { - $deletePerms = [ - 'add ssh key to git', - 'add ssh key to non-prod', - 'add ssh key to prod', - ]; - foreach ($permissionsResponse->_embedded->items as $index => $item) { - if (in_array($item->name, $deletePerms, TRUE)) { - unset($permissionsResponse->_embedded->items[$index]); + ], + ], + DataStoreContract::SEND_TELEMETRY => false, + ]; } - } - } - $this->clientProphecy->request('get', - '/applications/' . $applicationResponse->uuid . '/permissions') - ->willReturn($permissionsResponse->_embedded->items) - ->shouldBeCalled(); - - return $permissionsResponse; - } - - public function mockEnvironmentsRequest( - object $applicationsResponse - ): object { - $response = $this->getMockEnvironmentsResponse(); - $this->clientProphecy->request('get', - "/applications/{$applicationsResponse->{'_embedded'}->items[0]->uuid}/environments") - ->willReturn($response->_embedded->items) - ->shouldBeCalled(); - - return $response; - } - - protected function getMockEnvironmentResponse(string $method = 'get', string $httpCode = '200'): object { - return $this->getMockResponseFromSpec('/environments/{environmentId}', - $method, $httpCode); - } - - protected function getMockEnvironmentsResponse(): object { - return $this->getMockResponseFromSpec('/applications/{applicationUuid}/environments', - 'get', 200); - } - - /** - * @return array - */ - protected function mockListSshKeysRequest(): array { - return $this->mockRequest('getAccountSshKeys'); - } - - protected function mockListSshKeysRequestWithIdeKey(string $ideLabel, string $ideUuid): object { - $mockBody = $this->getMockResponseFromSpec('/account/ssh-keys', 'get', '200'); - $mockBody->{'_embedded'}->items[0]->label = preg_replace('/\W/', '', 'IDE_' . $ideLabel . '_' . $ideUuid); - $this->clientProphecy->request('get', '/account/ssh-keys') - ->willReturn($mockBody->{'_embedded'}->items) - ->shouldBeCalled(); - return $mockBody; - } - - protected function mockGenerateSshKey(ObjectProphecy|LocalMachineHelper $localMachineHelper, ?string $keyContents = NULL): void { - $keyContents = $keyContents ?: 'thekey!'; - $publicKeyPath = 'id_rsa.pub'; - $process = $this->prophet->prophesize(Process::class); - $process->isSuccessful()->willReturn(TRUE); - $process->getOutput()->willReturn($keyContents); - $localMachineHelper->checkRequiredBinariesExist(["ssh-keygen"])->shouldBeCalled(); - $localMachineHelper->execute(Argument::withEntry(0, 'ssh-keygen'), NULL, NULL, FALSE) - ->willReturn($process->reveal()) - ->shouldBeCalled(); - $localMachineHelper->readFile($publicKeyPath)->willReturn($keyContents); - $localMachineHelper->readFile(Argument::containingString('id_rsa'))->willReturn($keyContents); - } - - protected function mockAddSshKeyToAgent(mixed $localMachineHelper, mixed $fileSystem): void { - $process = $this->prophet->prophesize(Process::class); - $process->isSuccessful()->willReturn(TRUE); - $localMachineHelper->executeFromCmd(Argument::containingString('SSH_PASS'), NULL, NULL, FALSE)->willReturn($process->reveal()); - $fileSystem->tempnam(Argument::type('string'), 'acli')->willReturn('something'); - $fileSystem->chmod('something', 493)->shouldBeCalled(); - $fileSystem->remove('something')->shouldBeCalled(); - $localMachineHelper->writeFile('something', Argument::type('string'))->shouldBeCalled(); - } - - protected function mockSshAgentList(ObjectProphecy|LocalMachineHelper $localMachineHelper, bool $success = FALSE): void { - $process = $this->prophet->prophesize(Process::class); - $process->isSuccessful()->willReturn($success); - $process->getExitCode()->willReturn($success ? 0 : 1); - $process->getOutput()->willReturn('thekey!'); - $localMachineHelper->getLocalFilepath($this->passphraseFilepath) - ->willReturn('/tmp/.passphrase'); - $localMachineHelper->execute([ - 'ssh-add', - '-L', - ], NULL, NULL, FALSE)->shouldBeCalled()->willReturn($process->reveal()); - } - - protected function mockDeleteSshKeyRequest(string $keyUuid): void { - $this->mockRequest('deleteAccountSshKey', $keyUuid, NULL, 'Removed key'); - } - - protected function mockListSshKeyRequestWithUploadedKey( - mixed $mockRequestArgs - ): void { - $mockBody = $this->getMockResponseFromSpec('/account/ssh-keys', 'get', - '200'); - $newItem = array_merge((array) $mockBody->_embedded->items[2], $mockRequestArgs); - $mockBody->_embedded->items[3] = (object) $newItem; - $this->clientProphecy->request('get', '/account/ssh-keys') - ->willReturn($mockBody->{'_embedded'}->items) - ->shouldBeCalled(); - } - - protected function mockStartPhp(ObjectProphecy|LocalMachineHelper $localMachineHelper): ObjectProphecy { - $process = $this->prophet->prophesize(Process::class); - $process->isSuccessful()->willReturn(TRUE); - $process->getExitCode()->willReturn(0); - $localMachineHelper->execute([ - 'supervisorctl', - 'start', - 'php-fpm', - ], NULL, NULL, FALSE)->willReturn($process->reveal())->shouldBeCalled(); - return $process; - } - - protected function mockStopPhp(ObjectProphecy|LocalMachineHelper $localMachineHelper): ObjectProphecy { - $process = $this->prophet->prophesize(Process::class); - $process->isSuccessful()->willReturn(TRUE); - $process->getExitCode()->willReturn(0); - $localMachineHelper->execute([ - 'supervisorctl', - 'stop', - 'php-fpm', - ], NULL, NULL, FALSE)->willReturn($process->reveal())->shouldBeCalled(); - return $process; - } - - protected function mockRestartPhp(ObjectProphecy|LocalMachineHelper $localMachineHelper): ObjectProphecy { - $process = $this->prophet->prophesize(Process::class); - $process->isSuccessful()->willReturn(TRUE); - $process->getExitCode()->willReturn(0); - $localMachineHelper->execute([ - 'supervisorctl', - 'restart', - 'php-fpm', - ], NULL, NULL, FALSE)->willReturn($process->reveal())->shouldBeCalled(); - return $process; - } - - /** - * @return \Prophecy\Prophecy\ObjectProphecy|\Symfony\Component\Filesystem\Filesystem - */ - protected function mockGetFilesystem(ObjectProphecy|LocalMachineHelper $localMachineHelper): ObjectProphecy|Filesystem { - $localMachineHelper->getFilesystem()->willReturn($this->fs)->shouldBeCalled(); - - return $this->fs; - } - - protected function removeMockConfigFiles(): void { - $this->removeMockCloudConfigFile(); - $this->removeMockAcliConfigFile(); - } - - protected function removeMockCloudConfigFile(): void { - $this->fs->remove($this->cloudConfigFilepath); - } - - protected function removeMockAcliConfigFile(): void { - $this->fs->remove($this->acliConfigFilepath); - } - - public function mockGuzzleClientForUpdate(array $releases): ObjectProphecy { - $stream = $this->prophet->prophesize(StreamInterface::class); - $stream->getContents()->willReturn(json_encode($releases)); - $response = $this->prophet->prophesize(Response::class); - $response->getBody()->willReturn($stream->reveal()); - $guzzleClient = $this->prophet->prophesize(\GuzzleHttp\Client::class); - $guzzleClient->request('GET', Argument::containingString('https://api.github.com/repos'), Argument::type('array')) - ->willReturn($response->reveal()); - - $stream = $this->prophet->prophesize(StreamInterface::class); - $pharContents = file_get_contents(Path::join($this->fixtureDir, 'test.phar')); - $stream->getContents()->willReturn($pharContents); - $response = $this->prophet->prophesize(Response::class); - $response->getBody()->willReturn($stream->reveal()); - $guzzleClient->request('GET', 'https://github.com/acquia/cli/releases/download/v1.0.0-beta3/acli.phar', - Argument::type('array'))->willReturn($response->reveal()); - - return $guzzleClient; - } - - protected function setClientProphecies(): void { - $this->clientProphecy = $this->prophet->prophesize(Client::class); - $this->clientProphecy->addOption('headers', ['User-Agent' => 'acli/UNKNOWN']); - $this->clientProphecy->addOption('debug', Argument::type(OutputInterface::class)); - $this->clientServiceProphecy = $this->prophet->prophesize(ClientService::class); - $this->clientServiceProphecy->getClient() - ->willReturn($this->clientProphecy->reveal()); - $this->clientServiceProphecy->isMachineAuthenticated() - ->willReturn(TRUE); - } - - protected function createDataStores(): void { - $this->datastoreAcli = new AcquiaCliDatastore($this->localMachineHelper, new AcquiaCliConfig(), $this->acliConfigFilepath); - $this->datastoreCloud = new CloudDataStore($this->localMachineHelper, new CloudDataConfig(), $this->cloudConfigFilepath); - } + $cloudConfig = array_merge($defaultValues, $this->cloudConfig); + $contents = json_encode($cloudConfig, JSON_THROW_ON_ERROR); + $filepath = $this->cloudConfigFilepath; + $this->fs->dumpFile($filepath, $contents); + } + protected function createMockAcliConfigFile(string $cloudAppUuid): void + { + $this->datastoreAcli->set('cloud_app_uuid', $cloudAppUuid); + } + + /** + * This is the preferred generic way of mocking requests and responses. We still maintain a lot of boilerplate mocking methods for legacy reasons. + * + * Auto-completion and return type inferencing is provided by .phpstorm.meta.php. + */ + protected function mockRequest(string $operationId, string|array|null $params = null, ?array $body = null, ?string $exampleResponse = null, Closure $tamper = null): object|array + { + if (is_string($params)) { + $params = [$params]; + } elseif (is_null($params)) { + $params = []; + } + [$path, $method, $code] = $this->getPathMethodCodeFromSpec($operationId); + if (count($params) !== substr_count($path, '{')) { + throw new RuntimeException('Invalid number of parameters'); + } + $response = $this->getMockResponseFromSpec($path, $method, $code); + + // This is a set of example responses. + if (isset($exampleResponse) && property_exists($response, $exampleResponse)) { + $response = $response->$exampleResponse->value; + } + // This has multiple responses. + if (property_exists($response, '_embedded') && property_exists($response->_embedded, 'items')) { + $response = $response->_embedded->items; + } + if (isset($tamper)) { + $tamper($response); + } + foreach ($params as $param) { + $path = preg_replace('/\{\w*}/', $param, $path, 1); + } + $this->clientProphecy->request($method, $path, $body) + ->willReturn($response) + ->shouldBeCalled(); + return $response; + } + + /** + * @param int $count The number of applications to return. Use this to simulate query filters. + */ + public function mockApplicationsRequest(int $count = 2, bool $unique = true): object + { + // Request for applications. + $applicationsResponse = $this->getMockResponseFromSpec( + '/applications', + 'get', + '200' + ); + $applicationsResponse = $this->filterApplicationsResponse($applicationsResponse, $count, $unique); + $this->clientProphecy->request('get', '/applications') + ->willReturn($applicationsResponse->{'_embedded'}->items) + ->shouldBeCalled(); + return $applicationsResponse; + } + + public function mockUnauthorizedRequest(): void + { + $response = [ + 'error' => 'invalid_client', + 'error_description' => 'Client credentials were not found in the headers or body', + ]; + $this->clientProphecy->request('get', Argument::type('string')) + ->willThrow(new IdentityProviderException($response['error'], 0, $response)); + } + + public function mockApiError(): void + { + $response = (object) [ + 'error' => 'some error', + 'message' => 'some error', + ]; + $this->clientProphecy->request('get', Argument::type('string')) + ->willThrow(new ApiErrorException($response, $response->message)); + } + + public function mockNoAvailableIdes(): void + { + $response = (object) [ + 'error' => "There are no available Cloud IDEs for this application.\n", + 'message' => "There are no available Cloud IDEs for this application.\n", + ]; + $this->clientProphecy->request('get', Argument::type('string')) + ->willThrow(new ApiErrorException($response, $response->message)); + } + + protected function mockApplicationRequest(): object + { + $applicationsResponse = $this->getMockResponseFromSpec( + '/applications', + 'get', + '200' + ); + $applicationResponse = $applicationsResponse->{'_embedded'}->items[0]; + $this->clientProphecy->request( + 'get', + '/applications/' . $applicationsResponse->{'_embedded'}->items[0]->uuid + ) + ->willReturn($applicationResponse) + ->shouldBeCalled(); + + return $applicationResponse; + } + + protected function mockPermissionsRequest(mixed $applicationResponse, mixed $perms = true): object + { + $permissionsResponse = $this->getMockResponseFromSpec( + "/applications/{applicationUuid}/permissions", + 'get', + '200' + ); + if (!$perms) { + $deletePerms = [ + 'add ssh key to git', + 'add ssh key to non-prod', + 'add ssh key to prod', + ]; + foreach ($permissionsResponse->_embedded->items as $index => $item) { + if (in_array($item->name, $deletePerms, true)) { + unset($permissionsResponse->_embedded->items[$index]); + } + } + } + $this->clientProphecy->request( + 'get', + '/applications/' . $applicationResponse->uuid . '/permissions' + ) + ->willReturn($permissionsResponse->_embedded->items) + ->shouldBeCalled(); + + return $permissionsResponse; + } + + public function mockEnvironmentsRequest( + object $applicationsResponse + ): object { + $response = $this->getMockEnvironmentsResponse(); + $this->clientProphecy->request( + 'get', + "/applications/{$applicationsResponse->{'_embedded'}->items[0]->uuid}/environments" + ) + ->willReturn($response->_embedded->items) + ->shouldBeCalled(); + + return $response; + } + + protected function getMockEnvironmentResponse(string $method = 'get', string $httpCode = '200'): object + { + return $this->getMockResponseFromSpec( + '/environments/{environmentId}', + $method, + $httpCode + ); + } + + protected function getMockEnvironmentsResponse(): object + { + return $this->getMockResponseFromSpec( + '/applications/{applicationUuid}/environments', + 'get', + 200 + ); + } + + /** + * @return array + */ + protected function mockListSshKeysRequest(): array + { + return $this->mockRequest('getAccountSshKeys'); + } + + protected function mockListSshKeysRequestWithIdeKey(string $ideLabel, string $ideUuid): object + { + $mockBody = $this->getMockResponseFromSpec('/account/ssh-keys', 'get', '200'); + $mockBody->{'_embedded'}->items[0]->label = preg_replace('/\W/', '', 'IDE_' . $ideLabel . '_' . $ideUuid); + $this->clientProphecy->request('get', '/account/ssh-keys') + ->willReturn($mockBody->{'_embedded'}->items) + ->shouldBeCalled(); + return $mockBody; + } + + protected function mockGenerateSshKey(ObjectProphecy|LocalMachineHelper $localMachineHelper, ?string $keyContents = null): void + { + $keyContents = $keyContents ?: 'thekey!'; + $publicKeyPath = 'id_rsa.pub'; + $process = $this->prophet->prophesize(Process::class); + $process->isSuccessful()->willReturn(true); + $process->getOutput()->willReturn($keyContents); + $localMachineHelper->checkRequiredBinariesExist(["ssh-keygen"])->shouldBeCalled(); + $localMachineHelper->execute(Argument::withEntry(0, 'ssh-keygen'), null, null, false) + ->willReturn($process->reveal()) + ->shouldBeCalled(); + $localMachineHelper->readFile($publicKeyPath)->willReturn($keyContents); + $localMachineHelper->readFile(Argument::containingString('id_rsa'))->willReturn($keyContents); + } + + protected function mockAddSshKeyToAgent(mixed $localMachineHelper, mixed $fileSystem): void + { + $process = $this->prophet->prophesize(Process::class); + $process->isSuccessful()->willReturn(true); + $localMachineHelper->executeFromCmd(Argument::containingString('SSH_PASS'), null, null, false)->willReturn($process->reveal()); + $fileSystem->tempnam(Argument::type('string'), 'acli')->willReturn('something'); + $fileSystem->chmod('something', 493)->shouldBeCalled(); + $fileSystem->remove('something')->shouldBeCalled(); + $localMachineHelper->writeFile('something', Argument::type('string'))->shouldBeCalled(); + } + + protected function mockSshAgentList(ObjectProphecy|LocalMachineHelper $localMachineHelper, bool $success = false): void + { + $process = $this->prophet->prophesize(Process::class); + $process->isSuccessful()->willReturn($success); + $process->getExitCode()->willReturn($success ? 0 : 1); + $process->getOutput()->willReturn('thekey!'); + $localMachineHelper->getLocalFilepath($this->passphraseFilepath) + ->willReturn('/tmp/.passphrase'); + $localMachineHelper->execute([ + 'ssh-add', + '-L', + ], null, null, false)->shouldBeCalled()->willReturn($process->reveal()); + } + + protected function mockDeleteSshKeyRequest(string $keyUuid): void + { + $this->mockRequest('deleteAccountSshKey', $keyUuid, null, 'Removed key'); + } + + protected function mockListSshKeyRequestWithUploadedKey( + mixed $mockRequestArgs + ): void { + $mockBody = $this->getMockResponseFromSpec( + '/account/ssh-keys', + 'get', + '200' + ); + $newItem = array_merge((array) $mockBody->_embedded->items[2], $mockRequestArgs); + $mockBody->_embedded->items[3] = (object) $newItem; + $this->clientProphecy->request('get', '/account/ssh-keys') + ->willReturn($mockBody->{'_embedded'}->items) + ->shouldBeCalled(); + } + + protected function mockStartPhp(ObjectProphecy|LocalMachineHelper $localMachineHelper): ObjectProphecy + { + $process = $this->prophet->prophesize(Process::class); + $process->isSuccessful()->willReturn(true); + $process->getExitCode()->willReturn(0); + $localMachineHelper->execute([ + 'supervisorctl', + 'start', + 'php-fpm', + ], null, null, false)->willReturn($process->reveal())->shouldBeCalled(); + return $process; + } + + protected function mockStopPhp(ObjectProphecy|LocalMachineHelper $localMachineHelper): ObjectProphecy + { + $process = $this->prophet->prophesize(Process::class); + $process->isSuccessful()->willReturn(true); + $process->getExitCode()->willReturn(0); + $localMachineHelper->execute([ + 'supervisorctl', + 'stop', + 'php-fpm', + ], null, null, false)->willReturn($process->reveal())->shouldBeCalled(); + return $process; + } + + protected function mockRestartPhp(ObjectProphecy|LocalMachineHelper $localMachineHelper): ObjectProphecy + { + $process = $this->prophet->prophesize(Process::class); + $process->isSuccessful()->willReturn(true); + $process->getExitCode()->willReturn(0); + $localMachineHelper->execute([ + 'supervisorctl', + 'restart', + 'php-fpm', + ], null, null, false)->willReturn($process->reveal())->shouldBeCalled(); + return $process; + } + + /** + * @return \Prophecy\Prophecy\ObjectProphecy|\Symfony\Component\Filesystem\Filesystem + */ + protected function mockGetFilesystem(ObjectProphecy|LocalMachineHelper $localMachineHelper): ObjectProphecy|Filesystem + { + $localMachineHelper->getFilesystem()->willReturn($this->fs)->shouldBeCalled(); + + return $this->fs; + } + + protected function removeMockConfigFiles(): void + { + $this->removeMockCloudConfigFile(); + $this->removeMockAcliConfigFile(); + } + + protected function removeMockCloudConfigFile(): void + { + $this->fs->remove($this->cloudConfigFilepath); + } + + protected function removeMockAcliConfigFile(): void + { + $this->fs->remove($this->acliConfigFilepath); + } + + public function mockGuzzleClientForUpdate(array $releases): ObjectProphecy + { + $stream = $this->prophet->prophesize(StreamInterface::class); + $stream->getContents()->willReturn(json_encode($releases)); + $response = $this->prophet->prophesize(Response::class); + $response->getBody()->willReturn($stream->reveal()); + $guzzleClient = $this->prophet->prophesize(\GuzzleHttp\Client::class); + $guzzleClient->request('GET', Argument::containingString('https://api.github.com/repos'), Argument::type('array')) + ->willReturn($response->reveal()); + + $stream = $this->prophet->prophesize(StreamInterface::class); + $pharContents = file_get_contents(Path::join($this->fixtureDir, 'test.phar')); + $stream->getContents()->willReturn($pharContents); + $response = $this->prophet->prophesize(Response::class); + $response->getBody()->willReturn($stream->reveal()); + $guzzleClient->request( + 'GET', + 'https://github.com/acquia/cli/releases/download/v1.0.0-beta3/acli.phar', + Argument::type('array') + )->willReturn($response->reveal()); + + return $guzzleClient; + } + + protected function setClientProphecies(): void + { + $this->clientProphecy = $this->prophet->prophesize(Client::class); + $this->clientProphecy->addOption('headers', ['User-Agent' => 'acli/UNKNOWN']); + $this->clientProphecy->addOption('debug', Argument::type(OutputInterface::class)); + $this->clientServiceProphecy = $this->prophet->prophesize(ClientService::class); + $this->clientServiceProphecy->getClient() + ->willReturn($this->clientProphecy->reveal()); + $this->clientServiceProphecy->isMachineAuthenticated() + ->willReturn(true); + } + + protected function createDataStores(): void + { + $this->datastoreAcli = new AcquiaCliDatastore($this->localMachineHelper, new AcquiaCliConfig(), $this->acliConfigFilepath); + $this->datastoreCloud = new CloudDataStore($this->localMachineHelper, new CloudDataConfig(), $this->cloudConfigFilepath); + } }