create(); $tenant = Tenant::factory()->create(); // No membership created $this->actingAs($user); $tenant->makeCurrent(); Filament::setTenant($tenant, true); Livewire::test(ListPolicies::class) ->assertActionHidden('sync'); Queue::assertNothingPushed(); }); it('hides sync action for authenticated users accessing wrong tenant', function () { // User is member of tenantA but accessing tenantB [$user, $tenantA] = createUserWithTenant(role: 'owner'); $tenantB = Tenant::factory()->create(); // User has no membership to tenantB $this->actingAs($user); $tenantB->makeCurrent(); Filament::setTenant($tenantB, true); Livewire::test(ListPolicies::class) ->assertActionHidden('sync'); Queue::assertNothingPushed(); }); }); describe('US2: Non-member action execution is blocked', function () { beforeEach(function () { Queue::fake(); }); it('blocks action execution for non-members (no side effects)', function () { $user = User::factory()->create(); $tenant = Tenant::factory()->create(); // No membership $this->actingAs($user); $tenant->makeCurrent(); Filament::setTenant($tenant, true); // Hidden actions are treated as disabled by Filament // The action call returns 200 but no execution occurs Livewire::test(ListPolicies::class) ->mountAction('sync') ->callMountedAction() ->assertSuccessful(); // Verify no side effects Queue::assertNothingPushed(); expect(OperationRun::query()->where('tenant_id', $tenant->getKey())->count())->toBe(0); }); }); describe('US2: Membership revoked mid-session still enforces protection', function () { beforeEach(function () { Queue::fake(); }); it('blocks action execution when membership is revoked between page load and action click', function () { bindFailHardGraphClient(); [$user, $tenant] = createUserWithTenant(role: 'owner'); $this->actingAs($user); $tenant->makeCurrent(); Filament::setTenant($tenant, true); // Start the test - action should be visible for member $component = Livewire::test(ListPolicies::class) ->assertActionVisible('sync') ->assertActionEnabled('sync'); // Simulate membership revocation mid-session $user->tenants()->detach($tenant->getKey()); // Clear capability cache to ensure fresh check app(\App\Services\Auth\CapabilityResolver::class)->clearCache(); // Now try to execute - action is now hidden (via fresh isVisible evaluation) // Filament blocks execution (returns 200 but no side effects) $component ->mountAction('sync') ->callMountedAction() ->assertSuccessful(); // Verify no side effects Queue::assertNothingPushed(); expect(OperationRun::query()->where('tenant_id', $tenant->getKey())->count())->toBe(0); }); it('hides action in UI after membership revocation on re-render', function () { [$user, $tenant] = createUserWithTenant(role: 'owner'); $this->actingAs($user); $tenant->makeCurrent(); Filament::setTenant($tenant, true); // Initial state - action visible Livewire::test(ListPolicies::class) ->assertActionVisible('sync'); // Revoke membership $user->tenants()->detach($tenant->getKey()); app(\App\Services\Auth\CapabilityResolver::class)->clearCache(); // New component instance (simulates page refresh) Livewire::test(ListPolicies::class) ->assertActionHidden('sync'); Queue::assertNothingPushed(); }); });