From 0021b38c5db1f7b6a38db6e7666bb842e5d1c453 Mon Sep 17 00:00:00 2001 From: Kalindi Adhiya Date: Mon, 15 Jun 2026 10:54:17 +0530 Subject: [PATCH 1/9] CLI-1631: handeling silent failure of db failure --- src/Command/Pull/PullCommandBase.php | 4 ++-- src/Command/Push/PushDatabaseCommand.php | 2 +- tests/phpunit/src/Commands/Pull/PullCommandTestBase.php | 2 +- tests/phpunit/src/Commands/Push/PushDatabaseCommandTest.php | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Command/Pull/PullCommandBase.php b/src/Command/Pull/PullCommandBase.php index 0e6fd4e3e..e8c0573fa 100644 --- a/src/Command/Pull/PullCommandBase.php +++ b/src/Command/Pull/PullCommandBase.php @@ -485,10 +485,10 @@ private function importDatabaseDump(string $localDumpFilepath, string $dbHost, s 'mysql', ]); if ($this->localMachineHelper->commandExists('pv')) { - $command = 'pv "${:LOCAL_DUMP_FILEPATH}" --bytes --rate | gunzip | MYSQL_PWD="${:MYSQL_PASSWORD}" mysql --host="${:MYSQL_HOST}" --user="${:MYSQL_USER}" "${:MYSQL_DATABASE}"'; + $command = 'set -o pipefail && pv "${:LOCAL_DUMP_FILEPATH}" --bytes --rate | gunzip | MYSQL_PWD="${:MYSQL_PASSWORD}" mysql --host="${:MYSQL_HOST}" --user="${:MYSQL_USER}" "${:MYSQL_DATABASE}"'; } else { $this->io->warning('Install `pv` to see progress bar'); - $command = 'gunzip -c "${:LOCAL_DUMP_FILEPATH}" | MYSQL_PWD="${:MYSQL_PASSWORD}" mysql --host="${:MYSQL_HOST}" --user="${:MYSQL_USER}" "${:MYSQL_DATABASE}"'; + $command = 'set -o pipefail && gunzip -c "${:LOCAL_DUMP_FILEPATH}" | MYSQL_PWD="${:MYSQL_PASSWORD}" mysql --host="${:MYSQL_HOST}" --user="${:MYSQL_USER}" "${:MYSQL_DATABASE}"'; } $env = [ diff --git a/src/Command/Push/PushDatabaseCommand.php b/src/Command/Push/PushDatabaseCommand.php index 8104a147c..533539f36 100644 --- a/src/Command/Push/PushDatabaseCommand.php +++ b/src/Command/Push/PushDatabaseCommand.php @@ -98,7 +98,7 @@ private function uploadDatabaseDump( private function importDatabaseDumpOnRemote(EnvironmentResponse $environment, string $remoteDumpFilepath, DatabaseResponse $database): void { $this->logger->debug("Importing $remoteDumpFilepath to MySQL on remote machine"); - $command = "pv $remoteDumpFilepath --bytes --rate | gunzip | MYSQL_PWD=$database->password mysql --host={$this->getHostFromDatabaseResponse($environment, $database)} --user=$database->user_name {$this->getNameFromDatabaseResponse($database)}"; + $command = "set -o pipefail && pv $remoteDumpFilepath --bytes --rate | gunzip | MYSQL_PWD=$database->password mysql --host={$this->getHostFromDatabaseResponse($environment, $database)} --user=$database->user_name {$this->getNameFromDatabaseResponse($database)}"; $process = $this->sshHelper->executeCommand($environment->sshUrl, [$command], ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL)); if (!$process->isSuccessful()) { throw new AcquiaCliException('Unable to import database on remote machine. {message}', ['message' => $process->getErrorOutput()]); diff --git a/tests/phpunit/src/Commands/Pull/PullCommandTestBase.php b/tests/phpunit/src/Commands/Pull/PullCommandTestBase.php index 2c1c5a605..3df870a5d 100644 --- a/tests/phpunit/src/Commands/Pull/PullCommandTestBase.php +++ b/tests/phpunit/src/Commands/Pull/PullCommandTestBase.php @@ -319,7 +319,7 @@ protected function mockExecuteMySqlImport( $this->mockExecutePvExists($localMachineHelper, $pvExists); $process = $this->mockProcess($success); $filePath = Path::join(sys_get_temp_dir(), "$env-$dbName-$dbMachineName-$createdAt.sql.gz"); - $command = $pvExists ? 'pv "${:LOCAL_DUMP_FILEPATH}" --bytes --rate | gunzip | MYSQL_PWD="${:MYSQL_PASSWORD}" mysql --host="${:MYSQL_HOST}" --user="${:MYSQL_USER}" "${:MYSQL_DATABASE}"' : 'gunzip -c "${:LOCAL_DUMP_FILEPATH}" | MYSQL_PWD="${:MYSQL_PASSWORD}" mysql --host="${:MYSQL_HOST}" --user="${:MYSQL_USER}" "${:MYSQL_DATABASE}"'; + $command = $pvExists ? 'set -o pipefail && pv "${:LOCAL_DUMP_FILEPATH}" --bytes --rate | gunzip | MYSQL_PWD="${:MYSQL_PASSWORD}" mysql --host="${:MYSQL_HOST}" --user="${:MYSQL_USER}" "${:MYSQL_DATABASE}"' : 'set -o pipefail && gunzip -c "${:LOCAL_DUMP_FILEPATH}" | MYSQL_PWD="${:MYSQL_PASSWORD}" mysql --host="${:MYSQL_HOST}" --user="${:MYSQL_USER}" "${:MYSQL_DATABASE}"'; $expectedEnv = [ 'LOCAL_DUMP_FILEPATH' => $filePath, 'MYSQL_DATABASE' => $localDbName, diff --git a/tests/phpunit/src/Commands/Push/PushDatabaseCommandTest.php b/tests/phpunit/src/Commands/Push/PushDatabaseCommandTest.php index 9562f1197..1248fc7cc 100644 --- a/tests/phpunit/src/Commands/Push/PushDatabaseCommandTest.php +++ b/tests/phpunit/src/Commands/Push/PushDatabaseCommandTest.php @@ -200,7 +200,7 @@ private function mockImportDatabaseDumpOnRemote(ObjectProphecy|LocalMachineHelpe 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', + 6 => 'set -o pipefail && 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, null) ->willReturn($process->reveal()) From f24cbfc891fc81692c2dfd578d25e18b639d1d69 Mon Sep 17 00:00:00 2001 From: Kalindi Adhiya Date: Mon, 15 Jun 2026 11:17:57 +0530 Subject: [PATCH 2/9] making compatible to ide --- src/Command/Pull/PullCommandBase.php | 4 ++-- src/Command/Push/PushDatabaseCommand.php | 2 +- tests/phpunit/src/Commands/Pull/PullCommandTestBase.php | 2 +- tests/phpunit/src/Commands/Push/PushDatabaseCommandTest.php | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Command/Pull/PullCommandBase.php b/src/Command/Pull/PullCommandBase.php index e8c0573fa..f06d89627 100644 --- a/src/Command/Pull/PullCommandBase.php +++ b/src/Command/Pull/PullCommandBase.php @@ -485,10 +485,10 @@ private function importDatabaseDump(string $localDumpFilepath, string $dbHost, s 'mysql', ]); if ($this->localMachineHelper->commandExists('pv')) { - $command = 'set -o pipefail && pv "${:LOCAL_DUMP_FILEPATH}" --bytes --rate | gunzip | MYSQL_PWD="${:MYSQL_PASSWORD}" mysql --host="${:MYSQL_HOST}" --user="${:MYSQL_USER}" "${:MYSQL_DATABASE}"'; + $command = 'bash -o pipefail -c \'pv "${:LOCAL_DUMP_FILEPATH}" --bytes --rate | gunzip | MYSQL_PWD="${:MYSQL_PASSWORD}" mysql --host="${:MYSQL_HOST}" --user="${:MYSQL_USER}" "${:MYSQL_DATABASE}"\''; } else { $this->io->warning('Install `pv` to see progress bar'); - $command = 'set -o pipefail && gunzip -c "${:LOCAL_DUMP_FILEPATH}" | MYSQL_PWD="${:MYSQL_PASSWORD}" mysql --host="${:MYSQL_HOST}" --user="${:MYSQL_USER}" "${:MYSQL_DATABASE}"'; + $command = 'bash -o pipefail -c \'gunzip -c "${:LOCAL_DUMP_FILEPATH}" | MYSQL_PWD="${:MYSQL_PASSWORD}" mysql --host="${:MYSQL_HOST}" --user="${:MYSQL_USER}" "${:MYSQL_DATABASE}"\''; } $env = [ diff --git a/src/Command/Push/PushDatabaseCommand.php b/src/Command/Push/PushDatabaseCommand.php index 533539f36..e5bd17eb4 100644 --- a/src/Command/Push/PushDatabaseCommand.php +++ b/src/Command/Push/PushDatabaseCommand.php @@ -98,7 +98,7 @@ private function uploadDatabaseDump( private function importDatabaseDumpOnRemote(EnvironmentResponse $environment, string $remoteDumpFilepath, DatabaseResponse $database): void { $this->logger->debug("Importing $remoteDumpFilepath to MySQL on remote machine"); - $command = "set -o pipefail && pv $remoteDumpFilepath --bytes --rate | gunzip | MYSQL_PWD=$database->password mysql --host={$this->getHostFromDatabaseResponse($environment, $database)} --user=$database->user_name {$this->getNameFromDatabaseResponse($database)}"; + $command = "bash -o pipefail -c 'pv $remoteDumpFilepath --bytes --rate | gunzip | MYSQL_PWD=$database->password mysql --host={$this->getHostFromDatabaseResponse($environment, $database)} --user=$database->user_name {$this->getNameFromDatabaseResponse($database)}'"; $process = $this->sshHelper->executeCommand($environment->sshUrl, [$command], ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL)); if (!$process->isSuccessful()) { throw new AcquiaCliException('Unable to import database on remote machine. {message}', ['message' => $process->getErrorOutput()]); diff --git a/tests/phpunit/src/Commands/Pull/PullCommandTestBase.php b/tests/phpunit/src/Commands/Pull/PullCommandTestBase.php index 3df870a5d..3264bec81 100644 --- a/tests/phpunit/src/Commands/Pull/PullCommandTestBase.php +++ b/tests/phpunit/src/Commands/Pull/PullCommandTestBase.php @@ -319,7 +319,7 @@ protected function mockExecuteMySqlImport( $this->mockExecutePvExists($localMachineHelper, $pvExists); $process = $this->mockProcess($success); $filePath = Path::join(sys_get_temp_dir(), "$env-$dbName-$dbMachineName-$createdAt.sql.gz"); - $command = $pvExists ? 'set -o pipefail && pv "${:LOCAL_DUMP_FILEPATH}" --bytes --rate | gunzip | MYSQL_PWD="${:MYSQL_PASSWORD}" mysql --host="${:MYSQL_HOST}" --user="${:MYSQL_USER}" "${:MYSQL_DATABASE}"' : 'set -o pipefail && gunzip -c "${:LOCAL_DUMP_FILEPATH}" | MYSQL_PWD="${:MYSQL_PASSWORD}" mysql --host="${:MYSQL_HOST}" --user="${:MYSQL_USER}" "${:MYSQL_DATABASE}"'; + $command = $pvExists ? 'bash -o pipefail -c \'pv "${:LOCAL_DUMP_FILEPATH}" --bytes --rate | gunzip | MYSQL_PWD="${:MYSQL_PASSWORD}" mysql --host="${:MYSQL_HOST}" --user="${:MYSQL_USER}" "${:MYSQL_DATABASE}"\'' : 'bash -o pipefail -c \'gunzip -c "${:LOCAL_DUMP_FILEPATH}" | MYSQL_PWD="${:MYSQL_PASSWORD}" mysql --host="${:MYSQL_HOST}" --user="${:MYSQL_USER}" "${:MYSQL_DATABASE}"\''; $expectedEnv = [ 'LOCAL_DUMP_FILEPATH' => $filePath, 'MYSQL_DATABASE' => $localDbName, diff --git a/tests/phpunit/src/Commands/Push/PushDatabaseCommandTest.php b/tests/phpunit/src/Commands/Push/PushDatabaseCommandTest.php index 1248fc7cc..184ec4535 100644 --- a/tests/phpunit/src/Commands/Push/PushDatabaseCommandTest.php +++ b/tests/phpunit/src/Commands/Push/PushDatabaseCommandTest.php @@ -200,7 +200,7 @@ private function mockImportDatabaseDumpOnRemote(ObjectProphecy|LocalMachineHelpe 3 => '-o StrictHostKeyChecking=no', 4 => '-o AddressFamily inet', 5 => '-o LogLevel=ERROR', - 6 => 'set -o pipefail && 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', + 6 => "bash -o pipefail -c '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, null) ->willReturn($process->reveal()) From 1d17aedc292be04d22c76fd99f9cf76398dc321a Mon Sep 17 00:00:00 2001 From: Kalindi Adhiya Date: Mon, 15 Jun 2026 15:47:00 +0530 Subject: [PATCH 3/9] review accomodated --- src/Command/Pull/PullCommandBase.php | 4 ++-- src/Command/Push/PushDatabaseCommand.php | 8 +++++++- tests/phpunit/src/Commands/Pull/PullCommandTestBase.php | 2 +- .../phpunit/src/Commands/Push/PushDatabaseCommandTest.php | 2 +- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Command/Pull/PullCommandBase.php b/src/Command/Pull/PullCommandBase.php index f06d89627..6bc7d2b9a 100644 --- a/src/Command/Pull/PullCommandBase.php +++ b/src/Command/Pull/PullCommandBase.php @@ -485,10 +485,10 @@ private function importDatabaseDump(string $localDumpFilepath, string $dbHost, s 'mysql', ]); if ($this->localMachineHelper->commandExists('pv')) { - $command = 'bash -o pipefail -c \'pv "${:LOCAL_DUMP_FILEPATH}" --bytes --rate | gunzip | MYSQL_PWD="${:MYSQL_PASSWORD}" mysql --host="${:MYSQL_HOST}" --user="${:MYSQL_USER}" "${:MYSQL_DATABASE}"\''; + $command = 'bash -o pipefail -c "pv \\"${:LOCAL_DUMP_FILEPATH}\\" --bytes --rate | gunzip | MYSQL_PWD=\\"${:MYSQL_PASSWORD}\\" mysql --host=\\"${:MYSQL_HOST}\\" --user=\\"${:MYSQL_USER}\\" \\"${:MYSQL_DATABASE}\\""'; } else { $this->io->warning('Install `pv` to see progress bar'); - $command = 'bash -o pipefail -c \'gunzip -c "${:LOCAL_DUMP_FILEPATH}" | MYSQL_PWD="${:MYSQL_PASSWORD}" mysql --host="${:MYSQL_HOST}" --user="${:MYSQL_USER}" "${:MYSQL_DATABASE}"\''; + $command = 'bash -o pipefail -c "gunzip -c \\"${:LOCAL_DUMP_FILEPATH}\\" | MYSQL_PWD=\\"${:MYSQL_PASSWORD}\\" mysql --host=\\"${:MYSQL_HOST}\\" --user=\\"${:MYSQL_USER}\\" \\"${:MYSQL_DATABASE}\\""'; } $env = [ diff --git a/src/Command/Push/PushDatabaseCommand.php b/src/Command/Push/PushDatabaseCommand.php index e5bd17eb4..8d3ef19ed 100644 --- a/src/Command/Push/PushDatabaseCommand.php +++ b/src/Command/Push/PushDatabaseCommand.php @@ -98,7 +98,13 @@ private function uploadDatabaseDump( private function importDatabaseDumpOnRemote(EnvironmentResponse $environment, string $remoteDumpFilepath, DatabaseResponse $database): void { $this->logger->debug("Importing $remoteDumpFilepath to MySQL on remote machine"); - $command = "bash -o pipefail -c 'pv $remoteDumpFilepath --bytes --rate | gunzip | MYSQL_PWD=$database->password mysql --host={$this->getHostFromDatabaseResponse($environment, $database)} --user=$database->user_name {$this->getNameFromDatabaseResponse($database)}'"; + // Manually escape for single-quoted bash strings (replace ' with '\'' - end quote, escaped quote, start quote) + $host = str_replace("'", "'\\''", $this->getHostFromDatabaseResponse($environment, $database)); + $user = str_replace("'", "'\\''", $database->user_name); + $dbName = str_replace("'", "'\\''", $this->getNameFromDatabaseResponse($database)); + $password = str_replace("'", "'\\''", $database->password); + $filepath = str_replace("'", "'\\''", $remoteDumpFilepath); + $command = "bash -o pipefail -c 'pv '$filepath' --bytes --rate | gunzip | MYSQL_PWD='$password' mysql --host='$host' --user='$user' '$dbName''"; $process = $this->sshHelper->executeCommand($environment->sshUrl, [$command], ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL)); if (!$process->isSuccessful()) { throw new AcquiaCliException('Unable to import database on remote machine. {message}', ['message' => $process->getErrorOutput()]); diff --git a/tests/phpunit/src/Commands/Pull/PullCommandTestBase.php b/tests/phpunit/src/Commands/Pull/PullCommandTestBase.php index 3264bec81..be7edb100 100644 --- a/tests/phpunit/src/Commands/Pull/PullCommandTestBase.php +++ b/tests/phpunit/src/Commands/Pull/PullCommandTestBase.php @@ -319,7 +319,7 @@ protected function mockExecuteMySqlImport( $this->mockExecutePvExists($localMachineHelper, $pvExists); $process = $this->mockProcess($success); $filePath = Path::join(sys_get_temp_dir(), "$env-$dbName-$dbMachineName-$createdAt.sql.gz"); - $command = $pvExists ? 'bash -o pipefail -c \'pv "${:LOCAL_DUMP_FILEPATH}" --bytes --rate | gunzip | MYSQL_PWD="${:MYSQL_PASSWORD}" mysql --host="${:MYSQL_HOST}" --user="${:MYSQL_USER}" "${:MYSQL_DATABASE}"\'' : 'bash -o pipefail -c \'gunzip -c "${:LOCAL_DUMP_FILEPATH}" | MYSQL_PWD="${:MYSQL_PASSWORD}" mysql --host="${:MYSQL_HOST}" --user="${:MYSQL_USER}" "${:MYSQL_DATABASE}"\''; + $command = $pvExists ? 'bash -o pipefail -c "pv \\"${:LOCAL_DUMP_FILEPATH}\\" --bytes --rate | gunzip | MYSQL_PWD=\\"${:MYSQL_PASSWORD}\\" mysql --host=\\"${:MYSQL_HOST}\\" --user=\\"${:MYSQL_USER}\\" \\"${:MYSQL_DATABASE}\\""' : 'bash -o pipefail -c "gunzip -c \\"${:LOCAL_DUMP_FILEPATH}\\" | MYSQL_PWD=\\"${:MYSQL_PASSWORD}\\" mysql --host=\\"${:MYSQL_HOST}\\" --user=\\"${:MYSQL_USER}\\" \\"${:MYSQL_DATABASE}\\""'; $expectedEnv = [ 'LOCAL_DUMP_FILEPATH' => $filePath, 'MYSQL_DATABASE' => $localDbName, diff --git a/tests/phpunit/src/Commands/Push/PushDatabaseCommandTest.php b/tests/phpunit/src/Commands/Push/PushDatabaseCommandTest.php index 184ec4535..94edad4cc 100644 --- a/tests/phpunit/src/Commands/Push/PushDatabaseCommandTest.php +++ b/tests/phpunit/src/Commands/Push/PushDatabaseCommandTest.php @@ -200,7 +200,7 @@ private function mockImportDatabaseDumpOnRemote(ObjectProphecy|LocalMachineHelpe 3 => '-o StrictHostKeyChecking=no', 4 => '-o AddressFamily inet', 5 => '-o LogLevel=ERROR', - 6 => "bash -o pipefail -c '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'", + 6 => "bash -o pipefail -c '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, null) ->willReturn($process->reveal()) From 008a82b5adface3b43a0ac1caff1c59a83db0605 Mon Sep 17 00:00:00 2001 From: Kalindi Adhiya Date: Mon, 15 Jun 2026 15:53:16 +0530 Subject: [PATCH 4/9] mutation fix --- src/Command/Push/PushDatabaseCommand.php | 4 ++ .../Commands/Push/PushDatabaseCommandTest.php | 67 +++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/src/Command/Push/PushDatabaseCommand.php b/src/Command/Push/PushDatabaseCommand.php index 8d3ef19ed..eec68c8d8 100644 --- a/src/Command/Push/PushDatabaseCommand.php +++ b/src/Command/Push/PushDatabaseCommand.php @@ -95,6 +95,10 @@ private function uploadDatabaseDump( return $remoteFilepath; } + /** + * Import database dump on remote server. + * Manually escapes single quotes for bash command safety. + */ private function importDatabaseDumpOnRemote(EnvironmentResponse $environment, string $remoteDumpFilepath, DatabaseResponse $database): void { $this->logger->debug("Importing $remoteDumpFilepath to MySQL on remote machine"); diff --git a/tests/phpunit/src/Commands/Push/PushDatabaseCommandTest.php b/tests/phpunit/src/Commands/Push/PushDatabaseCommandTest.php index 94edad4cc..7be11855c 100644 --- a/tests/phpunit/src/Commands/Push/PushDatabaseCommandTest.php +++ b/tests/phpunit/src/Commands/Push/PushDatabaseCommandTest.php @@ -206,4 +206,71 @@ private function mockImportDatabaseDumpOnRemote(ObjectProphecy|LocalMachineHelpe ->willReturn($process->reveal()) ->shouldBeCalled(); } + + /** + * Test that special characters in passwords are properly escaped. + */ + public function testPushDatabaseWithSpecialCharsInPassword(): 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(); + + // Mock database with special characters in password. + $tamperDatabase = function ($databases): void { + // Password with single quote. + $databases[0]->password = "pass'word"; + // Username with single quote. + $databases[0]->user_name = "user'name"; + }; + $this->mockRequest('getEnvironmentsDatabases', $environments[self::$INPUT_DEFAULT_CHOICE]->id, null, null, $tamperDatabase); + + $process = $this->mockProcess(); + $localMachineHelper = $this->mockLocalMachineHelper(); + $localMachineHelper->checkRequiredBinariesExist(['ssh']) + ->shouldBeCalled(); + + $this->mockExecutePvExists($localMachineHelper, true); + $this->mockCreateMySqlDumpOnLocal($localMachineHelper, false, true); + $this->mockUploadDatabaseDump($localMachineHelper, $process, false); + + // Verify the command has properly escaped single quotes using '\'' pattern. + $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 => "bash -o pipefail -c 'pv '/mnt/tmp/profserv2.01dev/acli-mysql-dump-drupal.sql.gz' --bytes --rate | gunzip | MYSQL_PWD='pass'\\''word' mysql --host='fsdb-74.enterprise-g1.hosting.acquia.com.enterprise-g1.hosting.acquia.com' --user='user'\\''name' 'profserv2db14390''", + ]; + $localMachineHelper->execute($cmd, Argument::type('callable'), null, false, null, null) + ->willReturn($process->reveal()) + ->shouldBeCalled(); + + $this->command->sshHelper = new SshHelper($this->output, $localMachineHelper->reveal(), $this->logger); + + $inputs = [ + // Would you like Acquia CLI to search for a Cloud application? + 'n', + // Select a Cloud Platform application. + 0, + // Would you like to link the project? + 'n', + // Choose a Cloud Platform environment. + 0, + // Overwrite the database? + 'y', + ]; + + $this->executeCommand([], $inputs); + $this->prophet->checkPredictions(); + } } From 546ebd01726b96a964994617b68ca4f599d8f1bd Mon Sep 17 00:00:00 2001 From: Kalindi Adhiya Date: Mon, 15 Jun 2026 16:01:39 +0530 Subject: [PATCH 5/9] test --- .../Commands/Push/PushDatabaseCommandTest.php | 63 ++++++++++++------- 1 file changed, 41 insertions(+), 22 deletions(-) diff --git a/tests/phpunit/src/Commands/Push/PushDatabaseCommandTest.php b/tests/phpunit/src/Commands/Push/PushDatabaseCommandTest.php index 7be11855c..d93e73c5f 100644 --- a/tests/phpunit/src/Commands/Push/PushDatabaseCommandTest.php +++ b/tests/phpunit/src/Commands/Push/PushDatabaseCommandTest.php @@ -10,6 +10,7 @@ use Acquia\Cli\Helpers\LocalMachineHelper; use Acquia\Cli\Helpers\SshHelper; use Acquia\Cli\Tests\CommandTestBase; +use AcquiaCloudApi\Response\DatabaseResponse; use Prophecy\Argument; use Prophecy\Prophecy\ObjectProphecy; use Symfony\Component\Console\Output\OutputInterface; @@ -223,37 +224,36 @@ public function testPushDatabaseWithSpecialCharsInPassword(): void $environments = $this->mockRequest('getApplicationEnvironments', $application->uuid, null, null, $tamper); $this->createMockGitConfigFile(); - // Mock database with special characters in password. - $tamperDatabase = function ($databases): void { - // Password with single quote. - $databases[0]->password = "pass'word"; - // Username with single quote. - $databases[0]->user_name = "user'name"; - }; - $this->mockRequest('getEnvironmentsDatabases', $environments[self::$INPUT_DEFAULT_CHOICE]->id, null, null, $tamperDatabase); + // Create database response with special characters in password and username. + $environment = $environments[self::$INPUT_DEFAULT_CHOICE]; + $databaseResponseJson = json_decode(file_get_contents(Path::join($this->realFixtureDir, '/acsf_db_response.json')), false, 512, JSON_THROW_ON_ERROR); + $databasesResponse = array_map( + static function (mixed $databaseResponse) { + return new DatabaseResponse($databaseResponse); + }, + $databaseResponseJson + ); + // Modify first database to have special characters. + $databasesResponse[0]->password = "pass'word"; + $databasesResponse[0]->user_name = "user'name"; + + $this->clientProphecy->request( + 'get', + "/environments/$environment->id/databases" + ) + ->willReturn($databasesResponse) + ->shouldBeCalled(); $process = $this->mockProcess(); $localMachineHelper = $this->mockLocalMachineHelper(); $localMachineHelper->checkRequiredBinariesExist(['ssh']) ->shouldBeCalled(); + $this->mockGetAcsfSitesLMH($localMachineHelper); $this->mockExecutePvExists($localMachineHelper, true); $this->mockCreateMySqlDumpOnLocal($localMachineHelper, false, true); $this->mockUploadDatabaseDump($localMachineHelper, $process, false); - - // Verify the command has properly escaped single quotes using '\'' pattern. - $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 => "bash -o pipefail -c 'pv '/mnt/tmp/profserv2.01dev/acli-mysql-dump-drupal.sql.gz' --bytes --rate | gunzip | MYSQL_PWD='pass'\\''word' mysql --host='fsdb-74.enterprise-g1.hosting.acquia.com.enterprise-g1.hosting.acquia.com' --user='user'\\''name' 'profserv2db14390''", - ]; - $localMachineHelper->execute($cmd, Argument::type('callable'), null, false, null, null) - ->willReturn($process->reveal()) - ->shouldBeCalled(); + $this->mockImportDatabaseDumpOnRemoteWithSpecialChars($localMachineHelper, $process, false); $this->command->sshHelper = new SshHelper($this->output, $localMachineHelper->reveal(), $this->logger); @@ -266,6 +266,8 @@ public function testPushDatabaseWithSpecialCharsInPassword(): void 'n', // Choose a Cloud Platform environment. 0, + // Choose a database. + 0, // Overwrite the database? 'y', ]; @@ -273,4 +275,21 @@ public function testPushDatabaseWithSpecialCharsInPassword(): void $this->executeCommand([], $inputs); $this->prophet->checkPredictions(); } + + private function mockImportDatabaseDumpOnRemoteWithSpecialChars(ObjectProphecy|LocalMachineHelper $localMachineHelper, Process|ObjectProphecy $process, bool $printOutput = true): void + { + // Verify the command has properly escaped single quotes using '\'' pattern. + $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 => "bash -o pipefail -c 'pv '/mnt/tmp/profserv2.01dev/acli-mysql-dump-drupal.sql.gz' --bytes --rate | gunzip | MYSQL_PWD='pass'\\''word' mysql --host='fsdb-74.enterprise-g1.hosting.acquia.com.enterprise-g1.hosting.acquia.com' --user='user'\\''name' 'profserv2db14390''", + ]; + $localMachineHelper->execute($cmd, Argument::type('callable'), null, $printOutput, null, null) + ->willReturn($process->reveal()) + ->shouldBeCalled(); + } } From c7de98086e83c56ccf3e0325100a79da9f17c5b8 Mon Sep 17 00:00:00 2001 From: Kalindi Adhiya Date: Mon, 15 Jun 2026 16:08:42 +0530 Subject: [PATCH 6/9] test fix --- .../phpunit/src/Commands/Push/PushDatabaseCommandTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/phpunit/src/Commands/Push/PushDatabaseCommandTest.php b/tests/phpunit/src/Commands/Push/PushDatabaseCommandTest.php index d93e73c5f..40e2e1491 100644 --- a/tests/phpunit/src/Commands/Push/PushDatabaseCommandTest.php +++ b/tests/phpunit/src/Commands/Push/PushDatabaseCommandTest.php @@ -251,9 +251,9 @@ static function (mixed $databaseResponse) { $this->mockGetAcsfSitesLMH($localMachineHelper); $this->mockExecutePvExists($localMachineHelper, true); - $this->mockCreateMySqlDumpOnLocal($localMachineHelper, false, true); - $this->mockUploadDatabaseDump($localMachineHelper, $process, false); - $this->mockImportDatabaseDumpOnRemoteWithSpecialChars($localMachineHelper, $process, false); + $this->mockCreateMySqlDumpOnLocal($localMachineHelper, true, true); + $this->mockUploadDatabaseDump($localMachineHelper, $process, true); + $this->mockImportDatabaseDumpOnRemoteWithSpecialChars($localMachineHelper, $process, true); $this->command->sshHelper = new SshHelper($this->output, $localMachineHelper->reveal(), $this->logger); @@ -272,7 +272,7 @@ static function (mixed $databaseResponse) { 'y', ]; - $this->executeCommand([], $inputs); + $this->executeCommand([], $inputs, OutputInterface::VERBOSITY_VERY_VERBOSE); $this->prophet->checkPredictions(); } From 2ac4e4c90fe7fbbf5bc7643b704b9f36852fe3b3 Mon Sep 17 00:00:00 2001 From: Kalindi Adhiya Date: Mon, 15 Jun 2026 16:19:18 +0530 Subject: [PATCH 7/9] test --- .../Commands/Push/PushDatabaseCommandTest.php | 24 ++++--------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/tests/phpunit/src/Commands/Push/PushDatabaseCommandTest.php b/tests/phpunit/src/Commands/Push/PushDatabaseCommandTest.php index 40e2e1491..663b91079 100644 --- a/tests/phpunit/src/Commands/Push/PushDatabaseCommandTest.php +++ b/tests/phpunit/src/Commands/Push/PushDatabaseCommandTest.php @@ -10,7 +10,6 @@ use Acquia\Cli\Helpers\LocalMachineHelper; use Acquia\Cli\Helpers\SshHelper; use Acquia\Cli\Tests\CommandTestBase; -use AcquiaCloudApi\Response\DatabaseResponse; use Prophecy\Argument; use Prophecy\Prophecy\ObjectProphecy; use Symfony\Component\Console\Output\OutputInterface; @@ -224,25 +223,10 @@ public function testPushDatabaseWithSpecialCharsInPassword(): void $environments = $this->mockRequest('getApplicationEnvironments', $application->uuid, null, null, $tamper); $this->createMockGitConfigFile(); - // Create database response with special characters in password and username. - $environment = $environments[self::$INPUT_DEFAULT_CHOICE]; - $databaseResponseJson = json_decode(file_get_contents(Path::join($this->realFixtureDir, '/acsf_db_response.json')), false, 512, JSON_THROW_ON_ERROR); - $databasesResponse = array_map( - static function (mixed $databaseResponse) { - return new DatabaseResponse($databaseResponse); - }, - $databaseResponseJson - ); - // Modify first database to have special characters. - $databasesResponse[0]->password = "pass'word"; - $databasesResponse[0]->user_name = "user'name"; - - $this->clientProphecy->request( - 'get', - "/environments/$environment->id/databases" - ) - ->willReturn($databasesResponse) - ->shouldBeCalled(); + // Mock database with special characters in password and username. + $databases = $this->mockAcsfDatabasesResponse($environments[self::$INPUT_DEFAULT_CHOICE]); + $databases[0]->password = "pass'word"; + $databases[0]->user_name = "user'name"; $process = $this->mockProcess(); $localMachineHelper = $this->mockLocalMachineHelper(); From 86e4fcca62082f6a2c112e88c819ed011a9769ea Mon Sep 17 00:00:00 2001 From: Kalindi Adhiya Date: Mon, 15 Jun 2026 16:38:18 +0530 Subject: [PATCH 8/9] mutation fix --- src/Command/Push/PushDatabaseCommand.php | 1 + tests/phpunit/src/Commands/Push/PushDatabaseCommandTest.php | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Command/Push/PushDatabaseCommand.php b/src/Command/Push/PushDatabaseCommand.php index eec68c8d8..94b7f7f51 100644 --- a/src/Command/Push/PushDatabaseCommand.php +++ b/src/Command/Push/PushDatabaseCommand.php @@ -107,6 +107,7 @@ private function importDatabaseDumpOnRemote(EnvironmentResponse $environment, st $user = str_replace("'", "'\\''", $database->user_name); $dbName = str_replace("'", "'\\''", $this->getNameFromDatabaseResponse($database)); $password = str_replace("'", "'\\''", $database->password); + // @infection-ignore-all System-generated path under our control, defensive escaping $filepath = str_replace("'", "'\\''", $remoteDumpFilepath); $command = "bash -o pipefail -c 'pv '$filepath' --bytes --rate | gunzip | MYSQL_PWD='$password' mysql --host='$host' --user='$user' '$dbName''"; $process = $this->sshHelper->executeCommand($environment->sshUrl, [$command], ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL)); diff --git a/tests/phpunit/src/Commands/Push/PushDatabaseCommandTest.php b/tests/phpunit/src/Commands/Push/PushDatabaseCommandTest.php index 663b91079..03d788389 100644 --- a/tests/phpunit/src/Commands/Push/PushDatabaseCommandTest.php +++ b/tests/phpunit/src/Commands/Push/PushDatabaseCommandTest.php @@ -223,10 +223,12 @@ public function testPushDatabaseWithSpecialCharsInPassword(): void $environments = $this->mockRequest('getApplicationEnvironments', $application->uuid, null, null, $tamper); $this->createMockGitConfigFile(); - // Mock database with special characters in password and username. + // Mock database with special characters in password, username, hostname, and database name. $databases = $this->mockAcsfDatabasesResponse($environments[self::$INPUT_DEFAULT_CHOICE]); $databases[0]->password = "pass'word"; $databases[0]->user_name = "user'name"; + $databases[0]->db_host = "db'host"; + $databases[0]->url = "mysqli://s164:password@127.0.0.1:3306/db'name"; $process = $this->mockProcess(); $localMachineHelper = $this->mockLocalMachineHelper(); @@ -270,7 +272,7 @@ private function mockImportDatabaseDumpOnRemoteWithSpecialChars(ObjectProphecy|L 3 => '-o StrictHostKeyChecking=no', 4 => '-o AddressFamily inet', 5 => '-o LogLevel=ERROR', - 6 => "bash -o pipefail -c 'pv '/mnt/tmp/profserv2.01dev/acli-mysql-dump-drupal.sql.gz' --bytes --rate | gunzip | MYSQL_PWD='pass'\\''word' mysql --host='fsdb-74.enterprise-g1.hosting.acquia.com.enterprise-g1.hosting.acquia.com' --user='user'\\''name' 'profserv2db14390''", + 6 => "bash -o pipefail -c 'pv '/mnt/tmp/profserv2.01dev/acli-mysql-dump-drupal.sql.gz' --bytes --rate | gunzip | MYSQL_PWD='pass'\\''word' mysql --host='db'\\''host.enterprise-g1.hosting.acquia.com' --user='user'\\''name' 'db'\\''name''", ]; $localMachineHelper->execute($cmd, Argument::type('callable'), null, $printOutput, null, null) ->willReturn($process->reveal()) From 72cd78c70e87394dcac37d231ede1f6144598b2e Mon Sep 17 00:00:00 2001 From: Kalindi Adhiya Date: Mon, 15 Jun 2026 17:30:56 +0530 Subject: [PATCH 9/9] error fix --- src/Command/Pull/PullCommandBase.php | 4 ++-- tests/phpunit/src/Commands/Pull/PullCommandTestBase.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Command/Pull/PullCommandBase.php b/src/Command/Pull/PullCommandBase.php index 6bc7d2b9a..d50e49fd9 100644 --- a/src/Command/Pull/PullCommandBase.php +++ b/src/Command/Pull/PullCommandBase.php @@ -485,10 +485,10 @@ private function importDatabaseDump(string $localDumpFilepath, string $dbHost, s 'mysql', ]); if ($this->localMachineHelper->commandExists('pv')) { - $command = 'bash -o pipefail -c "pv \\"${:LOCAL_DUMP_FILEPATH}\\" --bytes --rate | gunzip | MYSQL_PWD=\\"${:MYSQL_PASSWORD}\\" mysql --host=\\"${:MYSQL_HOST}\\" --user=\\"${:MYSQL_USER}\\" \\"${:MYSQL_DATABASE}\\""'; + $command = 'bash -o pipefail -c "pv \\"$LOCAL_DUMP_FILEPATH\\" --bytes --rate | gunzip | MYSQL_PWD=\\"$MYSQL_PASSWORD\\" mysql --host=\\"$MYSQL_HOST\\" --user=\\"$MYSQL_USER\\" \\"$MYSQL_DATABASE\\""'; } else { $this->io->warning('Install `pv` to see progress bar'); - $command = 'bash -o pipefail -c "gunzip -c \\"${:LOCAL_DUMP_FILEPATH}\\" | MYSQL_PWD=\\"${:MYSQL_PASSWORD}\\" mysql --host=\\"${:MYSQL_HOST}\\" --user=\\"${:MYSQL_USER}\\" \\"${:MYSQL_DATABASE}\\""'; + $command = 'bash -o pipefail -c "gunzip -c \\"$LOCAL_DUMP_FILEPATH\\" | MYSQL_PWD=\\"$MYSQL_PASSWORD\\" mysql --host=\\"$MYSQL_HOST\\" --user=\\"$MYSQL_USER\\" \\"$MYSQL_DATABASE\\""'; } $env = [ diff --git a/tests/phpunit/src/Commands/Pull/PullCommandTestBase.php b/tests/phpunit/src/Commands/Pull/PullCommandTestBase.php index be7edb100..ee01569f8 100644 --- a/tests/phpunit/src/Commands/Pull/PullCommandTestBase.php +++ b/tests/phpunit/src/Commands/Pull/PullCommandTestBase.php @@ -319,7 +319,7 @@ protected function mockExecuteMySqlImport( $this->mockExecutePvExists($localMachineHelper, $pvExists); $process = $this->mockProcess($success); $filePath = Path::join(sys_get_temp_dir(), "$env-$dbName-$dbMachineName-$createdAt.sql.gz"); - $command = $pvExists ? 'bash -o pipefail -c "pv \\"${:LOCAL_DUMP_FILEPATH}\\" --bytes --rate | gunzip | MYSQL_PWD=\\"${:MYSQL_PASSWORD}\\" mysql --host=\\"${:MYSQL_HOST}\\" --user=\\"${:MYSQL_USER}\\" \\"${:MYSQL_DATABASE}\\""' : 'bash -o pipefail -c "gunzip -c \\"${:LOCAL_DUMP_FILEPATH}\\" | MYSQL_PWD=\\"${:MYSQL_PASSWORD}\\" mysql --host=\\"${:MYSQL_HOST}\\" --user=\\"${:MYSQL_USER}\\" \\"${:MYSQL_DATABASE}\\""'; + $command = $pvExists ? 'bash -o pipefail -c "pv \\"$LOCAL_DUMP_FILEPATH\\" --bytes --rate | gunzip | MYSQL_PWD=\\"$MYSQL_PASSWORD\\" mysql --host=\\"$MYSQL_HOST\\" --user=\\"$MYSQL_USER\\" \\"$MYSQL_DATABASE\\""' : 'bash -o pipefail -c "gunzip -c \\"$LOCAL_DUMP_FILEPATH\\" | MYSQL_PWD=\\"$MYSQL_PASSWORD\\" mysql --host=\\"$MYSQL_HOST\\" --user=\\"$MYSQL_USER\\" \\"$MYSQL_DATABASE\\""'; $expectedEnv = [ 'LOCAL_DUMP_FILEPATH' => $filePath, 'MYSQL_DATABASE' => $localDbName,