Tests/sessions/Connect-RdpSession.Tests.ps1

#Requires -Version 5.1

BeforeAll {
    <#
.SYNOPSIS
    Test suite for Connect-RdpSession v1.2.0
 
.DESCRIPTION
    Validates Connect-RdpSession behavior: shadow session initiation via mstsc.exe,
    session verification via the private Invoke-QwinstaQuery helper, Control and
    View modes, ShouldProcess support (-WhatIf / -Confirm), and error handling for
    missing executables, failed launches, and non-zero exit codes.
 
    All native command calls are isolated: Invoke-QwinstaQuery and Start-Process
    are mocked at module scope. No real qwinsta.exe or mstsc.exe is invoked.
 
.NOTES
    Author: Franck SALLET
    Version: 2.0.0
    Last Modified: 2026-03-11
    Requires: PowerShell 5.1+, Pester 5.x
    Permissions: None (all external calls mocked)
#>

    $script:modulePath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\PSWinOps.psd1'
    Import-Module -Name $script:modulePath -Force -ErrorAction Stop

    # ---------------------------------------------------------------------------
    # Shared qwinsta-formatted output fixtures
    # ---------------------------------------------------------------------------

    # One active session -- session ID 2
    $script:qwinstaSession2 = @(
        ' SESSIONNAME USERNAME ID STATE',
        ' rdp-tcp#0 adm-fsallet 2 Active'
    )

    # One active session -- session ID 3 (used in pipeline tests)
    $script:qwinstaSession3 = @(
        ' SESSIONNAME USERNAME ID STATE',
        ' rdp-tcp#1 domain\helpdesk 3 Active'
    )
}

