CommonServerPowerShell

Common code that will be merged into each PowerShell script/integration when it runs.

powershell · Base

Details

IDCommonServerPowerShell
Languagepowershell
From Version5.5.0
Docker Imagedemisto/powershell:7.5.0.9017890
Tagsinfra server
BeforeAll {
    . $PSScriptRoot\CommonServerPowerShell.ps1
}


Describe 'Check-DemistoServerRequest' {
    It 'Check that a call to demisto DemistoServerRequest mock works. Should always return an empty response' {
        global:DemistoServerRequest @{} | Should -BeNullOrEmpty
        $demisto.GetAllSupportedCommands() | Should -BeNullOrEmpty
    }
}

Describe 'Check-incidents'{
    It 'Check Incidents creation in a case of more than one incident'{
        $test_incidents = @()
        0..2 | ForEach-Object {
            $test_incidents += [PSCustomObject]@{
                id           = "1111"
                name         = "incident_name"
                type         = "type"
                createdOn    = "2023-05-28T05:57:18.404Z"
            }
        }
        $incidents = @()
        $test_incidents | Foreach-Object {
            $NewAlert = @{
                name = "$($_.type)-$($_.name)"
                occurred = $_.createdOn
                rawJSON = $_ | ConvertTo-JSON -Depth 2
                severity = 1
            }
            $Incidents += $NewAlert
        }
        class DemistoObject {
            DemistoObject () {
            }

            [array] Incidents ($Incidents) {
                # This cmdlet returns a string representing the input object converted to a JSON string.
                $Incidents = $Incidents | ConvertTo-Json -Depth 6 -AsArray
                return $Incidents
            }
        }
        [DemistoObject]$demisto = [DemistoObject]::New()
        $r = $demisto.Incidents($incidents)
        $r.GetType().IsArray | Should -BeTrue
        $incidentArray = $r | ConvertFrom-Json
        $incidentArray.Length | Should -Be 3
    }

    It 'Check Incidents creation in a case of only one incident'{
        $test_incidents = @()
        0..0 | ForEach-Object {
            $test_incidents += [PSCustomObject]@{
                id           = "1111"
                name         = "incident_name"
                type         = "type"
                createdOn    = "2023-05-28T05:57:18.404Z"
            }
        }
        $incidents = @()
        $test_incidents | Foreach-Object {
            $NewAlert = @{
                name = "$($_.type)-$($_.name)"
                occurred = $_.createdOn
                rawJSON = $_ | ConvertTo-JSON -Depth 2
                severity = 1
            }
            $Incidents += $NewAlert
        }
        class DemistoObject {
            DemistoObject () {
            }
            
            [array] Incidents ($Incidents) {
                # This cmdlet returns a string representing the input object converted to a JSON string.
                $Incidents = $Incidents | ConvertTo-Json -Depth 6 -AsArray
                return $Incidents
            }
        }
        [DemistoObject]$demisto = [DemistoObject]::New()
        $r = $demisto.Incidents($incidents)
        $r.GetType().IsArray | Should -BeTrue
        $incidentArray = $r | ConvertFrom-Json
        $incidentArray.Length | Should -Be 1
    }
}

