modules/Azure/Discovery/Tests/Unit/InvokeCIEMEntraRelationshipCollection.Tests.ps1

BeforeAll {
    . (Join-Path $PSScriptRoot 'TestSetup.ps1')
    Remove-Module Devolutions.CIEM -Force -ErrorAction SilentlyContinue
    Import-Module (Join-Path $PSScriptRoot '..' '..' '..' '..' '..' 'Devolutions.CIEM.psd1')
    $script:LegacyRelationshipFixture = Get-Content (Join-Path $PSScriptRoot '..' 'Fixtures' 'legacy-relationship-output.json') -Raw | ConvertFrom-Json
}

Describe 'InvokeCIEMEntraRelationshipCollection' {
    BeforeEach {
        Initialize-DiscoveryTestDatabase
        $script:relationshipBatchCallSizes = @()
        Mock -ModuleName Devolutions.CIEM Write-CIEMLog {}
        Mock -ModuleName Devolutions.CIEM Write-Progress {}
    }

    It 'Uses Graph batch requests for group memberships, owners, and directory role members' {
        $groups = foreach ($index in 1..60) {
            [pscustomobject]@{ Id = "group-$index"; DisplayName = "Group $index"; Type = 'group' }
        }
        $roles = @(
            [pscustomobject]@{ Id = 'role-1'; DisplayName = 'Role 1'; Type = 'directoryRole' }
            [pscustomobject]@{ Id = 'role-2'; DisplayName = 'Role 2'; Type = 'directoryRole' }
        )

        Mock -ModuleName Devolutions.CIEM Invoke-AzureApi {
            $script:relationshipBatchCallSizes += @($Requests).Count
            $results = @{}
            foreach ($request in @($Requests)) {
                $results[$request.Id] = [pscustomobject]@{
                    Success = $true
                    StatusCode = 200
                    Items = @()
                }
            }
            $results
        }

        $result = InModuleScope Devolutions.CIEM -Parameters @{ groups = $groups; roles = $roles } {
            @(InvokeCIEMEntraRelationshipCollection -Groups $groups -DirectoryRoles $roles -Users @())
        }

        $result | Should -HaveCount 0
        $script:relationshipBatchCallSizes | Should -HaveCount 2
        $script:relationshipBatchCallSizes[0] | Should -Be 120
        $script:relationshipBatchCallSizes[1] | Should -Be 2
    }

    It 'Builds direct and transitive membership relationships without per-user transitiveMemberOf calls' {
        $groups = @(
            [pscustomobject]@{ Id = 'group-a'; DisplayName = 'Group A'; Type = 'group' }
            [pscustomobject]@{ Id = 'group-b'; DisplayName = 'Group B'; Type = 'group' }
        )
        $users = @(
            [pscustomobject]@{ Id = 'user-1'; DisplayName = 'User 1'; Type = 'user' }
        )
        $script:relationshipRequestPaths = @()

        Mock -ModuleName Devolutions.CIEM Invoke-AzureApi {
            $script:relationshipRequestPaths += @($Requests | ForEach-Object { $_.Path })
            $results = @{}
            foreach ($request in @($Requests)) {
                switch -Wildcard ($request.Path) {
                    '/groups/group-a/members*' {
                        $results[$request.Id] = [pscustomobject]@{
                            Success = $true
                            StatusCode = 200
                            Items = @(
                                [pscustomobject]@{ id = 'user-1'; '@odata.type' = '#microsoft.graph.user' }
                            )
                        }
                    }
                    '/groups/group-a/owners*' {
                        $results[$request.Id] = [pscustomobject]@{
                            Success = $true
                            StatusCode = 200
                            Items = @([pscustomobject]@{ id = 'user-1'; '@odata.type' = '#microsoft.graph.user' })
                        }
                    }
                    '/groups/group-b/members*' {
                        $results[$request.Id] = [pscustomobject]@{
                            Success = $true
                            StatusCode = 200
                            Items = @(
                                [pscustomobject]@{ id = 'group-a'; '@odata.type' = '#microsoft.graph.group' }
                            )
                        }
                    }
                    '/groups/group-b/owners*' {
                        $results[$request.Id] = [pscustomobject]@{
                            Success = $true
                            StatusCode = 200
                            Items = @()
                        }
                    }
                    default {
                        $results[$request.Id] = [pscustomobject]@{
                            Success = $true
                            StatusCode = 200
                            Items = @()
                        }
                    }
                }
            }
            $results
        }

        $relationships = InModuleScope Devolutions.CIEM -Parameters @{ groups = $groups; users = $users } {
            @(InvokeCIEMEntraRelationshipCollection -Groups $groups -DirectoryRoles @() -Users $users)
        }

        ($relationships | Where-Object { $_.Relationship -eq 'member_of' }) | Should -HaveCount 2
        ($relationships | Where-Object { $_.Relationship -eq 'owner_of' }) | Should -HaveCount 1
        ($relationships | Where-Object { $_.Relationship -eq 'transitive_member_of' -and $_.SourceId -eq 'user-1' }).TargetId | Sort-Object | Should -Be @('group-a', 'group-b')
        @($script:relationshipRequestPaths | Where-Object { $_ -like '/users/*/transitiveMemberOf*' }) | Should -BeNullOrEmpty

        $normalizedRelationships = @(
            $relationships |
                Sort-Object SourceId, SourceType, TargetId, TargetType, Relationship |
                ForEach-Object {
                    [pscustomobject]@{
                        SourceId = $_.SourceId
                        SourceType = $_.SourceType
                        TargetId = $_.TargetId
                        TargetType = $_.TargetType
                        Relationship = $_.Relationship
                    }
                }
        )
        $normalizedFixture = @(
            $script:LegacyRelationshipFixture |
                Sort-Object SourceId, SourceType, TargetId, TargetType, Relationship
        )

        ($normalizedRelationships | ConvertTo-Json -Depth 5) | Should -Be ($normalizedFixture | ConvertTo-Json -Depth 5)
    }
}