Tests/sessions/Disconnect-RdpSession.Tests.ps1

#Requires -Version 5.1
#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' }

[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
    'PSAvoidUsingConvertToSecureStringWithPlainText',
    '',
    Justification = 'Test fixture only -- not a real credential'
)]
param()

BeforeAll -Scriptblock {
    $script:functionPath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\Public\sessions\Disconnect-RdpSession.ps1'
    . $script:functionPath
}

Describe -Name 'Disconnect-RdpSession' -Fixture {

    BeforeAll -Scriptblock {
        # Default mock: tsdiscon.exe exists
        Mock -CommandName 'Get-Command' -MockWith {
            return [PSCustomObject]@{
                Name        = 'tsdiscon.exe'
                CommandType = 'Application'
                Source      = 'C:\Windows\System32\tsdiscon.exe'
            }
        } -ParameterFilter { $Name -eq 'tsdiscon.exe' }

        # Default mock: Invoke-Command returns exit code 0 (success)
        Mock -CommandName 'Invoke-Command' -MockWith { return 0 }
    }

    Context -Name 'When disconnecting a local session' -Fixture {

        It -Name 'Should return a PSCustomObject with all expected properties' -Test {
            $result = Disconnect-RdpSession -SessionID 3 -Confirm:$false
            $result | Should -BeOfType -ExpectedType ([PSCustomObject])
            $result.PSObject.Properties.Name | Should -Contain -ExpectedValue 'ComputerName'
            $result.PSObject.Properties.Name | Should -Contain -ExpectedValue 'SessionID'
            $result.PSObject.Properties.Name | Should -Contain -ExpectedValue 'Action'
            $result.PSObject.Properties.Name | Should -Contain -ExpectedValue 'Success'
            $result.PSObject.Properties.Name | Should -Contain -ExpectedValue 'Timestamp'
        }

        It -Name 'Should return Success true when tsdiscon exits with code 0' -Test {
            $result = Disconnect-RdpSession -SessionID 3 -Confirm:$false
            $result.Success | Should -BeTrue
            $result.Action | Should -Be -ExpectedValue 'Disconnect'
            $result.SessionID | Should -Be -ExpectedValue 3
        }

        It -Name 'Should default ComputerName to the local machine name' -Test {
            $result = Disconnect-RdpSession -SessionID 3 -Confirm:$false
            $result.ComputerName | Should -Be -ExpectedValue $env:COMPUTERNAME
        }

        It -Name 'Should invoke Invoke-Command without ComputerName for local sessions' -Test {
            Disconnect-RdpSession -SessionID 3 -Confirm:$false
            Should -Invoke -CommandName 'Invoke-Command' -Times 1 -Exactly -ParameterFilter {
                $null -eq $ComputerName
            }
        }
    }

    Context -Name 'When disconnecting a remote session without credentials' -Fixture {

        It -Name 'Should return Success true with the remote ComputerName' -Test {
            $result = Disconnect-RdpSession -ComputerName 'SRV-REMOTE-01' -SessionID 5 -Confirm:$false
            $result.Success | Should -BeTrue
            $result.ComputerName | Should -Be -ExpectedValue 'SRV-REMOTE-01'
            $result.SessionID | Should -Be -ExpectedValue 5
        }

        It -Name 'Should pass ComputerName to Invoke-Command for remote sessions' -Test {
            Disconnect-RdpSession -ComputerName 'SRV-REMOTE-01' -SessionID 5 -Confirm:$false
            Should -Invoke -CommandName 'Invoke-Command' -Times 1 -Exactly -ParameterFilter {
                $ComputerName -eq 'SRV-REMOTE-01'
            }
        }
    }

    Context -Name 'When credentials are provided for a remote session' -Fixture {

        BeforeAll -Scriptblock {
            $script:testCredential = [PSCredential]::new(
                'DOMAIN\testuser',
                (ConvertTo-SecureString -String 'FakeP@ss1' -AsPlainText -Force)
            )
        }

        It -Name 'Should pass Credential to Invoke-Command' -Test {
            Disconnect-RdpSession -ComputerName 'SRV-REMOTE-01' -SessionID 3 -Credential $script:testCredential -Confirm:$false
            Should -Invoke -CommandName 'Invoke-Command' -Times 1 -Exactly -ParameterFilter {
                $null -ne $Credential -and $ComputerName -eq 'SRV-REMOTE-01'
            }
        }

        It -Name 'Should return a valid result object with credentials' -Test {
            $result = Disconnect-RdpSession -ComputerName 'SRV-REMOTE-01' -SessionID 3 -Credential $script:testCredential -Confirm:$false
            $result.Success | Should -BeTrue
            $result.ComputerName | Should -Be -ExpectedValue 'SRV-REMOTE-01'
        }
    }

    Context -Name 'When multiple sessions are provided via pipeline' -Fixture {

        It -Name 'Should process each session and return multiple results' -Test {
            $script:pipelineInput = @(
                [PSCustomObject]@{ ComputerName = 'SRV01'; SessionID = 3 }
                [PSCustomObject]@{ ComputerName = 'SRV01'; SessionID = 7 }
            )
            $results = $script:pipelineInput | Disconnect-RdpSession -Confirm:$false
            $results.Count | Should -Be -ExpectedValue 2
            $results[0].SessionID | Should -Be -ExpectedValue 3
            $results[1].SessionID | Should -Be -ExpectedValue 7
        }

        It -Name 'Should call Invoke-Command once per piped session' -Test {
            $script:pipelineInput = @(
                [PSCustomObject]@{ ComputerName = 'SRV01'; SessionID = 3 }
                [PSCustomObject]@{ ComputerName = 'SRV01'; SessionID = 7 }
            )
            $script:pipelineInput | Disconnect-RdpSession -Confirm:$false
            Should -Invoke -CommandName 'Invoke-Command' -Times 2 -Exactly
        }
    }

    Context -Name 'When multiple SessionIDs are passed as an array parameter' -Fixture {

        It -Name 'Should disconnect each session ID individually' -Test {
            $results = Disconnect-RdpSession -ComputerName 'SRV01' -SessionID 2, 4, 6 -Confirm:$false
            $results.Count | Should -Be -ExpectedValue 3
            Should -Invoke -CommandName 'Invoke-Command' -Times 3 -Exactly
        }
    }

    Context -Name 'When tsdiscon.exe returns a non-zero exit code' -Fixture {

        BeforeAll -Scriptblock {
            Mock -CommandName 'Invoke-Command' -MockWith { return 1 }
        }

        It -Name 'Should return Success as false' -Test {
            $result = Disconnect-RdpSession -SessionID 3 -Confirm:$false
            $result.Success | Should -BeFalse
        }

        It -Name 'Should still return a complete result object' -Test {
            $result = Disconnect-RdpSession -SessionID 3 -Confirm:$false
            $result.ComputerName | Should -Not -BeNullOrEmpty
            $result.Action | Should -Be -ExpectedValue 'Disconnect'
            $result.Timestamp | Should -Not -BeNullOrEmpty
        }
    }

    Context -Name 'When Invoke-Command returns null exit code' -Fixture {

        BeforeAll -Scriptblock {
            Mock -CommandName 'Invoke-Command' -MockWith { return $null }
        }

        It -Name 'Should return Success as false when exit code is null' -Test {
            $result = Disconnect-RdpSession -SessionID 3 -Confirm:$false
            $result.Success | Should -BeFalse
        }
    }

    Context -Name 'When tsdiscon.exe is not found on the system' -Fixture {

        BeforeAll -Scriptblock {
            Mock -CommandName 'Get-Command' -MockWith {
                return $null
            } -ParameterFilter { $Name -eq 'tsdiscon.exe' }
        }

        It -Name 'Should throw a terminating error mentioning tsdiscon' -Test {
            { Disconnect-RdpSession -SessionID 3 -Confirm:$false } | Should -Throw -ExpectedMessage '*tsdiscon.exe*'
        }

        It -Name 'Should not attempt to invoke any disconnect command' -Test {
            { Disconnect-RdpSession -SessionID 3 -Confirm:$false } | Should -Throw
            Should -Invoke -CommandName 'Invoke-Command' -Times 0 -Exactly
        }
    }

    Context -Name 'When WhatIf is specified' -Fixture {

        It -Name 'Should not execute any disconnect command' -Test {
            Disconnect-RdpSession -ComputerName 'SRV01' -SessionID 3 -WhatIf
            Should -Invoke -CommandName 'Invoke-Command' -Times 0 -Exactly
        }

        It -Name 'Should not return any output' -Test {
            $result = Disconnect-RdpSession -ComputerName 'SRV01' -SessionID 3 -WhatIf
            $result | Should -BeNullOrEmpty
        }
    }

    Context -Name 'When a WinRM remoting error occurs' -Fixture {

        BeforeAll -Scriptblock {
            Mock -CommandName 'Invoke-Command' -MockWith {
                throw [System.Management.Automation.Remoting.PSRemotingTransportException]::new(
                    'WinRM cannot complete the operation.'
                )
            }
        }

        It -Name 'Should return Success as false without terminating the pipeline' -Test {
            $result = Disconnect-RdpSession -ComputerName 'SRV-UNREACHABLE' -SessionID 3 -Confirm:$false -ErrorAction SilentlyContinue
            $result.Success | Should -BeFalse
            $result.ComputerName | Should -Be -ExpectedValue 'SRV-UNREACHABLE'
        }

        It -Name 'Should write a non-terminating error to the error stream' -Test {
            $script:disconnectErrors = $null
            Disconnect-RdpSession -ComputerName 'SRV-UNREACHABLE' -SessionID 3 -Confirm:$false -ErrorVariable script:disconnectErrors -ErrorAction SilentlyContinue
            $script:disconnectErrors | Should -Not -BeNullOrEmpty
        }
    }

    Context -Name 'When a generic error occurs during disconnect' -Fixture {

        BeforeAll -Scriptblock {
            Mock -CommandName 'Invoke-Command' -MockWith {
                throw [System.InvalidOperationException]::new('Unexpected failure')
            }
        }

        It -Name 'Should return Success as false and continue processing' -Test {
            $result = Disconnect-RdpSession -SessionID 3 -Confirm:$false -ErrorAction SilentlyContinue
            $result.Success | Should -BeFalse
        }
    }

    Context -Name 'Parameter validation' -Fixture {

        It -Name 'Should reject an empty ComputerName' -Test {
            { Disconnect-RdpSession -ComputerName '' -SessionID 3 -Confirm:$false } | Should -Throw
        }

        It -Name 'Should reject a SessionID outside valid range' -Test {
            { Disconnect-RdpSession -SessionID -1 -Confirm:$false } | Should -Throw
            { Disconnect-RdpSession -SessionID 65537 -Confirm:$false } | Should -Throw
        }

        It -Name 'Should require SessionID as mandatory' -Test {
            { Disconnect-RdpSession -ComputerName 'SRV01' -Confirm:$false } | Should -Throw
        }
    }
}