170 lines
5.9 KiB
PHP
170 lines
5.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Filament\Resources\FindingResource;
|
|
use App\Filament\Widgets\Dashboard\DashboardKpis;
|
|
use App\Models\Finding;
|
|
use App\Models\OperationRun;
|
|
use App\Support\Auth\Capabilities;
|
|
use App\Support\OperationRunLinks;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use App\Support\Rbac\UiTooltips;
|
|
use Filament\Facades\Filament;
|
|
use Filament\Widgets\StatsOverviewWidget\Stat;
|
|
use Illuminate\Support\Facades\Gate;
|
|
use Livewire\Livewire;
|
|
|
|
/**
|
|
* @return array<string, array{value:string,description:string|null,url:string|null}>
|
|
*/
|
|
function dashboardKpiStatPayloads($component): array
|
|
{
|
|
$method = new ReflectionMethod(DashboardKpis::class, 'getStats');
|
|
$method->setAccessible(true);
|
|
|
|
return collect($method->invoke($component->instance()))
|
|
->mapWithKeys(fn (Stat $stat): array => [
|
|
(string) $stat->getLabel() => [
|
|
'value' => (string) $stat->getValue(),
|
|
'description' => $stat->getDescription(),
|
|
'url' => $stat->getUrl(),
|
|
],
|
|
])
|
|
->all();
|
|
}
|
|
|
|
it('aligns dashboard KPI counts and drill-throughs to canonical findings and operations semantics', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
$this->actingAs($user);
|
|
|
|
Finding::factory()->for($tenant)->create([
|
|
'finding_type' => Finding::FINDING_TYPE_DRIFT,
|
|
'status' => Finding::STATUS_NEW,
|
|
'severity' => Finding::SEVERITY_LOW,
|
|
]);
|
|
|
|
Finding::factory()->for($tenant)->create([
|
|
'finding_type' => Finding::FINDING_TYPE_DRIFT,
|
|
'status' => Finding::STATUS_TRIAGED,
|
|
'severity' => Finding::SEVERITY_MEDIUM,
|
|
]);
|
|
|
|
Finding::factory()->for($tenant)->create([
|
|
'finding_type' => Finding::FINDING_TYPE_DRIFT,
|
|
'status' => Finding::STATUS_REOPENED,
|
|
'severity' => Finding::SEVERITY_CRITICAL,
|
|
]);
|
|
|
|
Finding::factory()->for($tenant)->create([
|
|
'finding_type' => Finding::FINDING_TYPE_PERMISSION_POSTURE,
|
|
'status' => Finding::STATUS_IN_PROGRESS,
|
|
'severity' => Finding::SEVERITY_HIGH,
|
|
]);
|
|
|
|
Finding::factory()->for($tenant)->create([
|
|
'finding_type' => Finding::FINDING_TYPE_DRIFT,
|
|
'status' => Finding::STATUS_RESOLVED,
|
|
'severity' => Finding::SEVERITY_HIGH,
|
|
]);
|
|
|
|
OperationRun::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'type' => 'inventory_sync',
|
|
'status' => OperationRunStatus::Queued->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
'created_at' => now()->subMinute(),
|
|
]);
|
|
|
|
OperationRun::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'type' => 'inventory_sync',
|
|
'status' => OperationRunStatus::Queued->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
'created_at' => now()->subHour(),
|
|
]);
|
|
|
|
OperationRun::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'type' => 'policy.sync',
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::PartiallySucceeded->value,
|
|
]);
|
|
|
|
OperationRun::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'type' => 'policy.sync',
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Failed->value,
|
|
]);
|
|
|
|
Filament::setCurrentPanel(Filament::getPanel('tenant'));
|
|
Filament::setTenant($tenant, true);
|
|
|
|
$stats = dashboardKpiStatPayloads(Livewire::test(DashboardKpis::class));
|
|
|
|
expect($stats)->toMatchArray([
|
|
'Open drift findings' => [
|
|
'value' => '3',
|
|
'description' => 'active drift workflow items',
|
|
'url' => FindingResource::getUrl('index', [
|
|
'tab' => 'needs_action',
|
|
'finding_type' => Finding::FINDING_TYPE_DRIFT,
|
|
], panel: 'tenant', tenant: $tenant),
|
|
],
|
|
'High severity active findings' => [
|
|
'value' => '2',
|
|
'description' => 'high or critical findings needing review',
|
|
'url' => FindingResource::getUrl('index', [
|
|
'tab' => 'needs_action',
|
|
'high_severity' => 1,
|
|
], panel: 'tenant', tenant: $tenant),
|
|
],
|
|
'Active operations' => [
|
|
'value' => '1',
|
|
'description' => 'healthy queued or running tenant work',
|
|
'url' => OperationRunLinks::index($tenant, activeTab: 'active'),
|
|
],
|
|
'Operations needing follow-up' => [
|
|
'value' => '3',
|
|
'description' => 'failed, warning, or stalled runs',
|
|
'url' => OperationRunLinks::index($tenant, activeTab: 'blocked'),
|
|
],
|
|
]);
|
|
});
|
|
|
|
it('keeps findings KPI truth visible while disabling dead-end drill-throughs for members without findings access', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
$this->actingAs($user);
|
|
|
|
Finding::factory()->for($tenant)->create([
|
|
'finding_type' => Finding::FINDING_TYPE_DRIFT,
|
|
'status' => Finding::STATUS_NEW,
|
|
'severity' => Finding::SEVERITY_CRITICAL,
|
|
]);
|
|
|
|
Gate::define(Capabilities::TENANT_FINDINGS_VIEW, fn (): bool => false);
|
|
|
|
Filament::setCurrentPanel(Filament::getPanel('tenant'));
|
|
Filament::setTenant($tenant, true);
|
|
|
|
$stats = dashboardKpiStatPayloads(Livewire::test(DashboardKpis::class));
|
|
|
|
expect($stats['Open drift findings'])->toMatchArray([
|
|
'value' => '1',
|
|
'description' => UiTooltips::INSUFFICIENT_PERMISSION,
|
|
'url' => null,
|
|
]);
|
|
|
|
expect($stats['High severity active findings'])->toMatchArray([
|
|
'value' => '1',
|
|
'description' => UiTooltips::INSUFFICIENT_PERMISSION,
|
|
'url' => null,
|
|
]);
|
|
});
|