create([ 'managed_environment_id' => 'tenant-1', 'name' => 'ManagedEnvironment One', 'metadata' => [], 'rbac_status' => 'ok', 'rbac_last_checked_at' => now(), ]); $policy = Policy::create([ 'managed_environment_id' => $tenant->id, 'external_id' => 'policy-1', 'policy_type' => 'settingsCatalogPolicy', 'display_name' => 'Settings Catalog', 'platform' => 'windows', ]); $backupSet = BackupSet::create([ 'managed_environment_id' => $tenant->id, 'name' => 'Backup', 'status' => 'completed', 'item_count' => 1, ]); $backupItem = BackupItem::create([ 'managed_environment_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, '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(); [$user, $tenant] = createUserWithTenant(tenant: $tenant, user: $user, role: 'owner'); $this->actingAs($user); setAdminPanelContext($tenant); $component = Livewire::test(CreateRestoreRun::class) ->fillForm([ 'backup_set_id' => $backupSet->id, ]) ->goToNextWizardStep() ->fillForm([ 'scope_mode' => 'selected', 'backup_item_ids' => [$backupItem->id], ]); $mapping = $component->get('data.group_mapping'); expect($mapping)->toBeArray(); expect(array_key_exists('source-group-1', $mapping))->toBeTrue(); expect($mapping['source-group-1'])->toBeNull(); $component->assertFormFieldVisible('group_mapping.source-group-1'); }); test('restore group mapping picker productizes the empty directory cache state', function () { $tenant = ManagedEnvironment::factory()->create([ 'managed_environment_id' => 'tenant-1', 'name' => 'ManagedEnvironment One', 'metadata' => [], 'rbac_status' => 'ok', 'rbac_last_checked_at' => now(), ]); $user = User::factory()->create(); [$user, $tenant] = createUserWithTenant(tenant: $tenant, user: $user, role: 'owner'); $this->actingAs($user); setAdminPanelContext($tenant); $component = Livewire::actingAs($user)->test(EntraGroupCachePickerTable::class, [ 'sourceGroupId' => '00000000-0000-0000-0000-d908d2dd', 'sourceGroupDisplayName' => 'ADSyncOperators', 'tenantId' => (int) $tenant->getKey(), ]); $component ->assertSee('Source group') ->assertSee('ADSyncOperators') ->assertSee('Source ID: 00000000-0000-0000-0000-d908d2dd') ->assertSee('No directory group cache available') ->assertSee('TenantPilot needs cached directory groups before target mappings can be selected.') ->assertSee('Sync directory groups, then return to this mapping.') ->assertSee('Open group sync') ->assertSee('View group sync operations') ->assertDontSee('No cached groups found') ->assertDontSee('Directory Groups') ->assertDontSee('No groups found in tenant') ->assertDontSee('Search groups…') ->assertDontSee('Stale'); $html = html_entity_decode($component->html(), ENT_QUOTES | ENT_HTML5); $expectedGroupSyncUrl = \App\Filament\Resources\EntraGroupResource::getUrl('index', tenant: $tenant); $expectedOperationsUrl = \App\Support\OperationRunLinks::index($tenant, operationType: 'directory.groups.sync'); expect($html) ->toContain('data-testid="restore-group-picker-empty-state"') ->toContain('href="'.$expectedGroupSyncUrl.'"') ->toContain('href="'.$expectedOperationsUrl.'"') ->toContain('target="_blank"') ->not->toMatch('/>\s*Operations\s*create([ 'managed_environment_id' => 'tenant-1', 'name' => 'ManagedEnvironment One', 'metadata' => [], 'rbac_status' => 'ok', 'rbac_last_checked_at' => now(), ]); $otherTenant = ManagedEnvironment::factory()->create([ 'managed_environment_id' => 'tenant-2', 'name' => 'ManagedEnvironment Two', 'metadata' => [], 'rbac_status' => 'ok', 'rbac_last_checked_at' => now(), ]); $cached = EntraGroup::factory()->for($tenant)->create([ 'display_name' => 'Spec332 Cached Group', 'entra_id' => 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', ]); EntraGroup::factory()->for($otherTenant)->create([ 'display_name' => 'Foreign Cached Group', 'entra_id' => 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', ]); $user = User::factory()->create(); [$user, $tenant] = createUserWithTenant(tenant: $tenant, user: $user, role: 'owner'); $this->actingAs($user); setAdminPanelContext($tenant); $component = Livewire::actingAs($user)->test(EntraGroupCachePickerTable::class, [ 'sourceGroupId' => '00000000-0000-0000-0000-d908d2dd', 'sourceGroupDisplayName' => 'ADSyncOperators', 'tenantId' => (int) $tenant->getKey(), ]); $component ->assertSee('Source group') ->assertSee('ADSyncOperators') ->assertSee('Source ID: 00000000-0000-0000-0000-d908d2dd') ->assertSee('Spec332 Cached Group') ->assertDontSee('Foreign Cached Group') ->assertDontSee('No directory group cache available'); $html = html_entity_decode($component->html(), ENT_QUOTES | ENT_HTML5); expect($html) ->toContain('data-testid="restore-group-picker-table"'); }); test('restore wizard persists group mapping selections', function () { $tenant = ManagedEnvironment::factory()->create([ 'managed_environment_id' => 'tenant-1', 'name' => 'ManagedEnvironment One', 'metadata' => [], 'rbac_status' => 'ok', 'rbac_last_checked_at' => now(), ]); $policy = Policy::create([ 'managed_environment_id' => $tenant->id, 'external_id' => 'policy-1', 'policy_type' => 'settingsCatalogPolicy', 'display_name' => 'Settings Catalog', 'platform' => 'windows', ]); $backupSet = BackupSet::create([ 'managed_environment_id' => $tenant->id, 'name' => 'Backup', 'status' => 'completed', 'item_count' => 1, ]); $backupItem = BackupItem::create([ 'managed_environment_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, 'payload' => ['id' => $policy->external_id], 'assignments' => [[ 'target' => [ '@odata.type' => '#microsoft.graph.groupAssignmentTarget', 'groupId' => 'source-group-1', 'group_display_name' => 'Source Group', ], 'intent' => 'apply', ]], ]); $targetGroupId = fake()->uuid(); $this->mock(GroupResolver::class, function (MockInterface $mock) use ($targetGroupId): void { $mock->shouldReceive('resolveGroupIds') ->andReturnUsing(function (array $groupIds) use ($targetGroupId): array { return collect($groupIds) ->mapWithKeys(function (string $id) use ($targetGroupId): array { $resolved = $id === $targetGroupId; return [$id => [ 'id' => $id, 'displayName' => $resolved ? 'Target Group' : null, 'orphaned' => ! $resolved, ]]; }) ->all(); }); }); $user = User::factory()->create(); [$user, $tenant] = createUserWithTenant(tenant: $tenant, user: $user, role: 'owner'); $this->actingAs($user); setAdminPanelContext($tenant); Livewire::test(CreateRestoreRun::class) ->fillForm([ 'backup_set_id' => $backupSet->id, ]) ->goToNextWizardStep() ->fillForm([ 'scope_mode' => 'selected', 'backup_item_ids' => [$backupItem->id], 'group_mapping' => [ 'source-group-1' => $targetGroupId, ], ]) ->goToNextWizardStep() ->goToNextWizardStep() ->callFormComponentAction('preview_diffs', 'run_restore_preview') ->goToNextWizardStep() ->call('create') ->assertHasNoFormErrors(); $run = RestoreRun::first(); expect($run)->not->toBeNull(); expect($run->group_mapping)->toBe([ 'source-group-1' => $targetGroupId, ]); $this->assertDatabaseHas('audit_logs', [ 'managed_environment_id' => $tenant->id, 'action' => 'restore.group_mapping.applied', ]); }); test('restore wizard can fill a group mapping entry from directory cache picker', function () { $tenant = ManagedEnvironment::factory()->create([ 'managed_environment_id' => 'tenant-1', 'name' => 'ManagedEnvironment One', 'metadata' => [], 'rbac_status' => 'ok', 'rbac_last_checked_at' => now(), ]); $user = User::factory()->create(); [$user, $tenant] = createUserWithTenant(tenant: $tenant, user: $user, role: 'owner'); $this->actingAs($user); setAdminPanelContext($tenant); $sourceGroupId = fake()->uuid(); $targetGroupId = fake()->uuid(); $existingCheckSummary = [ 'blocking' => 0, 'warning' => 0, 'safe' => 1, ]; $existingCheckResults = [[ 'code' => 'safe', 'severity' => 'safe', ]]; $oldGroupMapping = [ $sourceGroupId => 'old', ]; $oldScopeFingerprint = RestoreScopeFingerprint::fromInputs( backupSetId: 10, scopeMode: 'all', selectedItemIds: [], groupMapping: $oldGroupMapping, ); $component = Livewire::test(CreateRestoreRun::class) ->set('data.backup_set_id', 10) ->set('data.scope_mode', 'all') ->set('data.group_mapping', $oldGroupMapping) ->set('data.check_summary', $existingCheckSummary) ->set('data.check_results', $existingCheckResults) ->set('data.check_basis', [ 'fingerprint' => $oldScopeFingerprint->fingerprint, 'ran_at' => now('UTC')->toIso8601String(), 'blocking_count' => 0, 'warning_count' => 0, 'result_codes' => ['safe'], ]) ->call('applyEntraGroupCachePick', sourceGroupId: $sourceGroupId, entraId: $targetGroupId) ->assertSet("data.group_mapping.{$sourceGroupId}", $targetGroupId) ->assertSet('data.is_dry_run', true) ->assertSet('data.acknowledged_impact', false) ->assertSet('data.tenant_confirm', null); expect($component->get('data.check_summary'))->toBe($existingCheckSummary) ->and($component->get('data.check_results'))->toBe($existingCheckResults) ->and($component->get('data.check_invalidation_reasons'))->toContain('scope_mismatch'); });