## 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
167 lines
7.6 KiB
PHP
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);
|
|
});
|