Describe 'Check-UtilityFunctions' {
    It "VersionEqualGreaterThen" {
        class DemistoObject {
            DemistoObject () {
            }

            [array] DemistoVersion () {
                return @{
                    "version" = "6.0.0-build"
                }
            }
        }
        [DemistoObject]$demisto = [DemistoObject]::New()
        DemistoVersionGreaterEqualThen -version "6.0.0"  | Should -BeTrue
        DemistoVersionGreaterEqualThen "5.0.1-build"  | Should -BeTrue
        DemistoVersionGreaterEqualThen -version "6.0.1"  | Should -BeFalse
        DemistoVersionGreaterEqualThen -version "6.0.2-build"  | Should -BeFalse
    }

    It "ArgToList" {
        $r = argToList "a,b,c,2"
        $r.GetType().IsArray | Should -BeTrue
        $r[0] | Should -Be "a"
        $r.Length | Should -Be 4
        $r = argToList '["a","b","c",2]'
        $r.GetType().IsArray | Should -BeTrue
        $r[0] | Should -Be "a"
        $r[3] | Should -Be 2
        $r.Length | Should -Be 4
        $r = argToList @("a","b","c",2)
        $r.GetType().IsArray | Should -BeTrue
        $r[0] | Should -Be "a"
        $r[3] | Should -Be 2
        $r.Length | Should -Be 4
        $r = argToList "a"
        $r.GetType().IsArray | Should -BeTrue
        $r[0] | Should -Be "a"
        $r.Length | Should -Be 1
    }

    It "ReturnOutputs" {
        $msg = "Human readable"
        $r = ReturnOutputs $msg @{Test="test"} @{Raw="raw"}
        $r.ContentsFormat | Should -Be "json"
        $r.HumanReadable | Should -Be $msg
        $r.EntryContext.Test | Should -Be "test"
        $r.Contents.Raw | Should -Be "raw"
        $r = ReturnOutputs $msg
        $r.ContentsFormat | Should -Be "text"
        $r.Contents | Should -Be $msg
    }

    It "ReturnOutputs PsCustomObject" {
        $msg = "Human readable"
        $output = [PsCustomObject]@{Test="test"}
        $raw = [PSCustomObject]@{Raw="raw"}
        $r = ReturnOutputs $msg  $output $raw
        $r.ContentsFormat | Should -Be "json"
        $r.HumanReadable | Should -Be $msg
        $r.EntryContext.Test | Should -Be "test"
        $r.Contents.Raw | Should -Be "raw"
        $r = ReturnOutputs $msg
        $r.ContentsFormat | Should -Be "text"
        $r.Contents | Should -Be $msg
    }

    It "ReturnError simple" {
        $msg = "this is an error"
        $r = ReturnError $msg
        $r.ContentsFormat | Should -Be "text"
        $r.Type | Should -Be 4
        $r.Contents | Should -Be $msg
        $r.EntryContext | Should -BeNullOrEmpty
    }

    Context "FileResult checks" {
        
        BeforeAll {    
            # move into a temp directory which we later on delete from the files created in the  tests         
            $temp_parent = [System.IO.Path]::GetTempPath()
            [string] $temp_dir = [System.Guid]::NewGuid()
            $temp_path = Join-Path $temp_parent $temp_dir
            New-Item -ItemType Directory -Path $temp_path
            Set-Location $temp_path -PassThru
        }

        It "FileResult default" {
            $data = "this is a test"
            $r = FileResult "test.txt" $data
            $r.Type | Should -Be 3
            $r.File | Should -Be "test.txt"
            $inv_id = $demisto.Investigation().id
            Get-Content -Path "${inv_id}_$($r.FileID)" | Should -Be $data
        }

        It "FileResult info" {
            $data = "this is a test"
            # pass $true to indicate that this is a file info entry
            $r = FileResult "test.txt" $data $true
            $r.Type | Should -Be 9
            $r.File | Should -Be "test.txt"
            $inv_id = $demisto.Investigation().id
            Get-Content -Path "${inv_id}_$($r.FileID)" | Should -Be $data
        }

        AfterAll {
            Set-Location -Path - -PassThru
            Remove-Item $temp_path -Recurse
        }
    }
    

    Context "Check log function" {
        BeforeAll {
            Mock DemistoServerLog {}
        }

        It "ReturnError complex" {
            # simulate an error
            Test-JSON "{badjson}" -ErrorAction SilentlyContinue -ErrorVariable err
            $msg = "this is a complex error"
            $r = ReturnError $msg $err @{Failed = $true}
            $r.Contents | Should -Be $msg
            $r.EntryContext.Failed | Should -BeTrue
            # ReturnError call demisto.Error() make sure it was called
            Assert-MockCalled -CommandName DemistoServerLog -Times 2 -ParameterFilter {$level -eq "error"}
            Assert-MockCalled -CommandName DemistoServerLog -Times 1 -ParameterFilter {$msg.Contains("Cannot parse the JSON")}
        }
    }
    Context "TableToMarkdown" {
        BeforeAll {
            $HashTableWithTwoEntries = @(
            [ordered]@{
                Index = '0'
                Name = 'First element'
            },
            [ordered]@{
                Index = '1'
                Name = 'Second element'
            }
            )
            $HashTableWithOneEntry = @(
            [ordered]@{
                Index = '0'
                Name = 'First element'
            })
            $OneElementObject = @()
            ForEach ($object in $HashTableWithOneEntry)
            {
                $OneElementObject += New-Object PSObject -Property $object
            }
            $TwoElementObject = @()
            ForEach ($object in $HashTableWithTwoEntries)
            {
                $TwoElementObject += New-Object PSObject -Property $object
            }
            $hashTable = [ordered]@{"key1" = "value1";"key2" = "value2"}
        }
        It "Empty list without a name" {
            TableToMarkdown @() | Should -Be "**No entries.**`n"
        }
        It "A list with one element and no name" {
            TableToMarkdown $OneElementObject | Should -Be "| Index | Name`n| --- | ---`n| 0 | First element`n"
        }
        It "A list with two elements and no name" {
            TableToMarkdown $TwoElementObject | Should -Be "| Index | Name`n| --- | ---`n| 0 | First element`n| 1 | Second element`n"
        }
        It "Empty list with a name" {
            TableToMarkdown @() "Test Name" | Should -Be "### Test Name`n**No entries.**`n"
        }
        It "A list with two elements and a name" {
            TableToMarkdown $TwoElementObject "Test Name" | Should -Be "### Test Name`n| Index | Name`n| --- | ---`n| 0 | First element`n| 1 | Second element`n"
        }
        It "A list with one elements and a name" {
            TableToMarkdown $OneElementObject "Test Name" | Should -Be "### Test Name`n| Index | Name`n| --- | ---`n| 0 | First element`n"
        }
        It "Check alias to ConvertTo-Markdown" {
            ConvertTo-Markdown @() "Test Name" | Should -Be "### Test Name`n**No entries.**`n"
        }
        It "Check with nested objects" {
            $TwoElementObject += New-Object PSObject -Property ([ordered]@{Index = "2"; Name = $HashTableWithOneEntry})
            TableToMarkdown $TwoElementObject "Test Name" | Should -Be "### Test Name`n| Index | Name`n| --- | ---`n| 0 | First element`n| 1 | Second element`n| 2 | \{`"Index`":`"0`",`"Name`":`"First element`"\}`n"
        }
        It "check with a single hashtable" {
            $hashTable | TableToMarkdown | Should -Be "| key1 | key2`n| --- | ---`n| value1 | value2`n"
        }
        It "Check with a single PSObject" {
            New-Object PSObject -Property $hashTable | TableToMarkdown | Should -Be "| key1 | key2`n| --- | ---`n| value1 | value2`n"
        }
        It "Check with a list of hashtables"{
             $HashTableWithOneEntry | TableToMarkdown | Should -Be "| Index | Name`n| --- | ---`n| 0 | First element`n"
        }
        It "Check with False boolean that is not $null" {
            @{test=$false} | TableToMarkdown | Should -Be "| test`n| ---`n| False`n"
        }
        It "Check with PSObject that nested list"{
            $OneElementObject += New-Object PSObject -Property @{Index=1;Name=@('test1';'test2')}
            $OneElementObject | TableToMarkdown | Should -Be "| Index | Name`n| --- | ---`n| 0 | First element`n| 1 | \[`"test1`",`"test2`"\]`n"
        }
        It "ArrayList object"{
            $ArrLst = [System.Collections.ArrayList]::new()
            $ArrLst.Add("a string")
            $ArrLst.Add("another string")
            $tbl = @{"arraylist" = $ArrLst} | TableToMarkdown
            $tbl | Should -Match "a string"
            $tbl | Should -Match "another string"
        }
    }
    Context "Test stringEscapeMD" {
        It "Escaping special chars"{
            '\ ` * _ { } [ ] ( ) # + - | ! `n `r `r`n' | stringEscapeMD | Should -Be '\\ \` \* \_ \{ \} \[ \] \( \) \# \+ \- \| \! \`n \`r \`r\`n'
        }
    }
}
Describe "Test ParseDateRange"{
        It "Naive Tests" -Tag naive{
            $m_date = Get-Date "2020-11-30T23:30:00.0000000"
            $three_mins = $m_date.AddMinutes(-3)
            $three_hours = $m_date.AddHours(-3)
            $three_days = $m_date.AddDays(-3)
            $three_weeks = $m_date.AddDays(-7 * 3)
            $three_months = $m_date.AddMonths(-3)
            $three_years = $m_date.AddYears(-3)
            Mock -CommandName Get-Date -MockWith {$m_date}
            ParseDateRange("3 minutes") | Should -Be @($three_mins, $m_date)
            ParseDateRange("3 minute") | Should -Be @($three_mins, $m_date)
            ParseDateRange("3 hours") | Should -Be @($three_hours, $m_date)
            ParseDateRange("3 hour") | Should -Be @($three_hours, $m_date)
            ParseDateRange("3 days") | Should -Be @($three_days, $m_date)
            ParseDateRange("3 day") | Should -Be @($three_days, $m_date)
            ParseDateRange("3 week") | Should -Be @($three_weeks, $m_date)
            ParseDateRange("3 weeks") | Should -Be @($three_weeks, $m_date)
            ParseDateRange("3 month") | Should -Be @($three_months, $m_date)
            ParseDateRange("3 months") | Should -Be @($three_months, $m_date)
            ParseDateRange("3 year") | Should -Be @($three_years, $m_date)
            ParseDateRange("3 years") | Should -Be @($three_years, $m_date)
        }
        It "Too many arguments"{
            { ParseDateRange("3 days long") } | Should -Throw
        }
        It "Not a number"{
            { ParseDateRange("lol days") } | Should -Throw
        }
        It "Wrong time unit" {
            { ParseDateRange("3 planets") } | Should -Throw
        }
    }

