diff --git a/tests/phpunit/src/Commands/Acsf/AcsfApiCommandTest.php b/tests/phpunit/src/Commands/Acsf/AcsfApiCommandTest.php new file mode 100644 index 000000000..e4bc4f748 --- /dev/null +++ b/tests/phpunit/src/Commands/Acsf/AcsfApiCommandTest.php @@ -0,0 +1,154 @@ +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]); + } + $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 new file mode 100644 index 000000000..1ba32949a --- /dev/null +++ b/tests/phpunit/src/Commands/Acsf/AcsfAuthLoginCommandTest.php @@ -0,0 +1,140 @@ +cloudCredentials = new AcsfCredentials($this->datastoreCloud); + return $this->injectCommand(AuthAcsfLoginCommand::class); + } + + /** + * @return array + */ + public function providerTestAuthLoginCommand(): array { + return [ + // Data set 0. + [ + // $machineIsAuthenticated + 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, + ], + // No arguments, all interactive. + [], + // Output to assert. + 'Saved credentials', + ], + // Data set 1. + [ + // $machineIsAuthenticated + 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, + ], + // Output to assert. + 'Saved credentials', + // $config. + $this->getAcsfCredentialsFileContents(), + ], + // Data set 2. + [ + // $machineIsAuthenticated + TRUE, + // $inputs + [ + // Choose a factory to log in to. + $this->acsfCurrentFactoryUrl, + // Choose which user to log in as. + $this->acsfUsername, + ], + // Arguments. + [], + // Output to assert. + "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()); + + } + + 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 new file mode 100644 index 000000000..b02959a5a --- /dev/null +++ b/tests/phpunit/src/Commands/Acsf/AcsfAuthLogoutCommandTest.php @@ -0,0 +1,73 @@ +cloudCredentials = new AcsfCredentials($this->datastoreCloud); + return $this->injectCommand(AuthAcsfLogoutCommand::class); + } + + /** + * @return array + */ + public function providerTestAuthLogoutCommand(): array { + return [ + // Data set 0. + [ + // $machineIsAuthenticated + FALSE, + // $inputs + [], + ], + // Data set 1. + [ + // $machineIsAuthenticated + TRUE, + // $inputs + [ + // 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')); + } + +} diff --git a/tests/phpunit/src/Commands/Acsf/AcsfCommandTestBase.php b/tests/phpunit/src/Commands/Acsf/AcsfCommandTestBase.php new file mode 100644 index 000000000..90f76fb38 --- /dev/null +++ b/tests/phpunit/src/Commands/Acsf/AcsfCommandTestBase.php @@ -0,0 +1,50 @@ + + */ + 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, + ], + ], + ], + ], + DataStoreContract::SEND_TELEMETRY => FALSE, + ]; + } + +} diff --git a/tests/phpunit/src/Commands/Acsf/AcsfListCommandTest.php b/tests/phpunit/src/Commands/Acsf/AcsfListCommandTest.php new file mode 100644 index 000000000..caf262883 --- /dev/null +++ b/tests/phpunit/src/Commands/Acsf/AcsfListCommandTest.php @@ -0,0 +1,53 @@ +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 new file mode 100644 index 000000000..6cf4a8f5f --- /dev/null +++ b/tests/phpunit/src/Commands/Api/ApiBaseCommandTest.php @@ -0,0 +1,24 @@ +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 new file mode 100644 index 000000000..4c93a3da3 --- /dev/null +++ b/tests/phpunit/src/Commands/Api/ApiCommandTest.php @@ -0,0 +1,528 @@ +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([], [ + '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], []); + + } + + /** + * @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(); + } + $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(); + } + $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); + } + + /** + * @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']}" + ); + } + + $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"); + } + $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, + ], + ); + } + +} diff --git a/tests/phpunit/src/Commands/Api/ApiListCommandTest.php b/tests/phpunit/src/Commands/Api/ApiListCommandTest.php new file mode 100644 index 000000000..028aa8f50 --- /dev/null +++ b/tests/phpunit/src/Commands/Api/ApiListCommandTest.php @@ -0,0 +1,53 @@ +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 new file mode 100644 index 000000000..98f5bbd6e --- /dev/null +++ b/tests/phpunit/src/Commands/App/AppOpenCommandTest.php @@ -0,0 +1,45 @@ +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 new file mode 100644 index 000000000..a74eacdb5 --- /dev/null +++ b/tests/phpunit/src/Commands/App/AppVcsInfoTest.php @@ -0,0 +1,156 @@ +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(); + } + + $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 = <<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(); + } + + /** + * @covers ::getVersionConstraint + */ + public function testVersionConstraint(): void { + $this->expectException(\LogicException::class); + $this->sut->getVersionConstraint(); + } + + /** + * @covers ::hasModulesToInstall + */ + public function testHasModulesToInstall(): void { + $this->expectException(\LogicException::class); + $this->sut->hasModulesToInstall(); + } + + /** + * @covers ::getModulesToInstall + */ + public function testGetModulesToInstall(): void { + $this->expectException(\LogicException::class); + $this->sut->getModulesToInstall(); + } + + /** + * @covers ::hasPatches + */ + public function testHasPatches(): void { + $this->expectException(\LogicException::class); + $this->sut->hasPatches(); + } + + /** + * @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 new file mode 100644 index 000000000..cb8af7e21 --- /dev/null +++ b/tests/phpunit/src/Commands/App/From/ConfigurationTest.php @@ -0,0 +1,43 @@ +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 new file mode 100755 index 000000000..7e72e9eba --- /dev/null +++ b/tests/phpunit/src/Commands/App/From/DefinedRecommendationTest.php @@ -0,0 +1,179 @@ +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' => [ + ['package' => '', 'constraint' => ''], + new NoRecommendation(), + ], + 'key value does not match schema' => [ + ['package' => 42, 'constraint' => '', 'replaces' => ['name' => '']], + new NoRecommendation(), + ], + 'nested key value does not match schema' => [ + ['package' => '', 'constraint' => '', 'replaces' => ['name' => 42]], + new NoRecommendation(), + ], + 'invalid patches key' => [ + [ + 'package' => 'foo', + 'constraint' => '^1.42', + 'patches' => [ + 0 => 'https://example.com', + ], + 'replaces' => [ + 'name' => 'foo', + ], + ], + new NoRecommendation(), + ], + 'invalid patches key value' => [ + [ + 'package' => 'foo', + 'constraint' => '^1.42', + 'patches' => [ + 'A patch description' => TRUE, + ], + 'replaces' => [ + 'name' => 'foo', + ], + ], + new NoRecommendation(), + ], + 'missing replaces key, not universal by default' => [ + [ + 'package' => 'foo', + 'constraint' => '^1.42', + ], + new NoRecommendation(), + ], + 'missing replaces key, explicitly not universal' => [ + [ + 'universal' => FALSE, + 'package' => 'foo', + 'constraint' => '^1.42', + ], + new NoRecommendation(), + ], + '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' => [ + [ + 'universal' => TRUE, + 'package' => 'foo', + 'constraint' => '^1.42', + ], + 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', + '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', + ]), + ], + '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 + } + +} diff --git a/tests/phpunit/src/Commands/App/From/ProjectBuilderTest.php b/tests/phpunit/src/Commands/App/From/ProjectBuilderTest.php new file mode 100644 index 000000000..19f71f038 --- /dev/null +++ b/tests/phpunit/src/Commands/App/From/ProjectBuilderTest.php @@ -0,0 +1,73 @@ +assertSame($project_builder->buildProject(), $expected_project_definition); + } + + /** + * @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' => [], + ]), + json_encode([ + 'data' => [], + ]), + [ + 'installModules' => [], + 'filePaths' => [ + 'public' => 'sites/default/files', + 'private' => NULL, + ], + '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 new file mode 100644 index 000000000..f9b391982 --- /dev/null +++ b/tests/phpunit/src/Commands/App/From/RecommendationsTest.php @@ -0,0 +1,92 @@ +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' => [ + '{,}', + static::NO_RECOMMENDATIONS, + ], + 'empty configuration file' => [ + json_encode((object) []), + static::NO_RECOMMENDATIONS, + ], + 'unexpected recommendations value' => [ + json_encode(['data' => TRUE]), + static::NO_RECOMMENDATIONS, + ], + 'empty recommendations key' => [ + json_encode(['data' => []]), + static::NO_RECOMMENDATIONS, + ], + 'populated recommendations key with invalid item' => [ + json_encode(['recommendations' => [[]]]), + static::NO_RECOMMENDATIONS, + ], + 'populated recommendations key with valid item' => [ + json_encode([ + 'data' => [ + [ + 'package' => 'foo', + 'constraint' => '^1.42', + 'replaces' => [ + 'name' => 'foo', + ], + ], + ], + ]), + 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 new file mode 100644 index 000000000..fde9df632 --- /dev/null +++ b/tests/phpunit/src/Commands/App/From/TestRecommendation.php @@ -0,0 +1,69 @@ +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 new file mode 100644 index 000000000..7082a2eb1 --- /dev/null +++ b/tests/phpunit/src/Commands/App/From/TestSiteInspector.php @@ -0,0 +1,44 @@ +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 new file mode 100644 index 000000000..25604aaa7 --- /dev/null +++ b/tests/phpunit/src/Commands/App/LinkCommandTest.php @@ -0,0 +1,61 @@ +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 new file mode 100644 index 000000000..3190abfec --- /dev/null +++ b/tests/phpunit/src/Commands/App/LogTailCommandTest.php @@ -0,0 +1,133 @@ +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() + ); + } + + /** + * @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); + } + + 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); + } + + 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 new file mode 100644 index 000000000..cc2171f3e --- /dev/null +++ b/tests/phpunit/src/Commands/App/NewCommandTest.php @@ -0,0 +1,214 @@ +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 new file mode 100644 index 000000000..826dd7ef5 --- /dev/null +++ b/tests/phpunit/src/Commands/App/NewFromDrupal7CommandTest.php @@ -0,0 +1,199 @@ +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", + ]; + } + 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))); + } + + $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 new file mode 100644 index 000000000..03525a3cd --- /dev/null +++ b/tests/phpunit/src/Commands/App/TaskWaitCommandTest.php @@ -0,0 +1,160 @@ +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()); + } + + 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 [ + [ + '1bd3487e-71d1-4fca-a2d9-5f969b3d35c1', + ], + [ + 'https://cloud.acquia.com/api/notifications/1bd3487e-71d1-4fca-a2d9-5f969b3d35c1', + ], + [ + <<<'EOT' +{ + "message": "Caches are being cleared.", + "_links": { + "self": { + "href": "https://cloud.acquia.com/api/environments/12-d314739e-296f-11e9-b210-d663bd873d93/domains/example.com/actions/clear-caches" + }, + "notification": { + "href": "https://cloud.acquia.com/api/notifications/1bd3487e-71d1-4fca-a2d9-5f969b3d35c1" + } + } +} +EOT, + ], + [ + '"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' => '{}']); + + // 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']); + + // Assert. + } + + /** + * @dataProvider providerTestTaskWaitCommandWithInvalidJson + */ + public function testTaskWaitCommandWithInvalidJson(string $notification): void { + $this->expectException(AcquiaCliException::class); + $this->executeCommand([ + 'notification-uuid' => $notification, + ]); + } + + /** + * @return string[] + */ + public function providerTestTaskWaitCommandWithInvalidJson(): array { + return [ + [ + <<<'EOT' +{ + "message": "Caches are being cleared.", + "_links": { + "self": { + "href": "https://cloud.acquia.com/api/environments/12-d314739e-296f-11e9-b210-d663bd873d93/domains/example.com/actions/clear-caches", + "invalid": { + "too-deep": "5" + } + }, + "notification": { + "href": "https://cloud.acquia.com/api/notifications/1bd3487e-71d1-4fca-a2d9-5f969b3d35c1" + } + } +} +EOT, + ], + [ + <<<'EOT' +{ + "message": "Caches are being cleared.", + "_links": { + "self": { + "href": "https://cloud.acquia.com/api/environments/12-d314739e-296f-11e9-b210-d663bd873d93/domains/example.com/actions/clear-caches" + } + } +} +EOT, + ], + [ + '"11bd3487e-71d1-4fca-a2d9-5f969b3d35c1"', + ], + ]; + } + +} diff --git a/tests/phpunit/src/Commands/App/UnlinkCommandTest.php b/tests/phpunit/src/Commands/App/UnlinkCommandTest.php new file mode 100644 index 000000000..a36100fa6 --- /dev/null +++ b/tests/phpunit/src/Commands/App/UnlinkCommandTest.php @@ -0,0 +1,46 @@ +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 new file mode 100644 index 000000000..e88c7b7c3 --- /dev/null +++ b/tests/phpunit/src/Commands/Archive/ArchiveExporterCommandTest.php @@ -0,0 +1,73 @@ +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 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()); + + $inputs = [ + // ... Do you want to continue? (yes/no) [yes] + 'y', + ]; + $this->executeCommand([ + 'destination-dir' => $destinationDir, + ], $inputs); + + $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; + } + +} diff --git a/tests/phpunit/src/Commands/Auth/AuthLoginCommandTest.php b/tests/phpunit/src/Commands/Auth/AuthLoginCommandTest.php new file mode 100644 index 000000000..d1638e777 --- /dev/null +++ b/tests/phpunit/src/Commands/Auth/AuthLoginCommandTest.php @@ -0,0 +1,131 @@ +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 + [ + [], + ['--key' => 'shorty' , '--secret' => $this->secret], + ]; + 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' => [ + 'key1' => [ + 'label' => 'foo', + 'secret' => 'foo', + 'uuid' => 'foo', + ], + ], + ]; + $this->fs->dumpFile($this->cloudConfigFilepath, json_encode($data)); + $this->createDataStores(); + $this->command = $this->createCommand(); + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage("Invalid key in datastore at $this->cloudConfigFilepath"); + $this->executeCommand(); + } + + 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 new file mode 100644 index 000000000..c034dfc51 --- /dev/null +++ b/tests/phpunit/src/Commands/Auth/AuthLogoutCommandTest.php @@ -0,0 +1,56 @@ +injectCommand(AuthLogoutCommand::class); + } + + public function testAuthLogoutCommand(): void { + $this->executeCommand(); + $output = $this->getDisplay(); + $this->assertFileExists($this->cloudConfigFilepath); + $config = new CloudDataStore($this->localMachineHelper, new CloudDataConfig(), $this->cloudConfigFilepath); + $this->assertFalse($config->exists('acli_key')); + $this->assertNotEmpty($config->get('keys')); + $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', + ], + ], + ]; + $this->fs->dumpFile($this->cloudConfigFilepath, json_encode($data)); + $this->createDataStores(); + $this->command = $this->createCommand(); + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage("Invalid key in datastore at $this->cloudConfigFilepath"); + $this->executeCommand(); + } + +} diff --git a/tests/phpunit/src/Commands/ChecklistTest.php b/tests/phpunit/src/Commands/ChecklistTest.php new file mode 100644 index 000000000..c1c203449 --- /dev/null +++ b/tests/phpunit/src/Commands/ChecklistTest.php @@ -0,0 +1,51 @@ +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 new file mode 100644 index 000000000..543be2ad0 --- /dev/null +++ b/tests/phpunit/src/Commands/ClearCacheCommandTest.php @@ -0,0 +1,67 @@ +injectCommand(ClearCacheCommand::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); + + $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', + ]; + + $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()); + } + + 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 new file mode 100644 index 000000000..b18f7d2fb --- /dev/null +++ b/tests/phpunit/src/Commands/CodeStudio/CodeStudioCiCdVariablesTest.php @@ -0,0 +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); + } + } + +} diff --git a/tests/phpunit/src/Commands/CodeStudio/CodeStudioPhpVersionCommandTest.php b/tests/phpunit/src/Commands/CodeStudio/CodeStudioPhpVersionCommandTest.php new file mode 100644 index 000000000..a345d6022 --- /dev/null +++ b/tests/phpunit/src/Commands/CodeStudio/CodeStudioPhpVersionCommandTest.php @@ -0,0 +1,227 @@ +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 new file mode 100644 index 000000000..0e4b9af06 --- /dev/null +++ b/tests/phpunit/src/Commands/CodeStudio/CodeStudioPipelinesMigrateCommandTest.php @@ -0,0 +1,175 @@ +mockApplicationRequest(); + TestBase::setEnvVars(['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); + } + + /** + * @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', + ], + // Args. + [ + '--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 = [ + [ + 'key' => 'ACQUIA_APPLICATION_UUID', + 'masked' => TRUE, + 'protected' => FALSE, + 'value' => NULL, + 'variable_type' => 'env_var', + ], + [ + 'key' => 'ACQUIA_CLOUD_API_TOKEN_KEY', + 'masked' => TRUE, + 'protected' => FALSE, + 'value' => NULL, + 'variable_type' => 'env_var', + ], + [ + 'key' => 'ACQUIA_CLOUD_API_TOKEN_SECRET', + 'masked' => TRUE, + 'protected' => FALSE, + 'value' => NULL, + 'variable_type' => 'env_var', + ], + [ + 'key' => 'ACQUIA_GLAB_TOKEN_NAME', + 'masked' => TRUE, + 'protected' => FALSE, + 'value' => NULL, + 'variable_type' => 'env_var', + ], + [ + 'key' => 'ACQUIA_GLAB_TOKEN_SECRET', + 'masked' => TRUE, + 'protected' => FALSE, + 'value' => NULL, + 'variable_type' => 'env_var', + ], + [ + 'key' => 'PHP_VERSION', + '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()); + + $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"); + } + } + } + $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 new file mode 100644 index 000000000..e431dcf12 --- /dev/null +++ b/tests/phpunit/src/Commands/CodeStudio/CodeStudioWizardCommandTest.php @@ -0,0 +1,534 @@ +mockApplicationRequest(); + TestBase::setEnvVars(['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); + } + + /** + * @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', + ], + // Args. + [ + '--key' => $this->key, + '--secret' => $this->secret, + ], + ], + // 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', + ], + // Args. + [ + '--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', + ], + // Args. + [ + '--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', + ], + // Args. + [ + '--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', + ], + // Args. + [ + '--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', + ], + // Args. + [ + '--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', + ], + // Args. + [ + '--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', + ], + // 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', + ], + // 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', + ], + // 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', + ], + // 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', + 'description' => 'Source repository for Acquia Cloud Platform application a47ac10b-58cc-4372-a567-0e02b2c3d470', + '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', + 'description' => 'Source repository for Acquia Cloud Platform application a47ac10b-58cc-4372-a567-0e02b2c3d470', + 'topics' => 'Acquia Cloud Application', + ]; + $projects->update($this->gitLabProjectId, $parameters)->shouldBeCalled(); + $projects->uploadAvatar( + 33, + Argument::type('string'), + )->shouldBeCalled(); + $this->mockGitLabVariables($this->gitLabProjectId, $projects); + + 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(); + } + $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->projects()->willReturn($projects); + + $this->command->setGitLabClient($gitlabClient->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()); + + $this->mockGetCurrentBranchName($localMachineHelper); + $this->mockGitlabGetHost($localMachineHelper, $this->gitLabHost); + $this->mockGitlabGetToken($localMachineHelper, $this->gitLabToken, $this->gitLabHost); + + /** @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); + } + + // Assertions. + $this->assertEquals(0, $this->getStatusCode()); + } + + /** + * @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('Unable to authenticate with Code Studio'); + $this->executeCommand([ + '--key' => $this->key, + '--secret' => $this->secret, + ]); + } + + /** + * @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); + + $this->expectException(AcquiaCliException::class); + $this->expectExceptionMessage('Could not determine GitLab token'); + $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); + } + + 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 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 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 new file mode 100644 index 000000000..79c36c4ba --- /dev/null +++ b/tests/phpunit/src/Commands/CommandBaseTest.php @@ -0,0 +1,106 @@ +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 new file mode 100644 index 000000000..cb7f3e1cc --- /dev/null +++ b/tests/phpunit/src/Commands/DocsCommandTest.php @@ -0,0 +1,114 @@ +injectCommand(DocsCommand::class); + } + + /** + * @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); + } + + /** + * @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 new file mode 100644 index 000000000..ad5ab4356 --- /dev/null +++ b/tests/phpunit/src/Commands/Email/ConfigurePlatformEmailCommandTest.php @@ -0,0 +1,635 @@ +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', + ], + // Status code. + 0, + // Expected text. + ["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', + ], + // Status code. + 1, + // Expected text. + ["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', + ], + // Status code. + 1, + // Expected text. + ["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', + ], + // Status code. + 1, + // Expected text. + ["Refreshing...", "Check your DNS records with your DNS provider"], + // Domain registration responses. + "404", + ], + ]; + } + + /** + * @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', + ], + // Status code. + 0, + // Enablement response code. + '409', + // Spec key for enablement response code. + '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', + ], + // Status code. + 1, + // Enablement response code. + '403', + // Spec key for enablement response code. + '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' => [ + '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'); + $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); + } + 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); + } + + $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); + + $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' => [ + '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' => [ + '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); + + $domainsRegistrationResponse404 = $this->getMockResponseFromSpec('/subscriptions/{subscriptionUuid}/domains/{domainRegistrationUuid}', 'get', '404'); + + $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. + '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', + // 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' => [ + '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); + + $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'; + + $this->clientProphecy->request('get', "/subscriptions/{$subscriptionsResponse->_embedded->items[0]->uuid}/domains/{$getDomainsResponse->_embedded->items[0]->uuid}")->willReturn($domainsRegistrationResponse200); + + $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 new file mode 100644 index 000000000..54df9b283 --- /dev/null +++ b/tests/phpunit/src/Commands/Email/EmailInfoForSubscriptionCommandTest.php @@ -0,0 +1,177 @@ +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)); + } + + $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; + } + + $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()); + + } + + public function testEmailInfoForSubscriptionNoDomains(): void { + $inputs = [ + // Select a Cloud Platform subscription. + 0, + ]; + $subscriptions = $this->mockRequest('getSubscriptions'); + + $this->clientProphecy->request('get', "/subscriptions/{$subscriptions[0]->uuid}/domains")->willReturn([]); + + $this->executeCommand([], $inputs); + $output = $this->getDisplay(); + $this->assertStringContainsString('No email domains', $output); + } + + public function testEmailInfoForSubscriptionNoAppDomains(): 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); + + $applicationsResponse = $this->mockApplicationsRequest(); + + $applicationsResponse->_embedded->items[0]->subscription->uuid = $subscriptions[0]->uuid; + + $this->clientProphecy->request('get', "/applications/{$applicationsResponse->_embedded->items[0]->uuid}/email/domains")->willReturn([]); + + $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 new file mode 100644 index 000000000..73d5cea5c --- /dev/null +++ b/tests/phpunit/src/Commands/Env/EnvCertCreateCommandTest.php @@ -0,0 +1,123 @@ +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(); + + $sslResponse = $this->getMockResponseFromSpec('/environments/{environmentId}/ssl/certificates', + 'post', '202'); + $options = [ + 'json' => [ + 'ca_certificates' => NULL, + 'certificate' => $certContents, + 'csr_id' => $csrId, + 'label' => $label, + 'legacy' => FALSE, + 'private_key' => $keyContents, + ], + ]; + $this->clientProphecy->request('post', "/environments/{$environments[1]->id}/ssl/certificates", $options) + ->willReturn($sslResponse->{'Site is being imported'}->value) + ->shouldBeCalled(); + $this->mockNotificationResponseFromObject($sslResponse->{'Site is being imported'}->value); + + $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(); + + $sslResponse = $this->getMockResponseFromSpec('/environments/{environmentId}/ssl/certificates', + 'post', '202'); + $options = [ + 'json' => [ + 'ca_certificates' => NULL, + 'certificate' => $certContents, + 'csr_id' => $csrId, + 'label' => $label, + 'legacy' => FALSE, + 'private_key' => $keyContents, + ], + ]; + $this->clientProphecy->request('post', "/environments/{$environments[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 new file mode 100644 index 000000000..fbea13de7 --- /dev/null +++ b/tests/phpunit/src/Commands/Env/EnvCopyCronCommandTest.php @@ -0,0 +1,124 @@ +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); + } + +} diff --git a/tests/phpunit/src/Commands/Env/EnvCreateCommandTest.php b/tests/phpunit/src/Commands/Env/EnvCreateCommandTest.php new file mode 100644 index 000000000..25f551f82 --- /dev/null +++ b/tests/phpunit/src/Commands/Env/EnvCreateCommandTest.php @@ -0,0 +1,144 @@ +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 new file mode 100644 index 000000000..fda51ec9c --- /dev/null +++ b/tests/phpunit/src/Commands/Env/EnvDeleteCommandTest.php @@ -0,0 +1,147 @@ +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; + } + $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); + } + + 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(); + + $dbCopyResponse = $this->getMockResponseFromSpec("/environments/{environmentId}/databases", 'post', '202'); + $response = $dbCopyResponse->{'Database being copied'}->value; + $this->mockNotificationResponseFromObject($response); + $response->links = $response->{'_links'}; + $this->clientProphecy->request('post', "/environments/{$environmentResponse->id}/databases", [ + 'json' => [ + 'name' => $databasesResponse->_embedded->items[0]->name, + 'source' => $environmentResponse->id, + ], + ]) + ->willReturn($response) + ->shouldBeCalled(); + + $filesCopyResponse = $this->getMockResponseFromSpec("/environments/{environmentId}/files", 'post', '202'); + $response = $filesCopyResponse->{'Files queued for copying'}->value; + $this->mockNotificationResponseFromObject($response); + $response->links = $response->{'_links'}; + $this->clientProphecy->request('post', "/environments/{$environmentResponse->id}/files", [ + 'json' => [ + 'source' => $environmentResponse->id, + ], + ]) + ->willReturn($response) + ->shouldBeCalled(); + + $environmentUpdateResponse = $this->getMockResponseFromSpec("/environments/{environmentId}", 'put', '202'); + $this->clientProphecy->request('put', "/environments/{$environmentResponse->id}", Argument::type('array')) + ->willReturn($environmentUpdateResponse) + ->shouldBeCalled(); + $this->mockNotificationResponseFromObject($environmentUpdateResponse); + + $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 new file mode 100644 index 000000000..77ec3f4bd --- /dev/null +++ b/tests/phpunit/src/Commands/Ide/IdeCreateCommandTest.php @@ -0,0 +1,82 @@ +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() + ); + } + + /** + * @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(); + + $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 new file mode 100644 index 000000000..fc446f66f --- /dev/null +++ b/tests/phpunit/src/Commands/Ide/IdeDeleteCommandTest.php @@ -0,0 +1,105 @@ +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 new file mode 100644 index 000000000..3f261723a --- /dev/null +++ b/tests/phpunit/src/Commands/Ide/IdeHelper.php @@ -0,0 +1,34 @@ + + */ + 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 new file mode 100644 index 000000000..f7db8153d --- /dev/null +++ b/tests/phpunit/src/Commands/Ide/IdeInfoCommandTest.php @@ -0,0 +1,50 @@ +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 new file mode 100644 index 000000000..589f0e030 --- /dev/null +++ b/tests/phpunit/src/Commands/Ide/IdeListCommandMineTest.php @@ -0,0 +1,71 @@ +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(); + } + + $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); + + // 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); + + $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); + } + + 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 new file mode 100644 index 000000000..5f94791ff --- /dev/null +++ b/tests/phpunit/src/Commands/Ide/IdeListCommandTest.php @@ -0,0 +1,79 @@ +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); + + // 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(); + + $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 new file mode 100644 index 000000000..9f6c4388d --- /dev/null +++ b/tests/phpunit/src/Commands/Ide/IdeOpenCommandTest.php @@ -0,0 +1,54 @@ +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 new file mode 100644 index 000000000..f7e0337fd --- /dev/null +++ b/tests/phpunit/src/Commands/Ide/IdePhpVersionCommandTest.php @@ -0,0 +1,113 @@ +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; + } + +} diff --git a/tests/phpunit/src/Commands/Ide/IdeRequiredTestTrait.php b/tests/phpunit/src/Commands/Ide/IdeRequiredTestTrait.php new file mode 100644 index 000000000..db1024dfa --- /dev/null +++ b/tests/phpunit/src/Commands/Ide/IdeRequiredTestTrait.php @@ -0,0 +1,22 @@ +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 new file mode 100644 index 000000000..88c19d6f5 --- /dev/null +++ b/tests/phpunit/src/Commands/Ide/IdeServiceStartCommandTest.php @@ -0,0 +1,46 @@ +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 new file mode 100644 index 000000000..96eda23be --- /dev/null +++ b/tests/phpunit/src/Commands/Ide/IdeServiceStopCommandTest.php @@ -0,0 +1,46 @@ +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 new file mode 100644 index 000000000..f0bbf6e0a --- /dev/null +++ b/tests/phpunit/src/Commands/Ide/IdeShareCommandTest.php @@ -0,0 +1,56 @@ + + */ + 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 new file mode 100644 index 000000000..976281515 --- /dev/null +++ b/tests/phpunit/src/Commands/Ide/IdeXdebugToggleCommandTest.php @@ -0,0 +1,82 @@ +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([ + 'supervisorctl', + 'restart', + 'php-fpm', + ], NULL, NULL, FALSE) + ->willReturn($process->reveal()) + ->shouldBeCalled(); + + } + + protected function createCommand(): CommandBase { + return $this->injectCommand(IdeXdebugToggleCommand::class); + } + + /** + * @return array + */ + public function providerTestXdebugCommandEnable(): array { + return [ + ['7.4'], + ['8.0'], + ['8.1'], + ]; + } + + /** + * @dataProvider providerTestXdebugCommandEnable + */ + public function testXdebugCommandEnable(mixed $phpVersion): void { + $this->setUpXdebug($phpVersion); + $this->executeCommand(); + + $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 new file mode 100644 index 000000000..334e65b56 --- /dev/null +++ b/tests/phpunit/src/Commands/Ide/Wizard/IdeWizardCreateSshKeyCommandTest.php @@ -0,0 +1,44 @@ +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 new file mode 100644 index 000000000..046139177 --- /dev/null +++ b/tests/phpunit/src/Commands/Ide/Wizard/IdeWizardDeleteSshKeyCommandTest.php @@ -0,0 +1,39 @@ +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 new file mode 100644 index 000000000..f95850741 --- /dev/null +++ b/tests/phpunit/src/Commands/Ide/Wizard/IdeWizardTestBase.php @@ -0,0 +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', + ]); + } + +} diff --git a/tests/phpunit/src/Commands/Pull/PullCodeCommandTest.php b/tests/phpunit/src/Commands/Pull/PullCodeCommandTest.php new file mode 100644 index 000000000..9aba199a0 --- /dev/null +++ b/tests/phpunit/src/Commands/Pull/PullCodeCommandTest.php @@ -0,0 +1,292 @@ +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); + } + 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 new file mode 100644 index 000000000..1118245cf --- /dev/null +++ b/tests/phpunit/src/Commands/Pull/PullCommandTest.php @@ -0,0 +1,106 @@ +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() + ); + } + + 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 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(); + + $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 new file mode 100644 index 000000000..7f37384a4 --- /dev/null +++ b/tests/phpunit/src/Commands/Pull/PullCommandTestBase.php @@ -0,0 +1,380 @@ +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([ + 'drush', + 'status', + '--fields=db-status,drush-version', + '--format=json', + '--no-interaction', + ], Argument::any(), $dir, FALSE) + ->willReturn($drushStatusProcess->reveal()) + ->shouldBeCalled(); + } + + 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(); + } + + 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(); + } + + protected function mockExecuteComposerExists( + ObjectProphecy $localMachineHelper + ): void { + $localMachineHelper + ->commandExists('composer') + ->willReturn(TRUE) + ->shouldBeCalled(); + } + + 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(); + } + + 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 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 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 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(); + } + + protected function mockSettingsFiles(ObjectProphecy $fs): void { + $fs->remove(Argument::type('string')) + ->willReturn() + ->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(); + } + + 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 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; + } + +} diff --git a/tests/phpunit/src/Commands/Pull/PullDatabaseCommandTest.php b/tests/phpunit/src/Commands/Pull/PullDatabaseCommandTest.php new file mode 100644 index 000000000..865a3b1a2 --- /dev/null +++ b/tests/phpunit/src/Commands/Pull/PullDatabaseCommandTest.php @@ -0,0 +1,388 @@ +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); + } + + $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 new file mode 100644 index 000000000..2dbc70905 --- /dev/null +++ b/tests/phpunit/src/Commands/Pull/PullFilesCommandTest.php @@ -0,0 +1,118 @@ +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 new file mode 100644 index 000000000..cfd042e5d --- /dev/null +++ b/tests/phpunit/src/Commands/Pull/PullScriptsCommandTest.php @@ -0,0 +1,53 @@ +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 new file mode 100644 index 000000000..fbef27e9a --- /dev/null +++ b/tests/phpunit/src/Commands/Push/PushArtifactCommandTest.php @@ -0,0 +1,324 @@ +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); + } + 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(); + } + + 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' => [ + 'drupal-scaffold' => [ + 'file-mapping' => [ + '[web-root]/index.php' => [], + ], + ], + 'installer-paths' => [ + '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(); + } + } + + 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 new file mode 100644 index 000000000..cc65e14cb --- /dev/null +++ b/tests/phpunit/src/Commands/Push/PushCodeCommandTest.php @@ -0,0 +1,28 @@ +injectCommand(PushCodeCommand::class); + } + + public function testPushCode(): void { + $this->executeCommand(); + + $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 new file mode 100644 index 000000000..03568a58c --- /dev/null +++ b/tests/phpunit/src/Commands/Push/PushDatabaseCommandTest.php @@ -0,0 +1,163 @@ +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 new file mode 100644 index 000000000..2c5c81ac2 --- /dev/null +++ b/tests/phpunit/src/Commands/Push/PushFilesCommandTest.php @@ -0,0 +1,169 @@ +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 new file mode 100644 index 000000000..78eb58c9f --- /dev/null +++ b/tests/phpunit/src/Commands/Remote/AliasesDownloadCommandTest.php @@ -0,0 +1,111 @@ +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'); + } + 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 new file mode 100644 index 000000000..1bffca17a --- /dev/null +++ b/tests/phpunit/src/Commands/Remote/AliasesListCommandTest.php @@ -0,0 +1,41 @@ +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 new file mode 100644 index 000000000..2376421e3 --- /dev/null +++ b/tests/phpunit/src/Commands/Remote/DrushCommandTest.php @@ -0,0 +1,72 @@ +injectCommand(DrushCommand::class); + } + + /** + * @return array>> + */ + public function providerTestRemoteDrushCommand(): array { + return [ + [ + [ + '-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()); + + // Assert. + $this->getDisplay(); + } + +} diff --git a/tests/phpunit/src/Commands/Remote/SshCommandTest.php b/tests/phpunit/src/Commands/Remote/SshCommandTest.php new file mode 100644 index 000000000..57a37c46a --- /dev/null +++ b/tests/phpunit/src/Commands/Remote/SshCommandTest.php @@ -0,0 +1,55 @@ +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 new file mode 100644 index 000000000..0ba1b816a --- /dev/null +++ b/tests/phpunit/src/Commands/Remote/SshCommandTestBase.php @@ -0,0 +1,32 @@ +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 new file mode 100644 index 000000000..803637b0d --- /dev/null +++ b/tests/phpunit/src/Commands/Self/MakeDocsCommandTest.php @@ -0,0 +1,28 @@ +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 new file mode 100644 index 000000000..23d69be25 --- /dev/null +++ b/tests/phpunit/src/Commands/Ssh/SshKeyCreateCommandTest.php @@ -0,0 +1,85 @@ +injectCommand(SshKeyCreateCommand::class); + } + + /** + * @return array + */ + public function providerTestCreate(): array { + return [ + [ + TRUE, + // Args. + [ + '--filename' => $this->filename, + '--password' => 'acli123', + ], + // Inputs. + [], + ], + [ + TRUE, + // Args. + [], + // Inputs. + [ + // Enter a filename for your new local SSH key: + $this->filename, + // Enter a password for your SSH key: + 'acli123', + ], + ], + [ + FALSE, + // Args. + [], + // Inputs. + [ + // 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(); + + $this->executeCommand($args, $inputs); + } + +} diff --git a/tests/phpunit/src/Commands/Ssh/SshKeyCreateUploadCommandTest.php b/tests/phpunit/src/Commands/Ssh/SshKeyCreateUploadCommandTest.php new file mode 100644 index 000000000..566307ef8 --- /dev/null +++ b/tests/phpunit/src/Commands/Ssh/SshKeyCreateUploadCommandTest.php @@ -0,0 +1,78 @@ +getCommandTester(); + $this->application->addCommands([ + $this->injectCommand(SshKeyCreateCommand::class), + $this->injectCommand(SshKeyUploadCommand::class), + ]); + } + + protected function createCommand(): CommandBase { + return $this->injectCommand(SshKeyCreateUploadCommand::class); + } + + 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']); + + $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(); + + /** @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; + + $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 new file mode 100644 index 000000000..39e7645e9 --- /dev/null +++ b/tests/phpunit/src/Commands/Ssh/SshKeyDeleteCommandTest.php @@ -0,0 +1,38 @@ +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 new file mode 100644 index 000000000..75f871742 --- /dev/null +++ b/tests/phpunit/src/Commands/Ssh/SshKeyInfoCommandTest.php @@ -0,0 +1,42 @@ +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 new file mode 100644 index 000000000..d52ba54f0 --- /dev/null +++ b/tests/phpunit/src/Commands/Ssh/SshKeyListCommandTest.php @@ -0,0 +1,46 @@ +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 new file mode 100644 index 000000000..edeb76a61 --- /dev/null +++ b/tests/phpunit/src/Commands/Ssh/SshKeyUploadCommandTest.php @@ -0,0 +1,174 @@ +injectCommand(SshKeyUploadCommand::class); + } + + /** + * @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', + ], + // Perms. + TRUE, + ], + [ + // Args. + [ + '--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', + ], + // Perms. + 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(); + } + + // 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' => [ + '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); + } + +} diff --git a/tests/phpunit/src/Commands/TelemetryCommandTest.php b/tests/phpunit/src/Commands/TelemetryCommandTest.php new file mode 100644 index 000000000..b7ac1897a --- /dev/null +++ b/tests/phpunit/src/Commands/TelemetryCommandTest.php @@ -0,0 +1,104 @@ +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); + } + +} diff --git a/tests/phpunit/src/Commands/TelemetryDisableCommandTest.php b/tests/phpunit/src/Commands/TelemetryDisableCommandTest.php new file mode 100644 index 000000000..25647d72d --- /dev/null +++ b/tests/phpunit/src/Commands/TelemetryDisableCommandTest.php @@ -0,0 +1,29 @@ +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 new file mode 100644 index 000000000..5cfc79953 --- /dev/null +++ b/tests/phpunit/src/Commands/TelemetryEnableCommandTest.php @@ -0,0 +1,31 @@ +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 new file mode 100644 index 000000000..11c4d3da9 --- /dev/null +++ b/tests/phpunit/src/Commands/UpdateCommandTest.php @@ -0,0 +1,46 @@ +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 new file mode 100644 index 000000000..5b2142571 --- /dev/null +++ b/tests/phpunit/src/Commands/WizardTestBase.php @@ -0,0 +1,165 @@ +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' => [ + '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", + ]; + } + +}