TenantAtlas/tests/Feature/Filament/Alerts/AlertDeliveryViewerTest.php
ahmido 73a3a62451 Spec 122: Empty state consistency pass (#148)
## Summary
- unify empty-state UX across the six in-scope Filament list pages
- move empty-state ownership toward resource `table()` definitions while preserving existing RBAC behavior
- add focused Pest coverage for empty-state rendering, CTA outcomes, populated-state regression behavior, and action-surface compliance
- add the Spec 122 planning artifacts and product discovery documents used for this pass

## Changed surfaces
- `PolicyResource`
- `BackupSetResource`
- `RestoreRunResource`
- `BackupScheduleResource`
- `WorkspaceResource`
- `AlertDeliveryResource`

## Tests
- `vendor/bin/sail artisan test --compact tests/Feature/Filament/EmptyStateConsistencyTest.php`
- `vendor/bin/sail artisan test --compact tests/Feature/Filament/Alerts/AlertDeliveryViewerTest.php`
- `vendor/bin/sail artisan test --compact tests/Feature/Filament/CreateCtaPlacementTest.php`
- `vendor/bin/sail artisan test --compact tests/Feature/PolicySyncStartSurfaceTest.php`
- `vendor/bin/sail artisan test --compact tests/Feature/BackupScheduling/BackupScheduleLifecycleAuthorizationTest.php`
- `vendor/bin/sail artisan test --compact tests/Feature/Filament/BackupSetUiEnforcementTest.php`
- `vendor/bin/sail artisan test --compact tests/Feature/Filament/RestoreRunUiEnforcementTest.php`
- `vendor/bin/sail artisan test --compact tests/Feature/Guards/ActionSurfaceContractTest.php`
- `vendor/bin/sail bin pint --dirty --format agent`

## Notes
- Filament v5 / Livewire v4.0+ compliance is preserved.
- Panel provider registration remains unchanged in `bootstrap/providers.php`.
- No new globally searchable resources were added.
- Destructive actions were not introduced by this pass.
- Alert Deliveries is documented as the explicit no-header-action exemption for the empty-state CTA relocation rule.
- Manual light/dark visual QA evidence is still expected in the PR/review artifact set for the remaining checklist items (`T018`, `T025`).

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #148
2026-03-08 02:17:51 +00:00

183 lines
6.2 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 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();
});