TenantAtlas/apps/platform/tests/Feature/PortfolioCompare/CrossTenantCompareLaunchContextTest.php
ahmido 61feb48d8a
Some checks failed
Main Confidence / confidence (push) Failing after 54s
chore(platform): merge platform-dev into dev (#308)
Integrates latest TenantPilot platform changes from `platform-dev` into `dev`.

Refresh method in this update: merge from `origin/dev` into `platform-dev` on explicit user request.

This PR was created by agent on user request; do not merge automatically.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #308
2026-04-30 07:52:08 +00:00

188 lines
9.2 KiB
PHP

<?php
declare(strict_types=1);
use App\Filament\Pages\CrossTenantComparePage;
use App\Filament\Resources\TenantResource;
use App\Models\Tenant;
use App\Services\Auth\CapabilityResolver;
use App\Services\Auth\WorkspaceCapabilityResolver;
use App\Support\Auth\Capabilities;
use App\Support\BackupHealth\TenantBackupHealthAssessment;
use App\Support\RestoreSafety\RestoreResultAttention;
use App\Support\Tenants\TenantRecoveryTriagePresentation;
use Filament\Actions\Action;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Tests\Feature\Concerns\BuildsPortfolioTriageFixtures;
uses(RefreshDatabase::class, BuildsPortfolioTriageFixtures::class);
function crossTenantCompareLaunchQuery(string $url): array
{
parse_str((string) parse_url($url, PHP_URL_QUERY), $query);
return $query;
}
it('launches cross-tenant compare from the tenant registry with target prefill and return context', function (): void {
[$user, $anchorTenant] = $this->makePortfolioTriageActor('Anchor Tenant');
$targetTenant = $this->makePortfolioTriagePeer($user, $anchorTenant, 'Target Tenant');
$backupSet = $this->seedPortfolioBackupConcern($targetTenant, TenantBackupHealthAssessment::POSTURE_STALE);
$this->seedPortfolioRecoveryConcern($targetTenant, RestoreResultAttention::STATE_COMPLETED_WITH_FOLLOW_UP, $backupSet);
$triageState = $this->portfolioReturnFilters(
[TenantBackupHealthAssessment::POSTURE_STALE],
[TenantRecoveryTriagePresentation::RECOVERY_EVIDENCE_WEAKENED],
[],
TenantRecoveryTriagePresentation::TRIAGE_SORT_WORST_FIRST,
);
$expectedUrl = TenantResource::crossTenantCompareOpenUrl($targetTenant, $triageState);
$this->portfolioTriageRegistryList($user, $anchorTenant, $triageState)
->assertTableActionVisible('compareTenants', $targetTenant)
->assertTableActionHasUrl('compareTenants', $expectedUrl, $targetTenant);
$query = crossTenantCompareLaunchQuery($expectedUrl);
$backUrl = urldecode((string) data_get($query, 'nav.back_url'));
expect($query)->toMatchArray([
'target_tenant_id' => (string) $targetTenant->getKey(),
])
->and(data_get($query, 'nav.source_surface'))->toBe('tenant_registry')
->and(data_get($query, 'nav.tenant_id'))->toBe((string) $targetTenant->getKey())
->and(data_get($query, 'nav.back_label'))->toBe('Back to tenant registry')
->and($backUrl)->toContain('backup_posture[0]='.TenantBackupHealthAssessment::POSTURE_STALE)
->and($backUrl)->toContain('recovery_evidence[0]='.TenantRecoveryTriagePresentation::RECOVERY_EVIDENCE_WEAKENED)
->and($backUrl)->toContain('triage_sort='.TenantRecoveryTriagePresentation::TRIAGE_SORT_WORST_FIRST);
Livewire::withQueryParams($query)
->actingAs($user)
->test(CrossTenantComparePage::class)
->assertSet('sourceTenantId', null)
->assertSet('targetTenantId', (string) $targetTenant->getKey())
->assertActionVisible('return_to_origin')
->assertActionExists('return_to_origin', fn (Action $action): bool => $action->getLabel() === 'Back to tenant registry'
&& $action->getUrl() === TenantResource::getUrl(panel: 'admin', parameters: $triageState));
});
it('launches cross-tenant compare from an exact-two bulk selection with both tenants prefilled', function (): void {
[$user, $anchorTenant] = $this->makePortfolioTriageActor('Anchor Tenant');
$targetTenant = $this->makePortfolioTriagePeer($user, $anchorTenant, 'Target Tenant');
$anchorBackupSet = $this->seedPortfolioBackupConcern($anchorTenant, TenantBackupHealthAssessment::POSTURE_STALE);
$this->seedPortfolioRecoveryConcern($anchorTenant, RestoreResultAttention::STATE_COMPLETED_WITH_FOLLOW_UP, $anchorBackupSet);
$backupSet = $this->seedPortfolioBackupConcern($targetTenant, TenantBackupHealthAssessment::POSTURE_STALE);
$this->seedPortfolioRecoveryConcern($targetTenant, RestoreResultAttention::STATE_COMPLETED_WITH_FOLLOW_UP, $backupSet);
$triageState = $this->portfolioReturnFilters(
[TenantBackupHealthAssessment::POSTURE_STALE],
[TenantRecoveryTriagePresentation::RECOVERY_EVIDENCE_WEAKENED],
[],
TenantRecoveryTriagePresentation::TRIAGE_SORT_WORST_FIRST,
);
$expectedUrl = TenantResource::crossTenantCompareOpenUrlForSelection(
targetTenant: $targetTenant,
triageState: $triageState,
sourceTenant: $anchorTenant,
);
$this->portfolioTriageRegistryList($user, $anchorTenant, $triageState)
->selectTableRecords([$anchorTenant, $targetTenant])
->assertTableBulkActionVisible('compareSelected')
->callTableBulkAction('compareSelected', [$anchorTenant, $targetTenant])
->assertRedirect($expectedUrl);
$query = crossTenantCompareLaunchQuery($expectedUrl);
$backUrl = urldecode((string) data_get($query, 'nav.back_url'));
expect($query)->toMatchArray([
'source_tenant_id' => (string) $anchorTenant->getKey(),
'target_tenant_id' => (string) $targetTenant->getKey(),
])
->and(data_get($query, 'nav.source_surface'))->toBe('tenant_registry')
->and(data_get($query, 'nav.back_label'))->toBe('Back to tenant registry')
->and(data_get($query, 'nav.tenant_id'))->toBeNull()
->and($backUrl)->toContain('backup_posture[0]='.TenantBackupHealthAssessment::POSTURE_STALE)
->and($backUrl)->toContain('recovery_evidence[0]='.TenantRecoveryTriagePresentation::RECOVERY_EVIDENCE_WEAKENED)
->and($backUrl)->toContain('triage_sort='.TenantRecoveryTriagePresentation::TRIAGE_SORT_WORST_FIRST);
Livewire::withQueryParams($query)
->actingAs($user)
->test(CrossTenantComparePage::class)
->assertSet('sourceTenantId', (string) $anchorTenant->getKey())
->assertSet('targetTenantId', (string) $targetTenant->getKey())
->assertActionVisible('return_to_origin');
});
it('rejects the bulk compare action until exactly two active tenants are selected', function (): void {
[$user, $anchorTenant] = $this->makePortfolioTriageActor('Anchor Tenant');
$targetTenant = $this->makePortfolioTriagePeer($user, $anchorTenant, 'Target Tenant');
$thirdTenant = $this->makePortfolioTriagePeer($user, $anchorTenant, 'Third Tenant');
$this->portfolioTriageRegistryList($user, $anchorTenant)
->selectTableRecords([$anchorTenant])
->assertTableBulkActionVisible('compareSelected')
->callTableBulkAction('compareSelected', [$anchorTenant])
->assertNotified('Select exactly two tenants to compare.');
$this->portfolioTriageRegistryList($user, $anchorTenant)
->selectTableRecords([$anchorTenant, $targetTenant, $thirdTenant])
->assertTableBulkActionVisible('compareSelected')
->callTableBulkAction('compareSelected', [$anchorTenant, $targetTenant, $thirdTenant])
->assertNotified('Select exactly two tenants to compare.');
});
it('rejects the bulk compare action when a selected tenant is not active', function (): void {
[$user, $anchorTenant] = $this->makePortfolioTriageActor('Anchor Tenant');
$onboardingTenant = $this->makePortfolioTriagePeer($user, $anchorTenant, 'Onboarding Tenant');
$onboardingTenant->forceFill([
'status' => Tenant::STATUS_ONBOARDING,
])->save();
$this->portfolioTriageRegistryList($user, $anchorTenant)
->selectTableRecords([$anchorTenant, $onboardingTenant])
->assertTableBulkActionVisible('compareSelected')
->callTableBulkAction('compareSelected', [$anchorTenant, $onboardingTenant])
->assertNotified('Only active tenants can be compared.');
});
it('hides the compare launch action when workspace baseline view capability is missing', function (): void {
[$user, $anchorTenant] = $this->makePortfolioTriageActor('Anchor Tenant');
$targetTenant = $this->makePortfolioTriagePeer($user, $anchorTenant, 'Target Tenant');
$resolver = \Mockery::mock(WorkspaceCapabilityResolver::class);
$resolver->shouldReceive('isMember')->andReturnTrue();
$resolver->shouldReceive('can')->andReturnFalse();
app()->instance(WorkspaceCapabilityResolver::class, $resolver);
$this->portfolioTriageRegistryList($user, $anchorTenant)
->assertTableActionHidden('compareTenants', $targetTenant);
});
it('hides the compare launch action when the actor lacks tenant view on the launched tenant', function (): void {
[$user, $anchorTenant] = $this->makePortfolioTriageActor('Anchor Tenant');
$targetTenant = $this->makePortfolioTriagePeer($user, $anchorTenant, 'Target Tenant');
$resolver = \Mockery::mock(CapabilityResolver::class);
$resolver->shouldReceive('primeMemberships')->andReturnNull();
$resolver->shouldReceive('isMember')->andReturnTrue();
$resolver->shouldReceive('can')->andReturnUsing(function (mixed $actor, mixed $tenant, string $capability) use ($targetTenant): bool {
if ($tenant instanceof Tenant
&& (int) $tenant->getKey() === (int) $targetTenant->getKey()
&& $capability === Capabilities::TENANT_VIEW) {
return false;
}
return true;
});
app()->instance(CapabilityResolver::class, $resolver);
$this->portfolioTriageRegistryList($user, $anchorTenant)
->assertTableActionHidden('compareTenants', $targetTenant);
});