graphClient = Mockery::mock(MicrosoftGraphClient::class); $this->logger = Mockery::mock(GraphLogger::class); $this->resolver = new GroupResolver($this->graphClient, $this->logger); }); test('resolves all groups', function () { $tenantId = 'tenant-123'; $groupIds = ['group-1', 'group-2', 'group-3']; $graphResponse = [ 'value' => [ ['id' => 'group-1', 'displayName' => 'All Users'], ['id' => 'group-2', 'displayName' => 'HR Team'], ['id' => 'group-3', 'displayName' => 'Contractors'], ], ]; $this->graphClient ->shouldReceive('post') ->once() ->with('/directoryObjects/getByIds', [ 'ids' => $groupIds, 'types' => ['group'], ], $tenantId) ->andReturn($graphResponse); $this->logger ->shouldReceive('logDebug') ->once(); $result = $this->resolver->resolveGroupIds($groupIds, $tenantId); expect($result)->toHaveKey('group-1') ->and($result['group-1'])->toBe([ 'id' => 'group-1', 'displayName' => 'All Users', 'orphaned' => false, ]) ->and($result)->toHaveKey('group-2') ->and($result['group-2']['orphaned'])->toBeFalse() ->and($result)->toHaveKey('group-3') ->and($result['group-3']['orphaned'])->toBeFalse(); }); test('handles orphaned ids', function () { $tenantId = 'tenant-123'; $groupIds = ['group-1', 'group-2', 'group-3']; $graphResponse = [ 'value' => [ ['id' => 'group-1', 'displayName' => 'All Users'], // group-2 and group-3 are missing (deleted) ], ]; $this->graphClient ->shouldReceive('post') ->once() ->andReturn($graphResponse); $this->logger ->shouldReceive('logDebug') ->once() ->with('Resolved group IDs', Mockery::on(function ($context) { return $context['requested'] === 3 && $context['resolved'] === 1 && $context['orphaned'] === 2; })); $result = $this->resolver->resolveGroupIds($groupIds, $tenantId); expect($result)->toHaveKey('group-1') ->and($result['group-1']['orphaned'])->toBeFalse() ->and($result)->toHaveKey('group-2') ->and($result['group-2'])->toBe([ 'id' => 'group-2', 'displayName' => null, 'orphaned' => true, ]) ->and($result)->toHaveKey('group-3') ->and($result['group-3']['orphaned'])->toBeTrue(); }); test('caches results', function () { $tenantId = 'tenant-123'; $groupIds = ['group-1', 'group-2']; $graphResponse = [ 'value' => [ ['id' => 'group-1', 'displayName' => 'All Users'], ['id' => 'group-2', 'displayName' => 'HR Team'], ], ]; // First call - should hit Graph API $this->graphClient ->shouldReceive('post') ->once() ->andReturn($graphResponse); $this->logger ->shouldReceive('logDebug') ->once(); $result1 = $this->resolver->resolveGroupIds($groupIds, $tenantId); // Second call - should use cache (no Graph API call) $result2 = $this->resolver->resolveGroupIds($groupIds, $tenantId); expect($result1)->toBe($result2) ->and($result1)->toHaveCount(2); }); test('returns empty array for empty input', function () { $result = $this->resolver->resolveGroupIds([], 'tenant-123'); expect($result)->toBe([]); }); test('handles graph exception gracefully', function () { $tenantId = 'tenant-123'; $groupIds = ['group-1', 'group-2']; $this->graphClient ->shouldReceive('post') ->once() ->andThrow(new GraphException('Graph API error', 500, ['request_id' => 'request-id-123'])); $this->logger ->shouldReceive('logWarning') ->once() ->with('Failed to resolve group IDs', Mockery::on(function ($context) use ($groupIds) { return $context['group_ids'] === $groupIds && isset($context['context']['request_id']); })); $result = $this->resolver->resolveGroupIds($groupIds, $tenantId); // All groups should be marked as orphaned on failure expect($result)->toHaveKey('group-1') ->and($result['group-1']['orphaned'])->toBeTrue() ->and($result['group-1']['displayName'])->toBeNull() ->and($result)->toHaveKey('group-2') ->and($result['group-2']['orphaned'])->toBeTrue(); }); test('cache key is consistent regardless of array order', function () { $tenantId = 'tenant-123'; $groupIds1 = ['group-1', 'group-2', 'group-3']; $groupIds2 = ['group-3', 'group-1', 'group-2']; // Different order $graphResponse = [ 'value' => [ ['id' => 'group-1', 'displayName' => 'All Users'], ['id' => 'group-2', 'displayName' => 'HR Team'], ['id' => 'group-3', 'displayName' => 'Contractors'], ], ]; // First call with groupIds1 $this->graphClient ->shouldReceive('post') ->once() ->andReturn($graphResponse); $this->logger ->shouldReceive('logDebug') ->once(); $result1 = $this->resolver->resolveGroupIds($groupIds1, $tenantId); // Second call with groupIds2 (different order) - should use cache $result2 = $this->resolver->resolveGroupIds($groupIds2, $tenantId); expect($result1)->toBe($result2); });