From: Dan Brown Date: Wed, 5 Apr 2023 20:07:33 +0000 (+0100) Subject: Added main-path restore command testing X-Git-Url: http://source.bookstackapp.com/system-cli/commitdiff_plain/551a5d0beeb88926dad6b7b9554628f7db35e715 Added main-path restore command testing Addressed some issues in the process including: - Added dropping of existing db tables before restore. - Changed how passwords are used in MySQL CLI actions to prevent warnings. - Updated docker setup for proper healthcheck/cleanup actions since was previously misled by existing running container instances. --- diff --git a/composer.json b/composer.json index 70380e8..085e1ab 100644 --- a/composer.json +++ b/composer.json @@ -20,6 +20,7 @@ } }, "require-dev": { - "phpunit/phpunit": "^9.6" + "phpunit/phpunit": "^9.6", + "ext-mysqli": "*" } } diff --git a/docker-compose.yml b/docker-compose.yml index e41da62..04cb5ba 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,7 +8,7 @@ services: - ./:/cli depends_on: db: - condition: service_completed_successfully + condition: service_healthy db: image: mysql:8.0 environment: @@ -17,4 +17,8 @@ services: MYSQL_USER: bookstack MYSQL_PASSWORD: bookstack volumes: - - ./docker-mysql-init.sql:/docker-entrypoint-initdb.d/init.sql \ No newline at end of file + - ./docker-mysql-init.sql:/docker-entrypoint-initdb.d/init.sql + healthcheck: + test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"] + timeout: 20s + retries: 10 \ No newline at end of file diff --git a/readme.md b/readme.md index 064e134..35112ca 100644 --- a/readme.md +++ b/readme.md @@ -47,7 +47,7 @@ composer install vendor/bin/phpunit # To clean-up and delete the environment: -docker compose rm -fsv +docker compose down -v --remove-orphans ``` Within the environment a pre-existing BookStack instance can be found at `/var/www/bookstack` for testing. diff --git a/src/Commands/RestoreCommand.php b/src/Commands/RestoreCommand.php index 829b180..c5dc6f2 100644 --- a/src/Commands/RestoreCommand.php +++ b/src/Commands/RestoreCommand.php @@ -30,6 +30,7 @@ class RestoreCommand extends Command /** * @throws CommandError + * @throws \Exception */ protected function execute(InputInterface $input, OutputInterface $output): int { @@ -199,6 +200,13 @@ class RestoreCommand extends Command $dbDump = $extractDir . DIRECTORY_SEPARATOR . 'db.sql'; $currentEnv = EnvironmentLoader::load($appDir); $mysql = MySqlRunner::fromEnvOptions($currentEnv); + + // Drop existing tables + $dropSqlTempFile = tempnam(sys_get_temp_dir(), 'bs-cli-restore'); + file_put_contents($dropSqlTempFile, $mysql->dropTablesSql()); + $mysql->importSqlFile($dropSqlTempFile); + + $mysql->importSqlFile($dbDump); } } diff --git a/src/Services/MySqlRunner.php b/src/Services/MySqlRunner.php index 5b130a5..3263def 100644 --- a/src/Services/MySqlRunner.php +++ b/src/Services/MySqlRunner.php @@ -31,13 +31,13 @@ class MySqlRunner public function testConnection(): bool { $output = (new ProgramRunner('mysql', '/usr/bin/mysql')) + ->withEnvironment(['MYSQL_PWD' => $this->password]) ->withTimeout(240) ->withIdleTimeout(5) ->runCapturingStdErr([ '-h', $this->host, '-P', $this->port, '-u', $this->user, - '-p' . $this->password, $this->database, '-e', "show tables;" ]); @@ -48,15 +48,15 @@ class MySqlRunner public function importSqlFile(string $sqlFilePath): void { $output = (new ProgramRunner('mysql', '/usr/bin/mysql')) + ->withEnvironment(['MYSQL_PWD' => $this->password]) ->withTimeout(240) ->withIdleTimeout(5) ->runCapturingStdErr([ '-h', $this->host, '-P', $this->port, '-u', $this->user, - '-p' . $this->password, $this->database, - '<', $sqlFilePath + '-e', "source {$sqlFilePath}" ]); if ($output) { @@ -64,6 +64,25 @@ class MySqlRunner } } + public function dropTablesSql(): string + { + return <<<'HEREDOC' +SET FOREIGN_KEY_CHECKS = 0; +SET GROUP_CONCAT_MAX_LEN=32768; +SET @tables = NULL; +SELECT GROUP_CONCAT('`', table_name, '`') INTO @tables + FROM information_schema.tables + WHERE table_schema = (SELECT DATABASE()); +SELECT IFNULL(@tables,'dummy') INTO @tables; + +SET @tables = CONCAT('DROP TABLE IF EXISTS ', @tables); +PREPARE stmt FROM @tables; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; +SET FOREIGN_KEY_CHECKS = 1; +HEREDOC; + } + public function runDumpToFile(string $filePath): void { $file = fopen($filePath, 'w'); @@ -74,11 +93,11 @@ class MySqlRunner (new ProgramRunner('mysqldump', '/usr/bin/mysqldump')) ->withTimeout(240) ->withIdleTimeout(15) + ->withEnvironment(['MYSQL_PWD' => $this->password]) ->runWithoutOutputCallbacks([ '-h', $this->host, '-P', $this->port, '-u', $this->user, - '-p' . $this->password, '--single-transaction', '--no-tablespaces', $this->database, @@ -86,9 +105,7 @@ class MySqlRunner fwrite($file, $data); $hasOutput = true; }, function ($error) use (&$errors) { - if (!str_contains($error, '[Warning] ')) { - $errors .= $error . "\n"; - } + $errors .= $error . "\n"; }); } catch (\Exception $exception) { fclose($file); diff --git a/tests/Commands/RestoreCommandTest.php b/tests/Commands/RestoreCommandTest.php new file mode 100644 index 0000000..c985abd --- /dev/null +++ b/tests/Commands/RestoreCommandTest.php @@ -0,0 +1,62 @@ +query('CREATE TABLE xx_testing (labels varchar(255));'); + + $result = $mysql->query('SHOW TABLES LIKE \'zz_testing\';'); + $this->assertEquals(0, mysqli_num_rows($result)); + + $zipFile = $this->buildZip(function (\ZipArchive $zip) { + $zip->addFromString('.env', "APP_KEY=abc123\nAPP_URL=https://example.com"); + $zip->addFromString('public/uploads/test.txt', 'hello-public-uploads'); + $zip->addFromString('storage/uploads/test.txt', 'hello-storage-uploads'); + $zip->addFromString('themes/test.txt', 'hello-themes'); + $zip->addFromString('db.sql', "CREATE TABLE zz_testing (names varchar(255));\nINSERT INTO zz_testing values ('barry');"); + }); + + exec('cp -r /var/www/bookstack /var/www/bookstack-restore'); + chdir('/var/www/bookstack-restore'); + + $result = $this->runCommand('restore', [ + 'backup-zip' => $zipFile, + ], [ + 'yes', '1' + ]); + + $result->dumpError(); + $result->assertSuccessfulExit(); + $result->assertStdoutContains('Restore operation complete!'); + + $result = $mysql->query('SELECT * FROM zz_testing where names = \'barry\';'); + $this->assertEquals(1, mysqli_num_rows($result)); + $result = $mysql->query('SHOW TABLES LIKE \'xx_testing\';'); + $this->assertEquals(0, mysqli_num_rows($result)); + + $this->assertStringEqualsFile('/var/www/bookstack-restore/public/uploads/test.txt', 'hello-public-uploads'); + $this->assertStringEqualsFile('/var/www/bookstack-restore/storage/uploads/test.txt', 'hello-storage-uploads'); + $this->assertStringEqualsFile('/var/www/bookstack-restore/themes/test.txt', 'hello-themes'); + + $mysql->query("DROP TABLE zz_testing;"); + exec('rm -rf /var/www/bookstack-restore'); + } + + protected function buildZip(callable $builder): string + { + $zipFile = tempnam(sys_get_temp_dir(), 'cli-test'); + $testZip = new \ZipArchive(''); + $testZip->open($zipFile); + $builder($testZip); + $testZip->close(); + + return $zipFile; + } +} \ No newline at end of file diff --git a/tests/TestCase.php b/tests/TestCase.php index 92cc2b6..5683e46 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -40,7 +40,7 @@ class TestCase extends \PHPUnit\Framework\TestCase return require dirname(__DIR__) . '/src/app.php'; } - protected function runCommand(string $command, array $args = []): CommandResult + protected function runCommand(string $command, array $args = [], array $inputs = []): CommandResult { $app = $this->getApp(); $command = $app->find($command); @@ -48,6 +48,10 @@ class TestCase extends \PHPUnit\Framework\TestCase $err = null; $commandTester = new CommandTester($command); + if (!empty($inputs)) { + $commandTester->setInputs($inputs); + } + try { $commandTester->execute($args); } catch (\Exception $exception) {