Describe "Test Remove-SelfReferences" {
<#
Given: Outputs or RawOutputs complicated objects with inner properties.
When: Remove-SelfReferences is called as a part of ReturnOutputs.
Then: Make sure removing self references works as expected.
#>
    It "One self-reference" {
    # (Case 1): Outputs dict containing 1 self-reference. Expect: self-reference removed, otherwise no change.
        $inputDict = [PSCustomObject]@{
            Property1 = "Value1"
            NestedProperty = [PSCustomObject]@{
                NestedProperty1 = "NestedValue1"
                NestedProperty2 = [PSCustomObject]@{
                    DeeplyNestedProperty = [PSCustomObject]@{
                        Deep3 = "DeepValue"
                        Deep4 = "Deep4"
                        }
                    }
                }
            }

        $inputDict.NestedProperty.NestedProperty1 = $inputDict.NestedProperty

        $outDict = Remove-SelfReferences($inputDict)
        $outDict.Property1 | Should -Be "Value1"
        $outDict.NestedProperty.NestedProperty2.DeeplyNestedProperty.Deep3 | Should -Be "DeepValue"
        $outDict.NestedProperty.NestedProperty2.DeeplyNestedProperty.Deep4 | Should -Be "Deep4"

        $outDict.NestedProperty.PSObject.Properties.name -match "NestedProperty1" | Should -Be $false

    }
    It "No self-reference" {
    # (Case 2): Outputs dict containing no self-reference. Expect: no change.
        $inputDict = [PSCustomObject]@{
            Property1 = "Value1"
            NestedProperty = [PSCustomObject]@{
                NestedProperty1 = "NestedValue1"
                NestedProperty2 = [PSCustomObject]@{
                    DeeplyNestedProperty = [PSCustomObject]@{
                        Deep3 = "DeepValue"
                        Deep4 = "Deep4"
                        }
                    }
                }
            }

        $outDict = Remove-SelfReferences($inputDict)
        $outDict.Property1 | Should -Be "Value1"
        $outDict.NestedProperty.NestedProperty1 | Should -Be "NestedValue1"
        $outDict.NestedProperty.NestedProperty2.DeeplyNestedProperty.Deep3 | Should -Be "DeepValue"
        $outDict.NestedProperty.NestedProperty2.DeeplyNestedProperty.Deep4 | Should -Be "Deep4"

    }
    It "Two self-reference" {
    # (Case 3): Outputs dict containing 2 self-references. Expect: both self-references removed, otherwise no change.
        $inputDict = [PSCustomObject]@{
            Property1 = [PSCustomObject]@{
                SelfRef = $null
                OtherProp = 5
            }
            NestedProperty = [PSCustomObject]@{
                NestedProperty1 = "NestedValue1"
                NestedProperty2 = [PSCustomObject]@{
                    DeeplyNestedProperty = [PSCustomObject]@{
                        Deep3 = "DeepValue"
                        Deep4 = "Deep4"
                    }
                }
            }
        }

        $inputDict.NestedProperty.NestedProperty1 = $inputDict.NestedProperty
        $inputDict.Property1.SelfRef = $inputDict.Property1

        $outDict = Remove-SelfReferences($inputDict)
        $outDict.Property1.PSObject.Properties.name -match "SelfRef" | Should -Be $false
        $outDict.NestedProperty.PSObject.Properties.name -match "NestedProperty1" | Should -Be $false

        $outDict.Property1.OtherProp | Should -Be 5
        $outDict.NestedProperty.NestedProperty2.DeeplyNestedProperty.Deep3 | Should -Be "DeepValue"
        $outDict.NestedProperty.NestedProperty2.DeeplyNestedProperty.Deep4 | Should -Be "Deep4"


    }
    It "Deep self-reference" {
    # (Case 4): Outputs dict containing a self-references in a deeper level. Expect: self-reference removed, otherwise no change.
        $inputDict = [PSCustomObject]@{
            Property1 = "Value1"
            NestedProperty = [PSCustomObject]@{
                NestedProperty1 = "NestedValue1"
                NestedProperty2 = [PSCustomObject]@{
                    DeeplyNestedProperty = [PSCustomObject]@{
                        Deep3 = "DeepValue"
                        Deep4 = "Deep4"
                        }
                    }
                }
            }

        $inputDict.NestedProperty.NestedProperty2.DeeplyNestedProperty.Deep4 = $inputDict.NestedProperty

        $outDict = Remove-SelfReferences($inputDict)
        $outDict.NestedProperty.NestedProperty2.DeeplyNestedProperty.PSObject.Properties.name -match "Deep4" | Should -Be $false

        $outDict.Property1 | Should -Be "Value1"
        $outDict.NestedProperty.NestedProperty1 | Should -Be "NestedValue1"
        $outDict.NestedProperty.NestedProperty2.DeeplyNestedProperty.Deep3 | Should -Be "DeepValue"
    }
}

