TenantAtlas/tests/Feature/Filament/Alerts/AlertDeliveryViewerTest.php
ahmido a490261eca feat: standardize filter UX across key resources (#154)
## Summary
- standardize filter UX across key Filament resources with shared thin filter helpers for centralized option sourcing and archived/date-range presets
- add persistence, essential filters, and OperationCatalog-aligned labels across the targeted resource tables
- add and extend focused Pest coverage for guards, persistence, filter behavior, scope safety, and the new Spec 126 planning artifacts

## Spec 126
- add the full Spec 126 artifact set under `specs/126-filter-ux-standardization/`
- align spec, plan, research, data model, quickstart, contract, checklist, and tasks for implementation readiness

## Validation
- `vendor/bin/sail bin pint --dirty --format agent`
- `vendor/bin/sail artisan test --compact tests/Feature/Guards/FilamentTableStandardsGuardTest.php tests/Feature/Filament/TableStatePersistenceTest.php tests/Feature/Findings/FindingsListFiltersTest.php tests/Feature/Findings/FindingsListDefaultsTest.php tests/Feature/Alerts/AlertDeliveryDeepLinkFiltersTest.php tests/Feature/Filament/Alerts/AlertDeliveryViewerTest.php tests/Feature/Filament/OperationRunListFiltersTest.php tests/Feature/Filament/PolicyVersionListFiltersTest.php tests/Feature/Filament/RestoreRunListFiltersTest.php tests/Feature/Filament/InventoryItemListFiltersTest.php tests/Feature/Filament/BaselineProfileListFiltersTest.php tests/Feature/ProviderConnections/TenantFilterOverrideTest.php tests/Feature/Rbac/InventoryItemResourceAuthorizationTest.php tests/Feature/Filament/BaselineTenantAssignmentsRelationManagerTest.php`

## Notes
- no new OperationRun lifecycle or operational workflow behavior is introduced; only existing OperationRun table filter-label alignment and related coverage are in scope
- existing authorization and action-surface semantics remain unchanged

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #154
2026-03-09 06:27:22 +00:00

234 lines
8.0 KiB
PHP

<?php
declare(strict_types=1);
use App\Filament\Resources\AlertDeliveryResource;
use App\Filament\Resources\AlertDeliveryResource\Pages\ListAlertDeliveries;
use App\Filament\Resources\AlertRuleResource;
use App\Models\AlertDelivery;
use App\Models\AlertDestination;
use App\Models\AlertRule;
use App\Models\Tenant;
use App\Models\User;
use App\Models\Workspace;
use App\Models\WorkspaceMembership;
use App\Services\Auth\WorkspaceCapabilityResolver;
use Filament\Actions\Action;
use Filament\Facades\Filament;
use Livewire\Features\SupportTesting\Testable;
use Livewire\Livewire;
function getAlertDeliveryEmptyStateAction(Testable $component, string $name): ?Action
{
foreach ($component->instance()->getTable()->getEmptyStateActions() as $action) {
if ($action instanceof Action && $action->getName() === $name) {
return $action;
}
}
return null;
}
function getAlertDeliveryHeaderAction(Testable $component, string $name): ?Action
{
$instance = $component->instance();
$instance->cacheInteractsWithHeaderActions();
foreach ($instance->getCachedHeaderActions() as $action) {
if ($action instanceof Action && $action->getName() === $name) {
return $action;
}
}
return null;
}
it('lists only deliveries for entitled tenants', function (): void {
[$user, $tenantA] = createUserWithTenant(role: 'readonly');
$tenantB = Tenant::factory()->create([
'workspace_id' => (int) $tenantA->workspace_id,
]);
$workspaceId = (int) $tenantA->workspace_id;
$rule = AlertRule::factory()->create([
'workspace_id' => $workspaceId,
]);
$destination = AlertDestination::factory()->create([
'workspace_id' => $workspaceId,
]);
$tenantADelivery = AlertDelivery::factory()->create([
'workspace_id' => $workspaceId,
'tenant_id' => (int) $tenantA->getKey(),
'alert_rule_id' => (int) $rule->getKey(),
'alert_destination_id' => (int) $destination->getKey(),
'event_type' => 'high_drift',
]);
$tenantBDelivery = AlertDelivery::factory()->create([
'workspace_id' => $workspaceId,
'tenant_id' => (int) $tenantB->getKey(),
'alert_rule_id' => (int) $rule->getKey(),
'alert_destination_id' => (int) $destination->getKey(),
'event_type' => 'compare_failed',
]);
$this->actingAs($user);
Livewire::test(ListAlertDeliveries::class)
->assertCanSeeTableRecords([$tenantADelivery])
->assertCanNotSeeTableRecords([$tenantBDelivery]);
});
it('shows a guided empty state on alert deliveries and links to alert rules', function (): void {
[$user] = createUserWithTenant(role: 'owner');
$this->actingAs($user);
$component = Livewire::test(ListAlertDeliveries::class)
->assertTableEmptyStateActionsExistInOrder(['view_alert_rules'])
->assertSee('No alert deliveries')
->assertSee('Deliveries appear automatically when alert rules fire.');
$table = $component->instance()->getTable();
expect($table->getEmptyStateHeading())->toBe('No alert deliveries');
expect($table->getEmptyStateDescription())->toBe('Deliveries appear automatically when alert rules fire.');
expect($table->getEmptyStateIcon())->toBe('heroicon-o-bell-alert');
$action = getAlertDeliveryEmptyStateAction($component, 'view_alert_rules');
expect($action)->not->toBeNull();
expect($action?->getLabel())->toBe('View alert rules');
expect($action?->getUrl())->toBe(AlertRuleResource::getUrl(panel: 'admin'));
expect(getAlertDeliveryHeaderAction($component, 'view_alert_rules'))->toBeNull();
});
it('keeps alert deliveries header-action free after records exist', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$workspaceId = (int) $tenant->workspace_id;
$rule = AlertRule::factory()->create([
'workspace_id' => $workspaceId,
]);
$destination = AlertDestination::factory()->create([
'workspace_id' => $workspaceId,
]);
$delivery = AlertDelivery::factory()->create([
'workspace_id' => $workspaceId,
'tenant_id' => (int) $tenant->getKey(),
'alert_rule_id' => (int) $rule->getKey(),
'alert_destination_id' => (int) $destination->getKey(),
]);
$this->actingAs($user);
$component = Livewire::test(ListAlertDeliveries::class)
->assertCanSeeTableRecords([$delivery]);
expect(getAlertDeliveryHeaderAction($component, 'view_alert_rules'))->toBeNull();
});
it('returns 404 when a member from another workspace tries to view a delivery', function (): void {
[$user] = createUserWithTenant(role: 'owner');
$otherWorkspace = Workspace::factory()->create();
$tenant = Tenant::factory()->create([
'workspace_id' => (int) $otherWorkspace->getKey(),
]);
$rule = AlertRule::factory()->create([
'workspace_id' => (int) $otherWorkspace->getKey(),
]);
$destination = AlertDestination::factory()->create([
'workspace_id' => (int) $otherWorkspace->getKey(),
]);
$delivery = AlertDelivery::factory()->create([
'workspace_id' => (int) $otherWorkspace->getKey(),
'tenant_id' => (int) $tenant->getKey(),
'alert_rule_id' => (int) $rule->getKey(),
'alert_destination_id' => (int) $destination->getKey(),
]);
$this->actingAs($user)
->get(AlertDeliveryResource::getUrl('view', ['record' => $delivery], panel: 'admin'))
->assertNotFound();
});
it('returns 403 for members missing alerts view capability on deliveries index', function (): void {
$workspace = Workspace::factory()->create();
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $user->getKey(),
'role' => 'readonly',
]);
$resolver = \Mockery::mock(WorkspaceCapabilityResolver::class);
$resolver->shouldReceive('isMember')->andReturnTrue();
$resolver->shouldReceive('can')->andReturnFalse();
app()->instance(WorkspaceCapabilityResolver::class, $resolver);
session()->put(\App\Support\Workspaces\WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
$this->actingAs($user)
->get(AlertDeliveryResource::getUrl(panel: 'admin'))
->assertForbidden();
});
it('keeps persisted alert delivery filters inside the active tenant scope', function (): void {
$tenantA = Tenant::factory()->create();
[$user, $tenantA] = createUserWithTenant(tenant: $tenantA, role: 'owner');
$tenantB = Tenant::factory()->create([
'workspace_id' => (int) $tenantA->workspace_id,
]);
createUserWithTenant(tenant: $tenantB, user: $user, role: 'owner');
$workspaceId = (int) $tenantA->workspace_id;
$rule = AlertRule::factory()->create([
'workspace_id' => $workspaceId,
]);
$destination = AlertDestination::factory()->create([
'workspace_id' => $workspaceId,
]);
$tenantADelivery = AlertDelivery::factory()->create([
'workspace_id' => $workspaceId,
'tenant_id' => (int) $tenantA->getKey(),
'alert_rule_id' => (int) $rule->getKey(),
'alert_destination_id' => (int) $destination->getKey(),
'status' => AlertDelivery::STATUS_SENT,
]);
$tenantBDelivery = AlertDelivery::factory()->create([
'workspace_id' => $workspaceId,
'tenant_id' => (int) $tenantB->getKey(),
'alert_rule_id' => (int) $rule->getKey(),
'alert_destination_id' => (int) $destination->getKey(),
'status' => AlertDelivery::STATUS_SENT,
]);
$this->actingAs($user);
Filament::setTenant($tenantA, true);
Livewire::test(ListAlertDeliveries::class)
->filterTable('status', AlertDelivery::STATUS_SENT)
->assertCanSeeTableRecords([$tenantADelivery])
->assertCanNotSeeTableRecords([$tenantBDelivery]);
Livewire::test(ListAlertDeliveries::class)
->assertSet('tableFilters.status.value', AlertDelivery::STATUS_SENT)
->assertCanSeeTableRecords([$tenantADelivery])
->assertCanNotSeeTableRecords([$tenantBDelivery]);
});