Tests/GraphShell.Tests.ps1

#Requires -Modules Pester

<#
    Pester tests for the GraphShell PowerShell module.
    Run locally with:
        Invoke-Pester -Path .\powershell\GraphShell\Tests\GraphShell.Tests.ps1 -Output Detailed
#>


BeforeAll {
    $script:ModuleRoot = Split-Path -Parent $PSScriptRoot
    $script:ManifestPath = Join-Path $ModuleRoot 'GraphShell.psd1'

    Remove-Module GraphShell -ErrorAction SilentlyContinue
    Import-Module $ManifestPath -Force -ErrorAction Stop
}

AfterAll {
    Remove-Module GraphShell -ErrorAction SilentlyContinue
}

Describe 'GraphShell module packaging' {
    It 'imports without error' {
        { Import-Module $script:ManifestPath -Force } | Should -Not -Throw
    }

    It 'has a valid module manifest' {
        { Test-ModuleManifest -Path $script:ManifestPath -ErrorAction Stop } | Should -Not -Throw
    }

    It 'exports exactly Get-GraphMapping, Get-GraphDetail, Get-GraphParity, Get-GraphLegacyMapping and Get-GraphPermission' {
        $exported = (Get-Command -Module GraphShell).Name | Sort-Object
        $exported | Should -Be @('Get-GraphDetail', 'Get-GraphLegacyMapping', 'Get-GraphMapping', 'Get-GraphParity', 'Get-GraphPermission')
    }

    It 'declares the GraphShell/Microsoft.Graph distinction in the manifest description' {
        $manifest = Test-ModuleManifest -Path $script:ManifestPath
        $manifest.Description | Should -Match 'does not replace the Microsoft Graph PowerShell SDK'
    }
}

Describe 'Get-GraphMapping -Cmdlet' {
    It 'resolves a known cmdlet to its REST relationship' {
        $result = Get-GraphMapping -Cmdlet 'Get-MgUser'
        $result | Should -Not -BeNullOrEmpty
        ($result | Where-Object Endpoint -eq '/users').Method | Should -Be 'GET'
        $result[0].PSObject.TypeNames | Should -Contain 'GraphShell.Mapping'
    }

    It 'resolves a renamed cmdlet to the current cmdlet' {
        $result = Get-GraphMapping -Cmdlet 'Remove-MgApplicationOwnerByRef'
        $result | Should -Not -BeNullOrEmpty
        $result[0].Cmdlet | Should -Be 'Remove-MgApplicationOwnerDirectoryObjectByRef'
        $result[0].RenamedFrom | Should -Be 'Remove-MgApplicationOwnerByRef'
    }

    It 'returns nothing for a cmdlet outside the Microsoft Graph SDK' {
        Get-GraphMapping -Cmdlet 'Get-AzRoleAssignment' | Should -BeNullOrEmpty
    }
}

Describe 'Get-GraphMapping -Endpoint' {
    It 'resolves a known REST endpoint to its cmdlet(s)' {
        $result = Get-GraphMapping -Endpoint '/users'
        $result | Should -Not -BeNullOrEmpty
        ($result.Cmdlet -contains 'Get-MgUser') | Should -Be $true
    }
}

Describe 'Get-GraphMapping -Permission' {
    It 'uses the permission reverse index to list related operations' {
        $result = Get-GraphMapping -Permission 'User.Read.All'
        $result | Should -Not -BeNullOrEmpty
        $result[0].MatchedPermission | Should -Be 'User.Read.All'
        ($result.Cmdlet -contains 'Get-MgUser') | Should -Be $true
    }

    It 'returns nothing for an unknown permission' {
        Get-GraphMapping -Permission 'Not.A.Real.Permission' | Should -BeNullOrEmpty
    }
}

Describe 'Get-GraphMapping -Module' {
    It 'returns operations for a known module name' {
        $result = Get-GraphMapping -Module 'Microsoft.Graph.Users'
        $result | Should -Not -BeNullOrEmpty
        ($result.ModuleName -contains 'Microsoft.Graph.Users') | Should -Be $true
    }
}

Describe 'Get-GraphMapping -Query' {
    It 'finds PIM-eligible role operations from a free-text concept' {
        $result = Get-GraphMapping -Query 'PIM eligible'
        $result | Should -Not -BeNullOrEmpty
        ($result | Where-Object Cmdlet -like '*RoleEligibilitySchedule*') | Should -Not -BeNullOrEmpty
    }

    It 'prioritizes records covering all concepts over unrelated compound-word matches' {
        $topResults = @(Get-GraphMapping -Query 'group membership' | Select-Object -First 10)

        $topResults.Cmdlet | Should -Contain 'Get-MgGroupMember'
        $topResults.ModuleName | Should -Not -Match 'DeviceManagement'
    }
}