# ================================================================================
# UCP (Unified Connector Platform / ConnectUs) param interpolation tests.
# Parity with CommonServerPython_test.py UCP suites (TestUcpInterpolation,
# TestUcpInterpolationPassthroughDeep, get_ucp_credentials / invalidate tests).
# ================================================================================

Describe "Test ConvertFrom-UcpParamMap" {
    It "Parses a canonical comma-separated string into ordered pairs" {
        $pairs = ConvertFrom-UcpParamMap -ParamMap 'username:credentials.identifier,password:credentials.password'
        @($pairs).Count | Should -Be 2
        $pairs[0].FieldId | Should -Be 'username'
        $pairs[0].Destination | Should -Be 'credentials.identifier'
        $pairs[1].FieldId | Should -Be 'password'
        $pairs[1].Destination | Should -Be 'credentials.password'
    }

    It "Skips empty and malformed (no-colon) entries" {
        $pairs = ConvertFrom-UcpParamMap -ParamMap 'good:dest, ,nocolon,empty:'
        @($pairs).Count | Should -Be 1
        $pairs[0].FieldId | Should -Be 'good'
        $pairs[0].Destination | Should -Be 'dest'
    }

    It "Returns empty for `$null input" {
        @(ConvertFrom-UcpParamMap -ParamMap $null).Count | Should -Be 0
    }

    It "Returns empty for empty-string input" {
        @(ConvertFrom-UcpParamMap -ParamMap '').Count | Should -Be 0
    }

    It "Trims whitespace around field id and destination" {
        $pairs = ConvertFrom-UcpParamMap -ParamMap ' a : b.c '
        @($pairs).Count | Should -Be 1
        $pairs[0].FieldId | Should -Be 'a'
        $pairs[0].Destination | Should -Be 'b.c'
    }

    It "Splits only on the first colon (dotted destinations preserved)" {
        $pairs = ConvertFrom-UcpParamMap -ParamMap 'tenant:credentials.connection.tenant.slug'
        $pairs[0].FieldId | Should -Be 'tenant'
        $pairs[0].Destination | Should -Be 'credentials.connection.tenant.slug'
    }
}

