mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-04 18:52:49 +00:00
199 lines
9.6 KiB
PHP
199 lines
9.6 KiB
PHP
<?php
|
|
|
|
use App\Models\ServiceDatabase;
|
|
use App\Models\StandaloneMariadb;
|
|
use App\Models\StandaloneMongodb;
|
|
use App\Models\StandaloneMysql;
|
|
use App\Models\StandalonePostgresql;
|
|
use App\Models\StandaloneRedis;
|
|
use App\Models\StandaloneSqlite;
|
|
use App\Support\DatabaseImport\DatabaseImportCommandBuilder;
|
|
|
|
function importResource(string $class, ?string $databaseType = null): object
|
|
{
|
|
$resource = Mockery::mock($class);
|
|
$resource->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 $client_safety -u $MYSQL_USER'],
|
|
'mariadb' => [StandaloneMariadb::class, null, 'mariadb $client_safety -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 $client_safety -u $MYSQL_USER'],
|
|
'service mariadb' => [ServiceDatabase::class, 'mariadb', 'mariadb $client_safety -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 $client_safety -u $MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE'],
|
|
'mariadb' => [StandaloneMariadb::class, null, 'mariadb $client_safety -u $MARIADB_USER -p$MARIADB_PASSWORD $MARIADB_DATABASE'],
|
|
'service mysql' => [ServiceDatabase::class, 'mysql', 'mysql $client_safety -u $MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE'],
|
|
'service mariadb' => [ServiceDatabase::class, 'mariadb', 'mariadb $client_safety -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' '~~'");
|
|
});
|
|
|
|
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 "$pg_user" -d "$db"')
|
|
->toContain('stream | psql -U "$pg_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('restores PostgreSQL archives without owners and privileges by default', function (bool $replaceExisting) {
|
|
$command = (new DatabaseImportCommandBuilder)->buildRestoreCommand(importResource(StandalonePostgresql::class), '/tmp/backup.dump', false, $replaceExisting);
|
|
|
|
// Dumps from other hosts (for example RDS) reference roles that do not exist here, and
|
|
// --single-transaction would roll back the whole restore on the first ALTER OWNER.
|
|
expect($command)->toContain('pg_restore --exit-on-error --single-transaction --no-owner --no-acl');
|
|
})->with(['keep existing objects' => [false], 'replace existing objects' => [true]]);
|
|
|
|
test('keeps PostgreSQL owners and privileges when requested', function () {
|
|
$command = (new DatabaseImportCommandBuilder)->buildRestoreCommand(importResource(StandalonePostgresql::class), '/tmp/backup.dump', false, false, keepOwners: true);
|
|
|
|
expect($command)->toContain('pg_restore --exit-on-error --single-transaction -U')
|
|
->not->toContain('--no-owner')
|
|
->not->toContain('--no-acl');
|
|
});
|
|
|
|
test('skips the system databases of MySQL and MariaDB all-databases backups by default', function (string $class, string $binary) {
|
|
$command = (new DatabaseImportCommandBuilder)->buildRestoreCommand(importResource($class), '/tmp/backup.sql', true);
|
|
|
|
expect($command)->toContain('command -v awk')
|
|
->toMatch('/stream \| \{?\s*awk /')
|
|
->toContain('"mysql"')->toContain('"sys"')->toContain('"performance_schema"')->toContain('"information_schema"')
|
|
->toContain("| {$binary} \$client_safety -u root");
|
|
})->with([
|
|
'mysql' => [StandaloneMysql::class, 'mysql'],
|
|
'mariadb' => [StandaloneMariadb::class, 'mariadb'],
|
|
]);
|
|
|
|
test('restores the system databases of MySQL and MariaDB all-databases backups when requested', function (string $class, string $binary) {
|
|
$command = (new DatabaseImportCommandBuilder)->buildRestoreCommand(importResource($class), '/tmp/backup.sql', true, restoreMysqlUsers: true);
|
|
|
|
expect($command)->not->toContain('awk')
|
|
->toContain("stream | {$binary} \$client_safety -u root");
|
|
})->with([
|
|
'mysql' => [StandaloneMysql::class, 'mysql'],
|
|
'mariadb' => [StandaloneMariadb::class, 'mariadb'],
|
|
]);
|
|
|
|
test('restores single MySQL databases without the system database filter', function () {
|
|
expect((new DatabaseImportCommandBuilder)->buildRestoreCommand(importResource(StandaloneMysql::class), '/tmp/backup.sql', false))
|
|
->not->toContain('awk');
|
|
});
|
|
|
|
test('restores SQLite backups into the selected database file', function () {
|
|
$resource = Mockery::mock(StandaloneSqlite::class);
|
|
$resource->shouldReceive('getMorphClass')->andReturn(StandaloneSqlite::class);
|
|
$resource->shouldReceive('databaseFilePath')->once()->with('cache.db')->andReturn('/var/lib/sqlite/cache.db');
|
|
|
|
$command = (new DatabaseImportCommandBuilder)->buildRestoreCommand($resource, '/tmp/backup', false, sqliteDatabase: 'cache.db');
|
|
|
|
expect($command)->toContain("sqlite3 -bail '/var/lib/sqlite/cache.db'");
|
|
});
|
|
|
|
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');
|
|
});
|