Describe 'Get-GraphMapping output shape' {
    It 'returns real PowerShell objects usable with Select-Object/Where-Object' {
        $result = Get-GraphMapping -Permission 'User.Read.All' |
            Where-Object ApiVersion -eq 'v1.0' |
            Select-Object -First 1 Cmdlet, Module, Method, Endpoint
        $result | Should -Not -BeNullOrEmpty
        $result.PSObject.Properties.Name | Should -Contain 'Cmdlet'
    }
}

Describe 'Get-GraphDetail' {
    It 'loads only the necessary detail shard for a cmdlet' {
        $result = Get-GraphDetail -Name 'Get-MgUser'
        $result | Should -Not -BeNullOrEmpty
        ($result | Where-Object Endpoint -eq '/users').Permissions | Should -Not -BeNullOrEmpty
        $result[0].DetailShard | Should -Be 'g'
    }

    It 'skips generating a REST equivalent when the endpoint needs a path parameter' {
        $result = Get-GraphDetail -Name 'Get-MgUser' | Where-Object Endpoint -eq '/users/{user-id}'
        $result.RestEquivalent | Should -BeNullOrEmpty
        $result.RestNote | Should -Match 'path parameter'
    }

    It 'generates an Invoke-MgGraphRequest equivalent when the endpoint has no path parameters' {
        $result = Get-GraphDetail -Name 'Get-MgUser' | Where-Object Endpoint -eq '/users'
        $result.RestEquivalent | Should -Match 'Invoke-MgGraphRequest -Method GET'
    }

    It 'accepts pipeline input from Get-GraphMapping' {
        $result = Get-GraphMapping -Cmdlet 'Get-MgUser' | Select-Object -First 1 | Get-GraphDetail
        $result | Should -Not -BeNullOrEmpty
    }

    It 'warns and returns nothing for an unknown cmdlet' {
        $warnings = @()
        Get-GraphDetail -Name 'Get-MgDoesNotExist' -WarningVariable warnings -WarningAction SilentlyContinue |
            Should -BeNullOrEmpty
        $warnings.Count | Should -BeGreaterThan 0
    }
}

Describe 'GraphShell catalog cache' {
    It 'reuses the cached index across calls instead of reloading the file' {
        InModuleScope GraphShell {
            $script:GraphCatalogCache = $null
            $first = Get-GraphCatalogIndex
            $second = Get-GraphCatalogIndex
            [object]::ReferenceEquals($first, $second) | Should -Be $true
        }
    }
}

Describe 'Get-GraphParity' {
    It 'reports SdkAndOpenApi for an endpoint that has both a cmdlet and an OpenAPI operation' {
        $result = Get-GraphParity -Endpoint '/users' -Method GET
        $result | Should -Not -BeNullOrEmpty
        $result.State | Should -Be 'SdkAndOpenApi'
        $result.HasCmdlet | Should -Be $true
        $result.Cmdlets | Should -Contain 'Get-MgUser'
        $result.PSObject.TypeNames | Should -Contain 'GraphShell.Parity'
    }

    It 'reports OpenApiOnly for an endpoint the Graph API exposes without a matching cmdlet' {
        $result = Get-GraphParity -Endpoint '/applications/{id}/appmanagementpolicies' -Method DELETE
        $result | Should -Not -BeNullOrEmpty
        $result.State | Should -Be 'OpenApiOnly'
        $result.HasCmdlet | Should -Be $false
        $result.EstimatedModule | Should -Be 'Applications'
    }

    It 'reports SdkOnly for a cmdlet-backed endpoint that has no matching OpenAPI operation' {
        $result = Get-GraphParity -Endpoint '/directory/impactedResources/addTag' -Method POST
        $result | Should -Not -BeNullOrEmpty
        $result.State | Should -Be 'SdkOnly'
        $result.HasCmdlet | Should -Be $true
    }

    It 'warns and returns nothing for an endpoint unknown to both the SDK and the Graph API' {
        $warnings = @()
        Get-GraphParity -Endpoint '/this/does/not/exist/anywhere' -WarningVariable warnings -WarningAction SilentlyContinue |
            Should -BeNullOrEmpty
        $warnings.Count | Should -BeGreaterThan 0
    }

    It 'accepts pipeline input from Get-GraphMapping' {
        $result = Get-GraphMapping -Cmdlet 'Get-MgUser' | Get-GraphParity
        $result | Should -Not -BeNullOrEmpty
        $result | ForEach-Object { $_.State | Should -Be 'SdkAndOpenApi' }
    }

    It 'reports parity for every endpoint a cmdlet maps to when using -Cmdlet' {
        $result = Get-GraphParity -Cmdlet 'Get-MgUser'
        $result | Should -Not -BeNullOrEmpty
        $result.Count | Should -BeGreaterThan 1
        $result | ForEach-Object { $_.HasCmdlet | Should -Be $true }
        $result | ForEach-Object { $_.PSObject.TypeNames | Should -Contain 'GraphShell.Parity' }
    }

    It 'binds -Cmdlet from a pipeline object exposing a Cmdlet property' {
        $result = [pscustomobject]@{ Cmdlet = 'Get-MgUser' } | Get-GraphParity
        $result | Should -Not -BeNullOrEmpty
        ($result.Cmdlets | Select-Object -Unique) | Should -Contain 'Get-MgUser'
    }

    It 'warns and returns nothing for a cmdlet outside the catalog when using -Cmdlet' {
        $warnings = @()
        Get-GraphParity -Cmdlet 'Get-ThisCmdletDoesNotExist' -WarningVariable warnings -WarningAction SilentlyContinue |
            Should -BeNullOrEmpty
        $warnings.Count | Should -BeGreaterThan 0
    }
}