Describe "Test Set-UcpByPath" {
    It "Places a value at a single-segment path" {
        $target = [ordered]@{}
        Set-UcpByPath -Target $target -Path 'dest' -Value 'v'
        $target['dest'] | Should -Be 'v'
    }

    It "Creates intermediate dicts for a dotted path" {
        $target = [ordered]@{}
        Set-UcpByPath -Target $target -Path 'credentials.identifier' -Value 'alice'
        $target['credentials']['identifier'] | Should -Be 'alice'
    }

    It "Folds two paths sharing a parent into one nested dict" {
        $target = [ordered]@{}
        Set-UcpByPath -Target $target -Path 'credentials.identifier' -Value 'alice'
        Set-UcpByPath -Target $target -Path 'credentials.password' -Value 's3cr3t'
        $target['credentials']['identifier'] | Should -Be 'alice'
        $target['credentials']['password'] | Should -Be 's3cr3t'
    }

    It "Empty path is a no-op (existing keys preserved)" {
        $target = [ordered]@{ keep = 1 }
        Set-UcpByPath -Target $target -Path '' -Value 'x'
        $target['keep'] | Should -Be 1
        $target.Contains('') | Should -Be $false
    }

    It "Overwrites a non-dict intermediate with a dict" {
        $target = [ordered]@{ a = 'scalar' }
        Set-UcpByPath -Target $target -Path 'a.b' -Value 2
        $target['a']['b'] | Should -Be 2
    }
}

Describe "Test Merge-UcpDeep" {
    It "Recursively merges nested dicts, preserving siblings" {
        $target = [ordered]@{ credentials = [ordered]@{ identifier = 'original'; password = 'old'; field3 = 'value' } }
        $source = [ordered]@{ credentials = [ordered]@{ identifier = 'new'; password = 'fresh' } }
        Merge-UcpDeep -Target $target -Source $source | Out-Null
        $target['credentials']['identifier'] | Should -Be 'new'
        $target['credentials']['password'] | Should -Be 'fresh'
        $target['credentials']['field3'] | Should -Be 'value'
    }

    It "Incoming value wins on a scalar conflict" {
        $target = [ordered]@{ a = 1; b = 2 }
        $source = [ordered]@{ a = 99 }
        Merge-UcpDeep -Target $target -Source $source | Out-Null
        $target['a'] | Should -Be 99
        $target['b'] | Should -Be 2
    }

    It "A dict overwrites a scalar" {
        $target = [ordered]@{ a = 'scalar' }
        $source = [ordered]@{ a = [ordered]@{ nested = 1 } }
        Merge-UcpDeep -Target $target -Source $source | Out-Null
        $target['a']['nested'] | Should -Be 1
    }

    It "A scalar overwrites a dict" {
        $target = [ordered]@{ a = [ordered]@{ nested = 1 } }
        $source = [ordered]@{ a = 'scalar' }
        Merge-UcpDeep -Target $target -Source $source | Out-Null
        $target['a'] | Should -Be 'scalar'
    }

    It "Adds keys present only in the source" {
        $target = [ordered]@{ a = 1 }
        $source = [ordered]@{ b = 2 }
        Merge-UcpDeep -Target $target -Source $source | Out-Null
        $target['a'] | Should -Be 1
        $target['b'] | Should -Be 2
    }
}

