diff --git a/app/Services/AssignmentBackupService.php b/app/Services/AssignmentBackupService.php index 300f45f..d46c92a 100644 --- a/app/Services/AssignmentBackupService.php +++ b/app/Services/AssignmentBackupService.php @@ -54,8 +54,9 @@ public function enrichWithAssignments( } // Fetch assignments from Graph API - $tenantId = $tenant->external_id ?? $tenant->tenant_id; - $assignments = $this->assignmentFetcher->fetch($tenantId, $policyId); + $graphOptions = $tenant->graphOptions(); + $tenantId = $graphOptions['tenant'] ?? $tenant->external_id ?? $tenant->tenant_id; + $assignments = $this->assignmentFetcher->fetch($tenantId, $policyId, $graphOptions); if (empty($assignments)) { // No assignments or fetch failed @@ -82,7 +83,7 @@ public function enrichWithAssignments( $hasOrphanedGroups = false; if (! empty($groupIds)) { - $resolvedGroups = $this->groupResolver->resolveGroupIds($groupIds, $tenantId); + $resolvedGroups = $this->groupResolver->resolveGroupIds($groupIds, $tenantId, $graphOptions); $hasOrphanedGroups = collect($resolvedGroups)->contains('orphaned', true); } diff --git a/app/Services/Graph/AssignmentFetcher.php b/app/Services/Graph/AssignmentFetcher.php index 6fba731..53d3ec6 100644 --- a/app/Services/Graph/AssignmentFetcher.php +++ b/app/Services/Graph/AssignmentFetcher.php @@ -18,11 +18,13 @@ public function __construct( * * @return array Returns assignment array or empty array on failure */ - public function fetch(string $tenantId, string $policyId): array + public function fetch(string $tenantId, string $policyId, array $options = []): array { try { + $requestOptions = array_merge($options, ['tenant' => $tenantId]); + // Try primary endpoint - $assignments = $this->fetchPrimary($tenantId, $policyId); + $assignments = $this->fetchPrimary($policyId, $requestOptions); if (! empty($assignments)) { Log::debug('Fetched assignments via primary endpoint', [ @@ -40,7 +42,7 @@ public function fetch(string $tenantId, string $policyId): array 'policy_id' => $policyId, ]); - $assignments = $this->fetchWithExpand($tenantId, $policyId); + $assignments = $this->fetchWithExpand($policyId, $requestOptions); if (! empty($assignments)) { Log::debug('Fetched assignments via fallback endpoint', [ @@ -74,13 +76,11 @@ public function fetch(string $tenantId, string $policyId): array /** * Fetch assignments using primary endpoint. */ - private function fetchPrimary(string $tenantId, string $policyId): array + private function fetchPrimary(string $policyId, array $options): array { $path = "/deviceManagement/configurationPolicies/{$policyId}/assignments"; - $response = $this->graphClient->request('GET', $path, [ - 'tenant' => $tenantId, - ]); + $response = $this->graphClient->request('GET', $path, $options); return $response->data['value'] ?? []; } @@ -88,7 +88,7 @@ private function fetchPrimary(string $tenantId, string $policyId): array /** * Fetch assignments using $expand fallback. */ - private function fetchWithExpand(string $tenantId, string $policyId): array + private function fetchWithExpand(string $policyId, array $options): array { $path = '/deviceManagement/configurationPolicies'; $params = [ @@ -96,10 +96,9 @@ private function fetchWithExpand(string $tenantId, string $policyId): array '$filter' => "id eq '{$policyId}'", ]; - $response = $this->graphClient->request('GET', $path, [ - 'tenant' => $tenantId, + $response = $this->graphClient->request('GET', $path, array_merge($options, [ 'query' => $params, - ]); + ])); $policies = $response->data['value'] ?? []; diff --git a/app/Services/Graph/GroupResolver.php b/app/Services/Graph/GroupResolver.php index e0083de..afac6a4 100644 --- a/app/Services/Graph/GroupResolver.php +++ b/app/Services/Graph/GroupResolver.php @@ -21,7 +21,7 @@ public function __construct( * @param string $tenantId Target tenant ID * @return array Keyed array: ['group-id' => ['id' => ..., 'displayName' => ..., 'orphaned' => bool]] */ - public function resolveGroupIds(array $groupIds, string $tenantId): array + public function resolveGroupIds(array $groupIds, string $tenantId, array $options = []): array { if (empty($groupIds)) { return []; @@ -30,27 +30,27 @@ public function resolveGroupIds(array $groupIds, string $tenantId): array // Create cache key $cacheKey = $this->getCacheKey($groupIds, $tenantId); - return Cache::remember($cacheKey, 300, function () use ($groupIds, $tenantId) { - return $this->fetchAndResolveGroups($groupIds, $tenantId); + return Cache::remember($cacheKey, 300, function () use ($groupIds, $tenantId, $options) { + return $this->fetchAndResolveGroups($groupIds, $tenantId, $options); }); } /** * Fetch groups from Graph API and resolve orphaned IDs. */ - private function fetchAndResolveGroups(array $groupIds, string $tenantId): array + private function fetchAndResolveGroups(array $groupIds, string $tenantId, array $options = []): array { try { $response = $this->graphClient->request( 'POST', '/directoryObjects/getByIds', - [ + array_merge($options, [ 'tenant' => $tenantId, 'json' => [ 'ids' => array_values($groupIds), 'types' => ['group'], ], - ] + ]) ); $resolvedGroups = $response->data['value'] ?? []; diff --git a/app/Services/Intune/PolicyCaptureOrchestrator.php b/app/Services/Intune/PolicyCaptureOrchestrator.php index 685f412..d4d10ba 100644 --- a/app/Services/Intune/PolicyCaptureOrchestrator.php +++ b/app/Services/Intune/PolicyCaptureOrchestrator.php @@ -7,11 +7,12 @@ use App\Models\Tenant; use App\Services\Graph\AssignmentFetcher; use App\Services\Graph\GroupResolver; +use App\Services\Graph\ScopeTagResolver; use Illuminate\Support\Facades\Log; /** * Orchestrates policy capture with assignments and scope tags. - * + * * Ensures PolicyVersion is the source of truth, with BackupItem as restore copy. */ class PolicyCaptureOrchestrator @@ -21,17 +22,12 @@ public function __construct( private readonly PolicySnapshotService $snapshotService, private readonly AssignmentFetcher $assignmentFetcher, private readonly GroupResolver $groupResolver, + private readonly ScopeTagResolver $scopeTagResolver, ) {} /** * Capture policy snapshot with optional assignments/scope tags. - * - * @param Policy $policy - * @param Tenant $tenant - * @param bool $includeAssignments - * @param bool $includeScopeTags - * @param string|null $createdBy - * @param array $metadata + * * @return array ['version' => PolicyVersion, 'captured' => array] */ public function capture( @@ -42,6 +38,9 @@ public function capture( ?string $createdBy = null, array $metadata = [] ): array { + $graphOptions = $tenant->graphOptions(); + $tenantIdentifier = $graphOptions['tenant'] ?? $tenant->graphTenantId() ?? (string) $tenant->getKey(); + // 1. Fetch policy snapshot $snapshot = $this->snapshotService->fetch($tenant, $policy, $createdBy); @@ -57,9 +56,9 @@ public function capture( // 2. Fetch assignments if requested if ($includeAssignments) { try { - $rawAssignments = $this->assignmentFetcher->fetch($tenant->id, $policy->external_id); + $rawAssignments = $this->assignmentFetcher->fetch($tenantIdentifier, $policy->external_id, $graphOptions); - if (!empty($rawAssignments)) { + if (! empty($rawAssignments)) { $assignments = $rawAssignments; // Resolve groups for orphaned detection @@ -70,9 +69,10 @@ public function capture( ->values() ->toArray(); - if (!empty($groupIds)) { - $resolvedGroups = $this->groupResolver->resolve($tenant->id, $groupIds); - $captureMetadata['has_orphaned_assignments'] = !empty($resolvedGroups['orphaned']); + if (! empty($groupIds)) { + $resolvedGroups = $this->groupResolver->resolveGroupIds($groupIds, $tenantIdentifier, $graphOptions); + $captureMetadata['has_orphaned_assignments'] = collect($resolvedGroups) + ->contains(fn (array $group) => $group['orphaned'] ?? false); } $captureMetadata['assignments_count'] = count($rawAssignments); @@ -80,7 +80,7 @@ public function capture( } catch (\Throwable $e) { $captureMetadata['assignments_fetch_failed'] = true; $captureMetadata['assignments_fetch_error'] = $e->getMessage(); - + Log::warning('Failed to fetch assignments during capture', [ 'tenant_id' => $tenant->id, 'policy_id' => $policy->id, @@ -91,15 +91,13 @@ public function capture( // 3. Fetch scope tags if requested if ($includeScopeTags) { - $scopeTags = [ - 'ids' => $payload['roleScopeTagIds'] ?? ['0'], - 'names' => ['Default'], // Could fetch from Graph if needed - ]; + $scopeTagIds = $payload['roleScopeTagIds'] ?? ['0']; + $scopeTags = $this->resolveScopeTags($tenant, $scopeTagIds); } // 4. Check if PolicyVersion with same snapshot already exists $snapshotHash = hash('sha256', json_encode($payload)); - + // Find existing version by comparing snapshot content (database-agnostic) $existingVersion = PolicyVersion::where('policy_id', $policy->id) ->get() @@ -107,35 +105,42 @@ public function capture( return hash('sha256', json_encode($version->snapshot)) === $snapshotHash; }); - if ($existingVersion && $includeAssignments && is_null($existingVersion->assignments)) { - // Backfill existing version with assignments (idempotent) - $existingVersion->update([ - 'assignments' => $assignments, - 'scope_tags' => $scopeTags, - 'assignments_hash' => $assignments ? hash('sha256', json_encode($assignments)) : null, - 'scope_tags_hash' => $scopeTags ? hash('sha256', json_encode($scopeTags)) : null, - ]); - - Log::info('Backfilled existing PolicyVersion with assignments', [ - 'tenant_id' => $tenant->id, - 'policy_id' => $policy->id, - 'version_id' => $existingVersion->id, - 'version_number' => $existingVersion->version_number, - ]); - - return [ - 'version' => $existingVersion->fresh(), - 'captured' => [ - 'payload' => $payload, - 'assignments' => $assignments, - 'scope_tags' => $scopeTags, - 'metadata' => $captureMetadata, - ], - ]; - } - if ($existingVersion) { - // Reuse existing version without modification + $updates = []; + + if ($includeAssignments && $existingVersion->assignments === null) { + $updates['assignments'] = $assignments; + $updates['assignments_hash'] = $assignments ? hash('sha256', json_encode($assignments)) : null; + } + + if ($includeScopeTags && $existingVersion->scope_tags === null) { + $updates['scope_tags'] = $scopeTags; + $updates['scope_tags_hash'] = $scopeTags ? hash('sha256', json_encode($scopeTags)) : null; + } + + if (! empty($updates)) { + $existingVersion->update($updates); + + Log::info('Backfilled existing PolicyVersion with capture data', [ + 'tenant_id' => $tenant->id, + 'policy_id' => $policy->id, + 'version_id' => $existingVersion->id, + 'version_number' => $existingVersion->version_number, + 'assignments_backfilled' => array_key_exists('assignments', $updates), + 'scope_tags_backfilled' => array_key_exists('scope_tags', $updates), + ]); + + return [ + 'version' => $existingVersion->fresh(), + 'captured' => [ + 'payload' => $payload, + 'assignments' => $assignments, + 'scope_tags' => $scopeTags, + 'metadata' => $captureMetadata, + ], + ]; + } + Log::info('Reusing existing PolicyVersion', [ 'tenant_id' => $tenant->id, 'policy_id' => $policy->id, @@ -175,8 +180,8 @@ public function capture( 'policy_id' => $policy->id, 'version_id' => $version->id, 'version_number' => $version->version_number, - 'has_assignments' => !is_null($assignments), - 'has_scope_tags' => !is_null($scopeTags), + 'has_assignments' => ! is_null($assignments), + 'has_scope_tags' => ! is_null($scopeTags), ]); return [ @@ -192,13 +197,6 @@ public function capture( /** * Ensure existing PolicyVersion has assignments if missing. - * - * @param PolicyVersion $version - * @param Tenant $tenant - * @param Policy $policy - * @param bool $includeAssignments - * @param bool $includeScopeTags - * @return PolicyVersion */ public function ensureVersionHasAssignments( PolicyVersion $version, @@ -207,17 +205,19 @@ public function ensureVersionHasAssignments( bool $includeAssignments = false, bool $includeScopeTags = false ): PolicyVersion { - // If version already has assignments, don't overwrite (idempotent) - if ($version->assignments !== null) { + $graphOptions = $tenant->graphOptions(); + $tenantIdentifier = $graphOptions['tenant'] ?? $tenant->graphTenantId() ?? (string) $tenant->getKey(); + + if ($version->assignments !== null && $version->scope_tags !== null) { Log::debug('Version already has assignments, skipping', [ 'version_id' => $version->id, ]); - + return $version; } // Only fetch if requested - if (!$includeAssignments && !$includeScopeTags) { + if (! $includeAssignments && ! $includeScopeTags) { return $version; } @@ -225,12 +225,11 @@ public function ensureVersionHasAssignments( $scopeTags = null; $metadata = $version->metadata ?? []; - // Fetch assignments - if ($includeAssignments) { + if ($includeAssignments && $version->assignments === null) { try { - $rawAssignments = $this->assignmentFetcher->fetch($tenant->id, $policy->external_id); + $rawAssignments = $this->assignmentFetcher->fetch($tenantIdentifier, $policy->external_id, $graphOptions); - if (!empty($rawAssignments)) { + if (! empty($rawAssignments)) { $assignments = $rawAssignments; // Resolve groups @@ -241,9 +240,10 @@ public function ensureVersionHasAssignments( ->values() ->toArray(); - if (!empty($groupIds)) { - $resolvedGroups = $this->groupResolver->resolve($tenant->id, $groupIds); - $metadata['has_orphaned_assignments'] = !empty($resolvedGroups['orphaned']); + if (! empty($groupIds)) { + $resolvedGroups = $this->groupResolver->resolveGroupIds($groupIds, $tenantIdentifier, $graphOptions); + $metadata['has_orphaned_assignments'] = collect($resolvedGroups) + ->contains(fn (array $group) => $group['orphaned'] ?? false); } $metadata['assignments_count'] = count($rawAssignments); @@ -251,7 +251,7 @@ public function ensureVersionHasAssignments( } catch (\Throwable $e) { $metadata['assignments_fetch_failed'] = true; $metadata['assignments_fetch_error'] = $e->getMessage(); - + Log::warning('Failed to backfill assignments for version', [ 'version_id' => $version->id, 'error' => $e->getMessage(), @@ -261,28 +261,53 @@ public function ensureVersionHasAssignments( // Fetch scope tags if ($includeScopeTags && $version->scope_tags === null) { - // Try to get from snapshot - $scopeTags = [ - 'ids' => $version->snapshot['roleScopeTagIds'] ?? ['0'], - 'names' => ['Default'], - ]; + $scopeTagIds = $version->snapshot['roleScopeTagIds'] ?? ['0']; + $scopeTags = $this->resolveScopeTags($tenant, $scopeTagIds); } - // Update version - $version->update([ - 'assignments' => $assignments, - 'scope_tags' => $scopeTags, - 'assignments_hash' => $assignments ? hash('sha256', json_encode($assignments)) : null, - 'scope_tags_hash' => $scopeTags ? hash('sha256', json_encode($scopeTags)) : null, - 'metadata' => $metadata, - ]); + $updates = []; - Log::info('Version backfilled with assignments', [ + if ($includeAssignments && $version->assignments === null) { + $updates['assignments'] = $assignments; + $updates['assignments_hash'] = $assignments ? hash('sha256', json_encode($assignments)) : null; + } + + if ($includeScopeTags && $version->scope_tags === null) { + $updates['scope_tags'] = $scopeTags; + $updates['scope_tags_hash'] = $scopeTags ? hash('sha256', json_encode($scopeTags)) : null; + } + + if (! empty($updates)) { + $updates['metadata'] = $metadata; + $version->update($updates); + } + + Log::info('Version backfilled with capture data', [ 'version_id' => $version->id, - 'has_assignments' => !is_null($assignments), - 'has_scope_tags' => !is_null($scopeTags), + 'has_assignments' => ! is_null($assignments), + 'has_scope_tags' => ! is_null($scopeTags), ]); return $version->refresh(); } + + /** + * @param array $scopeTagIds + * @return array{ids:array,names:array} + */ + private function resolveScopeTags(Tenant $tenant, array $scopeTagIds): array + { + $scopeTags = $this->scopeTagResolver->resolve($scopeTagIds, $tenant); + + $names = []; + foreach ($scopeTagIds as $id) { + $scopeTag = collect($scopeTags)->firstWhere('id', $id); + $names[] = $scopeTag['displayName'] ?? ($id === '0' ? 'Default' : "Unknown (ID: {$id})"); + } + + return [ + 'ids' => $scopeTagIds, + 'names' => $names, + ]; + } } diff --git a/app/Services/Intune/VersionService.php b/app/Services/Intune/VersionService.php index 6c4984e..1aad5bc 100644 --- a/app/Services/Intune/VersionService.php +++ b/app/Services/Intune/VersionService.php @@ -7,6 +7,7 @@ use App\Models\Tenant; use App\Services\Graph\AssignmentFetcher; use App\Services\Graph\GroupResolver; +use App\Services\Graph\ScopeTagResolver; use Carbon\CarbonImmutable; class VersionService @@ -16,6 +17,7 @@ public function __construct( private readonly PolicySnapshotService $snapshotService, private readonly AssignmentFetcher $assignmentFetcher, private readonly GroupResolver $groupResolver, + private readonly ScopeTagResolver $scopeTagResolver, ) {} public function captureVersion( @@ -69,6 +71,9 @@ public function captureFromGraph( bool $includeAssignments = true, bool $includeScopeTags = true, ): PolicyVersion { + $graphOptions = $tenant->graphOptions(); + $tenantIdentifier = $graphOptions['tenant'] ?? $tenant->graphTenantId() ?? (string) $tenant->getKey(); + $snapshot = $this->snapshotService->fetch($tenant, $policy, $createdBy); if (isset($snapshot['failure'])) { @@ -84,7 +89,7 @@ public function captureFromGraph( if ($includeAssignments) { try { - $rawAssignments = $this->assignmentFetcher->fetch($tenant->id, $policy->external_id); + $rawAssignments = $this->assignmentFetcher->fetch($tenantIdentifier, $policy->external_id, $graphOptions); if (! empty($rawAssignments)) { $assignments = $rawAssignments; @@ -97,9 +102,10 @@ public function captureFromGraph( ->values() ->toArray(); - $resolvedGroups = $this->groupResolver->resolve($tenant->id, $groupIds); + $resolvedGroups = $this->groupResolver->resolveGroupIds($groupIds, $tenantIdentifier, $graphOptions); - $assignmentMetadata['has_orphaned_assignments'] = ! empty($resolvedGroups['orphaned']); + $assignmentMetadata['has_orphaned_assignments'] = collect($resolvedGroups) + ->contains(fn (array $group) => $group['orphaned'] ?? false); $assignmentMetadata['assignments_count'] = count($rawAssignments); } } catch (\Throwable $e) { @@ -109,10 +115,8 @@ public function captureFromGraph( } if ($includeScopeTags) { - $scopeTags = [ - 'ids' => $payload['roleScopeTagIds'] ?? ['0'], - 'names' => ['Default'], // Could be fetched from Graph if needed - ]; + $scopeTagIds = $payload['roleScopeTagIds'] ?? ['0']; + $scopeTags = $this->resolveScopeTags($tenant, $scopeTagIds); } $metadata = array_merge( @@ -131,6 +135,26 @@ public function captureFromGraph( ); } + /** + * @param array $scopeTagIds + * @return array{ids:array,names:array} + */ + private function resolveScopeTags(Tenant $tenant, array $scopeTagIds): array + { + $scopeTags = $this->scopeTagResolver->resolve($scopeTagIds, $tenant); + + $names = []; + foreach ($scopeTagIds as $id) { + $scopeTag = collect($scopeTags)->firstWhere('id', $id); + $names[] = $scopeTag['displayName'] ?? ($id === '0' ? 'Default' : "Unknown (ID: {$id})"); + } + + return [ + 'ids' => $scopeTagIds, + 'names' => $names, + ]; + } + private function nextVersionNumber(Policy $policy): int { $current = PolicyVersion::query() diff --git a/tests/Feature/BackupWithAssignmentsConsistencyTest.php b/tests/Feature/BackupWithAssignmentsConsistencyTest.php index 37c6d2b..2a43ee6 100644 --- a/tests/Feature/BackupWithAssignmentsConsistencyTest.php +++ b/tests/Feature/BackupWithAssignmentsConsistencyTest.php @@ -5,9 +5,8 @@ use App\Models\PolicyVersion; use App\Models\Tenant; use App\Services\Graph\AssignmentFetcher; -use App\Services\Graph\MicrosoftGraphClient; +use App\Services\Graph\GroupResolver; use App\Services\Intune\BackupService; -use App\Services\Intune\GroupResolver; use App\Services\Intune\PolicySnapshotService; use Illuminate\Foundation\Testing\RefreshDatabase; use Mockery\MockInterface; @@ -16,7 +15,7 @@ beforeEach(function () { $this->tenant = Tenant::factory()->create(['status' => 'active']); - + $this->policy = Policy::factory()->create([ 'tenant_id' => $this->tenant->id, 'external_id' => 'test-policy-123', @@ -86,10 +85,13 @@ // Mock GroupResolver $this->mock(GroupResolver::class, function (MockInterface $mock) { - $mock->shouldReceive('resolve') + $mock->shouldReceive('resolveGroupIds') ->andReturn([ - 'resolved' => ['group-123' => 'Test Group'], - 'orphaned' => [], + 'group-123' => [ + 'id' => 'group-123', + 'displayName' => 'Test Group', + 'orphaned' => false, + ], ]); }); }); @@ -189,7 +191,7 @@ expect($backupSet->items)->toHaveCount(1); $backupItem = $backupSet->items->first(); - + // BackupItem should have assignments expect($backupItem->assignments)->not->toBeNull(); expect($backupItem->assignments)->toHaveCount(2); diff --git a/tests/Feature/Filament/PolicyCaptureSnapshotOptionsTest.php b/tests/Feature/Filament/PolicyCaptureSnapshotOptionsTest.php index abd8857..f547b1b 100644 --- a/tests/Feature/Filament/PolicyCaptureSnapshotOptionsTest.php +++ b/tests/Feature/Filament/PolicyCaptureSnapshotOptionsTest.php @@ -5,6 +5,7 @@ use App\Models\Tenant; use App\Models\User; use App\Services\Graph\AssignmentFetcher; +use App\Services\Graph\ScopeTagResolver; use App\Services\Intune\PolicySnapshotService; use Illuminate\Foundation\Testing\RefreshDatabase; use Livewire\Livewire; @@ -38,6 +39,14 @@ $mock->shouldReceive('fetch')->never(); }); + $this->mock(ScopeTagResolver::class, function (MockInterface $mock) { + $mock->shouldReceive('resolve') + ->once() + ->andReturn([ + ['id' => '0', 'displayName' => 'Default'], + ]); + }); + Livewire::test(ViewPolicy::class, ['record' => $policy->getRouteKey()]) ->callAction('capture_snapshot', data: [ 'include_assignments' => false, diff --git a/tests/Feature/VersionCaptureWithAssignmentsTest.php b/tests/Feature/VersionCaptureWithAssignmentsTest.php index 416e2a2..9b0ef51 100644 --- a/tests/Feature/VersionCaptureWithAssignmentsTest.php +++ b/tests/Feature/VersionCaptureWithAssignmentsTest.php @@ -4,6 +4,7 @@ use App\Models\Tenant; use App\Services\Graph\AssignmentFetcher; use App\Services\Graph\GroupResolver; +use App\Services\Graph\ScopeTagResolver; use App\Services\Intune\PolicySnapshotService; use App\Services\Intune\VersionService; @@ -13,6 +14,13 @@ 'tenant_id' => $this->tenant->id, 'external_id' => 'test-policy-id', ]); + + $this->mock(ScopeTagResolver::class, function ($mock) { + $mock->shouldReceive('resolve') + ->andReturn([ + ['id' => '0', 'displayName' => 'Default'], + ]); + }); }); it('captures policy version with assignments from graph', function () { @@ -45,13 +53,14 @@ }); $this->mock(GroupResolver::class, function ($mock) { - $mock->shouldReceive('resolve') + $mock->shouldReceive('resolveGroupIds') ->once() ->andReturn([ - 'resolved' => [ - 'group-123' => ['id' => 'group-123', 'displayName' => 'Test Group'], + 'group-123' => [ + 'id' => 'group-123', + 'displayName' => 'Test Group', + 'orphaned' => false, ], - 'orphaned' => [], ]); }); @@ -68,7 +77,11 @@ ->and($version->assignments[0]['target']['groupId'])->toBe('group-123') ->and($version->assignments_hash)->not->toBeNull() ->and($version->metadata['assignments_count'])->toBe(1) - ->and($version->metadata['has_orphaned_assignments'])->toBeFalse(); + ->and($version->metadata['has_orphaned_assignments'])->toBeFalse() + ->and($version->scope_tags)->toBe([ + 'ids' => ['0'], + 'names' => ['Default'], + ]); }); it('captures policy version without assignments when none exist', function () {