Describe -Name 'Connect-RdpSession' -Fixture {

    BeforeEach {
        # -----------------------------------------------------------------------
        # Default happy-path mocks -- overridden per context as needed.
        # Test-Path returns $true so begin{} never throws on missing mstsc.exe.
        # Invoke-QwinstaQuery returns session 2 with ExitCode 0.
        # Start-Process returns ExitCode 0 (shadow session ended normally).
        # -----------------------------------------------------------------------
        Mock -CommandName 'Test-Path' -ModuleName 'PSWinOps' -MockWith { $true }

        Mock -CommandName 'Invoke-QwinstaQuery' -ModuleName 'PSWinOps' -MockWith {
            [PSCustomObject]@{
                Output   = $script:qwinstaSession2
                ExitCode = 0
            }
        }

        Mock -CommandName 'Start-Process' -ModuleName 'PSWinOps' -MockWith {
            [PSCustomObject]@{ ExitCode = 0 }
        }
    }

    # ===========================================================================
    Context -Name 'When entering a session successfully in Control mode' -Fixture {

        It -Name 'Should return a result object with Success set to true' -Test {
            $result = Connect-RdpSession -SessionID 2 -Confirm:$false
            $result.Success | Should -Be $true
            $result.ExitCode | Should -Be 0
        }

        It -Name 'Should report Action as Shadow' -Test {
            $result = Connect-RdpSession -SessionID 2 -Confirm:$false
            $result.Action | Should -Be 'Shadow'
        }

        It -Name 'Should default ControlMode to Control' -Test {
            $result = Connect-RdpSession -SessionID 2 -Confirm:$false
            $result.ControlMode | Should -Be 'Control'
        }

        It -Name 'Should include ComputerName in the result object' -Test {
            $result = Connect-RdpSession -SessionID 2 -ComputerName 'ecrmut-ad-02' -Confirm:$false
            $result.ComputerName | Should -Be 'ecrmut-ad-02'
        }

        It -Name 'Should include SessionID in the result object' -Test {
            $result = Connect-RdpSession -SessionID 2 -Confirm:$false
            $result.SessionID | Should -Be 2
        }

        It -Name 'Should invoke Start-Process to launch the shadow window' -Test {
            Connect-RdpSession -SessionID 2 -Confirm:$false
            Should -Invoke -CommandName 'Start-Process' -ModuleName 'PSWinOps' -Times 1 -Exactly
        }

        It -Name 'Should verify session existence via Invoke-QwinstaQuery before launching' -Test {
            Connect-RdpSession -SessionID 2 -Confirm:$false
            Should -Invoke -CommandName 'Invoke-QwinstaQuery' -ModuleName 'PSWinOps' -Times 1 -Exactly
        }

        It -Name 'Should pass /shadow and /v arguments to mstsc.exe' -Test {
            Connect-RdpSession -SessionID 2 -ComputerName 'ecrmut-ad-02' -Confirm:$false
            Should -Invoke -CommandName 'Start-Process' -ModuleName 'PSWinOps' -Times 1 -Exactly `
                -ParameterFilter {
                ($ArgumentList -contains '/shadow:2') -and
                ($ArgumentList -contains '/v:ecrmut-ad-02')
            }
        }

        It -Name 'Should pass /control argument to mstsc.exe in Control mode' -Test {
            Connect-RdpSession -SessionID 2 -Confirm:$false
            Should -Invoke -CommandName 'Start-Process' -ModuleName 'PSWinOps' -Times 1 -Exactly `
                -ParameterFilter { $ArgumentList -contains '/control' }
        }
    }

    # ===========================================================================
    Context -Name 'When entering a session in View mode' -Fixture {

        It -Name 'Should report ControlMode as View' -Test {
            $result = Connect-RdpSession -SessionID 2 -ControlMode View -Confirm:$false
            $result.ControlMode | Should -Be 'View'
        }

        It -Name 'Should not pass /control argument to mstsc.exe in View mode' -Test {
            Connect-RdpSession -SessionID 2 -ControlMode View -Confirm:$false
            Should -Invoke -CommandName 'Start-Process' -ModuleName 'PSWinOps' -Times 1 -Exactly `
                -ParameterFilter { -not ($ArgumentList -contains '/control') }
        }
    }

    # ===========================================================================
    Context -Name 'When the target session does not exist' -Fixture {
        # Default mock returns only session 2 -- session 999 is intentionally absent.

        It -Name 'Should write an error and not launch mstsc.exe' -Test {
            Connect-RdpSession -SessionID 999 -Confirm:$false -ErrorAction SilentlyContinue
            Should -Invoke -CommandName 'Start-Process' -ModuleName 'PSWinOps' -Times 0 -Exactly
        }

        It -Name 'Should return no output when the session is not found' -Test {
            $result = Connect-RdpSession -SessionID 999 -Confirm:$false -ErrorAction SilentlyContinue
            $result | Should -BeNullOrEmpty
        }
    }

    # ===========================================================================
    Context -Name 'When qwinsta reports an error (non-zero exit code)' -Fixture {

        BeforeEach {
            Mock -CommandName 'Invoke-QwinstaQuery' -ModuleName 'PSWinOps' -MockWith {
                [PSCustomObject]@{
                    Output   = @('[ERROR] Access denied to remote server')
                    ExitCode = 5
                }
            }
        }

        It -Name 'Should write an error and not launch mstsc.exe' -Test {
            Connect-RdpSession -SessionID 2 -Confirm:$false -ErrorAction SilentlyContinue
            Should -Invoke -CommandName 'Start-Process' -ModuleName 'PSWinOps' -Times 0 -Exactly
        }

        It -Name 'Should return no output on qwinsta failure' -Test {
            $result = Connect-RdpSession -SessionID 2 -Confirm:$false -ErrorAction SilentlyContinue
            $result | Should -BeNullOrEmpty
        }
    }

    # ===========================================================================
    Context -Name 'When ShouldProcess is declined via WhatIf' -Fixture {
        # Session verification runs OUTSIDE ShouldProcess -- Invoke-QwinstaQuery is
        # always called. Start-Process is INSIDE ShouldProcess -- never called with -WhatIf.

        It -Name 'Should not invoke Start-Process when WhatIf is specified' -Test {
            Connect-RdpSession -SessionID 2 -WhatIf
            Should -Invoke -CommandName 'Start-Process' -ModuleName 'PSWinOps' -Times 0 -Exactly
        }

        It -Name 'Should still verify session existence via qwinsta when WhatIf is specified' -Test {
            Connect-RdpSession -SessionID 2 -WhatIf
            Should -Invoke -CommandName 'Invoke-QwinstaQuery' -ModuleName 'PSWinOps' -Times 1 -Exactly
        }
    }

    # ===========================================================================
    Context -Name 'When mstsc.exe exits with a non-zero exit code' -Fixture {

        BeforeEach {
            Mock -CommandName 'Start-Process' -ModuleName 'PSWinOps' -MockWith {
                [PSCustomObject]@{ ExitCode = 1 }
            }
        }

        It -Name 'Should return a result object with Success set to false' -Test {
            $result = Connect-RdpSession -SessionID 2 -Confirm:$false
            $result | Should -Not -BeNullOrEmpty
            $result.Success | Should -Be $false
        }

        It -Name 'Should include the non-zero exit code in the result object' -Test {
            $result = Connect-RdpSession -SessionID 2 -Confirm:$false
            $result.ExitCode | Should -Be 1
        }
    }

    # ===========================================================================
    Context -Name 'When Start-Process throws an exception' -Fixture {

        BeforeEach {
            Mock -CommandName 'Start-Process' -ModuleName 'PSWinOps' -MockWith {
                throw 'Test-induced Start-Process failure'
            }
        }

        It -Name 'Should write an error without propagating the exception to the caller' -Test {
            { Connect-RdpSession -SessionID 2 -Confirm:$false -ErrorAction SilentlyContinue } |
                Should -Not -Throw
        }
    }

    # ===========================================================================
    Context -Name 'When processing pipeline input from Get-ActiveRdpSession' -Fixture {

        It -Name 'Should accept SessionID and ComputerName from pipeline by property name' -Test {
            Mock -CommandName 'Invoke-QwinstaQuery' -ModuleName 'PSWinOps' -MockWith {
                [PSCustomObject]@{
                    Output   = $script:qwinstaSession3
                    ExitCode = 0
                }
            }
            $pipelineInput = [PSCustomObject]@{ SessionID = 3; ComputerName = 'SRV01' }
            $result = $pipelineInput | Connect-RdpSession -Confirm:$false
            $result.SessionID | Should -Be 3
            $result.ComputerName | Should -Be 'SRV01'
        }
    }

    # ===========================================================================
    Context -Name 'When NoUserPrompt switch is specified' -Fixture {

        It -Name 'Should pass /noConsentPrompt to mstsc.exe' -Test {
            Connect-RdpSession -SessionID 2 -NoUserPrompt -Confirm:$false
            Should -Invoke -CommandName 'Start-Process' -ModuleName 'PSWinOps' -Times 1 -Exactly `
                -ParameterFilter { $ArgumentList -contains '/noConsentPrompt' }
        }

        It -Name 'Should complete successfully when NoUserPrompt is specified' -Test {
            $result = Connect-RdpSession -SessionID 2 -NoUserPrompt -Confirm:$false
            $result.Success | Should -Be $true
        }
    }

    # ===========================================================================
    Context -Name 'When a Credential is specified' -Fixture {

        It -Name 'Should forward the credential to Start-Process' -Test {
            $securePassword = New-Object System.Security.SecureString
            'TestPassword1!'.ToCharArray() | ForEach-Object { $securePassword.AppendChar($_) }
            $testCred = [System.Management.Automation.PSCredential]::new(
                'DOMAIN\testuser',
                $securePassword
            )
            Connect-RdpSession -SessionID 2 -Credential $testCred -Confirm:$false
            Should -Invoke -CommandName 'Start-Process' -ModuleName 'PSWinOps' -Times 1 -Exactly `
                -ParameterFilter { $null -ne $Credential }
        }
    }

    # ===========================================================================
    Context -Name 'When mstsc.exe is not found on the system' -Fixture {

        BeforeEach {
            Mock -CommandName 'Test-Path' -ModuleName 'PSWinOps' -MockWith { $false }
        }

        It -Name 'Should throw before attempting session verification' -Test {
            { Connect-RdpSession -SessionID 2 } | Should -Throw
            Should -Invoke -CommandName 'Invoke-QwinstaQuery' -ModuleName 'PSWinOps' -Times 0 -Exactly
        }
    }
}