Describe "Test Select-UcpProfiles" {
    It "Selects the single profile matching the capability" {
        $profiles = @(
            [ordered]@{ capability = 'cap-a'; method_unique_id = 'A' },
            [ordered]@{ capability = 'cap-b'; method_unique_id = 'B' }
        )
        $result = @(Select-UcpProfiles -Profiles $profiles -Capability 'cap-a')
        $result.Count | Should -Be 1
        $result[0]['method_unique_id'] | Should -Be 'A'
    }

    It "Selects all profiles matching the capability" {
        $profiles = @(
            [ordered]@{ capability = 'cap-x'; method_unique_id = 'A' },
            [ordered]@{ capability = 'cap-x'; method_unique_id = 'B' }
        )
        $result = @(Select-UcpProfiles -Profiles $profiles -Capability 'cap-x')
        $result.Count | Should -Be 2
        $result[0]['method_unique_id'] | Should -Be 'A'
        $result[1]['method_unique_id'] | Should -Be 'B'
    }

    It "Falls back to the first profile with an interpolation_mapping when no capability matches" {
        $profiles = @(
            [ordered]@{ capability = 'other'; method_unique_id = 'A' },
            [ordered]@{ capability = 'other'; method_unique_id = 'B'; metadata = [ordered]@{ xsoar = [ordered]@{ interpolation_mapping = 'u:x' } } }
        )
        $result = @(Select-UcpProfiles -Profiles $profiles -Capability 'no-match')
        $result.Count | Should -Be 1
        $result[0]['method_unique_id'] | Should -Be 'B'
    }

    It "Returns empty for an empty profile list" {
        @(Select-UcpProfiles -Profiles @() -Capability 'cap').Count | Should -Be 0
    }
}

Describe "Test ConvertFrom-UcpCredentials" {
    It "Flattens a plain envelope (type -> inner dict)" {
        $envelope = [ordered]@{ type = 'plain'; plain = [ordered]@{ username = 'alice'; password = 's3cr3t' } }
        $flat = ConvertFrom-UcpCredentials -Credentials $envelope
        $flat['username'] | Should -Be 'alice'
        $flat['password'] | Should -Be 's3cr3t'
    }

    It "Descends into the 'parameters' sub-dict for a passthrough envelope" {
        $envelope = [ordered]@{ type = 'passthrough'; passthrough = [ordered]@{ parameters = [ordered]@{ username = 'bob'; token = 'tok' } } }
        $flat = ConvertFrom-UcpCredentials -Credentials $envelope
        $flat['username'] | Should -Be 'bob'
        $flat['token'] | Should -Be 'tok'
    }
}

Describe "Test Resolve-UcpCapability" {
    It "Maps a known command to its capability" {
        Resolve-UcpCapability -Command 'fetch-incidents' | Should -Be 'fetch-issues'
    }

    It "Falls back to the default capability for an unknown command" {
        Resolve-UcpCapability -Command 'some-random-command' | Should -Be 'automation-and-remediation'
    }
}

