'tenant-1', 'name' => 'Tenant One', 'metadata' => [], ]); $tenant->makeCurrent(); $policy = Policy::create([ 'tenant_id' => $tenant->id, 'external_id' => 'policy-1', 'policy_type' => 'settingsCatalogPolicy', 'display_name' => 'Settings Catalog', 'platform' => 'windows', ]); $backupSet = BackupSet::create([ 'tenant_id' => $tenant->id, 'name' => 'Backup', 'status' => 'completed', 'item_count' => 1, ]); $backupItem = 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' => ['id' => $policy->external_id], 'assignments' => [[ 'target' => [ '@odata.type' => '#microsoft.graph.groupAssignmentTarget', 'groupId' => 'source-group-1', 'group_display_name' => 'Source Group', ], 'intent' => 'apply', ]], ]); $this->mock(GroupResolver::class, function (MockInterface $mock) { $mock->shouldReceive('resolveGroupIds') ->andReturnUsing(function (array $groupIds): array { return collect($groupIds) ->mapWithKeys(fn (string $id) => [$id => [ 'id' => $id, 'displayName' => null, 'orphaned' => true, ]]) ->all(); }); }); $user = User::factory()->create(); $this->actingAs($user); $user->tenants()->syncWithoutDetaching([ $tenant->getKey() => ['role' => 'owner'], ]); Filament::setTenant($tenant, true); $component = Livewire::test(CreateRestoreRun::class) ->fillForm([ 'backup_set_id' => $backupSet->id, ]) ->goToNextWizardStep() ->fillForm([ 'scope_mode' => 'selected', 'backup_item_ids' => [$backupItem->id], ]) ->goToNextWizardStep() ->assertFormComponentActionVisible('check_results', 'run_restore_checks') ->callFormComponentAction('check_results', 'run_restore_checks'); $summary = $component->get('data.check_summary'); $results = $component->get('data.check_results'); expect($summary)->toBeArray(); expect($summary['blocking'] ?? null)->toBe(1); expect($summary['has_blockers'] ?? null)->toBeTrue(); expect($results)->toBeArray(); expect($results)->not->toBeEmpty(); $assignmentCheck = collect($results)->firstWhere('code', 'assignment_groups'); expect($assignmentCheck)->toBeArray(); expect($assignmentCheck['severity'] ?? null)->toBe('blocking'); $unmappedGroups = $assignmentCheck['meta']['unmapped'] ?? []; expect($unmappedGroups)->toBeArray(); expect($unmappedGroups[0]['id'] ?? null)->toBe('source-group-1'); $checksRanAt = $component->get('data.checks_ran_at'); expect($checksRanAt)->toBeString(); $component ->goToNextWizardStep() ->set('data.group_mapping.source-group-1', 'SKIP') ->set('data.check_summary', $summary) ->set('data.check_results', $results) ->set('data.checks_ran_at', $checksRanAt) ->callFormComponentAction('preview_diffs', 'run_restore_preview') ->goToNextWizardStep() ->call('create') ->assertHasNoFormErrors(); $run = RestoreRun::query()->latest('id')->first(); expect($run)->not->toBeNull(); expect($run->metadata)->toHaveKeys([ 'check_summary', 'check_results', 'checks_ran_at', ]); expect($run->metadata['check_summary']['blocking'] ?? null)->toBe(1); }); test('restore wizard treats skipped orphaned groups as a warning instead of a blocker', function () { $tenant = Tenant::create([ 'tenant_id' => 'tenant-1', 'name' => 'Tenant One', 'metadata' => [], ]); $tenant->makeCurrent(); $policy = Policy::create([ 'tenant_id' => $tenant->id, 'external_id' => 'policy-1', 'policy_type' => 'settingsCatalogPolicy', 'display_name' => 'Settings Catalog', 'platform' => 'windows', ]); $backupSet = BackupSet::create([ 'tenant_id' => $tenant->id, 'name' => 'Backup', 'status' => 'completed', 'item_count' => 1, ]); $backupItem = 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' => ['id' => $policy->external_id], 'assignments' => [[ 'target' => [ '@odata.type' => '#microsoft.graph.groupAssignmentTarget', 'groupId' => 'source-group-1', 'group_display_name' => 'Source Group', ], 'intent' => 'apply', ]], ]); $this->mock(GroupResolver::class, function (MockInterface $mock) { $mock->shouldReceive('resolveGroupIds') ->andReturnUsing(function (array $groupIds): array { return collect($groupIds) ->mapWithKeys(fn (string $id) => [$id => [ 'id' => $id, 'displayName' => null, 'orphaned' => true, ]]) ->all(); }); }); $user = User::factory()->create(); $this->actingAs($user); $user->tenants()->syncWithoutDetaching([ $tenant->getKey() => ['role' => 'owner'], ]); Filament::setTenant($tenant, true); $component = Livewire::test(CreateRestoreRun::class) ->fillForm([ 'backup_set_id' => $backupSet->id, ]) ->goToNextWizardStep() ->fillForm([ 'scope_mode' => 'selected', 'backup_item_ids' => [$backupItem->id], ]) ->goToNextWizardStep() ->set('data.group_mapping', (object) [ 'source-group-1' => 'SKIP', ]) ->callFormComponentAction('check_results', 'run_restore_checks'); $summary = $component->get('data.check_summary'); $results = $component->get('data.check_results'); expect($summary)->toBeArray(); expect($summary['blocking'] ?? null)->toBe(0); expect($summary['has_blockers'] ?? null)->toBeFalse(); expect($summary['warning'] ?? null)->toBe(1); $assignmentCheck = collect($results)->firstWhere('code', 'assignment_groups'); expect($assignmentCheck)->toBeArray(); expect($assignmentCheck['severity'] ?? null)->toBe('warning'); $skippedGroups = $assignmentCheck['meta']['skipped'] ?? []; expect($skippedGroups)->toBeArray(); expect($skippedGroups[0]['id'] ?? null)->toBe('source-group-1'); }); test('restore wizard flags metadata-only snapshots as blocking for restore-enabled types', function () { $tenant = Tenant::create([ 'tenant_id' => 'tenant-1', 'name' => 'Tenant One', 'metadata' => [], ]); $tenant->makeCurrent(); $policy = Policy::create([ 'tenant_id' => $tenant->id, 'external_id' => 'policy-1', 'policy_type' => 'mamAppConfiguration', 'display_name' => 'MAM App Config', 'platform' => 'mobile', ]); $backupSet = BackupSet::create([ 'tenant_id' => $tenant->id, 'name' => 'Backup', 'status' => 'completed', 'item_count' => 1, ]); $backupItem = 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' => ['id' => $policy->external_id, 'displayName' => $policy->display_name], 'assignments' => [], 'metadata' => [ 'source' => 'metadata_only', 'warnings' => [ 'Graph returned 500 for this policy type. Only local metadata was saved; settings and restore are unavailable until Graph works again.', ], ], ]); $this->mock(GroupResolver::class, function (MockInterface $mock) { $mock->shouldReceive('resolveGroupIds') ->andReturn([]); }); $user = User::factory()->create(); $this->actingAs($user); $user->tenants()->syncWithoutDetaching([ $tenant->getKey() => ['role' => 'owner'], ]); Filament::setTenant($tenant, true); $component = Livewire::test(CreateRestoreRun::class) ->fillForm([ 'backup_set_id' => $backupSet->id, ]) ->goToNextWizardStep() ->fillForm([ 'scope_mode' => 'selected', 'backup_item_ids' => [$backupItem->id], ]) ->goToNextWizardStep() ->callFormComponentAction('check_results', 'run_restore_checks'); $summary = $component->get('data.check_summary'); $results = $component->get('data.check_results'); expect($summary['blocking'] ?? null)->toBe(1); expect($summary['has_blockers'] ?? null)->toBeTrue(); $metadataOnly = collect($results)->firstWhere('code', 'metadata_only'); expect($metadataOnly)->toBeArray(); expect($metadataOnly['severity'] ?? null)->toBe('blocking'); });