tenant = Tenant::factory()->create(); $this->policy = Policy::factory()->create([ 'tenant_id' => $this->tenant->id, 'external_id' => 'test-policy-id', ]); }); it('captures policy version with assignments from graph', function () { // Mock dependencies $this->mock(PolicySnapshotService::class, function ($mock) { $mock->shouldReceive('fetch') ->once() ->andReturn([ 'payload' => [ 'id' => 'test-policy-id', 'name' => 'Test Policy', 'settings' => [], ], ]); }); $this->mock(AssignmentFetcher::class, function ($mock) { $mock->shouldReceive('fetch') ->once() ->andReturn([ [ 'id' => 'assignment-1', 'intent' => 'apply', 'target' => [ '@odata.type' => '#microsoft.graph.groupAssignmentTarget', 'groupId' => 'group-123', ], ], ]); }); $this->mock(GroupResolver::class, function ($mock) { $mock->shouldReceive('resolve') ->once() ->andReturn([ 'resolved' => [ 'group-123' => ['id' => 'group-123', 'displayName' => 'Test Group'], ], 'orphaned' => [], ]); }); $versionService = app(VersionService::class); $version = $versionService->captureFromGraph( $this->tenant, $this->policy, 'test@example.com' ); expect($version)->not->toBeNull() ->and($version->assignments)->not->toBeNull() ->and($version->assignments)->toHaveCount(1) ->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(); }); it('captures policy version without assignments when none exist', function () { // Mock dependencies $this->mock(PolicySnapshotService::class, function ($mock) { $mock->shouldReceive('fetch') ->once() ->andReturn([ 'payload' => [ 'id' => 'test-policy-id', 'name' => 'Test Policy', 'settings' => [], ], ]); }); $this->mock(AssignmentFetcher::class, function ($mock) { $mock->shouldReceive('fetch') ->once() ->andReturn([]); }); $versionService = app(VersionService::class); $version = $versionService->captureFromGraph( $this->tenant, $this->policy, 'test@example.com' ); expect($version)->not->toBeNull() ->and($version->assignments)->toBeNull() ->and($version->assignments_hash)->toBeNull(); }); it('handles assignment fetch failure gracefully', function () { // Mock dependencies $this->mock(PolicySnapshotService::class, function ($mock) { $mock->shouldReceive('fetch') ->once() ->andReturn([ 'payload' => [ 'id' => 'test-policy-id', 'name' => 'Test Policy', 'settings' => [], ], ]); }); $this->mock(AssignmentFetcher::class, function ($mock) { $mock->shouldReceive('fetch') ->once() ->andThrow(new \Exception('Graph API error')); }); $versionService = app(VersionService::class); $version = $versionService->captureFromGraph( $this->tenant, $this->policy, 'test@example.com' ); expect($version)->not->toBeNull() ->and($version->assignments)->toBeNull() ->and($version->metadata['assignments_fetch_failed'])->toBeTrue() ->and($version->metadata['assignments_fetch_error'])->toBe('Graph API error'); }); it('calculates correct hash for assignments', function () { $assignments = [ ['id' => '1', 'target' => ['groupId' => 'group-1']], ['id' => '2', 'target' => ['groupId' => 'group-2']], ]; $version = $this->policy->versions()->create([ 'tenant_id' => $this->tenant->id, 'policy_id' => $this->policy->id, 'version_number' => 1, 'policy_type' => 'deviceManagementConfigurationPolicy', 'snapshot' => ['test' => 'data'], 'assignments' => $assignments, 'assignments_hash' => hash('sha256', json_encode($assignments)), 'captured_at' => now(), ]); $expectedHash = hash('sha256', json_encode($assignments)); expect($version->assignments_hash)->toBe($expectedHash); // Verify same assignments produce same hash $version2 = $this->policy->versions()->create([ 'tenant_id' => $this->tenant->id, 'policy_id' => $this->policy->id, 'version_number' => 2, 'policy_type' => 'deviceManagementConfigurationPolicy', 'snapshot' => ['test' => 'data'], 'assignments' => $assignments, 'assignments_hash' => hash('sha256', json_encode($assignments)), 'captured_at' => now(), ]); expect($version2->assignments_hash)->toBe($version->assignments_hash); });