'file', 'constants.ssh.mux_enabled' => false]); Storage::fake('ssh-keys'); Server::flushIdentityMap(); Once::flush(); InstanceSettings::forceCreate(['id' => 0]); Team::factory()->create(['id' => 0]); $this->downloadStorage = S3Storage::create([ 'name' => 'Downloads', 'region' => 'us-east-1', 'key' => 'key', 'secret' => 'secret', 'bucket' => 'downloads', 'endpoint' => 'https://s3.example.com', 'is_usable' => true, 'team_id' => 0, ]); InstanceSettings::find(0)->update(['file_download_s3_storage_id' => $this->downloadStorage->id]); Once::flush(); $this->team = Team::factory()->create(); $this->otherTeam = Team::factory()->create(); $privateKey = PrivateKey::factory()->create(['team_id' => $this->team->id]); $this->server = Server::factory()->create(['team_id' => $this->team->id, 'private_key_id' => $privateKey->id]); $destination = StandaloneDocker::where('server_id', $this->server->id)->firstOrFail(); $environment = Environment::factory()->create(['project_id' => Project::factory()->create(['team_id' => $this->team->id])->id]); $this->database = StandalonePostgresql::create([ 'name' => 'orders-db', 'postgres_user' => 'postgres', 'postgres_password' => 'password', 'postgres_db' => 'db', 'image' => 'postgres:17', 'environment_id' => $environment->id, 'destination_id' => $destination->id, 'destination_type' => $destination->getMorphClass(), ]); $this->backup = ScheduledDatabaseBackup::create([ 'team_id' => $this->team->id, 'frequency' => 'daily', 'database_id' => $this->database->id, 'database_type' => $this->database->getMorphClass(), ]); $this->execution = ScheduledDatabaseBackupExecution::create([ 'scheduled_database_backup_id' => $this->backup->id, 'status' => 'success', 'filename' => BACKUP_S3_DOWNLOAD_FILE, ]); $this->service = Service::factory()->create([ 'server_id' => $this->server->id, 'destination_id' => $destination->id, 'destination_type' => $destination->getMorphClass(), 'environment_id' => $environment->id, ]); $this->user = User::factory()->create(); $this->user->teams()->attach($this->team, ['role' => 'admin']); $this->user->teams()->attach($this->otherTeam, ['role' => 'owner']); $this->actingAs($this->user); session(['currentTeam' => $this->team]); Bus::fake([PrepareFileDownloadJob::class]); }); function fakeBackupFileSize(int|string $size): void { Process::fake(['*' => Process::result(output: (string) $size)]); } it('uploads a database backup through S3 for an admin of its team after a team switch', function () { fakeBackupFileSize(1024); $component = Livewire::test(BackupExecutions::class, ['backup' => $this->backup]); session(['currentTeam' => $this->otherTeam]); $component->call('download_file', $this->execution->id) ->assertDispatched('info') ->assertNotDispatched('open-backup-download') ->assertCount('pendingDownloads', 1); Bus::assertDispatched(PrepareFileDownloadJob::class, fn (PrepareFileDownloadJob $job) => $job->container === null && $job->path === BACKUP_S3_DOWNLOAD_FILE && $job->serverId === $this->server->id && $job->downloadName === 'pg-dump-orders-1.dmp' && $job->s3StorageId === $this->downloadStorage->id && $job->userId === $this->user->id); }); it('streams through the download route when no download storage is set', function () { InstanceSettings::find(0)->update(['file_download_s3_storage_id' => null]); Once::flush(); Process::fake(); Livewire::test(BackupExecutions::class, ['backup' => $this->backup]) ->call('download_file', $this->execution->id) ->assertDispatched('open-backup-download', url: route('download.backup', $this->execution->id)); Process::assertNothingRan(); Bus::assertNotDispatched(PrepareFileDownloadJob::class); }); it('streams through the download route when the backup is larger than one S3 upload', function () { fakeBackupFileSize(FileDownloadStorageService::MAX_SIZE + 1); Livewire::test(BackupExecutions::class, ['backup' => $this->backup]) ->call('download_file', $this->execution->id) ->assertDispatched('open-backup-download', url: route('download.backup', $this->execution->id)); Bus::assertNotDispatched(PrepareFileDownloadJob::class); }); it('lets the download route answer when the backup file is not on the server', function () { fakeBackupFileSize(''); Livewire::test(BackupExecutions::class, ['backup' => $this->backup]) ->call('download_file', $this->execution->id) ->assertDispatched('open-backup-download', url: route('download.backup', $this->execution->id)); Bus::assertNotDispatched(PrepareFileDownloadJob::class); }); it('refuses team members', function () { Process::fake(); $this->user->teams()->updateExistingPivot($this->team->id, ['role' => 'member']); $this->user->unsetRelation('teams'); $component = Livewire::test(BackupExecutions::class, ['backup' => $this->backup]); $component->call('download_file', $this->execution->id) ->assertNotDispatched('open-backup-download'); Process::assertNothingRan(); Bus::assertNotDispatched(PrepareFileDownloadJob::class); }); it('does not download an execution of another backup', function () { Process::fake(); $foreignBackup = ScheduledDatabaseBackup::create([ 'team_id' => $this->otherTeam->id, 'frequency' => 'daily', 'database_id' => $this->database->id + 1000, 'database_type' => $this->database->getMorphClass(), ]); $foreignExecution = ScheduledDatabaseBackupExecution::create([ 'scheduled_database_backup_id' => $foreignBackup->id, 'status' => 'success', 'filename' => '/data/coolify/backups/databases/other/secret.dmp', ]); Livewire::test(BackupExecutions::class, ['backup' => $this->backup]) ->call('download_file', $foreignExecution->id) ->assertDispatched('error', 'Backup execution not found.') ->assertNotDispatched('open-backup-download'); Process::assertNothingRan(); Bus::assertNotDispatched(PrepareFileDownloadJob::class); }); it('uploads a volume backup through S3 and refuses members', function () { fakeBackupFileSize(2048); $application = Application::factory()->create([ 'environment_id' => $this->database->environment_id, 'destination_id' => $this->database->destination_id, 'destination_type' => $this->database->destination_type, ]); $volume = LocalPersistentVolume::create([ 'name' => 'app-data', 'mount_path' => '/data', 'resource_id' => $application->id, 'resource_type' => $application->getMorphClass(), ]); $backup = $volume->scheduledBackups()->create(['team_id' => $this->team->id, 'frequency' => 'daily']); $execution = ScheduledVolumeBackupExecution::create([ 'scheduled_volume_backup_id' => $backup->id, 'status' => 'success', 'filename' => '/data/coolify/backups/volumes/app/archive.tar.gz', ]); $component = Livewire::test(VolumeBackups::class, ['storage' => $volume, 'resource' => $application]); session(['currentTeam' => $this->otherTeam]); $component->call('downloadBackup', $execution->id)->assertCount('pendingDownloads', 1); Bus::assertDispatched(PrepareFileDownloadJob::class, fn (PrepareFileDownloadJob $job) => $job->container === null && $job->path === '/data/coolify/backups/volumes/app/archive.tar.gz' && $job->serverId === $this->server->id); $this->user->teams()->updateExistingPivot($this->team->id, ['role' => 'member']); $this->user->unsetRelation('teams'); $component->call('downloadBackup', $execution->id)->assertForbidden(); Bus::assertDispatchedTimes(PrepareFileDownloadJob::class, 1); }); it('uploads a service database backup through S3', function () { fakeBackupFileSize(1024); $serviceDatabase = ServiceDatabase::create([ 'service_id' => $this->service->id, 'name' => 'service-db', 'image' => 'postgres:16-alpine', 'custom_type' => 'postgresql', ]); $backup = ScheduledDatabaseBackup::create([ 'team_id' => $this->team->id, 'frequency' => 'daily', 'database_id' => $serviceDatabase->id, 'database_type' => $serviceDatabase->getMorphClass(), ]); $execution = ScheduledDatabaseBackupExecution::create([ 'scheduled_database_backup_id' => $backup->id, 'status' => 'success', 'filename' => '/data/coolify/backups/service/db.dmp', ]); Livewire::test(ServiceBackupExecutions::class, ['service' => $this->service]) ->call('downloadExecution', $execution->uuid) ->assertCount('pendingDownloads', 1); Bus::assertDispatched(PrepareFileDownloadJob::class, fn (PrepareFileDownloadJob $job) => $job->container === null && $job->path === '/data/coolify/backups/service/db.dmp'); }); it('opens a finished link only for the user who asked for it', function () { fakeBackupFileSize(1024); $component = Livewire::test(BackupExecutions::class, ['backup' => $this->backup]) ->call('download_file', $this->execution->id); $downloadId = $component->get('pendingDownloads')[0]; Cache::put(PrepareFileDownloadJob::cacheKey($downloadId), ['user_id' => $this->user->id + 1, 'status' => 'ready', 'url' => 'https://s3.example.com/other']); $component->call('checkDownloads')->assertNotDispatched('open-backup-download')->assertCount('pendingDownloads', 1); Cache::put(PrepareFileDownloadJob::cacheKey($downloadId), ['user_id' => $this->user->id, 'status' => 'ready', 'url' => 'https://s3.example.com/get']); $component->call('checkDownloads') ->assertDispatched('open-backup-download', url: 'https://s3.example.com/get') ->assertCount('pendingDownloads', 0); }); it('uploads the backup file from the server host with curl', function () { Bus::fake([DeleteFileDownloadJob::class]); Process::fake(['*' => Process::result(output: '')]); $downloads = Mockery::mock(FileDownloadStorageService::class); $downloads->shouldReceive('uploadUrl')->andReturn(['url' => 'https://s3.example.com/put?X-Amz-Signature=abc&x=1', 'headers' => ['Content-Disposition' => 'attachment']]); $downloads->shouldReceive('downloadUrl')->andReturn('https://s3.example.com/get'); (new PrepareFileDownloadJob('dl-host', $this->user->id, $this->server->id, null, BACKUP_S3_DOWNLOAD_FILE, false, 'pg.dmp', $this->downloadStorage->id))->handle($downloads); expect(Cache::get(PrepareFileDownloadJob::cacheKey('dl-host')))->toMatchArray(['status' => 'ready', 'url' => 'https://s3.example.com/get']); Process::assertRan(fn ($process) => str_contains(remoteProcessScript($process), 'curl -fsS') && str_contains(remoteProcessScript($process), BACKUP_S3_DOWNLOAD_FILE) && str_contains(remoteProcessScript($process), 'X-Amz-Signature=abc&x=1') && ! str_contains(remoteProcessScript($process), 'docker exec')); }); it('builds host file commands that the non-root sudo parser keeps whole', function () { $nonRoot = Server::factory()->make(['user' => 'deploy']); $upload = FileDownloadStorageService::buildHostFileUploadCommand("/data/it's.dmp", 'https://s3.example.com/put?a=1&b=2', ['Content-Disposition' => 'attachment']); $size = FileDownloadStorageService::buildHostFileSizeCommand("/data/it's.dmp"); expect(parseCommandsByLineForSudo(collect([$upload, $size]), $nonRoot))->toBe(['sudo '.$upload, 'sudo '.$size]) ->and(fn () => FileDownloadStorageService::buildHostFileSizeCommand('/data/a;rm -rf /'))->toThrow(Exception::class); });