177 lines
8.1 KiB
PHP
177 lines
8.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\AlertDelivery;
|
|
use App\Models\Finding;
|
|
use App\Models\FindingException;
|
|
use App\Models\OperationRun;
|
|
use App\Models\Tenant;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use App\Support\OperationRunType;
|
|
use App\Support\Workspaces\WorkspaceOverviewBuilder;
|
|
|
|
it('prioritizes governance-critical tenants above activity-only and alert-only items', function (): void {
|
|
$tenantGovernance = Tenant::factory()->create(['status' => 'active']);
|
|
[$user, $tenantGovernance] = createUserWithTenant($tenantGovernance, role: 'owner', workspaceRole: 'readonly');
|
|
[$profile, $snapshot] = seedActiveBaselineForTenant($tenantGovernance);
|
|
seedBaselineCompareRun($tenantGovernance, $profile, $snapshot, workspaceOverviewCompareCoverage());
|
|
|
|
Finding::factory()->for($tenantGovernance)->create([
|
|
'workspace_id' => (int) $tenantGovernance->workspace_id,
|
|
'status' => Finding::STATUS_TRIAGED,
|
|
'due_at' => now()->subDay(),
|
|
]);
|
|
|
|
$tenantActivity = Tenant::factory()->create([
|
|
'status' => 'active',
|
|
'workspace_id' => (int) $tenantGovernance->workspace_id,
|
|
'name' => 'Busy Tenant',
|
|
]);
|
|
createUserWithTenant($tenantActivity, $user, role: 'owner', workspaceRole: 'readonly');
|
|
[$activityProfile, $activitySnapshot] = seedActiveBaselineForTenant($tenantActivity);
|
|
seedBaselineCompareRun($tenantActivity, $activityProfile, $activitySnapshot, workspaceOverviewCompareCoverage());
|
|
|
|
OperationRun::factory()->create([
|
|
'tenant_id' => (int) $tenantActivity->getKey(),
|
|
'workspace_id' => (int) $tenantActivity->workspace_id,
|
|
'type' => OperationRunType::InventorySync->value,
|
|
'status' => OperationRunStatus::Running->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
]);
|
|
|
|
$tenantAlerts = Tenant::factory()->create([
|
|
'status' => 'active',
|
|
'workspace_id' => (int) $tenantGovernance->workspace_id,
|
|
'name' => 'Alerts Tenant',
|
|
]);
|
|
createUserWithTenant($tenantAlerts, $user, role: 'owner', workspaceRole: 'readonly');
|
|
[$alertsProfile, $alertsSnapshot] = seedActiveBaselineForTenant($tenantAlerts);
|
|
seedBaselineCompareRun($tenantAlerts, $alertsProfile, $alertsSnapshot, workspaceOverviewCompareCoverage());
|
|
|
|
AlertDelivery::factory()->create([
|
|
'tenant_id' => (int) $tenantAlerts->getKey(),
|
|
'workspace_id' => (int) $tenantAlerts->workspace_id,
|
|
'status' => AlertDelivery::STATUS_FAILED,
|
|
'created_at' => now(),
|
|
]);
|
|
|
|
$workspace = $tenantGovernance->workspace()->firstOrFail();
|
|
$overview = app(WorkspaceOverviewBuilder::class)->build($workspace, $user);
|
|
|
|
expect($overview['calmness']['is_calm'])->toBeFalse()
|
|
->and($overview['attention_items'])->toHaveCount(3)
|
|
->and($overview['attention_items'][0]['key'])->toBe('tenant_overdue_findings')
|
|
->and($overview['attention_items'][0]['tenant_label'])->toBe((string) $tenantGovernance->name)
|
|
->and($overview['attention_items'][1]['key'])->toBe('tenant_active_operations')
|
|
->and($overview['attention_items'][2]['key'])->toBe('tenant_alert_delivery_failures');
|
|
});
|
|
|
|
it('surfaces expiring governance and stale compare posture as governance attention without a false calm state', function (): void {
|
|
$tenantExpiring = Tenant::factory()->create(['status' => 'active']);
|
|
[$user, $tenantExpiring] = createUserWithTenant($tenantExpiring, role: 'owner', workspaceRole: 'readonly');
|
|
[$expiringProfile, $expiringSnapshot] = seedActiveBaselineForTenant($tenantExpiring);
|
|
seedBaselineCompareRun($tenantExpiring, $expiringProfile, $expiringSnapshot, workspaceOverviewCompareCoverage());
|
|
|
|
$finding = Finding::factory()->riskAccepted()->create([
|
|
'workspace_id' => (int) $tenantExpiring->workspace_id,
|
|
'tenant_id' => (int) $tenantExpiring->getKey(),
|
|
]);
|
|
|
|
FindingException::query()->create([
|
|
'workspace_id' => (int) $tenantExpiring->workspace_id,
|
|
'tenant_id' => (int) $tenantExpiring->getKey(),
|
|
'finding_id' => (int) $finding->getKey(),
|
|
'requested_by_user_id' => (int) $user->getKey(),
|
|
'owner_user_id' => (int) $user->getKey(),
|
|
'approved_by_user_id' => (int) $user->getKey(),
|
|
'status' => FindingException::STATUS_EXPIRING,
|
|
'current_validity_state' => FindingException::VALIDITY_EXPIRING,
|
|
'request_reason' => 'Governance nearly expired',
|
|
'approval_reason' => 'Accepted for a short window',
|
|
'requested_at' => now()->subDays(2),
|
|
'approved_at' => now()->subDay(),
|
|
'effective_from' => now()->subDay(),
|
|
'expires_at' => now()->addDay(),
|
|
'review_due_at' => now()->addDay(),
|
|
'evidence_summary' => ['reference_count' => 0],
|
|
]);
|
|
|
|
$tenantStale = Tenant::factory()->create([
|
|
'status' => 'active',
|
|
'workspace_id' => (int) $tenantExpiring->workspace_id,
|
|
'name' => 'Stale Tenant',
|
|
]);
|
|
createUserWithTenant($tenantStale, $user, role: 'owner', workspaceRole: 'readonly');
|
|
[$staleProfile, $staleSnapshot] = seedActiveBaselineForTenant($tenantStale);
|
|
seedBaselineCompareRun(
|
|
$tenantStale,
|
|
$staleProfile,
|
|
$staleSnapshot,
|
|
workspaceOverviewCompareCoverage(),
|
|
completedAt: now()->subDays(10),
|
|
);
|
|
|
|
$workspace = $tenantExpiring->workspace()->firstOrFail();
|
|
$overview = app(WorkspaceOverviewBuilder::class)->build($workspace, $user);
|
|
$items = collect($overview['attention_items'])->keyBy('key');
|
|
|
|
expect($overview['calmness']['is_calm'])->toBeFalse()
|
|
->and($items->keys()->all())->toContain('tenant_expiring_governance', 'tenant_compare_attention')
|
|
->and($items->get('tenant_compare_attention')['badge'])->toBe('Baseline')
|
|
->and($items->get('tenant_expiring_governance')['family'])->toBe('governance');
|
|
});
|
|
|
|
it('ranks lapsed governance and failed compare posture ahead of high-severity findings', function (): void {
|
|
$tenantLapsed = Tenant::factory()->create(['status' => 'active']);
|
|
[$user, $tenantLapsed] = createUserWithTenant($tenantLapsed, role: 'owner', workspaceRole: 'readonly');
|
|
[$lapsedProfile, $lapsedSnapshot] = seedActiveBaselineForTenant($tenantLapsed);
|
|
seedBaselineCompareRun($tenantLapsed, $lapsedProfile, $lapsedSnapshot, workspaceOverviewCompareCoverage());
|
|
|
|
Finding::factory()->riskAccepted()->create([
|
|
'workspace_id' => (int) $tenantLapsed->workspace_id,
|
|
'tenant_id' => (int) $tenantLapsed->getKey(),
|
|
]);
|
|
|
|
$tenantFailedCompare = Tenant::factory()->create([
|
|
'status' => 'active',
|
|
'workspace_id' => (int) $tenantLapsed->workspace_id,
|
|
'name' => 'Failed Compare Tenant',
|
|
]);
|
|
createUserWithTenant($tenantFailedCompare, $user, role: 'owner', workspaceRole: 'readonly');
|
|
[$failedProfile, $failedSnapshot] = seedActiveBaselineForTenant($tenantFailedCompare);
|
|
seedBaselineCompareRun(
|
|
$tenantFailedCompare,
|
|
$failedProfile,
|
|
$failedSnapshot,
|
|
workspaceOverviewCompareCoverage(),
|
|
outcome: OperationRunOutcome::Failed->value,
|
|
);
|
|
|
|
$tenantHighSeverity = Tenant::factory()->create([
|
|
'status' => 'active',
|
|
'workspace_id' => (int) $tenantLapsed->workspace_id,
|
|
'name' => 'High Severity Tenant',
|
|
]);
|
|
createUserWithTenant($tenantHighSeverity, $user, role: 'owner', workspaceRole: 'readonly');
|
|
[$highProfile, $highSnapshot] = seedActiveBaselineForTenant($tenantHighSeverity);
|
|
seedBaselineCompareRun($tenantHighSeverity, $highProfile, $highSnapshot, workspaceOverviewCompareCoverage());
|
|
|
|
Finding::factory()->for($tenantHighSeverity)->create([
|
|
'workspace_id' => (int) $tenantHighSeverity->workspace_id,
|
|
'status' => Finding::STATUS_TRIAGED,
|
|
'severity' => Finding::SEVERITY_CRITICAL,
|
|
]);
|
|
|
|
$workspace = $tenantLapsed->workspace()->firstOrFail();
|
|
$overview = app(WorkspaceOverviewBuilder::class)->build($workspace, $user);
|
|
|
|
expect(collect($overview['attention_items'])->pluck('key')->take(3)->all())
|
|
->toBe([
|
|
'tenant_lapsed_governance',
|
|
'tenant_compare_attention',
|
|
'tenant_high_severity_findings',
|
|
]);
|
|
});
|