TenantAtlas/apps/platform/tests/Feature/Filament/Spec390RestoreReadinessGuidanceTest.php
Ahmed Darrazi e80a1f87c3
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m19s
feat: add restore readiness resolution adapter improvements
2026-06-20 14:49:48 +02:00

269 lines
10 KiB
PHP

<?php
declare(strict_types=1);
use App\Filament\Resources\RestoreRunResource;
use App\Filament\Resources\RestoreRunResource\Pages\CreateRestoreRun;
use App\Filament\Resources\RestoreRunResource\Pages\ViewRestoreRun;
use App\Filament\Resources\RestoreRunResource\Presenters\RestoreRunCreatePresenter;
use App\Models\BackupItem;
use App\Models\BackupSet;
use App\Models\ManagedEnvironment;
use App\Models\OperationRun;
use App\Models\Policy;
use App\Models\ReviewPublicationResolutionCase;
use App\Models\ReviewPublicationResolutionStep;
use App\Models\RestoreRun;
use App\Services\Intune\RestoreService;
use App\Support\OperationRunStatus;
use App\Support\OperationRunType;
use App\Support\RestoreReadinessResolution\RestoreReadinessAction;
use App\Support\RestoreReadinessResolution\RestoreReadinessReason;
use App\Support\RestoreReadinessResolution\RestoreReadinessState;
use App\Support\RestoreRunStatus;
use App\Support\RestoreSafety\RestoreSafetyResolver;
use Filament\Facades\Filament;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Mockery\MockInterface;
uses(RefreshDatabase::class);
/**
* @return array{operation_runs:int, review_publication_resolution_cases:int, review_publication_resolution_steps:int}
*/
function spec390GuidanceSideEffectCounts(): array
{
return [
'operation_runs' => OperationRun::query()->count(),
'review_publication_resolution_cases' => ReviewPublicationResolutionCase::query()->count(),
'review_publication_resolution_steps' => ReviewPublicationResolutionStep::query()->count(),
];
}
/**
* @param array{operation_runs:int, review_publication_resolution_cases:int, review_publication_resolution_steps:int} $expected
*/
function spec390ExpectGuidanceSideEffectCounts(array $expected): void
{
expect(spec390GuidanceSideEffectCounts())->toBe($expected);
}
function spec390BackupSetWithItem(ManagedEnvironment $tenant): BackupSet
{
$policy = Policy::create([
'managed_environment_id' => (int) $tenant->getKey(),
'external_id' => 'spec390-policy',
'policy_type' => 'deviceConfiguration',
'display_name' => 'Spec390 Policy',
'platform' => 'windows',
'metadata' => [],
]);
$backupSet = BackupSet::factory()->create([
'managed_environment_id' => (int) $tenant->getKey(),
'name' => 'Spec390 Backup',
'status' => 'completed',
'item_count' => 1,
]);
BackupItem::factory()->create([
'managed_environment_id' => (int) $tenant->getKey(),
'backup_set_id' => (int) $backupSet->getKey(),
'policy_id' => (int) $policy->getKey(),
'policy_identifier' => $policy->external_id,
'policy_type' => $policy->policy_type,
'platform' => $policy->platform,
'payload' => [
'id' => $policy->external_id,
'displayName' => 'Spec390 Policy',
'settings' => ['enabled' => true],
],
'assignments' => [],
'metadata' => [],
]);
return $backupSet;
}
/**
* @return array<string, mixed>
*/
function spec390ReadyWizardData(BackupSet $backupSet): array
{
/** @var RestoreSafetyResolver $safety */
$safety = app(RestoreSafetyResolver::class);
$data = [
'backup_set_id' => (int) $backupSet->getKey(),
'scope_mode' => 'all',
'backup_item_ids' => [],
'group_mapping' => [],
'check_summary' => ['blocking' => 0, 'warning' => 0, 'safe' => 1],
'check_results' => [['code' => 'safe', 'severity' => 'safe']],
'checks_ran_at' => now('UTC')->toIso8601String(),
'preview_summary' => ['generated_at' => now('UTC')->toIso8601String(), 'policies_total' => 1],
'preview_diffs' => [['policy_identifier' => 'spec390-policy']],
'preview_ran_at' => now('UTC')->toIso8601String(),
'is_dry_run' => true,
];
$data['check_basis'] = $safety->checksBasisFromData($data);
$data['preview_basis'] = $safety->previewBasisFromData($data);
return RestoreRunResource::synchronizeRestoreSafetyDraft($data);
}
it('adds blocked readiness guidance to the restore create presenter', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$this->actingAs($user);
ensureDefaultProviderConnection($tenant, 'microsoft');
$backupSet = spec390BackupSetWithItem($tenant);
$sideEffectCounts = spec390GuidanceSideEffectCounts();
$contract = RestoreRunCreatePresenter::contract(
data: [
'backup_set_id' => (int) $backupSet->getKey(),
'scope_mode' => 'all',
'backup_item_ids' => [],
'group_mapping' => [],
],
currentStep: 3,
compactFlow: true,
tenant: $tenant,
user: $user,
);
expect(data_get($contract, 'readinessGuidance.state'))->toBe(RestoreReadinessState::Blocked->value)
->and(data_get($contract, 'readinessGuidance.reason'))->toBe(RestoreReadinessReason::ChecksNotRun->value)
->and(data_get($contract, 'readinessGuidance.nextAction'))->toBe(RestoreReadinessAction::RunReadinessChecks->value)
->and(data_get($contract, 'readinessGuidance.actionSafetyCopy'))->toBe('This will not execute the restore.');
spec390ExpectGuidanceSideEffectCounts($sideEffectCounts);
});
it('adds ready-for-confirmation guidance to the restore create presenter', function (): void {
$tenant = ManagedEnvironment::factory()->create([
'rbac_status' => 'ok',
'rbac_last_checked_at' => now(),
]);
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
$this->actingAs($user);
ensureDefaultProviderConnection($tenant, 'microsoft');
$backupSet = spec390BackupSetWithItem($tenant);
$contract = RestoreRunCreatePresenter::contract(
data: spec390ReadyWizardData($backupSet),
currentStep: 5,
compactFlow: true,
tenant: $tenant,
user: $user,
);
expect(data_get($contract, 'readinessGuidance.state'))->toBe(RestoreReadinessState::ReadyForConfirmation->value)
->and(data_get($contract, 'readinessGuidance.nextAction'))->toBe(RestoreReadinessAction::ContinueToConfirmation->value)
->and(data_get($contract, 'readinessGuidance.actionSafetyCopy'))->toBe('The restore still requires final confirmation before execution.');
});
it('blocks create presenter readiness when execution prerequisites are unavailable', function (): void {
$tenant = ManagedEnvironment::factory()->create([
'rbac_status' => 'ok',
'rbac_last_checked_at' => now(),
]);
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
$this->actingAs($user);
ensureDefaultProviderConnection($tenant, 'microsoft', ensureCredential: false);
$backupSet = spec390BackupSetWithItem($tenant);
$contract = RestoreRunCreatePresenter::contract(
data: spec390ReadyWizardData($backupSet),
currentStep: 5,
compactFlow: true,
tenant: $tenant,
user: $user,
);
expect(data_get($contract, 'readinessGuidance.state'))->toBe(RestoreReadinessState::Blocked->value)
->and(data_get($contract, 'readinessGuidance.reason'))->toBe(RestoreReadinessReason::ExecutionPrerequisiteBlocked->value)
->and(data_get($contract, 'readinessGuidance.nextAction'))->toBe(RestoreReadinessAction::ReviewValidationBlockers->value)
->and(data_get($contract, 'wizardGate.execution_state'))->toBe('unavailable_until_prerequisites');
});
it('renders persisted restore-run readiness guidance on the view page', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$this->actingAs($user);
Filament::setTenant($tenant, true);
$backupSet = spec390BackupSetWithItem($tenant);
$operationRun = OperationRun::factory()->forTenant($tenant)->create([
'type' => OperationRunType::RestoreExecute->value,
'status' => OperationRunStatus::Running->value,
]);
$restoreRun = RestoreRun::factory()->for($tenant, 'tenant')->for($backupSet)->create([
'status' => RestoreRunStatus::Running->value,
'operation_run_id' => (int) $operationRun->getKey(),
]);
$sideEffectCounts = spec390GuidanceSideEffectCounts();
Livewire::test(ViewRestoreRun::class, ['record' => $restoreRun->getKey()])
->assertSee('Restore readiness')
->assertSee('Restore execution is in progress.')
->assertSee('Next safe action: Open operation')
->assertSee('This guidance only opens existing execution evidence.');
spec390ExpectGuidanceSideEffectCounts($sideEffectCounts);
});
it('lets readonly users inspect persisted readiness guidance but keeps create mutations forbidden', function (): void {
$tenant = ManagedEnvironment::factory()->create();
[$user] = createUserWithTenant($tenant, role: 'readonly');
$this->actingAs($user);
Filament::setTenant($tenant, true);
$backupSet = spec390BackupSetWithItem($tenant);
$restoreRun = RestoreRun::factory()->for($tenant, 'tenant')->for($backupSet)->previewOnly()->create();
Livewire::actingAs($user)
->test(ViewRestoreRun::class, ['record' => $restoreRun->getKey()])
->assertSee('Restore readiness')
->assertSee("Restore can't continue yet.");
$this->actingAs($user)
->get(RestoreRunResource::getUrl('create', panel: 'admin', tenant: $tenant))
->assertForbidden();
});
it('preserves final execution confirmation and safety gates', function (): void {
$tenant = ManagedEnvironment::factory()->create([
'rbac_status' => 'ok',
'rbac_last_checked_at' => now(),
]);
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
$this->actingAs($user);
Filament::setTenant($tenant, true);
ensureDefaultProviderConnection($tenant, 'microsoft');
$backupSet = spec390BackupSetWithItem($tenant);
$sideEffectCounts = spec390GuidanceSideEffectCounts();
$this->mock(RestoreService::class, function (MockInterface $mock): void {
$mock->shouldNotReceive('preview');
$mock->shouldNotReceive('execute');
});
Livewire::test(CreateRestoreRun::class)
->fillForm([
...spec390ReadyWizardData($backupSet),
'is_dry_run' => false,
'acknowledged_impact' => false,
'tenant_confirm' => null,
])
->call('create')
->assertHasFormErrors(['acknowledged_impact']);
spec390ExpectGuidanceSideEffectCounts($sideEffectCounts);
});