Describe 'Get-GraphLegacyMapping' {
    It 'resolves a legacy AzureAD command to its current Mg command' {
        $result = Get-GraphLegacyMapping -Command 'Get-AzureADUser'
        $result | Should -Not -BeNullOrEmpty
        $result | ForEach-Object { $_.GraphCommand | Should -Be 'Get-MgUser' }
        $result | ForEach-Object { $_.Status | Should -Be 'Mapped' }
        $result[0].PSObject.TypeNames | Should -Contain 'GraphShell.LegacyMapping'
    }

    It 'resolves a legacy MSOnline command to its current Mg command' {
        $result = Get-GraphLegacyMapping -Command 'Get-MsolUser'
        $result | Should -Not -BeNullOrEmpty
        $result | ForEach-Object { $_.GraphCommand | Should -Be 'Get-MgUser' }
    }

    It 'reports AmbiguousMapped when the official source lists more than one current command' {
        $result = Get-GraphLegacyMapping -Command 'Get-MsolDirSyncProvisioningError'
        $result | Should -Not -BeNullOrEmpty
        ($result | Select-Object -ExpandProperty GraphCommand -Unique).Count | Should -BeGreaterThan 1
        $result | ForEach-Object { $_.Status | Should -Be 'AmbiguousMapped' }
    }

    It 'includes permissions only when -IncludeDetails is used' {
        $withoutDetails = Get-GraphLegacyMapping -Command 'Get-AzureADUser' | Select-Object -First 1
        $withoutDetails.Permissions | Should -BeNullOrEmpty

        $withDetails = Get-GraphLegacyMapping -Command 'Get-AzureADUser' -IncludeDetails | Select-Object -First 1
        $withDetails.Permissions | Should -Not -BeNullOrEmpty
    }

    It 'warns and returns nothing for a command outside the official legacy mapping' {
        $warnings = @()
        Get-GraphLegacyMapping -Command 'Get-DoesNotExist' -WarningVariable warnings -WarningAction SilentlyContinue |
            Should -BeNullOrEmpty
        $warnings.Count | Should -BeGreaterThan 0
    }
}

Describe 'Get-GraphPermission' {
    It 'returns both the Application and Delegated definitions for a dual-type permission' {
        $result = Get-GraphPermission -Name 'User.Read.All'
        $result | Should -Not -BeNullOrEmpty
        ($result | Select-Object -ExpandProperty Type | Sort-Object -Unique) | Should -Be @('Application', 'Delegated')
        $result | ForEach-Object { $_.Id | Should -Not -BeNullOrEmpty }
        $result[0].PSObject.TypeNames | Should -Contain 'GraphShell.Permission'
    }

    It 'filters to a single type with -Type' {
        $result = Get-GraphPermission -Name 'User.Read.All' -Type Application
        $result | Should -Not -BeNullOrEmpty
        $result | ForEach-Object { $_.Type | Should -Be 'Application' }
    }

    It 'reports the related-operation count without duplicating the permission reverse index' {
        $result = Get-GraphPermission -Name 'User.Read.All' -Type Application
        $result.Operations | Should -BeGreaterThan 0
        $result.Cmdlets | Should -BeNullOrEmpty
    }

    It 'includes cmdlets and endpoints only with -IncludeOperations' {
        $result = Get-GraphPermission -Name 'User.Read.All' -Type Application -IncludeOperations
        $result.Cmdlets | Should -Not -BeNullOrEmpty
        $result.Endpoints | Should -Not -BeNullOrEmpty
    }

    It 'warns and returns nothing for a permission outside the official catalog' {
        $warnings = @()
        Get-GraphPermission -Name 'Not.A.Real.Permission' -WarningVariable warnings -WarningAction SilentlyContinue |
            Should -BeNullOrEmpty
        $warnings.Count | Should -BeGreaterThan 0
    }

    It 'accepts a permission name over the pipeline from Get-GraphMapping -Permission' {
        $permissionName = Get-GraphMapping -Permission 'User.Read.All' | Select-Object -First 1 -ExpandProperty MatchedPermission
        $result = $permissionName | Get-GraphPermission
        $result | Should -Not -BeNullOrEmpty
    }
}