Describe "Test Build-UcpParams" {
    It "Folds flat fields into a credentials dict via interpolation_mapping" {
        Mock Get-UcpCredentials { return [ordered]@{ type = 'plain'; plain = [ordered]@{ username = 'alice'; password = 's3cr3t' } } }
        $meta = [ordered]@{
            connectionProfiles = @(
                [ordered]@{
                    capability        = 'automation-and-remediation'
                    method_unique_id  = 'A'
                    metadata          = [ordered]@{ xsoar = [ordered]@{ interpolation_mapping = 'username:credentials.identifier,password:credentials.password' } }
                }
            )
        }
        $result = Build-UcpParams -ConnectorMetadata $meta -Capability 'automation-and-remediation'
        $result['credentials']['identifier'] | Should -Be 'alice'
        $result['credentials']['password'] | Should -Be 's3cr3t'
    }

    It "Only interpolates profiles matching the capability" {
        Mock Get-UcpCredentials { return [ordered]@{ type = 'plain'; plain = [ordered]@{ k = 'COLLECTOR' } } }
        $meta = [ordered]@{
            connectionProfiles = @(
                [ordered]@{ capability = 'cap-a'; method_unique_id = 'A'; metadata = [ordered]@{ xsoar = [ordered]@{ interpolation_mapping = 'u:credentials.identifier' } } },
                [ordered]@{ capability = 'cap-b'; method_unique_id = 'B'; metadata = [ordered]@{ xsoar = [ordered]@{ interpolation_mapping = 'k:credentials.password' } } }
            )
        }
        $result = Build-UcpParams -ConnectorMetadata $meta -Capability 'cap-b'
        $result['credentials']['password'] | Should -Be 'COLLECTOR'
        $result['credentials'].Contains('identifier') | Should -Be $false
    }

    It "Merges values from multiple active profiles" {
        Mock Get-UcpCredentials {
            param($MethodUniqueId, $Body)
            if ($MethodUniqueId -eq 'A') { return [ordered]@{ type = 'plain'; plain = [ordered]@{ u = 'alice' } } }
            return [ordered]@{ type = 'plain'; plain = [ordered]@{ p = 'pw' } }
        }
        $meta = [ordered]@{
            connectionProfiles = @(
                [ordered]@{ capability = 'x'; method_unique_id = 'A'; metadata = [ordered]@{ xsoar = [ordered]@{ interpolation_mapping = 'u:credentials.identifier' } } },
                [ordered]@{ capability = 'x'; method_unique_id = 'B'; metadata = [ordered]@{ xsoar = [ordered]@{ interpolation_mapping = 'p:credentials.password' } } }
            )
        }
        $result = Build-UcpParams -ConnectorMetadata $meta -Capability 'x'
        $result['credentials']['identifier'] | Should -Be 'alice'
        $result['credentials']['password'] | Should -Be 'pw'
    }

    It "Last profile wins on a destination conflict" {
        Mock Get-UcpCredentials {
            param($MethodUniqueId, $Body)
            if ($MethodUniqueId -eq 'A') { return [ordered]@{ type = 'plain'; plain = [ordered]@{ v = 'first' } } }
            return [ordered]@{ type = 'plain'; plain = [ordered]@{ v = 'second' } }
        }
        $meta = [ordered]@{
            connectionProfiles = @(
                [ordered]@{ capability = 'x'; method_unique_id = 'A'; metadata = [ordered]@{ xsoar = [ordered]@{ interpolation_mapping = 'v:dest' } } },
                [ordered]@{ capability = 'x'; method_unique_id = 'B'; metadata = [ordered]@{ xsoar = [ordered]@{ interpolation_mapping = 'v:dest' } } }
            )
        }
        $result = Build-UcpParams -ConnectorMetadata $meta -Capability 'x'
        $result['dest'] | Should -Be 'second'
    }

    It "Skips fields with a missing (null) value" {
        Mock Get-UcpCredentials { return [ordered]@{ type = 'plain'; plain = [ordered]@{ present = 1 } } }
        $meta = [ordered]@{
            connectionProfiles = @(
                [ordered]@{ capability = 'x'; method_unique_id = 'A'; metadata = [ordered]@{ xsoar = [ordered]@{ interpolation_mapping = 'present:a,absent:b' } } }
            )
        }
        $result = Build-UcpParams -ConnectorMetadata $meta -Capability 'x'
        $result['a'] | Should -Be 1
        $result.Contains('b') | Should -Be $false
    }

    It "Returns empty when metadata is missing" {
        $result = Build-UcpParams -ConnectorMetadata $null -Capability 'x'
        @($result.Keys).Count | Should -Be 0
    }

    It "Returns empty when no profile carries an interpolation_mapping" {
        $meta = [ordered]@{ connectionProfiles = @([ordered]@{ capability = 'x'; method_unique_id = 'A' }) }
        $result = Build-UcpParams -ConnectorMetadata $meta -Capability 'x'
        @($result.Keys).Count | Should -Be 0
    }

    It "Resolves the capability automatically when not provided" {
        Mock Get-UcpCredentials { return [ordered]@{ type = 'plain'; plain = [ordered]@{ k = 'tok' } } }
        Mock Resolve-UcpCapability { return 'fetch-issues' }
        $meta = [ordered]@{
            connectionProfiles = @(
                [ordered]@{ capability = 'fetch-issues'; method_unique_id = 'A'; metadata = [ordered]@{ xsoar = [ordered]@{ interpolation_mapping = 'k:credentials.password' } } }
            )
        }
        $result = Build-UcpParams -ConnectorMetadata $meta -Capability $null
        $result['credentials']['password'] | Should -Be 'tok'
    }

    It "Performs deep, multi-level interpolation for a passthrough profile" {
        $mapping = (
            "username:credentials.identifier," +
            "app_password:credentials.password," +
            "bitbucket_email:credentials.metadata.email," +
            "account_id:credentials.metadata.accountId," +
            "api_token:credentials.metadata.token," +
            "client_key:credentials.oauth.consumerKey," +
            "client_secret:credentials.oauth.consumerSecret," +
            "base_url:credentials.connection.host," +
            "auth_scheme:credentials.connection.scheme," +
            "tenant:credentials.connection.tenant.slug"
        )
        Mock Get-UcpCredentials {
            return [ordered]@{
                type        = 'passthrough'
                passthrough = [ordered]@{
                    parameters = [ordered]@{
                        username        = 'alice'
                        app_password    = 's3cr3t'
                        bitbucket_email = 'you@example.com'
                        account_id      = '557058:abc'
                        api_token       = 'tok-123'
                        client_key      = 'ck'
                        client_secret   = 'cs'
                        base_url        = 'host-value'
                        auth_scheme     = 'basic'
                        tenant          = 'my-tenant'
                    }
                }
            }
        }
        $meta = [ordered]@{
            connectionProfiles = @(
                [ordered]@{
                    capability       = 'automation-and-remediation'
                    method_unique_id = 'pt-method-1'
                    type             = 'passthrough'
                    metadata         = [ordered]@{ xsoar = [ordered]@{ interpolation_mapping = $mapping } }
                }
            )
        }
        $result = Build-UcpParams -ConnectorMetadata $meta -Capability 'automation-and-remediation'
        $creds = $result['credentials']
        $creds['identifier'] | Should -Be 'alice'
        $creds['password'] | Should -Be 's3cr3t'
        $creds['metadata']['email'] | Should -Be 'you@example.com'
        $creds['metadata']['accountId'] | Should -Be '557058:abc'
        $creds['metadata']['token'] | Should -Be 'tok-123'
        $creds['oauth']['consumerKey'] | Should -Be 'ck'
        $creds['oauth']['consumerSecret'] | Should -Be 'cs'
        $creds['connection']['host'] | Should -Be 'host-value'
        $creds['connection']['scheme'] | Should -Be 'basic'
        $creds['connection']['tenant']['slug'] | Should -Be 'my-tenant'
    }
}

