TenantAtlas/apps/platform/tests/Feature/Alerts/SlaDueAlertTest.php
ahmido e64bae9cfc feat: cut over tenant core to managed environments (#335)
## Summary
- replace the legacy Tenant and TenantMembership core models with ManagedEnvironment and ManagedEnvironmentMembership
- propagate the managed environment naming and key changes across Filament resources, pages, controllers, jobs, models, and supporting runtime paths
- add feature 279 spec artifacts and focused managed-environment test coverage for model behavior, route binding, panel context, authorization, and legacy guardrails

## Validation
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/ManagedEnvironment/LegacyTenantCoreGuardTest.php tests/Feature/ManagedEnvironment/ManagedEnvironmentAuthorizationTest.php tests/Feature/ManagedEnvironment/ManagedEnvironmentPanelContextTest.php tests/Feature/ManagedEnvironment/ManagedEnvironmentRouteBindingTest.php tests/Unit/ManagedEnvironment/ManagedEnvironmentContextResolverTest.php tests/Unit/ManagedEnvironment/ManagedEnvironmentModelTest.php`
- `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`

## Notes
- branch pushed from commit `1123b122`
- browser smoke test file was added but not run in this pass

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #335
2026-05-07 06:38:14 +00:00

235 lines
7.9 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\AlertRule;
use App\Models\Finding;
use App\Models\ManagedEnvironment;
use App\Support\Workspaces\WorkspaceContext;
use Carbon\CarbonImmutable;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
afterEach(function (): void {
CarbonImmutable::setTestNow();
});
/**
* @return array<int, array<string, mixed>>
*/
function invokeSlaDueEvents(int $workspaceId, CarbonImmutable $windowStart): array
{
$job = new \App\Jobs\Alerts\EvaluateAlertsJob($workspaceId);
$reflection = new ReflectionMethod($job, 'slaDueEvents');
/** @var array<int, array<string, mixed>> $events */
$events = $reflection->invoke($job, $workspaceId, $windowStart);
return $events;
}
it('produces one sla due event per tenant and summarizes current overdue open findings', function (): void {
$now = CarbonImmutable::parse('2026-02-24T12:00:00Z');
CarbonImmutable::setTestNow($now);
[$user, $tenantA] = createUserWithTenant(role: 'owner');
$workspaceId = (int) session()->get(WorkspaceContext::SESSION_KEY);
$tenantB = ManagedEnvironment::factory()->create(['workspace_id' => $workspaceId]);
$tenantC = ManagedEnvironment::factory()->create(['workspace_id' => $workspaceId]);
$windowStart = $now->subHour();
Finding::factory()->create([
'workspace_id' => $workspaceId,
'managed_environment_id' => $tenantA->getKey(),
'status' => Finding::STATUS_IN_PROGRESS,
'severity' => Finding::SEVERITY_CRITICAL,
'due_at' => $now->subMinutes(10),
]);
Finding::factory()->create([
'workspace_id' => $workspaceId,
'managed_environment_id' => $tenantA->getKey(),
'status' => Finding::STATUS_TRIAGED,
'severity' => Finding::SEVERITY_HIGH,
'due_at' => $now->subDays(1),
]);
Finding::factory()->create([
'workspace_id' => $workspaceId,
'managed_environment_id' => $tenantA->getKey(),
'status' => Finding::STATUS_NEW,
'severity' => Finding::SEVERITY_MEDIUM,
'due_at' => $windowStart,
]);
Finding::factory()->create([
'workspace_id' => $workspaceId,
'managed_environment_id' => $tenantA->getKey(),
'status' => Finding::STATUS_RESOLVED,
'severity' => Finding::SEVERITY_LOW,
'due_at' => $now->subMinutes(5),
]);
Finding::factory()->create([
'workspace_id' => $workspaceId,
'managed_environment_id' => $tenantB->getKey(),
'status' => Finding::STATUS_REOPENED,
'severity' => Finding::SEVERITY_HIGH,
'due_at' => $now->subDays(2),
]);
Finding::factory()->create([
'workspace_id' => $workspaceId,
'managed_environment_id' => $tenantC->getKey(),
'status' => Finding::STATUS_NEW,
'severity' => Finding::SEVERITY_LOW,
'due_at' => $now->subMinutes(20),
]);
$events = invokeSlaDueEvents($workspaceId, $windowStart);
expect($events)->toHaveCount(2);
$eventsByTenant = collect($events)->keyBy(static fn (array $event): int => (int) $event['managed_environment_id']);
expect($eventsByTenant->keys()->all())
->toEqualCanonicalizing([(int) $tenantA->getKey(), (int) $tenantC->getKey()]);
$tenantAEvent = $eventsByTenant->get((int) $tenantA->getKey());
expect($tenantAEvent)
->not->toBeNull()
->and($tenantAEvent['event_type'])->toBe(AlertRule::EVENT_SLA_DUE)
->and($tenantAEvent['severity'])->toBe(Finding::SEVERITY_CRITICAL)
->and($tenantAEvent['metadata'])->toMatchArray([
'overdue_total' => 3,
'overdue_by_severity' => [
'critical' => 1,
'high' => 1,
'medium' => 1,
'low' => 0,
],
])
->and($tenantAEvent['metadata'])->not->toHaveKey('finding_ids');
$tenantCEvent = $eventsByTenant->get((int) $tenantC->getKey());
expect($tenantCEvent)
->not->toBeNull()
->and($tenantCEvent['severity'])->toBe(Finding::SEVERITY_LOW)
->and($tenantCEvent['metadata'])->toMatchArray([
'overdue_total' => 1,
'overdue_by_severity' => [
'critical' => 0,
'high' => 0,
'medium' => 0,
'low' => 1,
],
]);
});
it('gates sla due events to newly overdue open findings after window start', function (): void {
$now = CarbonImmutable::parse('2026-02-24T12:00:00Z');
CarbonImmutable::setTestNow($now);
[$user, $tenant] = createUserWithTenant(role: 'owner');
$workspaceId = (int) session()->get(WorkspaceContext::SESSION_KEY);
$windowStart = $now->subHour();
Finding::factory()->create([
'workspace_id' => $workspaceId,
'managed_environment_id' => $tenant->getKey(),
'status' => Finding::STATUS_NEW,
'severity' => Finding::SEVERITY_HIGH,
'due_at' => $now->subDays(1),
]);
Finding::factory()->create([
'workspace_id' => $workspaceId,
'managed_environment_id' => $tenant->getKey(),
'status' => Finding::STATUS_NEW,
'severity' => Finding::SEVERITY_MEDIUM,
'due_at' => $windowStart,
]);
Finding::factory()->create([
'workspace_id' => $workspaceId,
'managed_environment_id' => $tenant->getKey(),
'status' => Finding::STATUS_CLOSED,
'severity' => Finding::SEVERITY_CRITICAL,
'due_at' => $now->subMinutes(5),
]);
expect(invokeSlaDueEvents($workspaceId, $windowStart))->toBe([]);
});
it('uses a stable fingerprint per tenant and alert window', function (): void {
$now = CarbonImmutable::parse('2026-02-24T12:00:00Z');
CarbonImmutable::setTestNow($now);
[$user, $tenant] = createUserWithTenant(role: 'owner');
$workspaceId = (int) session()->get(WorkspaceContext::SESSION_KEY);
Finding::factory()->create([
'workspace_id' => $workspaceId,
'managed_environment_id' => $tenant->getKey(),
'status' => Finding::STATUS_NEW,
'severity' => Finding::SEVERITY_HIGH,
'due_at' => $now->subMinute(),
]);
$windowA = $now->subMinutes(5);
$windowB = $now->subMinutes(2);
$first = invokeSlaDueEvents($workspaceId, $windowA);
$second = invokeSlaDueEvents($workspaceId, $windowA);
$third = invokeSlaDueEvents($workspaceId, $windowB);
expect($first)->toHaveCount(1)
->and($second)->toHaveCount(1)
->and($third)->toHaveCount(1)
->and($first[0]['fingerprint_key'])->toBe($second[0]['fingerprint_key'])
->and($first[0]['fingerprint_key'])->not->toBe($third[0]['fingerprint_key']);
});
it('keeps aggregate sla due alerts separate from finding-level due soon reminders', function (): void {
$now = CarbonImmutable::parse('2026-04-22T12:00:00Z');
CarbonImmutable::setTestNow($now);
[$user, $tenant] = createUserWithTenant(role: 'owner');
$workspaceId = (int) session()->get(WorkspaceContext::SESSION_KEY);
Finding::factory()->create([
'workspace_id' => $workspaceId,
'managed_environment_id' => $tenant->getKey(),
'status' => Finding::STATUS_TRIAGED,
'severity' => Finding::SEVERITY_HIGH,
'due_at' => $now->subHour(),
]);
Finding::factory()->create([
'workspace_id' => $workspaceId,
'managed_environment_id' => $tenant->getKey(),
'status' => Finding::STATUS_IN_PROGRESS,
'severity' => Finding::SEVERITY_CRITICAL,
'due_at' => $now->addHours(6),
]);
$events = invokeSlaDueEvents($workspaceId, $now->subDay());
expect($events)->toHaveCount(1)
->and($events[0]['event_type'])->toBe(AlertRule::EVENT_SLA_DUE)
->and($events[0]['metadata'])->toMatchArray([
'overdue_total' => 1,
'overdue_by_severity' => [
'critical' => 0,
'high' => 1,
'medium' => 0,
'low' => 0,
],
]);
});