feat/005-bulk-operations #5

Merged
ahmido merged 25 commits from feat/005-bulk-operations into dev 2025-12-25 13:32:37 +00:00
6 changed files with 42 additions and 29 deletions
Showing only changes of commit 160c5e42a9 - Show all commits

View File

@ -78,16 +78,6 @@ public static function table(Table $table): Table
->requiresConfirmation() ->requiresConfirmation()
->visible(fn (BackupSet $record) => ! $record->trashed()) ->visible(fn (BackupSet $record) => ! $record->trashed())
->action(function (BackupSet $record, AuditLogger $auditLogger) { ->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(); $record->delete();
if ($record->tenant) { if ($record->tenant) {
@ -159,7 +149,7 @@ public static function table(Table $table): Table
return $isOnlyTrashed; 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) { ->form(function (Collection $records) {
if ($records->count() >= 10) { if ($records->count() >= 10) {
return [ return [

View File

@ -82,14 +82,6 @@ public function handle(BulkOperationService $service): void
continue; 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(); $backupSet->delete();
$service->recordSuccess($run); $service->recordSuccess($run);
$succeeded++; $succeeded++;

View File

@ -32,7 +32,7 @@ public function tenant(): BelongsTo
public function backupSet(): BelongsTo public function backupSet(): BelongsTo
{ {
return $this->belongsTo(BackupSet::class); return $this->belongsTo(BackupSet::class)->withTrashed();
} }
public function scopeDeletable($query) public function scopeDeletable($query)

View File

@ -4,6 +4,7 @@
use App\Models\BackupItem; use App\Models\BackupItem;
use App\Models\BackupSet; use App\Models\BackupSet;
use App\Models\BulkOperationRun; use App\Models\BulkOperationRun;
use App\Models\RestoreRun;
use App\Models\Tenant; use App\Models\Tenant;
use App\Models\User; use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
@ -55,6 +56,35 @@
expect($bulkRun->status)->toBe('completed'); 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 () { test('backup sets table bulk archive requires type-to-confirm for 10+ sets', function () {
$tenant = Tenant::factory()->create(); $tenant = Tenant::factory()->create();
$tenant->makeCurrent(); $tenant->makeCurrent();

View File

@ -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 = Tenant::create([
'tenant_id' => 'tenant-2', 'tenant_id' => 'tenant-2',
'name' => 'Tenant 2', 'name' => 'Tenant 2',
@ -65,7 +65,7 @@
'status' => 'completed', 'status' => 'completed',
]); ]);
RestoreRun::create([ $restoreRun = RestoreRun::create([
'tenant_id' => $tenant->id, 'tenant_id' => $tenant->id,
'backup_set_id' => $backupSet->id, 'backup_set_id' => $backupSet->id,
'status' => 'completed', 'status' => 'completed',
@ -77,12 +77,13 @@
Livewire::test(ListBackupSets::class) Livewire::test(ListBackupSets::class)
->callTableAction('archive', $backupSet); ->callTableAction('archive', $backupSet);
$this->assertDatabaseMissing('audit_logs', [ $this->assertSoftDeleted('backup_sets', ['id' => $backupSet->id]);
$this->assertDatabaseHas('audit_logs', [
'resource_type' => 'backup_set', 'resource_type' => 'backup_set',
'resource_id' => (string) $backupSet->id, 'resource_id' => (string) $backupSet->id,
'action' => 'backup.deleted', '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 () { test('backup set can be force deleted when trashed and unused', function () {

View File

@ -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(); $tenant = Tenant::factory()->create();
$user = User::factory()->create(); $user = User::factory()->create();
@ -82,10 +82,10 @@
$run->refresh(); $run->refresh();
expect($run->status)->toBe('completed') expect($run->status)->toBe('completed')
->and($run->processed_items)->toBe(1) ->and($run->processed_items)->toBe(1)
->and($run->succeeded)->toBe(0) ->and($run->succeeded)->toBe(1)
->and($run->failed)->toBe(0) ->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())->toBeTrue();
expect(BackupSet::withTrashed()->find($set->id)?->trashed())->toBeFalse(); expect(RestoreRun::query()->where('backup_set_id', $set->id)->exists())->toBeTrue();
}); });