create([ 'workspace_id' => $workspaceId, 'is_enabled' => true, ]); $rule = AlertRule::factory()->create([ 'workspace_id' => $workspaceId, 'event_type' => $eventType, 'minimum_severity' => $minSeverity, 'is_enabled' => true, 'cooldown_seconds' => $cooldownSeconds, ]); $rule->destinations()->attach($destination->getKey(), [ 'workspace_id' => $workspaceId, ]); return [$rule, $destination]; } // (1) Alert rule for EVENT_PERMISSION_MISSING with min severity=high + finding of severity=high → delivery queued it('queues delivery when permission missing finding matches alert rule severity', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $workspaceId = (int) session()->get(WorkspaceContext::SESSION_KEY); [$rule, $destination] = createAlertRuleWithDestination( $workspaceId, AlertRule::EVENT_PERMISSION_MISSING, 'high', ); // Create a high-severity permission posture finding $finding = Finding::factory()->permissionPosture()->create([ 'workspace_id' => $workspaceId, 'tenant_id' => $tenant->getKey(), 'severity' => Finding::SEVERITY_HIGH, 'status' => Finding::STATUS_NEW, ]); $event = [ 'event_type' => AlertRule::EVENT_PERMISSION_MISSING, 'tenant_id' => (int) $tenant->getKey(), 'severity' => Finding::SEVERITY_HIGH, 'fingerprint_key' => 'finding:'.(int) $finding->getKey(), 'title' => 'Missing permission detected', 'body' => 'Permission "DeviceManagementConfiguration.ReadWrite.All" is missing.', 'metadata' => ['finding_id' => (int) $finding->getKey()], ]; $dispatchService = app(AlertDispatchService::class); $workspace = \App\Models\Workspace::query()->find($workspaceId); $created = $dispatchService->dispatchEvent($workspace, $event); expect($created)->toBe(1); $delivery = AlertDelivery::query() ->where('workspace_id', $workspaceId) ->where('event_type', AlertRule::EVENT_PERMISSION_MISSING) ->first(); expect($delivery)->not->toBeNull() ->and($delivery->status)->toBe(AlertDelivery::STATUS_QUEUED) ->and($delivery->severity)->toBe(Finding::SEVERITY_HIGH); }); // (2) Alert rule with min severity=critical + finding of severity=high → no delivery queued it('does not queue delivery when finding severity is below minimum', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $workspaceId = (int) session()->get(WorkspaceContext::SESSION_KEY); [$rule, $destination] = createAlertRuleWithDestination( $workspaceId, AlertRule::EVENT_PERMISSION_MISSING, 'critical', // minimum severity is critical ); $event = [ 'event_type' => AlertRule::EVENT_PERMISSION_MISSING, 'tenant_id' => (int) $tenant->getKey(), 'severity' => Finding::SEVERITY_HIGH, // high < critical — should not match 'fingerprint_key' => 'finding:999', 'title' => 'Missing permission detected', 'body' => 'Permission "DeviceManagementConfiguration.ReadWrite.All" is missing.', 'metadata' => [], ]; $dispatchService = app(AlertDispatchService::class); $workspace = \App\Models\Workspace::query()->find($workspaceId); $created = $dispatchService->dispatchEvent($workspace, $event); expect($created)->toBe(0); }); // (3) Same missing permission across two runs → cooldown prevents duplicate delivery it('suppresses duplicate delivery via fingerprint cooldown', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $workspaceId = (int) session()->get(WorkspaceContext::SESSION_KEY); [$rule, $destination] = createAlertRuleWithDestination( $workspaceId, AlertRule::EVENT_PERMISSION_MISSING, 'high', cooldownSeconds: 3600, // 1 hour cooldown ); $finding = Finding::factory()->permissionPosture()->create([ 'workspace_id' => $workspaceId, 'tenant_id' => $tenant->getKey(), 'severity' => Finding::SEVERITY_HIGH, 'status' => Finding::STATUS_NEW, ]); $event = [ 'event_type' => AlertRule::EVENT_PERMISSION_MISSING, 'tenant_id' => (int) $tenant->getKey(), 'severity' => Finding::SEVERITY_HIGH, 'fingerprint_key' => 'finding:'.(int) $finding->getKey(), 'title' => 'Missing permission detected', 'body' => 'Finding '.$finding->getKey().' was created with severity high.', 'metadata' => ['finding_id' => (int) $finding->getKey()], ]; $dispatchService = app(AlertDispatchService::class); $workspace = \App\Models\Workspace::query()->find($workspaceId); // First dispatch — should create a QUEUED delivery $firstCreated = $dispatchService->dispatchEvent($workspace, $event); expect($firstCreated)->toBe(1); // Second dispatch — same fingerprint within cooldown → SUPPRESSED $secondCreated = $dispatchService->dispatchEvent($workspace, $event); expect($secondCreated)->toBe(1); $deliveries = AlertDelivery::query() ->where('workspace_id', $workspaceId) ->where('event_type', AlertRule::EVENT_PERMISSION_MISSING) ->orderBy('id') ->get(); expect($deliveries)->toHaveCount(2) ->and($deliveries[0]->status)->toBe(AlertDelivery::STATUS_QUEUED) ->and($deliveries[1]->status)->toBe(AlertDelivery::STATUS_SUPPRESSED); }); // (4) Resolved findings do not produce alert events via permissionMissingEvents() it('resolved findings are excluded from permissionMissingEvents', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $workspaceId = (int) session()->get(WorkspaceContext::SESSION_KEY); // Create a resolved finding — should not appear in events Finding::factory()->permissionPosture()->resolved()->create([ 'workspace_id' => $workspaceId, 'tenant_id' => $tenant->getKey(), 'severity' => Finding::SEVERITY_HIGH, ]); // Use reflection to call the private permissionMissingEvents method $job = new \App\Jobs\Alerts\EvaluateAlertsJob($workspaceId); $reflection = new ReflectionMethod($job, 'permissionMissingEvents'); $events = $reflection->invoke( $job, $workspaceId, \Carbon\CarbonImmutable::now('UTC')->subHours(1), ); expect($events)->toBe([]); });