619 lines
25 KiB
PHP
619 lines
25 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Filament\Resources\RestoreRunResource;
|
|
use App\Filament\Resources\RestoreRunResource\Pages\CreateRestoreRun;
|
|
use App\Filament\Resources\RestoreRunResource\Presenters\RestoreRunCreatePresenter;
|
|
use App\Models\BackupItem;
|
|
use App\Models\BackupSet;
|
|
use App\Models\EntraGroup;
|
|
use App\Models\ManagedEnvironment;
|
|
use App\Models\Policy;
|
|
use App\Support\RestoreSafety\RestoreSafetyResolver;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Livewire\Livewire;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
function spec332ProductProcessFlowTenant(): array
|
|
{
|
|
$tenant = ManagedEnvironment::factory()->create([
|
|
'rbac_status' => 'ok',
|
|
'rbac_last_checked_at' => now(),
|
|
]);
|
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
|
ensureDefaultProviderConnection($tenant, 'microsoft');
|
|
|
|
return [$user, $tenant];
|
|
}
|
|
|
|
function spec332UsableBackupFixture(ManagedEnvironment $tenant): array
|
|
{
|
|
$policy = Policy::create([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'external_id' => 'spec332-policy-usable',
|
|
'policy_type' => 'deviceConfiguration',
|
|
'display_name' => 'Spec332 Device Policy',
|
|
'platform' => 'windows',
|
|
]);
|
|
|
|
$backupSet = BackupSet::factory()->create([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'name' => 'Spec332 Usable Backup',
|
|
'status' => 'completed',
|
|
'item_count' => 1,
|
|
]);
|
|
|
|
$backupItem = 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' => [
|
|
'displayName' => 'Spec332 Device Policy',
|
|
'settings' => ['foo' => 'bar'],
|
|
],
|
|
'metadata' => [
|
|
'displayName' => 'Spec332 Device Policy',
|
|
],
|
|
'assignments' => [],
|
|
]);
|
|
|
|
return [$backupSet, $backupItem];
|
|
}
|
|
|
|
function spec332EmptyBackupFixture(ManagedEnvironment $tenant): BackupSet
|
|
{
|
|
return BackupSet::factory()->create([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'name' => 'Spec332 Empty Backup',
|
|
'status' => 'completed',
|
|
'item_count' => 0,
|
|
]);
|
|
}
|
|
|
|
function spec332MetadataOnlyBackupFixture(ManagedEnvironment $tenant): array
|
|
{
|
|
$policy = Policy::create([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'external_id' => 'spec332-policy-metadata-only',
|
|
'policy_type' => 'deviceConfiguration',
|
|
'display_name' => 'Spec332 Metadata Only Policy',
|
|
'platform' => 'windows',
|
|
]);
|
|
|
|
$backupSet = BackupSet::factory()->create([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'name' => 'Spec332 Metadata-only Backup',
|
|
'status' => 'completed',
|
|
'item_count' => 1,
|
|
]);
|
|
|
|
$backupItem = 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' => [],
|
|
'metadata' => [
|
|
'displayName' => 'Spec332 Metadata Only Policy',
|
|
'snapshot_source' => 'metadata_only',
|
|
'warnings' => ['metadata only fallback'],
|
|
],
|
|
'assignments' => [],
|
|
]);
|
|
|
|
return [$backupSet, $backupItem];
|
|
}
|
|
|
|
function spec332UnresolvedGroupBackupFixture(ManagedEnvironment $tenant): array
|
|
{
|
|
$policy = Policy::create([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'external_id' => 'spec332-policy-group',
|
|
'policy_type' => 'deviceConfiguration',
|
|
'display_name' => 'Spec332 Group Mapping Policy',
|
|
'platform' => 'windows',
|
|
]);
|
|
|
|
$backupSet = BackupSet::factory()->create([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'name' => 'Spec332 Group Backup',
|
|
'status' => 'completed',
|
|
'item_count' => 1,
|
|
]);
|
|
|
|
$backupItem = 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' => [
|
|
'displayName' => 'Spec332 Group Mapping Policy',
|
|
],
|
|
'metadata' => [
|
|
'displayName' => 'Spec332 Group Mapping Policy',
|
|
],
|
|
'assignments' => [[
|
|
'target' => [
|
|
'@odata.type' => '#microsoft.graph.groupAssignmentTarget',
|
|
'groupId' => '11111111-1111-1111-1111-111111111111',
|
|
'group_display_name' => 'Spec332 Missing Group',
|
|
],
|
|
]],
|
|
]);
|
|
|
|
return [$backupSet, $backupItem];
|
|
}
|
|
|
|
function spec332WizardComponent($user, ManagedEnvironment $tenant): \Livewire\Features\SupportTesting\Testable
|
|
{
|
|
setAdminPanelContext($tenant);
|
|
|
|
return Livewire::actingAs($user)->test(CreateRestoreRun::class);
|
|
}
|
|
|
|
function spec332CurrentPreviewData(BackupSet $backupSet, BackupItem $backupItem): array
|
|
{
|
|
/** @var RestoreSafetyResolver $resolver */
|
|
$resolver = app(RestoreSafetyResolver::class);
|
|
|
|
$data = [
|
|
'backup_set_id' => (int) $backupSet->getKey(),
|
|
'scope_mode' => 'selected',
|
|
'backup_item_ids' => [(int) $backupItem->getKey()],
|
|
'is_dry_run' => true,
|
|
'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,
|
|
'policies_changed' => 0,
|
|
'assignments_changed' => 0,
|
|
'scope_tags_changed' => 0,
|
|
'raw_payload_marker' => 'spec332 raw payload should stay hidden',
|
|
],
|
|
'preview_diffs' => [[
|
|
'policy_identifier' => 'spec332-policy-usable',
|
|
'display_name' => 'Spec332 Device Policy',
|
|
'policy_type' => 'deviceConfiguration',
|
|
'platform' => 'windows',
|
|
'action' => 'update',
|
|
'assignments_changed' => false,
|
|
'scope_tags_changed' => false,
|
|
'diff' => [
|
|
'summary' => ['added' => 0, 'removed' => 0, 'changed' => 0],
|
|
'changed' => [],
|
|
'added' => [],
|
|
'removed' => [],
|
|
],
|
|
]],
|
|
'preview_ran_at' => now('UTC')->toIso8601String(),
|
|
];
|
|
|
|
$data['check_basis'] = $resolver->checksBasisFromData($data);
|
|
$data['preview_basis'] = $resolver->previewBasisFromData($data);
|
|
|
|
return RestoreRunResource::synchronizeRestoreSafetyDraft($data);
|
|
}
|
|
|
|
it('keeps only the reconciled spec 332 directory under the active product process flow path', function (): void {
|
|
$spec332Directories = collect(glob(repo_path('specs/332-*')) ?: [])
|
|
->map(static fn (string $path): string => basename($path))
|
|
->sort()
|
|
->values()
|
|
->all();
|
|
|
|
expect($spec332Directories)->toBe([
|
|
'332-product-process-flow-system-v1',
|
|
])
|
|
->and(is_dir(repo_path('specs/332-product-process-flow-system-v1')))->toBeTrue()
|
|
->and(is_dir(repo_path('specs/332-restore-run-preview-productization')))->toBeFalse()
|
|
->and((string) file_get_contents(repo_path('specs/332-product-process-flow-system-v1/spec.md')))
|
|
->toContain('Product Process Flow System v1')
|
|
->toContain('Spec 332 was reconciled from the narrower `specs/332-restore-run-preview-productization` path');
|
|
});
|
|
|
|
it('renders compact safety evidence on step 1 for a usable backup source', function (): void {
|
|
[$user, $tenant] = spec332ProductProcessFlowTenant();
|
|
[$backupSet] = spec332UsableBackupFixture($tenant);
|
|
|
|
$component = spec332WizardComponent($user, $tenant)
|
|
->fillForm([
|
|
'backup_set_id' => (int) $backupSet->getKey(),
|
|
])
|
|
->assertSee('Restore Safety')
|
|
->assertSee('Backup quality summary')
|
|
->assertSee('Safety evidence')
|
|
->assertSee('View safety gates and proof')
|
|
->assertSee('View quality caveat and detail')
|
|
->assertSee('Restore safety gates')
|
|
->assertSee('Restore Proof')
|
|
->assertSee('Diagnostics - Collapsed')
|
|
->assertSee('Input quality signals do not prove that execution is safe or that recovery is verified.')
|
|
->assertSee('A usable source backup is selected for this restore draft.')
|
|
->assertSee('Continue to scope.')
|
|
->assertSee('Validate impact.')
|
|
->assertSee('This create flow does not prove recoverability before execution and post-run evidence exist.')
|
|
->assertDontSee('Technical startability')
|
|
->assertDontSee('write-gate')
|
|
->assertDontSee('hard-blocker')
|
|
->assertDontSee('Is this dangerous?')
|
|
->assertDontSee('tenant-wide recoverability')
|
|
->assertSeeHtml('data-step-label="Usable source selected"')
|
|
->assertSeeHtml('data-proof-label="Operation proof"');
|
|
|
|
expect($component->html())
|
|
->toContain('data-testid="restore-run-safety-evidence"')
|
|
->not->toContain('data-testid="restore-run-process-flow-full"');
|
|
});
|
|
|
|
it('does not mark usable source as complete when the backup has no captured items', function (): void {
|
|
[$user, $tenant] = spec332ProductProcessFlowTenant();
|
|
$backupSet = spec332EmptyBackupFixture($tenant);
|
|
|
|
spec332WizardComponent($user, $tenant)
|
|
->fillForm([
|
|
'backup_set_id' => (int) $backupSet->getKey(),
|
|
])
|
|
->assertSee('Input quality signals do not prove that execution is safe or that recovery is verified.')
|
|
->assertSeeHtml('data-step-label="Usable source selected"')
|
|
->assertSeeHtml('data-step-status="required"');
|
|
});
|
|
|
|
it('renders compact restore evidence on step 2 without a separate proof rail', function (): void {
|
|
[$user, $tenant] = spec332ProductProcessFlowTenant();
|
|
[$backupSet] = spec332UsableBackupFixture($tenant);
|
|
|
|
$component = spec332WizardComponent($user, $tenant)
|
|
->fillForm([
|
|
'backup_set_id' => (int) $backupSet->getKey(),
|
|
])
|
|
->goToNextWizardStep()
|
|
->assertWizardCurrentStep(2)
|
|
->assertSee('Restore evidence')
|
|
->assertSee('3/7 gates complete')
|
|
->assertSee('View validation gates and restore proof')
|
|
->assertSee('Requested by')
|
|
->assertSee('Diagnostics - Collapsed')
|
|
->assertSeeHtml('data-testid="restore-run-safety-evidence"');
|
|
|
|
expect($component->html())->toContain('data-testid="restore-run-safety-evidence"');
|
|
});
|
|
|
|
it('keeps group mapping details collapsed by default on step 2 until the resolver is opened explicitly', function (): void {
|
|
[$user, $tenant] = spec332ProductProcessFlowTenant();
|
|
[$backupSet] = spec332UnresolvedGroupBackupFixture($tenant);
|
|
|
|
$component = spec332WizardComponent($user, $tenant)
|
|
->fillForm([
|
|
'backup_set_id' => (int) $backupSet->getKey(),
|
|
])
|
|
->goToNextWizardStep()
|
|
->assertWizardCurrentStep(2)
|
|
->assertSee('Scope summary')
|
|
->assertSee('1 mapping required')
|
|
->assertSee('Validation blocked')
|
|
->assertSee('Resolve target mappings')
|
|
->assertSee('Restore evidence')
|
|
->assertSee('View validation gates and restore proof')
|
|
->assertSee('0 of 1 mappings resolved')
|
|
->assertSee('1 unresolved')
|
|
->assertSee('0 skipped')
|
|
->assertSee('Resolve required mappings before validation can run.')
|
|
->assertSee('Target mapping requirements')
|
|
->assertSee('Select a target group from the directory cache or enter a target group object ID as a fallback. Required mappings must be resolved before validation can run.')
|
|
->assertDontSee('Paste the target Entra ID group Object ID (GUID).');
|
|
|
|
$html = html_entity_decode($component->html(), ENT_QUOTES | ENT_HTML5);
|
|
|
|
expect(preg_match('/isCollapsed:\s*true[\s\S]{0,2200}Resolve target mappings/', $html))->toBe(1);
|
|
});
|
|
|
|
it('shows cached target group identity in mapping helper text when a cached target is selected', function (): void {
|
|
[$user, $tenant] = spec332ProductProcessFlowTenant();
|
|
[$backupSet] = spec332UnresolvedGroupBackupFixture($tenant);
|
|
|
|
$sourceGroupId = '11111111-1111-1111-1111-111111111111';
|
|
$targetGroupId = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa';
|
|
|
|
EntraGroup::factory()->for($tenant)->create([
|
|
'entra_id' => $targetGroupId,
|
|
'display_name' => 'Spec332 Cached Target Group',
|
|
]);
|
|
|
|
spec332WizardComponent($user, $tenant)
|
|
->fillForm([
|
|
'backup_set_id' => (int) $backupSet->getKey(),
|
|
])
|
|
->goToNextWizardStep()
|
|
->assertWizardCurrentStep(2)
|
|
->fillForm([
|
|
'group_mapping' => [
|
|
$sourceGroupId => $targetGroupId,
|
|
],
|
|
])
|
|
->assertSee('Target group: Spec332 Cached Target Group')
|
|
->assertSee('Target ID:')
|
|
->assertSee($targetGroupId);
|
|
});
|
|
|
|
it('labels manual GUID mapping as a manual fallback and counts it in the resolver summary', function (): void {
|
|
[$user, $tenant] = spec332ProductProcessFlowTenant();
|
|
[$backupSet] = spec332UnresolvedGroupBackupFixture($tenant);
|
|
|
|
$sourceGroupId = '11111111-1111-1111-1111-111111111111';
|
|
$targetGroupId = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb';
|
|
|
|
spec332WizardComponent($user, $tenant)
|
|
->fillForm([
|
|
'backup_set_id' => (int) $backupSet->getKey(),
|
|
])
|
|
->goToNextWizardStep()
|
|
->assertWizardCurrentStep(2)
|
|
->fillForm([
|
|
'group_mapping' => [
|
|
$sourceGroupId => $targetGroupId,
|
|
],
|
|
])
|
|
->assertSee('1 of 1 mappings resolved')
|
|
->assertSee('0 unresolved')
|
|
->assertSee('0 skipped')
|
|
->assertSee('1 manual fallback')
|
|
->assertSee('Manual fallback target object ID')
|
|
->assertSee('Badge: Manual fallback');
|
|
});
|
|
|
|
it('does not treat invalid GUID values as resolved in the resolver summary', function (): void {
|
|
[$user, $tenant] = spec332ProductProcessFlowTenant();
|
|
[$backupSet] = spec332UnresolvedGroupBackupFixture($tenant);
|
|
|
|
$sourceGroupId = '11111111-1111-1111-1111-111111111111';
|
|
|
|
spec332WizardComponent($user, $tenant)
|
|
->fillForm([
|
|
'backup_set_id' => (int) $backupSet->getKey(),
|
|
])
|
|
->goToNextWizardStep()
|
|
->assertWizardCurrentStep(2)
|
|
->fillForm([
|
|
'group_mapping' => [
|
|
$sourceGroupId => 'not-a-guid',
|
|
],
|
|
])
|
|
->assertSee('0 of 1 mappings resolved')
|
|
->assertSee('1 unresolved')
|
|
->assertSee('Invalid group object ID (GUID).');
|
|
});
|
|
|
|
it('supports skipping and undoing a mapping assignment on step 2', function (): void {
|
|
[$user, $tenant] = spec332ProductProcessFlowTenant();
|
|
[$backupSet] = spec332UnresolvedGroupBackupFixture($tenant);
|
|
|
|
$sourceGroupId = '11111111-1111-1111-1111-111111111111';
|
|
$sourceGroupToken = '11111111_1111_1111_1111_111111111111';
|
|
|
|
$component = spec332WizardComponent($user, $tenant)
|
|
->fillForm([
|
|
'backup_set_id' => (int) $backupSet->getKey(),
|
|
])
|
|
->goToNextWizardStep()
|
|
->assertWizardCurrentStep(2)
|
|
->assertFormComponentActionVisible("group_mapping.{$sourceGroupId}", "skip_assignment_{$sourceGroupToken}")
|
|
->callFormComponentAction("group_mapping.{$sourceGroupId}", "skip_assignment_{$sourceGroupToken}")
|
|
->assertSet("data.group_mapping.{$sourceGroupId}", 'SKIP')
|
|
->assertFormFieldHidden("group_mapping.{$sourceGroupId}")
|
|
->assertFormFieldVisible("group_mapping_skipped_{$sourceGroupToken}")
|
|
->assertSee('1 skipped')
|
|
->assertSee('This assignment will not be restored.')
|
|
->assertFormComponentActionVisible("group_mapping_skipped_{$sourceGroupToken}", "undo_skip_assignment_{$sourceGroupToken}")
|
|
->callFormComponentAction("group_mapping_skipped_{$sourceGroupToken}", "undo_skip_assignment_{$sourceGroupToken}")
|
|
->assertSet("data.group_mapping.{$sourceGroupId}", null)
|
|
->assertFormFieldVisible("group_mapping.{$sourceGroupId}");
|
|
|
|
expect($component)->not->toBeNull();
|
|
});
|
|
|
|
it('blocks progression out of step 2 while required mappings are unresolved', function (): void {
|
|
[$user, $tenant] = spec332ProductProcessFlowTenant();
|
|
[$backupSet] = spec332UnresolvedGroupBackupFixture($tenant);
|
|
|
|
$component = spec332WizardComponent($user, $tenant)
|
|
->fillForm([
|
|
'backup_set_id' => (int) $backupSet->getKey(),
|
|
])
|
|
->goToNextWizardStep()
|
|
->assertWizardCurrentStep(2);
|
|
|
|
$component
|
|
->goToNextWizardStep()
|
|
->assertWizardCurrentStep(2)
|
|
->assertNotified('Mappings required')
|
|
->assertSee('Resolve required mappings before validation can run.');
|
|
});
|
|
|
|
it('productizes the empty target group picker when step 2 needs dependency mapping', function (): void {
|
|
[$user, $tenant] = spec332ProductProcessFlowTenant();
|
|
[$backupSet, $backupItem] = spec332UnresolvedGroupBackupFixture($tenant);
|
|
|
|
$component = spec332WizardComponent($user, $tenant)
|
|
->fillForm([
|
|
'backup_set_id' => (int) $backupSet->getKey(),
|
|
])
|
|
->goToNextWizardStep()
|
|
->assertWizardCurrentStep(2);
|
|
|
|
$component
|
|
->fillForm([
|
|
'scope_mode' => 'selected',
|
|
'backup_item_ids' => [(int) $backupItem->getKey()],
|
|
]);
|
|
|
|
/** @var \Filament\Schemas\Schema $schema */
|
|
$schema = $component->instance()->form;
|
|
$field = $schema->getComponentByStatePath('group_mapping.11111111-1111-1111-1111-111111111111');
|
|
$action = collect($field?->getSuffixActions() ?? [])
|
|
->first(fn ($candidate) => $candidate->getName() === 'select_from_directory_cache_11111111_1111_1111_1111_111111111111');
|
|
|
|
expect($field)->not->toBeNull()
|
|
->and($action)->not->toBeNull()
|
|
->and($action->getModalHeading())->toBe('Resolve target group mapping');
|
|
});
|
|
|
|
it('shows a product-safe blocked state for checks when provider credentials are missing', function (): void {
|
|
$tenant = ManagedEnvironment::factory()->create([
|
|
'rbac_status' => 'ok',
|
|
'rbac_last_checked_at' => now(),
|
|
]);
|
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
|
|
|
ensureDefaultProviderConnection($tenant, 'microsoft', ensureCredential: false);
|
|
[$backupSet] = spec332UsableBackupFixture($tenant);
|
|
|
|
$component = spec332WizardComponent($user, $tenant)
|
|
->fillForm([
|
|
'backup_set_id' => (int) $backupSet->getKey(),
|
|
])
|
|
->goToNextWizardStep()
|
|
->goToNextWizardStep()
|
|
->assertWizardCurrentStep(3)
|
|
->assertSee('Validation decision')
|
|
->assertSee('Validation blocked')
|
|
->assertSee('Provider access must be repaired before restore checks can run.')
|
|
->assertSee('Provider credentials are not available for this environment.')
|
|
->assertSee('Restore checks cannot run until the provider connection is repaired.')
|
|
->assertSee('Repair the provider connection before validation can run.')
|
|
->assertSee('Review provider connection')
|
|
->assertFormComponentActionDoesNotExist('check_results', 'run_restore_checks')
|
|
->assertDontSee('Provider credentials are missing');
|
|
|
|
$component
|
|
->goToNextWizardStep()
|
|
->assertWizardCurrentStep(3)
|
|
->assertNotified('Validation blocked');
|
|
|
|
expect($component->html())->not->toContain('Exception');
|
|
});
|
|
|
|
it('renders compact validation evidence on step 3 after checks pass', function (): void {
|
|
[$user, $tenant] = spec332ProductProcessFlowTenant();
|
|
[$backupSet, $backupItem] = spec332UsableBackupFixture($tenant);
|
|
$data = spec332CurrentPreviewData($backupSet, $backupItem);
|
|
|
|
$component = spec332WizardComponent($user, $tenant)
|
|
->set('data', $data)
|
|
->goToWizardStep(3)
|
|
->assertWizardCurrentStep(3)
|
|
->assertSee('Validation decision')
|
|
->assertSee('Validation passed')
|
|
->assertSee('Blockers')
|
|
->assertSee('Warnings')
|
|
->assertSee('Safe')
|
|
->assertSee('View 1 safe check detail')
|
|
->assertSee('Validation evidence')
|
|
->assertSee('View validation gates and restore proof')
|
|
->assertSee('Checks are current for this scope. Rerun only after scope or mapping changes.')
|
|
->assertDontSee('No checks have been recorded for this scope yet.')
|
|
->assertDontSee('Run checks after defining scope and mapping missing groups.');
|
|
|
|
$html = $component->html();
|
|
|
|
expect($html)
|
|
->toContain('data-testid="restore-run-safe-checks-details"')
|
|
->toContain('data-testid="restore-run-safety-evidence"');
|
|
});
|
|
|
|
it('keeps preview decision-first while showing compact safety status and restore proof', function (): void {
|
|
[$user, $tenant] = spec332ProductProcessFlowTenant();
|
|
[$backupSet, $backupItem] = spec332UsableBackupFixture($tenant);
|
|
$data = spec332CurrentPreviewData($backupSet, $backupItem);
|
|
|
|
$component = spec332WizardComponent($user, $tenant)
|
|
->set('data', $data)
|
|
->goToWizardStep(4)
|
|
->assertWizardCurrentStep(4)
|
|
->assertSee('Review the preview and continue to confirmation.')
|
|
->assertDontSee('Review prerequisites before execution.')
|
|
->assertSee('Preview evidence')
|
|
->assertSee('View safety gates and restore proof')
|
|
->assertSee('Confirmation')
|
|
->assertSee('Restore Proof')
|
|
->assertSee('Operation proof')
|
|
->assertSee('Post-run evidence')
|
|
->assertSee('Diagnostics - Collapsed')
|
|
->assertDontSee('spec332 raw payload should stay hidden')
|
|
->assertDontSee('tenant-wide recovery is proven');
|
|
|
|
$html = $component->html();
|
|
|
|
expect($html)
|
|
->toContain('data-testid="restore-run-preview-evidence"');
|
|
|
|
expect(preg_match('/<details[^>]*data-testid="restore-run-preview-evidence-details"[^>]*>/', $html, $matches))->toBe(1);
|
|
expect($matches[0])->not->toContain(' open');
|
|
});
|
|
|
|
it('keeps confirm step locked 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');
|
|
|
|
ensureDefaultProviderConnection($tenant, 'microsoft', ensureCredential: false);
|
|
[$backupSet, $backupItem] = spec332UsableBackupFixture($tenant);
|
|
$data = spec332CurrentPreviewData($backupSet, $backupItem);
|
|
|
|
$contract = RestoreRunCreatePresenter::contract($data, 4, true, $tenant, $user);
|
|
|
|
expect(data_get($contract, 'previewSummary.primaryNextActionLabel'))
|
|
->toBe('Review preview and continue to confirmation; resolve execution prerequisites before executing.')
|
|
->and(data_get($contract, 'wizard_gate.next_gate_label'))->toBe('Confirmation required')
|
|
->and(data_get($contract, 'wizard_gate.can_continue'))->toBeTrue()
|
|
->and(data_get($contract, 'wizard_gate.can_execute'))->toBeFalse();
|
|
|
|
$component = spec332WizardComponent($user, $tenant)
|
|
->set('data', $data)
|
|
->goToWizardStep(5)
|
|
->assertWizardCurrentStep(5)
|
|
->assertSee('Confirmation summary')
|
|
->assertSee('Execution prerequisites blocked')
|
|
->assertSee('Create preview-only run')
|
|
->assertSee('Create a preview-only run, or resolve execution prerequisites before queueing real execution.')
|
|
->assertSee('Execution')
|
|
->assertSee('Execution unavailable until prerequisites are resolved')
|
|
->assertSee('Restore execution is blocked until required prerequisites are available. Evidence does not exist yet.')
|
|
->assertSee('Recovery is not verified until post-run evidence exists.')
|
|
->assertFormFieldDisabled('is_dry_run');
|
|
|
|
expect($component->html())->not->toContain('Operation proof is complete');
|
|
});
|
|
|
|
it('shows confirm step readiness when execution prerequisites are healthy', function (): void {
|
|
[$user, $tenant] = spec332ProductProcessFlowTenant();
|
|
[$backupSet, $backupItem] = spec332UsableBackupFixture($tenant);
|
|
$data = spec332CurrentPreviewData($backupSet, $backupItem);
|
|
|
|
spec332WizardComponent($user, $tenant)
|
|
->set('data', $data)
|
|
->goToWizardStep(5)
|
|
->assertWizardCurrentStep(5)
|
|
->assertSee('Confirmation summary')
|
|
->assertSee('Preview-only run ready')
|
|
->assertSee('Create preview-only run')
|
|
->assertSee('Create a preview-only restore run.')
|
|
->assertSee('Execution')
|
|
->assertSee('Execution unavailable until confirmation')
|
|
->assertSee('Recovery is not verified until post-run evidence exists.')
|
|
->assertFormFieldEnabled('is_dry_run');
|
|
});
|