actingAs($user); $tenant->makeCurrent(); Filament::setTenant($tenant, true); $backupSet = BackupSet::factory()->create([ 'tenant_id' => $tenant->id, 'name' => 'Test backup', ]); $policies = Policy::factory()->count(2)->create([ 'tenant_id' => $tenant->id, 'ignored_at' => null, 'last_synced_at' => now(), ]); $this->mock(BackupService::class, function (MockInterface $mock) { $mock->shouldReceive('addPoliciesToSet')->never(); }); bindFailHardGraphClient(); Livewire::actingAs($user) ->test(BackupSetPolicyPickerTable::class, [ 'backupSetId' => $backupSet->id, ]) ->callTableBulkAction('add_selected_to_backup_set', $policies) ->assertHasNoTableBulkActionErrors(); Queue::assertPushed(AddPoliciesToBackupSetJob::class, 1); $policyIds = $policies ->pluck('id') ->map(fn (mixed $value): int => (int) $value) ->sort() ->values() ->all(); $run = OperationRun::query() ->where('tenant_id', $tenant->id) ->where('type', 'backup_set.add_policies') ->latest('id') ->first(); expect($run)->not->toBeNull(); expect($run?->status)->toBe('queued'); expect($run?->outcome)->toBe('pending'); expect($run?->context['backup_set_id'] ?? null)->toBe($backupSet->getKey()); expect($run?->context['policy_count'] ?? null)->toBe(count($policyIds)); expect($run?->context['operation']['type'] ?? null)->toBe('backup_set.add_policies'); expect($run?->context['selection']['kind'] ?? null)->toBe('ids'); expect($run?->context['idempotency']['fingerprint'] ?? null)->not->toBeNull(); $notifications = session('filament.notifications', []); expect($notifications)->not->toBeEmpty(); expect(collect($notifications)->last()['title'] ?? null)->toBe('Backup set update queued'); }); test('policy picker table reuses an active run on double click (idempotency)', function () { Queue::fake(); [$user, $tenant] = createUserWithTenant(role: 'owner'); $this->actingAs($user); $tenant->makeCurrent(); Filament::setTenant($tenant, true); $backupSet = BackupSet::factory()->create([ 'tenant_id' => $tenant->id, 'name' => 'Test backup', ]); $policies = Policy::factory()->count(2)->create([ 'tenant_id' => $tenant->id, 'ignored_at' => null, 'last_synced_at' => now(), ]); $policyIds = $policies ->pluck('id') ->map(fn (mixed $value): int => (int) $value) ->sort() ->values() ->all(); Livewire::actingAs($user) ->test(BackupSetPolicyPickerTable::class, [ 'backupSetId' => $backupSet->id, ]) ->callTableBulkAction('add_selected_to_backup_set', $policies); Livewire::actingAs($user) ->test(BackupSetPolicyPickerTable::class, [ 'backupSetId' => $backupSet->id, ]) ->callTableBulkAction('add_selected_to_backup_set', $policies); expect(OperationRun::query() ->where('tenant_id', $tenant->id) ->where('type', 'backup_set.add_policies') ->count())->toBe(1); Queue::assertPushed(AddPoliciesToBackupSetJob::class, 1); }); test('policy picker table forbids readonly users from starting add policies (403)', function () { Queue::fake(); [$user, $tenant] = createUserWithTenant(role: 'readonly'); $this->actingAs($user); $tenant->makeCurrent(); Filament::setTenant($tenant, true); $backupSet = BackupSet::factory()->create([ 'tenant_id' => $tenant->id, 'name' => 'Test backup', ]); $policies = Policy::factory()->count(1)->create([ 'tenant_id' => $tenant->id, 'ignored_at' => null, 'last_synced_at' => now(), ]); $thrown = null; try { Livewire::actingAs($user) ->test(BackupSetPolicyPickerTable::class, [ 'backupSetId' => $backupSet->id, ]) ->callTableBulkAction('add_selected_to_backup_set', $policies); } catch (Throwable $exception) { $thrown = $exception; } expect($thrown)->not->toBeNull(); Queue::assertNothingPushed(); expect(OperationRun::query() ->where('tenant_id', $tenant->id) ->where('type', 'backup_set.add_policies') ->exists())->toBeFalse(); }); test('policy picker table rejects cross-tenant starts (403) with no run records created', function () { Queue::fake(); $tenantA = Tenant::factory()->create(); $tenantB = Tenant::factory()->create(); $user = User::factory()->create(); $user->tenants()->syncWithoutDetaching([ $tenantA->getKey() => ['role' => 'owner'], $tenantB->getKey() => ['role' => 'owner'], ]); $this->actingAs($user); $tenantA->makeCurrent(); Filament::setTenant($tenantA, true); $backupSetB = BackupSet::factory()->create([ 'tenant_id' => $tenantB->id, 'name' => 'Tenant B backup', ]); $policiesB = Policy::factory()->count(1)->create([ 'tenant_id' => $tenantB->id, 'ignored_at' => null, 'last_synced_at' => now(), ]); $thrown = null; try { Livewire::actingAs($user) ->test(BackupSetPolicyPickerTable::class, [ 'backupSetId' => $backupSetB->id, ]) ->callTableBulkAction('add_selected_to_backup_set', $policiesB); } catch (Throwable $exception) { $thrown = $exception; } expect($thrown)->not->toBeNull(); Queue::assertNothingPushed(); expect(OperationRun::query() ->where('tenant_id', $tenantA->id) ->where('type', 'backup_set.add_policies') ->exists())->toBeFalse(); expect(OperationRun::query() ->where('tenant_id', $tenantB->id) ->where('type', 'backup_set.add_policies') ->exists())->toBeFalse(); }); test('policy picker table can filter by has versions', function () { $tenant = Tenant::factory()->create(); $tenant->makeCurrent(); $user = User::factory()->create(); $backupSet = BackupSet::factory()->create([ 'tenant_id' => $tenant->id, 'name' => 'Test backup', ]); $withVersions = Policy::factory()->create([ 'tenant_id' => $tenant->id, 'display_name' => 'With Versions', 'ignored_at' => null, 'last_synced_at' => now(), ]); PolicyVersion::factory()->create([ 'tenant_id' => $tenant->id, 'policy_id' => $withVersions->id, 'policy_type' => $withVersions->policy_type, 'platform' => $withVersions->platform, ]); $withoutVersions = Policy::factory()->create([ 'tenant_id' => $tenant->id, 'display_name' => 'Without Versions', 'ignored_at' => null, 'last_synced_at' => now(), ]); Livewire::actingAs($user) ->test(BackupSetPolicyPickerTable::class, [ 'backupSetId' => $backupSet->id, ]) ->filterTable('has_versions', '1') ->assertSee('With Versions') ->assertDontSee('Without Versions'); });