fix(backups): keep service DB context after deleting a schedule

Cache the related database before deleting the backup so authorization,
server lookup, and the service redirect do not touch a deleted morph.
Skip rendering after delete and cover the service-database path.
This commit is contained in:
Andras Bacsai
2026-09-12 08:54:17 +02:00
parent 33f453912a
commit 6f58ad4b97
2 changed files with 69 additions and 12 deletions
+12 -12
View File
@@ -180,7 +180,8 @@ class BackupEdit extends Component
public function delete($password, $selectedActions = [])
{
$this->authorize('manageBackups', $this->backup->database);
$database = $this->backup->database;
$this->authorize('manageBackups', $database);
if (! verifyPasswordConfirmation($password, $this)) {
return 'The provided password is incorrect.';
@@ -188,10 +189,10 @@ class BackupEdit extends Component
try {
$server = null;
if ($this->backup->database instanceof ServiceDatabase) {
$server = $this->backup->database->service->destination->server;
} elseif ($this->backup->database->destination && $this->backup->database->destination->server) {
$server = $this->backup->database->destination->server;
if ($database instanceof ServiceDatabase) {
$server = $database->service->destination->server;
} elseif ($database->destination && $database->destination->server) {
$server = $database->destination->server;
}
$filenames = $this->backup->executions()
@@ -213,15 +214,14 @@ class BackupEdit extends Component
}
$this->backup->delete();
$this->skipRender();
if ($this->backup->database->getMorphClass() === ServiceDatabase::class) {
$serviceDatabase = $this->backup->database;
if ($database instanceof ServiceDatabase) {
return redirectRoute($this, 'project.service.database.backups', [
'project_uuid' => $this->parameters['project_uuid'],
'environment_uuid' => $this->parameters['environment_uuid'],
'service_uuid' => $serviceDatabase->service->uuid,
'stack_service_uuid' => $serviceDatabase->uuid,
'project_uuid' => $database->service->project()->uuid,
'environment_uuid' => $database->service->environment->uuid,
'service_uuid' => $database->service->uuid,
'stack_service_uuid' => $database->uuid,
]);
} else {
return redirectRoute($this, 'project.database.backup.index', [
@@ -8,6 +8,8 @@ use App\Models\Project;
use App\Models\S3Storage;
use App\Models\ScheduledDatabaseBackup;
use App\Models\Server;
use App\Models\Service;
use App\Models\ServiceDatabase;
use App\Models\StandaloneDocker;
use App\Models\StandalonePostgresql;
use App\Models\Team;
@@ -343,6 +345,61 @@ it('disables S3 backup when saved without a selected S3 storage', function () {
expect($backup->s3_storage_id)->toBeNull();
});
it('deletes a service database backup schedule without rendering the deleted relationship', function () {
InstanceSettings::get()->update(['disable_two_step_confirmation' => true]);
$server = Server::factory()->create(['team_id' => $this->team->id]);
$destination = StandaloneDocker::where('server_id', $server->id)->firstOrFail();
$project = Project::factory()->create(['team_id' => $this->team->id]);
$environment = Environment::factory()->create(['project_id' => $project->id]);
$service = Service::factory()->create([
'server_id' => $server->id,
'environment_id' => $environment->id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
]);
$database = ServiceDatabase::create([
'service_id' => $service->id,
'name' => 'postgres',
'image' => 'postgres:16-alpine',
'custom_type' => 'postgresql',
]);
$backup = ScheduledDatabaseBackup::create([
'frequency' => '0 0 * * *',
'enabled' => true,
'save_s3' => false,
'database_backup_retention_amount_locally' => 0,
'database_backup_retention_days_locally' => 0,
'database_backup_retention_max_storage_locally' => 0,
'database_backup_retention_amount_s3' => 0,
'database_backup_retention_days_s3' => 0,
'database_backup_retention_max_storage_s3' => 0,
'dump_all' => false,
'timeout' => 3600,
'missing_backup_notification_days' => 0,
'database_type' => $database->getMorphClass(),
'database_id' => $database->id,
'team_id' => $this->team->id,
]);
$parameters = [
'project_uuid' => $project->uuid,
'environment_uuid' => $environment->uuid,
'service_uuid' => $service->uuid,
'stack_service_uuid' => $database->uuid,
];
$component = Livewire::test(BackupEdit::class, [
'backup' => $backup,
'availableS3Storages' => collect(),
'section' => 'danger',
]);
$component
->call('delete', '')
->assertRedirectToRoute('project.service.database.backups', $parameters);
expect(ScheduledDatabaseBackup::find($backup->id))->toBeNull();
});
it('cascades to disabling local backup deletion when S3 is force-disabled', function () {
$backup = createBackupForEditValidationTest($this->team, [
'disable_local_backup' => true,