Tests/Private/Logging.Tests.ps1
|
BeforeDiscovery { Import-Module (Join-Path $PSScriptRoot '../../Posh-SecretRotation.psd1') -Force } Describe 'Private/Logging' -Tag Unit { InModuleScope 'Posh-SecretRotation' { BeforeEach { $script:LoggingConfigWarned = $false $script:LoggingSinkFailed = $false } Describe 'Get-SecretRotationLoggingConfig' { It 'defaults Enabled to $false when Logging is absent' { $config = [PSCustomObject]@{ version = '1.0' } (Get-SecretRotationLoggingConfig -Config $config).Enabled | Should -Be $false } It 'passes through Enabled/MinimumLevel/Mode when valid' { $config = [PSCustomObject]@{ Logging = [PSCustomObject]@{ Enabled = $true; MinimumLevel = 'Debug'; Mode = 'EventLog' } } $result = Get-SecretRotationLoggingConfig -Config $config $result.Enabled | Should -Be $true $result.MinimumLevel | Should -Be 'Debug' $result.Mode | Should -Be 'EventLog' } It 'defaults every File/EventLog field when absent' { $config = [PSCustomObject]@{ Logging = [PSCustomObject]@{ Enabled = $true } } $result = Get-SecretRotationLoggingConfig -Config $config $result.File.Path | Should -Be '%TEMP%\Posh-SecretRotation' $result.File.FileName | Should -Be 'Posh-SecretRotation-{yyyyMMdd}.log' $result.EventLog.LogName | Should -Be 'Application' $result.EventLog.Source | Should -Be 'Posh-SecretRotation' $result.EventLog.EventIdInformation | Should -Be 1000 $result.EventLog.EventIdWarning | Should -Be 2000 $result.EventLog.EventIdError | Should -Be 3000 } It 'falls back to Information and warns once on an invalid MinimumLevel' { Mock Write-Warning {} $config = [PSCustomObject]@{ Logging = [PSCustomObject]@{ MinimumLevel = 'Bogus' } } (Get-SecretRotationLoggingConfig -Config $config).MinimumLevel | Should -Be 'Information' Get-SecretRotationLoggingConfig -Config $config | Out-Null Should -Invoke Write-Warning -Times 1 -Exactly } It 'falls back to File and warns once on an invalid Mode' { Mock Write-Warning {} $config = [PSCustomObject]@{ Logging = [PSCustomObject]@{ Mode = 'Bogus' } } (Get-SecretRotationLoggingConfig -Config $config).Mode | Should -Be 'File' Get-SecretRotationLoggingConfig -Config $config | Out-Null Should -Invoke Write-Warning -Times 1 -Exactly } } Describe 'Resolve-SecretRotationLogFilePath' { BeforeAll { $script:logDir = Join-Path ([System.IO.Path]::GetTempPath()) "SecretRotationLogTest-$([guid]::NewGuid())" } AfterAll { Remove-Item -Path $script:logDir -Recurse -Force -ErrorAction SilentlyContinue } It 'expands the path, expands the date token, and creates the directory' { $file = [PSCustomObject]@{ Path = $script:logDir; FileName = 'test-{yyyyMMdd}.log' } $expectedName = "test-$((Get-Date).ToString('yyyyMMdd')).log" $result = Resolve-SecretRotationLogFilePath -File $file $result | Should -Be (Join-Path $script:logDir $expectedName) Test-Path -Path $script:logDir | Should -Be $true } } Describe 'Write-SecretRotationFileLogEntry' { BeforeAll { $script:logFile = Join-Path ([System.IO.Path]::GetTempPath()) "SecretRotationFileLogTest-$([guid]::NewGuid()).log" } AfterAll { Remove-Item -Path $script:logFile -Force -ErrorAction SilentlyContinue } It 'appends a formatted line containing level, user, cmdlet, target, and message' { Mock Resolve-SecretRotationLogFilePath { $script:logFile } $entry = [PSCustomObject]@{ Timestamp = Get-Date Level = 'Information' UserName = 'alice' CmdletName = 'Update-SecretRotationAccountPassword' TargetName = 'corp-ad-svcaccount1' Message = 'Password rotated' } Write-SecretRotationFileLogEntry -File ([PSCustomObject]@{ Path = 'unused'; FileName = 'unused' }) -Entry $entry $content = Get-Content -Path $script:logFile -Raw $content | Should -BeLike '*[INFORMATION]*' $content | Should -BeLike '*alice*' $content | Should -BeLike '*Update-SecretRotationAccountPassword*' $content | Should -BeLike '*Target=corp-ad-svcaccount1*' $content | Should -BeLike '*Password rotated*' } } Describe 'Test-SecretRotationEventSourceExists' { It 'returns a boolean, or is skipped when this session cannot search all local event logs' { # [System.Diagnostics.EventLog]::SourceExists() searches every local log and # throws (rather than returning $false) when one of them - typically "Security" - # is inaccessible to a non-elevated session. Confirmed in this dev environment: # a non-elevated call throws even for a source name that doesn't exist anywhere. # That is an environment/elevation limitation, not a defect in this thin wrapper. try { $result = Test-SecretRotationEventSourceExists -Source 'Posh-SecretRotation-Nonexistent-Test-Source' $result | Should -BeOfType [bool] } catch { if ($_.Exception.Message -like '*Inaccessible logs*') { Set-ItResult -Skipped -Because 'SourceExists() requires an elevated session to search all local event logs in this environment' } else { throw } } } } Describe 'Write-SecretRotationEventLogEntry' { It 'warns once and no-ops when Write-EventLog is unavailable on this platform' { Mock Get-Command { $null } -ParameterFilter { $Name -eq 'Write-EventLog' } Mock Write-Warning {} Mock New-EventLog {} $eventLog = [PSCustomObject]@{ LogName = 'Application'; Source = 'Posh-SecretRotation'; EventIdInformation = 1000; EventIdWarning = 2000; EventIdError = 3000 } $entry = [PSCustomObject]@{ Timestamp = Get-Date; Level = 'Information'; UserName = 'alice'; CmdletName = 'Test'; TargetName = $null; Message = 'msg' } { Write-SecretRotationEventLogEntry -EventLog $eventLog -Entry $entry } | Should -Not -Throw Should -Invoke Write-Warning -Times 1 -Exactly Should -Invoke New-EventLog -Times 0 -Exactly } It 'creates the event source when missing, then writes the entry' { Mock Test-SecretRotationEventSourceExists { $false } Mock New-EventLog {} Mock Write-EventLog {} $eventLog = [PSCustomObject]@{ LogName = 'Application'; Source = 'Posh-SecretRotation'; EventIdInformation = 1000; EventIdWarning = 2000; EventIdError = 3000 } $entry = [PSCustomObject]@{ Timestamp = Get-Date; Level = 'Error'; UserName = 'alice'; CmdletName = 'Test'; TargetName = 'corp-ad-svcaccount1'; Message = 'boom' } Write-SecretRotationEventLogEntry -EventLog $eventLog -Entry $entry Should -Invoke New-EventLog -Times 1 -Exactly Should -Invoke Write-EventLog -Times 1 -Exactly -ParameterFilter { $EventId -eq 3000 -and $EntryType -eq [System.Diagnostics.EventLogEntryType]::Error } } It 'never throws when the underlying Event Log call fails' { Mock Test-SecretRotationEventSourceExists { $true } Mock Write-EventLog { throw 'access denied' } Mock Write-Warning {} $eventLog = [PSCustomObject]@{ LogName = 'Application'; Source = 'Posh-SecretRotation'; EventIdInformation = 1000; EventIdWarning = 2000; EventIdError = 3000 } $entry = [PSCustomObject]@{ Timestamp = Get-Date; Level = 'Information'; UserName = 'alice'; CmdletName = 'Test'; TargetName = $null; Message = 'msg' } { Write-SecretRotationEventLogEntry -EventLog $eventLog -Entry $entry } | Should -Not -Throw Should -Invoke Write-Warning -Times 1 -Exactly } } Describe 'Write-SecretRotationLog' { It 'no-ops when logging is disabled' { Mock Get-SecretRotationLoggingConfig { [PSCustomObject]@{ Enabled = $false; MinimumLevel = 'Debug'; Mode = 'File'; File = $null; EventLog = $null } } Mock Write-SecretRotationFileLogEntry {} Mock Write-SecretRotationEventLogEntry {} Write-SecretRotationLog -Config ([PSCustomObject]@{}) -Level Information -Message 'msg' -CmdletName 'Test' Should -Invoke Write-SecretRotationFileLogEntry -Times 0 -Exactly Should -Invoke Write-SecretRotationEventLogEntry -Times 0 -Exactly } It 'filters out entries below MinimumLevel' { Mock Get-SecretRotationLoggingConfig { [PSCustomObject]@{ Enabled = $true; MinimumLevel = 'Warning'; Mode = 'File'; File = [PSCustomObject]@{}; EventLog = [PSCustomObject]@{} } } Mock Write-SecretRotationFileLogEntry {} Write-SecretRotationLog -Config ([PSCustomObject]@{}) -Level Information -Message 'msg' -CmdletName 'Test' Should -Invoke Write-SecretRotationFileLogEntry -Times 0 -Exactly Write-SecretRotationLog -Config ([PSCustomObject]@{}) -Level Error -Message 'msg' -CmdletName 'Test' Should -Invoke Write-SecretRotationFileLogEntry -Times 1 -Exactly } It 'dispatches to the File sink when Mode is File' { Mock Get-SecretRotationLoggingConfig { [PSCustomObject]@{ Enabled = $true; MinimumLevel = 'Debug'; Mode = 'File'; File = [PSCustomObject]@{}; EventLog = [PSCustomObject]@{} } } Mock Write-SecretRotationFileLogEntry {} Mock Write-SecretRotationEventLogEntry {} Write-SecretRotationLog -Config ([PSCustomObject]@{}) -Level Information -Message 'msg' -CmdletName 'Test' Should -Invoke Write-SecretRotationFileLogEntry -Times 1 -Exactly Should -Invoke Write-SecretRotationEventLogEntry -Times 0 -Exactly } It 'dispatches to the EventLog sink when Mode is EventLog' { Mock Get-SecretRotationLoggingConfig { [PSCustomObject]@{ Enabled = $true; MinimumLevel = 'Debug'; Mode = 'EventLog'; File = [PSCustomObject]@{}; EventLog = [PSCustomObject]@{} } } Mock Write-SecretRotationFileLogEntry {} Mock Write-SecretRotationEventLogEntry {} Write-SecretRotationLog -Config ([PSCustomObject]@{}) -Level Information -Message 'msg' -CmdletName 'Test' Should -Invoke Write-SecretRotationEventLogEntry -Times 1 -Exactly Should -Invoke Write-SecretRotationFileLogEntry -Times 0 -Exactly } It 'redacts Password/Secret/Token/Credential/APIKey-named bound parameters but keeps others' { Mock Get-SecretRotationLoggingConfig { [PSCustomObject]@{ Enabled = $true; MinimumLevel = 'Debug'; Mode = 'File'; File = [PSCustomObject]@{}; EventLog = [PSCustomObject]@{} } } $script:capturedEntry = $null Mock Write-SecretRotationFileLogEntry { $script:capturedEntry = $Entry } Write-SecretRotationLog -Config ([PSCustomObject]@{}) -Level Debug -Message 'Cmdlet invoked' -CmdletName 'Test' ` -BoundParameters @{ Password = 'super-secret-value'; Identifier = 'svc1' } $script:capturedEntry.Message | Should -Not -BeLike '*super-secret-value*' $script:capturedEntry.Message | Should -BeLike '*Password=<redacted>*' $script:capturedEntry.Message | Should -BeLike '*Identifier=svc1*' } It 'stamps UserName from [System.Environment]::UserName' { Mock Get-SecretRotationLoggingConfig { [PSCustomObject]@{ Enabled = $true; MinimumLevel = 'Debug'; Mode = 'File'; File = [PSCustomObject]@{}; EventLog = [PSCustomObject]@{} } } $script:capturedEntry = $null Mock Write-SecretRotationFileLogEntry { $script:capturedEntry = $Entry } Write-SecretRotationLog -Config ([PSCustomObject]@{}) -Level Information -Message 'msg' -CmdletName 'Test' $script:capturedEntry.UserName | Should -Be ([System.Environment]::UserName) } It 'never throws even when the logging config itself is broken' { Mock Get-SecretRotationLoggingConfig { throw 'config is corrupt' } Mock Write-Warning {} { Write-SecretRotationLog -Config ([PSCustomObject]@{}) -Level Information -Message 'msg' -CmdletName 'Test' } | Should -Not -Throw Should -Invoke Write-Warning -Times 1 -Exactly } } } } |