TenantAtlas/apps/platform/tests/Feature/Dashboard/TenantDashboardProductizationSummaryTest.php
ahmido 3aeb0d04b8 Auto: 266-tenant-dashboard-productization-v1 → platform-dev (#322)
Automated PR created by Copilot per user request. Branch pushed: 266-tenant-dashboard-productization-v1

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #322
2026-05-03 14:03:46 +00:00

304 lines
14 KiB
PHP

<?php
declare(strict_types=1);
use App\Filament\Pages\TenantDashboard;
use App\Models\Finding;
use App\Models\OperationRun;
use App\Models\ProviderConnection;
use App\Services\Intune\TenantRequiredPermissionsViewModelBuilder;
use App\Support\OperationCatalog;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\TenantDashboard\TenantDashboardSummaryBuilder;
use Illuminate\Support\Carbon;
use function Pest\Laravel\mock;
function mockTenantDashboardSummaryPermissions(array $overview = []): void
{
mock(TenantRequiredPermissionsViewModelBuilder::class, function ($mock) use ($overview): void {
$mock->shouldReceive('build')->andReturn([
'overview' => array_replace_recursive([
'overall' => 'ready',
'counts' => [
'missing_application' => 0,
'missing_delegated' => 0,
],
'freshness' => [
'is_stale' => false,
'last_refreshed_at' => now()->toIso8601String(),
],
], $overview),
]);
});
}
it('renders the decision-first tenant overview with the capped first-screen structure', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
mockTenantDashboardSummaryPermissions();
[$profile, $snapshot] = seedActiveBaselineForTenant($tenant);
seedBaselineCompareRun($tenant, $profile, $snapshot, workspaceOverviewCompareCoverage());
$backupSet = workspaceOverviewSeedHealthyBackup($tenant);
workspaceOverviewSeedRestoreHistory($tenant, $backupSet);
Finding::factory()->create([
'tenant_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'finding_type' => Finding::FINDING_TYPE_DRIFT,
'severity' => Finding::SEVERITY_HIGH,
'status' => Finding::STATUS_NEW,
]);
OperationRun::factory()->create([
'tenant_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'type' => 'inventory_sync',
'status' => OperationRunStatus::Completed->value,
'outcome' => OperationRunOutcome::Failed->value,
'completed_at' => now()->subHour(),
]);
ProviderConnection::factory()->platform()->consentGranted()->create([
'tenant_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'is_default' => true,
]);
$this->actingAs($user);
setTenantPanelContext($tenant);
$response = $this->get(TenantDashboard::getUrl(panel: 'tenant', tenant: $tenant))
->assertSuccessful()
->assertSee($tenant->name)
->assertSee('Recommended next actions')
->assertSee('Governance status')
->assertSee('Current review')
->assertSee('Risk exceptions')
->assertSee('Provider Health')
->assertSee('Customer-safe output')
->assertSee('Recent operations');
$content = $response->getContent();
$contextChipsPosition = strpos($content, 'data-testid="tenant-dashboard-context-chips"');
$firstKpiPosition = strpos($content, 'data-testid="tenant-dashboard-kpi"');
$governanceStatusCount = substr_count($content, 'data-testid="tenant-dashboard-governance-status"');
$recentOperationCount = substr_count($content, 'data-testid="tenant-dashboard-recent-operation"');
$secondaryListRowCount = substr_count($content, 'data-overview-row-style="secondary-list-row"');
expect(substr_count($content, 'data-testid="tenant-dashboard-kpi"'))->toBe(4)
->and($content)->toContain('data-testid="tenant-dashboard-posture-pill"')
->and($content)->toContain('data-testid="tenant-dashboard-context-chips"')
->and($content)->toContain('lg:grid-cols-[minmax(16rem,1fr)_auto_auto] lg:items-center')
->and($content)->toContain('data-testid="tenant-dashboard-context-chip-workspace"')
->and($content)->toContain('data-testid="tenant-dashboard-context-chip-workspace" class="inline-flex min-w-0 w-full items-center')
->and($content)->toContain('Workspace: '.$tenant->workspace->name)
->and($content)->toContain('data-testid="tenant-dashboard-context-chip-provider"')
->and($content)->toContain('data-testid="tenant-dashboard-context-chip-provider-microsoft-logo"')
->and($content)->toContain('data-provider-key="microsoft"')
->and($content)->toContain('Microsoft tenant')
->and($content)->toContain('data-testid="tenant-dashboard-context-chip-latest-activity" class="inline-flex items-center gap-2 whitespace-nowrap')
->and($content)->toContain('data-testid="tenant-dashboard-context-chip-latest-activity-icon"')
->and($content)->toContain('Latest activity:')
->and($contextChipsPosition)->not->toBeFalse()
->and($firstKpiPosition)->not->toBeFalse()
->and($contextChipsPosition)->toBeLessThan($firstKpiPosition)
->and($secondaryListRowCount)->toBe($governanceStatusCount + $recentOperationCount)
->and($content)->toContain('hover:shadow-md')
->and($content)->toContain('hover:ring-1')
->and(substr_count($content, 'data-kpi-has-icon="true"'))->toBe(4)
->and(substr_count($content, 'data-kpi-has-chart="true"'))->toBe(2)
->and(substr_count($content, 'data-testid="tenant-dashboard-recommended-action"'))->toBeLessThanOrEqual(3)
->and(substr_count($content, 'tenant-dashboard-recommended-actions'))->toBeGreaterThanOrEqual(1)
->and(substr_count($content, 'data-testid="tenant-dashboard-governance-status-icon"'))->toBe(substr_count($content, 'data-testid="tenant-dashboard-governance-status"'))
->and(substr_count($content, 'data-testid="tenant-dashboard-recent-operation-icon"'))->toBe(substr_count($content, 'data-testid="tenant-dashboard-recent-operation"'))
->and(substr_count($content, 'data-testid="tenant-dashboard-readiness-card"'))->toBe(4)
->and($content)->toContain('data-readiness-key="provider_health"')
->and($content)->not->toContain('Open customer workspace')
->and($content)->not->toContain('fixed bottom-4 right-4 z-[999999] w-96 space-y-2')
->and($content)->toContain('High severity findings');
});
it('adds repo-real icon metadata and only supported sparkline series to tenant dashboard kpis', function (): void {
Carbon::setTestNow('2026-05-03 12:00:00');
try {
[$user, $tenant] = createUserWithTenant(role: 'owner');
mockTenantDashboardSummaryPermissions([
'counts' => [
'missing_application' => 2,
'missing_delegated' => 1,
],
]);
[$profile, $snapshot] = seedActiveBaselineForTenant($tenant);
seedBaselineCompareRun($tenant, $profile, $snapshot, workspaceOverviewCompareCoverage());
$backupSet = workspaceOverviewSeedHealthyBackup($tenant);
workspaceOverviewSeedRestoreHistory($tenant, $backupSet);
foreach ([6, 6, 4, 1] as $daysAgo) {
Finding::factory()->create([
'tenant_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'finding_type' => Finding::FINDING_TYPE_DRIFT,
'severity' => $daysAgo === 4 ? Finding::SEVERITY_CRITICAL : Finding::SEVERITY_HIGH,
'status' => Finding::STATUS_NEW,
'first_seen_at' => now()->subDays($daysAgo),
'last_seen_at' => now()->subDays($daysAgo),
]);
}
Finding::factory()->create([
'tenant_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'finding_type' => Finding::FINDING_TYPE_DRIFT,
'severity' => Finding::SEVERITY_MEDIUM,
'status' => Finding::STATUS_NEW,
'first_seen_at' => now()->subDays(2),
'last_seen_at' => now()->subDays(2),
]);
Finding::factory()->create([
'tenant_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'finding_type' => Finding::FINDING_TYPE_DRIFT,
'severity' => Finding::SEVERITY_MEDIUM,
'status' => Finding::STATUS_NEW,
'first_seen_at' => now()->subDays(2),
'last_seen_at' => now()->subDays(2),
'due_at' => now()->subDay(),
]);
foreach ([5, 2, 2] as $daysAgo) {
OperationRun::factory()->create([
'tenant_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'type' => 'inventory_sync',
'status' => OperationRunStatus::Completed->value,
'outcome' => OperationRunOutcome::Failed->value,
'created_at' => now()->subDays($daysAgo)->subHours(3),
'completed_at' => now()->subDays($daysAgo),
]);
}
$kpis = collect(app(TenantDashboardSummaryBuilder::class)
->build($tenant, $user)
->toArray()['kpis'])
->keyBy('key');
expect($kpis->keys()->all())->toBe([
'high_severity_findings',
'overdue_findings',
'missing_permissions',
'active_operations',
])
->and($kpis->pluck('icon')->filter()->count())->toBe(4)
->and($kpis['high_severity_findings']['icon'])->toBe('heroicon-m-arrow-trending-up')
->and($kpis['high_severity_findings']['description'])->toBe('4 active · 4 new in 7d')
->and($kpis['high_severity_findings']['chart'])->toBe([2, 0, 1, 0, 0, 1, 0])
->and($kpis['overdue_findings']['icon'])->toBe('heroicon-m-arrow-trending-up')
->and($kpis['overdue_findings']['description'])->toBe('1 overdue now')
->and($kpis['missing_permissions']['icon'])->toBe('heroicon-m-arrow-trending-up')
->and($kpis['missing_permissions']['description'])->toBe('2 app · 1 delegated missing')
->and($kpis['active_operations']['icon'])->toBe('heroicon-m-arrow-trending-up')
->and($kpis['active_operations']['description'])->toBe('3 need follow-up · 3 in 7d')
->and($kpis['active_operations']['chart'])->toBe([0, 1, 0, 0, 2, 0, 0])
->and($kpis['overdue_findings']['chart'])->toBeNull()
->and($kpis['missing_permissions']['chart'])->toBeNull();
} finally {
Carbon::setTestNow();
}
});
it('adds semantic icon metadata to governance status rows and repo-real recent operation types', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
mockTenantDashboardSummaryPermissions();
OperationRun::factory()->create([
'tenant_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'type' => 'inventory_sync',
'status' => OperationRunStatus::Completed->value,
'outcome' => OperationRunOutcome::Succeeded->value,
'created_at' => now()->subMinutes(3),
'completed_at' => now()->subMinutes(3),
]);
OperationRun::factory()->create([
'tenant_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'type' => 'tenant.review_pack.generate',
'status' => OperationRunStatus::Completed->value,
'outcome' => OperationRunOutcome::Succeeded->value,
'created_at' => now()->subMinutes(2),
'completed_at' => now()->subMinutes(2),
]);
OperationRun::factory()->create([
'tenant_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'type' => OperationCatalog::TYPE_PERMISSION_POSTURE_CHECK,
'status' => OperationRunStatus::Completed->value,
'outcome' => OperationRunOutcome::Succeeded->value,
'created_at' => now()->subMinute(),
'completed_at' => now()->subMinute(),
]);
$summary = app(TenantDashboardSummaryBuilder::class)
->build($tenant, $user)
->toArray();
$governanceStatus = collect($summary['governanceStatus'])->keyBy('key');
$recentOperations = collect($summary['recentOperations'])->keyBy('type');
expect($governanceStatus['baseline_compare']['icon'] ?? null)->toBe('heroicon-m-arrows-right-left')
->and($governanceStatus['evidence_coverage']['icon'] ?? null)->toBe('heroicon-m-document-check')
->and($governanceStatus['review_freshness']['icon'] ?? null)->toBe('heroicon-m-clipboard-document-check')
->and($governanceStatus['provider_permissions']['icon'] ?? null)->toBe('heroicon-m-key')
->and($governanceStatus['backup_posture']['icon'] ?? null)->toBe('heroicon-m-archive-box')
->and($recentOperations['Inventory sync']['icon'] ?? null)->toBe('heroicon-m-arrow-path')
->and($recentOperations['Review pack generation']['icon'] ?? null)->toBe('heroicon-m-document-arrow-down')
->and($recentOperations['Permission posture check']['icon'] ?? null)->toBe('heroicon-m-key');
});
it('shows calm honest fallbacks when no urgent tenant follow-up is visible', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
mockTenantDashboardSummaryPermissions();
[$profile, $snapshot] = seedActiveBaselineForTenant($tenant);
seedBaselineCompareRun($tenant, $profile, $snapshot, workspaceOverviewCompareCoverage());
$backupSet = workspaceOverviewSeedHealthyBackup($tenant);
workspaceOverviewSeedRestoreHistory($tenant, $backupSet);
$this->actingAs($user);
setTenantPanelContext($tenant);
$response = $this->get(TenantDashboard::getUrl(panel: 'tenant', tenant: $tenant))
->assertSuccessful()
->assertSee('No immediate action is waiting.')
->assertSee('Recent operations');
$content = $response->getContent();
$recentOperationCount = substr_count($content, 'data-testid="tenant-dashboard-recent-operation"');
expect(substr_count($content, 'data-testid="tenant-dashboard-recommended-actions-empty"'))->toBe(1)
->and($recentOperationCount)->toBeGreaterThan(0)
->and($recentOperationCount)->toBeLessThanOrEqual(4)
->and($content)->not->toContain('data-testid="tenant-dashboard-recent-operations-empty"');
});