From 41eee14bd33d67e27e14f81b0d8c7bc78662c08c Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 15 Jul 2026 17:39:48 +0200 Subject: [PATCH] fix(backups): redirect to executions after manual backup queueing Queue manual database and volume backups before redirecting to their execution views, including schedules with unusable S3 storage. --- app/Livewire/Project/Database/BackupEdit.php | 19 ++++++++ .../Project/Shared/Storages/VolumeBackups.php | 19 +++++--- tests/Feature/BackupEditValidationTest.php | 36 +++++++++++++++ tests/Feature/VolumeBackupTest.php | 44 ++++++++++++++++++- 4 files changed, 111 insertions(+), 7 deletions(-) diff --git a/app/Livewire/Project/Database/BackupEdit.php b/app/Livewire/Project/Database/BackupEdit.php index bbb33e8ed..fc7be5295 100644 --- a/app/Livewire/Project/Database/BackupEdit.php +++ b/app/Livewire/Project/Database/BackupEdit.php @@ -206,6 +206,25 @@ public function backupNow() DatabaseBackupJob::dispatch($this->backup); $this->dispatch('success', 'Backup queued. It will be available in a few minutes.'); + + $database = $this->backup->database; + + if ($database instanceof ServiceDatabase) { + return redirect()->route('project.service.database.backup.executions', [ + 'project_uuid' => $database->service->project()->uuid, + 'environment_uuid' => $database->service->environment->uuid, + 'service_uuid' => $database->service->uuid, + 'stack_service_uuid' => $database->uuid, + 'backup_uuid' => $this->backup->uuid, + ]); + } + + return redirect()->route('project.database.backup.executions', [ + 'project_uuid' => $database->project()->uuid, + 'environment_uuid' => $database->environment->uuid, + 'database_uuid' => $database->uuid, + 'backup_uuid' => $this->backup->uuid, + ]); } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Project/Shared/Storages/VolumeBackups.php b/app/Livewire/Project/Shared/Storages/VolumeBackups.php index f86f4dc87..e85036c55 100644 --- a/app/Livewire/Project/Shared/Storages/VolumeBackups.php +++ b/app/Livewire/Project/Shared/Storages/VolumeBackups.php @@ -178,21 +178,28 @@ public function toggleEnabled(): void $this->dispatch('success', $this->enabled ? 'Storage backups enabled.' : 'Storage backups disabled.'); } - public function backupNow(): void + public function backupNow() { $this->authorize('update', $this->resource); - if (! $this->validateSettings()) { - return; - } - if (! $this->backup) { + if (! $this->validateSettings()) { + return; + } + $this->enabled = false; + $this->backup = $this->persistBackup(false); } - $this->backup = $this->persistBackup($this->enabled); VolumeBackupJob::dispatch($this->backup); $this->dispatch('success', 'Storage backup queued.'); + + return redirect()->route('project.application.backup.executions', [ + 'project_uuid' => $this->resource->project()->uuid, + 'environment_uuid' => $this->resource->environment->uuid, + 'application_uuid' => $this->resource->uuid, + 'backup_uuid' => $this->backup->uuid, + ]); } public function delete(?string $password = null, array $selectedActions = []) diff --git a/tests/Feature/BackupEditValidationTest.php b/tests/Feature/BackupEditValidationTest.php index 3d8920f4a..3218c3b8d 100644 --- a/tests/Feature/BackupEditValidationTest.php +++ b/tests/Feature/BackupEditValidationTest.php @@ -1,5 +1,6 @@ refresh()->enabled)->toBeFalsy(); }); +it('redirects to executions after queuing a database backup with unusable S3 storage', function () { + Queue::fake(); + $s3Storage = createS3StorageForBackupEditValidationTest($this->team, 'Unavailable storage'); + $s3Storage->update(['is_usable' => false]); + $backup = createBackupForEditValidationTest($this->team, [ + 'enabled' => true, + 's3_storage_id' => $s3Storage->id, + 'database_backup_retention_amount_locally' => 7, + 'database_backup_retention_days_locally' => 0, + 'database_backup_retention_max_storage_locally' => 0, + 'database_backup_retention_amount_s3' => 7, + 'database_backup_retention_days_s3' => 0, + 'database_backup_retention_max_storage_s3' => 0, + 'dump_all' => false, + 'timeout' => 3600, + ]); + $database = $backup->database; + $parameters = [ + 'project_uuid' => $database->project()->uuid, + 'environment_uuid' => $database->environment->uuid, + 'database_uuid' => $database->uuid, + 'backup_uuid' => $backup->uuid, + ]; + + Livewire::test(BackupEdit::class, [ + 'backup' => $backup, + 'availableS3Storages' => $this->team->s3s, + ]) + ->call('backupNow') + ->assertRedirectToRoute('project.database.backup.executions', $parameters); + + Queue::assertPushed(DatabaseBackupJob::class); +}); + it('disables S3 backup when saved without a selected S3 storage', function () { $backup = createBackupForEditValidationTest($this->team); diff --git a/tests/Feature/VolumeBackupTest.php b/tests/Feature/VolumeBackupTest.php index 084c93eab..e98d59ec7 100644 --- a/tests/Feature/VolumeBackupTest.php +++ b/tests/Feature/VolumeBackupTest.php @@ -1008,16 +1008,58 @@ function signInForVolumeBackups($testCase, Team $team): User signInForVolumeBackups($this, $team); [$application, $volume] = createVolumeBackupApplication($team); - Livewire::test(VolumeBackups::class, ['storage' => $volume, 'resource' => $application]) + $component = Livewire::test(VolumeBackups::class, ['storage' => $volume, 'resource' => $application]) ->call('backupNow') ->assertDispatched('success'); $backup = ScheduledVolumeBackup::query()->sole(); + $component->assertRedirectToRoute('project.application.backup.executions', [ + 'project_uuid' => $application->project()->uuid, + 'environment_uuid' => $application->environment->uuid, + 'application_uuid' => $application->uuid, + 'backup_uuid' => $backup->uuid, + ]); + expect($backup->enabled)->toBeFalse(); Queue::assertPushed(VolumeBackupJob::class, fn (VolumeBackupJob $job) => $job->backup->is($backup)); }); +it('queues a manual backup when its S3 storage has become unusable', function () { + Queue::fake(); + $team = Team::factory()->create(); + signInForVolumeBackups($this, $team); + [$application, $volume] = createVolumeBackupApplication($team); + $s3Storage = S3Storage::create([ + 'name' => 'Unavailable storage', + 'region' => 'us-east-1', + 'key' => 'key', + 'secret' => 'secret', + 'bucket' => 'bucket', + 'endpoint' => 'https://s3.example.com', + 'team_id' => $team->id, + 'is_usable' => false, + ]); + $backup = $volume->scheduledBackups()->create([ + 'team_id' => $team->id, + 's3_storage_id' => $s3Storage->id, + 'frequency' => 'daily', + 'save_s3' => true, + ]); + $parameters = [ + 'project_uuid' => $application->project()->uuid, + 'environment_uuid' => $application->environment->uuid, + 'application_uuid' => $application->uuid, + 'backup_uuid' => $backup->uuid, + ]; + + Livewire::test(VolumeBackups::class, ['storage' => $volume, 'resource' => $application]) + ->call('backupNow') + ->assertRedirectToRoute('project.application.backup.executions', $parameters); + + Queue::assertPushed(VolumeBackupJob::class, fn (VolumeBackupJob $job) => $job->backup->is($backup)); +}); + it('deletes local archives before deleting a volume backup schedule', function () { Process::fake(); $team = Team::factory()->create();