TenantAtlas/tests/Feature/Filament/Alerts/AlertDeliveryViewerTest.php
ahmido b15d1950b4 feat: add cross-resource navigation cohesion (#160)
## Summary
- add a shared cross-resource navigation layer with canonical navigation context and related-context rendering
- wire findings, policy versions, baseline snapshots, backup sets, and canonical operations surfaces into consistent drill-down flows
- extend focused Pest coverage for canonical operations links, related navigation, and tenant-context preservation

## Testing
- focused Pest coverage for spec 131 was added and the task list marks the implementation verification and Pint steps as completed

## Follow-up
- manual QA checklist item `T036` in `specs/131-cross-resource-navigation/tasks.md` is still open and should be completed during review

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #160
2026-03-10 16:08:14 +00:00

294 lines
10 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 App\Support\Workspaces\WorkspaceContext;
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]);
});
it('preselects the tenant filter when a tenant context exists', function (): void {
$tenant = Tenant::factory()->create();
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
$this->actingAs($user);
Filament::setTenant($tenant, true);
Livewire::test(ListAlertDeliveries::class)
->assertSet('tableFilters.tenant_id.value', (string) $tenant->getKey());
});
it('scopes alert deliveries to the remembered tenant context when filament tenant is absent', 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,
]);
Filament::setTenant(null, true);
app(WorkspaceContext::class)->rememberLastTenantId($workspaceId, (int) $tenantA->getKey());
$this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => $workspaceId]);
Livewire::test(ListAlertDeliveries::class)
->assertSet('tableFilters.tenant_id.value', (string) $tenantA->getKey())
->assertCanSeeTableRecords([$tenantADelivery])
->assertCanNotSeeTableRecords([$tenantBDelivery]);
});