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(); $key = RunIdempotency::buildKey( tenantId: (int) $tenant->getKey(), operationType: 'backup_set.add_policies', targetId: (string) $backupSet->getKey(), context: [ 'policy_ids' => $policyIds, 'include_assignments' => true, 'include_scope_tags' => true, 'include_foundations' => true, ], ); $run = BulkOperationRun::query() ->where('tenant_id', $tenant->id) ->where('resource', 'backup_set') ->where('action', 'add_policies') ->where('idempotency_key', $key) ->latest('id') ->first(); expect($run)->not->toBeNull(); expect($run?->status)->toBe('pending'); expect($run?->total_items)->toBe(count($policyIds)); expect($run?->item_ids['backup_set_id'] ?? null)->toBe($backupSet->getKey()); expect($run?->item_ids['policy_ids'] ?? null)->toBe($policyIds); expect($run?->item_ids['options']['include_foundations'] ?? null)->toBeTrue(); $notifications = session('filament.notifications', []); expect($notifications)->not->toBeEmpty(); expect(collect($notifications)->last()['title'] ?? null)->toBe('Backup items 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(); $key = RunIdempotency::buildKey( tenantId: (int) $tenant->getKey(), operationType: 'backup_set.add_policies', targetId: (string) $backupSet->getKey(), context: [ 'policy_ids' => $policyIds, 'include_assignments' => true, 'include_scope_tags' => true, 'include_foundations' => true, ], ); 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(BulkOperationRun::query() ->where('tenant_id', $tenant->id) ->where('idempotency_key', $key) ->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(BulkOperationRun::query()->where('tenant_id', $tenant->id)->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(BulkOperationRun::query()->where('tenant_id', $tenantB->id)->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'); });