Describe "Test Get-UcpCredentials cache" {
    BeforeEach {
        $script:UcpCredsCache.Clear()
    }
    AfterEach {
        $script:UcpCredsCache.Clear()
    }

    It "Serves a fresh (far-future expiry) cache entry without re-fetching" {
        $creds = [ordered]@{ type = 'oauth2'; oauth2 = [ordered]@{ access_token = 'cached' } }
        $future = ([double][System.DateTimeOffset]::UtcNow.ToUnixTimeSeconds()) + 3600
        $script:UcpCredsCache['abc123'] = @{ result = $creds; expiry = $future }
        $result = Get-UcpCredentials -MethodUniqueId 'abc123'
        $result['oauth2']['access_token'] | Should -Be 'cached'
    }

    It "Serves a `$null-expiry entry indefinitely" {
        $creds = [ordered]@{ type = 'api_key'; api_key = [ordered]@{ key = 'k' } }
        $script:UcpCredsCache['key123'] = @{ result = $creds; expiry = $null }
        $result = Get-UcpCredentials -MethodUniqueId 'key123'
        $result['api_key']['key'] | Should -Be 'k'
    }
}

Describe "Test Clear-UcpCredentialEntry" {
    BeforeEach {
        $script:UcpCredsCache.Clear()
    }
    AfterEach {
        $script:UcpCredsCache.Clear()
    }

    It "Removes the specified cache entry" {
        $script:UcpCredsCache['abc123'] = @{ result = @{}; expiry = $null }
        Clear-UcpCredentialEntry -MethodUniqueId 'abc123'
        $script:UcpCredsCache.ContainsKey('abc123') | Should -Be $false
    }

    It "Does not raise for a non-existent key" {
        { Clear-UcpCredentialEntry -MethodUniqueId 'nope' } | Should -Not -Throw
    }

    It "Only removes the specified key, leaving others intact" {
        $script:UcpCredsCache['key1'] = @{ result = @{}; expiry = $null }
        $script:UcpCredsCache['key2'] = @{ result = @{}; expiry = $null }
        Clear-UcpCredentialEntry -MethodUniqueId 'key1'
        $script:UcpCredsCache.ContainsKey('key1') | Should -Be $false
        $script:UcpCredsCache.ContainsKey('key2') | Should -Be $true
    }
}

Describe "Test Invoke-UcpParamInterpolation" {
    BeforeEach {
        $script:UcpAuthParamsInjected = $false
    }
    AfterEach {
        $script:UcpAuthParamsInjected = $false
    }

    It "Sets the injected flag and merges interpolated params into the merge target" {
        Mock Build-UcpParams { return [ordered]@{ credentials = [ordered]@{ identifier = 'alice'; password = 's3cr3t' } } }
        $meta = [ordered]@{ connectionProfiles = @([ordered]@{ capability = 'automation-and-remediation'; method_unique_id = 'A' }) }

        $result = Invoke-UcpParamInterpolation -ConnectorMetadata $meta

        $result | Should -Be $true
        $script:UcpAuthParamsInjected | Should -Be $true
        # Merge target is $demisto._ucpParamsMergeTarget(); the interpolated
        # credentials must be observable through Params().
        $demisto.Params()['credentials']['identifier'] | Should -Be 'alice'
        $demisto.Params()['credentials']['password'] | Should -Be 's3cr3t'
    }

    It "Is a no-op (flag stays false) when no params are produced" {
        Mock Build-UcpParams { return [ordered]@{} }
        $meta = [ordered]@{ connectionProfiles = @([ordered]@{ capability = 'automation-and-remediation'; method_unique_id = 'A' }) }

        $result = Invoke-UcpParamInterpolation -ConnectorMetadata $meta

        $result | Should -Be $false
        $script:UcpAuthParamsInjected | Should -Be $false
    }
}