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
336 lines
14 KiB
PHP
336 lines
14 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
|
|
use App\Filament\Pages\TenantDashboard;
|
|
use App\Filament\Resources\EvidenceSnapshotResource;
|
|
use App\Filament\Resources\ReviewPackResource;
|
|
use App\Filament\Resources\TenantReviewResource;
|
|
use App\Filament\Widgets\Dashboard\RecoveryReadiness;
|
|
use App\Models\BackupItem;
|
|
use App\Models\BackupSet;
|
|
use App\Models\Finding;
|
|
use App\Models\ProviderConnection;
|
|
use App\Models\ReviewPack;
|
|
use App\Services\Intune\TenantRequiredPermissionsViewModelBuilder;
|
|
use App\Support\Links\RequiredPermissionsLinks;
|
|
use App\Support\TenantDashboard\TenantDashboardSummaryBuilder;
|
|
use Filament\Facades\Filament;
|
|
use Livewire\Livewire;
|
|
|
|
use function Pest\Laravel\mock;
|
|
|
|
function mockTenantDashboardReadinessPermissions(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 recovery-readiness seam as a productization baseline', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$backupSet = BackupSet::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'name' => 'Productization baseline backup',
|
|
'item_count' => 1,
|
|
'completed_at' => now()->subMinutes(15),
|
|
]);
|
|
|
|
BackupItem::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'backup_set_id' => (int) $backupSet->getKey(),
|
|
'payload' => ['id' => 'baseline-policy'],
|
|
'metadata' => [],
|
|
'assignments' => [],
|
|
]);
|
|
|
|
$this->actingAs($user);
|
|
Filament::setTenant($tenant, true);
|
|
|
|
Livewire::test(RecoveryReadiness::class)
|
|
->assertSee('Backup posture')
|
|
->assertSee('Healthy');
|
|
});
|
|
|
|
it('surfaces customer-safe output honestly when evidence exists but no review pack is ready', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
mockTenantDashboardReadinessPermissions();
|
|
|
|
seedTenantReviewEvidence($tenant);
|
|
|
|
$summary = app(TenantDashboardSummaryBuilder::class)
|
|
->build($tenant, $user)
|
|
->toArray();
|
|
|
|
$outputCard = collect($summary['readinessCards'])->firstWhere('key', 'customer_safe_output');
|
|
|
|
expect($outputCard)
|
|
->not->toBeNull()
|
|
->and($outputCard['status'])->toBe('Evidence available')
|
|
->and($outputCard['actionLabel'])->toBe('View export artifacts')
|
|
->and($outputCard['actionUrl'])->toBe(CustomerReviewWorkspace::tenantPrefilterUrl($tenant));
|
|
});
|
|
|
|
it('links ready customer-safe output directly to the latest review pack', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
mockTenantDashboardReadinessPermissions();
|
|
|
|
$snapshot = seedTenantReviewEvidence($tenant);
|
|
$review = composeTenantReviewForTest($tenant, $user, $snapshot);
|
|
$pack = ReviewPack::factory()->ready()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'tenant_review_id' => (int) $review->getKey(),
|
|
'evidence_snapshot_id' => (int) $snapshot->getKey(),
|
|
]);
|
|
|
|
$summary = app(TenantDashboardSummaryBuilder::class)
|
|
->build($tenant, $user)
|
|
->toArray();
|
|
|
|
$outputCard = collect($summary['readinessCards'])->firstWhere('key', 'customer_safe_output');
|
|
|
|
expect($outputCard)
|
|
->not->toBeNull()
|
|
->and($outputCard['actionLabel'])->toBe('Open review pack')
|
|
->and($outputCard['actionUrl'])->toBe(ReviewPackResource::getUrl('view', ['record' => $pack], panel: 'tenant', tenant: $tenant))
|
|
->and($outputCard['helperText'])->toBeNull();
|
|
});
|
|
|
|
it('uses required-permissions truth for provider blockage readiness summaries', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
mockTenantDashboardReadinessPermissions([
|
|
'overall' => 'blocked',
|
|
'counts' => [
|
|
'missing_application' => 2,
|
|
'missing_delegated' => 1,
|
|
],
|
|
'freshness' => [
|
|
'is_stale' => true,
|
|
],
|
|
]);
|
|
|
|
ProviderConnection::factory()->platform()->consentGranted()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'is_default' => true,
|
|
'verification_status' => 'blocked',
|
|
'last_health_check_at' => now()->subMinutes(12),
|
|
'display_name' => 'Microsoft Graph',
|
|
]);
|
|
|
|
$summary = app(TenantDashboardSummaryBuilder::class)
|
|
->build($tenant, $user)
|
|
->toArray();
|
|
|
|
$providerPermissions = collect($summary['governanceStatus'])->firstWhere('label', 'Provider permissions');
|
|
$providerHealth = collect($summary['readinessCards'])->firstWhere('key', 'provider_health');
|
|
|
|
expect($providerPermissions)
|
|
->not->toBeNull()
|
|
->and($providerPermissions['value'])->toBe('Blocked')
|
|
->and($providerPermissions['tone'])->toBe('danger')
|
|
->and($providerPermissions['description'])->toContain('2 application permission(s) are still missing.')
|
|
->and($providerPermissions['description'])->toContain('The verification snapshot is stale.')
|
|
->and($providerHealth)
|
|
->not->toBeNull()
|
|
->and($providerHealth['headline'])->toBe('Microsoft Graph')
|
|
->and($providerHealth['status'])->toBe('Blocked')
|
|
->and($providerHealth['body'])->toContain('2 application permission(s) are still missing.')
|
|
->and(collect($providerHealth['meta'])->firstWhere('label', 'Missing permissions')['value'] ?? null)->toBe('3');
|
|
});
|
|
|
|
it('keeps readiness follow-up destinations tenant-scoped across review, evidence, output, and permissions surfaces', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
mockTenantDashboardReadinessPermissions();
|
|
|
|
$snapshot = seedTenantReviewEvidence($tenant);
|
|
$review = composeTenantReviewForTest($tenant, $user, $snapshot);
|
|
|
|
$summary = app(TenantDashboardSummaryBuilder::class)
|
|
->build($tenant, $user)
|
|
->toArray();
|
|
|
|
$currentReview = collect($summary['readinessCards'])->firstWhere('key', 'current_review');
|
|
$providerHealth = collect($summary['readinessCards'])->firstWhere('key', 'provider_health');
|
|
$outputCard = collect($summary['readinessCards'])->firstWhere('key', 'customer_safe_output');
|
|
$evidenceCoverage = collect($summary['governanceStatus'])->firstWhere('label', 'Evidence coverage');
|
|
$providerPermissions = collect($summary['governanceStatus'])->firstWhere('label', 'Provider permissions');
|
|
|
|
expect($currentReview)
|
|
->not->toBeNull()
|
|
->and($currentReview['actionUrl'])->toBe(TenantReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant))
|
|
->and($evidenceCoverage)
|
|
->not->toBeNull()
|
|
->and($evidenceCoverage['actionUrl'])->toBe(EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], panel: 'tenant', tenant: $tenant))
|
|
->and($outputCard)
|
|
->not->toBeNull()
|
|
->and($outputCard['actionUrl'])->toBe(CustomerReviewWorkspace::tenantPrefilterUrl($tenant))
|
|
->and($providerHealth)
|
|
->not->toBeNull()
|
|
->and($providerHealth['actionUrl'])->toBe(RequiredPermissionsLinks::requiredPermissions($tenant))
|
|
->and($providerPermissions)
|
|
->not->toBeNull()
|
|
->and($providerPermissions['actionUrl'])->toBe(RequiredPermissionsLinks::requiredPermissions($tenant));
|
|
});
|
|
|
|
it('surfaces current-review progress only from repo-real review summary metrics', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
mockTenantDashboardReadinessPermissions();
|
|
|
|
Finding::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'status' => Finding::STATUS_RESOLVED,
|
|
'resolved_reason' => Finding::RESOLVE_REASON_REMEDIATED,
|
|
'resolved_at' => now(),
|
|
]);
|
|
|
|
Finding::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'status' => Finding::STATUS_RISK_ACCEPTED,
|
|
]);
|
|
|
|
$snapshot = seedTenantReviewEvidence($tenant, findingCount: 1, driftCount: 0);
|
|
$review = composeTenantReviewForTest($tenant, $user, $snapshot);
|
|
$reviewSummary = is_array($review->summary) ? $review->summary : [];
|
|
$completedSections = (int) ($reviewSummary['section_state_counts']['complete'] ?? 0);
|
|
$totalSections = max(1, (int) ($reviewSummary['section_count'] ?? 0));
|
|
$reviewCompletionLabel = sprintf(
|
|
'%d/%d (%d%%)',
|
|
$completedSections,
|
|
$totalSections,
|
|
(int) round(($completedSections / $totalSections) * 100),
|
|
);
|
|
|
|
$summary = app(TenantDashboardSummaryBuilder::class)
|
|
->build($tenant, $user)
|
|
->toArray();
|
|
|
|
$currentReview = collect($summary['readinessCards'])->firstWhere('key', 'current_review');
|
|
$progress = collect($currentReview['progress'] ?? []);
|
|
|
|
expect($currentReview)
|
|
->not->toBeNull()
|
|
->and($progress)->toHaveCount(2)
|
|
->and($progress->pluck('key')->all())->toBe(['findings_with_outcome', 'review_completion'])
|
|
->and($progress->pluck('key')->contains('evidence_attachment'))->toBeFalse()
|
|
->and($progress->firstWhere('key', 'findings_with_outcome')['valueLabel'] ?? null)->toBe('2/3 (67%)')
|
|
->and($progress->firstWhere('key', 'review_completion')['valueLabel'] ?? null)->toBe($reviewCompletionLabel);
|
|
});
|
|
|
|
it('renders current-review progress bars with a fixed visible track height and filament tone colors', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
mockTenantDashboardReadinessPermissions();
|
|
|
|
Finding::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'status' => Finding::STATUS_RESOLVED,
|
|
'resolved_reason' => Finding::RESOLVE_REASON_REMEDIATED,
|
|
'resolved_at' => now(),
|
|
]);
|
|
|
|
Finding::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'status' => Finding::STATUS_RISK_ACCEPTED,
|
|
]);
|
|
|
|
$snapshot = seedTenantReviewEvidence($tenant, findingCount: 1, driftCount: 0);
|
|
composeTenantReviewForTest($tenant, $user, $snapshot);
|
|
|
|
$this->actingAs($user);
|
|
setTenantPanelContext($tenant);
|
|
|
|
$content = $this->get(TenantDashboard::getUrl(panel: 'tenant', tenant: $tenant))
|
|
->assertSuccessful()
|
|
->getContent();
|
|
|
|
expect(substr_count($content, 'role="progressbar"'))->toBeGreaterThanOrEqual(2)
|
|
->and($content)->toContain('style="height: 0.5rem;"')
|
|
->and($content)->toContain('background-color: var(--primary-500);')
|
|
->and($content)->toContain('background-color: var(--warning-500);');
|
|
});
|
|
|
|
it('omits current-review progress bars when the review summary has no real denominators', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
mockTenantDashboardReadinessPermissions();
|
|
|
|
$snapshot = seedTenantReviewEvidence($tenant);
|
|
$review = composeTenantReviewForTest($tenant, $user, $snapshot);
|
|
|
|
$review->forceFill([
|
|
'summary' => [],
|
|
])->save();
|
|
|
|
$summary = app(TenantDashboardSummaryBuilder::class)
|
|
->build($tenant, $user)
|
|
->toArray();
|
|
|
|
$currentReview = collect($summary['readinessCards'])->firstWhere('key', 'current_review');
|
|
|
|
expect($currentReview)
|
|
->not->toBeNull()
|
|
->and($currentReview['progress'] ?? null)->toBe([]);
|
|
});
|
|
|
|
it('shows honest fallback states when review and evidence artifacts are not available yet', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
mockTenantDashboardReadinessPermissions();
|
|
|
|
$summary = app(TenantDashboardSummaryBuilder::class)
|
|
->build($tenant, $user)
|
|
->toArray();
|
|
|
|
$currentReview = collect($summary['readinessCards'])->firstWhere('key', 'current_review');
|
|
$providerHealth = collect($summary['readinessCards'])->firstWhere('key', 'provider_health');
|
|
$outputCard = collect($summary['readinessCards'])->firstWhere('key', 'customer_safe_output');
|
|
$evidenceCoverage = collect($summary['governanceStatus'])->firstWhere('label', 'Evidence coverage');
|
|
|
|
expect($currentReview)
|
|
->not->toBeNull()
|
|
->and($currentReview['status'])->toBe('No active review')
|
|
->and($currentReview['body'])->toBe('There is currently no review in progress for this tenant.')
|
|
->and($currentReview['actionUrl'])->toBe(TenantReviewResource::tenantScopedUrl('index', tenant: $tenant))
|
|
->and($providerHealth)
|
|
->not->toBeNull()
|
|
->and($providerHealth['status'])->toBe('Provider status unavailable')
|
|
->and($providerHealth['body'])->toBe('No provider health snapshot is currently available for this tenant.')
|
|
->and($providerHealth['actionUrl'])->toBe(RequiredPermissionsLinks::requiredPermissions($tenant))
|
|
->and($outputCard)
|
|
->not->toBeNull()
|
|
->and($outputCard['status'])->toBe('No customer-safe output available')
|
|
->and($outputCard['body'])->toBe('Generate a review pack once review and evidence are ready for handoff.')
|
|
->and($outputCard['actionLabel'])->toBe('View export artifacts')
|
|
->and($outputCard['actionUrl'])->toBe(CustomerReviewWorkspace::tenantPrefilterUrl($tenant))
|
|
->and($evidenceCoverage)
|
|
->not->toBeNull()
|
|
->and($evidenceCoverage['value'])->toBe('Unavailable')
|
|
->and($evidenceCoverage['description'])->toBe('No evidence snapshot is currently available for customer-safe output.')
|
|
->and($evidenceCoverage['actionUrl'])->toBe(EvidenceSnapshotResource::getUrl('index', panel: 'tenant', tenant: $tenant));
|
|
});
|