TenantAtlas/tests/Feature/Filament/DashboardKpisWidgetTest.php
ahmido 3a2a06e8d7 feat: align tenant dashboard truth surfaces (#204)
## Summary
- align tenant dashboard KPI, attention, compare, and operations truth so the page does not read calmer than the tenant's actual state
- preserve tenant-safe drill-through continuity into findings, baseline compare, and canonical operations, including disabled helper states for permission-limited members
- add the Spec 173 artifact set and focused regression coverage for dashboard truth alignment and drill-through behavior

## Validation
- `vendor/bin/sail bin pint --dirty --format agent`
- `vendor/bin/sail artisan test --compact tests/Feature/Filament/DashboardKpisWidgetTest.php tests/Feature/Filament/TenantDashboardTruthAlignmentTest.php tests/Feature/Monitoring/OperationsDashboardDrillthroughTest.php tests/Feature/Filament/NeedsAttentionWidgetTest.php tests/Feature/Filament/BaselineCompareNowWidgetTest.php tests/Feature/Filament/BaselineCompareSummaryConsistencyTest.php tests/Feature/Findings/FindingsListDefaultsTest.php tests/Feature/Findings/FindingsListFiltersTest.php tests/Feature/Findings/FindingAdminTenantParityTest.php tests/Feature/OpsUx/CanonicalViewRunLinksTest.php tests/Feature/Filament/TenantDashboardTenantScopeTest.php tests/Feature/Filament/TenantDashboardDbOnlyTest.php tests/Feature/Filament/TableStandardsBaselineTest.php tests/Feature/Filament/TableDetailVisibilityTest.php`
- integrated browser smoke on the tenant dashboard, including a permission-limited member scenario

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #204
2026-04-03 20:26:15 +00:00

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,
]);
});