diff --git a/app/Filament/Resources/RestoreRunResource.php b/app/Filament/Resources/RestoreRunResource.php index c1a92b3..f453c81 100644 --- a/app/Filament/Resources/RestoreRunResource.php +++ b/app/Filament/Resources/RestoreRunResource.php @@ -13,6 +13,7 @@ use App\Services\BulkOperationService; use App\Services\Graph\GraphClientInterface; use App\Services\Graph\GroupResolver; +use App\Services\Intune\RestoreDiffGenerator; use App\Services\Intune\RestoreRiskChecker; use App\Services\Intune\RestoreService; use BackedEnum; @@ -180,6 +181,9 @@ public static function getWizardSteps(): array $set('check_summary', null); $set('check_results', []); $set('checks_ran_at', null); + $set('preview_summary', null); + $set('preview_diffs', []); + $set('preview_ran_at', null); }) ->required(), ]), @@ -199,6 +203,9 @@ public static function getWizardSteps(): array $set('check_summary', null); $set('check_results', []); $set('checks_ran_at', null); + $set('preview_summary', null); + $set('preview_diffs', []); + $set('preview_ran_at', null); if ($state === 'all') { $set('backup_item_ids', null); @@ -223,6 +230,9 @@ public static function getWizardSteps(): array $set('check_summary', null); $set('check_results', []); $set('checks_ran_at', null); + $set('preview_summary', null); + $set('preview_diffs', []); + $set('preview_ran_at', null); }) ->visible(fn (Get $get): bool => $get('scope_mode') === 'selected') ->required(fn (Get $get): bool => $get('scope_mode') === 'selected') @@ -292,6 +302,9 @@ public static function getWizardSteps(): array $set('check_summary', null); $set('check_results', []); $set('checks_ran_at', null); + $set('preview_summary', null); + $set('preview_diffs', []); + $set('preview_ran_at', null); }) ->helperText('Choose a target group or select Skip.'); }, $unresolved); @@ -414,16 +427,99 @@ public static function getWizardSteps(): array ->helperText('Run checks after defining scope and mapping missing groups.'), ]), Step::make('Preview') - ->description('Dry-run preview (Phase 5)') + ->description('Dry-run preview') ->schema([ Forms\Components\Toggle::make('is_dry_run') ->label('Preview only (dry-run)') ->default(true) ->disabled() ->helperText('Execution will be enabled once checks, preview, and confirmations are implemented (Phase 6).'), - Forms\Components\Placeholder::make('preview_placeholder') + Forms\Components\Hidden::make('preview_summary') + ->default(null), + Forms\Components\Hidden::make('preview_ran_at') + ->default(null) + ->required(), + Forms\Components\ViewField::make('preview_diffs') ->label('Preview') - ->content('Preview diff summary will be added in Phase 5.'), + ->default([]) + ->view('filament.forms.components.restore-run-preview') + ->viewData(fn (Get $get): array => [ + 'summary' => $get('preview_summary'), + 'ranAt' => $get('preview_ran_at'), + ]) + ->hintActions([ + Actions\Action::make('run_restore_preview') + ->label('Generate preview') + ->icon('heroicon-o-eye') + ->color('gray') + ->visible(fn (Get $get): bool => filled($get('backup_set_id'))) + ->action(function (Get $get, Set $set): void { + $tenant = Tenant::current(); + + if (! $tenant) { + return; + } + + $backupSetId = $get('backup_set_id'); + + if (! $backupSetId) { + return; + } + + $backupSet = BackupSet::find($backupSetId); + + if (! $backupSet || $backupSet->tenant_id !== $tenant->id) { + Notification::make() + ->title('Unable to generate preview') + ->body('Backup set is not available for the active tenant.') + ->danger() + ->send(); + + return; + } + + $scopeMode = $get('scope_mode') ?? 'all'; + $selectedItemIds = ($scopeMode === 'selected') + ? ($get('backup_item_ids') ?? null) + : null; + + $selectedItemIds = is_array($selectedItemIds) ? $selectedItemIds : null; + + $generator = app(RestoreDiffGenerator::class); + $outcome = $generator->generate( + tenant: $tenant, + backupSet: $backupSet, + selectedItemIds: $selectedItemIds, + ); + + $summary = $outcome['summary'] ?? []; + $diffs = $outcome['diffs'] ?? []; + + $set('preview_summary', $summary, shouldCallUpdatedHooks: true); + $set('preview_diffs', $diffs, shouldCallUpdatedHooks: true); + $set('preview_ran_at', $summary['generated_at'] ?? now()->toIso8601String(), shouldCallUpdatedHooks: true); + + $policiesChanged = (int) ($summary['policies_changed'] ?? 0); + $policiesTotal = (int) ($summary['policies_total'] ?? 0); + + Notification::make() + ->title('Preview generated') + ->body("Policies: {$policiesChanged}/{$policiesTotal} changed") + ->status($policiesChanged > 0 ? 'warning' : 'success') + ->send(); + }), + Actions\Action::make('clear_restore_preview') + ->label('Clear') + ->icon('heroicon-o-x-mark') + ->color('gray') + ->visible(fn (Get $get): bool => filled($get('preview_diffs')) || filled($get('preview_summary'))) + ->action(function (Set $set): void { + $set('preview_summary', null, shouldCallUpdatedHooks: true); + $set('preview_diffs', [], shouldCallUpdatedHooks: true); + $set('preview_ran_at', null, shouldCallUpdatedHooks: true); + }), + ]) + ->helperText('Generate a normalized diff preview before creating the dry-run restore.'), ]), Step::make('Confirm & Execute') ->description('Explicit confirmations (Phase 6)') @@ -976,8 +1072,18 @@ public static function createRestoreRun(array $data): RestoreRun $checkSummary = $data['check_summary'] ?? null; $checkResults = $data['check_results'] ?? null; $checksRanAt = $data['checks_ran_at'] ?? null; + $previewSummary = $data['preview_summary'] ?? null; + $previewDiffs = $data['preview_diffs'] ?? null; + $previewRanAt = $data['preview_ran_at'] ?? null; - if (is_array($checkSummary) || is_array($checkResults) || (is_string($checksRanAt) && $checksRanAt !== '')) { + if ( + is_array($checkSummary) + || is_array($checkResults) + || (is_string($checksRanAt) && $checksRanAt !== '') + || is_array($previewSummary) + || is_array($previewDiffs) + || (is_string($previewRanAt) && $previewRanAt !== '') + ) { $metadata = $restoreRun->metadata ?? []; if (is_array($checkSummary)) { @@ -992,6 +1098,18 @@ public static function createRestoreRun(array $data): RestoreRun $metadata['checks_ran_at'] = $checksRanAt; } + if (is_array($previewSummary)) { + $metadata['preview_summary'] = $previewSummary; + } + + if (is_array($previewDiffs)) { + $metadata['preview_diffs'] = $previewDiffs; + } + + if (is_string($previewRanAt) && $previewRanAt !== '') { + $metadata['preview_ran_at'] = $previewRanAt; + } + $restoreRun->update([ 'metadata' => $metadata, ]); diff --git a/app/Services/Intune/RestoreDiffGenerator.php b/app/Services/Intune/RestoreDiffGenerator.php new file mode 100644 index 0000000..200eb65 --- /dev/null +++ b/app/Services/Intune/RestoreDiffGenerator.php @@ -0,0 +1,248 @@ +|null $selectedItemIds + * @return array{summary: array, diffs: array>} + */ + public function generate(Tenant $tenant, BackupSet $backupSet, ?array $selectedItemIds = null): array + { + if ($backupSet->tenant_id !== $tenant->id) { + throw new \InvalidArgumentException('Backup set does not belong to the provided tenant.'); + } + + if ($selectedItemIds === []) { + $selectedItemIds = null; + } + + $items = $this->loadItems($backupSet, $selectedItemIds); + $policyItems = $items + ->reject(fn (BackupItem $item): bool => $item->isFoundation()) + ->values(); + + $policyIds = $policyItems + ->pluck('policy_id') + ->filter() + ->unique() + ->values() + ->all(); + + $latestVersions = $this->latestVersionsByPolicyId($tenant, $policyIds); + + $maxDetailedDiffs = 25; + $maxEntriesPerSection = 200; + + $policiesChanged = 0; + $assignmentsChanged = 0; + $scopeTagsChanged = 0; + + $diffs = []; + $diffsOmitted = 0; + + foreach ($policyItems as $index => $item) { + $policyId = $item->policy_id ? (int) $item->policy_id : null; + $currentVersion = $policyId ? ($latestVersions[$policyId] ?? null) : null; + + $currentSnapshot = is_array($currentVersion?->snapshot) ? $currentVersion->snapshot : []; + $backupSnapshot = is_array($item->payload) ? $item->payload : []; + + $policyType = (string) ($item->policy_type ?? ''); + $platform = $item->platform; + + $from = $this->policyNormalizer->flattenForDiff($currentSnapshot, $policyType, $platform); + $to = $this->policyNormalizer->flattenForDiff($backupSnapshot, $policyType, $platform); + + $diff = $this->versionDiff->compare($from, $to); + $summary = $diff['summary'] ?? ['added' => 0, 'removed' => 0, 'changed' => 0]; + + $hasPolicyChanges = ((int) ($summary['added'] ?? 0) + (int) ($summary['removed'] ?? 0) + (int) ($summary['changed'] ?? 0)) > 0; + + if ($hasPolicyChanges) { + $policiesChanged++; + } + + $assignmentDiff = $this->assignmentsChanged($item->assignments, $currentVersion?->assignments); + if ($assignmentDiff) { + $assignmentsChanged++; + } + + $scopeTagDiff = $this->scopeTagsChanged($item, $currentVersion); + if ($scopeTagDiff) { + $scopeTagsChanged++; + } + + $diffEntry = [ + 'backup_item_id' => $item->id, + 'display_name' => $item->resolvedDisplayName(), + 'policy_identifier' => $item->policy_identifier, + 'policy_type' => $policyType, + 'platform' => $platform, + 'action' => $currentVersion ? 'update' : 'create', + 'diff' => [ + 'summary' => $summary, + 'added' => [], + 'removed' => [], + 'changed' => [], + ], + 'assignments_changed' => $assignmentDiff, + 'scope_tags_changed' => $scopeTagDiff, + 'diff_omitted' => false, + 'diff_truncated' => false, + ]; + + if ($index >= $maxDetailedDiffs) { + $diffEntry['diff_omitted'] = true; + $diffEntry['diff_truncated'] = true; + $diffEntry['diff'] = [ + 'summary' => $summary, + ]; + $diffsOmitted++; + $diffs[] = $diffEntry; + + continue; + } + + $added = is_array($diff['added'] ?? null) ? $diff['added'] : []; + $removed = is_array($diff['removed'] ?? null) ? $diff['removed'] : []; + $changed = is_array($diff['changed'] ?? null) ? $diff['changed'] : []; + + $diffEntry['diff_truncated'] = count($added) > $maxEntriesPerSection + || count($removed) > $maxEntriesPerSection + || count($changed) > $maxEntriesPerSection; + + $diffEntry['diff'] = [ + 'summary' => $summary, + 'added' => array_slice($added, 0, $maxEntriesPerSection, true), + 'removed' => array_slice($removed, 0, $maxEntriesPerSection, true), + 'changed' => array_slice($changed, 0, $maxEntriesPerSection, true), + ]; + + $diffs[] = $diffEntry; + } + + return [ + 'summary' => [ + 'generated_at' => CarbonImmutable::now()->toIso8601String(), + 'policies_total' => $policyItems->count(), + 'policies_changed' => $policiesChanged, + 'assignments_changed' => $assignmentsChanged, + 'scope_tags_changed' => $scopeTagsChanged, + 'diffs_detailed' => min($policyItems->count(), $maxDetailedDiffs), + 'diffs_omitted' => $diffsOmitted, + 'limits' => [ + 'max_detailed_diffs' => $maxDetailedDiffs, + 'max_entries_per_section' => $maxEntriesPerSection, + ], + ], + 'diffs' => $diffs, + ]; + } + + /** + * @param array|null $selectedItemIds + * @return Collection + */ + private function loadItems(BackupSet $backupSet, ?array $selectedItemIds): Collection + { + $query = $backupSet->items()->getQuery(); + + if ($selectedItemIds !== null) { + $query->whereIn('id', $selectedItemIds); + } + + return $query->orderBy('id')->get(); + } + + /** + * @param array $policyIds + * @return array + */ + private function latestVersionsByPolicyId(Tenant $tenant, array $policyIds): array + { + if ($policyIds === []) { + return []; + } + + $latestVersionsQuery = PolicyVersion::query() + ->where('tenant_id', $tenant->id) + ->whereIn('policy_id', $policyIds) + ->selectRaw('policy_id, max(version_number) as version_number') + ->groupBy('policy_id'); + + return PolicyVersion::query() + ->where('tenant_id', $tenant->id) + ->joinSub($latestVersionsQuery, 'latest_versions', function ($join): void { + $join->on('policy_versions.policy_id', '=', 'latest_versions.policy_id') + ->on('policy_versions.version_number', '=', 'latest_versions.version_number'); + }) + ->get() + ->keyBy('policy_id') + ->all(); + } + + private function assignmentsChanged(?array $backupAssignments, ?array $currentAssignments): bool + { + $backup = $this->normalizeAssignments($backupAssignments); + $current = $this->normalizeAssignments($currentAssignments); + + return $backup !== $current; + } + + private function scopeTagsChanged(BackupItem $backupItem, ?PolicyVersion $currentVersion): bool + { + $backupIds = $backupItem->scope_tag_ids; + $backupIds = is_array($backupIds) ? $backupIds : []; + $backupIds = array_values(array_filter($backupIds, fn (mixed $id): bool => is_string($id) && $id !== '' && $id !== '0')); + sort($backupIds); + + $scopeTags = $currentVersion?->scope_tags; + $currentIds = is_array($scopeTags) ? ($scopeTags['ids'] ?? []) : []; + $currentIds = is_array($currentIds) ? $currentIds : []; + $currentIds = array_values(array_filter($currentIds, fn (mixed $id): bool => is_string($id) && $id !== '' && $id !== '0')); + sort($currentIds); + + return $backupIds !== $currentIds; + } + + /** + * @return array> + */ + private function normalizeAssignments(?array $assignments): array + { + $assignments = is_array($assignments) ? $assignments : []; + + $normalized = []; + + foreach ($assignments as $assignment) { + if (! is_array($assignment)) { + continue; + } + + $normalized[] = $assignment; + } + + usort($normalized, function (array $a, array $b): int { + $left = json_encode($a, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?: ''; + $right = json_encode($b, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?: ''; + + return $left <=> $right; + }); + + return $normalized; + } +} diff --git a/resources/views/filament/forms/components/restore-run-preview.blade.php b/resources/views/filament/forms/components/restore-run-preview.blade.php new file mode 100644 index 0000000..174c6ea --- /dev/null +++ b/resources/views/filament/forms/components/restore-run-preview.blade.php @@ -0,0 +1,177 @@ +@php + $diffs = $getState() ?? []; + $diffs = is_array($diffs) ? $diffs : []; + + $summary = $summary ?? []; + $summary = is_array($summary) ? $summary : []; + + $ranAt = $ranAt ?? null; + $ranAtLabel = null; + + if (is_string($ranAt) && $ranAt !== '') { + try { + $ranAtLabel = \Carbon\CarbonImmutable::parse($ranAt)->format('Y-m-d H:i'); + } catch (\Throwable) { + $ranAtLabel = $ranAt; + } + } + + $policiesTotal = (int) ($summary['policies_total'] ?? 0); + $policiesChanged = (int) ($summary['policies_changed'] ?? 0); + $assignmentsChanged = (int) ($summary['assignments_changed'] ?? 0); + $scopeTagsChanged = (int) ($summary['scope_tags_changed'] ?? 0); + $diffsOmitted = (int) ($summary['diffs_omitted'] ?? 0); + + $limitedKeys = static function (array $items, int $limit = 8): array { + $keys = array_keys($items); + + if (count($keys) <= $limit) { + return $keys; + } + + return array_slice($keys, 0, $limit); + }; +@endphp + +
+ +
+ + {{ $policiesChanged }}/{{ $policiesTotal }} policies changed + + + {{ $assignmentsChanged }} assignments changed + + + {{ $scopeTagsChanged }} scope tags changed + + @if ($diffsOmitted > 0) + + {{ $diffsOmitted }} diffs omitted (limit) + + @endif +
+
+ + @if ($diffs === []) + +
+ No preview generated yet. +
+
+ @else +
+ @foreach ($diffs as $entry) + @php + $entry = is_array($entry) ? $entry : []; + $name = $entry['display_name'] ?? $entry['policy_identifier'] ?? 'Item'; + $type = $entry['policy_type'] ?? 'type'; + $platform = $entry['platform'] ?? 'platform'; + $action = $entry['action'] ?? 'update'; + $diff = is_array($entry['diff'] ?? null) ? $entry['diff'] : []; + $diffSummary = is_array($diff['summary'] ?? null) ? $diff['summary'] : []; + + $added = (int) ($diffSummary['added'] ?? 0); + $removed = (int) ($diffSummary['removed'] ?? 0); + $changed = (int) ($diffSummary['changed'] ?? 0); + + $assignmentsDelta = (bool) ($entry['assignments_changed'] ?? false); + $scopeTagsDelta = (bool) ($entry['scope_tags_changed'] ?? false); + $diffOmitted = (bool) ($entry['diff_omitted'] ?? false); + $diffTruncated = (bool) ($entry['diff_truncated'] ?? false); + + $changedKeys = $limitedKeys(is_array($diff['changed'] ?? null) ? $diff['changed'] : []); + $addedKeys = $limitedKeys(is_array($diff['added'] ?? null) ? $diff['added'] : []); + $removedKeys = $limitedKeys(is_array($diff['removed'] ?? null) ? $diff['removed'] : []); + @endphp + + +
+ + {{ $action }} + + + {{ $added }} added + + + {{ $removed }} removed + + + {{ $changed }} changed + + @if ($assignmentsDelta) + + assignments + + @endif + @if ($scopeTagsDelta) + + scope tags + + @endif + @if ($diffTruncated) + + truncated + + @endif +
+ + @if ($diffOmitted) +
+ Diff details omitted due to preview limits. Narrow scope to see more items in detail. +
+ @elseif ($changedKeys !== [] || $addedKeys !== [] || $removedKeys !== []) +
+ @if ($changedKeys !== []) +
+
+ Changed keys (sample) +
+
    + @foreach ($changedKeys as $key) +
  • + {{ $key }} +
  • + @endforeach +
+
+ @endif + @if ($addedKeys !== []) +
+
+ Added keys (sample) +
+
    + @foreach ($addedKeys as $key) +
  • + {{ $key }} +
  • + @endforeach +
+
+ @endif + @if ($removedKeys !== []) +
+
+ Removed keys (sample) +
+
    + @foreach ($removedKeys as $key) +
  • + {{ $key }} +
  • + @endforeach +
+
+ @endif +
+ @endif +
+ @endforeach +
+ @endif +
+ diff --git a/specs/011-restore-run-wizard/tasks.md b/specs/011-restore-run-wizard/tasks.md index 1e7ed97..338a32a 100644 --- a/specs/011-restore-run-wizard/tasks.md +++ b/specs/011-restore-run-wizard/tasks.md @@ -26,8 +26,8 @@ ## Phase 4 — Safety & Conflict Checks - [x] T012 Render check results with severity (blocking/warning/safe) and block execute when blockers exist. ## Phase 5 — Preview (Diff) -- [ ] T013 Implement `RestoreDiffGenerator` using `PolicyNormalizer` + `VersionDiff`. -- [ ] T014 Persist preview summary (and per-item diffs with safe limits) and require preview completion before execute. +- [x] T013 Implement `RestoreDiffGenerator` using `PolicyNormalizer` + `VersionDiff`. +- [x] T014 Persist preview summary (and per-item diffs with safe limits) and require preview completion before execute. ## Phase 6 — Confirm & Execute - [ ] T015 Implement Step 5 confirmations (ack checkbox + tenant hard-confirm). @@ -36,8 +36,8 @@ ## Phase 6 — Confirm & Execute ## Phase 7 — Tests + Formatting - [ ] T018 Add Pest tests for wizard gating rules and status transitions. -- [ ] T019 Add Pest tests for safety checks persistence and blocking behavior. -- [ ] T020 Add Pest tests for preview summary generation. +- [x] T019 Add Pest tests for safety checks persistence and blocking behavior. +- [x] T020 Add Pest tests for preview summary generation. - [x] T021 Run `./vendor/bin/pint --dirty`. - [x] T022 Run targeted tests (e.g. `./vendor/bin/sail artisan test --filter=RestoreRunWizard` once tests exist). diff --git a/tests/Feature/RestoreGroupMappingTest.php b/tests/Feature/RestoreGroupMappingTest.php index 260cc99..7b83c99 100644 --- a/tests/Feature/RestoreGroupMappingTest.php +++ b/tests/Feature/RestoreGroupMappingTest.php @@ -168,6 +168,7 @@ ->fillForm([ 'is_dry_run' => true, ]) + ->callFormComponentAction('preview_diffs', 'run_restore_preview') ->goToNextWizardStep() ->call('create') ->assertHasNoFormErrors(); diff --git a/tests/Feature/RestorePreviewDiffWizardTest.php b/tests/Feature/RestorePreviewDiffWizardTest.php new file mode 100644 index 0000000..ab62af1 --- /dev/null +++ b/tests/Feature/RestorePreviewDiffWizardTest.php @@ -0,0 +1,132 @@ + 'tenant-1', + 'name' => 'Tenant One', + 'metadata' => [], + ]); + + $tenant->makeCurrent(); + + $policy = Policy::create([ + 'tenant_id' => $tenant->id, + 'external_id' => 'policy-1', + 'policy_type' => 'deviceConfiguration', + 'display_name' => 'Device Config Policy', + 'platform' => 'windows', + ]); + + PolicyVersion::create([ + 'tenant_id' => $tenant->id, + 'policy_id' => $policy->id, + 'version_number' => 1, + 'policy_type' => $policy->policy_type, + 'platform' => $policy->platform, + 'captured_at' => now()->subDay(), + 'snapshot' => [ + 'foo' => 'current', + ], + 'metadata' => [], + 'assignments' => [], + 'scope_tags' => [ + 'ids' => ['tag-2'], + 'names' => ['Tag Two'], + ], + ]); + + $backupSet = BackupSet::create([ + 'tenant_id' => $tenant->id, + 'name' => 'Backup', + 'status' => 'completed', + 'item_count' => 1, + ]); + + BackupItem::create([ + 'tenant_id' => $tenant->id, + 'backup_set_id' => $backupSet->id, + 'policy_id' => $policy->id, + 'policy_identifier' => $policy->external_id, + 'policy_type' => $policy->policy_type, + 'platform' => $policy->platform, + 'captured_at' => now(), + 'payload' => [ + 'foo' => 'backup', + ], + 'assignments' => [[ + 'target' => [ + '@odata.type' => '#microsoft.graph.groupAssignmentTarget', + 'groupId' => 'group-1', + ], + 'intent' => 'apply', + ]], + 'metadata' => [ + 'scope_tag_ids' => ['tag-1'], + 'scope_tag_names' => ['Tag One'], + ], + ]); + + $user = User::factory()->create(); + $this->actingAs($user); + + $component = Livewire::test(CreateRestoreRun::class) + ->fillForm([ + 'backup_set_id' => $backupSet->id, + ]) + ->goToNextWizardStep() + ->goToNextWizardStep() + ->goToNextWizardStep() + ->callFormComponentAction('preview_diffs', 'run_restore_preview'); + + $summary = $component->get('data.preview_summary'); + $diffs = $component->get('data.preview_diffs'); + + expect($summary)->toBeArray(); + expect($summary['policies_total'] ?? null)->toBe(1); + expect($summary['policies_changed'] ?? null)->toBe(1); + expect($summary['assignments_changed'] ?? null)->toBe(1); + expect($summary['scope_tags_changed'] ?? null)->toBe(1); + + expect($diffs)->toBeArray(); + expect($diffs)->not->toBeEmpty(); + + $first = $diffs[0] ?? []; + expect($first)->toBeArray(); + expect($first['action'] ?? null)->toBe('update'); + expect($first['assignments_changed'] ?? null)->toBeTrue(); + expect($first['scope_tags_changed'] ?? null)->toBeTrue(); + expect($first['diff']['summary']['changed'] ?? null)->toBe(1); + + $component + ->goToNextWizardStep() + ->call('create') + ->assertHasNoFormErrors(); + + $run = RestoreRun::query()->latest('id')->first(); + + expect($run)->not->toBeNull(); + expect($run->metadata)->toHaveKeys([ + 'preview_summary', + 'preview_diffs', + 'preview_ran_at', + ]); + expect($run->metadata['preview_summary']['policies_changed'] ?? null)->toBe(1); +}); diff --git a/tests/Feature/RestoreRiskChecksWizardTest.php b/tests/Feature/RestoreRiskChecksWizardTest.php index 12c98a6..79e90e1 100644 --- a/tests/Feature/RestoreRiskChecksWizardTest.php +++ b/tests/Feature/RestoreRiskChecksWizardTest.php @@ -114,6 +114,7 @@ $component ->goToNextWizardStep() + ->callFormComponentAction('preview_diffs', 'run_restore_preview') ->goToNextWizardStep() ->call('create') ->assertHasNoFormErrors(); diff --git a/tests/Feature/RestoreRunWizardMetadataTest.php b/tests/Feature/RestoreRunWizardMetadataTest.php index b8f13e1..7e8469c 100644 --- a/tests/Feature/RestoreRunWizardMetadataTest.php +++ b/tests/Feature/RestoreRunWizardMetadataTest.php @@ -65,6 +65,7 @@ ->fillForm([ 'is_dry_run' => true, ]) + ->callFormComponentAction('preview_diffs', 'run_restore_preview') ->goToNextWizardStep() ->call('create') ->assertHasNoFormErrors(); @@ -127,6 +128,7 @@ ->goToNextWizardStep() ->goToNextWizardStep() ->set('data.is_dry_run', false) + ->callFormComponentAction('preview_diffs', 'run_restore_preview') ->goToNextWizardStep() ->call('create') ->assertHasNoFormErrors();