TenantAtlas/apps/platform/tests/Feature/PortfolioCompare/CrossTenantCompareLaunchContextTest.php
Ahmed Darrazi 983abb18a1
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 3m22s
chore: commit workspace changes (automated)
2026-05-02 16:36:21 +02:00

267 lines
13 KiB
PHP

<?php
declare(strict_types=1);
use App\Filament\Pages\CrossTenantComparePage;
use App\Filament\Resources\TenantResource;
use App\Jobs\Operations\CrossTenantPromotionExecutionJob;
use App\Models\OperationRun;
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\Navigation\CanonicalNavigationContext;
use App\Support\RestoreSafety\RestoreResultAttention;
use App\Support\Tenants\TenantRecoveryTriagePresentation;
use Filament\Actions\Action;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue;
use Livewire\Livewire;
use Tests\Feature\Concerns\BuildsPortfolioCompareFixtures;
use Tests\Feature\Concerns\BuildsPortfolioTriageFixtures;
uses(RefreshDatabase::class, BuildsPortfolioTriageFixtures::class, BuildsPortfolioCompareFixtures::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('keeps launch context after queueing promotion from an exact-two registry launch', function (): void {
Queue::fake();
[$user, $anchorTenant] = $this->makePortfolioTriageActor(
tenantName: 'Anchor Tenant',
workspaceRole: 'owner',
);
$targetTenant = $this->makePortfolioTriagePeer($user, $anchorTenant, 'Target Tenant');
createMinimalUserWithTenant(
tenant: $targetTenant,
user: $user,
role: 'owner',
workspaceRole: 'owner',
);
$this->createPortfolioCompareSubject(
tenant: $anchorTenant,
displayName: 'Queued Launch Context Policy',
snapshot: ['settings' => [['key' => 'launch-context', 'value' => 1]]],
);
$triageState = $this->portfolioReturnFilters(
[TenantBackupHealthAssessment::POSTURE_STALE],
[TenantRecoveryTriagePresentation::RECOVERY_EVIDENCE_WEAKENED],
[],
TenantRecoveryTriagePresentation::TRIAGE_SORT_WORST_FIRST,
);
$expectedUrl = TenantResource::crossTenantCompareOpenUrlForSelection(
targetTenant: $targetTenant,
triageState: $triageState,
sourceTenant: $anchorTenant,
);
$expectedBackUrl = TenantResource::getUrl(panel: 'admin', parameters: $triageState);
$query = crossTenantCompareLaunchQuery($expectedUrl);
$query['policy_type'] = ['deviceConfiguration'];
$this->usePortfolioTriageWorkspace($user, $anchorTenant);
$component = Livewire::withQueryParams($query)
->actingAs($user)
->test(CrossTenantComparePage::class)
->assertSet('sourceTenantId', (string) $anchorTenant->getKey())
->assertSet('targetTenantId', (string) $targetTenant->getKey())
->assertSet('selectedPolicyTypes', ['deviceConfiguration'])
->assertActionVisible('return_to_origin')
->assertActionExists('return_to_origin', fn (Action $action): bool => $action->getLabel() === 'Back to tenant registry'
&& $action->getUrl() === $expectedBackUrl);
$page = $component->instance();
$page->generatePromotionPreflight();
$page->executePromotion();
$run = OperationRun::query()->latest('id')->first();
$navigationContext = CanonicalNavigationContext::fromPayload($page->navigationContextPayload);
expect($run)
->not->toBeNull()
->and($run?->type)->toBe('promotion.execute')
->and(data_get($run?->context, 'selection.sourceTenantId'))->toBe((int) $anchorTenant->getKey())
->and(data_get($run?->context, 'selection.targetTenantId'))->toBe((int) $targetTenant->getKey())
->and(data_get($run?->context, 'selection.policyTypes'))->toBe(['deviceConfiguration'])
->and($page->sourceTenantId)->toBe((string) $anchorTenant->getKey())
->and($page->targetTenantId)->toBe((string) $targetTenant->getKey())
->and($page->selectedPolicyTypes)->toBe(['deviceConfiguration'])
->and($page->navigationContextPayload)->toBe($query['nav'])
->and($navigationContext?->backLinkLabel)->toBe('Back to tenant registry')
->and($navigationContext?->backLinkUrl)->toBe($expectedBackUrl);
Queue::assertPushed(CrossTenantPromotionExecutionJob::class, function (CrossTenantPromotionExecutionJob $job) use ($run): bool {
return $job->getOperationRun()?->is($run);
});
});
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);
});