Tests/Get-SecretInfo.Tests.ps1

BeforeAll {
    Get-Module 'SecretManagement.CyberArk' | Remove-Module -Force
    Get-Module 'Microsoft.Powershell.SecretManagement' | Remove-Module -Force

    Import-Module Microsoft.PowerShell.SecretManagement
    $ExtensionModule = Import-Module "$PSScriptRoot/../SecretManagement.CyberArk.Extension/*.psd1" -Force -PassThru
    $VaultName = 'CyberArk.Test'

    Mock Get-PASAccount -MockWith {
        return [PSCustomObject]@{
            name     = 'localAdmin01'
            userName = 'localAdmin01'
            Id       = '1'
            safeName = 'LocalAdministrators'
        }
    } -ModuleName $ExtensionModule.Name
}

AfterAll {
    Remove-Module $ExtensionModule -Force
}
Describe 'Get-SecretInfo' {
    Context 'when connection type is REST' {
        BeforeAll {


            $VaultParams = @{ConnectionType = 'REST' }
            Mock Get-PASSession -MockWith { [PSCustomObject]@{ BaseURI = 'https://pvwa' } } -ModuleName $ExtensionModule.Name
        }

        It 'returns information on a secret' {
            $SecretInfo = Get-SecretInfo -Filter 'localAdmin01' -VaultName $VaultName -AdditionalParameters $VaultParams
            $SecretInfo | Should -Not -BeNullOrEmpty
            $SecretInfo | Should -BeOfType [Microsoft.PowerShell.SecretManagement.SecretInformation]
            $SecretInfo.Name | Should -Be 'localAdmin01'
            $SecretInfo.Metadata.userName | Should -Be 'localAdmin01'
            $SecretInfo.Metadata.id | Should -Be '1'
            $SecretInfo.Metadata.safeName | Should -Be 'LocalAdministrators'
        }

        It 'returns only allowed metadata keys with allowed types' {
            Mock Get-PASAccount -MockWith {
                [PSCustomObject]@{
                    id = '7'; name = 'acct'; userName = 'u'; address = 'host'; safeName = 'S'; platformId = 'P'; secretType = 'password'
                    createdTime = 1700000000; secret = 'sekrit'; Password = 'sekrit'; Content = 'sekrit'
                    platformAccountProperties = @{ a = 'b' }; secretManagement = @{ automaticManagementEnabled = $true }
                    remoteMachinesAccess = @{ x = 'y' }; empty = $null
                }
            } -ModuleName $ExtensionModule.Name
            $Info = Get-SecretInfo -Filter 'acct' -VaultName $VaultName -AdditionalParameters $VaultParams
            ($Info.Metadata.Keys | Sort-Object) | Should -Be @('address', 'createdTime', 'id', 'name', 'platformId', 'safeName', 'secretType', 'userName')
            $Info.Metadata['createdTime'] | Should -BeOfType [datetime]
            $Info.Metadata['createdTime'] | Should -Be ([datetime]::new(2023, 11, 14, 22, 13, 20, [DateTimeKind]::Utc))
            $Info.Metadata['id'] | Should -BeOfType [string]
            "$($Info.Metadata.Values)" | Should -Not -BeLike '*sekrit*'
        }

        It 'scopes results to the vault safeName' {
            Mock Get-PASAccount -MockWith {
                @(
                    [PSCustomObject]@{ id = '1'; name = 'acctA'; safeName = 'SafeA' },
                    [PSCustomObject]@{ id = '2'; name = 'acctB'; safeName = 'SafeB' }
                )
            } -ModuleName $ExtensionModule.Name
            $Info = @(Get-SecretInfo -Filter '*' -VaultName $VaultName -AdditionalParameters ($VaultParams + @{ SafeName = 'safea' }))
            $Info.Name | Should -Be 'acctA'
            Should -Invoke -CommandName Get-PASAccount -ModuleName $ExtensionModule.Name -Times 1 -Exactly -ParameterFilter { $safeName -eq 'safea' }
        }

        It 'writes an error with the exception message instead of throwing when the lookup fails' {
            Mock Get-PASAccount -MockWith { throw 'pvwa unreachable' } -ModuleName $ExtensionModule.Name
            Get-SecretInfo -Filter '*' -VaultName $VaultName -AdditionalParameters $VaultParams -ErrorAction SilentlyContinue -ErrorVariable Err | Should -BeNullOrEmpty
            "$Err" | Should -BeLike '*pvwa unreachable*'
        }

        It 'converts createdTime given in milliseconds' {
            Mock Get-PASAccount -MockWith { [PSCustomObject]@{ id = '7'; name = 'acct'; createdTime = 1700000000000 } } -ModuleName $ExtensionModule.Name
            $Info = Get-SecretInfo -Filter 'acct' -VaultName $VaultName -AdditionalParameters $VaultParams
            $Info.Metadata['createdTime'] | Should -Be ([datetime]::new(2023, 11, 14, 22, 13, 20, [DateTimeKind]::Utc))
        }

        It 'drops an out-of-range or invalid createdTime without throwing' -ForEach @(
            @{ Value = 9000000000000000000L }, @{ Value = -9000000000000000000L }, @{ Value = [double]::MaxValue }, @{ Value = 'not-a-time' }, @{ Value = $true }
        ) {
            $global:CreatedTimeValue = $Value
            Mock Get-PASAccount -MockWith { [PSCustomObject]@{ id = '7'; name = 'acct'; createdTime = $global:CreatedTimeValue } } -ModuleName $ExtensionModule.Name
            $Info = Get-SecretInfo -Filter 'acct' -VaultName $VaultName -AdditionalParameters $VaultParams -ErrorAction Stop
            $Info.Name | Should -Be 'acct'
            $Info.Metadata.Keys | Should -Not -Contain 'createdTime'
        }

        Context 'wildcard filtering' {
            BeforeEach {
                Mock Get-PASAccount -MockWith {
                    @(
                        [PSCustomObject]@{ id = '1'; name = 'localAdmin01' },
                        [PSCustomObject]@{ id = '2'; name = 'localAdmin02' },
                        [PSCustomObject]@{ id = '3'; name = 'domainAdmin' }
                    )
                } -ModuleName $ExtensionModule.Name
            }

            It 'searches with the non-wildcard text and filters results with -like' {
                $Info = @(Get-SecretInfo -Filter 'local*01' -VaultName $VaultName -AdditionalParameters $VaultParams)
                $Info.Name | Should -Be 'localAdmin01'
                Should -Invoke -CommandName Get-PASAccount -ModuleName $ExtensionModule.Name -Times 1 -Exactly -ParameterFilter { $search -eq 'local' }
            }

            It 'returns everything and does not search for *' {
                $Info = @(Get-SecretInfo -Filter '*' -VaultName $VaultName -AdditionalParameters $VaultParams)
                $Info.Count | Should -Be 3
                Should -Invoke -CommandName Get-PASAccount -ModuleName $ExtensionModule.Name -Times 1 -Exactly -ParameterFilter { -not $PesterBoundParameters.ContainsKey('search') }
            }

            It 'treats an empty filter as *' {
                @(Get-SecretInfo -Filter '' -VaultName $VaultName -AdditionalParameters $VaultParams).Count | Should -Be 3
            }

            It 'matches an exact name only' {
                $Info = @(Get-SecretInfo -Filter 'localAdmin0' -VaultName $VaultName -AdditionalParameters $VaultParams)
                $Info | Should -BeNullOrEmpty
            }

            It 'does not prefilter with -search when the filter has a [ character class, and still matches' {
                Mock Get-PASAccount -MockWith ([scriptblock]::Create('$global:PASBound = @($PesterBoundParameters.Keys); @([PSCustomObject]@{ id = "1"; name = "abz" }, [PSCustomObject]@{ id = "2"; name = "bc" })')) -ModuleName $ExtensionModule.Name
                $Info = @(Get-SecretInfo -Filter 'a[bc]z' -VaultName $VaultName -AdditionalParameters $VaultParams)
                $Info.Name | Should -Be 'abz'
                $global:PASBound | Should -Not -Contain 'search'
            }

            It 'does not prefilter with -search when the filter has an escaped wildcard, and matches it literally' {
                Mock Get-PASAccount -MockWith ([scriptblock]::Create('$global:PASBound = @($PesterBoundParameters.Keys); @([PSCustomObject]@{ id = "1"; name = "foo*bar" }, [PSCustomObject]@{ id = "2"; name = "fooXbar" })')) -ModuleName $ExtensionModule.Name
                $Info = @(Get-SecretInfo -Filter 'foo`*bar' -VaultName $VaultName -AdditionalParameters $VaultParams)
                $Info.Name | Should -Be 'foo*bar'
                $global:PASBound | Should -Not -Contain 'search'
            }

            It 'supports ? wildcards' {
                @(Get-SecretInfo -Filter 'localAdmin0?' -VaultName $VaultName -AdditionalParameters $VaultParams).Count | Should -Be 2
            }
        }

    }
    Context 'when connection type is Central Credential Provder' {
        BeforeAll {
            $VaultParams = @{ConnectionType = 'CentralCredentialProvider'; AppID = 'banana'; URL = 'https://banana.com' }
        }

        It 'invokes Get-CCPCredential' {
            Mock Get-CCPCredential -MockWith {
                [PSCustomObject]@{ UserName = 'svc-ccp'; Content = 'ccp-pass'; Address = 'host1'; Name = 'admin' }
            } -ModuleName $ExtensionModule.Name
            $Info = Get-SecretInfo -Filter 'admin' -VaultName $VaultName -AdditionalParameters $VaultParams
            Should -Invoke -CommandName Get-CCPCredential -ModuleName $ExtensionModule.Name
            $Info.Type | Should -Be 'PSCredential'
            $Info.Name | Should -Be 'admin'
            $Info.Metadata.Keys | Should -Not -Contain 'Content'
        }

        It 'writes an error with the exception message when Get-CCPCredential throws' {
            Mock Get-CCPCredential -MockWith { throw 'ccp exploded' } -ModuleName $ExtensionModule.Name
            $Info = Get-SecretInfo -Filter 'admin' -VaultName $VaultName -AdditionalParameters $VaultParams -ErrorAction SilentlyContinue -ErrorVariable Err
            $Info | Should -BeNullOrEmpty
            "$Err" | Should -BeLike '*admin*ccp exploded*'
        }

        It 'returns only allowed metadata keys with allowed types' {
            Mock Get-CCPCredential -MockWith {
                [PSCustomObject]@{ Content = 'sekrit'; Name = 'admin'; UserName = 'svc'; Address = 'host'; Safe = 'S'; Folder = 'Root'; PolicyID = 'Pol'; DeviceType = 'Operating System'
                    PasswordChangeInProcess = $false; Extra = @{ a = 'b' }; Missing = $null }
            } -ModuleName $ExtensionModule.Name
            $Info = Get-SecretInfo -Filter 'admin' -VaultName $VaultName -AdditionalParameters $VaultParams
            ($Info.Metadata.Keys | Sort-Object) | Should -Be @('Address', 'DeviceType', 'Folder', 'Name', 'PolicyID', 'Safe', 'UserName')
            "$($Info.Metadata.Values)" | Should -Not -BeLike '*sekrit*'
        }

        It 'returns nothing without calling Get-CCPCredential when the filter has wildcards' -ForEach @('*', 'adm*', 'a?min', 'ad[a-z]in', '') {
            Mock Get-CCPCredential -MockWith { throw 'should not be called' } -ModuleName $ExtensionModule.Name
            $Verbose = Get-SecretInfo -Filter $_ -VaultName $VaultName -AdditionalParameters ($VaultParams + @{ Verbose = $true }) 4>&1
            @($Verbose | Where-Object { $_ -is [Microsoft.PowerShell.SecretManagement.SecretInformation] }) | Should -BeNullOrEmpty
            "$Verbose" | Should -BeLike '*wildcard*'
            Should -Invoke -CommandName Get-CCPCredential -ModuleName $ExtensionModule.Name -Times 0 -Exactly
        }

        It 'treats a filter without wildcards as the exact name' {
            Mock Get-CCPCredential -MockWith { [PSCustomObject]@{ Content = 'sekrit'; UserName = 'svc' } } -ModuleName $ExtensionModule.Name
            $Info = Get-SecretInfo -Filter 'myobject' -VaultName $VaultName -AdditionalParameters $VaultParams
            $Info.Name | Should -Be 'myobject'
        }

    }

    Context 'when connection type is Credential Provder' {
        BeforeAll {
            $VaultParams = @{ConnectionType = 'CredentialProvider'; AppID = 'banana' }
        }

        It 'invokes Get-AIMCredential' {
            Mock Get-AIMCredential -MockWith {
                [PSCustomObject]@{ UserName = 'svc-aim'; Password = 'aim-pass'; Address = 'host2'; Name = 'admin' }
            } -ModuleName $ExtensionModule.Name
            $Info = Get-SecretInfo -Filter 'admin' -VaultName $VaultName -AdditionalParameters $VaultParams
            Should -Invoke -CommandName Get-AIMCredential -ModuleName $ExtensionModule.Name
            $Info.Type | Should -Be 'PSCredential'
            $Info.Name | Should -Be 'admin'
            $Info.Metadata.Keys | Should -Not -Contain 'Password'
        }

        It 'writes an error with the exception message when Get-AIMCredential throws' {
            Mock Get-AIMCredential -MockWith { throw 'aim exploded' } -ModuleName $ExtensionModule.Name
            $Info = Get-SecretInfo -Filter 'admin' -VaultName $VaultName -AdditionalParameters $VaultParams -ErrorAction SilentlyContinue -ErrorVariable Err
            $Info | Should -BeNullOrEmpty
            "$Err" | Should -BeLike '*admin*aim exploded*'
        }

        It 'reports the name it was looked up by (UserName) so the name round-trips into Get-Secret' {
            Mock Get-AIMCredential -MockWith {
                [PSCustomObject]@{ UserName = 'svc-aim'; Name = 'Operating System-WinDomain-host-svc-aim'; Password = 'x' }
            } -ModuleName $ExtensionModule.Name
            $Info = Get-SecretInfo -Filter 'svc-aim' -VaultName $VaultName -AdditionalParameters $VaultParams
            $Info.Name | Should -Be 'svc-aim'
            $null = Get-Secret -Name $Info.Name -VaultName $VaultName -AdditionalParameters $VaultParams -ErrorAction SilentlyContinue
            Should -Invoke -CommandName Get-AIMCredential -ModuleName $ExtensionModule.Name -Times 2 -Exactly -ParameterFilter { $UserName -eq 'svc-aim' }
        }

        It 'passes the SafeName vault parameter to Get-AIMCredential as Safe' {
            Mock Get-AIMCredential -MockWith {
                [PSCustomObject]@{ UserName = 'svc-aim'; Name = 'admin' }
            } -ModuleName $ExtensionModule.Name
            $null = Get-SecretInfo -Filter 'admin' -VaultName $VaultName -AdditionalParameters ($VaultParams + @{SafeName = 'Windows' })
            Should -Invoke -CommandName Get-AIMCredential -ModuleName $ExtensionModule.Name -Times 1 -Exactly -ParameterFilter { $Safe -eq 'Windows' }
        }

        It 'returns only allowed metadata keys with allowed types' {
            Mock Get-AIMCredential -MockWith {
                [PSCustomObject]@{ Password = 'sekrit'; Name = 'admin'; UserName = 'svc'; Address = 'host'; Safe = 'S'; Folder = 'Root'; PolicyID = 'Pol'; DeviceType = 'Operating System'
                    PasswordChangeInProcess = $false; Extra = @{ a = 'b' }; Missing = $null }
            } -ModuleName $ExtensionModule.Name
            $Info = Get-SecretInfo -Filter 'admin' -VaultName $VaultName -AdditionalParameters $VaultParams
            ($Info.Metadata.Keys | Sort-Object) | Should -Be @('Address', 'DeviceType', 'Folder', 'Name', 'PolicyID', 'Safe', 'UserName')
            "$($Info.Metadata.Values)" | Should -Not -BeLike '*sekrit*'
        }

        It 'returns nothing without calling Get-AIMCredential when the filter has wildcards' -ForEach @('*', 'adm*', 'a?min', 'ad[a-z]in', '') {
            Mock Get-AIMCredential -MockWith { throw 'should not be called' } -ModuleName $ExtensionModule.Name
            $Verbose = Get-SecretInfo -Filter $_ -VaultName $VaultName -AdditionalParameters ($VaultParams + @{ Verbose = $true }) 4>&1
            @($Verbose | Where-Object { $_ -is [Microsoft.PowerShell.SecretManagement.SecretInformation] }) | Should -BeNullOrEmpty
            "$Verbose" | Should -BeLike '*wildcard*'
            Should -Invoke -CommandName Get-AIMCredential -ModuleName $ExtensionModule.Name -Times 0 -Exactly
        }

        It 'treats a filter without wildcards as the exact name' {
            Mock Get-AIMCredential -MockWith { [PSCustomObject]@{ Password = 'sekrit'; UserName = 'svc' } } -ModuleName $ExtensionModule.Name
            $Info = Get-SecretInfo -Filter 'myobject' -VaultName $VaultName -AdditionalParameters $VaultParams
            $Info.Name | Should -Be 'myobject'
        }

    }
}