diff --git a/app/Filament/Resources/BackupSetResource.php b/app/Filament/Resources/BackupSetResource.php index c2dee2a..890898a 100644 --- a/app/Filament/Resources/BackupSetResource.php +++ b/app/Filament/Resources/BackupSetResource.php @@ -78,16 +78,6 @@ public static function table(Table $table): Table ->requiresConfirmation() ->visible(fn (BackupSet $record) => ! $record->trashed()) ->action(function (BackupSet $record, AuditLogger $auditLogger) { - if ($record->restoreRuns()->withTrashed()->exists()) { - Notification::make() - ->title('Cannot archive backup set') - ->body('Backup sets used by restore runs cannot be archived.') - ->danger() - ->send(); - - return; - } - $record->delete(); if ($record->tenant) { @@ -159,7 +149,7 @@ public static function table(Table $table): Table return $isOnlyTrashed; }) - ->modalDescription('This archives backup sets (soft delete). Backup sets referenced by restore runs will be skipped.') + ->modalDescription('This archives backup sets (soft delete). Already archived backup sets will be skipped.') ->form(function (Collection $records) { if ($records->count() >= 10) { return [ diff --git a/app/Jobs/BulkBackupSetDeleteJob.php b/app/Jobs/BulkBackupSetDeleteJob.php index 22bc167..fce556e 100644 --- a/app/Jobs/BulkBackupSetDeleteJob.php +++ b/app/Jobs/BulkBackupSetDeleteJob.php @@ -82,14 +82,6 @@ public function handle(BulkOperationService $service): void continue; } - if ($backupSet->restoreRuns()->withTrashed()->exists()) { - $service->recordSkippedWithReason($run, (string) $backupSet->id, 'Referenced by restore runs'); - $skipped++; - $skipReasons['Referenced by restore runs'] = ($skipReasons['Referenced by restore runs'] ?? 0) + 1; - - continue; - } - $backupSet->delete(); $service->recordSuccess($run); $succeeded++; diff --git a/app/Models/RestoreRun.php b/app/Models/RestoreRun.php index fe6bdcf..28945c4 100644 --- a/app/Models/RestoreRun.php +++ b/app/Models/RestoreRun.php @@ -32,7 +32,7 @@ public function tenant(): BelongsTo public function backupSet(): BelongsTo { - return $this->belongsTo(BackupSet::class); + return $this->belongsTo(BackupSet::class)->withTrashed(); } public function scopeDeletable($query) diff --git a/tests/Feature/BulkDeleteBackupSetsTest.php b/tests/Feature/BulkDeleteBackupSetsTest.php index 1dc7427..260b458 100644 --- a/tests/Feature/BulkDeleteBackupSetsTest.php +++ b/tests/Feature/BulkDeleteBackupSetsTest.php @@ -4,6 +4,7 @@ use App\Models\BackupItem; use App\Models\BackupSet; use App\Models\BulkOperationRun; +use App\Models\RestoreRun; use App\Models\Tenant; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -55,6 +56,35 @@ expect($bulkRun->status)->toBe('completed'); }); +test('backup sets can be archived even when referenced by restore runs', function () { + $tenant = Tenant::factory()->create(); + $tenant->makeCurrent(); + $user = User::factory()->create(); + + $set = BackupSet::create([ + 'tenant_id' => $tenant->id, + 'name' => 'Backup', + 'status' => 'completed', + 'item_count' => 0, + ]); + + $restoreRun = RestoreRun::create([ + 'tenant_id' => $tenant->id, + 'backup_set_id' => $set->id, + 'status' => 'completed', + 'is_dry_run' => true, + 'requested_by' => 'tester@example.com', + ]); + + Livewire::actingAs($user) + ->test(BackupSetResource\Pages\ListBackupSets::class) + ->callTableBulkAction('bulk_delete', collect([$set])) + ->assertHasNoTableBulkActionErrors(); + + expect(BackupSet::withTrashed()->find($set->id)?->trashed())->toBeTrue(); + expect(RestoreRun::withTrashed()->find($restoreRun->id))->not->toBeNull(); +}); + test('backup sets table bulk archive requires type-to-confirm for 10+ sets', function () { $tenant = Tenant::factory()->create(); $tenant->makeCurrent(); diff --git a/tests/Feature/Filament/HousekeepingTest.php b/tests/Feature/Filament/HousekeepingTest.php index 3030ae0..5967be1 100644 --- a/tests/Feature/Filament/HousekeepingTest.php +++ b/tests/Feature/Filament/HousekeepingTest.php @@ -53,7 +53,7 @@ ]); }); -test('backup set archive is blocked when restore runs exist', function () { +test('backup set can be archived when restore runs exist', function () { $tenant = Tenant::create([ 'tenant_id' => 'tenant-2', 'name' => 'Tenant 2', @@ -65,7 +65,7 @@ 'status' => 'completed', ]); - RestoreRun::create([ + $restoreRun = RestoreRun::create([ 'tenant_id' => $tenant->id, 'backup_set_id' => $backupSet->id, 'status' => 'completed', @@ -77,12 +77,13 @@ Livewire::test(ListBackupSets::class) ->callTableAction('archive', $backupSet); - $this->assertDatabaseMissing('audit_logs', [ + $this->assertSoftDeleted('backup_sets', ['id' => $backupSet->id]); + $this->assertDatabaseHas('audit_logs', [ 'resource_type' => 'backup_set', 'resource_id' => (string) $backupSet->id, 'action' => 'backup.deleted', ]); - $this->assertDatabaseHas('backup_sets', ['id' => $backupSet->id, 'deleted_at' => null]); + $this->assertDatabaseHas('restore_runs', ['id' => $restoreRun->id]); }); test('backup set can be force deleted when trashed and unused', function () { diff --git a/tests/Unit/BulkBackupSetDeleteJobTest.php b/tests/Unit/BulkBackupSetDeleteJobTest.php index fa44a5d..03c9e4c 100644 --- a/tests/Unit/BulkBackupSetDeleteJobTest.php +++ b/tests/Unit/BulkBackupSetDeleteJobTest.php @@ -55,7 +55,7 @@ }); }); -test('bulk backup set delete job skips sets referenced by restore runs', function () { +test('bulk backup set delete job archives sets even when referenced by restore runs', function () { $tenant = Tenant::factory()->create(); $user = User::factory()->create(); @@ -82,10 +82,10 @@ $run->refresh(); expect($run->status)->toBe('completed') ->and($run->processed_items)->toBe(1) - ->and($run->succeeded)->toBe(0) + ->and($run->succeeded)->toBe(1) ->and($run->failed)->toBe(0) - ->and($run->skipped)->toBe(1); + ->and($run->skipped)->toBe(0); - expect(collect($run->failures)->pluck('reason')->join(' '))->toContain('restore runs'); - expect(BackupSet::withTrashed()->find($set->id)?->trashed())->toBeFalse(); + expect(BackupSet::withTrashed()->find($set->id)?->trashed())->toBeTrue(); + expect(RestoreRun::query()->where('backup_set_id', $set->id)->exists())->toBeTrue(); });