Tests/sessions/Get-RdpSessionHistory.Tests.ps1

#Requires -Version 5.1

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

    # Mock event data
    $script:mockEventXml = @'
<Event>
  <UserData>
    <EventXML>
      <User>DOMAIN\testuser</User>
      <Address>192.168.1.100</Address>
    </EventXML>
  </UserData>
</Event>
'@


    $script:mockEventEntry = [PSCustomObject]@{
        TimeCreated = [datetime]'2026-03-11 08:00:00'
        Id          = 21
    } | Add-Member -MemberType ScriptMethod -Name 'ToXml' -Value {
        return $script:mockEventXml
    } -PassThru
}

Describe -Name 'Get-RdpSessionHistory' -Fixture {

    Context -Name 'Parameter validation' -Fixture {

        It -Name 'Should accept ComputerName from pipeline by value' -Test {
            Mock -CommandName 'Get-WinEvent' -ModuleName 'PSWinOps' -MockWith { return @() }

            { 'SRV01' | Get-RdpSessionHistory } | Should -Not -Throw

            Should -Invoke -CommandName 'Get-WinEvent' -ModuleName 'PSWinOps' -Times 1 -Exactly
        }

        It -Name 'Should accept ComputerName from pipeline by property name' -Test {
            Mock -CommandName 'Get-WinEvent' -ModuleName 'PSWinOps' -MockWith { return @() }
            $inputObject = [PSCustomObject]@{ Name = 'SRV02' }

            { $inputObject | Get-RdpSessionHistory } | Should -Not -Throw

            Should -Invoke -CommandName 'Get-WinEvent' -ModuleName 'PSWinOps' -Times 1 -Exactly
        }

        It -Name 'Should default to local computer when no ComputerName specified' -Test {
            Mock -CommandName 'Get-WinEvent' -ModuleName 'PSWinOps' -MockWith { return @() }

            Get-RdpSessionHistory

            Should -Invoke -CommandName 'Get-WinEvent' -ModuleName 'PSWinOps' -Times 1 -Exactly -ParameterFilter {
                $ComputerName -eq $env:COMPUTERNAME
            }
        }

        It -Name 'Should default StartTime to 1970-01-01' -Test {
            Mock -CommandName 'Get-WinEvent' -ModuleName 'PSWinOps' -MockWith { return @() }

            Get-RdpSessionHistory

            Should -Invoke -CommandName 'Get-WinEvent' -ModuleName 'PSWinOps' -Times 1 -Exactly -ParameterFilter {
                $FilterHashtable.StartTime -eq [datetime]'1970-01-01'
            }
        }
    }

    Context -Name 'When events are found' -Fixture {

        It -Name 'Should return PSCustomObject with correct properties' -Test {
            Mock -CommandName 'Get-WinEvent' -ModuleName 'PSWinOps' -MockWith { return @($script:mockEventEntry) }

            $result = Get-RdpSessionHistory -ComputerName 'SRV01'

            $result | Should -Not -BeNullOrEmpty
            $result | Should -BeOfType ([PSCustomObject])
            # PSTypeName est une clé réservée, pas une propriété : utiliser PSObject.TypeNames
            $result.PSObject.TypeNames[0] | Should -Be 'PSWinOps.RdpSessionHistory'
        }

        It -Name 'Should include all expected properties' -Test {
            Mock -CommandName 'Get-WinEvent' -ModuleName 'PSWinOps' -MockWith { return @($script:mockEventEntry) }

            $result = Get-RdpSessionHistory -ComputerName 'SRV01'

            $result.PSObject.Properties.Name | Should -Contain 'TimeCreated'
            $result.PSObject.Properties.Name | Should -Contain 'ComputerName'
            $result.PSObject.Properties.Name | Should -Contain 'User'
            $result.PSObject.Properties.Name | Should -Contain 'IPAddress'
            $result.PSObject.Properties.Name | Should -Contain 'Action'
            $result.PSObject.Properties.Name | Should -Contain 'EventID'
        }

        It -Name 'Should map Event ID 21 to Logon action' -Test {
            Mock -CommandName 'Get-WinEvent' -ModuleName 'PSWinOps' -MockWith { return @($script:mockEventEntry) }

            $result = Get-RdpSessionHistory -ComputerName 'SRV01'

            $result.Action | Should -Be 'Logon'
            $result.EventID | Should -Be 21
        }

        It -Name 'Should extract user and IP address from event XML' -Test {
            Mock -CommandName 'Get-WinEvent' -ModuleName 'PSWinOps' -MockWith { return @($script:mockEventEntry) }

            $result = Get-RdpSessionHistory -ComputerName 'SRV01'

            $result.User | Should -Be 'DOMAIN\testuser'
            $result.IPAddress | Should -Be '192.168.1.100'
        }

        It -Name 'Should process multiple events' -Test {
            Mock -CommandName 'Get-WinEvent' -ModuleName 'PSWinOps' -MockWith {
                return @($script:mockEventEntry, $script:mockEventEntry)
            }

            $result = Get-RdpSessionHistory -ComputerName 'SRV01'

            $result.Count | Should -Be 2
        }
    }

    Context -Name 'When no events are found' -Fixture {

        It -Name 'Should return nothing when event log is empty' -Test {
            Mock -CommandName 'Get-WinEvent' -ModuleName 'PSWinOps' -MockWith { return @() }

            $result = Get-RdpSessionHistory -ComputerName 'SRV01'

            $result | Should -BeNullOrEmpty
        }

        It -Name 'Should return nothing when events is null' -Test {
            Mock -CommandName 'Get-WinEvent' -ModuleName 'PSWinOps' -MockWith { return $null }

            $result = Get-RdpSessionHistory -ComputerName 'SRV01'

            $result | Should -BeNullOrEmpty
        }
    }

    Context -Name 'Error handling' -Fixture {

        It -Name 'Should write error on EventLogException' -Test {
            Mock -CommandName 'Get-WinEvent' -ModuleName 'PSWinOps' -MockWith {
                throw [System.Diagnostics.Eventing.Reader.EventLogException]::new('Log not found')
            }

            Get-RdpSessionHistory -ComputerName 'SRV01' -ErrorAction SilentlyContinue

            Should -Invoke -CommandName 'Get-WinEvent' -ModuleName 'PSWinOps' -Times 1 -Exactly
        }

        It -Name 'Should write error on UnauthorizedAccessException' -Test {
            Mock -CommandName 'Get-WinEvent' -ModuleName 'PSWinOps' -MockWith {
                throw [System.UnauthorizedAccessException]::new('Access denied')
            }

            Get-RdpSessionHistory -ComputerName 'SRV01' -ErrorAction SilentlyContinue

            Should -Invoke -CommandName 'Get-WinEvent' -ModuleName 'PSWinOps' -Times 1 -Exactly
        }

        It -Name 'Should continue processing other computers if one fails' -Test {
            Mock -CommandName 'Get-WinEvent' -ModuleName 'PSWinOps' -MockWith {
                param($FilterHashtable, $ComputerName, $ErrorAction, $Credential)
                if ($ComputerName -eq 'SRV-FAIL') {
                    throw [System.Exception]::new('Connection failed')
                }
                return @($script:mockEventEntry)
            }

            $result = Get-RdpSessionHistory -ComputerName 'SRV-FAIL', 'SRV-OK' -ErrorAction SilentlyContinue

            $result.Count | Should -Be 1
            $result.ComputerName | Should -Be 'SRV-OK'
        }

        It -Name 'Should write warning if event XML parsing fails' -Test {
            $badEventEntry = [PSCustomObject]@{
                TimeCreated = [datetime]'2026-03-11'
                Id          = 21
            } | Add-Member -MemberType ScriptMethod -Name 'ToXml' -Value {
                return 'NOT_VALID_XML'
            } -PassThru

            Mock -CommandName 'Get-WinEvent' -ModuleName 'PSWinOps' -MockWith { return @($badEventEntry) }

            $result = Get-RdpSessionHistory -ComputerName 'SRV01' -WarningAction SilentlyContinue

            $result | Should -BeNullOrEmpty
        }
    }

    Context -Name 'Credential handling' -Fixture {

        It -Name 'Should pass Credential parameter to Get-WinEvent when provided' -Test {
            $secureString = New-Object System.Security.SecureString
            $testCred = [PSCredential]::new('testuser', $secureString)

            Mock -CommandName 'Get-WinEvent' -ModuleName 'PSWinOps' -MockWith { return @() }

            Get-RdpSessionHistory -ComputerName 'SRV01' -Credential $testCred

            Should -Invoke -CommandName 'Get-WinEvent' -ModuleName 'PSWinOps' -Times 1 -Exactly -ParameterFilter {
                $Credential -eq $testCred
            }
        }

        It -Name 'Should not pass Credential when not specified' -Test {
            Mock -CommandName 'Get-WinEvent' -ModuleName 'PSWinOps' -MockWith { return @() }

            Get-RdpSessionHistory -ComputerName 'SRV01'

            Should -Invoke -CommandName 'Get-WinEvent' -ModuleName 'PSWinOps' -Times 1 -Exactly -ParameterFilter {
                $null -eq $Credential
            }
        }
    }

    Context -Name 'Event ID action mapping' -Fixture {

        It -Name 'Should map Event ID 23 to Logoff' -Test {
            $logoffEvent = [PSCustomObject]@{
                TimeCreated = [datetime]'2026-03-11'
                Id          = 23
            } | Add-Member -MemberType ScriptMethod -Name 'ToXml' -Value {
                return $script:mockEventXml
            } -PassThru

            Mock -CommandName 'Get-WinEvent' -ModuleName 'PSWinOps' -MockWith { return @($logoffEvent) }

            $result = Get-RdpSessionHistory -ComputerName 'SRV01'

            $result.Action | Should -Be 'Logoff'
        }

        It -Name 'Should map Event ID 24 to Disconnected' -Test {
            $disconnectEvent = [PSCustomObject]@{
                TimeCreated = [datetime]'2026-03-11'
                Id          = 24
            } | Add-Member -MemberType ScriptMethod -Name 'ToXml' -Value {
                return $script:mockEventXml
            } -PassThru

            Mock -CommandName 'Get-WinEvent' -ModuleName 'PSWinOps' -MockWith { return @($disconnectEvent) }

            $result = Get-RdpSessionHistory -ComputerName 'SRV01'

            $result.Action | Should -Be 'Disconnected'
        }

        It -Name 'Should map Event ID 25 to Reconnection' -Test {
            $reconnectEvent = [PSCustomObject]@{
                TimeCreated = [datetime]'2026-03-11'
                Id          = 25
            } | Add-Member -MemberType ScriptMethod -Name 'ToXml' -Value {
                return $script:mockEventXml
            } -PassThru

            Mock -CommandName 'Get-WinEvent' -ModuleName 'PSWinOps' -MockWith { return @($reconnectEvent) }

            $result = Get-RdpSessionHistory -ComputerName 'SRV01'

            $result.Action | Should -Be 'Reconnection'
        }
    }
}