]> BookStack Code Mirror - system-cli/commitdiff
Added main-path restore command testing
authorDan Brown <redacted>
Wed, 5 Apr 2023 20:07:33 +0000 (21:07 +0100)
committerDan Brown <redacted>
Wed, 5 Apr 2023 20:07:33 +0000 (21:07 +0100)
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.

composer.json
docker-compose.yml
readme.md
src/Commands/RestoreCommand.php
src/Services/MySqlRunner.php
tests/Commands/RestoreCommandTest.php [new file with mode: 0644]
tests/TestCase.php

index 70380e8cdede19517cf892b4225eca8df59414db..085e1ab883a4f196ff6bfd3ecbe340b6ec0d222d 100644 (file)
@@ -20,6 +20,7 @@
         }
     },
     "require-dev": {
-        "phpunit/phpunit": "^9.6"
+        "phpunit/phpunit": "^9.6",
+        "ext-mysqli": "*"
     }
 }
index e41da6229171d3e21d0c702c082f77a9eb48ebef..04cb5ba73ccf12f85bc3a46d5caea5c364c49b23 100644 (file)
@@ -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
index 064e1348bc146c6a8f7287e5bfc34c801e91b552..35112ca4bc43b889437fd6730d48b5ee2ea8411f 100644 (file)
--- 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.
index 829b1809e522c902283c9f0a26fdf4db1e9d66b6..c5dc6f24b4db40febf84d2b63c6a0cdb4022cb18 100644 (file)
@@ -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);
     }
 }
index 5b130a5bbd7b11b467b0fcabaa2a1bd14cccbedb..3263def5ac694a58a92acef7e43e18e58389b566 100644 (file)
@@ -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 (file)
index 0000000..c985abd
--- /dev/null
@@ -0,0 +1,62 @@
+<?php declare(strict_types=1);
+
+namespace Tests\Commands;
+
+use Tests\TestCase;
+
+class RestoreCommandTest extends TestCase
+{
+
+    public function test_restore_into_cwd_by_default_with_all_content_types()
+    {
+        $mysql = new \mysqli('db', 'bookstack', 'bookstack', 'bookstack');
+        $mysql->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
index 92cc2b6b49e504a37347742eb3fe21f7e96752ac..5683e46f28ae9f04d6ab27acd6f1051eaed81155 100644 (file)
@@ -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) {