diff --git a/examples/sessiondata.ps1 b/examples/sessiondata.ps1 new file mode 100644 index 000000000..323c5cd33 --- /dev/null +++ b/examples/sessiondata.ps1 @@ -0,0 +1,43 @@ +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +Start-PodeServer -ScriptBlock { + Add-PodeEndpoint -Address localhost -Port $Port -Protocol Http + Add-PodeRoute -Method Get -Path '/close' -ScriptBlock { + Close-PodeServer + } + + Enable-PodeSessionMiddleware -Secret 'schwifty' -Duration 5 -Extend -UseHeaders + + New-PodeAuthScheme -Basic | Add-PodeAuth -Name 'Auth' -ScriptBlock { + param($username, $password) + + if (($username -eq 'morty') -and ($password -eq 'pickle')) { + return @{ User = @{ ID = 'M0R7Y302' } } + } + + return @{ Message = 'Invalid details supplied' } + } + + Add-PodeRoute -Method Post -Path '/auth/basic' -Authentication Auth -ScriptBlock { + $WebEvent.Session.Data.Views++ + + Write-PodeJsonResponse -Value @{ + Result = 'OK' + Username = $WebEvent.Auth.User.ID + Views = $WebEvent.Session.Data.Views + } + } +} \ No newline at end of file diff --git a/src/Private/FileWatchers.ps1 b/src/Private/FileWatchers.ps1 index e17bc9ed7..480a19dbf 100644 --- a/src/Private/FileWatchers.ps1 +++ b/src/Private/FileWatchers.ps1 @@ -13,7 +13,7 @@ function New-PodeFileWatcher { param() $watcher = [PodeWatcher]::new($PodeContext.Tokens.Cancellation.Token) $watcher.ErrorLoggingEnabled = (Test-PodeErrorLoggingEnabled) - $watcher.ErrorLoggingLevels = @(Get-PodeErrorLoggingLevel) + $watcher.ErrorLoggingLevels = If ( $listener.ErrorLoggingEnabled) { @(Get-PodeErrorLoggingLevel) } else { @() } return $watcher } diff --git a/src/Private/Logging.ps1 b/src/Private/Logging.ps1 index cbb73ec08..482c9f5a2 100644 --- a/src/Private/Logging.ps1 +++ b/src/Private/Logging.ps1 @@ -137,10 +137,10 @@ function Get-PodeLoggingFileMethod { $outString | Out-File -FilePath $path -Encoding $Options.Encoding -Append -Force # Remove log files beyond the MaxDays retention period, ensuring this runs once a day - if (($Options.MaxDays -gt 0) -and ($Options.NextClearDown -lt [DateTime]::Now.Date)) { + if (($Options.MaxDays -gt 0) -and ($Options.NextClearDown -le [DateTime]::Now.Date)) { $date = [DateTime]::Now.Date.AddDays(-$Options.MaxDays) - $null = Get-ChildItem -Path $options.Path -Filter '*.log' -Force | + $null = Get-ChildItem -Path $options.Path -Filter "$($options.Name)_*.log" -Force | Where-Object { $_.CreationTime -lt $date } | Remove-Item -Force diff --git a/src/Private/PodeServer.ps1 b/src/Private/PodeServer.ps1 index bb3356d8a..8bff9c498 100644 --- a/src/Private/PodeServer.ps1 +++ b/src/Private/PodeServer.ps1 @@ -88,7 +88,7 @@ function Start-PodeWebServer { # Create the listener $listener = & $("New-Pode$($PodeContext.Server.ListenerType)Listener") -CancellationToken $PodeContext.Tokens.Cancellation.Token $listener.ErrorLoggingEnabled = (Test-PodeErrorLoggingEnabled) - $listener.ErrorLoggingLevels = @(Get-PodeErrorLoggingLevel) + $listener.ErrorLoggingLevels = If ( $listener.ErrorLoggingEnabled) { @(Get-PodeErrorLoggingLevel) } else { @() } $listener.RequestTimeout = $PodeContext.Server.Request.Timeout $listener.RequestBodySize = $PodeContext.Server.Request.BodySize $listener.ShowServerDetails = [bool]$PodeContext.Server.Security.ServerDetails diff --git a/src/Private/SmtpServer.ps1 b/src/Private/SmtpServer.ps1 index e6ce3a762..a8becafab 100644 --- a/src/Private/SmtpServer.ps1 +++ b/src/Private/SmtpServer.ps1 @@ -67,7 +67,7 @@ function Start-PodeSmtpServer { # create the listener $listener = [PodeListener]::new($PodeContext.Tokens.Cancellation.Token) $listener.ErrorLoggingEnabled = (Test-PodeErrorLoggingEnabled) - $listener.ErrorLoggingLevels = @(Get-PodeErrorLoggingLevel) + $listener.ErrorLoggingLevels = If ( $listener.ErrorLoggingEnabled) { @(Get-PodeErrorLoggingLevel) } else { @() } $listener.RequestTimeout = $PodeContext.Server.Request.Timeout $listener.RequestBodySize = $PodeContext.Server.Request.BodySize diff --git a/src/Private/TcpServer.ps1 b/src/Private/TcpServer.ps1 index 12b829d1b..a5c63dd8c 100644 --- a/src/Private/TcpServer.ps1 +++ b/src/Private/TcpServer.ps1 @@ -61,7 +61,7 @@ function Start-PodeTcpServer { # create the listener $listener = [PodeListener]::new($PodeContext.Tokens.Cancellation.Token) $listener.ErrorLoggingEnabled = (Test-PodeErrorLoggingEnabled) - $listener.ErrorLoggingLevels = @(Get-PodeErrorLoggingLevel) + $listener.ErrorLoggingLevels = If ( $listener.ErrorLoggingEnabled) { @(Get-PodeErrorLoggingLevel) } else { @() } $listener.RequestTimeout = $PodeContext.Server.Request.Timeout $listener.RequestBodySize = $PodeContext.Server.Request.BodySize diff --git a/src/Private/WebSockets.ps1 b/src/Private/WebSockets.ps1 index cb7c92928..fb1377ce6 100644 --- a/src/Private/WebSockets.ps1 +++ b/src/Private/WebSockets.ps1 @@ -22,7 +22,7 @@ function New-PodeWebSocketReceiver { try { $receiver = [PodeReceiver]::new($PodeContext.Tokens.Cancellation.Token) $receiver.ErrorLoggingEnabled = (Test-PodeErrorLoggingEnabled) - $receiver.ErrorLoggingLevels = @(Get-PodeErrorLoggingLevel) + $receiver.ErrorLoggingLevels = If ( $listener.ErrorLoggingEnabled) { @(Get-PodeErrorLoggingLevel) } else { @() } $PodeContext.Server.WebSockets.Receiver = $receiver $PodeContext.Receivers += $receiver } diff --git a/src/Public/Logging.ps1 b/src/Public/Logging.ps1 index bb07b32bb..98741d1c0 100644 --- a/src/Public/Logging.ps1 +++ b/src/Public/Logging.ps1 @@ -746,7 +746,8 @@ function Remove-PodeLogger { # Finally, remove the logging type from the Types collection $null = $PodeContext.Server.Logging.Type.Remove($Name) - }else{ + } + else { throw $PodeLocale.loggerDoesNotExistExceptionMessage } } @@ -845,9 +846,11 @@ function Write-PodeErrorLog { Process { $name = [Pode.PodeLogger]::ErrorLogName - Write-PodeLog @PSBoundParameters -name $name -SuppressErrorLog - if ($PodeContext.Server.Logging.Type[$name].DuplicateToDefaultLog -and ! $SuppressDefaultLog) { - Write-PodeLog @PSBoundParameters -name ([Pode.PodeLogger]::DefaultLogName) -SuppressErrorLog + if (Test-PodeLoggerEnabled -Name $name) { + Write-PodeLog @PSBoundParameters -name $name -SuppressErrorLog + if ($PodeContext.Server.Logging.Type[$name].DuplicateToDefaultLog -and ! $SuppressDefaultLog) { + Write-PodeLog @PSBoundParameters -name ([Pode.PodeLogger]::DefaultLogName) -SuppressErrorLog + } } } } diff --git a/tests/integration/Sessions.Tests.ps1 b/tests/integration/Sessions.Tests.ps1 index b4d42f191..2e5aa69b9 100644 --- a/tests/integration/Sessions.Tests.ps1 +++ b/tests/integration/Sessions.Tests.ps1 @@ -5,13 +5,16 @@ param() Describe 'Session Requests' { BeforeAll { + $helperPath = (Split-Path -Parent -Path $PSCommandPath) -ireplace 'integration', 'shared' + . "$helperPath/TestHelper.ps1" + $Port = 8080 $Endpoint = "http://127.0.0.1:$($Port)" Start-Job -Name 'Pode' -ErrorAction Stop -ScriptBlock { Import-Module -Name "$($using:PSScriptRoot)\..\..\src\Pode.psm1" - Start-PodeServer -Quiet -ScriptBlock { + Start-PodeServer -Daemon -ScriptBlock { Add-PodeEndpoint -Address localhost -Port $using:Port -Protocol Http Add-PodeRoute -Method Get -Path '/close' -ScriptBlock { Close-PodeServer @@ -41,7 +44,7 @@ Describe 'Session Requests' { } } - Start-Sleep -Seconds 10 + Wait-ForWebServer -Port $Port } AfterAll { diff --git a/tests/shared/TestHelper.ps1 b/tests/shared/TestHelper.ps1 index 506acf841..f8168b34d 100644 --- a/tests/shared/TestHelper.ps1 +++ b/tests/shared/TestHelper.ps1 @@ -1,3 +1,5 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] +param() <# .SYNOPSIS Ensures the Pode assembly is loaded into the current session. @@ -49,7 +51,35 @@ function Import-PodeAssembly { } +<# +.SYNOPSIS + Compares two strings while normalizing line endings. + +.DESCRIPTION + This function trims both input strings and replaces all variations of line endings (`CRLF`, `LF`, `CR`) with a normalized `LF` (`\n`). + It then compares the normalized strings for equality. + +.PARAMETER InputString1 + The first string to compare. + +.PARAMETER InputString2 + The second string to compare. +.OUTPUTS + [bool] + Returns `$true` if both strings are equal after normalization; otherwise, returns `$false`. + +.EXAMPLE + Compare-StringRnLn -InputString1 "Hello`r`nWorld" -InputString2 "Hello`nWorld" + # Returns: $true + +.EXAMPLE + Compare-StringRnLn -InputString1 "Line1`r`nLine2" -InputString2 "Line1`rLine2" + # Returns: $true + +.NOTES + This function ensures that strings with different line-ending formats are treated as equal if their content is otherwise identical. +#> function Compare-StringRnLn { param ( [string]$InputString1, @@ -58,7 +88,34 @@ function Compare-StringRnLn { return ($InputString1.Trim() -replace "`r`n|`n|`r", "`n") -eq ($InputString2.Trim() -replace "`r`n|`n|`r", "`n") } +<# +.SYNOPSIS + Converts a PSCustomObject into an ordered hashtable. + +.DESCRIPTION + This function recursively converts a PSCustomObject, including nested objects and collections, into an ordered hashtable. + It ensures that all properties are retained while maintaining their original structure. + +.PARAMETER InputObject + The PSCustomObject to be converted into an ordered hashtable. + +.OUTPUTS + [System.Collections.Specialized.OrderedDictionary] + Returns an ordered hashtable representation of the input PSCustomObject. +.EXAMPLE + $object = [PSCustomObject]@{ Name = "Pode"; Version = "2.0"; Config = [PSCustomObject]@{ Debug = $true } } + Convert-PsCustomObjectToOrderedHashtable -InputObject $object + # Returns: An ordered hashtable representation of $object. + +.EXAMPLE + $object = [PSCustomObject]@{ Users = @([PSCustomObject]@{ Name = "Alice" }, [PSCustomObject]@{ Name = "Bob" }) } + Convert-PsCustomObjectToOrderedHashtable -InputObject $object + # Returns: An ordered hashtable where 'Users' is an array of ordered hashtables. + +.NOTES + This function preserves key order and supports recursive conversion of nested objects and collections. +#> function Convert-PsCustomObjectToOrderedHashtable { [CmdletBinding()] param ( @@ -113,6 +170,37 @@ function Convert-PsCustomObjectToOrderedHashtable { } } +<# +.SYNOPSIS + Compares two hashtables to determine if they are equal. + +.DESCRIPTION + This function recursively compares two hashtables, checking whether they contain the same keys and values. + It also handles nested hashtables and arrays, ensuring deep comparison of all elements. + +.PARAMETER Hashtable1 + The first hashtable to compare. + +.PARAMETER Hashtable2 + The second hashtable to compare. + +.OUTPUTS + [bool] + Returns `$true` if both hashtables are equal, otherwise returns `$false`. + +.EXAMPLE + $hash1 = @{ Name = "Pode"; Version = "2.0"; Config = @{ Debug = $true } } + $hash2 = @{ Name = "Pode"; Version = "2.0"; Config = @{ Debug = $true } } + Compare-Hashtable -Hashtable1 $hash1 -Hashtable2 $hash2 + # Returns: $true + +.EXAMPLE + $hash1 = @{ Name = "Pode"; Version = "2.0" } + $hash2 = @{ Name = "Pode"; Version = "2.1" } + Compare-Hashtable -Hashtable1 $hash1 -Hashtable2 $hash2 + # Returns: $false + +#> function Compare-Hashtable { param ( [object]$Hashtable1, @@ -251,6 +339,7 @@ function Wait-ForWebServer { try { # Send a request but ignore status codes (any response means the server is online) $null = Invoke-WebRequest -Uri $Uri -UseBasicParsing -TimeoutSec 3 + Write-Host "Webserver is online at $Uri" return $true } catch { @@ -258,7 +347,7 @@ function Wait-ForWebServer { return $true } else { - Write-Debug "Waiting for webserver to come online at $Uri... (Attempt $($RetryCount+1)/$MaxRetries)" + Write-Host "Waiting for webserver to come online at $Uri... (Attempt $($RetryCount+1)/$MaxRetries)" } }