Tests/Private/Logging.Tests.ps1

BeforeDiscovery {
    Import-Module (Resolve-Path "$PSScriptRoot\..\..\Posh-Certutil.psd1") -Force
}

BeforeAll {
    Import-Module (Resolve-Path "$PSScriptRoot\..\..\Posh-Certutil.psd1") -Force
}

AfterAll {
    Remove-Module Posh-Certutil -ErrorAction SilentlyContinue
}

InModuleScope Posh-Certutil {

    Describe 'Get-LoggingConfig' -Tag Unit {
        BeforeEach {
            $script:LoggingConfigWarned = $false
        }

        It 'Defaults Enabled to $false when Logging key is absent entirely' {
            $config = [PSCustomObject]@{ profiles = [PSCustomObject]@{} }
            $result = Get-LoggingConfig -Config $config
            $result.Enabled | Should -Be $false
        }

        It 'Defaults MinimumLevel, Mode, and nested File/EventLog values' {
            $config = [PSCustomObject]@{ profiles = [PSCustomObject]@{} }
            $result = Get-LoggingConfig -Config $config
            $result.MinimumLevel              | Should -Be 'Information'
            $result.Mode                      | Should -Be 'File'
            $result.File.FileName             | Should -Be 'Posh-Certutil-{yyyyMMdd}.log'
            $result.EventLog.LogName          | Should -Be 'Application'
            $result.EventLog.Source           | Should -Be 'Posh-Certutil'
            $result.EventLog.EventIdInformation | Should -Be 1000
            $result.EventLog.EventIdWarning     | Should -Be 2000
            $result.EventLog.EventIdError       | Should -Be 3000
        }

        It 'Returns the configured values when the Logging section is fully populated' {
            $config = [PSCustomObject]@{
                Logging = [PSCustomObject]@{
                    Enabled      = $true
                    MinimumLevel = 'Debug'
                    Mode         = 'EventLog'
                    File         = [PSCustomObject]@{ Path = 'C:\Logs'; FileName = 'app-{yyyyMMdd}.log' }
                    EventLog     = [PSCustomObject]@{
                        LogName = 'CustomLog'; Source = 'MySource'
                        EventIdInformation = 1; EventIdWarning = 2; EventIdError = 3
                    }
                }
            }
            $result = Get-LoggingConfig -Config $config
            $result.Enabled              | Should -Be $true
            $result.MinimumLevel         | Should -Be 'Debug'
            $result.Mode                 | Should -Be 'EventLog'
            $result.File.Path            | Should -Be 'C:\Logs'
            $result.EventLog.Source      | Should -Be 'MySource'
            $result.EventLog.EventIdError | Should -Be 3
        }

        It 'Falls back to Information and warns when MinimumLevel is not a known level' {
            $config = [PSCustomObject]@{ Logging = [PSCustomObject]@{ MinimumLevel = 'Verbose' } }
            $warnings = $null
            $result = Get-LoggingConfig -Config $config -WarningVariable warnings -WarningAction SilentlyContinue
            $result.MinimumLevel | Should -Be 'Information'
            $warnings | Should -Not -BeNullOrEmpty
        }

        It 'Falls back to File and warns when Mode is not File or EventLog' {
            $config = [PSCustomObject]@{ Logging = [PSCustomObject]@{ Mode = 'Syslog' } }
            $warnings = $null
            $result = Get-LoggingConfig -Config $config -WarningVariable warnings -WarningAction SilentlyContinue
            $result.Mode | Should -Be 'File'
            $warnings | Should -Not -BeNullOrEmpty
        }
    }

    Describe 'Resolve-LogFilePath' -Tag Unit {
        BeforeEach {
            $script:TestLogDir = Join-Path $env:TEMP "PoshCertutilLogTest-$([guid]::NewGuid())"
        }

        AfterEach {
            Remove-Item -Path $script:TestLogDir -Recurse -Force -ErrorAction SilentlyContinue
        }

        It 'Expands environment variables in Path' {
            $file = [PSCustomObject]@{ Path = '%TEMP%\PoshCertutilLogTest-EnvExpand'; FileName = 'x.log' }
            $result = Resolve-LogFilePath -File $file
            $result | Should -Not -BeLike '*%TEMP%*'
            $result | Should -BeLike "*$env:TEMP*"
            Remove-Item -Path (Split-Path $result -Parent) -Recurse -Force -ErrorAction SilentlyContinue
        }

        It 'Expands a {DateFormat} token in FileName using the current date' {
            $file = [PSCustomObject]@{ Path = $script:TestLogDir; FileName = 'app-{yyyyMMdd}.log' }
            $result = Resolve-LogFilePath -File $file
            $expected = "app-$((Get-Date).ToString('yyyyMMdd')).log"
            (Split-Path $result -Leaf) | Should -Be $expected
        }

        It 'Creates the target directory when it does not exist' {
            Test-Path $script:TestLogDir | Should -Be $false
            $file = [PSCustomObject]@{ Path = $script:TestLogDir; FileName = 'x.log' }
            Resolve-LogFilePath -File $file | Out-Null
            Test-Path $script:TestLogDir | Should -Be $true
        }
    }

    Describe 'Write-FileLogEntry' -Tag Unit {
        BeforeEach {
            $script:TestLogDir = Join-Path $env:TEMP "PoshCertutilLogTest-$([guid]::NewGuid())"
            $script:LoggingSinkFailed = $false
        }

        AfterEach {
            Remove-Item -Path $script:TestLogDir -Recurse -Force -ErrorAction SilentlyContinue
        }

        It 'Appends a formatted line containing level, user, cmdlet, and message' {
            $file  = [PSCustomObject]@{ Path = $script:TestLogDir; FileName = 'x.log' }
            $entry = [PSCustomObject]@{
                Timestamp = Get-Date; Level = 'Debug'; UserName = 'DOMAIN\alice'
                CmdletName = 'Test-Cmdlet'; ProfileName = 'prod'; CAServer = 'ca01'; Message = 'hello'
            }
            Write-FileLogEntry -File $file -Entry $entry
            $line = Get-Content -Path (Join-Path $script:TestLogDir 'x.log')
            $line | Should -BeLike '*[DEBUG]*'
            $line | Should -BeLike '*DOMAIN\alice*'
            $line | Should -BeLike '*Test-Cmdlet*'
            $line | Should -BeLike '*Profile=prod*'
            $line | Should -BeLike '*CAServer=ca01*'
            $line | Should -BeLike '*hello*'
        }

        It 'Does not throw and warns once when the destination cannot be written' {
            Mock Resolve-LogFilePath { throw 'access denied' }
            $file  = [PSCustomObject]@{ Path = $script:TestLogDir; FileName = 'x.log' }
            $entry = [PSCustomObject]@{ Timestamp = Get-Date; Level = 'Error'; UserName = 'u'; CmdletName = 'c'; Message = 'm' }

            $warnings = $null
            Write-FileLogEntry -File $file -Entry $entry -WarningVariable warnings -WarningAction SilentlyContinue
            $warnings | Should -Not -BeNullOrEmpty
            $script:LoggingSinkFailed | Should -Be $true
        }
    }

    Describe 'Write-EventLogEntry' -Tag Unit {
        BeforeEach {
            $script:LoggingSinkFailed = $false
        }

        It 'Creates the event source when it does not already exist' {
            Mock Test-EventSourceExists { $false }
            Mock New-EventLog {}
            Mock Write-EventLog {}

            $eventLog = [PSCustomObject]@{ LogName = 'Application'; Source = 'Posh-Certutil'; EventIdInformation = 1000; EventIdWarning = 2000; EventIdError = 3000 }
            $entry    = [PSCustomObject]@{ Timestamp = Get-Date; Level = 'Information'; UserName = 'u'; CmdletName = 'c'; Message = 'm' }

            Write-EventLogEntry -EventLog $eventLog -Entry $entry
            Should -Invoke New-EventLog -Times 1 -ParameterFilter { $LogName -eq 'Application' -and $Source -eq 'Posh-Certutil' }
        }

        It 'Skips source creation when the source already exists' {
            Mock Test-EventSourceExists { $true }
            Mock New-EventLog {}
            Mock Write-EventLog {}

            $eventLog = [PSCustomObject]@{ LogName = 'Application'; Source = 'Posh-Certutil'; EventIdInformation = 1000; EventIdWarning = 2000; EventIdError = 3000 }
            $entry    = [PSCustomObject]@{ Timestamp = Get-Date; Level = 'Information'; UserName = 'u'; CmdletName = 'c'; Message = 'm' }

            Write-EventLogEntry -EventLog $eventLog -Entry $entry
            Should -Invoke New-EventLog -Times 0
        }

        It 'Writes with EventIdWarning and EntryType Warning for Level Warning' {
            Mock Test-EventSourceExists { $true }
            Mock Write-EventLog {}

            $eventLog = [PSCustomObject]@{ LogName = 'Application'; Source = 'Posh-Certutil'; EventIdInformation = 1000; EventIdWarning = 2000; EventIdError = 3000 }
            $entry    = [PSCustomObject]@{ Timestamp = Get-Date; Level = 'Warning'; UserName = 'u'; CmdletName = 'c'; Message = 'm' }

            Write-EventLogEntry -EventLog $eventLog -Entry $entry
            Should -Invoke Write-EventLog -Times 1 -ParameterFilter {
                $EventId -eq 2000 -and $EntryType -eq [System.Diagnostics.EventLogEntryType]::Warning
            }
        }

        It 'Writes with EventIdError and EntryType Error for Level Error' {
            Mock Test-EventSourceExists { $true }
            Mock Write-EventLog {}

            $eventLog = [PSCustomObject]@{ LogName = 'Application'; Source = 'Posh-Certutil'; EventIdInformation = 1000; EventIdWarning = 2000; EventIdError = 3000 }
            $entry    = [PSCustomObject]@{ Timestamp = Get-Date; Level = 'Error'; UserName = 'u'; CmdletName = 'c'; Message = 'm' }

            Write-EventLogEntry -EventLog $eventLog -Entry $entry
            Should -Invoke Write-EventLog -Times 1 -ParameterFilter {
                $EventId -eq 3000 -and $EntryType -eq [System.Diagnostics.EventLogEntryType]::Error
            }
        }

        It 'Writes Debug-level entries as EventIdInformation/Information with a [DEBUG] prefix' {
            Mock Test-EventSourceExists { $true }
            Mock Write-EventLog {}

            $eventLog = [PSCustomObject]@{ LogName = 'Application'; Source = 'Posh-Certutil'; EventIdInformation = 1000; EventIdWarning = 2000; EventIdError = 3000 }
            $entry    = [PSCustomObject]@{ Timestamp = Get-Date; Level = 'Debug'; UserName = 'u'; CmdletName = 'c'; Message = 'm' }

            Write-EventLogEntry -EventLog $eventLog -Entry $entry
            Should -Invoke Write-EventLog -Times 1 -ParameterFilter {
                $EventId -eq 1000 -and $EntryType -eq [System.Diagnostics.EventLogEntryType]::Information -and $Message -like '*[DEBUG]*'
            }
        }

        It 'Does not throw and warns once when writing to the event log fails' {
            Mock Test-EventSourceExists { $true }
            Mock Write-EventLog { throw 'access denied' }

            $eventLog = [PSCustomObject]@{ LogName = 'Application'; Source = 'Posh-Certutil'; EventIdInformation = 1000; EventIdWarning = 2000; EventIdError = 3000 }
            $entry    = [PSCustomObject]@{ Timestamp = Get-Date; Level = 'Information'; UserName = 'u'; CmdletName = 'c'; Message = 'm' }

            $warnings = $null
            Write-EventLogEntry -EventLog $eventLog -Entry $entry -WarningVariable warnings -WarningAction SilentlyContinue
            $warnings | Should -Not -BeNullOrEmpty
            $script:LoggingSinkFailed | Should -Be $true
        }
    }

    Describe 'Write-PWSHCertutilLog' -Tag Unit {
        BeforeEach {
            $script:LoggingSinkFailed = $false
            $script:LoggingConfigWarned = $false
            Mock Write-FileLogEntry {}
            Mock Write-EventLogEntry {}
        }

        It 'No-ops without dispatching when Logging.Enabled is $false' {
            $config = [PSCustomObject]@{ Logging = [PSCustomObject]@{ Enabled = $false } }
            Write-PWSHCertutilLog -Config $config -Level Error -CmdletName 'Test' -Message 'm'
            Should -Invoke Write-FileLogEntry -Times 0
            Should -Invoke Write-EventLogEntry -Times 0
        }

        It 'No-ops when the entry Level is below MinimumLevel' {
            $config = [PSCustomObject]@{ Logging = [PSCustomObject]@{ Enabled = $true; MinimumLevel = 'Warning'; Mode = 'File' } }
            Write-PWSHCertutilLog -Config $config -Level Information -CmdletName 'Test' -Message 'm'
            Should -Invoke Write-FileLogEntry -Times 0
        }

        It 'Dispatches to Write-FileLogEntry when Mode is File and Level meets MinimumLevel' {
            $config = [PSCustomObject]@{ Logging = [PSCustomObject]@{ Enabled = $true; MinimumLevel = 'Debug'; Mode = 'File' } }
            Write-PWSHCertutilLog -Config $config -Level Debug -CmdletName 'Test' -Message 'm'
            Should -Invoke Write-FileLogEntry -Times 1
            Should -Invoke Write-EventLogEntry -Times 0
        }

        It 'Dispatches to Write-EventLogEntry when Mode is EventLog' {
            $config = [PSCustomObject]@{ Logging = [PSCustomObject]@{ Enabled = $true; MinimumLevel = 'Debug'; Mode = 'EventLog' } }
            Write-PWSHCertutilLog -Config $config -Level Debug -CmdletName 'Test' -Message 'm'
            Should -Invoke Write-EventLogEntry -Times 1
            Should -Invoke Write-FileLogEntry -Times 0
        }

        It 'Stamps the current Windows identity on the entry' {
            $config = [PSCustomObject]@{ Logging = [PSCustomObject]@{ Enabled = $true; MinimumLevel = 'Debug'; Mode = 'File' } }
            Write-PWSHCertutilLog -Config $config -Level Debug -CmdletName 'Test' -Message 'm'
            Should -Invoke Write-FileLogEntry -Times 1 -ParameterFilter {
                $Entry.UserName -eq [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
            }
        }

        It 'Redacts Credential/Password/Secret/Token-named bound parameters' {
            $config = [PSCustomObject]@{ Logging = [PSCustomObject]@{ Enabled = $true; MinimumLevel = 'Debug'; Mode = 'File' } }
            $bound  = @{ Profile = 'prod'; Credential = 'should-not-appear'; ApiToken = 'should-not-appear-either' }
            Write-PWSHCertutilLog -Config $config -Level Debug -CmdletName 'Test' -Message 'm' -BoundParameters $bound
            Should -Invoke Write-FileLogEntry -Times 1 -ParameterFilter {
                $Entry.Message -notlike '*should-not-appear*' -and $Entry.Message -like '*<redacted>*' -and $Entry.Message -like '*Profile=prod*'
            }
        }

        It 'Does not throw when Get-LoggingConfig itself throws' {
            Mock Get-LoggingConfig { throw 'bad config' }
            $config = [PSCustomObject]@{}
            { Write-PWSHCertutilLog -Config $config -Level Error -CmdletName 'Test' -Message 'm' -WarningAction SilentlyContinue } |
                Should -Not -Throw
        }
    }
}