graphClient = Mockery::mock(MicrosoftGraphClient::class); $this->resolver = new GroupResolver($this->graphClient); }); test('resolves all groups', function () { $tenantId = 'tenant-123'; $groupIds = ['group-1', 'group-2', 'group-3']; $graphData = [ 'value' => [ ['id' => 'group-1', 'displayName' => 'All Users'], ['id' => 'group-2', 'displayName' => 'HR Team'], ['id' => 'group-3', 'displayName' => 'Contractors'], ], ]; $response = new GraphResponse( success: true, data: $graphData ); $this->graphClient ->shouldReceive('request') ->once() ->with('POST', '/directoryObjects/getByIds', [ 'tenant' => $tenantId, 'json' => [ 'ids' => $groupIds, 'types' => ['group'], ], ]) ->andReturn($response); $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']; $graphData = [ 'value' => [ ['id' => 'group-1', 'displayName' => 'All Users'], // group-2 and group-3 are missing (deleted) ], ]; $response = new GraphResponse( success: true, data: $graphData ); $this->graphClient ->shouldReceive('request') ->once() ->andReturn($response); $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']; $graphData = [ 'value' => [ ['id' => 'group-1', 'displayName' => 'All Users'], ['id' => 'group-2', 'displayName' => 'HR Team'], ], ]; $response = new GraphResponse( success: true, data: $graphData ); // First call - should hit Graph API $this->graphClient ->shouldReceive('request') ->once() ->andReturn($response); $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('request') ->once() ->andThrow(new GraphException('Graph API error', 500, ['request_id' => 'request-id-123'])); $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 $graphData = [ 'value' => [ ['id' => 'group-1', 'displayName' => 'All Users'], ['id' => 'group-2', 'displayName' => 'HR Team'], ['id' => 'group-3', 'displayName' => 'Contractors'], ], ]; $response = new GraphResponse( success: true, data: $graphData ); // First call with groupIds1 $this->graphClient ->shouldReceive('request') ->once() ->andReturn($response); $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); });