shouldReceive('getMorphClass')->andReturn($class); if ($class === ServiceDatabase::class) { $resource->shouldReceive('databaseType')->andReturn($databaseType); } if ($class === StandaloneSqlite::class) { $resource->shouldReceive('databaseFilePath')->andReturn('/var/lib/sqlite/database.sqlite'); } return $resource; } test('builds database-specific restore commands', function (string $class, ?string $type, string $needle) { $builder = new DatabaseImportCommandBuilder; $command = $builder->buildRestoreCommand(importResource($class, $type), '/tmp/restore file', false); expect($command)->toContain($needle)->toContain("'/tmp/restore file'"); })->with([ 'postgresql' => [StandalonePostgresql::class, null, 'pg_restore'], 'mysql' => [StandaloneMysql::class, null, 'mysql -u $MYSQL_USER'], 'mariadb' => [StandaloneMariadb::class, null, 'mariadb -u $MARIADB_USER'], 'mongodb' => [StandaloneMongodb::class, null, 'mongorestore'], 'sqlite' => [StandaloneSqlite::class, null, '.restore'], 'service postgres' => [ServiceDatabase::class, 'postgresql', 'pg_restore'], 'service mysql' => [ServiceDatabase::class, 'mysql', 'mysql -u $MYSQL_USER'], 'service mariadb' => [ServiceDatabase::class, 'mariadb', 'mariadb -u $MARIADB_USER'], 'service mongo' => [ServiceDatabase::class, 'mongodb', 'mongorestore'], ]); test('decompresses gzip backups for single-database mysql and mariadb restores', function (string $class, ?string $type, string $client) { $builder = new DatabaseImportCommandBuilder; $command = $builder->buildRestoreCommand(importResource($class, $type), '/tmp/restore file.sql.gz', false); expect($command) ->toStartWith("backup='/tmp/restore file.sql.gz'\n") ->toContain('is_gzip() { [ "$(head -c 2 "$backup" | od -An -tx1 | tr -d \' \n\')" = 1f8b ]; }') ->toEndWith("stream | {$client}"); })->with([ 'mysql' => [StandaloneMysql::class, null, 'mysql -u $MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE'], 'mariadb' => [StandaloneMariadb::class, null, 'mariadb -u $MARIADB_USER -p$MARIADB_PASSWORD $MARIADB_DATABASE'], 'service mysql' => [ServiceDatabase::class, 'mysql', 'mysql -u $MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE'], 'service mariadb' => [ServiceDatabase::class, 'mariadb', 'mariadb -u $MARIADB_USER -p$MARIADB_PASSWORD $MARIADB_DATABASE'], ]); test('builds dump-all commands and postgres safety scan', function () { $builder = new DatabaseImportCommandBuilder; $postgres = importResource(StandalonePostgresql::class); $safety = $builder->buildPostgresSafetyCommand($postgres, 'postgres-safe', '/tmp/dump.sql.gz'); $script = $builder->buildPostgresRestoreScanScript($postgres, '/tmp/dump.sql.gz'); expect($builder->buildRestoreCommand($postgres, '/tmp/dump.sql.gz', true)) ->toContain('pg_terminate_backend') ->toStartWith("backup='/tmp/dump.sql.gz'\n") ->and($safety) ->toContain('COPY ... PROGRAM') ->toContain('docker exec postgres-safe') ->toContain('pg_restore -l') ->toContain('pg_restore -f -') ->toContain('unable to inspect custom archive') ->not->toContain('then exit 0') ->and($script) ->toContain("tr '\\n\\r\\t'"); }); test('restores dump-all PostgreSQL custom archives with pg_restore from the decompressed stream', function () { $command = (new DatabaseImportCommandBuilder)->buildRestoreCommand(importResource(StandalonePostgresql::class), '/tmp/backup.dump.gz', true); // pg_restore cannot read gzip files, so it must receive the decompressed archive on stdin. expect($command) ->toContain('stream | pg_restore -U ${POSTGRES_USER} -d "$db"') ->toContain('stream | psql -U ${POSTGRES_USER} -d "$db"') ->not->toContain("-d \"\$db\" '/tmp/backup.dump.gz'"); }); test('postgres safety command is null for non-postgres databases', function () { $builder = new DatabaseImportCommandBuilder; expect($builder->buildPostgresSafetyCommand(importResource(StandaloneMysql::class), 'mysql-test', '/tmp/restore_test')) ->toBeNull(); }); test('dump-all mysql and mariadb commands use valid shell parameter expansions', function (string $class, string $binary, string $prefix) { $builder = new DatabaseImportCommandBuilder; $command = $builder->buildRestoreCommand(importResource($class), '/tmp/dump.sql.gz', true); $rootPassword = '${'.$prefix.'_ROOT_PASSWORD}'; $database = '${'.$prefix.'_DATABASE:-default}'; expect($command) ->toContain($binary) ->toStartWith("backup='/tmp/dump.sql.gz'\n") ->toContain('-p'.$rootPassword) ->toContain('CREATE DATABASE IF NOT EXISTS \`'.$database.'\`') ->and(substr_count($command, $rootPassword))->toBe(6) ->and(substr_count($command, $database))->toBe(2) ->and($command)->not->toContain('${{'); })->with([ 'mysql' => [StandaloneMysql::class, 'mysql', 'MYSQL'], 'mariadb' => [StandaloneMariadb::class, 'mariadb', 'MARIADB'], ]); test('stops PostgreSQL restores on the first error without replacing existing objects by default', function () { $builder = new DatabaseImportCommandBuilder; $postgres = importResource(StandalonePostgresql::class); expect($builder->buildRestoreCommand($postgres, '/tmp/backup.dump', false, false)) ->toContain('--exit-on-error') ->not->toContain('--clean') ->not->toContain('--if-exists'); }); test('replaces existing PostgreSQL objects when requested', function () { $builder = new DatabaseImportCommandBuilder; $postgres = importResource(StandalonePostgresql::class); expect($builder->buildRestoreCommand($postgres, '/tmp/backup.dump', false, true)) ->toContain('--clean') ->toContain('--if-exists') ->toContain('--exit-on-error'); }); test('rejects unsupported database types', function () { $builder = new DatabaseImportCommandBuilder; $redis = importResource(StandaloneRedis::class); expect(fn () => $builder->buildRestoreCommand($redis, '/tmp/backup', false)) ->toThrow(InvalidArgumentException::class, 'not supported'); });