feat/004-assignments-scope-tags #4

Merged
ahmido merged 41 commits from feat/004-assignments-scope-tags into dev 2025-12-23 21:49:59 +00:00
8 changed files with 196 additions and 123 deletions
Showing only changes of commit e21643c46b - Show all commits

View File

@ -54,8 +54,9 @@ public function enrichWithAssignments(
} }
// Fetch assignments from Graph API // Fetch assignments from Graph API
$tenantId = $tenant->external_id ?? $tenant->tenant_id; $graphOptions = $tenant->graphOptions();
$assignments = $this->assignmentFetcher->fetch($tenantId, $policyId); $tenantId = $graphOptions['tenant'] ?? $tenant->external_id ?? $tenant->tenant_id;
$assignments = $this->assignmentFetcher->fetch($tenantId, $policyId, $graphOptions);
if (empty($assignments)) { if (empty($assignments)) {
// No assignments or fetch failed // No assignments or fetch failed
@ -82,7 +83,7 @@ public function enrichWithAssignments(
$hasOrphanedGroups = false; $hasOrphanedGroups = false;
if (! empty($groupIds)) { if (! empty($groupIds)) {
$resolvedGroups = $this->groupResolver->resolveGroupIds($groupIds, $tenantId); $resolvedGroups = $this->groupResolver->resolveGroupIds($groupIds, $tenantId, $graphOptions);
$hasOrphanedGroups = collect($resolvedGroups)->contains('orphaned', true); $hasOrphanedGroups = collect($resolvedGroups)->contains('orphaned', true);
} }

View File

@ -18,11 +18,13 @@ public function __construct(
* *
* @return array Returns assignment array or empty array on failure * @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 { try {
$requestOptions = array_merge($options, ['tenant' => $tenantId]);
// Try primary endpoint // Try primary endpoint
$assignments = $this->fetchPrimary($tenantId, $policyId); $assignments = $this->fetchPrimary($policyId, $requestOptions);
if (! empty($assignments)) { if (! empty($assignments)) {
Log::debug('Fetched assignments via primary endpoint', [ Log::debug('Fetched assignments via primary endpoint', [
@ -40,7 +42,7 @@ public function fetch(string $tenantId, string $policyId): array
'policy_id' => $policyId, 'policy_id' => $policyId,
]); ]);
$assignments = $this->fetchWithExpand($tenantId, $policyId); $assignments = $this->fetchWithExpand($policyId, $requestOptions);
if (! empty($assignments)) { if (! empty($assignments)) {
Log::debug('Fetched assignments via fallback endpoint', [ Log::debug('Fetched assignments via fallback endpoint', [
@ -74,13 +76,11 @@ public function fetch(string $tenantId, string $policyId): array
/** /**
* Fetch assignments using primary endpoint. * 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"; $path = "/deviceManagement/configurationPolicies/{$policyId}/assignments";
$response = $this->graphClient->request('GET', $path, [ $response = $this->graphClient->request('GET', $path, $options);
'tenant' => $tenantId,
]);
return $response->data['value'] ?? []; return $response->data['value'] ?? [];
} }
@ -88,7 +88,7 @@ private function fetchPrimary(string $tenantId, string $policyId): array
/** /**
* Fetch assignments using $expand fallback. * Fetch assignments using $expand fallback.
*/ */
private function fetchWithExpand(string $tenantId, string $policyId): array private function fetchWithExpand(string $policyId, array $options): array
{ {
$path = '/deviceManagement/configurationPolicies'; $path = '/deviceManagement/configurationPolicies';
$params = [ $params = [
@ -96,10 +96,9 @@ private function fetchWithExpand(string $tenantId, string $policyId): array
'$filter' => "id eq '{$policyId}'", '$filter' => "id eq '{$policyId}'",
]; ];
$response = $this->graphClient->request('GET', $path, [ $response = $this->graphClient->request('GET', $path, array_merge($options, [
'tenant' => $tenantId,
'query' => $params, 'query' => $params,
]); ]));
$policies = $response->data['value'] ?? []; $policies = $response->data['value'] ?? [];

View File

@ -21,7 +21,7 @@ public function __construct(
* @param string $tenantId Target tenant ID * @param string $tenantId Target tenant ID
* @return array Keyed array: ['group-id' => ['id' => ..., 'displayName' => ..., 'orphaned' => bool]] * @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)) { if (empty($groupIds)) {
return []; return [];
@ -30,27 +30,27 @@ public function resolveGroupIds(array $groupIds, string $tenantId): array
// Create cache key // Create cache key
$cacheKey = $this->getCacheKey($groupIds, $tenantId); $cacheKey = $this->getCacheKey($groupIds, $tenantId);
return Cache::remember($cacheKey, 300, function () use ($groupIds, $tenantId) { return Cache::remember($cacheKey, 300, function () use ($groupIds, $tenantId, $options) {
return $this->fetchAndResolveGroups($groupIds, $tenantId); return $this->fetchAndResolveGroups($groupIds, $tenantId, $options);
}); });
} }
/** /**
* Fetch groups from Graph API and resolve orphaned IDs. * 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 { try {
$response = $this->graphClient->request( $response = $this->graphClient->request(
'POST', 'POST',
'/directoryObjects/getByIds', '/directoryObjects/getByIds',
[ array_merge($options, [
'tenant' => $tenantId, 'tenant' => $tenantId,
'json' => [ 'json' => [
'ids' => array_values($groupIds), 'ids' => array_values($groupIds),
'types' => ['group'], 'types' => ['group'],
], ],
] ])
); );
$resolvedGroups = $response->data['value'] ?? []; $resolvedGroups = $response->data['value'] ?? [];

View File

@ -7,6 +7,7 @@
use App\Models\Tenant; use App\Models\Tenant;
use App\Services\Graph\AssignmentFetcher; use App\Services\Graph\AssignmentFetcher;
use App\Services\Graph\GroupResolver; use App\Services\Graph\GroupResolver;
use App\Services\Graph\ScopeTagResolver;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
/** /**
@ -21,17 +22,12 @@ public function __construct(
private readonly PolicySnapshotService $snapshotService, private readonly PolicySnapshotService $snapshotService,
private readonly AssignmentFetcher $assignmentFetcher, private readonly AssignmentFetcher $assignmentFetcher,
private readonly GroupResolver $groupResolver, private readonly GroupResolver $groupResolver,
private readonly ScopeTagResolver $scopeTagResolver,
) {} ) {}
/** /**
* Capture policy snapshot with optional assignments/scope tags. * 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] * @return array ['version' => PolicyVersion, 'captured' => array]
*/ */
public function capture( public function capture(
@ -42,6 +38,9 @@ public function capture(
?string $createdBy = null, ?string $createdBy = null,
array $metadata = [] array $metadata = []
): array { ): array {
$graphOptions = $tenant->graphOptions();
$tenantIdentifier = $graphOptions['tenant'] ?? $tenant->graphTenantId() ?? (string) $tenant->getKey();
// 1. Fetch policy snapshot // 1. Fetch policy snapshot
$snapshot = $this->snapshotService->fetch($tenant, $policy, $createdBy); $snapshot = $this->snapshotService->fetch($tenant, $policy, $createdBy);
@ -57,9 +56,9 @@ public function capture(
// 2. Fetch assignments if requested // 2. Fetch assignments if requested
if ($includeAssignments) { if ($includeAssignments) {
try { 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; $assignments = $rawAssignments;
// Resolve groups for orphaned detection // Resolve groups for orphaned detection
@ -70,9 +69,10 @@ public function capture(
->values() ->values()
->toArray(); ->toArray();
if (!empty($groupIds)) { if (! empty($groupIds)) {
$resolvedGroups = $this->groupResolver->resolve($tenant->id, $groupIds); $resolvedGroups = $this->groupResolver->resolveGroupIds($groupIds, $tenantIdentifier, $graphOptions);
$captureMetadata['has_orphaned_assignments'] = !empty($resolvedGroups['orphaned']); $captureMetadata['has_orphaned_assignments'] = collect($resolvedGroups)
->contains(fn (array $group) => $group['orphaned'] ?? false);
} }
$captureMetadata['assignments_count'] = count($rawAssignments); $captureMetadata['assignments_count'] = count($rawAssignments);
@ -91,10 +91,8 @@ public function capture(
// 3. Fetch scope tags if requested // 3. Fetch scope tags if requested
if ($includeScopeTags) { if ($includeScopeTags) {
$scopeTags = [ $scopeTagIds = $payload['roleScopeTagIds'] ?? ['0'];
'ids' => $payload['roleScopeTagIds'] ?? ['0'], $scopeTags = $this->resolveScopeTags($tenant, $scopeTagIds);
'names' => ['Default'], // Could fetch from Graph if needed
];
} }
// 4. Check if PolicyVersion with same snapshot already exists // 4. Check if PolicyVersion with same snapshot already exists
@ -107,35 +105,42 @@ public function capture(
return hash('sha256', json_encode($version->snapshot)) === $snapshotHash; 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) { 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', [ Log::info('Reusing existing PolicyVersion', [
'tenant_id' => $tenant->id, 'tenant_id' => $tenant->id,
'policy_id' => $policy->id, 'policy_id' => $policy->id,
@ -175,8 +180,8 @@ public function capture(
'policy_id' => $policy->id, 'policy_id' => $policy->id,
'version_id' => $version->id, 'version_id' => $version->id,
'version_number' => $version->version_number, 'version_number' => $version->version_number,
'has_assignments' => !is_null($assignments), 'has_assignments' => ! is_null($assignments),
'has_scope_tags' => !is_null($scopeTags), 'has_scope_tags' => ! is_null($scopeTags),
]); ]);
return [ return [
@ -192,13 +197,6 @@ public function capture(
/** /**
* Ensure existing PolicyVersion has assignments if missing. * 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( public function ensureVersionHasAssignments(
PolicyVersion $version, PolicyVersion $version,
@ -207,8 +205,10 @@ public function ensureVersionHasAssignments(
bool $includeAssignments = false, bool $includeAssignments = false,
bool $includeScopeTags = false bool $includeScopeTags = false
): PolicyVersion { ): PolicyVersion {
// If version already has assignments, don't overwrite (idempotent) $graphOptions = $tenant->graphOptions();
if ($version->assignments !== null) { $tenantIdentifier = $graphOptions['tenant'] ?? $tenant->graphTenantId() ?? (string) $tenant->getKey();
if ($version->assignments !== null && $version->scope_tags !== null) {
Log::debug('Version already has assignments, skipping', [ Log::debug('Version already has assignments, skipping', [
'version_id' => $version->id, 'version_id' => $version->id,
]); ]);
@ -217,7 +217,7 @@ public function ensureVersionHasAssignments(
} }
// Only fetch if requested // Only fetch if requested
if (!$includeAssignments && !$includeScopeTags) { if (! $includeAssignments && ! $includeScopeTags) {
return $version; return $version;
} }
@ -225,12 +225,11 @@ public function ensureVersionHasAssignments(
$scopeTags = null; $scopeTags = null;
$metadata = $version->metadata ?? []; $metadata = $version->metadata ?? [];
// Fetch assignments if ($includeAssignments && $version->assignments === null) {
if ($includeAssignments) {
try { 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; $assignments = $rawAssignments;
// Resolve groups // Resolve groups
@ -241,9 +240,10 @@ public function ensureVersionHasAssignments(
->values() ->values()
->toArray(); ->toArray();
if (!empty($groupIds)) { if (! empty($groupIds)) {
$resolvedGroups = $this->groupResolver->resolve($tenant->id, $groupIds); $resolvedGroups = $this->groupResolver->resolveGroupIds($groupIds, $tenantIdentifier, $graphOptions);
$metadata['has_orphaned_assignments'] = !empty($resolvedGroups['orphaned']); $metadata['has_orphaned_assignments'] = collect($resolvedGroups)
->contains(fn (array $group) => $group['orphaned'] ?? false);
} }
$metadata['assignments_count'] = count($rawAssignments); $metadata['assignments_count'] = count($rawAssignments);
@ -261,28 +261,53 @@ public function ensureVersionHasAssignments(
// Fetch scope tags // Fetch scope tags
if ($includeScopeTags && $version->scope_tags === null) { if ($includeScopeTags && $version->scope_tags === null) {
// Try to get from snapshot $scopeTagIds = $version->snapshot['roleScopeTagIds'] ?? ['0'];
$scopeTags = [ $scopeTags = $this->resolveScopeTags($tenant, $scopeTagIds);
'ids' => $version->snapshot['roleScopeTagIds'] ?? ['0'],
'names' => ['Default'],
];
} }
// Update version $updates = [];
$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,
]);
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, 'version_id' => $version->id,
'has_assignments' => !is_null($assignments), 'has_assignments' => ! is_null($assignments),
'has_scope_tags' => !is_null($scopeTags), 'has_scope_tags' => ! is_null($scopeTags),
]); ]);
return $version->refresh(); return $version->refresh();
} }
/**
* @param array<int, string> $scopeTagIds
* @return array{ids:array<int, string>,names:array<int, string>}
*/
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,
];
}
} }

View File

@ -7,6 +7,7 @@
use App\Models\Tenant; use App\Models\Tenant;
use App\Services\Graph\AssignmentFetcher; use App\Services\Graph\AssignmentFetcher;
use App\Services\Graph\GroupResolver; use App\Services\Graph\GroupResolver;
use App\Services\Graph\ScopeTagResolver;
use Carbon\CarbonImmutable; use Carbon\CarbonImmutable;
class VersionService class VersionService
@ -16,6 +17,7 @@ public function __construct(
private readonly PolicySnapshotService $snapshotService, private readonly PolicySnapshotService $snapshotService,
private readonly AssignmentFetcher $assignmentFetcher, private readonly AssignmentFetcher $assignmentFetcher,
private readonly GroupResolver $groupResolver, private readonly GroupResolver $groupResolver,
private readonly ScopeTagResolver $scopeTagResolver,
) {} ) {}
public function captureVersion( public function captureVersion(
@ -69,6 +71,9 @@ public function captureFromGraph(
bool $includeAssignments = true, bool $includeAssignments = true,
bool $includeScopeTags = true, bool $includeScopeTags = true,
): PolicyVersion { ): PolicyVersion {
$graphOptions = $tenant->graphOptions();
$tenantIdentifier = $graphOptions['tenant'] ?? $tenant->graphTenantId() ?? (string) $tenant->getKey();
$snapshot = $this->snapshotService->fetch($tenant, $policy, $createdBy); $snapshot = $this->snapshotService->fetch($tenant, $policy, $createdBy);
if (isset($snapshot['failure'])) { if (isset($snapshot['failure'])) {
@ -84,7 +89,7 @@ public function captureFromGraph(
if ($includeAssignments) { if ($includeAssignments) {
try { 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; $assignments = $rawAssignments;
@ -97,9 +102,10 @@ public function captureFromGraph(
->values() ->values()
->toArray(); ->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); $assignmentMetadata['assignments_count'] = count($rawAssignments);
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
@ -109,10 +115,8 @@ public function captureFromGraph(
} }
if ($includeScopeTags) { if ($includeScopeTags) {
$scopeTags = [ $scopeTagIds = $payload['roleScopeTagIds'] ?? ['0'];
'ids' => $payload['roleScopeTagIds'] ?? ['0'], $scopeTags = $this->resolveScopeTags($tenant, $scopeTagIds);
'names' => ['Default'], // Could be fetched from Graph if needed
];
} }
$metadata = array_merge( $metadata = array_merge(
@ -131,6 +135,26 @@ public function captureFromGraph(
); );
} }
/**
* @param array<int, string> $scopeTagIds
* @return array{ids:array<int, string>,names:array<int, string>}
*/
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 private function nextVersionNumber(Policy $policy): int
{ {
$current = PolicyVersion::query() $current = PolicyVersion::query()

View File

@ -5,9 +5,8 @@
use App\Models\PolicyVersion; use App\Models\PolicyVersion;
use App\Models\Tenant; use App\Models\Tenant;
use App\Services\Graph\AssignmentFetcher; use App\Services\Graph\AssignmentFetcher;
use App\Services\Graph\MicrosoftGraphClient; use App\Services\Graph\GroupResolver;
use App\Services\Intune\BackupService; use App\Services\Intune\BackupService;
use App\Services\Intune\GroupResolver;
use App\Services\Intune\PolicySnapshotService; use App\Services\Intune\PolicySnapshotService;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Mockery\MockInterface; use Mockery\MockInterface;
@ -86,10 +85,13 @@
// Mock GroupResolver // Mock GroupResolver
$this->mock(GroupResolver::class, function (MockInterface $mock) { $this->mock(GroupResolver::class, function (MockInterface $mock) {
$mock->shouldReceive('resolve') $mock->shouldReceive('resolveGroupIds')
->andReturn([ ->andReturn([
'resolved' => ['group-123' => 'Test Group'], 'group-123' => [
'orphaned' => [], 'id' => 'group-123',
'displayName' => 'Test Group',
'orphaned' => false,
],
]); ]);
}); });
}); });

View File

@ -5,6 +5,7 @@
use App\Models\Tenant; use App\Models\Tenant;
use App\Models\User; use App\Models\User;
use App\Services\Graph\AssignmentFetcher; use App\Services\Graph\AssignmentFetcher;
use App\Services\Graph\ScopeTagResolver;
use App\Services\Intune\PolicySnapshotService; use App\Services\Intune\PolicySnapshotService;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire; use Livewire\Livewire;
@ -38,6 +39,14 @@
$mock->shouldReceive('fetch')->never(); $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()]) Livewire::test(ViewPolicy::class, ['record' => $policy->getRouteKey()])
->callAction('capture_snapshot', data: [ ->callAction('capture_snapshot', data: [
'include_assignments' => false, 'include_assignments' => false,

View File

@ -4,6 +4,7 @@
use App\Models\Tenant; use App\Models\Tenant;
use App\Services\Graph\AssignmentFetcher; use App\Services\Graph\AssignmentFetcher;
use App\Services\Graph\GroupResolver; use App\Services\Graph\GroupResolver;
use App\Services\Graph\ScopeTagResolver;
use App\Services\Intune\PolicySnapshotService; use App\Services\Intune\PolicySnapshotService;
use App\Services\Intune\VersionService; use App\Services\Intune\VersionService;
@ -13,6 +14,13 @@
'tenant_id' => $this->tenant->id, 'tenant_id' => $this->tenant->id,
'external_id' => 'test-policy-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 () { it('captures policy version with assignments from graph', function () {
@ -45,13 +53,14 @@
}); });
$this->mock(GroupResolver::class, function ($mock) { $this->mock(GroupResolver::class, function ($mock) {
$mock->shouldReceive('resolve') $mock->shouldReceive('resolveGroupIds')
->once() ->once()
->andReturn([ ->andReturn([
'resolved' => [ 'group-123' => [
'group-123' => ['id' => 'group-123', 'displayName' => 'Test Group'], '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[0]['target']['groupId'])->toBe('group-123')
->and($version->assignments_hash)->not->toBeNull() ->and($version->assignments_hash)->not->toBeNull()
->and($version->metadata['assignments_count'])->toBe(1) ->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 () { it('captures policy version without assignments when none exist', function () {