TenantAtlas/apps/platform/tests/Feature/Filament/TenantDashboardArrivalContextTest.php
ahmido 28e62bd22c feat: preserve portfolio triage arrival context (#218)
## Summary
- preserve portfolio triage arrival context from workspace overview and tenant registry drill-throughs
- add a tenant dashboard continuity widget plus bounded arrival token and resolver support
- add focused Pest coverage for arrival routing, return flow, RBAC degradation, and request-local performance
- include the Spec 187 spec, plan, research, data model, quickstart, contract, and tasks artifacts

## Validation
- integrated browser smoke: workspace overview -> tenant dashboard arrival -> backup sets CTA
- integrated browser smoke: tenant registry triage -> tenant dashboard arrival -> return to tenant triage
- branch includes focused automated test coverage for the new arrival-context surfaces

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #218
2026-04-09 21:38:31 +00:00

167 lines
7.6 KiB
PHP

<?php
declare(strict_types=1);
use App\Filament\Pages\TenantDashboard;
use App\Filament\Resources\BackupSetResource;
use App\Filament\Resources\RestoreRunResource;
use App\Filament\Resources\TenantResource;
use App\Services\Auth\CapabilityResolver;
use App\Support\Auth\Capabilities;
use App\Support\BackupHealth\TenantBackupHealthAssessment;
use App\Support\PortfolioTriage\PortfolioArrivalContextToken;
use App\Support\Rbac\UiTooltips;
use App\Support\RestoreSafety\RestoreResultAttention;
use App\Support\Tenants\TenantRecoveryTriagePresentation;
use Tests\Feature\Concerns\BuildsPortfolioTriageFixtures;
use function Pest\Laravel\mock;
uses(BuildsPortfolioTriageFixtures::class);
function tenantDashboardArrivalUrl(\App\Models\Tenant $tenant, array $state): string
{
return TenantDashboard::getUrl([
PortfolioArrivalContextToken::QUERY_PARAMETER => PortfolioArrivalContextToken::encode($state),
], panel: 'tenant', tenant: $tenant);
}
it('renders source surface, concern, next step, and return flow for workspace backup arrivals', function (): void {
[$user, $tenant] = $this->makePortfolioTriageActor('Workspace Backup Tenant');
$this->actingAs($user);
$arrivalUrl = tenantDashboardArrivalUrl($tenant, [
'sourceSurface' => PortfolioArrivalContextToken::SOURCE_WORKSPACE_OVERVIEW,
'tenantRouteKey' => (string) $tenant->external_id,
'workspaceId' => (int) $tenant->workspace_id,
'concernFamily' => PortfolioArrivalContextToken::FAMILY_BACKUP_HEALTH,
'concernState' => TenantBackupHealthAssessment::POSTURE_ABSENT,
'concernReason' => TenantBackupHealthAssessment::REASON_NO_BACKUP_BASIS,
]);
$this->get($arrivalUrl)
->assertOk()
->assertSee('Triage arrival')
->assertSee('Workspace overview triage')
->assertSee('Backup health')
->assertSee('Absent')
->assertSee('Opened from workspace overview triage because no usable backup basis was visible.')
->assertSee('Open backup sets')
->assertSee('Return to workspace overview')
->assertSee(BackupSetResource::getUrl('index', [
'backup_health_reason' => TenantBackupHealthAssessment::REASON_NO_BACKUP_BASIS,
], panel: 'tenant', tenant: $tenant), false)
->assertSee(route('admin.home'), false);
});
it('renders registry arrival continuity with restore follow-up and preserved return filters', function (): void {
[$user, $tenant] = $this->makePortfolioTriageActor('Registry Recovery Tenant');
$restoreRun = $this->seedPortfolioRecoveryConcern($tenant, RestoreResultAttention::STATE_COMPLETED_WITH_FOLLOW_UP);
$this->actingAs($user);
$returnUrl = TenantResource::getUrl('index', [
'recovery_evidence' => [TenantRecoveryTriagePresentation::RECOVERY_EVIDENCE_WEAKENED],
'triage_sort' => TenantRecoveryTriagePresentation::TRIAGE_SORT_WORST_FIRST,
], panel: 'admin');
$arrivalUrl = tenantDashboardArrivalUrl($tenant, [
'sourceSurface' => PortfolioArrivalContextToken::SOURCE_TENANT_REGISTRY,
'tenantRouteKey' => (string) $tenant->external_id,
'workspaceId' => (int) $tenant->workspace_id,
'concernFamily' => PortfolioArrivalContextToken::FAMILY_RECOVERY_EVIDENCE,
'concernState' => TenantRecoveryTriagePresentation::RECOVERY_EVIDENCE_WEAKENED,
'concernReason' => RestoreResultAttention::STATE_COMPLETED_WITH_FOLLOW_UP,
'returnFilters' => [
'recovery_evidence' => [TenantRecoveryTriagePresentation::RECOVERY_EVIDENCE_WEAKENED],
'triage_sort' => TenantRecoveryTriagePresentation::TRIAGE_SORT_WORST_FIRST,
],
]);
$this->get($arrivalUrl)
->assertOk()
->assertSee('Tenant registry triage')
->assertSee('Recovery evidence')
->assertSee('Weakened')
->assertSee('Open restore run')
->assertSee('Return to tenant triage')
->assertSee(RestoreRunResource::getUrl('view', [
'record' => (int) $restoreRun?->getKey(),
'recovery_posture_reason' => RestoreResultAttention::STATE_COMPLETED_WITH_FOLLOW_UP,
], panel: 'tenant', tenant: $tenant), false)
->assertSee($returnUrl, false);
});
it('suppresses the continuity block for generic or malformed sessions', function (): void {
[$user, $tenant] = $this->makePortfolioTriageActor('Generic Tenant Session');
$this->actingAs($user);
$this->get(TenantDashboard::getUrl(panel: 'tenant', tenant: $tenant))
->assertOk()
->assertDontSee('Triage arrival');
$this->get(TenantDashboard::getUrl([
PortfolioArrivalContextToken::QUERY_PARAMETER => 'not-base64url',
], panel: 'tenant', tenant: $tenant))
->assertOk()
->assertDontSee('Triage arrival');
});
it('keeps the continuity block truthful when current truth has changed and multiple concerns remain visible', function (): void {
[$user, $tenant] = $this->makePortfolioTriageActor('Truth Shift Tenant');
$backupSet = $this->seedPortfolioBackupConcern($tenant, TenantBackupHealthAssessment::POSTURE_STALE);
$this->seedPortfolioRecoveryConcern($tenant, RestoreResultAttention::STATE_PARTIAL, $backupSet);
$this->actingAs($user);
$arrivalUrl = tenantDashboardArrivalUrl($tenant, [
'sourceSurface' => PortfolioArrivalContextToken::SOURCE_TENANT_REGISTRY,
'tenantRouteKey' => (string) $tenant->external_id,
'workspaceId' => (int) $tenant->workspace_id,
'concernFamily' => PortfolioArrivalContextToken::FAMILY_RECOVERY_EVIDENCE,
'concernState' => TenantRecoveryTriagePresentation::RECOVERY_EVIDENCE_UNVALIDATED,
'concernReason' => 'no_history',
'returnFilters' => [
'triage_sort' => TenantRecoveryTriagePresentation::TRIAGE_SORT_WORST_FIRST,
],
]);
$this->get($arrivalUrl)
->assertOk()
->assertSee('Current recovery evidence now looks Weakened.')
->assertSee('Backup posture also still needs follow-up.')
->assertSee('Open restore run');
});
it('degrades the next-step CTA when the operator cannot open deeper follow-up surfaces', function (): void {
[$user, $tenant] = $this->makePortfolioTriageActor('Restricted Arrival Tenant');
$this->actingAs($user);
mock(CapabilityResolver::class, function ($mock) use ($tenant): void {
$mock->shouldReceive('isMember')
->andReturnUsing(static fn ($user, $resolvedTenant): bool => (int) $resolvedTenant->getKey() === (int) $tenant->getKey());
$mock->shouldReceive('can')
->andReturnUsing(static function ($user, $resolvedTenant, string $capability) use ($tenant): bool {
expect((int) $resolvedTenant->getKey())->toBe((int) $tenant->getKey());
return $capability !== Capabilities::TENANT_VIEW;
});
});
$arrivalUrl = tenantDashboardArrivalUrl($tenant, [
'sourceSurface' => PortfolioArrivalContextToken::SOURCE_WORKSPACE_OVERVIEW,
'tenantRouteKey' => (string) $tenant->external_id,
'workspaceId' => (int) $tenant->workspace_id,
'concernFamily' => PortfolioArrivalContextToken::FAMILY_BACKUP_HEALTH,
'concernState' => TenantBackupHealthAssessment::POSTURE_ABSENT,
'concernReason' => TenantBackupHealthAssessment::REASON_NO_BACKUP_BASIS,
]);
$this->get($arrivalUrl)
->assertOk()
->assertSee('Triage arrival')
->assertSee(UiTooltips::INSUFFICIENT_PERMISSION)
->assertDontSee(BackupSetResource::getUrl('index', [
'backup_health_reason' => TenantBackupHealthAssessment::REASON_NO_BACKUP_BASIS,
], panel: 'tenant', tenant: $tenant), false);
});