Some checks failed
Main Confidence / confidence (push) Failing after 54s
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
188 lines
9.2 KiB
PHP
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);
|
|
}); |